1. 从一次“工具调用失败”说起:MCP 到底解决了什么问题
如果你最近在折腾 AI Agent,大概率遇到过这种场景:给模型接了一个查天气的接口,代码里写死了函数名和参数格式,跑起来没问题;过两天想换成另一个模型,或者想再加一个查数据库的工具,结果发现整套调用逻辑要重写一遍。Function Calling 本身不难,难的是每接一个模型、每加一个数据源,都要重新对齐一遍参数结构。
这就是 MCP(Model 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.0",method是方法名,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', errors='replace')}") log_file.flush() dst.write(line) dst.flush() process = subprocess.Popen( target_command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0 ) threading.Thread(target=forward_and_log, args=(sys.stdin.buffer, process.stdin, log_f, "输入"), daemon=True).start() threading.Thread(target=forward_and_log, args=(process.stdout, sys.stdout.buffer, log_f, "输出"), daemon=True).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里的 schema:
from mcp.server.fastmcp import FastMCP import httpx mcp = FastMCP("weather", log_level="ERROR") 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(transport="stdio")transport="stdio"表示走本地标准输入输出,这是本地 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/api,Key 在控制台生成,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 Server,Cline 能用,Claude Desktop 能用,任何支持 MCP 的 Host 都能用,不用为每个 Host 重写一遍集成代码。这就是它说的“替换碎片化的 Agent 代码集成”。
如果你想继续深入,下一步可以试试把多个 MCP Server 组合起来,比如一个查天气、一个查数据库、一个读本地文件,让 Agent 自己决定调哪个。这时候你会发现,MCP 的tools/list机制让工具发现变得很自然,Host 不需要提前知道有哪些工具,握手之后动态获取就行。这种动态性才是 MCP 相比硬编码 Function Calling 最大的优势。
配置和验证的完整流程走一遍,你对 MCP 协议栈的理解就不会停留在概念层面了。剩下的就是多写几个 Server,把常用的数据源都接进来,让 Agent 真正能干活。