2026-10-16 · 林一舟
上个月的一个数据挺有意思:MCP官方TypeScript SDK的npm周下载量到了5100万次。两年前这个协议刚开源时,社区还在争论"又造一个轮子";现在Claude、Cursor、ChatGPT全都要用它接外部工具。不会写MCP server,就像2015年不会写REST API——不影响吃饭,但影响你接到下一类活。
问题是你现在去搜教程,大概率会被坑:搜索排名靠前的文章里,一半还在教已经废弃的SSE传输写法(2025年6月的规范就把它标记为废弃了),抄下来能跑,但方向是错的。
这篇文章按"你要做的事"走:跑通 → 接入 → 调试 → 避坑,全程基于SDK 1.x当前基线。
写server之前先想清楚给AI暴露什么。MCP就三个原语,口诀很简单:
90%的场景只需要Tool。别一上来就想把三个全用上。
建工程,装官方SDK和zod:
mkdir my-first-mcp && cd my-first-mcp
npm init -y
npm i @modelcontextprotocol/sdk@^1 zod
npm i -D typescript @types/node
npx tsc --init
package.json里两个关键点:"type": "module"(ESSM是基线,别混CJS),以及将来发布用的bin字段。
然后是最小可运行的完整server,src/index.ts:
#!/usr/bin/env node
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: "my-first-mcp", version: "0.1.0" });
server.tool(
"add",
"两数相加",
{
a: z.number().describe("加数a"),
b: z.number().describe("加数b"),
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
})
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("server已启动"); // 重要:日志只能走stderr
}
main();
编译运行:npx tsc && node dist/index.js。没有输出、没有报错,就对了——stdio模式下它在等stdin上的JSON-RPC消息。
这里有个对使用体验影响巨大、却最常被忽略的细节:describe()里的文字会原样变成LLM看到的参数说明。描述写得准,模型调用得就准。把"加数a"想象成写给一个聪明但完全没上下文的实习生的注释。
还有一个新手大坑:stdout被协议独占。任何console.log都会污染JSON-RPC流,客户端直接断连。所有日志走console.error。
编辑配置文件(Windows:%APPDATA%\Claude\claude_desktop_config.json;macOS:~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"my-first-mcp": {
"command": "npx",
"args": ["-y", "你的包名"]
}
}
}
完全退出并重启Claude(托盘右键退出,不是关窗口),设置里看到工具列表就通了。
Windows用户专属坑:如果怎么都连不上,八成是Claude Desktop的PATH里没有npx。把command换成cmd、args前加"/c":
{ "command": "cmd", "args": ["/c", "npx", "-y", "你的包名"] }
这一个坑卡掉了我一晚上,你三十秒解决。
项目根建.cursor/mcp.json,格式一模一样。保存后Cursor的MCP设置页亮绿灯即接入。
只支持远端server(需要部署成HTTP端点+OAuth+manifest),本地开发阶段用Claude/Cursor就够了。注意远端用的是Streamable HTTP——网上教SSEServerTransport的教程直接关掉,那是废弃路线。
同一个server写一次,三个客户端通吃,这就是协议的意义。
写server最烦的是每次改完都要重启客户端验证。用官方Inspector,图形界面直连:
npx @modelcontextprotocol/inspector node dist/index.js
浏览器自动打开控制台,能直接调用tool、看原始JSON-RPC报文、测参数校验。先在Inspector里调通,再接客户端,能省掉一大半"到底是server的问题还是客户端的问题"式的抓瞎。
await server.connect()之后了。所有server.tool()必须在connect之前——顺序错了不报错,只是静默失效,最阴险Cannot use import statement outside a module:ESM/CJS混用。package.json确认"type": "module",或者干脆编译成CJS并保留文件头shebangnpx -y默认每次拉最新版,你的依赖悄悄漂移了。开发期就锁版本:npx -y 包名@0.1.0跑通本地之后,路线大致是:npm publish让别人"一行接入"你的server(bin+files两个字段是关键)→ 需要对外服务时上Streamable HTTP远端部署(Cloudflare Workers免费额度就够个人用)→ 加鉴权和用量记录,就能收费了。2026年7月的规范修订方向是无状态、可缓存——新server尽量做成无状态,天生适合放在CDN后面。
安全提醒只说最要紧的一条:每个tool就是开放给模型的一个权限。stdio server拥有你本机用户的全部权限,装别人的server之前,像审查npm依赖一样读一遍代码。
写这篇的夜里,第一个付费用户在群里说"速查表救了他半晚上"——这就是夜航船在做的事。
关于作者:林一舟,夜航船(NightShip)主理人。白天泊岸,夜里出航——一个只在夜里发布产品的匿名开发者,专注为开发者做省时间的工具和模板。 如果你想要这份完整版速查手册(10章:传输模式决策树、三端接入配置、9条报错速查表、远端部署与发布分发全套,附免费更新机制),欢迎到我主页查看,掘金读者首发价。