MCP这个缩写,最近在圈子里出现的频率越来越高。不管是mcp server、mcp协议还是tool calling,大家讨论的核心其实就是一件事:怎么让大模型稳定、标准化地调用外部工具和数据。我过去半年把好几个内部系统陆续接进了MCP,也基于官方SDK写过不少MCP Server和MCP Tool,从TypeScript到Java都踩过一轮坑。这篇文章会把整体思路、协议核心概念、代码套路和排查经验整理出来,既适合第一次接触MCP的开发者建立全局认知,也适合准备把已有REST接口改造成MCP Tool的团队直接拿来参考。
我想先说清楚一件事:MCP不是某个模型厂商的私有方案,而是一个开放的协议层。它解决的问题不是“能不能调工具”,而是“工具该怎么接入、怎么描述、怎么被模型安全可靠地使用”。如果你厌倦了为每个AI平台单独写一套Function Calling适配层,那这篇文章应该能帮你省下不少时间。
1. MCP协议到底在解决什么问题
1.1 从“一工具一接口”到“AI世界的USB-C”
在MCP出现之前,给AI接工具是一件相当折腾的事。OpenAI有Function Calling,Anthropic有Tool Use,各家Agent框架还有自己的Plugin机制。工具方如果想同时接入多个平台,就得为每个平台分别写一遍参数格式、认证方式和调用逻辑。表面看是重复劳动,实际上维护成本才是大头——上游接口一变,下游所有适配层都要跟着改。
MCP(Model Context Protocol)的思路是把工具接入变成一套公共标准。它由Anthropic在2024年11月开源,后来被移交到Linux基金会管理。协议定义了客户端(Client,也就是Claude Desktop、Cursor、Codex这类AI应用)和服务器(Server,也就是提供工具与数据的能力方)之间的通信方式。客户端负责把模型请求翻译成MCP调用,服务端负责执行工具并返回结果,双方都只需要遵守协议,不再需要关心对方实现细节。
用USB-C来类比特别直观。以前各种设备都有自己的充电口,现在统一成USB-C之后,一条线能跑数据也能供电。MCP就是AI世界的USB-C,它把“模型如何连接外部能力”这件事标准化了。工具接入方只要开发一个MCP Server,理论上就可以被任何支持MCP的客户端复用。
1.2 三个核心原语:Tools、Resources、Prompts
MCP协议定义了三个核心原语,理解它们比背协议字段重要得多。
Tools是最常用也最容易被理解的:一个可以被模型调用的函数。它有一个名字、一段描述、一个JSON Schema格式的入参定义。模型拿到这些信息后会决定“要不要调、传什么参数”,最终由客户端发起tools/call请求,服务端执行并返回结构化结果。比如一个“查询天气”的Tool,入参是城市名,输出是温度和天气状况。
Resources对应的是数据资源,它的定位更像“读文件”。通过类似file://的URI来标识数据,比如一个项目的README、一份数据库查询结果、一张配置表。模型可以主动读取这些资源来补充上下文,但资源本身不触发外部动作。这个设计和Tool的差异很重要:Tool是动作,Resource是数据。
Prompts则是一套可复用的提示词模板。服务端定义好模板和参数,客户端渲染后塞给模型。典型场景是“按周报格式整理工作日志”这类固定结构任务。这三个原语合在一起,覆盖了模型在真实业务里的大部分需要:读数据、调工具、按模板执行。
1.3 一条请求从模型到工具的完整旅程
实际跑起来,一次MCP调用的链路很清晰。
首先是初始化阶段。客户端向服务端发initialize请求,携带协议版本和客户端能力信息;服务端返回自己支持的协议版本、Server信息和能力列表。这一步相当于双方握手,决定后续通信规则。握手成功后,客户端发送notifications/initialized通知,告知服务端初始化完成。
接着是能力发现。客户端请求tools/list,服务端返回当前暴露的所有Tool清单,包括名称、描述和参数Schema。模型根据这些描述决定调用哪个工具。
最后是执行阶段。模型生成工具调用指令,客户端组装成tools/call请求发给服务端。服务端根据参数执行业务逻辑,返回content数组和结构化结果。整个过程基于JSON-RPC 2.0消息格式,传输层既可以是标准输入输出,也可以是HTTP,但消息本身的结构是固定的。
这里有一个经常被忽略的细节:服务端返回给模型的内容,会被当作模型对话上下文的一部分再次进入模型。这也就意味着返回内容不宜过长、不宜冗余,否则会浪费上下文窗口还可能干扰模型后续判断。
1.4 别再混淆:MCP、Function Calling、Agent Skill的区别
网上讨论MCP时,最常混在一起比较的就是Function Calling和Agent Skill,我梳理一下自己的理解。
Function Calling本质上是模型API的一种能力开关。模型在推理时输出一个结构化的工具调用意图,平台负责把它组装成可执行的请求。它解决的是“模型怎么表达调用意图”的问题,属于模型侧能力。
MCP是工程层面的标准化封装。它不关心模型内部怎么推理,只负责把工具接入方式统一。你可以理解为,Function Calling是“模型说我要调这个工具”,MCP是“工具到底长什么样、参数怎么定义、结果怎么传”,后者是前者之上的工程约定。
Agent Skill则更偏应用层。Skill一般指Agent的技能单元,它可能包含一段Prompt、一组执行步骤、一个决策逻辑,甚至绑定多个底层Tool。它解决的是“任务怎么拆解、流程怎么编排”的问题。很多人问agent skill和mcp有什么区别,我的答案是:一个管“怎么用得好”,一个管“怎么连得上”,两者是互补关系而不是替代关系。一个Agent可以把MCP Tool当作底层执行器,再通过Skill来编排它们。
| 概念 | 解决的问题 | 层级 |
|---|---|---|
| Function Calling | 模型如何表达调用意图 | 模型API能力 |
| MCP | 工具如何标准化接入 | 协议与工程层 |
| Agent Skill | 任务如何拆解和编排 | 应用编排层 |
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. MCP Server/Tool开发前你必须想清楚的四件事
2.1 语言与SDK选型:TypeScript、Python还是Java
MCP官方SDK覆盖了TypeScript、Python、Java、C#、Kotlin、Swift,基本主流后端语言都有支持。但生态成熟度和上手体验差异比较大,我实际用下来最顺的还是TypeScript和Python。
TypeScript SDK(@modelcontextprotocol/sdk)示例最多、更新最快,社区里很多MCP Server都是用TS写的。它的优势在于可以直接嵌入Electron、VS Code、Claude Desktop这类前端生态,很多桌面级MCP客户端本身就是JS/TS技术栈。如果你要做的Server主要是给本地开发工具用的,TS是默认选择。
Python SDK在AI数据类场景更强。如果你的Tool要串联Pandas、Pytorch或者LangChain,用Python更顺手。比如开发一个数据分析类的MCP Server,直接在Python进程里完成数据清洗和计算,比把结果序列化给TS进程再处理要高效得多。
Java的情况比较特殊。虽然官方有Java SDK,但真正让JAVA圈火起来的是一批Spring Boot MCP Starter,比如Spring AI Alibaba的MCP Server。它们提供了注解式的开发体验,你只要在方法上加一个@Tool注解,框架自动把它暴露成MCP工具。这对于已经有Spring Boot服务的团队来说,把REST接口发布为MCP几乎是零成本的。我后面会单独用一节演示这种写法。
选型建议很直白:面向桌面工具选TS,面向数据处理选Python,企业后端已有Java服务就选Java Starter。
2.2 传输方式怎么选:stdio还是Streamable HTTP
MCP的传输层选择,直接决定了Server的部署形态和使用场景。
stdio模式是最早也是最常见的模式。客户端以子进程方式启动Server,通过标准输入输出通信。这个模式的优势是简单安全,不需要起HTTP端口,不用处理跨域和鉴权,数据在进程内管道流动,天然适合本地单机场景。Claude Desktop、Cursor的本地MCP配置基本都是stdio模式。缺点是只能跑在客户端所在机器上,无法远程调用,也无法被多个客户端同时连接。
Streamable HTTP是MCP在2025-03-26协议版本中正式标准化的远程传输方式。Server暴露一个HTTP端点,客户端通过POST请求发送MCP消息,服务端可以返回单次响应或事件流。这个模式解决了远程调用和多人共享的问题。部署一个MCP Server到服务器上,团队成员都能通过HTTP访问,这就把MCP从“本机玩具”升级成了“团队基础设施”。
选型时我通常遵循一条判断:如果Server只在某个人的本地环境用,stdio足够了;只要涉及远程、共享、多端复用,就必须上HTTP。没有任何场景需要你纠结“哪个好”,只有“哪个符合部署边界”。
2.3 Tool的JSON Schema设计:决定模型会不会用错
很多人开发MCP Server时,把精力全放在业务逻辑上,参数描述随便写写,然后抱怨“模型调用工具总是传错参数”。这口锅一半得由参数Schema来背。
模型的工具调用不是写代码,它是在“理解”工具描述后做出的选择。参数的description就是给模型看的说明书,写得越清楚,模型传参越准。举个例子,一个查询天气的Tool,如果city参数只写“城市”,模型可能传“北京”也可能传“beijing”或“Beijing”;如果description写“城市名称,中文全称,例如:北京、上海”,模型的准确率会明显提升。
还有两个细节值得注意。一是能用枚举就用枚举,把可选值限定住,模型就不会自由发挥。二是入参结构不要设计得太深,嵌套三层以上模型很容易“迷路”。我发现扁平化的参数结构配合清晰描述,实际效果远好于复杂嵌套。最后加一个additionalProperties: false,避免模型塞进来无法识别的字段。
2.4 权限、鉴权和“危险工具”的底线
MCP工具一旦上线,就是被模型自动调用的。这意味着一个执行数据库删除的Tool,理论上模型只要判断“应该删”就会触发。所以权限设计必须前置,不能靠事后补救。
我的建议是三条原则。第一,能只读就别写。查询、搜索、读取这类的Tool可以大胆暴露,涉及写操作的Tool一定要想清楚是否真的需要模型自动执行。第二,危险操作加确认机制。比如“发送邮件”和“删除文件”这类工具,服务端侧必须增加二次确认或权限校验,不要只靠客户端的用户确认弹窗。第三,鉴权信息一律走环境变量或密钥管理,禁止硬编码在代码和配置里。
实际部署HTTP模式的MCP Server时,我习惯用Bearer Token做最基本的鉴权,再在网络层增加IP白名单。这些不是MCP协议强制要求的,但对生产环境来说,是底线。
3. 手把手实现一个MCP Server:把REST接口发布为MCP
3.1 场景与目标:封装一个实时天气查询工具
纸上谈兵没有意义,我拿一个非常常见的场景来做演示:假设你有一个REST接口,比如GET /api/weather?city=北京,它返回当前城市的天气和空气质量指数。现在你想让Claude Desktop、Codex或者Cursor里的AI模型能直接调用这个接口,就用它开发一个MCP Server。
目标很清晰:把天气查询REST接口包装成一个MCP Tool,让模型输入城市名就能拿到结构化天气数据。
顺带说一句,这个套路几乎可以平移到任何REST接口上。我后来把内部的订单查询、用户检索、知识库搜索都做成了MCP Tool,代码骨架完全一样,变的只是业务逻辑和参数定义。
3.2 TypeScript + stdio 最小实现
先建一个空的Node项目,安装SDK和一个参数校验库:
bash复制npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx
然后写核心的Server代码。这里我用TypeScript,传inputSchema用zod来定义,SDK会自动转成标准Schema:
typescript复制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-aqi-server",
version: "1.0.0",
});
server.tool(
"get_weather",
{
city: z.string().describe("城市名称,中文全称,例如:北京、上海"),
},
async (params) => {
const city = encodeURIComponent(params.city);
const url = `https://api.example.com/api/weather?city=${city}`;
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
});
if (!res.ok) {
return {
content: [{ type: "text", text: `请求失败: HTTP ${res.status}` }],
};
}
const data = await res.json();
return {
content: [{ type: "text", text: JSON.stringify(data) }],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
这段代码的逻辑很简单:注册一个叫get_weather的Tool,入参只允许一个city字段,执行时调用REST接口,最后把返回结果序列化为text类型的content。
编译并运行:
bash复制npx tsc
node dist/index.js
在stdio模式下,直接启动不会有任何输出,因为它在等待客户端通过stdin发消息。这是正常的,不要以为程序卡死了。要验证它是否正确,需要配合MCP客户端或者MCP Inspector。
3.3 客户端接入:Claude Desktop、Codex、Cursor里怎么配置
写好的MCP Server怎么让模型用起来?关键一步是在客户端注册。
以Claude Desktop为例。在Claude的配置文件中注册一个mcpServers项:
json复制{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/path/to/weather-server/dist/index.js"]
}
}
}
macOS用户配置在~/Library/Application Support/Claude/claude_desktop_config.json,Windows用户在%APPDATA%\Claude\claude_desktop_config.json。保存后重启Claude Desktop就能在工具列表里看到这个Server。
Codex的接入方式更现代一些,可以直接用命令行管理:
bash复制codex mcp add weather -- node /path/to/weather-server/dist/index.js
也可以手动修改~/.codex/config.toml,把它加进mcp_servers段。Cursor的话,在Settings > Features > MCP中Add,选择stdio类型,填上启动命令。
配置完成后务必注意:每次改Server代码都要重新构建,而且客户端需要重新加载或重启才能拿到最新的Tool定义。我踩过好多次“改了代码没重启客户端”的坑,结果模型一直用旧工具。
3.4 用Java/Spring Boot把现有REST接口变成MCP工具
如果你团队的技术栈是Java,那么最省力的方案是使用Spring AI Alibaba的MCP Server Starter。它把MCP Server开发做成了纯注解方式,全部代码加起来可能不到30行。
pom.xml里引入依赖后,写一个普通的Spring组件:
java复制@Component
public class WeatherToolService {
@Tool(name = "get_weather", description = "根据城市名称查询当前天气")
public String getWeather(String city) {
// 复用已有的WeatherService
WeatherDTO result = weatherService.query(city);
return JSON.toJSONString(result);
}
}
启动Spring Boot应用后,框架会自动扫描带@Tool注解的方法,把它们注册为MCP的工具,并暴露对应的HTTP端点。也就是说,你不需要手写JSON-RPC通信、不需要保持Server进程,MCP Server直接跟着Spring容器走了。
这个方案的实际价值在于:企业里大量内部系统已经基于Spring Boot,如果已经能用REST调用的能力,增加MCP暴露只是加个注解的事,不需要另起一套服务。这种“REST接口发布为MCP”的方式,是目前Java生态最主流的一种玩法。
3.5 Docker部署MCP Server:从本地脚本到服务化
本地跑通的Server如果要共享给团队或部署到服务器,我建议直接容器化。
以HTTP模式的MCP Server为例,一个典型的Dockerfile长这样:
dockerfile复制FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY dist ./dist
EXPOSE 3001
ENV API_TOKEN=${API_TOKEN}
CMD ["node", "dist/index.js"]
构建并运行:
bash复制docker build -t weather-mcp .
docker run -d -p 3001:3001 -e API_TOKEN=xxx --name weather-mcp weather-mcp
如果你用的是stdio模式,Docker里会有个麻烦:客户端通常需要直接在本地启动子进程,而stdio模式跨容器通信不好处理。所以Docker化部署时,我强烈建议Server暴露Streamable HTTP端点,客户端只要填一个URL就能对接。
部署环境上,Linux和Windows都能跑。只是如果目标机器是Windows Server或CentOS这类环境,记得先把防火墙和端口占用问题解决掉。我之前在Windows Server上部署Node Server,遇到过启动时报“登录失败”的坑,后来发现是服务尝试绑定已经被占用的端口,换个端口就好了,和MCP本身没什么关系,但排查起来确实容易绕弯路。
4. 生态、实战案例与问题排查实录
4.1 生态里那些“开箱即用”的MCP Server
MCP生态现在已经相当繁荣,很多常用工具都有人做了开源Server。比如Figma MCP,它把Figma设计稿信息暴露给AI,模型可以直接读取图层的结构、样式和标注,设计师做设计转代码时非常好用。Blender MCP则是给3D建模工具加上了AI控制通道,模型能通过MCP指令操作Blender场景。还有安全测试工具Yakit、Burpsuite也都有对应的MCP集成,让AI辅助做接口测试和分析。
这些生态项目给我的启示是:MCP Server的开发模式已经趋于成熟,很多Server就是“一个工具包装层”,核心逻辑仍然是调用已有的SDK或API。你自己写一个MCP Server并不难,难的是有没有找到用户真正需要的工具场景。
有一点提示给想开源MCP Server的同学:Gitee和GitHub上都能看到大量MCP Server项目,但质量参差不齐。选型时看三点:是否标明协议版本、示例是否可运行、最近的更新时间。三个月没更新的项目大概率已经跟不上客户端协议演进。
4.2 MCP Inspector:调试MCP Server的瑞士军刀
MCP Server开发过程中,调试是最容易被忽视的环节。stdio模式下你不知道进程内部发生了什么,报错也看不到,这时候MCP官方提供的Inspector就是神器。
一条命令就能启动:
bash复制npx @modelcontextprotocol/inspector node dist/index.js
Inspector会启动一个本地可视化页面,它能展示tools/list返回的全部工具定义,手动模拟tools/call调用,还能查看原始JSON-RPC消息。我习惯先用它做三件事:确认工具列表是否正确暴露、检查参数Schema是否符合预期、手动执行一次调用看返回结果结构。
很多时候模型“表现不好”并不是模型的问题,而是工具定义有误或返回结构不清晰。在让模型背锅之前,先用Inspector把工具链路验证一遍,这是最效率的排查顺序。
4.3 高频问题排查速查表
我整理了自己实际开发中遇到的高频问题,做成了一张速查表,方便你直接对照。
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 客户端看不到任何工具 | 服务端未正确暴露或客户端未重载 | 用Inspector验证tools/list;重启客户端 |
| 调用返回超时 | 上游REST接口慢或依赖未就绪 | 检查网络、超时时间、重试机制 |
| 返回内容被模型误解 | 返回结构不清晰或文本过长 | 精简返回内容,用结构化JSON |
| Schema校验报错 | 参数定义与实际调用不一致 | 检查zod定义和客户端缓存 |
| 模型自动工具选择报错 | 模型服务未开启工具调用能力 | 检查启动参数enable-auto-tool-choice和tool-call-parser |
| 底层模型进程退出导致500 | 显存不足或子进程崩溃 | 检查资源占用,重启模型服务 |
| Windows服务启动登录失败 | 端口被占用或权限不足 | 换端口、以管理员身份运行 |
这里特别展开两处。如果你在本地用llama-server跑模型,接到上层MCP Server后遇到类似auto “tool choice” requires --enable-auto-tool-choice and --tool-call-parser的报错,说明模型服务端根本没有开启工具调用相关能力,关键是要在llama-server启动参数里同时加上这两个开关,光改MCP端配置解决不了问题。如果上层返回500且日志里写着llama-server process has terminated,一般不是MCP代码的问题,先看GPU显存和进程存活状态。
4.4 多客户端复用、版本升级与协议兼容性
MCP协议迭代速度是很快的。从2024-11-05版本到2025-03-26版本,传输层和工具注解都有变化。客户端和服务端的协议版本不一致时,大多数情况下通过initialize握手协商可以兼容,但如果客户端比较老而Server用了很新的特性,就可能出现工具调用失败。
我建议团队维护MCP Server时固定SDK版本,不要盲目跟着升级。升级前先看Changelog,确认没有破坏性变更。另外,如果同一个Server要同时被Claude Desktop、Codex、Cursor等多个客户端使用,尽量保持Server无状态。无状态意味着每个请求独立处理,不依赖上次调用的内存状态,这样在不同客户端之间切换时才不会出现“上个会话的东西带到下次调用”的诡异问题。
这里可以展开说一下多客户端复用的价值。你把一个Server部署成HTTP模式后,团队里不同角色可以各用各的客户端去接同一套工具。运营同学用Claude Desktop查数据,开发同学用Cursor写代码时调同一套内部工具,这就是MCP在团队里真正发挥作用的地方。
最后再分享一点个人体会。开发MCP Server这半年,我最深的感受是:MCP本质上就是把工具接入问题做成了一道填空题,协议框架给你了,工具描述给你了,参数Schema给你了,剩下的就是专心把业务逻辑写干净。而真正拉开差距的,恰恰是那些协议之外的东西——参数描述写得够不够清楚、返回结构是不是足够简洁、权限边界是不是严格。反正我踩过的坑,大多是栽在“工具定义含糊”和“上线前没做权限控制”上。如果你准备开始做自己的MCP Server,建议先从一个只读查询工具入手,跑通协议链路后再逐步加复杂逻辑,这条路最稳。
