亲手写一个 MCP Server
上一章说 MCP 的门槛低,这一章证明给你看。我们用 TypeScript 官方 SDK 写一个最小但完整的 MCP server——它暴露两个工具,然后挂到 Claude Desktop 或 Claude Code 上真正用起来。写完你会发现:把一个能力变成「任何 Agent 都能插上的工具」,核心就三步——声明工具、实现逻辑、连上传输。
我们要造什么
造一个「便签」server,给 Agent 两个能力:add_note(text) 记一条便签,list_notes() 列出所有便签。麻雀虽小,但它涵盖了 MCP server 的全部要素:声明工具的 schema、处理调用、返回结果。你把这里的「便签」换成「查数据库」「调你司的内部 API」「读链上余额」,骨架完全一样。
第一步:项目骨架
mkdir note-mcp && cd note-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
@modelcontextprotocol/sdk 是官方 SDK,帮你处理 JSON-RPC 收发和协议细节;zod 用来声明参数 schema(比手写 JSON Schema 顺手)。
第二步:声明工具 + 实现逻辑 + 连上传输
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; const notes: string[] = []; // 我们的「数据库」 const server = new McpServer({ name: "note-mcp", version: "1.0.0" }); // 工具一:记便签 server.tool( "add_note", "新增一条便签。当用户说「记一下」「帮我记住」时调用。", { text: z.string().describe("便签内容") }, async ({ text }) => { notes.push(text); return { content: [{ type: "text", text: `已记下:${text}` }] }; } ); // 工具二:列便签 server.tool( "list_notes", "列出所有已记的便签。当用户问「我记了什么」时调用。", {}, async () => ({ content: [{ type: "text", text: notes.length ? notes.map((n, i) => `${i + 1}. ${n}`).join("\n") : "(还没有便签)" }] }) ); // 连上 stdio 传输,开跑 await server.connect(new StdioServerTransport());
就这些。数一下你写了什么:两次 server.tool(...) 各声明一个工具(名字、描述、参数 schema、执行函数),最后一行把 server 接到 stdio 传输上。SDK 替你处理了 initialize、tools/list、tools/call 那套握手(上一章的三步)——你只管声明和实现。注意每个工具的 description 都写了「什么时候调用我」,这正是第六章的原则落地。
server.tool(name, desc, schema, handler) 读起来就像给 Retrofit 声明一个接口方法,或者往路由表里注册一个 @GET。你声明「有这么个端点、参数长这样、命中时跑这段逻辑」,框架负责序列化、分发、响应。MCP server 就是一台专供 Agent 调用的迷你「路由服务器」。
第三步:挂载到 Host 上
server 写好了,怎么让 Agent 用上?以 Claude Desktop 为例,在它的配置文件里加一段,告诉它「有这么个 MCP server,这样启动它」:
{
"mcpServers": {
"note": {
"command": "node",
"args": ["/绝对路径/note-mcp/dist/server.js"]
}
}
}
重启 Host,它会用这条命令把你的 server 当子进程拉起来,通过 stdio 通信。然后你就能对 Claude 说「帮我记一下:明天三点开会」,它会调用你写的 add_note——你亲手写的能力,现在成了任何支持 MCP 的 Agent 都能用的工具。这就是那份「插上就能用」的数据线,由你这头制造。
本地 server 用 stdio(Host 把它当子进程),远程 server 用 HTTP(部署成一个 web 服务,多人共享)。hackathon 里,如果你想让评委和队友都能连你的能力,就部署成远程 HTTP server;如果只是本地增强自己的编码 Agent,stdio 最省事。同一份工具逻辑,换个 transport 就能从本地变远程。
stdio server 有个新手必踩的雷:不要往 stdout 打印日志。stdout 是 JSON-RPC 消息通道,你 console.log 一句调试信息,就会污染协议、让 Host 解析失败。要打日志,打到 stderr(console.error)。这个坑坑过无数人,记住它能省你一小时。
你刚刚做的事,本质是「给我长出一只新手」。在你写这个 server 之前,我不会记便签;写完挂上之后,我会了——而且我这颗大脑什么都没改,只是多了两个可调用的 name。这就是 MCP 生态的美妙:能力的增长不需要重训模型,只需要有人写 server。你在 hackathon 上真正该秀的,往往不是「我调用了大模型」,而是「我给 Agent 接上了一个别人没有的、有用的能力」——那个能力,就是一个 MCP server。到这里,「手」讲完了。下一章讲「眼」:多模态,让我从只会读文字,到能看画面、读屏、操作 GUI。