Turnscale#
MCP gateway for Tailscale. Proxies Model Context Protocol requests to multiple backends, using Tailscale identity for access control. Single Go binary, no credentials to manage.
- Identity from Tailscale
WhoIs— no API keys or OAuth - Policy via tailnet ACL grants (or YAML fallback)
- Per-tool deny globs (e.g. allow Gitea but deny
delete_*) - Audit log and optional session recording
- Dashboard with server health, request chart, tool discovery
- Single binary, no CGO
Quick Start#
go build ./cmd/turnscale
cp gateway.example.yaml gateway.yaml # edit with your servers + policies
./turnscale -config gateway.yaml
The gateway joins your tailnet as a node. Point MCP clients at https://{hostname}.{tailnet}.ts.net/{server}/mcp.
Configuration#
hostname: "mcp"
tailnet: "your-tailnet.ts.net"
state_dir: "~/.local/share/turnscale"
servers:
github:
url: "http://localhost:8091/mcp"
transport: "streamable-http"
slack:
url: "http://localhost:8092/mcp"
transport: "streamable-http"
policies:
- name: "admin"
match:
identity: ["you@github"]
allow: ["*"]
- name: "ai-agents"
match:
tags: ["tag:ai-agent"]
allow: ["github", "slack"]
deny_tools: ["mcp__github__delete_*"]
- name: "default-deny"
match:
identity: ["*"]
deny: ["*"]
Servers can also be added, edited, and removed from the dashboard UI.
Connecting Clients#
Claude Code#
Add to ~/.claude/settings.local.json:
{
"mcpServers": {
"github": {
"type": "http",
"url": "https://mcp.your-tailnet.ts.net/github/mcp"
},
"slack": {
"type": "http",
"url": "https://mcp.your-tailnet.ts.net/slack/mcp"
}
}
}
Any MCP Client (Streamable HTTP)#
POST https://mcp.your-tailnet.ts.net/{server}/mcp # JSON-RPC
GET https://mcp.your-tailnet.ts.net/{server}/mcp # SSE stream
DELETE https://mcp.your-tailnet.ts.net/{server}/mcp # Session termination
No auth headers — identity comes from Tailscale.
Tailscale ACL Grants#
The recommended way to manage access. Add grants to your tailnet policy — the gateway reads them from WhoIs().CapMap at request time:
{
"grants": [
{
"src": ["autogroup:member"],
"dst": ["tag:server"],
"app": {
"your-tailnet.ts.net/cap/mcp": [{
"servers": ["*"],
"admin": true
}]
}
},
{
"src": ["tag:ai-agent"],
"dst": ["tag:server"],
"app": {
"your-tailnet.ts.net/cap/mcp": [{
"servers": ["github", "slack"],
"denyTools": ["mcp__github__delete_*"],
"record": true
}]
}
}
]
}
| Field | Description |
|---|---|
servers |
Server names or ["*"] for all |
denyTools |
Glob patterns for denied tools |
admin |
Access to audit logs, server management, session recordings |
record |
Capture full request/response bodies for this caller |
Grants take priority over YAML policies. Multiple grants per caller are unioned.
Web UI#
All pages live under /ui/:
/ui/— dashboard: request chart, server health, policies, recent activity/ui/audit— full audit log with filtering by caller/server/tool/status/ui/session/{id}— session recording viewer
Architecture#
┌──────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Claude Code │────>│ Turnscale │────>│ github-mcp │
│ │ │ (tsnet node) │────>│ slack-mcp │
├──────────────┤ │ │────>│ jira-mcp │
│ AI Agents │────>│ - WhoIs identity│────>│ ... │
│ (tag:agent) │ │ - ACL grants │ └─────────────┘
└──────────────┘ │ - Audit + Record│
│ - Tool discovery│
│ - Dashboard │
└──────────────────┘
Development#
go build ./... # Build
go test -race ./... # Test (85 tests, race detector)
go test -cover ./... # Coverage
go vet ./... # Static analysis
Dependencies#
tailscale.com/tsnet— embedded Tailscale nodemodernc.org/sqlite— pure-Go SQLite (no CGO)gopkg.in/yaml.v3— config parsing