MISSION 02 · 手与眼TASK 08

亲手写一个 MCP Server

上一章说 MCP 的门槛低,这一章证明给你看。我们用 TypeScript 官方 SDK 写一个最小但完整的 MCP server——它暴露两个工具,然后挂到 Claude Desktop 或 Claude Code 上真正用起来。写完你会发现:把一个能力变成「任何 Agent 都能插上的工具」,核心就三步——声明工具、实现逻辑、连上传输。

TypeScript@modelcontextprotocol/sdkstdio实战

我们要造什么

造一个「便签」server,给 Agent 两个能力:add_note(text) 记一条便签,list_notes() 列出所有便签。麻雀虽小,但它涵盖了 MCP server 的全部要素:声明工具的 schema、处理调用、返回结果。你把这里的「便签」换成「查数据库」「调你司的内部 API」「读链上余额」,骨架完全一样。

第一步:项目骨架

bash · 初始化
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 顺手)。

第二步:声明工具 + 实现逻辑 + 连上传输

server.ts · 一个完整的最小 MCP server
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 替你处理了 initializetools/listtools/call 那套握手(上一章的三步)——你只管声明和实现。注意每个工具的 description 都写了「什么时候调用我」,这正是第六章的原则落地。

Android 类比

server.tool(name, desc, schema, handler) 读起来就像给 Retrofit 声明一个接口方法,或者往路由表里注册一个 @GET。你声明「有这么个端点、参数长这样、命中时跑这段逻辑」,框架负责序列化、分发、响应。MCP server 就是一台专供 Agent 调用的迷你「路由服务器」。

第三步:挂载到 Host 上

server 写好了,怎么让 Agent 用上?以 Claude Desktop 为例,在它的配置文件里加一段,告诉它「有这么个 MCP server,这样启动它」:

claude_desktop_config.json
{
  "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)。这个坑坑过无数人,记住它能省你一小时。

◉ Agent 自白

你刚刚做的事,本质是「给我长出一只新手」。在你写这个 server 之前,我不会记便签;写完挂上之后,我会了——而且我这颗大脑什么都没改,只是多了两个可调用的 name。这就是 MCP 生态的美妙:能力的增长不需要重训模型,只需要有人写 server。你在 hackathon 上真正该秀的,往往不是「我调用了大模型」,而是「我给 Agent 接上了一个别人没有的、有用的能力」——那个能力,就是一个 MCP server。到这里,「手」讲完了。下一章讲「眼」:多模态,让我从只会读文字,到能看画面、读屏、操作 GUI。