MCP服务原生调用-从本地工具到全世界
上篇你学会了工程化管理多个工具——注册表模式让新增工具只需一行。但你的工具是自己写的,别人的工具呢?这篇讲 MCP——一套标准协议,让任何 AI 宿主都能使用任何 MCP 服务,不限编程语言,不限产品。
小明的新困惑
小明用注册表模式把自己的工具管理得井井有条。但上周接了个需求,让他头疼了——
客户说:”我们的 AI 助手能不能查全球航班?”
小明想,自己写航班搜索 API?Kiwi.com 的接口得翻文档、处理请求限流、搞定缓存……光是搞清楚价格规则就要花一周。
他去问老张。
老张说:”你不需要自己写。Kiwi.com 已经把自己的航班搜索能力封装成了 MCP 服务,你直接用就行。”
“MCP?什么东西?”
“Model Context Protocol,模型上下文协议。 就像 USB 接口一样——任何设备插上就能用。MCP 就是工具世界的 USB。”
小明问:”这和 Function Calling 有什么区别?”
“问得好。Function Calling 和 MCP 解决的是不同层面的问题。”
Function Calling vs MCP:不是一个东西
老张在白板上画了张对比表:
| Function Calling | MCP | |
|---|---|---|
| 干什么 | 模型如何结构化地决定「调用哪个函数」 | 工具如何以标准方式暴露给任意 AI 宿主 |
| 工具放哪 | Agent 进程内的 Python 函数 | 独立 MCP Server 进程 |
| 谁管理工具 | 你自己的 main.py |
Cursor / Claude Desktop 等 Host |
| 跨产品复用 | 通常要重写适配代码 | 一份 Server,多个 Host 通用 |
| 协议层级 | OpenAI API 层面的约定 | 独立于任何模型的开放标准 |
“一个类比——”
“Function Calling 解决的是模型内部的问题:模型收到用户消息后,怎么决定要不要调函数、调哪个。MCP 解决的是系统间的问题:无论你用 Cursor、Claude Desktop 还是自己写的 Agent,都能用同一套协议发现和调用工具。”
小明说:”所以 MCP 是在 Function Calling 之上加了一层——把’工具’抽象成了独立服务?”
“完全正确。你之前写的 Function Calling 工具,本质上是 Agent 进程里的一个 Python 函数。MCP 把它变成了一个独立进程,通过标准协议来通信。”
MCP 的三个角色
老张画了张图:
| 角色 | 是什么 | 本课中的实体 |
|---|---|---|
| Host | 管对话 UI、调模型、决定何时用工具 | Cursor / Claude Desktop |
| Client | 与 Server 通信:握手、列出工具、调用工具 | Host 内置的 MCP Client |
| Server | 声明并执行工具 | 你写的 server.py 或外部的 Kiwi.com MCP |
“Host 是用户直接用的产品,Client 是 Host 内部的通信模块,Server 是独立运行的工具提供方。三者各干各的活,通过标准协议串起来。“
两种传输方式:stdio 和 HTTP
老张说:”MCP 支持两种传输方式,对应两种场景:”
| 传输方式 | 原理 | 适用场景 |
|---|---|---|
| stdio | Host 启动子进程,通过 stdin/stdout 传 JSON-RPC | 本地工具(自己写的 Server) |
| HTTP | 直连公网 HTTP 端点 | 外部服务(Kiwi 航班搜索等) |
stdio:本地工具
关键规则:stdout 走协议,stderr 走日志。 如果你在 Server 里写 print("hello"),它打到 stdout 上,Host 解析 JSON-RPC 时会直接崩溃。调试日志必须打到 stderr:
HTTP:外部服务
“stdio 适合你自己写的本地工具,HTTP 适合别人提供的、部署在云端的工具。Kiwi.com 的航班搜索就是一个标准的 HTTP MCP 服务——你不需要装任何东西,连上就能用。”
实战一:写一个最小 MCP Server
老张说:”先写一个最简单的 MCP Server,感受一下。用 FastMCP 框架,核心就三步——建 Server → 注册 Tool → 以 stdio 运行。”
“注意几个细节:”
| 要点 | 说明 |
|---|---|
| 类型注解 | a: float、b: float 会自动生成工具的 JSON Schema |
| docstring | 就是工具的 description,模型靠它判断何时调用 |
| FastMCP 名字 | 会出现在 Cursor 的 MCP 面板,起个有意义的名字 |
| transport=”stdio” | 走 stdin/stdout,适配所有本地 Host |
“对比你之前写 Function Calling 工具的代码——之前你要手写 JSON Schema、手动维护 available_functions 映射表、亲自管理 messages 列表。MCP Server 里这些全省了——框架自动处理。”
小明说:”看着比 Function Calling 的写法还简单?”
“是的。因为 MCP Server 不关心对话编排——编排是 Host 的事。Server 只管一件事:声明工具 + 执行工具。调用时机、参数提取、结果展示,全是 Host 替你干的。”
接入 Cursor
老张说:”Server 写好了,接上 Cursor 跑起来。在 Cursor 的 MCP 配置里加一段:”
“保存后,Cursor 会自动启动子进程运行你的 server.py。在 MCP 面板看到绿灯和 add、get_time 两个工具,就说明接入成功了。”
小明在 Cursor 对话里试了试:
“这就通了。一句话不用改,工具自动注册到 Cursor 里了。”
实战二:原生 Client 调用外部 HTTP MCP
小明又问:”自己写的 Server 搞懂了。Kiwi.com 那个外部 MCP 服务怎么用?”
“外部 HTTP MCP 服务不需要你起 Server——人家已经在公网上跑了。你需要的是一个 MCP Client,去连它、调它的工具。”
老张打开了他写的代码——直接使用 MCP 官方 SDK,不依赖 LangChain 等框架:
小明盯着代码看了半分钟:”这三步……不就是连上、问有什么工具、调工具吗?”
“对,和 USB 的流程一模一样——插上、枚举设备、收发数据。initialize 是握手,list_tools 是枚举设备,call_tool 是读写数据。”
逐段拆解
第一步:建立连接
老张说:”streamable_http_client 是 MCP SDK 提供的连接工厂。它用 httpx 建立一个到 https://mcp.kiwi.com 的 HTTP 长连接,返回三个东西:”
| 返回值 | 类型 | 作用 |
|---|---|---|
read |
异步流 | 从 Server 读取消息 |
write |
异步流 | 向 Server 发送消息 |
_get_session_id |
函数 | 获取会话 ID(调试用) |
“read 和 write 就是你和 Server 之间的通信通道。后面所有交互都通过这两个对象。”
“注意:streamable_http_client 用的是流式 HTTP(SSE),不是一问一答的短连接。Client 保持连接,随时接收 Server 推送的消息。”
第二步:握手
“ClientSession 用 read 和 write 创建会话。initialize() 执行握手——Client 告诉 Server 自己支持的协议版本和能力,Server 回应自己的信息和可用能力。”
“握手成功后,session 就是你和这个 MCP Server 的交互入口。此后一切操作都通过它。”
第三步a:列出工具
“list_tools() 返回这个 MCP Server 提供的所有工具列表。每个工具包含 name、description、inputSchema(JSON Schema 参数定义)。”
“对于 Kiwi MCP Server,输出类似:”
“你先看一眼有什么工具,确认工具名和参数格式——search-flight 的日期格式是 dd/mm/yyyy,不是 ISO 格式,不是美式 mm/dd/yyyy。直接用,别猜。“
第三步b:调用工具
“call_tool 是最核心的调用——传入工具名和参数,拿到结果。”
“注意 result.content 的结构——它不是简单的字符串,而是一个 content 列表,每个元素有 type 字段:”
| content.type | 含义 |
|---|---|
"text" |
文本,通过 .text 获取 |
"image" |
图片(base64),通过 .data + .mimeType 获取 |
"resource" |
资源引用,通过 .resource 获取 |
“所以获取文本结果的标准写法是:”
“不要直接 result.content[0].text——如果 Server 返回的第一个 content 不是 text 类型,你就炸了。”
小明说:”原来 MCP 不只是文本,还能返回图片和资源?”
“对。MCP 协议不只管 Tool,还管 Resource(文件、图片等数据)和 Prompt(模板化的提示词)。但入门阶段先搞懂 Tool 调用就够了。”
辅助函数:压缩结果
老张补充道:”航班搜索可能返回几百条结果,直接 print 会刷屏。实际代码里加了个 _summarize_search 函数:”
“核心逻辑——把原始 JSON 转成可读的文本摘要,只展示前几条,其余省略。这其实就是你之后在 Agent 里做的事——把工具返回的原始数据,整理成用户可以看的信息。“
stdio vs HTTP:怎么选
老张在白板上总结了两种传输方式的对比:
| 对比维度 | stdio | HTTP(Streamable) |
|---|---|---|
| 通信方式 | 子进程 stdin/stdout | HTTP 长连接(SSE) |
| 启动方式 | Host 拉起 Python 进程 | Client 连接公网 URL |
| 适用场景 | 本地工具、自己写的 Server | 第三方服务、云端部署的 Server |
| 依赖 | Python 环境 + 依赖包 | 只需要网络 + httpx |
| Server 在哪 | 你的机器上 | 远程服务器 |
| 配置复杂度 | ⭐⭐(要配 python 路径) | ⭐(一个 URL) |
| 典型用例 | 本地文件操作、数据库查询 | Kiwi 航班搜索、天气 API |
“一句话——自己写的工具用 stdio,别人的服务用 HTTP。“
原生 SDK vs 封装框架
小明问:”我看到 LangChain 也有 MCP Client——MultiServerMCPClient,你们为什么不用它?”
老张说:”LangChain 的封装确实方便——几行配置就能接入多个 MCP Server。但它加了一层抽象,当出问题时你很难定位是哪一层的问题。”
“对比两种写法:”
| LangChain 封装 | 原生 MCP SDK | |
|---|---|---|
| 连接代码 | 配置文件 + 3 行 | streamable_http_client + ClientSession |
| 理解成本 | 低(蒙在鼓里) | 中(看清全貌) |
| 调试难度 | 高(封装层太多) | 低(每个环节可打断点) |
| 定位问题 | 框架内部 → 难 | 直接看 SDK 调用 → 易 |
“LangChain 封装版:”
“原生版:”
“原生版多了几行,但每一步你都知道在干什么。initialize 是什么?握手。list_tools 是什么?列出工具。出了问题你知道该查哪一步。”
选择建议:
| 场景 | 推荐 |
|---|---|
| 快速验证、原型开发 | LangChain 封装 |
| 生产环境、需要精细控制 | 原生 MCP SDK |
| 学习中、想理解原理 | 原生 MCP SDK |
完整调用流程总结
老张在白板上画了整个流程的时序图:
“全程就四步——连接、握手、枚举、调用。和 Function Calling 的两轮对话比,MCP 多了枚举工具这一步。因为 MCP Server 的工具列表不是你在代码里定义的,而是 Server 动态告诉你的。”
小明恍然:”所以 list_tools() 就像 Function Calling 里你手动写的 tools 列表——只是 MCP 里这步是自动的、动态的!”
“对。还有一个区别——MCP 里你不需要管 messages 列表、不需要做两轮 API 调用。这些是 Host(Cursor 等)的事。你作为 Client,只需要调工具、拿结果。”
五个必踩的坑
坑一:stdout 日志搞坏协议
“print() 默认进 stdout,而 stdio 模式下 stdout 是协议通道。任何非 JSON-RPC 的输出都会破坏协议。”
坑二:路径配置错误
Cursor 配 MCP 时最常见的错误——路径写错了:
“Windows 上用正斜杠 / 或双反斜杠 \\ 都行,JSON 必须合法。”
坑三:工具名写错
“永远先 list_tools() 看一眼,拿到真实的工具名再调用。别靠猜。”
坑四:参数格式不对
Kiwi MCP 的日期格式是 dd/mm/yyyy,不是 Python 默认的 ISO 格式:
“每个 MCP Server 有自己的参数契约——list_tools() 返回的 inputSchema 里有 description,仔细看。Kiwi 的描述里写了日期格式是 dd/mm/yyyy。”
坑五:没看 result.isError
“MCP 的 call_tool 不会抛异常——即使失败也返回结果,只是 isError 为 True。不检查 isError 就把错误当成正常数据往下走,后面可能会出莫名其妙的问题。”
源码全景
老张把完整的客户端代码铺开,让小明对照着刚才的拆解看:
运行输出:
“从连接到拿到航班数据,核心代码不到 30 行。”
和 Function Calling 的关系
老张说:”最后帮你理清 MCP 和 Function Calling 的关系。它们不是竞争关系,是分工协作。”
“看清楚没?Function Calling 管「模型如何决定调哪个工具」,MCP 管「工具如何被调用」。Cursor 内部用 Function Calling 机制让模型选工具,通过 MCP Client 去实际调用工具。两层协作。”
| 层 | 协议/机制 | 解决的问题 |
|---|---|---|
| 模型层 | Function Calling | 模型怎么选工具、怎么填参数 |
| 传输层 | MCP | 工具怎么暴露、怎么跨进程通信 |
“你刚入门时可以不管 MCP——Function Calling 够用了。但当你需要用的工具不是自己写的、而是别人提供的——或者你想让别人用你的工具——MCP 就是唯一标准答案。”
总结
老张在白板上写了最后一句话:
Function Calling 让你写出能调工具的程序。MCP 让你调的工具能被任何程序用。
小明把整个笔记整理成了速查表:
| 概念 | 一句话 |
|---|---|
| MCP 是什么 | 标准化工具暴露协议,不限语言、不限产品 |
| Host / Client / Server | 宿主 / 通信模块 / 工具提供方 |
| stdio 传输 | stdout=协议通道,stderr=调试日志,print 只打 stderr |
| HTTP 传输 | 连公网 URL,不需要本地起进程 |
| FastMCP Server | 三步:FastMCP() + @mcp.tool() + mcp.run(transport="stdio") |
| 原生 Client | 四步:streamable_http_client → ClientSession → initialize → call_tool |
result.content |
列表结构,先判断 type 再取 .text,不要盲取 [0] |
result.isError |
调用可能成功但返回错误,必须检查 |
| 和 FC 的关系 | FC 管模型决策,MCP 管工具暴露;两层协作,各司其职 |
“从写第一个 @mcp.tool() 到调外部 HTTP MCP,核心就这点东西。剩下的——Resources、Prompts、多 Server 管理——都是在这个基础上做加法。”
小明问:”那我现在写的那些 Function Calling 工具,要不要改成 MCP?”
“看场景。”老张说,”如果只有你一个人用、只有一个 Agent 项目——Function Calling 够用。但如果你的工具会被多个项目复用、或者你想让别人接你的工具——上 MCP。MCP 解决的是’复用’问题,不是’功能’问题。“
“就像你写了个计算器函数——自己用就放项目里,想给别人用就打成 pip 包。MCP 就是工具界的打包发布协议。”
转载请注明来源:MCP服务原生调用-从本地工具到全世界




























