Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,9 @@ TORPROXY_SOCKS_PORT=9050
# TORPROXY_REQUEST_TIMEOUT=3
# TORPROXY_MAX_WORKERS=32
# TORPROXY_MAX_BODY_BYTES=16384

# Model Context Protocol (MCP) server settings (ISSUE-122).
# Built-in zero-dependency MCP server on port 2501 for AI agents (Claude Desktop, Cursor, Antigravity).
# TORPROXY_MCP_ENABLED=1
# TORPROXY_MCP_PORT=2501
# TORPROXY_MCP_LISTEN_ADDRESS=0.0.0.0
47 changes: 47 additions & 0 deletions .github/actions/live-verification/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ runs:
-p 8118:8118 \
-p 9050:9050 \
-p 8080:8080 \
-p 2501:2501 \
"${{ inputs.image_ref }}")
echo "container_id=$CONTAINER_ID" >> "$GITHUB_OUTPUT"

Expand Down Expand Up @@ -213,6 +214,52 @@ runs:
echo "✅ SOCKS5 Proxy OK: Exit-IP is $EXIT_IP (IsTor=true)"
fi

# -------------------------------------------------------------
# 5b. MCP Server & torproxy_fetch live verification
# -------------------------------------------------------------
- name: Verify MCP Server & torproxy_fetch Live Canary
shell: bash
run: |
echo "Testing MCP Server tools and live fetch over Tor..."
RUNNER_IP="${{ steps.host-info.outputs.runner_ip }}"

# 1. MCP Stdio test: ping and tool listing
PING_RESP=$(echo '{"jsonrpc":"2.0","id":1,"method":"ping"}' | docker exec -i torproxy-canary python3 -m torproxy mcp 2>/dev/null)
echo "MCP Ping: $PING_RESP"
echo "$PING_RESP" | jq -e '.result == {}' >/dev/null

# 2. Live fetch test via torproxy_fetch tool over Tor
FETCH_REQ='{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"torproxy_fetch","arguments":{"url":"https://check.torproject.org/api/ip"}}}'
FETCH_RESP=$(echo "$FETCH_REQ" | docker exec -i torproxy-canary python3 -m torproxy mcp 2>/dev/null)
echo "MCP Fetch Response: $FETCH_RESP"
echo "$FETCH_RESP" | jq -e '.result.isError == false' >/dev/null

PAYLOAD=$(echo "$FETCH_RESP" | jq -r '.result.content[0].text')
STATUS_CODE=$(echo "$PAYLOAD" | jq -r '.status')
BODY=$(echo "$PAYLOAD" | jq -r '.body')

if [ "$STATUS_CODE" != "200" ]; then
echo "❌ MCP torproxy_fetch returned status $STATUS_CODE (expected 200)"
exit 1
fi

FETCH_IP=$(echo "$BODY" | jq -r '.IP // empty')
IS_TOR=$(echo "$BODY" | jq -r '.IsTor // empty')

echo "MCP fetched exit IP: $FETCH_IP (IsTor: $IS_TOR)"

if [ "$FETCH_IP" = "$RUNNER_IP" ]; then
echo "❌ CRITICAL: MCP torproxy_fetch leaked host IP ($FETCH_IP == $RUNNER_IP)!"
exit 1
fi

if [ "$IS_TOR" = "false" ]; then
echo "❌ CRITICAL: check.torproject.org reported MCP fetch was NOT through Tor!"
exit 1
fi

echo "✅ MCP Server live verification passed: fetched through Tor, leak-free."

# -------------------------------------------------------------
# 6. Circuit rotation (/reconnect -> SIGNAL NEWNYM)
# -------------------------------------------------------------
Expand Down
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.6.0] — 2026-11-10

