使用说明
tsmcp-MCP尾网桥
一个公开私有的Go反向代理 主控程序 您的服务器 尾鳞 网络到 Claude.ai 通过单个公共FQDN,使用由Tailscale的身份提供者(tsidp)提供支持的OAuth身份验证。
运作原理
┌──Tailnet──▶ MCP Server A
Claude.ai ──HTTPS──▶ Caddy ──HTTP──▶ tsmcp ────────┤
│ │ └──Tailnet──▶ MCP Server B
│ │
│ └──Tailnet──▶ tsidp (/token, /register, /introspect)
│ ▲
└──────────HTTPS (Funnel)─────────────────────────┘
(browser: /authorize)
tsmcp通过两种方式连接到tsidp:
- tsmcp→ tsidp (通过tsnet通过Tailnet):OAuth令牌交换(
/token),客户注册(/register),以及象征性的内省(/introspect).MCP规范要求这些端点位于资源服务器的原点,因此tsmcp通过尾网将它们代理到tsidp。 - 浏览器→tsidp (通过HTTPS/漏斗):仅限用户授权(
/authorize).tsmcp发送302重定向——浏览器必须直接到达tsidp,这样Tailscale才能验证用户的身份。
配置中的每个路径都映射到一个单独的Claude.ai自定义连接器。一个部署服务于多个MCP服务器。
先决条件
- 尾秤账户 尾网上至少有一个MCP服务器
- tsidp 用尾巴网跑步 漏斗已启用 --用户的浏览器必须通过公共互联网访问它
/authorize步 - VPS或服务器 带着一个 公共IP,Docker已安装,并已加入您的Tailscale尾网
- 域名 指向主机(例如。,
mcp.example.com) - 卡迪 用于TLS终止的(或另一个反向代理)必须支持SSE刷新
- 尾标ACL 允许网桥节点访问tsidp和MCP服务器(请参见 ACL示例 在......下面
安装指南
1.在tsidp注册客户端
从尾网上的机器上为Claude.ai注册OAuth客户端:
curl -s -X POST https://idp.YOUR-TAILNET.ts.net/register \
-H "Content-Type: application/json" \
-d '{
"redirect_uris": ["https://claude.ai/api/mcp/auth_callback"],
"client_name": "Claude.ai MCP",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "client_secret_basic"
}' | python3 -m json.tool
这必须从尾网节点完成 --tsidp拒绝通过Funnel进行动态客户端注册。保存返回的 client_id 和 client_secret.
为什么
client_secret_basic? tsidp还不支持PKCS(代码交换证明密钥),因此它不能充当公共OAuth客户端。客户端必须使用HTTP基本身份验证使用密钥进行身份验证(client_secret_basic).这意味着Claude.ai需要两者client_id和client_secret当配置为连接器时。
2.生成Tailscale身份验证密钥
去 Tailscale管理控制台 并生成认证密钥。这允许tsmcp的嵌入式tsnet节点加入您的尾网。
- 可重复使用的:可选--tsnet状态持久化到Docker卷,因此节点在首次启动后会自动重新进行身份验证
- 短暂的:否--网桥节点必须保持注册状态,以便ACL规则和tsidp访问继续工作
- 标签例如。,
tag:tsmcp(适用于ACL规则)
3.创建配置文件
server:
listen: "0.0.0.0:8900"
allowed_origins:
- "https://claude.ai"
- "https://claude.com"
tailnet:
hostname: "tsmcp"
state_dir: "/var/lib/tsmcp/tsnet"
authkey_env: "TS_AUTHKEY"
auth:
issuer: "https://idp.your-tailnet.ts.net"
audience: "https://mcp.example.com"
introspection_url: "https://idp.your-tailnet.ts.net/introspect"
resource_metadata_url: "https://mcp.example.com/.well-known/oauth-protected-resource"
endpoints:
- path: "/mcp/my-server"
target: "https://my-mcp-server.your-tailnet.ts.net/mcp"
description: "My MCP Server"
这 target 是尾网上MCP服务器的尾标FQDN(或MagicDNS名称)。tsmcp通过Tailscale拨号——MCP服务器不需要公开访问。
这 auth 部分是可选的——完全省略它以在没有身份验证的情况下运行。
4.Docker编写
services:
tsmcp:
image: meltforce/tsmcp:edge
container_name: tsmcp
restart: unless-stopped
environment:
- TS_AUTHKEY=tskey-auth-...
volumes:
- ./config.yaml:/etc/tsmcp/config.yaml:ro
- tsnet-state:/var/lib/tsmcp/tsnet
networks:
- proxy-net
cap_drop:
- ALL
cap_add:
- NET_RAW
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp:noexec,nosuid,size=64m
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8900/healthz"]
interval: 10s
timeout: 3s
start_period: 30s
retries: 3
volumes:
tsnet-state:
networks:
proxy-net:
external: true
tsnet-state卷在重新启动时保持Tailscale节点状态proxy-net是与Caddy共享的外部Docker网络,因此它可以通过容器名称访问tsmcpTS_AUTHKEY仅在首次启动时需要;tsnet保存状态后,节点会自动重新进行身份验证NET_RAWtsnet的用户空间网络需要这种能力
docker network create proxy-net # if it doesn't exist yet
docker compose up -d
5.配置Caddy
为您的域添加路由。这 flush_interval -1 对于SSE流媒体至关重要:
mcp.example.com {
reverse_proxy tsmcp:8900 {
flush_interval -1
}
log {
output file /var/log/caddy/mcp-access.log {
roll_size 10MiB
roll_keep 3
roll_keep_for 168h
}
format json
}
header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
-Server
}
}
Caddy通过Let's Encrypt自动处理TLS。这 header block添加HSTS,防止MIME类型嗅探,并剥离 Server 头球
6.添加Claude.ai连接器
在 Claude.ai 设置→ 集成→ 添加自定义集成:
- 统一资源定位符:
https://mcp.example.com/mcp/my-server - 客户端ID:the
client_id从步骤1 - 客户端密钥:the
client_secret从步骤1
第ai条将:
- 点击MCP端点,获得401
- 通过以下方式了解tsidp
/.well-known/oauth-protected-resource - 从tsmcp获取授权服务器元数据(由tsidp代理,使用
/token和/register重写为tsmcp的起源) - 将浏览器重定向到tsidp进行授权(通过您的Tailscale身份验证)
- 通过tsmcp将代码交换为访问令牌
/token端点(通过尾网代理到tsidp) - 使用令牌调用MCP端点
7.验证
# Health check
curl https://mcp.example.com/healthz
# → {"status":"ok","tsnet_connected":true}
# Resource metadata
curl https://mcp.example.com/.well-known/oauth-protected-resource
# → {"resource":"https://mcp.example.com","authorization_servers":["https://idp.your-tailnet.ts.net"],...}
# Unauthenticated MCP request (should get 401)
curl -X POST https://mcp.example.com/mcp/my-server \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
-w "\nHTTP %{http_code}\n"
# → HTTP 401
# Container logs
docker logs tsmcp --tail 20
详细说明身份验证流程
tsmcp实现了 MCP授权规范 使用Tailscale的身份提供者(tsidp)作为OAuth授权服务器。
MCP规范要求OAuth端点(/token, /register, /.well-known/oauth-authorization-server)在 资源服务器的来源 --不在授权服务器上。tsmcp通过tsnet将这些端点代理到tailnet上的tsidp来处理这个问题。浏览器直接在tsidp上到达的唯一端点是 /authorize (通过302重定向),因为tsidp需要验证用户的Tailscale身份。
发现
Claude.ai ──POST──▶ https://mcp.example.com/mcp/my-server
◀── 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"
Claude.ai ──GET──▶ https://mcp.example.com/.well-known/oauth-protected-resource
◀── 200
{
"resource": "https://mcp.example.com",
"authorization_servers": ["https://idp.your-tailnet.ts.net"]
}
Claude.ai ──GET──▶ https://mcp.example.com/.well-known/oauth-authorization-server
◀── 200 (proxied from tsidp, with endpoints rewritten)
{
"authorization_endpoint": "https://idp.your-tailnet.ts.net/authorize",
"token_endpoint": "https://mcp.example.com/token",
"registration_endpoint": "https://mcp.example.com/register",
...
}
- Claude.ai在没有令牌的情况下点击MCP端点,得到
401具有资源元数据URL - Claude.ai取回 RFC 9728 受保护的资源元数据,指向tsidp作为授权服务器
- Claude.ai获取 RFC 8414 授权服务器元数据——来自tsidp的tsmcp代理和重写
token_endpoint和registration_endpoint指向自己,保持authorization_endpoint在tsidp
授权
Claude.ai ──GET──▶ https://mcp.example.com/authorize?client_id=...&...
◀── 302 → https://idp.your-tailnet.ts.net/authorize?client_id=...&...
[User's browser follows redirect to tsidp, authenticates via Tailscale identity]
tsidp ──redirect──▶ https://claude.ai/api/mcp/auth_callback?code=...&state=...
Claude.ai ──POST──▶ https://mcp.example.com/token (proxied to tsidp via tsnet)
Authorization: Basic base64(client_id:client_secret)
grant_type=authorization_code&code=...
◀── { "access_token": "<opaque>", "token_type": "Bearer", "expires_in": 300 }
- Claude.ai点击
/authorize在tsmcp上,获得302重定向到tsidp的授权端点 - tsidp通过用户的Tailscale身份对其进行身份验证 --浏览器通过Tailscale漏斗连接到tsidp,tsidp通过其tailnet节点识别用户。这是关键的安全边界:只有尾网上的用户才能授权。
- tsidp使用授权码重定向回Claude.ai
- Claude.ai通过以下方式将代码交换为不透明的访问令牌(5分钟TTL)
POST /token在tsmcp上,它通过尾网代理tsidp。身份验证使用client_secret_basic(带有客户端凭据的HTTP基本身份验证)--tsidp不支持PKCE。
验证的请求
Claude.ai ──POST──▶ https://mcp.example.com/mcp/my-server
Authorization: Bearer <opaque-token>
tsmcp ──POST──▶ https://idp.your-tailnet.ts.net/introspect (via tsnet)
token=<opaque-token>
◀── { "active": true, "sub": "user@github", "uid": "...", ... }
tsmcp ──proxy──▶ https://my-mcp-server.your-tailnet.ts.net/mcp (via tsnet)
◀── MCP response (JSON-RPC / SSE)
- Claude.ai发送带有Bearer令牌的MCP请求
- tsmcp通过调用tsidp来验证令牌 RFC 7662 自省端点 超过尾鳞片 (通过tsnet)——这一点至关重要,因为tsidp解析为一个Tailscale IP,否则无法从Docker的网络访问
- 如果令牌处于活动状态,tsmcp会通过Tailscale将请求代理到上游MCP服务器
- 自检结果被缓存(60秒或直到令牌到期,以较短者为准)
安全模型
- tsidp拒绝通过漏斗进行动态客户端注册(DCR) --客户端必须从尾网节点预先注册
- 授权端点需要Tailscale标识 --用户的浏览器必须通过尾网连接到tsidp。tsidp通过用户的Tailscale节点标识来识别用户,而不是通过登录表单。
- 令牌是不透明的 --tsidp发出不透明的访问令牌(不是JWT),因此只能通过自检来验证它们
- 反思通过tsnet --tsmcp通过尾网拨打tsidp,因此ACL控制哪些节点可以验证令牌
- 陌生人无法完成OAuth流程:他们可以发现tsidp(通过资源元数据),但他们无法注册客户端或授权,因为这些操作需要尾网访问
尾标ACL示例
网桥节点需要同时到达tsidp(用于令牌自检)和MCP服务器(用于代理)。tsidp还需要应用程序授权,以便客户端可以注册,用户可以授权:
// Tag definitions
"tagOwners": {
"tag:tsmcp": ["autogroup:admin"], // MCP bridge
"tag:idp": ["autogroup:admin"], // Tailscale identity provider
"tag:mcp": ["autogroup:admin"], // MCP servers
},
"acls": [
// Allow the bridge to reach MCP servers
{
"src": ["tag:tsmcp"],
"dst": ["tag:mcp"],
"ip": ["tcp:443"],
},
// tsidp — admin access (admin UI + DCR)
{
"src": ["autogroup:admin"],
"dst": ["tag:idp"],
"app": {
"tailscale.com/cap/tsidp": [{
"allow_admin_ui": true,
"allow_dcr": true,
"resources": ["*"],
"scopes": ["openid", "email", "profile"],
"users": ["*"],
}],
},
},
// tsidp — all tailnet users (DCR only, no admin UI)
{
"src": ["*"],
"dst": ["tag:idp"],
"app": {
"tailscale.com/cap/tsidp": [{
"allow_admin_ui": false,
"allow_dcr": true,
"resources": ["*"],
"scopes": ["openid", "email", "profile"],
"users": ["*"],
}],
},
},
]
特性
- 基于路径的路由 --单域、多个MCP服务器
- SSE流媒体 --服务器发送事件的自动刷新
- OAuth身份验证 --通过RFC 7662对tsidp的自检验证不透明令牌
- OAuth端点代理 —
/token,/register,AS元数据通过tsnet代理到tsidp;/authorize重定向到tsidp以获取Tailscale标识 - RFC 9728元数据 —
/.well-known/oauth-protected-resource用于MCP身份验证发现 - 尾秤标识 --身份验证由Tailscale节点标识支持,而不是密码
- 原产地验证 --限制到
claude.ai/claude.com - 健康检查 —
/healthz与tsnet准备就绪 - 结构化日志记录 --JSON通过
slog - 强化Docker --只读root,cap_drop ALL,无特权用户
配置参考
server
| 字段 | 必填 | 描述 |
|---|---|---|
listen |
是 | 绑定地址。必须是环回(127.0.0.1)或未指定(0.0.0.0). |
allowed_origins |
否 | 允许的Origin标头列表。空=允许所有(开发模式)。 |
tailnet
| 字段 | 必填 | 描述 |
|---|---|---|
hostname |
是 | 网桥的尾标节点名称。 |
state_dir |
是 | tsnet持久状态的目录。 |
authkey_env |
Yes | 包含Tailscale认证密钥的环境变量。 |
auth (可选--对于authless模式省略)
| 字段 | 必填 | 描述 |
|---|---|---|
issuer |
是 | tsidp发行者URL(您的尾网IDP)。 |
audience |
是 | 您的网桥的公共URL |
introspection_url |
是 | tsidp内省端点。必须 http 或 https. |
client_id |
是 | DCR注册中的客户端ID(步骤1)。用于验证自省请求。 |
client_secret |
是 | DCR注册的客户机密。用于验证自省请求。 |
resource_metadata_url |
是 | RFC 9728元数据端点的公共URL。 |
笔记:
client_id和client_secret是从向tsidp注册客户端获得的凭据(步骤1)。Claude.ai用于OAuth流,tsmcp用于通过HTTP Basic Auth对令牌自检请求进行身份验证。- 这
introspection_url必须可以从容器内部触及。由于tsidp解析为Tailscale IP,tsmcp会自动通过其嵌入式tsnet节点路由自检请求。 - 主动自检结果将缓存60秒(或直到令牌到期,以较短者为准),以减少往返。
endpoints
| 字段 | 必填 | 描述 |
|---|---|---|
path |
是 | 此端点的URL路径(例如。, /mcp/my-server).必须从以下内容开始 /. |
target |
是 | 上游MCP服务器URL(尾标FQDN)。必须 http 或 https. |
description |
否 | 日志记录的人类可读描述。 |
enabled |
否 | 设置为 false 禁用。违约: true. |
upstream_token_env |
否 | 包含要在上游请求上设置的承载令牌的环境变量。 |
健康检查
curl http://127.0.0.1:8900/healthz
{"status":"ok","tsnet_connected":true}
退货 200 当tsnet连接时, 503 当退化时。
发展
# Run tests
go test ./...
# Run with debug logging
go run . -config config.yaml -debug
# Build
go build -o tsmcp .
项目结构
├── main.go # Entry point, lifecycle management
├── internal/
│ ├── config/ # YAML config loading + validation
│ ├── auth/ # Token introspection + RFC 9728 metadata + OAuth endpoint proxying
│ ├── proxy/ # Reverse proxy handler + transport
│ ├── tsbridge/ # Tailscale network bridge (tsnet)
│ ├── health/ # Health check endpoint
│ └── server/ # HTTP server assembly + middleware
├── Dockerfile # Multi-stage build (48MB image)
├── docker-compose.yml # Production deployment
└── .github/workflows/ # CI/CD (deploy + release)
