ByteNoteByteNote

字节笔记本

2026年8月29日

28 行代码写一个 MCP 服务

API中转
¥120

MCP 服务不一定要先开一个仓库。TypeScript 官方 SDK 里,起一个 McpServer、注册一个 tool、用 stdio 接到客户端,大约三十行就能在 Cursor、Claude Code、Claude Desktop、Zed 里被模型调用。下面用一个假天气工具把这条最小闭环走通:装依赖、写文件、用官方 Inspector 点一下、再写进各客户端配置。

官方入口:MCP 构建服务教程TypeScript SDK 仓库v1 包 @modelcontextprotocol/sdk(本稿按 1.30 线)、v2 包 @modelcontextprotocol/server

这三十行在干什么

MCP 把「工具」从模型对话里拆出来。客户端(Cursor 等)拉起一个本地进程,双方在 stdin/stdout 上走 JSON-RPC:先 initialize,再 tools/list,需要时 tools/call

最小服务只做四件事:

  1. new McpServer({ name, version }) 报自己的身份。
  2. server.tool(...)(v1)或 server.registerTool(...)(v2)挂一个函数,Zod 描述入参。
  3. 函数返回带 content 数组的结果,里面是文本块。
  4. StdioServerTransport 把协议接到标准输入输出。客户端负责启动这个进程,你平时不要自己在终端里跑它。

下面的 getWeather 故意返回固定的 sunny,用来确认协议通了。真查天气是后一步的事。

建项目

需要 Node.js 20+。官方 Inspector 网页版要求 22.19+,没有的话可以先用客户端配置来验证。

新建目录 weather-mcp,初始化 Node 项目,安装 @modelcontextprotocol/sdkzod,开发依赖装 typescripttsx@types/nodepackage.json 里加上 "type": "module",否则后面的顶层 await 会报错。

最小服务(v1,约 28 行)

把下面存成 weather.ts。这是现在下载量最大的 @modelcontextprotocol/sdk 写法,也是常见的「二十八行天气服务」那条路径。

ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "Weather Service",
  version: "1.0.0",
});

server.tool(
  "getWeather",
  {
    city: z.string().describe("city name, e.g. Beijing"),
  },
  async ({ city }) => ({
    content: [
      {
        type: "text",
        text: `The weather in ${city} is sunny!`,
      },
    ],
  }),
);

const transport = new StdioServerTransport();
await server.connect(transport);

读代码时记住三点:

  • 导入走 SDK 的 mcp.js / stdio.js,不要自己实现 JSON-RPC。
  • city 用 Zod 标成字符串。客户端会按这个 schema 生成参数表,模型填错类型会在调用前被拦住。
  • 返回值必须是 MCP 的 content 数组。直接返回普通字符串,客户端接不住。

stdio 服务不要往标准输出打日志。stdout 被协议占用,打印会把 JSON-RPC 弄坏。调试用 console.error,它走 stderr,客户端一般会写进 MCP 日志。

用 Inspector 先点通

不要一上来就改 Cursor 配置。先用官方 Inspector 把 server 本身验证掉,避免「配了但没工具」时分不清是代码问题还是客户端问题。

包名是 @modelcontextprotocol/inspector。启动时把「用 tsx 跑 weather.ts」整条命令交给它,路径用绝对路径。终端会打出一个带一次性 token 的本地 URL。打开后:

  1. 确认 Transport 是 STDIO,command 指向刚才那条启动命令。
  2. Connect。
  3. 看 Tools 列表里有没有 getWeather
  4. 填 city=Beijing,Execute,返回里应出现 The weather in Beijing is sunny!

不想开浏览器时,给 Inspector 加 --cli,再带 --method tools/list。能列出工具后,再 --method tools/call,工具名 getWeather,参数 city=Beijing。说明见 MCP Inspector

接到客户端

路径一律用绝对路径。相对路径在客户端拉起子进程时经常找不到文件。把下面 JSON 里的 /ABS/weather-mcp/weather.ts 换成你机器上的真实路径。

Cursor

