Tailscale-native MCP gateway with identity-based access control, audit logging, and session recording
1# Turnscale
2
3[](https://tangled.sh/scottlanoue.com/turnscale)
4
5MCP gateway for Tailscale. Proxies [Model Context Protocol](https://modelcontextprotocol.io) requests to multiple backends, using Tailscale identity for access control. Single Go binary, no credentials to manage.
6
7- Identity from Tailscale `WhoIs` — no API keys or OAuth
8- Policy via tailnet ACL grants (or YAML fallback)
9- Per-tool deny globs (e.g. allow Gitea but deny `delete_*`)
10- Audit log and optional session recording
11- Dashboard with server health, request chart, tool discovery
12- Single binary, no CGO
13
14
15
16## Quick Start
17
18```bash
19go build ./cmd/turnscale
20cp gateway.example.yaml gateway.yaml # edit with your servers + policies
21./turnscale -config gateway.yaml
22```
23
24The gateway joins your tailnet as a node. Point MCP clients at `https://{hostname}.{tailnet}.ts.net/{server}/mcp`.
25
26## Configuration
27
28```yaml
29hostname: "mcp"
30tailnet: "your-tailnet.ts.net"
31state_dir: "~/.local/share/turnscale"
32
33servers:
34 github:
35 url: "http://localhost:8091/mcp"
36 transport: "streamable-http"
37 slack:
38 url: "http://localhost:8092/mcp"
39 transport: "streamable-http"
40
41policies:
42 - name: "admin"
43 match:
44 identity: ["you@github"]
45 allow: ["*"]
46
47 - name: "ai-agents"
48 match:
49 tags: ["tag:ai-agent"]
50 allow: ["github", "slack"]
51 deny_tools: ["mcp__github__delete_*"]
52
53 - name: "default-deny"
54 match:
55 identity: ["*"]
56 deny: ["*"]
57```
58
59Servers can also be added, edited, and removed from the dashboard UI.
60
61## Connecting Clients
62
63### Claude Code
64
65Add to `~/.claude/settings.local.json`:
66
67```json
68{
69 "mcpServers": {
70 "github": {
71 "type": "http",
72 "url": "https://mcp.your-tailnet.ts.net/github/mcp"
73 },
74 "slack": {
75 "type": "http",
76 "url": "https://mcp.your-tailnet.ts.net/slack/mcp"
77 }
78 }
79}
80```
81
82### Any MCP Client (Streamable HTTP)
83
84```
85POST https://mcp.your-tailnet.ts.net/{server}/mcp # JSON-RPC
86GET https://mcp.your-tailnet.ts.net/{server}/mcp # SSE stream
87DELETE https://mcp.your-tailnet.ts.net/{server}/mcp # Session termination
88```
89
90No auth headers — identity comes from Tailscale.
91
92## Tailscale ACL Grants
93
94The recommended way to manage access. Add grants to your tailnet policy — the gateway reads them from `WhoIs().CapMap` at request time:
95
96```json
97{
98 "grants": [
99 {
100 "src": ["autogroup:member"],
101 "dst": ["tag:server"],
102 "app": {
103 "your-tailnet.ts.net/cap/mcp": [{
104 "servers": ["*"],
105 "admin": true
106 }]
107 }
108 },
109 {
110 "src": ["tag:ai-agent"],
111 "dst": ["tag:server"],
112 "app": {
113 "your-tailnet.ts.net/cap/mcp": [{
114 "servers": ["github", "slack"],
115 "denyTools": ["mcp__github__delete_*"],
116 "record": true
117 }]
118 }
119 }
120 ]
121}
122```
123
124| Field | Description |
125|-------|-------------|
126| `servers` | Server names or `["*"]` for all |
127| `denyTools` | Glob patterns for denied tools |
128| `admin` | Access to audit logs, server management, session recordings |
129| `record` | Capture full request/response bodies for this caller |
130
131Grants take priority over YAML policies. Multiple grants per caller are unioned.
132
133## Web UI
134
135All pages live under `/ui/`:
136
137- `/ui/` — dashboard: request chart, server health, policies, recent activity
138- `/ui/audit` — full audit log with filtering by caller/server/tool/status
139- `/ui/session/{id}` — session recording viewer
140
141## Architecture
142
143```
144┌──────────────┐ ┌──────────────────┐ ┌─────────────┐
145│ Claude Code │────>│ Turnscale │────>│ github-mcp │
146│ │ │ (tsnet node) │────>│ slack-mcp │
147├──────────────┤ │ │────>│ jira-mcp │
148│ AI Agents │────>│ - WhoIs identity│────>│ ... │
149│ (tag:agent) │ │ - ACL grants │ └─────────────┘
150└──────────────┘ │ - Audit + Record│
151 │ - Tool discovery│
152 │ - Dashboard │
153 └──────────────────┘
154```
155
156## Development
157
158```bash
159go build ./... # Build
160go test -race ./... # Test (85 tests, race detector)
161go test -cover ./... # Coverage
162go vet ./... # Static analysis
163```
164
165## Dependencies
166
167- [`tailscale.com/tsnet`](https://pkg.go.dev/tailscale.com/tsnet) — embedded Tailscale node
168- [`modernc.org/sqlite`](https://pkg.go.dev/modernc.org/sqlite) — pure-Go SQLite (no CGO)
169- [`gopkg.in/yaml.v3`](https://pkg.go.dev/gopkg.in/yaml.v3) — config parsing
170
171## License
172
173[MIT](LICENSE)