导语
之前已经就MCP的基础概念做了简单介绍,今天我们进一步深入了解MCP:
- 写一个 MCP server
- 分析 MCP 底层协议
- MCP 含义与地位
创建 MCP server
创建 MCP server 语言有很多: python、node、java、c#等。

接下来我们按照 Model Context Protocol 进行简单的实战演练。

1. 按照教程步骤执行
会生成这样一个简单的项目工程:
1 2 3 4 5 6 7
| ├── weather ├── build # 静态资源 │ └── index.js # html模板 ├── src # 源代码 │ └── index.ts # 入口文件 ├── tsconfig # ts 配置 └── package.json # package.json
|
2. 接下来打开 vscode,按图中步骤执行:
vscode要事先安装好 cline 插件

1 2 3 4 5 6 7 8 9 10 11 12 13
| { "mcpServers": { "weather": { "disabled": false, "timeout": 300, "type": "stdio", "command": "node", "args": [ "/Users/zhangsan/Downloads/Code/cline/weather/build/index.js" ] } } }
|
3. 新建 cline 会话
点击 +

4. 询问天气
例如:你好,北京的天气怎么样

点击 Approve

MCP 底层协议
我们已经创建了一个 MCP server。但是我们仍然不知道 MCP 协议到底是如何运作的。
MCP server虽然是我们写的,但是我们用了 MCP 库来完成它的,它帮我们完成了很多事情。(我们并不清楚他到底做了什么)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
为了搞清楚 cline 与 MCP server 是如何沟通的,我们在二者沟通中间加入一个日志脚本 mcp-stdio-logger.cjs

创建 mcp-stdio-logger.cjs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
| // mcp-stdio-logger.cjs const { spawn } = require('child_process'); const fs = require('fs'); const path = require('path');
// 从命令行参数获取要运行的实际脚本路径 和 服务器名称 // 例如:node mcp-stdio-logger.cjs /path/to/my/server/index.js weather const targetScriptPath = process.argv[2]; const serverName = process.argv[3]; // 新增的参数:MCP Server 的名称 (e.g., "weather")
if (!targetScriptPath || !serverName) { console.error('错误: 未提供目标脚本路径或服务器名称。用法: node mcp-stdio-logger.cjs <path_to_your_index.js> <server_name>'); process.exit(1); }
// 确定日志文件的路径 // 日志文件与目标脚本在同级目录,且文件名基于服务器名称 const logDir = path.dirname(targetScriptPath); const logFileName = `mcp_interaction_log_${serverName}.json`; // 使用服务器名称作为日志文件名 const logFilePath = path.join(logDir, logFileName);
let logs = []; // 用于存储所有交互日志的数组 (包括历史和本次运行的)
// --- 辅助函数:加载现有日志 --- function loadExistingLogs() { if (fs.existsSync(logFilePath)) { try { const fileContent = fs.readFileSync(logFilePath, 'utf8'); // 确保文件内容不为空,且是有效的 JSON 数组 if (fileContent.trim().length > 0) { const parsed = JSON.parse(fileContent); if (Array.isArray(parsed)) { logs = parsed; console.log(`[MCP Logger] 已加载现有 ${logs.length} 条日志到内存。`); } else { console.warn(`[MCP Logger] 现有日志文件 ('${logFilePath}') 内容不是有效的 JSON 数组,将重新创建日志。`); logs = []; // 如果不是数组,清空并从一个空的日志数组开始 } } else { console.log(`[MCP Logger] 现有日志文件 ('${logFilePath}') 为空,从头开始记录。`); } } catch (err) { console.error(`[MCP Logger] 加载现有日志文件 ('${logFilePath}') 失败:`, err.message); // 如果解析失败,可能是文件损坏或格式错误,我们从一个空的日志数组开始,并会覆盖旧文件 logs = []; } } else { console.log(`[MCP Logger] 日志文件 ('${logFilePath}') 不存在,将创建新文件。`); } }
// --- 辅助函数:记录日志 --- function logInteraction(type, data, source = 'unknown') { const entry = { id: logs.length + 1, // 为每条日志添加一个自增ID,方便追踪 timestamp: new Date().toISOString(), type: type, // 'input_from_cline', 'output_to_cline', 'child_stderr' source: source, data: data.toString('utf8') // 假设数据是 UTF-8 编码 }; logs.push(entry); // 可以在这里选择性地输出到控制台,方便实时查看 // console.log(`[${type}] ${source}: ${data.toString('utf8').trim()}`); }
// --- 辅助函数:写入日志到文件 --- function writeLogsToFile() { if (logs.length === 0) { console.log(`[MCP Logger] 没有交互日志需要写入文件。`); return; } console.log(`\n[MCP Logger] 正在将 ${logs.length} 条交互日志写入文件: ${logFilePath}`); try { // 使用 fs.writeFileSync 覆盖写入整个 JSON 数组,确保 JSON 格式的正确性 fs.writeFileSync(logFilePath, JSON.stringify(logs, null, 2), 'utf8'); console.log(`[MCP Logger] 日志写入成功。`); } catch (err) { console.error(`[MCP Logger] 写入日志文件失败:`, err); } }
// --- 捕获进程退出事件,确保日志写入 --- process.on('exit', writeLogsToFile); process.on('SIGINT', () => { // Ctrl+C 信号 console.log('\n[MCP Logger] 捕获到 SIGINT 信号,正在退出...'); process.exit(0); }); process.on('SIGTERM', () => { // 终止信号 console.log('\n[MCP Logger] 捕获到 SIGTERM 信号,正在退出...'); process.exit(0); }); process.on('uncaughtException', (err) => { // 未捕获的异常 console.error('\n[MCP Logger] 捕获到未处理的异常,正在退出:', err); writeLogsToFile(); // 尝试在异常退出前写入日志 process.exit(1); });
// --- 在启动时加载现有日志 --- loadExistingLogs(); // 这一步至关重要,它会在新的日志被添加前,先加载之前的历史日志
console.log(`[MCP Logger] 启动代理('${serverName}'),代理脚本: ${targetScriptPath}`); console.log(`[MCP Logger] 日志文件路径: ${logFilePath}`);
// --- 启动目标脚本作为子进程 --- // 重要的:确保 'node' 命令的绝对路径是正确的,例如 '/usr/local/bin/node' // 如果之前您已经配置了绝对路径,请保持不变。否则,请更换为您的 'node' 绝对路径。 const child = spawn('node', [targetScriptPath], { stdio: 'pipe' });
// --- 代理 stdin (cline -> logger -> child) --- process.stdin.on('data', (data) => { logInteraction('input_from_cline', data, 'cline'); child.stdin.write(data); }); process.stdin.on('end', () => { child.stdin.end(); console.log('[MCP Logger] cline 标准输入流已关闭。'); });
// --- 代理 stdout (child -> logger -> cline) --- child.stdout.on('data', (data) => { logInteraction('output_to_cline', data, 'child_stdout'); process.stdout.write(data); // 将子进程输出转发回 cline }); child.stdout.on('end', () => { console.log('[MCP Logger] 子进程标准输出流已关闭。'); });
// --- 捕获子进程 stderr (child -> logger) --- child.stderr.on('data', (data) => { logInteraction('child_stderr', data, 'child_stderr'); process.stderr.write(data); // 将子进程的错误也转发到 cline 的 stderr,方便调试 }); child.stderr.on('end', () => { console.log('[MCP Logger] 子进程标准错误流已关闭。'); });
// --- 监听子进程退出事件 --- child.on('close', (code) => { console.log(`[MCP Logger] 子进程 '${targetScriptPath}' 已退出,退出码: ${code}`); // 如果子进程异常退出,我们也可以选择让主进程也退出 if (code !== 0) { process.exit(code); } });
child.on('error', (err) => { console.error(`[MCP Logger] 启动子进程 '${targetScriptPath}' 失败:`, err); process.exit(1); });
// 确保主进程的 stdin 不会自动关闭,以便保持监听 cline 的输入 process.stdin.resume();
|
别忘了修改 MCP server 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| { "mcpServers": { "weather": { "disabled": false, "timeout": 60, "type": "stdio", "command": "node", "args": [ "/Users/zhangsan/Downloads/Code/cline/weather/mcp-stdio-logger.cjs", "/Users/zhangsan/Downloads/Code/cline/weather/build/index.js", "weather" ] } } }
|
执行天气查询
建立新的会话,询问:洛杉矶的天气情况?
任务执行完毕后,获得日志文件 mcp_interaction_log_weather.json
解析
输入:Cline --> MCP server
输出:MCP server --> Cline

