1. 为什么要自建 MCP Server
上一篇我们让 AI 伴侣消费别人的 MCP Server。这一篇反过来:把自己的能力发布成 MCP Server,让其他 AI 应用来消费。
为什么要这么做?三个理由。
能力复用。AI 伴侣项目里有「记忆检索」「关系画像」「情绪轨迹分析」这些能力。如果只锁在自己后端,用户在 Claude Desktop、Cursor 或任何 MCP-aware 客户端里就用不上。发布成 MCP 之后,用户可以在 Claude Desktop 里直接问「伴侣最近觉得我怎么样」,调用的是我们后端的同一份逻辑。
生态曝光。MCP marketplace(官方和第三方的)越来越像 App Store。一个好 MCP Server 会成为 LLM 生态里被发现你产品的入口。
多端共享。本专栏 AI 伴侣的目标是 Web + Mobile + 第三方客户端。自建 MCP 能让「第三方客户端」这条路线不用额外适配。
这一篇用 MCP SDK 从零写一个 MCP Server,托管在 Cloudflare Workers 上,被上一篇的 MCP 客户端消费。
2. MCP Server 的核心概念
一个 MCP Server 要声明三类能力(都是可选的,按需提供):
| 能力 | 对应场景 | 示例 |
|---|---|---|
| Tools | LLM 主动调用的函数 | search_memories / analyze_emotion |
| Resources | LLM 可以读取的内容 | memory://session/{id}/{memoryId} |
| Prompts | 可复用的 prompt 模板 | companion_intro / weekly_review |
本章只讲 Tools(最常用)。Resources 和 Prompts 的实现逻辑类似,只是协议字段不同。
3. 技术栈选择
自建 MCP Server 有几种技术组合:
| 组合 | 特点 | 适用 |
|---|---|---|
@modelcontextprotocol/sdk + stdio | 跑在本地,CLI 启动 | 桌面端 Demo、开发工具 |
@modelcontextprotocol/sdk + HTTP | 跑在服务器 | 生产部署 |
Cloudflare Workers + @cloudflare/mcp-agent | Workers 原生,自带 OAuth / Durable Objects | 本专栏推荐 |
本篇用 Cloudflare 的 @cloudflare/mcp-agent(也叫 workers-mcp)。原因是它和我们的 AI 伴侣后端跑在同一个 Workers 环境,可以直接复用 D1 / KV / Vectorize binding。
4. 实现 MCP Server
4.1 最小实现(stdio)
先跑通一个 Hello World 级的 MCP Server。
1pnpm add @modelcontextprotocol/sdk zod
最简版本用官方 SDK(stdio 传输):
01import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'02import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'03import { z } from 'zod'0405const server = new McpServer({06name: 'ai-companion',07version: '1.0.0',08})0910// 声明一个工具11server.tool(12'greet',13'根据情绪返回一句问候',14{15emotion: z.enum(['happy', 'sad', 'neutral']),16},17async ({ emotion }) => {18const greetings = {19happy: '今天的你真有活力 ☀️',20sad: '没事的,我在这里陪你 🌧️',21neutral: '又见面啦,今天过得怎么样?',22}23return {24content: [{ type: 'text', text: greetings[emotion] }],25}26},27)2829// 启动30const transport = new StdioServerTransport()31await server.connect(transport)
这段代码跑起来就是一个长驻进程,通过 stdio 和 MCP 客户端通信。Claude Desktop 配置:
1{2"mcpServers": {3"ai-companion": {4"command": "tsx",5"args": ["/path/to/minimal-mcp.ts"]6}7}8}
重启 Claude Desktop,对话里就能调用 greet 工具了。
4.2 部署到 Cloudflare Workers
生产环境要的是远程 MCP Server,用 HTTP / Streamable HTTP 协议。用 @cloudflare/mcp-agent:
1pnpm add @cloudflare/mcp-agent zod hono
01import { Hono } from 'hono'02import { McpAgent } from '@cloudflare/mcp-agent'03import { z } from 'zod'04import type { Env } from './bindings'0506// 定义 MCP Agent(就是一个 Durable Object)07export class CompanionMCP extends McpAgent<Env> {08server = new McpServer({09name: 'ai-companion',10version: '1.0.0',11})1213async init() {14// 工具:检索记忆15this.server.tool(16'search_memories',17'检索指定用户的记忆,支持语义相似度查询',18{19userId: z.string().describe('用户 ID'),20query: z.string().describe('检索关键词'),21topK: z.number().int().min(1).max(10).default(5),22},23async ({ userId, query, topK }) => {24const embedding = await embed(this.env, query)25const results = await this.env.VECTORIZE.query(embedding, {26topK,27filter: { userId },28})29return {30content: [31{ type: 'text', text: JSON.stringify(results.matches, null, 2) },32],33}34},35)3637// 工具:分析情绪轨迹38this.server.tool(39'emotion_trend',40'分析指定时间范围内用户的情绪变化趋势',41{42userId: z.string(),43days: z.number().int().min(1).max(30).default(7),44},45async ({ userId, days }) => {46const since = Date.now() - days * 86400_00047const rows = await this.env.DB.prepare(48'SELECT emotion, intensity, created_at FROM emotion_logs WHERE user_id=? AND created_at>? ORDER BY created_at',49).bind(userId, since).all()5051return {52content: [53{ type: 'text', text: JSON.stringify(rows.results, null, 2) },54],55}56},57)5859// 工具:关系画像60this.server.tool(61'relationship_profile',62'获取用户和伴侣的关系画像:亲密度、话题偏好、性格标签',63{64userId: z.string(),65},66async ({ userId }) => {67const profile = await this.env.DB.prepare(68'SELECT intimacy, tags, topic_preferences FROM user_profiles WHERE id=?',69).bind(userId).first()70return {71content: [72{ type: 'text', text: JSON.stringify(profile, null, 2) },73],74}75},76)77}78}7980// HTTP 入口81const app = new Hono<{ Bindings: Env }>()8283app.route('/mcp', CompanionMCP.serve('/mcp'))8485export default app
wrangler.toml 里注册 Durable Object:
01name = "companion-mcp"02main = "src/index.ts"03compatibility_date = "2026-04-01"04compatibility_flags = ["nodejs_compat"]0506[[durable_objects.bindings]]07name = "MCP_AGENT"08class_name = "CompanionMCP"0910[[migrations]]11tag = "v1"12new_sqlite_classes = ["CompanionMCP"]1314[[d1_databases]]15binding = "DB"16database_name = "companion"17database_id = "..."1819[[vectorize]]20binding = "VECTORIZE"21index_name = "companion-memories"
部署:
1wrangler deploy
部署好后,MCP Server 的地址就是 https://companion-mcp.your-domain.workers.dev/mcp。
5. 添加认证
到这一步,任何人只要知道 URL 都能调用你的 MCP Server,包括调你的记忆数据。必须加认证。
最朴素的 API Key 方式:
01app.use('/mcp/*', async (c, next) => {02const auth = c.req.header('authorization')03if (!auth?.startsWith('Bearer ')) {04return c.json({ error: 'unauthorized' }, 401)05}0607const token = auth.slice(7)08const tokenInfo = await c.env.KV.get(`mcp-token:${token}`, 'json')09if (!tokenInfo) {10return c.json({ error: 'invalid token' }, 401)11}1213c.set('userId', tokenInfo.userId)14await next()15})
然后在工具里用 this.ctx 拿 userId:
01this.server.tool('search_memories',02'检索当前用户的记忆',03{04query: z.string(),05topK: z.number().int().min(1).max(10).default(5),06},07async ({ query, topK }) => {08const userId = this.ctx.userId // 从认证上下文拿09// ...10},11)
注意 userId 不再是工具参数——它由认证系统决定,LLM 无法伪造。
生产推荐:OAuth
API Key 适合内部使用。对外公开的 MCP Server,业界推荐用 OAuth(用户授权 Claude Desktop 或 Cursor 访问他的伴侣数据)。Cloudflare 的 MCP Agent 自带 OAuth 2.1 支持:
1export class CompanionMCP extends McpAgent<Env> {2static oAuth = {3authorizeEndpoint: '/oauth/authorize',4tokenEndpoint: '/oauth/token',5scopes: ['memories:read', 'emotions:read'],6}7// ...8}
完整的 OAuth 流程涉及前端同意页、授权码交换等步骤,超出本章范围。建议生产前参考 Cloudflare 官方的 Remote MCP Server 示例。
6. 客户端接入
上一篇的客户端代码稍微改一下:
01import { experimental_createMCPClient as createMCPClient } from 'ai'02import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'0304const client = await createMCPClient({05transport: new StreamableHTTPClientTransport(06new URL('https://companion-mcp.yourdomain.com/mcp'),07{08fetch: (url, init) =>09fetch(url, {10...init,11headers: {12...init?.headers,13Authorization: `Bearer ${COMPANION_MCP_TOKEN}`,14},15}),16},17),18})1920const tools = await client.tools()21// tools 里有 search_memories / emotion_trend / relationship_profile
从这一步开始,任何 AI 应用都能通过 MCP 消费 AI 伴侣的能力。
7. 设计 MCP 工具的原则
不是把后端 API 直接包一层就叫 MCP Server。好的 MCP 工具有六条原则。
动词 + 清晰语义。工具名要像动词,像一个函数名。search_memories 比 memories 好,analyze_emotion_trend 比 emotion 好。
参数最小化。只暴露 LLM 需要知道的参数。像 userId 这种身份类参数交给认证层处理,不要作为工具参数暴露。
description 要详尽。工具的 description 是 LLM 决定何时调它的唯一依据。写清楚:这个工具能做什么、什么时候该用它、返回的结果是什么形状。
1this.server.tool(2'search_memories',3[4'检索与指定查询最相关的用户记忆。',5'适用场景:用户提到过去的事、想知道自己之前和伴侣说过什么、做情感复盘。',6'返回按相似度排序的前 topK 条记忆,每条包含内容、时间戳、重要性评分。',7].join('\n'),8// ...9)
返回结构要稳定。MCP 的返回是 { content: [{ type: 'text', text: '...' }] }。text 字段里放 JSON 字符串时,字段名和结构要稳定(用 schema 明文记录),否则 LLM 每次都要重新理解。
大数据量要分页。别一次返回 1000 条。提供 limit / offset / cursor,让 LLM 按需拉。
副作用工具要醒目。写操作(更新数据、发消息)的 description 里要明确写「这是一个写操作」,客户端据此可以弹 HITL 确认。
8. 可观测性
MCP Server 和普通 API 一样需要埋点。在 Worker 入口加 telemetry:
01app.use('/mcp/*', async (c, next) => {02const start = Date.now()03const userId = c.get('userId')0405await next()0607const duration = Date.now() - start08c.executionCtx.waitUntil(09logMcpCall(c.env, {10userId,11path: c.req.path,12status: c.res.status,13duration,14timestamp: Date.now(),15}),16)17})
结合 可观测性:Telemetry 的 Langfuse,也能把 MCP 调用作为独立 event 打入同一条 trace。
9. 协同与分发
一个有意思的用法:AI 伴侣后端自己也用自己发布的 MCP Server,而不是直接调内部函数。
为什么?一致性:
- 线上 AI 伴侣用 MCP 路径调
search_memories - Claude Desktop 的第三方用户也用 MCP 路径调
search_memories - 同一份代码路径、同一份认证、同一份埋点
这种「自产自销」的模式让你不用维护「内部版」和「外部版」两套实现。
01// 伴侣主接口02app.post('/chat', async (c) => {03const mcpClient = await createMCPClient({04transport: new StreamableHTTPClientTransport(05new URL('https://companion-mcp.yourdomain.com/mcp'),06{07fetch: (url, init) =>08fetch(url, {09...init,10headers: { ...init?.headers, Authorization: `Bearer ${internalMcpToken(c.env)}` },11}),12},13),14})1516const mcpTools = await mcpClient.tools()1718const result = streamText({19model: models.chat,20messages,21tools: mcpTools, // 自家的 MCP 工具22onFinish: () => c.executionCtx.waitUntil(mcpClient.close()),23})2425return result.toUIMessageStreamResponse()26})
唯一代价是多了一跳 HTTP 调用(伴侣后端 → MCP Server)。对 Workers 这种低延迟环境,这一跳通常不到 10ms,可以接受。
发布与分发
自建 MCP Server 怎么让别人用?有几种方式。
私有使用:就自己或内部团队用,分享 URL 和 token 就行。
通过 MCP Registry:社区有几个 MCP registry(比如 mcp.so),提交到那里可以被索引。
通过 Claude MCP Directory:Anthropic 官方目录,能被 Claude Desktop 原生发现。
文档 + GitHub:在自己项目主页写接入文档,放 GitHub 示例。
对 AI 伴侣这种涉及个人数据的 MCP,推荐「文档 + GitHub + OAuth 认证」的组合,而不是直接发到公开 Registry 让人一看到就能用。
10. 小结
- 自建 MCP Server 让你的能力被整个 LLM 生态消费
- 技术栈推荐:Cloudflare Workers +
@cloudflare/mcp-agent,和 AI 伴侣后端同一环境 - 工具设计六原则:动词命名、参数最小化、详尽 description、返回结构稳定、分页、副作用醒目
- 认证上:内部用 API Key,对外用 OAuth;
userId从认证层拿,不暴露给 LLM - 可观测性:路由中间件 + Langfuse 打入同一个 trace
- Dogfooding 模式:自己用自家 MCP,保证内部和外部调用路径一致
- 分发:私有分享、MCP Registry、Claude Directory、文档 + GitHub
下一篇进入协同三连的收官——工程实战:AI SDK × LangChain/LangGraph 代码级协同。把本章所有能力和前面 LangChain / LangGraph 两章的能力组合起来,给出 AI 伴侣主管线的完整代码骨架。