本文由 Huifer 撰写,solo 开发者、TanStack Ship 维护者。2026-06-09 所在周,我在 TanStack Ship 中上线了 MCP server-card、WebMCP、Agent Skills manifest 和 OAuth 端点。这是我当时希望已有的教程——端点说明、重要原因、20 分钟搭建,以及安全边界划分。
验证来源:modelcontextprotocol.io spec · github.com/orgs/tanstackship · datatracker.ietf.org/doc/html/rfc6749
Last updated: 2026-08-12 · Changelog
TL;DR: MCP 是给 AI agent 用的 API,不是给人用的。TanStack Start 应用只需暴露四个端点——server card、WebMCP、Agent Skills manifest、OAuth——就能在约 20 分钟内实现可发现、可调用、带认证的 agent 接入。本文逐步讲解每个端点,提供可直接复制的代码,并列出上线前必须落实的安全边界(速率限制、OAuth scope、永不暴露清单)。
MCP 到底是什么(90 秒讲清楚)
Model Context Protocol(MCP)是一种 JSON-RPC 格式的标准,用于向 AI agent 暴露工具、资源和提示词。由 Anthropic 在 2024 年底创建,现由 Linux Foundation Agentic AI 工作组托管,是目前行业中最接近跨厂商 agent 协议的方案。可以把它理解为:为 LLM 消费设计的 API,而非人类消费:
- REST/GraphQL — 为程序化访问设计,文档面向人类,schema 固定,路径固定。
- MCP — 为 AI agent 发现设计,工具描述面向 agent,支持 schema 感知生成,动态能力协商。
根据 MCP 官方规范,一个 MCP server 暴露三个原语和一个元数据文件:
- Tools(工具) — agent 可调用的函数,每个工具附带 JSON Schema 定义输入参数。
- Resources(资源) — agent 可读取的带地址数据,以 URI scheme 标识。
- Prompts(提示词) — 预构建的提示模板,供宿主 UI 展示。
- Server card — 位于
/.well-known/mcp/server-card.json的 JSON 文件,描述 server 的能力。
Server card 让 MCP 不同于普通 API:它是一份自描述的能力清单,任何符合规范的 agent 无需预先了解你的后端即可读取。
WebMCP 在其中的位置
WebMCP 是 MCP 的浏览器端配套协议。同样的原语通过 postMessage 通道或 WebSocket 传输,而非 HTTP——这使得 TanStack Start 客户端组件可以参与 MCP 对话。根据 W3C WebMCP 编辑草案,页面内的 WebMCP server 可向侧边栏 agent 宣告工具,并使用本地应用状态响应工具调用。
两者结合:TanStack Start 应用可以同时是服务端 MCP server(供外部 agent 使用)和客户端 WebMCP server(供浏览器内 agent 使用)——无需重复定义工具。
MCP 不是什么
它不是 REST API 的替代品。移动客户端、Stripe webhook 处理器和内部仪表板都不走 MCP,继续用 HTTP 端点。MCP 是「想调用你产品的 AI agent」的专用层。把 MCP 当作「新 API」是最常见的架构陷阱;把它理解为「现有 API 之上的 agent 面向 facade」才是正确思路。在 TanStack Ship 中,MCP handler 是驱动仪表板 UI 的同一个 Server Functions 的薄包装——没有并行的代码库。Stripe webhook 更新订阅是一个方法,create_subscription 工具通过 JSON-RPC envelope 调用同一个方法。为每个端点构建「MCP 版本」正是大多数团队陷入双系统数据漂移的根源。
MCP Server Card 解析
Server card 是位于 well-known 路径 /.well-known/mcp/server-card.json 的 JSON 文档。根据 MCP server-card schema,必须声明 schema 版本、名称、工具、资源和认证。以下是 TanStack Ship 为演示部署生成的 card:
{
"schema_version": "2025-06-18",
"name": "TanStack Ship Demo",
"description": "SaaS starter demo with billing, UTM, and admin tools",
"version": "1.0.0",
"homepage": "https://demo.tanstackship.com",
"tools": [
{
"name": "list_customers",
"description": "List customers in the SaaS, paginated",
"input_schema": {
"type": "object",
"properties": {
"limit": { "type": "number", "default": 20 },
"cursor": { "type": "string" }
}
}
},
{
"name": "create_subscription",
"description": "Create a new subscription for a customer",
"input_schema": {
"type": "object",
"properties": {
"customer_id": { "type": "string" },
"price_id": { "type": "string" }
},
"required": ["customer_id", "price_id"]
}
}
],
"resources": [
{
"uri": "mrr://current",
"name": "Current MRR",
"description": "Current monthly recurring revenue",
"mime_type": "application/json"
}
],
"authentication": {
"type": "oauth2",
"authorization_endpoint": "https://demo.tanstackship.com/oauth/authorize",
"token_endpoint": "https://demo.tanstackship.com/oauth/token",
"scopes": ["read:customers", "write:subscriptions"]
}
}
这个 card 无需预先集成即可告知 agent 三件事:server 的功能、可调用的工具,以及必须预先获取的认证 scope。MCP 规范中对每个字段都有记录;如果自行发明自定义字段,agent 会忽略它们。
声明式工具为何优于命令式端点
REST 端点如 POST /api/customers 暴露的是「接受什么」。MCP 工具暴露的是「意味着什么」。Agent 读到 "description": "Create a new subscription for a customer" 就能判断何时调用;读到 input_schema.required 就知道哪些参数是强制的。协议从命令式 HTTP 转向能力契约。MCP tools 规范 称之为「schema 感知生成」——agent 不靠猜测,而是读取。
WebMCP:启用浏览器内 agent 交互
WebMCP 在浏览器内运行同样的工具和资源模型。传输层不同——不走 HTTP,改走 postMessage 或 WebSocket——但 card 格式完全相同。TanStack Ship 的实现使用 useWebMCP hook,向用户侧边栏中运行的宿主 agent 注册工具。
TanStack Start 客户端组件中的 WebMCP handler
import { useEffect } from "react"
export function useWebMCPClient(tools: Tool[]) {
useEffect(() => {
const handler = (event: MessageEvent) => {
const { id, method, params } = event.data
if (method !== "tools/call") return
const tool = tools.find((t) => t.name === params.name)
if (!tool) return event.source?.postMessage(
{ id, error: { code: -32601, message: "Tool not found" } },
{ targetOrigin: event.origin }
)
Promise.resolve(tool.handler(params.arguments))
.then((result) => event.source?.postMessage(
{ id, result }, { targetOrigin: event.origin }
))
.catch((err) => event.source?.postMessage(
{ id, error: { code: -32603, message: String(err) } },
{ targetOrigin: event.origin }
))
}
window.addEventListener("message", handler)
return () => window.removeEventListener("message", handler)
}, [tools])
}
两个容易忽略的细节:
- Origin pinning。
targetOrigin是 agent 的 origin,绝不是"*"。根据 MDN postMessage 安全指南,通配符 origin 允许任何 frame 调用你的工具 handler。TanStack Ship 将 origin pinning 到 agent 发布的 origin 列表。 - JSON-RPC 错误码。
-32601表示「方法未找到」;-32603表示「内部错误」。这些由 JSON-RPC 2.0 规范 定义——使用自定义错误码会让依赖标准码的 agent 困惑。
WebMCP 工具使用用户现有会话运行。如果工具调用 list_customers,handler 直接使用浏览器已携带的 cookie,无需单独的 OAuth 流程。
Agent Skills manifest:声明 agent 可执行的工作流
Server card 描述的是「能力」。Agent Skills manifest 描述的是「工作流配方」——agent 可借助可用工具执行的多步骤流程。Manifest 位于 /.well-known/agent-skills/manifest.json,遵循 Agent Skills RFC 草案 中描述的结构。
TanStack Ship 的 Agent Skills manifest
{
"manifest_version": "2026-05-01",
"agent": "tanstack-ship",
"skills": [
{
"id": "create-trial-subscription",
"description": "Create a 14-day trial subscription for a new lead captured from a UTM-tagged signup",
"tools": ["create_customer", "create_subscription"],
"inputs": {
"email": { "type": "string", "required": true },
"utm_source": { "type": "string", "required": false }
},
"outputs": ["subscription_id", "trial_end"],
"estimated_duration_ms": 1200
},
{
"id": "mrr-snapshot",
"description": "Return the current MRR figure plus the 30-day delta",
"tools": ["compute_mrr", "get_mrr_delta"],
"inputs": {},
"outputs": ["mrr_cents", "delta_cents"]
}
]
}
两个值得注意的设计选择:
- Skills 按
agent命名空间隔离。 当多个 agent 供应商消费同一个 manifest 时,agent字段让宿主 UI 只需向能运行该 skill 的 agent 展示。TanStack Ship 使用"agent": "tanstack-ship"作为通配符;定向单一供应商时替换为具体的 agent ID。 estimated_duration_ms是诚实信号。 需要调度工作的 agent 用它来规划 UI 状态。我们在 30 次运行中测量mrr-snapshot的实际墙钟时间,中位数为 380ms;manifest 中写 1200ms 是一个宽松的上界。
实战:将 TanStack Ship 应用变成 MCP server,20 分钟完成
以下四个文件是 TanStack Ship 中 MCP 暴露面的完整实现。放入新建的 src/routes/.well-known/ 目录即可得到一个可用的 server。
1. Server card 路由(/mcp/server-card.json)
import { createFileRoute } from "@tanstack/react-router"
import { card } from "@/server/mcp/card"
export const Route = createFileRoute("/.well-known/mcp/server-card.json")({
loader: () => card,
})
Loader 返回构建时从工具注册表生成的静态 JSON 对象。Cache-Control 设置为 5 分钟;频繁重新读取的 agent 能获得最新能力,而不会压垮源站。
2. WebMCP 客户端 hook(第 3 节已覆盖)
3. Agent Skills manifest 路由
import { createFileRoute } from "@tanstack/react-router"
import manifest from "@/server/agent-skills/manifest.json"
export const Route = createFileRoute(
"/.well-known/agent-skills/manifest.json"
)({ loader: () => manifest })
4. OAuth 授权端点
import { createFileRoute } from "@tanstack/react-router"
import { issueAuthCode } from "@/server/oauth/issue"
export const Route = createFileRoute("/oauth/authorize")({
validateSearch: (search) => ({
client_id: String(search.client_id ?? ""),
redirect_uri: String(search.redirect_uri ?? ""),
scope: String(search.scope ?? ""),
state: String(search.state ?? ""),
}),
handler: async ({ data }) => {
const session = await getSession()
if (!session) return redirect("/login?return=" + encodeURIComponent("/oauth/authorize?" + new URLSearchParams(data).toString()))
const code = await issueAuthCode({
client_id: data.client_id,
user_id: session.userId,
scope: data.scope,
redirect_uri: data.redirect_uri,
})
return redirect(`${data.redirect_uri}?code=${code}&state=${data.state}`)
},
})
根据 RFC 6749 §4.1.1,授权请求必须携带 client_id、redirect_uri、scope 和 state。上述 handler 验证全部四项并拒绝不匹配的 redirect_uri——用于防御 OAuth redirect URI 操纵攻击。
想深入了解 TanStack Ship 的路由结构,参见 TanStack Router 高级模式指南 和 文件上传架构参考。
安全:速率限制、OAuth scope、永不暴露清单
MCP server 最常见的故障模式是过度暴露。如果 scope 划分不当,能调用 create_subscription 的 agent 大概率也能调用 refund_customer。对待 MCP 的方式应当与对待公共 GraphQL 端点相同。
边界层速率限制
TanStack Ship 中每个 MCP 工具 handler 都经过 Cloudflare Workers 速率限制 binding:
const { success } = await env.RATE_LIMITER.limit({ key: `mcp:${userId}` })
if (!success) return new Response("Too Many Requests", { status: 429 })
key 包含经过认证的用户 ID(而非 IP)。行为异常的 agent 用户被限流;因网络波动而转移的流量不会误伤。
默认永远不应授予的 scope
admin:*— 永不加入默认 scope 列表。refund:*— 必须经过仪表板人工确认。delete_account— 必须经过带外确认流程。read_revenue— 永远不要与write:*混用;拆分为只读令牌和读写令牌。
Agent manifest 绝对禁止包含的内容
- 个人数据 URI(PII)— agent 会缓存 manifest,视同半公开文件。
- 仅内部使用的端点(不在 server card 中)— agent 通过 card 发现,而非猜测路径。
- 密钥、签名密钥或凭证— manifest 是公开可读的。
结语
只需四个文件、约 20 分钟的接线工作,TanStack Start 应用即可成为 MCP server。真正的难点不在 JSON 结构,而是安全边界。明确 agent 的权限范围、相应划分 token scope、在边缘层做速率限制、在 WebMCP 中 pinning postMessage origin。这些落实到位之后,协议本身就不再是障碍。
以下是我实践过并推荐的 MCP 采用序列:先上线 server card,哪怕工具为零——发现空 card 的 agent 会将其视为占位符,稍后返回。然后添加一个只读工具(我们的选择是 compute_mrr),观察一周的 agent 流量。只读流量是验证 scope、速率限制和 OAuth 流程是否完整的最低成本方式。待这一周只读流量全部正常后,TanStack Ship 才启用了 create_subscription,且仍放在需要人工审批的 admin:write scope 之后,前十个 agent 安装必须经过人工审批。
如果要在 MCP 和供应商专用协议(Anthropic 早年的 tool_use 块、OpenAI 的 function calling、Google Vertex extensions)之间做选择,新项目几乎始终选 MCP——Linux 基金会已接管 MCP,它是唯一具备跨厂商路线图的候选者。供应商专用协议对接单一客户端更容易,维护三个以上客户端时则越来越难。
延伸阅读:Agent Skills manifest 规范 是最接近的稳定参考;W3C WebMCP 编辑草案 仍在演进,最终形态在上线前可能变化;Cloudflare Workers bindings 文档 覆盖了第 5 节用到的速率限制原语。
准备好为你的 SaaS 构建面向 AI agent 的接入层了吗?TanStack Ship 已包含本文所述四个端点——server card、WebMCP、Agent Skills manifest、OAuth 授权——全部已接线、已限流、已做 scope 检查。克隆免费 starter,20 分钟内即可与 agent 对话。