文章目录
- 什么是MCP协议?
- 为什么要有MCP协议?
- MCP协议在Agent中如何工作?
- MCP协议详解
-
- [Client 与 Server 完整通信流程](#Client 与 Server 完整通信流程)
- [stdio 实现mcp伪代码](#stdio 实现mcp伪代码)
-
- [mcp client 实现](#mcp client 实现)
- [mcp server 实现](#mcp server 实现)
- 大模型如何知道调用那个工具?
什么是MCP协议?
MCP(Model Context Protocol,模型上下文协议)是一个用于连接大语言模型(如 GPT、Claude 等)与外部数据源和工具的开放协议 。可以把它理解为 AI 的 USB 标准 。
通过这个协议,大模型可以发起对外部世界的连接。操作和感知模型以外的世界。
为什么要有MCP协议?
如果没有外部工具,大模型就是一个只会说不会干的,并且无法实时获取外部信息,思想上的巨人,行动上的矮子。除了聊天什么都做不了。
MCP协议定义了外部工具接入大模型的统一标准,任何实现了这个标准的工具,就可以被大模型调用,实现 "一次开发,多处使用" 。
大模型这个时候才能变成Agent,一个真正的万能助手,可以根据自己的思考,付诸行动。
MCP协议在Agent中如何工作?
初始化阶段:MCP Server 会通过MCP协议 注册 当前MCP服务器提供的功能,以及调用函数的方式 到Agent。
LLM调用阶段:Agent会把 当前可用的的MCP工具,用户问题,发送到大模型。
工具执行:大模型根据用户问题,以及可用的MCP工具,决定执行执行某个mcp工具。
以使用Agent找咖啡店为例:
External MCP Server 大模型(API) Agent 用户 External MCP Server 大模型(API) Agent 用户 初始化阶段 用户提问 第一次LLM调用 工具执行 第二次LLM调用(可选) 启动服务器并获取工具列表 返回工具元数据 构建系统提示(包含工具描述) "帮我找一下附近的咖啡店" 发送完整提示 [系统提示 + 用户问题] 返回结构化响应 {"action": "search_places", "params": {...}} 调用工具 search_places(params) 调用百度地图API 返回POI数据 返回工具结果 [历史对话 + 工具结果 + 用户问题] 生成最终回答 "附近有3家咖啡店:星巴克、瑞幸、Tims"
MCP协议详解
MCP 支持多种传输协议。
- stdio (标准输入/输出): 本地进程间通信
- http/sse: HTTP + Server-Sent Events
- http/websocket: WebSocket 双向通信
- tcp/sse: TCP + Server-Sent Events
- tcp/websocket: TCP + WebSocket
Client 与 Server 完整通信流程
MCP Server Agent/MCP Client 用户 MCP Server Agent/MCP Client 用户 1. 初始化阶段 2. 用户请求阶段 3. 工具调用阶段 4. 结果处理阶段 5. 资源查询(可选) 发送initialize请求 返回initialize响应 发送notify_initialized通知 发送tools/list请求 返回工具列表 发送notify_tools/list通知 "搜索北京的咖啡店" 解析用户意图,选择工具 发送tools/call请求 执行工具逻辑 返回工具结果 "找到10家咖啡店..." 发送resources/list请求 返回资源列表
通信内容:
json
// 1. 初始化阶段
Client -> Server:
{
"jsonrpc": "2.0",
"id": "req_1",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {},
"resources": {}
},
"clientInfo": {
"name": "MCP测试客户端",
"version": "1.0.0"
}
}
}
Server -> Client:
{
"jsonrpc": "2.0",
"id": "req_1",
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {},
"resources": {}
},
"serverInfo": {
"name": "百度地图MCP服务器",
"version": "1.0.0"
}
}
}
Server -> Client (通知):
{
"jsonrpc": "2.0",
"method": "notify_initialized",
"params": {}
}
// 2. 工具列表查询
Client -> Server:
{
"jsonrpc": "2.0",
"id": "req_2",
"method": "tools/list",
"params": {}
}
Server -> Client:
{
"jsonrpc": "2.0",
"id": "req_2",
"result": {
"tools": [
{
"name": "search_places",
"description": "搜索地点信息",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"location": { "type": "string", "default": "北京" },
"radius": { "type": "number", "default": 5000 }
},
"required": ["query"]
}
},
{
"name": "get_directions",
"description": "获取导航路线",
"inputSchema": {
"type": "object",
"properties": {
"origin": { "type": "string" },
"destination": { "type": "string" },
"mode": {
"type": "string",
"enum": ["driving", "walking", "transit"]
}
},
"required": ["origin", "destination"]
}
}
]
}
}
Server -> Client (通知):
{
"jsonrpc": "2.0",
"method": "notify_tools/list",
"params": {
"tools": [...]
}
}
// 3. 工具调用
Client -> Server:
{
"jsonrpc": "2.0",
"id": "req_3",
"method": "tools/call",
"params": {
"name": "search_places",
"arguments": {
"query": "咖啡店",
"location": "北京",
"radius": 3000
}
}
}
Server -> Client:
{
"jsonrpc": "2.0",
"id": "req_3",
"result": {
"content": [
{
"type": "text",
"text": "[\n {\n \"name\": \"星巴克\",\n \"address\": \"朝阳区xxx\",\n \"distance\": \"500米\"\n },\n {\n \"name\": \"瑞幸咖啡\",\n \"address\": \"海淀区xxx\",\n \"distance\": \"800米\"\n }\n]"
}
]
}
}
// 4. 资源查询
Client -> Server:
{
"jsonrpc": "2.0",
"id": "req_4",
"method": "resources/list",
"params": {}
}
Server -> Client:
{
"jsonrpc": "2.0",
"id": "req_4",
"result": {
"resources": [
{
"uri": "file:///config/map.json",
"mimeType": "application/json",
"name": "地图配置"
}
]
}
}
stdio 实现mcp伪代码
mcp client 实现
管理mcp server进程,获取工具列表,发起mcp调用
js
// mcp_client.js
const { spawn } = require('child_process');
class MCPClient {
constructor(serverConfig) {
this.serverProcess = null;
this.requestCallbacks = new Map();
this.tools = new Map();
this.requestId = 0;
}
async connect(command, args = [], env = {}) {
return new Promise((resolve, reject) => {
// 启动子进程
this.serverProcess = spawn(command, args, {
env: { ...process.env, ...env },
stdio: ['pipe', 'pipe', 'pipe']
});
// 设置消息处理器
this.serverProcess.stdout.on('data', (data) => {
this.handleServerOutput(data);
});
this.serverProcess.stderr.on('data', (data) => {
console.error('MCP Server stderr:', data.toString());
});
this.serverProcess.on('error', (error) => {
reject(new Error(`启动MCP服务器失败: ${error.message}`));
});
this.serverProcess.on('close', (code) => {
console.log(`MCP服务器已关闭,退出码: ${code}`);
});
// 执行初始化握手
setTimeout(async () => {
try {
await this.initialize();
await this.listTools();
resolve();
} catch (error) {
reject(error);
}
}, 100);
});
}
handleServerOutput(data) {
const lines = data.toString().split('\n');
lines.forEach(line => {
if (!line.trim()) return;
try {
const message = JSON.parse(line);
this.handleMessage(message);
} catch (error) {
console.error('解析JSON失败:', line, error);
}
});
}
handleMessage(message) {
// 处理响应
if (message.id !== undefined && this.requestCallbacks.has(message.id)) {
const { resolve, reject } = this.requestCallbacks.get(message.id);
if (message.error) {
reject(new Error(`RPC错误 [${message.error.code}]: ${message.error.message}`));
} else {
resolve(message.result);
}
this.requestCallbacks.delete(message.id);
}
// 处理通知
else if (message.method) {
this.handleNotification(message.method, message.params);
}
}
handleNotification(method, params) {
switch (method) {
case "notify_initialized":
console.log('MCP服务器初始化完成');
break;
case "notify_tools/list":
console.log('工具列表已更新:', params.tools);
params.tools.forEach(tool => {
this.tools.set(tool.name, tool);
});
break;
case "notify_shutdown":
console.log('服务器关闭通知:', params.reason);
break;
default:
console.log('收到未知通知:', method, params);
}
}
async sendRequest(method, params) {
const id = `req_${++this.requestId}`;
const request = {
jsonrpc: "2.0",
id,
method,
params
};
return new Promise((resolve, reject) => {
this.requestCallbacks.set(id, { resolve, reject });
this.serverProcess.stdin.write(JSON.stringify(request) + '\n');
// 设置超时
setTimeout(() => {
if (this.requestCallbacks.has(id)) {
this.requestCallbacks.delete(id);
reject(new Error(`请求超时: ${method}`));
}
}, 30000);
});
}
async initialize() {
const result = await this.sendRequest('initialize', {
protocolVersion: "2024-11-05",
capabilities: {
tools: {},
resources: {}
},
clientInfo: {
name: "MCP测试客户端",
version: "1.0.0"
}
});
console.log('初始化成功:', result);
return result;
}
async listTools() {
const result = await this.sendRequest('tools/list', {});
result.tools.forEach(tool => {
this.tools.set(tool.name, tool);
});
console.log('可用工具:', Array.from(this.tools.keys()));
return result;
}
async callTool(toolName, args) {
if (!this.tools.has(toolName)) {
throw new Error(`工具未找到: ${toolName}`);
}
const result = await this.sendRequest('tools/call', {
name: toolName,
arguments: args
});
return result;
}
async listResources() {
return await this.sendRequest('resources/list', {});
}
disconnect() {
if (this.serverProcess) {
this.serverProcess.kill();
this.serverProcess = null;
}
}
}
// 客户端使用示例
async function main() {
const client = new MCPClient();
try {
// 连接MCP服务器
console.log('正在连接MCP服务器...');
await client.connect('node', ['mcp_server.js']);
// 用户模拟请求:搜索咖啡店,真实情况为模型返回结果中通知需要调用某个工具
console.log('\n模拟用户请求:搜索咖啡店');
const result = await client.callTool('search_places', {
query: "咖啡店",
location: "北京",
radius: 3000
});
console.log('工具调用结果:', JSON.stringify(result, null, 2));
// 查询可用资源
const resources = await client.listResources();
console.log('\n可用资源:', JSON.stringify(resources, null, 2));
} catch (error) {
console.error('错误:', error);
} finally {
// 断开连接
setTimeout(() => {
client.disconnect();
console.log('客户端已断开连接');
}, 1000);
}
}
// 运行客户端
if (require.main === module) {
main();
}
mcp server 实现
提供mcp 服务,注册工具
js
// mcp_server.js
const readline = require('readline');
class MCPServer {
constructor() {
this.tools = {
search_places: {
description: "搜索地点信息",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
location: { type: "string", default: "北京" },
radius: { type: "number", default: 5000 }
},
required: ["query"]
},
execute: async (args) => {
// 模拟调用百度地图API
return [
{ name: "星巴克", address: "朝阳区xxx", distance: "500米" },
{ name: "瑞幸咖啡", address: "海淀区xxx", distance: "800米" }
];
}
},
get_directions: {
description: "获取导航路线",
inputSchema: {
type: "object",
properties: {
origin: { type: "string" },
destination: { type: "string" },
mode: { type: "string", enum: ["driving", "walking", "transit"] }
},
required: ["origin", "destination"]
},
execute: async (args) => {
return {
distance: "10公里",
duration: "30分钟",
steps: ["从起点出发", "直行2公里", "左转到达"]
};
}
}
};
this.resources = {
"map-config": {
uri: "file:///config/map.json",
mimeType: "application/json",
name: "地图配置"
}
};
}
start() {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
rl.on('line', async (line) => {
try {
const message = JSON.parse(line);
await this.handleMessage(message);
} catch (error) {
this.sendError(null, -32700, "解析错误", error.message);
}
});
// 处理进程退出
process.on('SIGINT', () => this.shutdown());
process.on('SIGTERM', () => this.shutdown());
}
async handleMessage(message) {
const { jsonrpc, id, method, params } = message;
if (jsonrpc !== "2.0") {
this.sendError(id, -32600, "无效请求", "必须是JSON-RPC 2.0");
return;
}
switch (method) {
case "initialize":
await this.handleInitialize(id, params);
break;
case "tools/list":
await this.handleListTools(id);
break;
case "tools/call":
await this.handleToolCall(id, params);
break;
case "resources/list":
await this.handleListResources(id);
break;
default:
this.sendError(id, -32601, "方法未找到", `未知方法: ${method}`);
}
}
async handleInitialize(id, params) {
// 1. 发送初始化响应
this.sendResponse(id, {
protocolVersion: "2024-11-05",
capabilities: {
tools: {},
resources: {}
},
serverInfo: {
name: "百度地图MCP服务器",
version: "1.0.0"
}
});
// 2. 发送初始化完成通知(稍后发送)
setTimeout(() => {
this.sendNotification("notify_initialized", {});
}, 10);
}
async handleListTools(id) {
const tools = Object.entries(this.tools).map(([name, tool]) => ({
name,
description: tool.description,
inputSchema: tool.inputSchema
}));
this.sendResponse(id, { tools });
}
async handleToolCall(id, params) {
const { name, arguments: args } = params;
const tool = this.tools[name];
if (!tool) {
this.sendError(id, -32602, "无效参数", `工具未找到: ${name}`);
return;
}
try {
const result = await tool.execute(args);
this.sendResponse(id, {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
});
} catch (error) {
this.sendError(id, -32000, "执行错误", error.message);
}
}
async handleListResources(id) {
const resources = Object.entries(this.resources).map(([uri, resource]) => ({
uri: resource.uri,
mimeType: resource.mimeType,
name: resource.name
}));
this.sendResponse(id, { resources });
}
sendResponse(id, result) {
const response = {
jsonrpc: "2.0",
id,
result
};
this.writeLine(response);
}
sendNotification(method, params) {
const notification = {
jsonrpc: "2.0",
method,
params
};
this.writeLine(notification);
}
sendError(id, code, message, data = null) {
const error = {
jsonrpc: "2.0",
id,
error: {
code,
message,
data
}
};
this.writeLine(error);
}
writeLine(obj) {
console.log(JSON.stringify(obj));
}
shutdown() {
this.sendNotification("notify_shutdown", { reason: "服务器关闭" });
process.exit(0);
}
}
// 启动服务器
const server = new MCPServer();
server.start();
大模型如何知道调用那个工具?
部分大模型原生支持,可以直接作为参数传递。
原生支持:专用"信道"
以OpenAI的API为例,当你调用chat.completions.create时,传递一个独立的tools参数(在早期版本中是functions),其值就是一个JSON Schema的数组。
python
# 伪代码示例:OpenAI API 调用结构
response = openai.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "北京今天天气怎么样?"}],
tools=[{ # 这是一个独立的参数!
"type": "function",
"function": {
"name": "get_weather",
"description": "...",
"parameters": {...} # 完整的 JSON Schema
}
}]
)
模型在训练阶段就对这种结构化输入进行了特殊优化。收到请求后,模型内部的专用路径会被激活,它只负责解析用户意图、匹配工具、并生成一个严格符合参数Schema的JSON对象 。这个过程独立于文本生成逻辑,就像为"工具调用"这个任务开通了一条VIP专用通道,不与普通的"对话"通道混杂。
这种方式工具的调用识别比较准确。
提示工程:混入"对话流"
大模型的本质就是一个词语接龙,聊天工具,在提示词规定好交付方式就行,举个最简单的例子如下:
系统指令:你是一个助手,可以调用工具。可用工具如下:
1. 工具名: get_weather
描述: 获取指定城市的当前天气。
参数:
- location: (字符串) 城市名,如"北京"。
- unit: (字符串) 温度单位,可选"celsius"或"fahrenheit"。
你必须严格按照以下JSON格式输出调用决定:
{"action": "工具名", "action_input": {"参数": "值"}}
用户问题:北京今天天气怎么样?
此种方式可能 稳定性与精度比较低,在复杂场景下可能存在工具识别出错,参数不对等问题。