上图涵盖了 Cline 与MCP server之间,互相 “打招呼” ,确定对应版本号等
以上日志发生在注册工具的一瞬间!
接下来,解析查询天气日志信息:

日志输出了调用 get_forecast 方法,参数为目标地址的经纬度,然后得到 11
即最终的天气情况。
日志 10 调用 get_forecast 方法的入参遵循 日志 5 中的 get_forecast 规范 
直接与MCP server 进行交互
为了进一步理解日志内容,接下来我们不用 Cline ,直接通过命令行来与 MCP server 进行交互
- 在 weather 文件夹下启动命令行 执行
node ./build/index.js
- 命令行出现
Weather MCP Server running on stdio 即 MCP Server 已启动
- 复制日志中的第一行内容,把 cline 改成任意名称,比如: zhangsan
- 逐行复制日志中的输入项,会看到输出会按照日志的内容返回
到这里你应该已经理解 MCP 协议了,你甚至可以不使用 MCP 库也能够开发MCP server 了
你的 MCP server 只要遵循 MCP的这个规范即可!(日志中所示)
MCP 含义与地位
通过实战我们其实可以简单粗暴的理解 MCP 协议,即:函数的注册与调用

MCP协议就是规定了如何发现和调用函数的,这套协议与大模型没什么关系。因为它并没有规定与大模型的交互方式!
这里引出了大模型的交互协议,目前主流的交互协议有: XML、Function Calling等。他们规定了模型是如何调用函数的。
总结
MCP协议的本质:MCP协议规定了如何发现和调用函数的并未规定如何与模型进行交互


个人觉得 MCP 名字有点用错了,其实叫 Function-Call-Protocol 比较贴切
文章部分内容来自 马克的工作坊