☰
MCP 协议实战(上):什么是 MCP,怎么跑起来
2026/9/29 3:55:59 网站建设 项目流程

1. 从一次“工具调用失败”说起:MCP 到底解决什么问题

如果你正在做 AI Agent 或者智能客服,大概率遇到过这种场景:模型能理解用户意图,也能生成看起来很专业的回答,但一到“真正去查数据、调接口”这一步就掉链子。比如用户问“帮我查一下北京今天的天气”,模型会回你一句“好的,我帮您查询”,然后……就没有然后了。它没有工具可用,只能靠训练数据里的旧信息编一个答案。

传统做法是用 Function Call:把工具定义写进每次请求的 prompt 里,模型返回一个函数名和参数,你的业务代码再去执行。这个方案能用,但问题也很明显——工具定义和业务代码强耦合,换一个模型就要重写一遍适配层,工具多了以后 prompt 会膨胀得厉害,维护成本直线上升。

MCP(Model Context Protocol)就是冲着这个痛点来的。你可以把它理解成大模型和外部工具之间的“通用 USB 接口”:工具提供方只需要写一个 MCP Server,所有支持 MCP 的 Host(比如 Claude Desktop、Cursor、各类 Agent 平台)都能直接发现并调用它,不用为每个模型单独适配。通信层统一走 JSON-RPC 2.0,工具发现走tools/list,工具执行走tools/call,整个链路是标准化的。

这篇是实战上篇,目标很明确:带你在本地 30 分钟内跑通第一个 MCP 服务。我会从 Python 环境准备讲起,给出可复制的config.toml和settings.json配置片段,演示一次完整的 JSON-RPC 调用与结果验证,最后把常见的报错逐个排查一遍。适合谁看?有 Python 基础、想搞明白 MCP 通信骨架、准备给自己的 Agent 接工具的开发者。下篇会讲怎么把这个本地 Server 接到真实模型上做 Function Call 触发。

2. 前置准备:Python 环境与 TaoToken 接入信息

动手之前先把环境理清楚。MCP 的 Python SDK 对版本有要求,建议 Python 3.10 及以上,3.11 体验最稳。我试过在 3.9 上跑,asyncio相关的类型标注会报错,别在这上面浪费时间。

# 建议用虚拟环境隔离,避免污染全局包 python -m venv mcp-env source mcp-env/bin/activate # Windows 用 mcp-env\Scripts\activate # 升级 pip 并安装 MCP SDK pip install --upgrade pip pip install "mcp[cli]"

装完之后验证一下:

python -c "import mcp; print(mcp.__version__)"

能打印出版本号就说明 SDK 就位了。mcp[cli]这个 extras 很关键,它会额外装上mcp命令行工具,后面本地调试和mcp dev都靠它。

接下来是模型侧的准备。本地 Server 跑通后,你需要一个能发起 Function Call 的模型端点来验证完整链路。我用的是 TaoToken 的 API 服务,它兼容 OpenAI 风格的接口,接入成本低。先去控制台创建一个 API Key:

  • 控制台入口:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
  • API Key 管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite

创建好 Key 之后先存到环境变量里,别硬编码进代码:

export TAOTOKEN_API_KEY="sk-你的key"

API 的基础地址是https://taotoken.net/api,注意这个地址后面不加任何查询参数,直接作为base_url使用即可。如果你对某个模型的调用方式不确定,可以先去模型对话页面手动试一条消息,确认 Key 和模型名都对得上:

  • 模型对话体验:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite

这一步别跳过。很多人后面调不通,根源就是 Key 没生效或者模型名写错,先在这里排除掉,能省下大量排查时间。

3. 可复制配置:config.toml 与 settings.json 片段

MCP 的配置分两块:一块是 Server 自己的运行参数(config.toml),一块是 Host 侧声明要连接哪些 Server(settings.json)。很多人第一次跑不通,就是没搞清楚这两个文件各管什么。

先看config.toml。这是给 MCP Server 用的,放在项目根目录:

# config.toml [server] name = "weather-server" version = "0.1.0" transport = "stdio" # 本地调试用 stdio,部署到远端再换 http [server.capabilities] tools = true # 声明本 Server 提供工具能力 resources = false # 暂不提供资源读取 prompts = false # 暂不提供提示模板 [logging] level = "INFO" # 调试阶段可以改成 DEBUG,看完整 JSON-RPC 报文

transport = "stdio"是本地跑的关键,Server 通过标准输入输出和 Host 通信,不需要开端口,最省事。capabilities里把tools打开,Host 才会去调tools/list发现你的工具。

再看settings.json。这是 Host 侧的配置,告诉它去哪里找你的 Server、怎么启动:

{ "mcpServers": { "weather": { "command": "python", "args": ["/absolute/path/to/weather_server.py"], "env": { "TAOTOKEN_API_KEY": "sk-你的key", "PYTHONUNBUFFERED": "1" } } } }

几个容易踩的坑提前说清楚。第一,args里的路径必须是绝对路径,相对路径在 Host 启动子进程时经常解析失败。第二,PYTHONUNBUFFERED=1建议加上,否则 stdout 有缓冲,JSON-RPC 的响应可能延迟到你怀疑人生。第三,env里传的 Key 是给 Server 内部调用模型或外部 API 用的,和 Host 自己的 Key 是两回事,别混。

如果你用的是 Claude Desktop 这类客户端,settings.json的位置通常在用户配置目录下;如果是自己写的 Host,就按你的加载逻辑放。配置改完记得重启 Host,热加载不一定生效。

4. 完整调用演示:从 tools/list 到 tools/call

配置就位,现在写 Server 本体。核心就三件事:创建 Server 实例、注册工具、启动 stdio 循环。下面这段可以直接复制运行:

# weather_server.py import asyncio import json from mcp.server import Server, NotificationOptions from mcp.server.models import InitializationCapabilities from mcp.server.stdio import stdio_server # 1. 创建 Server 实例,名字要和 config.toml 里一致 server = Server("weather-server") # 2. 注册工具清单:告诉 Host "我能干什么" @server.list_tools() async def list_tools(): return [ { "name": "get_weather", "description": "查询指定城市的实时天气", "inputSchema": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如 北京、上海" } }, "required": ["city"] } } ] # 3. 实现工具逻辑:Host 调 tools/call 时真正执行的部分 @server.call_tool() async def call_tool(name: str, arguments: dict): if name == "get_weather": city = arguments.get("city", "") # 真实项目里这里换成你的天气 API 调用 mock_data = { "北京": "晴,25°C,湿度 40%", "上海": "多云,28°C,湿度 65%", "深圳": "阵雨,30°C,湿度 80%" } result = mock_data.get(city, f"暂不支持查询 {city}") return {"content": [{"type": "text", "text": result}]} raise ValueError(f"未知工具: {name}") # 4. 启动 stdio 服务循环 async def main(): async with stdio_server() as (read_stream, write_stream): await server.run( read_stream, write_stream, InitializationCapabilities(sampling=None, experimental=None, roots=None), NotificationOptions() ) if __name__ == "__main__": asyncio.run(main())

代码分四段,对应 MCP 通信的四个阶段。Server("weather-server")是初始化握手时对外暴露的身份;list_tools()响应tools/list请求,返回工具清单;call_tool()响应tools/call请求,执行实际逻辑;stdio_server()负责把 JSON-RPC 报文从 stdin 读进来、把结果写到 stdout。

启动它:

python weather_server.py

进程会挂起等待输入,这是正常的,说明 stdio 循环起来了。现在手动发一条 JSON-RPC 请求验证。MCP 的报文格式是标准的 JSON-RPC 2.0,初始化请求长这样:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0"}}}

把这一行贴进终端回车,你会看到 Server 返回一条包含serverInfo和capabilities的响应。接着发工具发现请求:

{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}

响应里应该能看到get_weather的完整定义。最后发调用请求:

{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_weather","arguments":{"city":"北京"}}}

预期返回:

{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"晴,25°C,湿度 40%"}]}}

看到这条,恭喜,你的第一个 MCP 服务完整跑通了。从initialize握手到tools/list发现,再到tools/call执行,整条 JSON-RPC 链路验证完毕。如果想让 Host 自动完成这套流程,用mcp dev weather_server.py启动调试器,它会帮你把交互界面搭好。

5. 本篇常见报错排查

跑不通的时候别慌,MCP 的报错信息其实挺明确,按下面这张表对号入座基本能解决。

报错现象根本原因解决方式
ModuleNotFoundError: No module named 'mcp'虚拟环境没激活,或装到了全局确认which python指向 venv,重装pip install "mcp[cli]"
Host 启动后无响应args路径是相对路径改成绝对路径,pwd确认
tools/list返回空数组没加@server.list_tools()装饰器检查装饰器是否漏写,函数名不重要但装饰器必须在
调用后卡住不返回stdout 缓冲未关闭环境变量加PYTHONUNBUFFERED=1
Invalid JSON-RPC手动测试时多打了换行或空格一行一条报文,末尾回车即可,别加多余字符
KeyError: 'city'inputSchema里 required 和实际参数不匹配核对arguments的 key 和 schema 定义
模型不触发工具调用Host 没把工具清单传给模型确认capabilities.tools=true,且 Host 支持 MCP

重点说两个高频坑。第一个是路径问题,占了新手报错的一半以上。Host 启动 Server 是 fork 子进程,工作目录和你终端里不一样,相对路径必挂。第二个是缓冲问题,Python 默认对 stdout 做行缓冲,非交互场景下可能攒够一块才输出,Host 等不到响应就超时了。PYTHONUNBUFFERED=1是标准解法。

还有一个隐蔽的坑:inputSchema里required字段如果写了["city"],但call_tool里用arguments["city"]直接取,模型偶尔不传就会抛KeyError。稳妥写法是arguments.get("city", ""),再自己判断空值返回友好提示。工具的参数校验尽量在 Server 侧做全,别指望模型每次都传对。

如果排查完还是不通,建议把config.toml里的日志级别调到DEBUG,完整报文会打出来,对着 JSON-RPC 规范逐字段看,问题基本无所遁形。

6. 下一步:把本地 Server 接到真实模型

本地链路验证完,接下来就是让它真正被模型用起来。这一步的核心是把 MCP Server 注册到支持 MCP 的 Host 或 Agent 平台,让模型在 Function Call 触发时能自动发现并调用你的工具。如果你打算长期做编码类 Agent 或者需要频繁调试工具调用,可以考虑用 Coding Plan 来管理模型额度和调用配额,接入方式和普通 API 一致:

  • Coding Plan 入口:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite

接入文档里有完整的 MCP 对接说明和示例,包括 Host 侧配置、工具注册、调用链路验证,建议对照着过一遍:

  • 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite

如果你用的是 Claude Code 这类工具,它本身对 MCP 的支持比较完整,配置方式略有不同,可以参考专门的接入说明:

  • Claude Code 接入:https://taotoken.net/claudecode-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode-anthropic&utm_campaign=rewrite

下篇我会讲怎么把今天这个天气 Server 接到真实模型上,走一遍完整的 Function Call 触发链路,包括模型如何从工具清单里选工具、参数怎么传、多轮调用怎么处理。今天先把本地这 30 分钟跑通,把 JSON-RPC 的骨架吃透,后面接什么都快。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询