### Added
- **mcp**: Integrated Model Context Protocol (MCP) server on port 2501 and Stdio transport (ISSUE-122, #123)
- **mcp**: Zero-config `torproxy_fetch` tool for leak-free HTTP/HTTPS and `.onion` fetches
- **canary**: Add live MCP server and `torproxy_fetch` verification to canary and container tests

## [2.1.1] — 2026-11-07

### Fixed
Expand Down Expand Up @@ -184,7 +191,8 @@ Sprint 1 security & contract correctness release.

---

[Unreleased]: https://github.com/hapheus/torproxy/compare/v2.1.1...HEAD
[Unreleased]: https://github.com/hapheus/torproxy/compare/v2.6.0...HEAD
[2.6.0]: https://github.com/hapheus/torproxy/compare/v2.5.0...v2.6.0
[2.1.1]: https://github.com/hapheus/torproxy/compare/v2.1.0...v2.1.1
[2.1.0]: https://github.com/hapheus/torproxy/compare/v2.0.2...v2.1.0
[2.0.2]: https://github.com/hapheus/torproxy/compare/v2.0.1...v2.0.2
Expand Down
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/hapheus/torproxy/badge)](https://scorecard.dev/viewer/?uri=github.com/hapheus/torproxy)
[![OpenSSF Best Practices](https://img.shields.io/cii/percentage/15828?style=flat-square&logo=linuxfoundation&label=OpenSSF%20Best%20Practices)](https://www.bestpractices.dev/projects/15828)

[Quick Start](#quick-start) · [Transparent Gateway](#transparent-tor-gateway) · [Architecture](#architecture) · [REST API](docs/api.md) · [Configuration](docs/configuration.md) · [Examples](examples/) · [Contributing](CONTRIBUTING.md)
[Quick Start](#quick-start) · [Model Context Protocol (MCP)](#-model-context-protocol-mcp) · [Transparent Gateway](#transparent-tor-gateway) · [Architecture](#architecture) · [REST API](docs/api.md) · [Configuration](docs/configuration.md) · [Examples](examples/) · [Contributing](CONTRIBUTING.md)

</div>

Expand Down Expand Up @@ -113,13 +113,54 @@ services:

---

## 🤖 Model Context Protocol (MCP)

`torproxy` includes a built-in, zero-dependency **Model Context Protocol (MCP)** server on port `2501` (SSE) and via Stdio (`torproxy mcp`), allowing AI agents (Claude Desktop, Cursor, Antigravity) to manage Tor circuits and execute anonymized web requests directly.

### Claude Desktop Integration (`claude_desktop_config.json`)
Connect Claude Desktop to your running `torproxy` container via Stdio without exposing host ports:
```json
{
"mcpServers": {
"torproxy": {
"command": "docker",
"args": ["exec", "-i", "torproxy", "torproxy", "mcp"]
}
}
}
```

### Cursor & Antigravity Integration (`.cursor/mcp.json`)
Connect via HTTP/SSE:
```json
{
"mcpServers": {
"torproxy": {
"url": "http://127.0.0.1:2501/sse"
}
}
}
```

### Key Tools Available to Agents
- `torproxy_fetch`: **Zero-config fetch** — execute HTTP/HTTPS or `.onion` requests strictly routed through Tor (no local curl or host proxy setup needed).
- `torproxy_status`: Retrieve live bootstrap progress, egress IP, and GeoIP metadata.
- `torproxy_get_ip`: Query the current verified Tor exit IP.
- `torproxy_reconnect`: Request a new Tor identity (`SIGNAL NEWNYM`), optionally changing exit country (`country="DE"`).
- `torproxy_health` & `torproxy_diagnostics`: Automated operational health and circuit diagnostics.

See [docs/mcp.md](docs/mcp.md) for full configuration details, authentication options, and tool schemas.

---

## Available Interfaces and Ports

| Interface | Default Port | Internal Service | Description |
| --------- | ------------ | ---------------- | ----------- |
| **HTTP Proxy** | `8118` | Privoxy | HTTP/HTTPS forward proxy routing traffic to Tor. Fails closed (500/503) if Tor is disconnected. |
| **SOCKS5 Proxy** | `9050` | Tor | Direct Tor SOCKS5 listener. Supports remote DNS resolution via `socks5h://`. |
| **REST API** | `8080` | Python REST API | JSON management and health endpoint. Circuit rotation and connection management. |
| **MCP Server (SSE)** | `2501` | Python MCP Server | Model Context Protocol server (SSE) for AI agents (Cursor, Antigravity). |
| **TransPort** *(Gateway Mode)* | `9040` | Tor | Transparent TCP proxy redirect listener when `TORPROXY_GATEWAY_MODE=true`. |
| **DNSPort** *(Gateway Mode)* | `9053` | Tor | Transparent DNS resolver listener when `TORPROXY_GATEWAY_MODE=true`. |

Expand Down
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ services:
TORPROXY_STRICT_NODES: ${TORPROXY_STRICT_NODES:-}
TORPROXY_API_TOKEN: ${TORPROXY_API_TOKEN:-}
TORPROXY_API_ALLOW_ANONYMOUS_HEALTH: ${TORPROXY_API_ALLOW_ANONYMOUS_HEALTH:-}
TORPROXY_MCP_ENABLED: ${TORPROXY_MCP_ENABLED:-true}
TORPROXY_MCP_PORT: ${TORPROXY_MCP_PORT:-2501}
ports:
- "127.0.0.1:${TORPROXY_HOST_PORT:-8118}:${TORPROXY_LISTEN_PORT:-8118}"
- "127.0.0.1:${TORPROXY_API_PORT:-8080}:${TORPROXY_API_PORT:-8080}"
- "127.0.0.1:${TORPROXY_SOCKS_PORT:-9050}:${TORPROXY_SOCKS_PORT:-9050}"
- "127.0.0.1:${TORPROXY_MCP_PORT:-2501}:${TORPROXY_MCP_PORT:-2501}"
restart: unless-stopped
8 changes: 5 additions & 3 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ ARG PYTHON_VERSION=3.14

LABEL org.opencontainers.image.title="torproxy" \
org.opencontainers.image.description="A minimal HTTP proxy routed through Tor" \
org.opencontainers.image.version="2.5.0" \
org.opencontainers.image.version="2.6.0" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.source="https://github.com/hapheus/torproxy" \
org.opencontainers.image.url="https://github.com/hapheus/torproxy"
Expand Down Expand Up @@ -36,7 +36,9 @@ ENV PYTHON_VERSION=${PYTHON_VERSION} \
TORPROXY_LISTEN_ADDRESS=0.0.0.0 \
TORPROXY_LISTEN_PORT=8118 \
TORPROXY_API_PORT=8080 \
TORPROXY_SOCKS_PORT=9050
TORPROXY_SOCKS_PORT=9050 \
TORPROXY_MCP_PORT=2501 \
TORPROXY_MCP_ENABLED=true

# Stage sources, then install into the sysconfig-derived site-packages so the
# layout follows the interpreter actually present in the image (ISSUE-15).
Expand All @@ -61,7 +63,7 @@ COPY --chmod=644 docker/torrc /etc/tor/torrc
# torproxy) so the gateway netfilter rules can scope the WAN bypass to
# the dedicated tor UID. No daemon ever runs as root.

EXPOSE 8118 8080 9050
EXPOSE 8118 8080 9050 2501

HEALTHCHECK --interval=30s --timeout=15s --start-period=180s --retries=3 \
CMD ["/usr/local/bin/healthcheck.sh"]
Expand Down
4 changes: 4 additions & 0 deletions docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This document defines the strict, non-negotiable backward compatibility contract
| **HTTP Proxy** | `8118` | `8118` (`127.0.0.1`) | HTTP (Privoxy) | Unauthenticated | Standard HTTP/HTTPS forward proxy routed via Tor SOCKS5. |
| **SOCKS5 Proxy** | `9050` | `9050` (`127.0.0.1`) | SOCKS5 (Tor) | Unauthenticated | Tor SOCKS5 proxy with remote DNS resolution (`socks5h`). |
| **Control REST API** | `8080` | `8080` (`127.0.0.1`) | HTTP/REST | Unauthenticated | JSON management and monitoring interface. |
| **MCP Server (SSE)** | `2501` | `2501` (`127.0.0.1`) | HTTP/SSE (JSON-RPC) | Inherits API Token | Built-in Model Context Protocol server for AI agents. |
| **Tor ControlPort** | `9051` | Not Published | Tor Control | Cookie Auth | Internal loopback only (`127.0.0.1:9051`), never published to host. |

### Network Guarantees vs. Implementation Details
Expand Down Expand Up @@ -49,6 +50,9 @@ This document defines the strict, non-negotiable backward compatibility contract
| `TORPROXY_API_TOKEN` | *(unset)* | String | Optional: Bearer token / API key securing REST API (8080). Default: unset (unauthenticated). |
| `TORPROXY_API_ALLOW_ANONYMOUS_HEALTH` | `0` | Boolean (`0`, `1`, `true`, `false`) | Optional: Permits unauthenticated `/health` probe access when token auth is enabled. |
| `TORPROXY_SOCKS_ALLOWED_CIDRS` | *(unset)* | Comma-separated CIDRs | Optional: Restricts SOCKS port access by source CIDR (Tor `SocksPolicy`). Default unset = accept all (legacy behavior). |
| `TORPROXY_MCP_ENABLED` | `1` | Boolean (`0`, `1`, `true`, `false`) | Optional additive: Enables built-in MCP server. Default `1` (enabled). |
| `TORPROXY_MCP_PORT` | `2501` | Integer `1`–`65535` | Listening port for the MCP HTTP/SSE server. |
| `TORPROXY_MCP_LISTEN_ADDRESS` | `0.0.0.0` | Valid IP address (IPv4 or IPv6) | Bind address for the MCP server inside the container. |

---

Expand Down
178 changes: 178 additions & 0 deletions docs/mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Model Context Protocol (MCP) Integration

`torproxy` features a built-in, zero-dependency **Model Context Protocol (MCP)** server enabling AI coding assistants (such as Claude Desktop, Cursor, Antigravity, and custom LLM agent pipelines) to inspect Tor status, trigger circuit renewals, and perform anonymized HTTP/HTTPS or `.onion` network requests without manual script execution or external bridge daemons.

---

## 1. Architecture & Dual-Transport Support

The MCP server adheres strictly to the `torproxy` zero-dependency standard (using pure Python standard library: `http.server`, `json`, `threading`, `queue`).

It supports two distinct transport protocols:

1. **Stdio Transport (`torproxy mcp`):**
- Designed for local processes and container execution: `docker exec -i torproxy torproxy mcp`.
- Communicates using newline-delimited JSON-RPC 2.0 frames over `stdin` and `stdout`.
- All internal diagnostics and operational warnings are directed to `stderr` to maintain clean protocol isolation.
- Recommended for **Claude Desktop**.
2. **HTTP/SSE Transport (Port 2501):**
- Runs as a non-blocking daemon thread on port `2501` (`TORPROXY_MCP_PORT=2501`).
- Exposes `GET /sse` for Server-Sent Events stream initialization and `POST /messages?sessionId=...` for JSON-RPC dispatch.
- Recommended for **Cursor**, **Antigravity**, and remote container orchestrators.

---

## 2. Quickstart Configuration Examples

### Claude Desktop (`claude_desktop_config.json`)

Connect Claude Desktop to your running `torproxy` container over Stdio without exposing any network ports:

```json
{
"mcpServers": {
"torproxy": {
"command": "docker",
"args": ["exec", "-i", "torproxy", "torproxy", "mcp"]
}
}
}
```

*When token authentication (`TORPROXY_API_TOKEN`) is enabled:*
```json
{
"mcpServers": {
"torproxy": {
"command": "docker",
"args": ["exec", "-i", "-e", "TORPROXY_API_TOKEN=your-token", "torproxy", "torproxy", "mcp"]
}
}
}
```

---

### Cursor (`.cursor/mcp.json`)

#### Option A: HTTP / SSE (Port 2501)
```json
{
"mcpServers": {
"torproxy": {
"url": "http://127.0.0.1:2501/sse"
}
}
}
```

*With authentication (`TORPROXY_API_TOKEN`):*
```json
{
"mcpServers": {
"torproxy": {
"url": "http://127.0.0.1:2501/sse?token=your-token",
"headers": {
"Authorization": "Bearer your-token"
}
}
}
}
```

#### Option B: Stdio Transport via Docker
```json
{
"mcpServers": {
"torproxy": {
"command": "docker",
"args": ["exec", "-i", "torproxy", "torproxy", "mcp"]
}
}
}
```

---

### Antigravity & Gemini CLI (`settings.json`)

```json
{
"mcpServers": {
"torproxy": {
"command": "docker",
"args": ["exec", "-i", "torproxy", "torproxy", "mcp"]
}
}
}
```

---

### Docker Compose (`docker-compose.yml`)

Publish port `2501` for SSE network clients:

```yaml
services:
torproxy:
image: hapheus/torproxy:latest
container_name: torproxy
restart: unless-stopped
ports:
- "127.0.0.1:8118:8118" # HTTP/HTTPS Proxy (Privoxy)
- "127.0.0.1:9050:9050" # SOCKS5 Proxy (Tor)
- "127.0.0.1:8080:8080" # REST API
- "127.0.0.1:2501:2501" # MCP Server (SSE)
environment:
- TORPROXY_MCP_ENABLED=true
- TORPROXY_MCP_PORT=2501
# Optional:
# - TORPROXY_API_TOKEN=your-token
```

---

## 3. Available Tools

| Tool | Parameters | Description |
|---|---|---|
| `torproxy_fetch` | `url` *(required)*, `method`, `headers`, `body`, `timeout`, `max_bytes` | **Zero-config fetch:** Execute an HTTP/HTTPS or `.onion` request routed strictly through Tor without shell or curl. |
| `torproxy_status` | *(none)* | Live status JSON: bootstrap progress, egress IP, and GeoIP metadata. |
| `torproxy_get_ip` | *(none)* | Current egress IP address verified through Tor. |
| `torproxy_reconnect` | `country` *(optional)*, `strict` *(optional)* | Signal Tor for a new identity/circuit (`SIGNAL NEWNYM`), optionally changing exit nodes. |
| `torproxy_connect` | `country` *(optional)*, `strict` *(optional)* | Enable the Tor network (`SETCONF DisableNetwork=0`). |
| `torproxy_disconnect` | *(none)* | Disable the Tor network (`SETCONF DisableNetwork=1`). |
| `torproxy_health` | *(none)* | Probe status of Tor, Privoxy HTTP, and SOCKS readiness. |
| `torproxy_diagnostics` | *(none)* | Comprehensive telemetry (circuits, cache age, reconnection stats, gateway mode). |

### `torproxy_fetch` Details
AI agents frequently need to fetch web pages or inspect `.onion` hidden services without having terminal or shell access on the host. `torproxy_fetch` executes requests from within the container routed through the local Privoxy proxy, guaranteeing zero clearnet leaks and remote Tor DNS resolution.

**Arguments:**
- `url` (string, required): Destination URL (`http://...`, `https://...`, or `http://...onion`).
- `method` (string, optional, default: `"GET"`): HTTP method (`GET`, `POST`, `PUT`, `DELETE`, `HEAD`).
- `headers` (object, optional): Key-value string dictionary of HTTP headers.
- `body` (string, optional): String payload for POST or PUT requests.
- `timeout` (number, optional, default: `30`): Timeout in seconds.
- `max_bytes` (integer, optional, default: `1048576`): Caps response body length to protect LLM context windows.

---

## 4. Available Resources

| URI | MIME Type | Description |
|---|---|---|
| `torproxy://status` | `application/json` | Real-time JSON status snapshot of the proxy instance. |
| `torproxy://metrics` | `text/plain` | Prometheus telemetry metrics export. |

---

## 5. Security & Authentication

When `TORPROXY_API_TOKEN` is configured:
- **Stdio:** Pass the token via environment variable (`-e TORPROXY_API_TOKEN=...`).
- **SSE:** Authenticate using one of:
- `Authorization: Bearer <token>`
- `X-API-Key: <token>`
- URL query parameter: `/sse?token=<token>` (useful for browser/EventSource clients where headers cannot be modified).
Loading
Loading