MCP服务原生调用-从本地工具到全世界

    |     2026年8月17日   |   agent技术, AI大模型应用   |     0 条评论   |    4

上篇你学会了工程化管理多个工具——注册表模式让新增工具只需一行。但你的工具是自己写的,别人的工具呢?这篇讲 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       ≈ 所有餐厅挂同一套菜单系统  ← 换一家餐厅(Host)也能点

“Function Calling 解决的是模型内部的问题:模型收到用户消息后,怎么决定要不要调函数、调哪个。MCP 解决的是系统间的问题:无论你用 Cursor、Claude Desktop 还是自己写的 Agent,都能用同一套协议发现和调用工具。”

小明说:”所以 MCP 是在 Function Calling 之上加了一层——把’工具’抽象成了独立服务?”

“完全正确。你之前写的 Function Calling 工具,本质上是 Agent 进程里的一个 Python 函数。MCP 把它变成了一个独立进程,通过标准协议来通信。”


MCP 的三个角色

老张画了张图:

┌─────────────────────────────────────────────────────┐
│            MCP 架构             
│                             
│  用户 ──→ Host(Cursor/Claude)           
│                          
│     ┌──────┴──────┐                
│     │        │                
│    大模型    MCP Client(内置)          
│                             
│         ┌────┴────┐              
│         │  JSON-RPC │  ← stdio / HTTP    
│         └────┬────┘              
│                            
│         MCP Server(独立进程)          
│         ├── tool: 查天气            
│         ├── tool: 搜航班           
│         └── tool: 算汇率            
└─────────────────────────────────────────────────────┘
角色 是什么 本课中的实体
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:本地工具

Host(Cursor)启动子进程
  │
  python server.py   ← 你写的 Server
  │
  stdin/stdout  ←  协议通道(JSON-RPC)
  stderr     ←  调试日志(人类可读)

关键规则:stdout 走协议,stderr 走日志。 如果你在 Server 里写 print("hello"),它打到 stdout 上,Host 解析 JSON-RPC 时会直接崩溃。调试日志必须打到 stderr:

import sys
print("Server started", file=sys.stderr)   # ✅ 正确
print("Server started")           # ❌ 会搞坏协议

HTTP:外部服务

Host(Cursor)发起 HTTP 请求
  │
  https://mcp.kiwi.com   ← 别人部署的公网 MCP Server
  │
  JSON-RPC over HTTP
  │
  返回航班数据

“stdio 适合你自己写的本地工具,HTTP 适合别人提供的、部署在云端的工具。Kiwi.com 的航班搜索就是一个标准的 HTTP MCP 服务——你不需要装任何东西,连上就能用。”


实战一:写一个最小 MCP Server

老张说:”先写一个最简单的 MCP Server,感受一下。用 FastMCP 框架,核心就三步——建 Server → 注册 Tool → 以 stdio 运行。”

from mcp.server.fastmcp import FastMCP

# 1. 创建 Server(名字会显示在 Host 的 MCP 面板里)
mcp = FastMCP("L1-HelloStdio")

# 2. 注册工具:用 @mcp.tool() 装饰器
@mcp.tool()
def add(a: float, b: float) -> float:
"""Add two numbers and return the sum."""
return a + b

@mcp.tool()
def get_time() -> str:
"""Get the current time in ISO 8601 format."""
from datetime import datetime
return datetime.now().isoformat()

# 3. 以 stdio 模式启动
if __name__ == "__main__":
  mcp.run(transport="stdio")

“注意几个细节:”

要点 说明
类型注解 a: floatb: 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 配置里加一段:”

{
"mcpServers": {
"course-mcp-l1": {
"command": "python",
"args": ["D:/your-path/server.py"]
}
}
}

“保存后,Cursor 会自动启动子进程运行你的 server.py。在 MCP 面板看到绿灯和 addget_time 两个工具,就说明接入成功了。”

小明在 Cursor 对话里试了试:

问:用工具算 12.5 + 7.5
答:12.5 + 7.5 = 20.0

“这就通了。一句话不用改,工具自动注册到 Cursor 里了。”


实战二:原生 Client 调用外部 HTTP MCP

小明又问:”自己写的 Server 搞懂了。Kiwi.com 那个外部 MCP 服务怎么用?”

