从零手搓一个MCP服务器,我踩过的坑比写代码还多
上个月老板突然在群里丢了一句话:"我们的AI客服能不能直接查公司的数据库?"
我当时觉得这是个小事——写个function calling不就完了?结果越做越发现不对劲。先是Python写了一版,换到TypeScript项目又要重写一遍。然后产品那边说他们的AI助手也想用,好家伙,又得再来一次。
就在我准备提桶跑路的时候,同事甩了一个链接给我:"看看MCP?"
然后我就入坑了。
MCP到底是啥?一句话说清楚
MCP全称Model Context Protocol,是Anthropic在2024年11月搞出来的一个开放协议。
用大白话说:它是一个标准化的插头。
你想想USB-C——不管什么设备,只要接口是USB-C就能插。MCP就是这个思路:不管你用什么AI客户端(Claude Desktop、Cursor、VS Code Copilot),不管你后端接什么工具(数据库、GitHub、Slack),只要两边都支持MCP,就能直接连上。
以前每换一个AI客户端,就得重新写一遍工具集成。现在写一次MCP Server,所有客户端都能用。
这玩意的重要性在于——微软、OpenAI、Google全部表态支持了。这不是Anthropic一家在玩,这是整个AI行业的共识方向。
和Function Calling有啥区别?
我知道你肯定在想:这不就是Function Calling换了个皮吗?
还真不是。核心区别在这:
Function Calling是"一对一"的——你给OpenAI的API定义了一组tools,换了Claude的API就得重新定义。每个AI模型有自己的一套tool schema格式,互相不通。
MCP是"多对多"的——你写一个MCP Server,暴露一组标准化的工具接口。任何MCP Client(Claude、Cursor、Gemini CLI、自定义Agent)都能自动发现并调用这些工具。Server和Client完全解耦。
Function Calling: AI模型 ←→ 你的代码(紧耦合)
MCP: AI客户端 ←→ MCP协议 ←→ MCP服务器(松耦合)
打个比方:Function Calling是你自己焊了一根充电线,只能充你的手机。MCP是一个USB-C标准,任何USB-C设备都能用。
实际开发中,这两种方式并不冲突——你的私有小工具用Function Calling就够了,跨团队跨项目的共享工具才值得上MCP。
开干:6步搭建你的第一个MCP Server
我花了整整一个周末才把这个流程跑通。官方文档写得很"优雅"——优雅到看不懂。下面是我踩完坑之后的版本。
第1步:环境准备
# 确保Node.js >= 18
node --version
# 创建项目目录
mkdir my-first-mcp-server && cd my-first-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
# 初始化TypeScript
npx tsc --init
对了,官方也有Python SDK,但TypeScript生态更成熟,社区里的MCP Server大部分都是TS写的。Python党别急,后面我也会提。
第2步:写服务器骨架
创建 src/index.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: "my-first-server",
version: "1.0.0",
});
// 注册一个工具
server.tool(
"get_weather", // 工具名称
"获取指定城市的天气信息", // 工具描述(AI靠这个理解工具用途)
{ city: z.string().describe("城市名") }, // 参数schema
async ({ city }) => {
// 这里写实际的业务逻辑
const temp = Math.floor(Math.random() * 35 + 5);
return {
content: [{ type: "text", text: `${city}当前温度:${temp}°C` }],
};
}
);
// 启动服务器
const transport = new StdioServerTransport();
await server.connect(transport);
这就是一个完整的MCP Server了。核心就三样东西:
server.tool()—— 注册工具z.object()—— 定义参数类型async callback—— 实现逻辑
第3步:配置编译
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"strict": true
},
"include": ["src/**/*"]
}
// package.json 加上
{
"scripts": {
"build": "tsc",
"start": "node build/index.js"
},
"type": "module"
}
npm run build
第一次编译大概率会报错——别慌,检查一下moduleResolution是不是设的Node16。这个坑我踩了半小时。
第4步:用Inspector调试
这是最爽的一步。Anthropic提供了一个可视化的调试工具:
npx @modelcontextprotocol/inspector node build/index.js
它会打开一个网页,你可以在里面直接测试你的工具——填参数、看返回值、调试错误。
这一步千万别跳过。 我之前直接配到Claude Desktop里,结果报错了完全不知道哪里出问题。Inspector能让你在本地先把逻辑跑通。
第5步:接到Claude Desktop
打开Claude Desktop的配置文件:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"my-first-server": {
"command": "node",
"args": ["/绝对路径/my-first-mcp-server/build/index.js"]
}
}
}
重启Claude Desktop,打开对话,点击右下角的🔧图标——如果能看到你的服务器和工具列表,就说明连上了。
第6步:试试效果
在Claude Desktop里输入:"北京今天天气怎么样?"
Claude会自动调用你的get_weather工具,传入city: "北京",然后把返回结果组织成自然语言回复给你。
注意这里的魔法——你从来没有告诉Claude"遇到天气问题就调用这个工具"。它靠工具的name和description自动判断什么时候该调用。所以工具的描述写得越清楚,AI调用就越准确。
进阶:做一个真正有用的Server
天气查询太玩具了。接下来我带你们做一个实际项目里能用的——GitHub代码库分析器。
server.tool(
"analyze_repo",
"分析GitHub仓库的代码统计信息",
{
owner: z.string().describe("仓库所有者"),
repo: z.string().describe("仓库名称"),
},
async ({ owner, repo }) => {
try {
const response = await fetch(
`https://api.github.com/repos/${owner}/${repo}`
);
if (!response.ok) {
return {
content: [{ type: "text", text: `❌ 仓库不存在或无法访问: ${response.status}` }],
isError: true,
};
}
const data = await response.json();
const summary = [
`📦 ${data.full_name}`,
`⭐ Stars: ${data.stargazers_count}`,
`🍴 Forks: ${data.forks_count}`,
`📝 语言: ${data.language || "未指定"}`,
`📋 License: ${data.license?.name || "无"}`,
`📊 Open Issues: ${data.open_issues_count}`,
`📅 最后更新: ${new Date(data.updated_at).toLocaleDateString()}`,
`📖 ${data.description || "无描述"}`,
].join("\n");
return {
content: [{ type: "text", text: summary }],
};
} catch (error) {
return {
content: [{ type: "text", text: `请求失败: ${error}` }],
isError: true,
};
}
}
);
这个工具不依赖任何数据库,直接调GitHub的公开API。在Claude Desktop里你可以问:"帮我分析一下facebook/react这个仓库",它就会调用这个工具帮你查。
MCP的三大核心概念
搞明白这三个概念,你就算真正理解MCP了:
Tools(工具)
就是上面演示的——让AI能执行动作。查询数据库、调API、读写文件,都算工具。
Resources(资源)
给AI提供数据源,但不执行动作。比如:
- 一个数据库表的schema
- 一份API文档
- 项目配置文件
server.resource(
"config",
"config://app",
async () => ({
contents: [{
uri: "config://app",
text: JSON.stringify({ version: "2.1", env: "production" }),
}],
})
);
Prompts(提示模板)
预定义的提示词模板,带参数:
server.prompt(
"code-review",
"代码审查模板",
{ code: z.string().describe("待审查的代码") },
async ({ code }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `请审查以下代码,关注安全性、性能和可维护性:\n\n${code}`,
},
}],
})
);
我踩过的5个大坑
坑1:ESM vs CJS,配置地狱
MCP SDK v1.x只支持ESM(ECMAScript Modules)。如果你的package.json里没有"type": "module",或者tsconfig.json的module设错了,编译能过但运行时报错。
解决: package.json必须加"type": "module",tsconfig.json用"module": "Node16"。
坑2:绝对路径和相对路径
Claude Desktop配置里args要用绝对路径。相对路径虽然不报错,但MCP Server会静默失败——就是连不上,也不告诉你为什么。
这个坑我卡了整整两小时。最后是在Claude Desktop的日志里(~/Library/Logs/Claude/)才发现的错误信息。
坑3:stderr是日志通道,别乱print
MCP协议通过stdout传输JSON-RPC消息。如果你在代码里用了console.log()调试,这些输出会被Claude Desktop当作协议消息解析,然后整个连接就炸了。
调试输出要用console.error()——stderr不会被MCP协议读取。
// ❌ 会破坏MCP通信
console.log("调试信息");
// ✅ 正确的调试方式
console.error("调试信息");
坑4:超时问题
MCP工具默认有30秒超时。如果你调的外部API比较慢(比如分析一个大的GitHub仓库),可能会被截断。
解决: 把耗时操作拆成异步的——先返回一个"正在处理",然后通过Resource或另一个Tool查结果。或者加缓存。
坑5:Schema不匹配
Zod schema定义和实际函数参数不一致的时候,MCP不会报编译错误——它会运行时静默失败。AI客户端调用你的工具,拿回一个莫名其妙的错误。
建议: 一定要在Inspector里手动测每一个工具,确认参数和返回值都对。
谁在用MCP?生态现状
截至2026年4月,MCP的生态已经相当丰富了:
官方/大厂出品:
- Docker MCP Toolkit —— 一键管理和运行MCP Server
- GitHub MCP Server —— 操作仓库、PR、Issue
- Microsoft Learn Docs —— 查Azure文档
- Playwright MCP —— AI操作浏览器做UI测试
- PostgreSQL MCP —— 直接查数据库
社区热门:
- Context7 —— 获取最新版本的库文档
- DuckDuckGo Search —— AI搜索
- Filesystem —— 安全的文件系统操作
- Notion MCP —— 读写Notion页面
有意思的是,连OpenAI都支持MCP了。他们虽然在2025年推了自己的tool calling格式,但在2026年也加上了MCP兼容层。毕竟,谁会跟行业标准对着干呢?
Python开发者怎么办?
如果你不想用TypeScript,Anthropic也提供了Python SDK:
pip install mcp
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-python-server")
@mcp.tool()
def get_weather(city: str) -> str:
"""获取指定城市的天气信息"""
temp = 15 # 假装在查API
return f"{city}当前温度:{temp}°C"
if __name__ == "__main__":
mcp.run()
比TypeScript版本简洁不少。但功能上两者完全对等,选哪个看你团队的栈。
最后说两句
MCP这个东西,目前还处在"早期基础设施"阶段。就像2010年你问"REST API有啥用",很多人觉得没必要——直接RPC不就完了?但现在REST已经是行业标准了。
MCP的定位是一样的:它是AI时代的标准接口协议。 现在可能觉得多此一举,等你的AI工具从1个变成10个,你就会感谢这个标准化。
对了,Anthropic官方有一个免费的MCP入门课程(在Skilljar上),涵盖Tools、Resources、Prompts三个核心概念,还有实操练习。比看文档强十倍,建议去看看。
你们有没有在项目里用过MCP?遇到什么问题?评论区聊聊,我最近在研究MCP + CI/CD的玩法,想看看有没有同好。