在项目 .cursor/mcp.json,或 Cursor 设置里的 MCP 配置,加入:

json
{
  "mcpServers": {
    "weather": {
      "command": "npx",
      "args": ["-y", "tsx", "/ABS/weather-mcp/weather.ts"]
    }
  }
}

保存后重载窗口。工具面板里应出现 getWeather。试一句:「用 weather 工具查一下 Shanghai 的天气。」模型应去调工具,而不是自己编气象。

Claude Code

claude mcp add,transport 选 stdio,名字叫 weather。双横线后面跟启动命令:tsx 跑那份 weather.ts 的绝对路径。双横线很重要,后面整段都是启动命令,不要让 Claude 自己的 flag 把 tsx 的参数吃掉。

加完后 claude mcp listclaude mcp get weather 看状态。会话里 /mcp 也能看。改完 weather.ts 后,stdio 进程要重启才加载新代码:先 remove 再 add,或在 /mcp 里 Reconnect。官方说明:Connect Claude Code to tools via MCP

Claude Desktop

编辑 claude_desktop_config.json(macOS 一般在 ~/Library/Application Support/Claude/),mcpServers 里放和 Cursor 同一段 JSON。必须完全退出再打开 Desktop,只关窗口不够。日志在 ~/Library/Logs/Claude/ 下的 mcp 日志。

Zed

在 Zed 设置的 MCP 段用同样的 command 和 args。Zed 也是拉起本地进程走 stdio,和 Cursor 同一套 server。

Windows 把路径换成盘符加双反斜杠,或改用正斜杠。tsx 不在 PATH 时,command 写成可执行文件的绝对路径。

2026 的 v2 写法

官方 TypeScript SDK 的 main 分支已经切到 v2 包 @modelcontextprotocol/server,对应 2026-07-28 协议。最小例子几乎一样长,注册 API 换成 registerTool,Zod 走 v4:

ts
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";

const server = new McpServer({
  name: "Weather Service",
  version: "1.0.0",
});

server.registerTool(
  "getWeather",
  {
    description: "Return weather by city (demo, always sunny)",
    inputSchema: z.object({
      city: z.string().describe("city name, e.g. Beijing"),
    }),
  },
  async ({ city }) => ({
    content: [
      { type: "text", text: `The weather in ${city} is sunny!` },
    ],
  }),
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

新项目可以直接用 v2。已经在跑的 v1 服务继续装 @modelcontextprotocol/sdk 的 1.x。官方说 v1 至少还会修 bug 和安全问题半年。两套不要混在同一个 package.json 里。

更想写 Python,用官方 mcp 包里的 FastMCP:装饰一个函数,再 run(transport="stdio")。完整天气示例在同一份 Build an MCP server 教程里。

从假天气换成真工具

协议通了之后,只改 handler 内部:

  1. 换成真实 HTTP。美国位置可以跟官方教程一样调 api.weather.gov;国内位置用你已有的天气接口,密钥放环境变量,不要写进仓库。
  2. 超时和错误返回成文本 content,让模型能读到失败原因,不要让进程崩溃。stdio 挂了,客户端只会显示 server 断线。
  3. 需要给别的机器用时,把传输换成 Streamable HTTP。本地编码助手继续用 stdio 最省事。

常见翻车:

现象先查
客户端里看不到工具路径是不是绝对路径;type: module 有没有加;Desktop 有没有彻底退出
一连上就断开是不是往 stdout 打了日志;tsx 单独跑时应无输出、干等
改了代码没生效stdio 是子进程,要 Reconnect 或重加 server
Inspector 能调、Cursor 不能两边启动命令是否一字不差

小结

自己写 MCP,先求一个 tool 加 stdio 通。v1 用 @modelcontextprotocol/sdkserver.tool,v2 用 @modelcontextprotocol/serverregisterTool,再交给 Cursor / Claude Code / Desktop / Zed 拉起进程。Inspector 点通之后再换真接口。协议说明以 modelcontextprotocol.iotypescript-sdk 为准。

分享: