MCP笔记:从JSON-RPC到Function Calling,一次讲透Model Context Protocol原理

发布时间:2026/10/3 11:51:40
MCP笔记:从JSON-RPC到Function Calling,一次讲透Model Context Protocol原理
1. 从一次“工具调用失败”说起MCP 到底解决了什么问题如果你最近在折腾 AI Agent大概率遇到过这种场景给模型接了一个查天气的接口代码里写死了函数名和参数格式跑起来没问题过两天想换成另一个模型或者想再加一个查数据库的工具结果发现整套调用逻辑要重写一遍。Function Calling 本身不难难的是每接一个模型、每加一个数据源都要重新对齐一遍参数结构。这就是 MCPModel Context Protocol模型上下文协议想解决的核心问题。你可以把它理解成 AI 世界里的 USB-C 接口以前每个设备都有自己的充电口现在统一成一个标准谁都能插。MCP 定义的是大模型应用和外部工具、数据源之间的通信规范让 MCP Server 提供能力MCP Client 按统一格式调用模型换不换、工具加不加协议层不用动。这篇文章面向第一次接触 MCP 的开发者从最底层的 JSON-RPC 通信切入把 MCP 和 Function Calling 的区别讲清楚再给出一份本地 MCP Server 的最小可运行配置和一次完整的请求-响应验证。读完你应该能建立对 MCP 协议栈的直观认知知道一条tools/call消息从发出到返回中间到底发生了什么。先说结论MCP 不是替代 Function Calling而是把“工具怎么描述、怎么被发现、怎么被调用”这件事标准化了。Function Calling 解决的是“模型决定调哪个函数”MCP 解决的是“这个函数从哪来、长什么样、怎么被统一管理”。两者在 AI Agent 里是配合关系不是竞争关系。2. JSON-RPC 通信层MCP 协议栈的地基MCP 所有消息都走 JSON-RPC 2.0 格式这是理解整个协议的关键。JSON-RPC 是一种轻量级远程调用协议请求和响应都是 JSON 对象核心字段就四个jsonrpc固定为2.0method是方法名params是参数id用来匹配请求和响应。通知类消息没有id因为不需要回复。MCP 支持两种传输方式。本地通信用 stdio也就是标准输入输出Client 和 Server 在同一台机器上通过管道传 JSON 行。远程通信用 SSE 加 HTTP适合跨网络访问。不管哪种传输消息体都是 JSON-RPC这一点不变。我试过用一个中间代理脚本把 stdio 上的原始消息全部打出来这样能直接看到协议层在干什么。代理的逻辑很简单拦截 Client 发给 Server 的 stdin转发给真正的 Server 进程同时把 Server 的 stdout 转发回 Client两边都写日志。核心代码结构是这样import subprocess, threading, sys def forward_and_log(src, dst, log_file, prefix): while True: line src.readline() if not line: break log_file.write(f{prefix}: {line.decode(utf-8, errorsreplace)}) log_file.flush() dst.write(line) dst.flush() process subprocess.Popen( target_command, stdinsubprocess.PIPE, stdoutsubprocess.PIPE, stderrsubprocess.PIPE, bufsize0 ) threading.Thread(targetforward_and_log, args(sys.stdin.buffer, process.stdin, log_f, 输入), daemonTrue).start() threading.Thread(targetforward_and_log, args(process.stdout, sys.stdout.buffer, log_f, 输出), daemonTrue).start() process.wait()跑起来之后日志里会看到一条完整的连接建立过程。第一步是 Client 发initialize{method:initialize,params:{protocolVersion:2025-03-26,capabilities:{},clientInfo:{name:Cline,version:3.17.11}},jsonrpc:2.0,id:0}Server 回一条带serverInfo和capabilities的响应声明自己支持哪些能力。接着 Client 发一个notifications/initialized通知没有id表示握手完成。然后 Client 发tools/list查询工具列表Server 返回所有注册的工具及其inputSchema。这一串消息就是 MCP 的“发现阶段”Client 通过它知道 Server 能干什么。这里有个容易忽略的点tools/list返回的inputSchema是标准 JSON Schema描述了每个工具的参数类型和必填项。模型拿到这个 schema 之后才能生成符合格式的调用参数。所以 MCP 的标准化不只是传输格式统一连工具描述的结构也统一了这才是它比裸写 Function Calling 更省事的地方。3. 最小可运行配置本地 MCP Server 接入实操理解了通信层接下来动手跑一个。我用 Python 的mcp库写一个查天气的 Server核心是用FastMCP注册工具。先装依赖pip install mcp httpx然后写 Server 代码关键是mcp.tool()装饰器它会把函数的名称、参数类型、docstring 自动转成tools/list里的 schemafrom mcp.server.fastmcp import FastMCP import httpx mcp FastMCP(weather, log_levelERROR) NWS_API_BASE https://api.weather.gov mcp.tool() async def get_alerts(state: str) - str: Get weather alerts for a US state. Args: state: Two-letter US state code (eg: CA, NY) url f{NWS_API_BASE}/alerts/active/area/{state} async with httpx.AsyncClient() as client: resp await client.get(url, headers{User-Agent: weather-app/1.0}) data resp.json() if not data.get(features): return No active alerts. return \n---\n.join( f{f[properties].get(event)}: {f[properties].get(areaDesc)} for f in data[features] ) if __name__ __main__: mcp.run(transportstdio)transportstdio表示走本地标准输入输出这是本地 MCP Server 最常用的方式。接下来是 Client 侧的配置。不同 Host 的配置文件位置不一样以 Cline 为例在 MCP 设置里加一段 JSON{ mcpServers: { weather: { command: python, args: [/path/to/weather_server.py], env: {} } } }如果你用的是 Claude Code 或 Codex 这类工具配置思路一样都是指定启动命令和参数。这里要强调三件套Base URL、Key、Model ID。MCP Server 本身不涉及模型调用但 Host 在把工具结果喂给模型时需要这三项才能完成一次完整的 Agent 循环。如果你用的是 TaoToken 这类统一接入层Base URL 填https://taotoken.net/apiKey 在控制台生成Model ID 按你选的模型填。配置片段长这样{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: claude-sonnet-4-20250514 }把 Server 路径、启动命令、模型接入信息都对齐之后Host 启动时会自动拉起 MCP Server 进程完成 initialize 握手然后tools/list拿到get_alerts这个工具。整个过程不需要你手写任何 Function Calling 的 schema装饰器已经帮你生成了。4. 验证请求从 tools/call 到成功返回配置好之后怎么确认真的通了最直接的办法是看日志。在 Host 里发一句“查一下 CA 州的天气警报”Host 会先让模型决定调哪个工具模型返回工具名和参数Host 再通过 MCP 发tools/call{method:tools/call,params:{name:get_alerts,arguments:{state:CA}},jsonrpc:2.0,id:4}Server 收到后执行get_alerts(CA)把结果包在content数组里返回{jsonrpc:2.0,id:4,result:{content:[{type:text,text:Heat Advisory: Southern California}],isError:false}}Host 拿到这段 text作为上下文再喂给模型模型生成最终的自然语言回复。这就是一次完整的 Agent 工具调用闭环。注意id字段请求和响应必须匹配JSON-RPC 靠它来对应异步消息。如果你想脱离 Host 单独验证 Server可以用mcp库自带的客户端或者直接手写 JSON-RPC 消息通过管道喂给 Server。手动验证的好处是能精确控制每一步看到原始报文。比如先发 initialize再发 initialized 通知再发 tools/list最后发 tools/call每一步的响应都能打印出来。实测下来只要 initialize 的protocolVersion和 Server 声明的一致后续调用基本不会出问题。验证成功的标志有三个tools/list能返回你注册的工具tools/call的isError为 false返回的content里有实际数据。三个都满足说明 MCP 链路是通的。5. 常见报错排查401、local proxy failed 与 OAuth 问题接入过程中最容易卡住的几个报错我按出现频率排一下。第一个是401 Unauthorized。这个通常不是 MCP Server 本身的问题而是 Host 在调用模型时 Key 不对或过期。检查你的 API Key 是否填在正确的位置Base URL 有没有多写或少写/v1。如果你用的是统一接入层确认 Key 是在对应控制台生成的且账户有余额。401 的排查顺序是先确认 Key 有效再确认 Base URL 拼写最后看请求头里的Authorization格式是不是Bearer sk-xxx。第二个是local proxy failed或MCP server failed to start。这个多半是 Server 启动命令或路径写错了。检查command字段是不是可执行文件的全路径args里的脚本路径是不是绝对路径。Python 环境的话确认python命令在 Host 的运行环境里能找到必要时用虚拟环境的绝对路径。还有一种情况是 Server 启动后立刻退出通常是依赖没装全手动在终端跑一遍启动命令就能看到真实报错。第三个是reading choices相关的解析错误。这个出现在模型返回格式不符合预期时比如模型没有按 Function Calling 格式返回工具调用而是返回了一段自然语言。原因可能是模型不支持 Function Calling或者 Host 和模型的交互格式不匹配。前面提过Cline 用 XML 和模型沟通Cherry Studio 用 Function Calling 格式不同 Host 对模型的输出格式要求不一样。遇到这个报错先确认你选的模型支持工具调用再检查 Host 的模型配置是否和实际模型匹配。第四个是 OAuth 授权失败。远程 MCP Server 如果走 SSE 加 HTTP可能需要 OAuth 认证。报错通常是invalid_token或unauthorized_client。排查时确认回调地址配置正确token 没有过期scope 包含所需权限。本地 stdio 的 Server 一般不涉及 OAuth如果你遇到这个报错说明你连的是远程 Server检查认证配置。排障的通用思路是先看 Host 日志再看 MCP Server 日志最后看模型调用日志。三层日志对照基本能定位到是哪一环出的问题。MCP 的接入文档里有各 Host 的配置示例对照着改比盲试快得多。6. 把 MCP 放进 AI Agent 的定位里看回到最开始的问题MCP 和 Function Calling 到底什么关系。Function Calling 是模型的能力模型根据上下文决定“我要调一个函数”并生成参数。MCP 是工程层的协议解决“这个函数从哪来、怎么描述、怎么被多个 Host 复用”。一个 Agent 的完整链路是MCP 负责把工具暴露出来Function Calling 负责让模型选择工具Host 负责把两者串起来。所以 MCP 的价值不在单次调用而在生态。你写一个 MCP ServerCline 能用Claude Desktop 能用任何支持 MCP 的 Host 都能用不用为每个 Host 重写一遍集成代码。这就是它说的“替换碎片化的 Agent 代码集成”。如果你想继续深入下一步可以试试把多个 MCP Server 组合起来比如一个查天气、一个查数据库、一个读本地文件让 Agent 自己决定调哪个。这时候你会发现MCP 的tools/list机制让工具发现变得很自然Host 不需要提前知道有哪些工具握手之后动态获取就行。这种动态性才是 MCP 相比硬编码 Function Calling 最大的优势。配置和验证的完整流程走一遍你对 MCP 协议栈的理解就不会停留在概念层面了。剩下的就是多写几个 Server把常用的数据源都接进来让 Agent 真正能干活。