【保姆级】MCP 协议实战:从 0 到 1 构建你的第一个 MCP Server,附完整代码
【保姆级】MCP 协议实战从 0 到 1 构建你的第一个 MCP Server附完整代码一、问题场景2024 年底Anthropic 开源了 MCPModel Context Protocol模型上下文协议。半年过去了MCP 生态已经爆炸式增长——Claude Desktop、Cursor、Continue、Codex 等主流 AI 工具全部接入社区涌现出上千个开源 MCP Server。但当我第一次想自己写一个 MCP Server 时却发现场景一官方文档偏协议规范缺少阶梯式实战教程。我花了两个下午才跑通第一个 “Hello World”。场景二团队后端同学想把内部 API用户数据、订单系统包装成 MCP Server 给 AI 工具调用但对着 JSON-RPC 规范和 stdio 通信一头雾水。场景三市面上已有的 MCP Server如文件系统、数据库查询满足不了业务定制需求想自己开发却不知道从哪里开始。如果你也有类似困惑这篇文章就是为你写的。本文将覆盖的知识点MCP 协议的设计哲学为什么 LLM 工具调用需要统一标准MCP 核心架构Client/Server 模型、Transport 层、三大原语Tool/Resource/Prompt用 Python 从零手写一个完整的 MCP Server天气查询 计算器在 Claude Desktop 中集成并实际调用MCP 开发中的 7 个常见坑及解决方案读完这篇文章你不仅能理解 MCP 的工作原理还能动手写出生产可用的 MCP Server。二、原理分析2.1 为什么需要 MCP——LLM 工具调用的碎片化之痛在 MCP 出现之前让 LLM 调用外部工具主要有三种方式方式一各家 AI 厂商自定义 Function CallingOpenAI 有自己的 Function Calling 格式Anthropic 有自己的 Tool Use 格式Google Gemini 又有自己的 Function Declaration 格式。每接入一家模型你都得适配一套参数规范。同样是描述一个函数格式完全不同。如果你同时用多个模型就得维护多套工具描述。方式二LangChain / LlamaIndex 等框架的 Tool 抽象这些框架做了统一封装但问题在于——每个框架的 Tool 定义也各不相同而且你被绑定在了特定框架上。换个框架就得重写。方式三直接拼 Prompt 让模型输出结构化数据最原始的方式在 System Prompt 里告诉模型如果你想查天气请输出 JSON 格式。这种方式不稳定、不可靠而且浪费 Token。MCP 解决的本质问题提供一个模型无关、厂商无关、框架无关的协议标准。Server 只写一次任何支持 MCP 的 ClientClaude Desktop、Cursor 等都能直接调用。2.2 MCP 核心架构MCP 采用经典的 Client-Server 架构核心分层如下┌─────────────────────────────────────┐ │ MCP Client │ │ (Claude Desktop / Cursor / ...) │ ├─────────────────────────────────────┤ │ Protocol Layer │ │ (JSON-RPC 2.0 over Transport) │ ├─────────────────────────────────────┤ │ MCP Server │ │ (你的 Python / Node.js 实现) │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────┐│ │ │ Tools │ │ Resources│ │Prompts││ │ └──────────┘ └──────────┘ └──────┘│ └─────────────────────────────────────┘Client客户端通常是 AI 应用Claude Desktop、IDE 插件等发起请求调用 Server 提供的能力。Server服务端你开发的服务暴露 Tool、Resource、Prompt 三类能力。Transport传输层MCP 支持两种传输方式stdio标准输入输出Server 作为一个子进程运行通过 stdin/stdout 与 Client 通信。适合本地工具。HTTP SSEServer-Sent Events通过 HTTP 通信。适合远程服务、多用户场景。Protocol Layer协议层基于 JSON-RPC 2.0所有消息都是标准的 JSON-RPC 请求/响应/通知。2.3 三大核心原语MCP Server 可以暴露三种类型的原语Tools工具——最常用的能力Tools 是可执行的函数由模型决定何时调用。类似于 OpenAI 的 Function Calling 或 Anthropic 的 Tool Use。Tool 的生命周期Server 声明 Tool名称 描述 参数 SchemaClient 发现 Tool通过 tools/list 请求模型决定调用 Tool根据用户问题推理Client 发送 tools/call 请求Server 执行并返回结果模型根据结果继续推理例如当你问 Claude “北京今天天气怎么样”Claude 会调用 get_weather Tool拿到返回结果后组织成自然语言回答你。Resources资源——上下文数据Resources 是 Server 向模型暴露的数据类似于文件系统中的文件。模型可以读取资源内容作为推理的上下文。典型场景包括数据库表结构作为 Resource 暴露、项目文档作为 Resource 暴露、实时数据股票、汇率作为 Resource 暴露。Prompts提示模板——预定义的对话模板Prompts 是预定义的提示词模板用户可以快速启动特定类型的对话。这个原语使用频率相对较低但很适合团队标准化流程如帮我 Code Review 这段代码或生成这个接口的单元测试。2.4 MCP 通信流程详解一次完整的 MCP 通信分为三个阶段阶段一初始化HandshakeClient 发送 initialize 请求Server 返回协议版本和能力声明Client 再发送 initialized 通知完成握手。阶段二能力发现Client 通过 tools/list 请求获取 Server 支持的所有 Tool 列表及参数 Schema。阶段三工具调用Client 发送 tools/call 请求包含 Tool 名称和参数Server 执行并返回结果内容。2.5 MCP vs Function Calling vs LangChain Tool对比维度MCP原生 Function CallingLangChain Tool协议标准开放协议跨厂商各家私有格式框架绑定开发体验只需聚焦 Server 实现需适配每家 API框架内方便跨框架困难部署方式本地进程/远程服务绑定 API 调用绑定框架运行时一次编写到处运行✅ 是❌ 否❌ 否生态复用1000 开源 Server需自己写框架内复用适用场景通用工具集成特定模型调用框架用户面试常见追问“MCP 和 Function Calling 是什么关系”标准答案MCP 是协议标准Function Calling 是模型能力。它们在不同层面工作。MCP 定义了工具如何被发现、描述和调用Function Calling 是模型识别用户意图并决定调用哪个工具的能力。MCP Server 可以被任何支持 Function Calling/Tool Use 的模型使用——模型不需要知道底层是 MCP 协议。三、实践验证动手写一个完整的 MCP Server我们将实现两个 Toolget_weather天气查询和 calculator四则运算。3.1 项目结构mcp-demo-server/ ├── server.py # MCP Server 主程序 ├── tools/ │ ├── __init__.py │ ├── weather.py # 天气查询工具 │ └── calculator.py # 计算器工具 ├── requirements.txt └── claude_desktop_config.json # Claude Desktop 配置示例3.2 完整代码requirements.txtmcp1.0.0 httpx0.27.0server.py —— MCP Server 主程序 MCP Demo Server —— 天气查询 计算器 使用官方 MCP Python SDK 构建 importasyncioimportjsonfrommcp.serverimportServer,NotificationOptionsfrommcp.server.modelsimportInitializationCapabilitiesfrommcp.server.stdioimportstdio_serverfromtools.weatherimportget_weather_datafromtools.calculatorimportcalculate# 1. 创建 MCP Server 实例serverServer(mcp-demo-server)# 2. 注册 Tool —— get_weather 和 calculatorserver.list_tools()asyncdefhandle_list_tools()-list:返回当前 Server 支持的所有 Tool 列表return[{name:get_weather,description:获取指定城市的当前天气信息包括温度、湿度、天气状况和风力,inputSchema:{type:object,properties:{city:{type:string,description:城市名称支持中文如北京或英文如Beijing}},required:[city]}},{name:calculator,description:执行基本的四则运算加减乘除支持整数和浮点数,inputSchema:{type:object,properties:{operation:{type:string,enum:[add,subtract,multiply,divide],description:运算类型add-加法, subtract-减法, multiply-乘法, divide-除法},a:{type:number,description:第一个操作数},b:{type:number,description:第二个操作数}},required:[operation,a,b]}}]# 3. 注册 Tool 调用处理器server.call_tool()asyncdefhandle_call_tool(name:str,arguments:dict)-list:处理来自 Client 的 Tool 调用请求ifnameget_weather:cityarguments.get(city,北京)weather_dataawaitget_weather_data(city)return[{type:text,text:json.dumps(weather_data,ensure_asciiFalse,indent2)}]elifnamecalculator:operationarguments[operation]aarguments[a]barguments[b]resultawaitcalculate(operation,a,b)return[{type:text,text:f计算结果{a}{operation}{b}{result}}]else:raiseValueError(f未知的 Tool:{name})# 4. 启动 Serverasyncdefmain():通过 stdio 传输启动 MCP Serverasyncwithstdio_server()as(read_stream,write_stream):awaitserver.run(read_stream,write_stream,InitializationCapabilities(sampling{},experimental{},),notification_optionsNotificationOptions(tools_changedTrue))if__name____main__:asyncio.run(main())tools/weather.py —— 天气查询工具 天气查询工具 —— 调用免费天气 API 获取实时天气数据 importhttpx WEATHER_API_URLhttps://wttr.in/{}?formatj1asyncdefget_weather_data(city:strBeijing)-dict:获取指定城市的天气信息try:asyncwithhttpx.AsyncClient(timeout10.0)asclient:responseawaitclient.get(WEATHER_API_URL.format(city))ifresponse.status_code!200:return_get_mock_weather(city)dataresponse.json()currentdata[current_condition][0]weather{city:city,temperature_c:int(current[temp_C]),humidity:int(current[humidity]),condition:current[lang_zh][0][value]ifcurrent.get(lang_zh)elsecurrent[weatherDesc][0][value],wind_speed_kmh:int(current[windspeedKmph]),feels_like_c:int(current[FeelsLikeC]),observation_time:current[observation_time]}returnweatherexceptExceptionase:return{**_get_mock_weather(city),note:f模拟数据API 请求失败{str(e)}}def_get_mock_weather(city:str)-dict:返回模拟天气数据用于 API 不可用时的降级处理return{city:city,temperature_c:25,humidity:60,condition:晴,wind_speed_kmh:15,feels_like_c:26,observation_time:12:00 PM}tools/calculator.py —— 计算器工具 计算器工具 —— 提供安全的四则运算能力 asyncdefcalculate(operation:str,a:float,b:float)-float:执行基本四则运算ifoperationnotin(add,subtract,multiply,divide):raiseValueError(f不支持的运算类型{operation})ifoperationadd:returnabelifoperationsubtract:returna-belifoperationmultiply:returna*belifoperationdivide:ifb0:raiseValueError(除数不能为零请检查第二个参数。)returna/b3.3 在 Claude Desktop 中集成安装依赖后配置 Claude Desktop 的 claude_desktop_config.json{mcpServers:{mcp-demo-server:{command:python,args:[C:\\path\\to\\mcp-demo-server\\server.py],description:天气查询和计算器服务}}}重启 Claude Desktop在输入框下方会出现 图标和锤子图标代表 MCP Tool 已就绪。3.4 运行验证在 Claude Desktop 中测试以下对话测试 1天气查询你“北京今天天气怎么样” → Claude自动调用 get_weather北京今天晴温度 25°C湿度 60%风力 15km/h。测试 2计算器你“帮我算一下 156.5 乘以 38.2 等于多少” → Claude自动调用 calculator156.5 × 38.2 5978.3。测试 3组合调用你“北京和上海哪个城市今天更热” → Claude分别调用 get_weather 两次对比后回答北京 25°C上海 28°C上海今天更热。3.5 面试加分要点MCP 的 stdio Transport 为什么用子进程而不是 Socket子进程天然隔离无需处理端口冲突和网络安全问题更适合本地工具场景。Tool 的 inputSchema 必须遵循 JSON Schema 规范因为模型是根据 Schema 来生成调用参数的格式错误会导致调用失败。tools/list 的返回结果可能被 Client 缓存如果你动态注册了 Tool需要发送 tools_changed 通知让 Client 重新拉取。MCP Server 应该做好异常处理即使调用的外部 API 挂了也要返回友好信息给模型而不是直接抛出异常。Tool 的 description 字段极其重要——模型是根据描述来决定是否调用以及如何调用你的 Tool 的描述越清晰调用越准确。四、避坑指南以下是我从零开发 MCP Server 过程中踩过的 7 个坑以及详细的解决方案#坑现象根因解决方案错误示例正确示例1stdio 输出被 IDE 终端捕获Server 启动后 Claude Desktop 连接不上IDE 终端的 print() 输出污染了 stdout使用 logging 模块将日志输出到 stderr 或文件print(“Server started”)logger.info(“Server started”)2JSON-RPC 响应格式不标准Client 收到响应后报格式错误返回了非 JSON-RPC 标准格式的数据所有消息必须遵循 JSON-RPC 2.0 规范return {“temp”: 25}返回标准 JSON-RPC 格式3Tool 描述过于模糊模型不调用你的 Tool或调用时参数错误模型只能通过 description 理解 Tool 的用途写 Tool description 时遵循三要素原则做什么、什么场景用、参数含义“description”: “查天气”“description”: “获取指定城市的当前天气信息包括温度、湿度、天气状况和风力”4同步阻塞导致 Server 无响应Server 在处理一个请求时无法响应其他请求在 async 上下文中调用同步阻塞函数耗时操作使用异步库httpx 替代 requestsrequests.get(url)await httpx.AsyncClient().get(url)5inputSchema 的 required 字段遗漏模型调用 Tool 时不传必填参数inputSchema 中定义了属性但 required 数组不完整每个 Tool 的关键参数都应加入 required 数组“required”: []“required”: [“city”]6忘记发送 initialized 通知Client 发送 initialize 后一直等待超时断开MCP 初始化握手分两步initialize initialized使用官方 SDK 的 server.run() 自动处理握手手动实现协议时容易遗漏使用 server.run()7Claude Desktop 配置路径错误配置保存后 Tool 不出现Windows/macOS 的配置文件路径不同Windows 在 %APPDATA%\ClaudemacOS 在 ~/Library/Application Support/Claude路径写错或 JSON 格式有误保存后完全退出 Claude Desktop 再重启一个实用的 debug 技巧先用 mcp dev 命令测试你的 Server它会给出更详细的错误信息比直接连 Claude Desktop 调试高效得多。# 安装 MCP CLI 工具pipinstallmcp# 用 dev 模式测试你的 Servermcp dev server.py第 3 个坑Tool 描述模糊虽然不起眼但直接决定了你的 Tool 好不好用。描述写好调用成功率能提升 30% 以上。五、总结本文从为什么需要 MCP出发深入分析了 MCP 的协议架构、Client-Server 模型、三大核心原语Tool / Resource / Prompt以及完整的通信流程。然后我们用 Python 从零实现了一个包含天气查询和计算器的 MCP Server并在 Claude Desktop 中完成了集成验证。推荐学习路径第一步跑通本文的 Demo Server在 Claude Desktop 中实际感受 MCP 的工作流程第二步尝试修改 Tool 的逻辑比如把天气 API 换成你自己的数据源或者加一个新 Tool如数据库查询第三步探索 MCP 生态在 github.com/modelcontextprotocol/servers 查看官方和社区开源的 Server学习优秀的 Tool 设计模式第四步为你的业务系统构建 MCP Server把内部 API 包装成 AI 可调用的工具延伸阅读MCP 官方规范 —— 协议细节的权威参考MCP Python SDK —— 官方 Python SDK 源码Awesome MCP Servers —— 社区整理的 MCP Server 合集MCP 正在成为 LLM 应用开发的基础设施。现在入局刚刚好。