Every web request your LLM tool makes leaks your IP. tor-mcp routes them through Tor, with selective per-URL routing so things that block Tor still work.
An MCP server for Claude Code, Claude Desktop, Cursor, Windsurf, and any other MCP-speaking client. Drop in, get anonymous web fetches as a tool.
When an LLM agent uses a tool like fetch or web_search, the request goes straight from your machine. Your IP, your ISP, your geo, your fingerprint. For OSINT lookups, breach checks, threat-intel APIs, or just "I don't want this corner of the web to know who's asking," that's a leak by design.
tor-mcp slots in as an MCP server so the agent's web requests go through your local Tor SOCKS5 proxy with DNS-safe routing (socks5h://, no DNS leaks). It's not trying to anonymize your browser — it's trying to anonymize the web traffic an AI tool makes on your behalf.
Four MCP tools, all with both JSON and Markdown output:
| Tool | What it does |
|---|---|
tor_private_fetch |
Fetch a URL. Routes via Tor or direct based on URL pattern matching, or force one with force_tor / force_direct (mutually exclusive). |
tor_check_anonymity |
Verify Tor is active. Compares the Tor exit IP with your real IP via a multi-provider fallback chain. |
tor_new_identity |
Rotate Tor circuit. Get a new exit IP via the Tor control port. Rate-limited to once per 10s by Tor itself. |
tor_privacy_status |
Full health check: connection, exit IP, control port, routing rules, config. |
tor_private_fetch(url="https://api.ipify.org?format=json") →
{
"status_code": 200,
"url": "https://api.ipify.org?format=json",
"content_type": "application/json",
"content": "{\"ip\":\"185.220.101.20\"}",
"routed_through": "tor",
"routing": {"route": "tor", "reason": "default route: tor"}
}tor_check_anonymity(response_format="markdown") →
## Anonymity Check
- **Status**: Anonymous
- **Tor Exit IP**: 185.220.101.20
- **Direct IP**: 106.213.80.181
- **Tor Verified**: Yes# macOS
brew install tor && brew services start tor
# Ubuntu/Debian
sudo apt install tor && sudo systemctl start tor
# Verify
curl --socks5-hostname 127.0.0.1:9050 https://check.torproject.org/api/ipgit clone https://github.com/rushikeshmore/tor-mcp.git
cd tor-mcp
uv syncClaude Code
claude mcp add tor-mcp -- uv --directory $(pwd) run tor-mcpClaude Desktop
In claude_desktop_config.json:
{
"mcpServers": {
"tor-mcp": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/tor-mcp", "run", "tor-mcp"]
}
}
}Cursor / Windsurf / generic MCP clients
{
"mcpServers": {
"tor-mcp": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/tor-mcp", "run", "tor-mcp"],
"env": {
"TOR_SOCKS_PORT": "9050",
"TOR_DEFAULT_ROUTE": "tor"
}
}
}
}You can also launch the server with python -m tor_mcp if it's installed in your environment.
To use tor_new_identity, edit your torrc (/usr/local/etc/tor/torrc on macOS, /etc/tor/torrc on Linux):
ControlPort 9051
CookieAuthentication 1
Restart Tor afterwards.
URL → route is decided in this order:
| Priority | Rule | Route |
|---|---|---|
| 1 | .onion domains |
Tor (always) |
| 2 | localhost, 127.0.0.1, *.local |
Direct (always) |
| 3 | User TOR_PATTERNS |
Tor |
| 4 | User DIRECT_PATTERNS |
Direct |
| 5 | Default OSINT/security domains (Shodan, HIBP, Censys, urlscan, VirusTotal, OTX, crt.sh) | Tor |
| 6 | Default Tor-blocking domains (GitHub, OpenAI, Anthropic, Google) | Direct |
| 7 | Everything else | TOR_DEFAULT_ROUTE (default: tor) |
URL inputs without a scheme get one added automatically — https:// for remote hosts, http:// for localhost / 127.0.0.1 / *.local so TLS doesn't blow up against a plain dev server.
All via environment variables:
| Variable | Default | Description |
|---|---|---|
TOR_SOCKS_PORT |
9050 |
Tor SOCKS5 proxy port |
TOR_CONTROL_PORT |
9051 |
Tor control port (for circuit rotation) |
TOR_CONTROL_PASSWORD |
Control port password (if set in torrc) | |
TOR_DEFAULT_ROUTE |
tor |
Fallback route: tor or direct |
TOR_TIMEOUT |
30 |
HTTP request timeout in seconds |
TOR_PATTERNS |
Comma-separated hostnames to route through Tor (supports *.example.com) |
|
DIRECT_PATTERNS |
Comma-separated hostnames to route direct |
What tor-mcp does protect against:
- Origin-IP exposure on outbound HTTP(S) requests made by your LLM tool. Traffic exits from a Tor relay, not your home IP.
- DNS leaks. SOCKS5h forces remote DNS resolution at the exit; your resolver never sees the target hostname.
- Trivial geo-fencing and IP-rate-limiting on a per-source basis (use
tor_new_identityto rotate).
What it does not protect against:
- TLS / JA3 / HTTP-header fingerprinting. The traffic is over Tor, but it's still recognizable as Python
httpx. If a target compares fingerprints, they can tell. - Traffic correlation by a global adversary. Standard Tor caveat.
- Account-level deanonymization. If you log in, the target knows who you are regardless of route.
- Browser fingerprinting. Not applicable — there's no browser here, just an HTTP client.
- OS / process / clipboard / filesystem metadata. tor-mcp only handles HTTP traffic.
- Malicious or surveilling exits. Use HTTPS-only targets, treat exit-served content as untrusted.
The Tor control port without authentication assumes a single-user machine. On shared or multi-user hosts, set TOR_CONTROL_PASSWORD and a hashed password in torrc.
This is a privacy tool, not an evasion tool. Don't use it for fraud, abuse, or anything you wouldn't be comfortable explaining. Tor exit operators get a lot of flak from people doing exactly that, and bad actors hurt the network for everyone.
url(string, required): Target URL.method(string, optional):GET,POST,HEAD,PUT,DELETE,PATCH,OPTIONS. DefaultGET.force_tor(bool, optional): Force Tor regardless of routing rules.force_direct(bool, optional): Force direct regardless of routing rules. Setting bothforce_tor=trueandforce_direct=trueis rejected as invalid input.response_format(string, optional):jsonormarkdown. Defaultjson.
response_format(string, optional):jsonormarkdown. Defaultjson.
Compares the Tor exit IP against the direct IP via a multi-provider chain (ipify → httpbin → ifconfig.co) so a single provider outage doesn't break the comparison.
verify(bool, optional): Check whether the IP actually changed. Defaulttrue.response_format(string, optional):jsonormarkdown. Defaultjson.
response_format(string, optional):jsonormarkdown. Defaultjson.
git clone https://github.com/rushikeshmore/tor-mcp.git
cd tor-mcp
uv sync --dev
uv run python -m pytest # run tests (no Tor needed, all mocked)
uv run python -m ruff check src/ tests/
uv run tor-mcp # start MCP server on stdioTests are fully mocked — you don't need Tor running to develop. Live verification (against a running Tor daemon) is documented in CLAUDE.md.
MIT.