“外部 HTTP MCP 服务不需要你起 Server——人家已经在公网上跑了。你需要的是一个 MCP Client,去连它、调它的工具。”

老张打开了他写的代码——直接使用 MCP 官方 SDK,不依赖 LangChain 等框架:

import asyncio
import json
from datetime import date, timedelta

import httpx
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client

KIWI_MCP_URL = "https://mcp.kiwi.com"


async def main():
# 构造查询参数:查两周后从布拉格到伦敦的航班
  departure = (date.today() + timedelta(days=14)).strftime("%d/%m/%Y")
  args = {
"flyFrom""PRG",
"flyTo""LON",
"departureDate": departure,
"adults"1,
"currency""EUR",
"sort""price",
  }

# === 三步:连接 → 握手 → 调用 ===

# 第1步:建立 HTTP 连接
async with httpx.AsyncClient(timeout=90.0as http:
async with streamable_http_client(KIWI_MCP_URL, http_client=http) as (
      read, write, _get_session_id,
    ):
# 第2步:创建会话并握手
async with ClientSession(read, write) as session:
await session.initialize()

# 第3步a:列出所有可用工具
        tools = await session.list_tools()
for t in tools.tools:
print(f"  {t.name}{t.description[:60]}...")

# 第3步b:调用具体工具
        result = await session.call_tool("search-flight", args)

# 解析结果(content 是列表,每个元素有 .text)
        texts = [c.text for c in result.content if c.type == "text"]
print(texts[0])


asyncio.run(main())

小明盯着代码看了半分钟:”这三步……不就是连上、问有什么工具、调工具吗?”

“对,和 USB 的流程一模一样——插上、枚举设备、收发数据initialize 是握手,list_tools 是枚举设备,call_tool 是读写数据。”


逐段拆解

第一步:建立连接

async with httpx.AsyncClient(timeout=90.0as http:
async with streamable_http_client(KIWI_MCP_URL, http_client=http) as (
    read, write, _get_session_id,
  ):

老张说:”streamable_http_client 是 MCP SDK 提供的连接工厂。它用 httpx 建立一个到 https://mcp.kiwi.com 的 HTTP 长连接,返回三个东西:”

返回值 类型 作用
read 异步流 从 Server 读取消息
write 异步流 向 Server 发送消息
_get_session_id 函数 获取会话 ID(调试用)

readwrite 就是你和 Server 之间的通信通道。后面所有交互都通过这两个对象。”

“注意:streamable_http_client 用的是流式 HTTP(SSE),不是一问一答的短连接。Client 保持连接,随时接收 Server 推送的消息。”

第二步:握手

async with ClientSession(read, write) as session:
await session.initialize()

ClientSessionreadwrite 创建会话。initialize() 执行握手——Client 告诉 Server 自己支持的协议版本和能力,Server 回应自己的信息和可用能力。”

“握手成功后,session 就是你和这个 MCP Server 的交互入口。此后一切操作都通过它。”

第三步a:列出工具

tools = await session.list_tools()
for t in tools.tools:
print(f"  {t.name}{t.description[:60]}...")

list_tools() 返回这个 MCP Server 提供的所有工具列表。每个工具包含 namedescriptioninputSchema(JSON Schema 参数定义)。”

“对于 Kiwi MCP Server,输出类似:”

  search-flight: Search for the cheapest flights and return...
  search-multi-city: Search for multi-city flights...

“你先看一眼有什么工具,确认工具名和参数格式——search-flight 的日期格式是 dd/mm/yyyy,不是 ISO 格式,不是美式 mm/dd/yyyy直接用,别猜。

第三步b:调用工具

args = {
"flyFrom""PRG",
"flyTo""LON",
"departureDate""15/08/2026",
"adults"1,
"currency""EUR",
"sort""price",
}

result = await session.call_tool("search-flight", args)
texts = [c.text for c in result.content if c.type == "text"]

call_tool 是最核心的调用——传入工具名和参数,拿到结果。”

“注意 result.content 的结构——它不是简单的字符串,而是一个 content 列表,每个元素有 type 字段:”

content.type 含义
"text" 文本,通过 .text 获取
"image" 图片(base64),通过 .data + .mimeType 获取
"resource" 资源引用,通过 .resource 获取

“所以获取文本结果的标准写法是:”

texts = [c.text for c in result.content if c.type == "text"]

“不要直接 result.content[0].text——如果 Server 返回的第一个 content 不是 text 类型,你就炸了。”

小明说:”原来 MCP 不只是文本,还能返回图片和资源?”

“对。MCP 协议不只管 Tool,还管 Resource(文件、图片等数据)和 Prompt(模板化的提示词)。但入门阶段先搞懂 Tool 调用就够了。”


辅助函数:压缩结果

老张补充道:”航班搜索可能返回几百条结果,直接 print 会刷屏。实际代码里加了个 _summarize_search 函数:”

def _summarize_search(raw: str, limit: int = 3) -> str:
  data = json.loads(raw)
  itineraries = data.get("itineraries"or []
  lines = [f"currency={data.get('currency')}  resultsCount={data.get('resultsCount')}"]
for i, it in enumerate(itineraries[:limit], 1):
    outbound = it.get("outbound"or {}
    route = " -> ".join(outbound.get("route"or [])
    lines.append(
f"  [{i}{it.get('priceFormatted')}  {route}  "
f"{outbound.get('departureTime')} -> {outbound.get('arrivalTime')}"
    )
return "\n".join(lines)

“核心逻辑——把原始 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 封装版:”

from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient({
"kiwi-com-flight-search": {
"transport""http",
"url""https://mcp.kiwi.com",
  }
})
tools = await client.get_tools()  # 一行搞定

“原生版:”

from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client

async with streamable_http_client(url) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
    tools = await session.list_tools()

“原生版多了几行,但每一步你都知道在干什么。initialize 是什么?握手。list_tools 是什么?列出工具。出了问题你知道该查哪一步。”

选择建议

场景 推荐
快速验证、原型开发 LangChain 封装
生产环境、需要精细控制 原生 MCP SDK
学习中、想理解原理 原生 MCP SDK

完整调用流程总结

老张在白板上画了整个流程的时序图:

┌──────┐    ┌──────────┐    ┌──────────────────┐
│ 你的  │    │  MCP   │    │  外部 MCP Server  │
│ Client│    │  Client  │    │  (kiwi.com)    │
│ (脚本)│    │ (SDK)  │    │          │
└──┬───┘    └────┬─────┘    └────────┬─────────┘
   │                     
   │  连上 URL               
   │──────────────→│   HTTP 握手      
   │         │─────────────────────→│
   │         │←─────────────────────│
   │                     
   │  initialize()             
   │──────────────→│  JSON-RPC initialize 
   │         │─────────────────────→│
   │         │←── Server 能力声明 ──│
   │                     
   │  list_tools() │            
   │──────────────→│  JSON-RPC list_tools │
   │         │─────────────────────→│
   │         │←── 工具列表 ─────────│
   │         │  [search-flight, ...]│
   │                     
   │  call_tool()             
   │──────────────→│  JSON-RPC call_tool  
   │         │─────────────────────→│
   │         │←── 航班数据 ─────────│
   │                     
   │  解析结果              
   │  展示航班             

“全程就四步——连接、握手、枚举、调用。和 Function Calling 的两轮对话比,MCP 多了枚举工具这一步。因为 MCP Server 的工具列表不是你在代码里定义的,而是 Server 动态告诉你的。”

小明恍然:”所以 list_tools() 就像 Function Calling 里你手动写的 tools 列表——只是 MCP 里这步是自动的、动态的!”

“对。还有一个区别——MCP 里你不需要管 messages 列表、不需要做两轮 API 调用。这些是 Host(Cursor 等)的事。你作为 Client,只需要调工具、拿结果。”


五个必踩的坑

坑一:stdout 日志搞坏协议

# ❌ 错误
print("Server started")   # 打到 stdout,Host 崩溃

# ✅ 正确
import sys
print("Server started", file=sys.stderr)

print() 默认进 stdout,而 stdio 模式下 stdout 是协议通道。任何非 JSON-RPC 的输出都会破坏协议。”

坑二:路径配置错误

Cursor 配 MCP 时最常见的错误——路径写错了:

// ❌ 相对路径(Cursor 不知道相对于哪)
{"args": ["server.py"]}

// ✅ 绝对路径
{"args": ["D:/my-project/server.py"]}

“Windows 上用正斜杠 / 或双反斜杠 \\ 都行,JSON 必须合法。”

坑三:工具名写错

# ❌ list_tools 返回的是 "search-flight",你写成了 "search_flight"
result = await session.call_tool("search_flight", args)
# → Error: Tool not found

永远先 list_tools() 看一眼,拿到真实的工具名再调用。别靠猜。”

坑四:参数格式不对

Kiwi MCP 的日期格式是 dd/mm/yyyy,不是 Python 默认的 ISO 格式:

# ❌ ISO 格式(Server 不认)
departure = date.today().isoformat()   # "2026-08-23"

# ✅ dd/mm/yyyy(Server 的契约)
departure = date.today().strftime("%d/%m/%Y")   # "23/08/2026"

“每个 MCP Server 有自己的参数契约——list_tools() 返回的 inputSchema 里有 description,仔细看。Kiwi 的描述里写了日期格式是 dd/mm/yyyy。”

坑五:没看 result.isError

result = await session.call_tool("search-flight", args)
if result.isError:
print("调用失败:", result.content[0].text)
return

“MCP 的 call_tool 不会抛异常——即使失败也返回结果,只是 isErrorTrue。不检查 isError 就把错误当成正常数据往下走,后面可能会出莫名其妙的问题。”


源码全景

老张把完整的客户端代码铺开,让小明对照着刚才的拆解看:

"""
原生 MCP SDK 调用外部 HTTP MCP 服务示例
连接 Kiwi.com 航班搜索 MCP Server
"""

import asyncio
import json
from datetime import date, timedelta

import httpx
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client

KIWI_MCP_URL = "https://mcp.kiwi.com"


def _text_parts(result):
"""提取结果中的文本内容"""
return [c.text for c in result.content if c.type == "text"]


async def main():
# 查询参数
  departure = (date.today() + timedelta(days=14)).strftime("%d/%m/%Y")
  args = {
"flyFrom""PRG",      # 出发地(布拉格)
"flyTo""LON",      # 目的地(伦敦)
"departureDate": departure, # 日期 dd/mm/yyyy
"adults"1,
"currency""EUR",
"sort""price",
  }

print(f"Connecting to {KIWI_MCP_URL}...")

# === 连接 → 握手 → 调用 ===
async with httpx.AsyncClient(timeout=90.0as http:
async with streamable_http_client(KIWI_MCP_URL, http_client=http) as (
      read, write, _,
    ):
async with ClientSession(read, write) as session:
# 握手
await session.initialize()
print("Handshake OK")

# 列出工具
        tools = await session.list_tools()
print(f"Available tools: {sorted(t.name for t in tools.tools)}")

# 调用航班搜索
print(f"Calling search-flight with args={args}")
        result = await session.call_tool("search-flight", args)
        texts = _text_parts(result)

if result.isError:
print(f"Error: {texts[0]}")
return

print("Results:", texts[0][:500])


if __name__ == "__main__":
  asyncio.run(main())

运行输出:

Connecting to https://mcp.kiwi.com...
Handshake OK
Available tools: ['search-flight', 'search-multi-city']
Calling search-flight with args={'flyFrom': 'PRG', ...}
Results: {"currency":"EUR","resultsCount":15,"itineraries":[...]}

“从连接到拿到航班数据,核心代码不到 30 行。”


和 Function Calling 的关系

老张说:”最后帮你理清 MCP 和 Function Calling 的关系。它们不是竞争关系,是分工协作。”

用户:「帮我查从成都到东京最便宜的航班」
  │
  ▼
Cursor(Host)
  │ 用大模型分析意图
  │ 大模型说:「需要调 search-flight 工具」
  │
  ▼
Cursor 的 MCP Client
  │ 通过 HTTP 连到 Kiwi.com MCP Server
  │ call_tool("search-flight", {...})
  ▼
Kiwi.com MCP Server
  │ 返回航班数据
  ▼
Cursor(Host)
  │ 把航班数据喂给大模型
  │ 大模型基于真实数据生成回答
  ▼
「成都到东京最便宜的航班是 XX 航空,1380元……」

“看清楚没?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_clientClientSessioninitializecall_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服务原生调用-从本地工具到全世界
本文链接地址:https://ai.zhousir.top/?p=3634

上一篇:

没有了

已经是最新文章
回复 取消