MCP反向代理服务

一个基于Go的反向代理,通过Tailscale网络将私有MCP服务器暴露给Claude.ai,并提供OAuth认证功能。

  • by meltforce
  • Go
  • 本地部署
  • 5 星标
  • 更新于
在 GitHub 查看 meltforce/tsmcp

使用说明

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网络,因此它可以通过容器名称访问tsmcp
  • TS_AUTHKEY 仅在首次启动时需要;tsnet保存状态后,节点会自动重新进行身份验证
  • NET_RAW tsnet的用户空间网络需要这种能力
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条将:

  1. 点击MCP端点,获得401
  2. 通过以下方式了解tsidp /.well-known/oauth-protected-resource
  3. 从tsmcp获取授权服务器元数据(由tsidp代理,使用 /token 和 /register 重写为tsmcp的起源)
  4. 将浏览器重定向到tsidp进行授权(通过您的Tailscale身份验证)
  5. 通过tsmcp将代码交换为访问令牌 /token 端点(通过尾网代理到tsidp)
  6. 使用令牌调用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",
                  ...
                }
  1. Claude.ai在没有令牌的情况下点击MCP端点,得到 401 具有资源元数据URL
  2. Claude.ai取回 RFC 9728 受保护的资源元数据,指向tsidp作为授权服务器
  3. 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 }
  1. Claude.ai点击 /authorize 在tsmcp上,获得302重定向到tsidp的授权端点
  2. tsidp通过用户的Tailscale身份对其进行身份验证 --浏览器通过Tailscale漏斗连接到tsidp,tsidp通过其tailnet节点识别用户。这是关键的安全边界:只有尾网上的用户才能授权。
  3. tsidp使用授权码重定向回Claude.ai
  4. 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)
  1. Claude.ai发送带有Bearer令牌的MCP请求
  2. tsmcp通过调用tsidp来验证令牌 RFC 7662 自省端点 超过尾鳞片 (通过tsnet)——这一点至关重要,因为tsidp解析为一个Tailscale IP,否则无法从Docker的网络访问
  3. 如果令牌处于活动状态,tsmcp会通过Tailscale将请求代理到上游MCP服务器
  4. 自检结果被缓存(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)