MCP(Model Context Protocol)一篇入门-上(持续更新中)

348 阅读6分钟

阅读本文章,你能得到什么?

  • 了解 MCP(Model Context Protocol)
  • 了解 MCP 最朴素的规范
  • 知道 MCP 是为了解决什么问题而出现的
  • 了解 MCP 的通讯协议,并且使用代码进行扩展验证
  • 动手实现一个遵循 MCP 的功能完备的 Agent 应用
  • 掌握快速了解一个新事物的思路(新事物一般来自于解决某些问题而衍生出来)

导读

最近 Model Context Protocol 比较火,有人说它无所不能,有人说它与 Function-Call 并无本质差别,有点小题大做,本着持续、高效学习新技术的心态,我也看了看各种博客以及官方文档,发现大多数博客都是基于官方文档的搬运,都是获取天气的案例,未考虑 Tool 的参数描述,等细节上的问题,而官方文档也只是说明了最核心的协议架构图,协议解决的问题,协议的优势,但是关于实际投入生产使用部分,都封装在代码 SDK 中,Get Start 文档也都是基于 Agent 侧展开的,并无详细的介绍,对于代码高级特效了解不多,或者使用 Windows 电脑安装的 Claude 找不到 developer 选项的人不太友好,因此我写下这篇文章,将 MCP 协议的相关知识进行拆分,一个一个攻克,攻克完成后统一汇总,实现一个功能基本完备的 LLM Agent,例如,使用大模型控制家里的电扇,控制家里的花盆浇水,当然了,在阅读这篇文章的时候,建议先阅读一下 官方文档

Model Context Protocol 协议

image.png 如上图所示,是我基于理解梳理的架构图,可见,编排工具,例如:Trae、cursor、claude 等,都是内置了 Agent 的,Agent 可以基于用户输入、Prompt 以及 LLM 的结果,完成一些任务,并且随着技术以及方法论的成熟,衍生出很多概念,例如 ReAct 模式,Agent 可以基于用户的任务,进行任务拆分、编排以及执行,当然在之前 Agent 完成任务的时候,手头的工具(Tool)都是预先定义好的,这些 Tool 在定义的时候,有功能描述,参数类型含义,返回结果类型与含义,MCP 出现之后,Agent 又多了一个工具 MCP-Client,可以基于 Client 与注册上来的 MCP-Server 们进行通讯,进行数据采集,任务下发,基于架构图可以看出来 MCP-Client - 1 ---> n - MCP-Server,并且 MCP-Server 可以动态的插拔,有点传统编程技术上的注册中心 Nacos 的意思,万变不离其宗,那么我们可以推理一下,如果要我们自己设计的话,需要考虑哪些事情,然后对照看看 MCP 的官网 SDK 是如何实现的?

  • MCP-Server 与 MCP-Client 是如何通讯的?
  • MCP-Server 注册的时候,需要提供哪些信息?
  • MCP-Client 调用 MCP-Server 的时候,需要传递哪些信息,信息如何获取?

Stdio 通讯原理

image.png

基于官方文档查看,Client 与 Server 是 stdio 进程通讯,因此可以解耦,只要满足协议,不限制编程语言,使用如下命令可以模拟通讯过程,需要安装 node 、npm 以及 python3.10,管理 python 建议使用虚拟环境,stdio 通讯(标准输入输出通信)是指进程之间通过标准输入(stdin)、标准输出(stdout)和标准错误输出(stderr)进行的数据传递方式。这是一种常见的进程间通信(IPC)机制,尤其在 Unix/Linux 系统中,用于命令行工具组合(如 ls | grep txt)和子进程交互(如通过编程语言调用其他程序并处理其输出)。

npm run parent.js

parent.js

#!parent.js 文件
#!/usr/bin/env node
const { spawn } = require('child_process');
const { EventEmitter } = require('events');

let nextId = 1, pending = new Map();
const emitter = new EventEmitter();
let buffer = '';

// 启动 Python 子进程
const child = spawn('python3', ['child.py'], { stdio: ['pipe', 'pipe', 'inherit'] });

// 监听 stdout,处理数据帧
child.stdout.on('data', chunk => {
  buffer += chunk.toString();
  while (hasFullFrame(buffer)) {
    const { body, rest } = extractFrame(buffer);
    buffer = rest;
    const msg = JSON.parse(body);

    if (msg.id !== undefined) {
      const { resolve, reject } = pending.get(msg.id) || {};
      pending.delete(msg.id);
      msg.error ? reject(msg.error) : resolve(msg.result);
    } else {
      emitter.emit(msg.method, msg.params);
    }
  }
});

// 判断 buffer 中是否有完整帧
function hasFullFrame(buffer) {
  const match = buffer.match(/^Content-Length: (\d+)\r\n\r\n/);
  if (!match) return false;
  const len = parseInt(match[1], 10);
  const totalLen = match[0].length + len;
  return buffer.length >= totalLen;
}

// 提取完整帧
function extractFrame(buffer) {
  const match = buffer.match(/^Content-Length: (\d+)\r\n\r\n/);
  const len = parseInt(match[1], 10);
  const start = match[0].length;
  const body = buffer.slice(start, start + len);
  const rest = buffer.slice(start + len);
  return { body, rest };
}

// 发送请求
function sendRequest(method, params) {
  return new Promise((resolve, reject) => {
    const id = nextId++;
    const req = { jsonrpc: '2.0', id, method, params };
    pending.set(id, { resolve, reject });
    const payload = JSON.stringify(req);
    child.stdin.write(`Content-Length: ${Buffer.byteLength(payload)}\r\n\r\n${payload}`);
  });
}

// 发送通知
function sendNotification(method, params) {
  const payload = JSON.stringify({ jsonrpc: '2.0', method, params });
  child.stdin.write(`Content-Length: ${Buffer.byteLength(payload)}\r\n\r\n${payload}`);
}

// 示例:调用 Python 中的 add 方法
sendRequest('add', { a: 3, b: 7 }).then(result => {
  console.log('Add result:', result);
}).catch(err => {
  console.error('Error:', err);
});

// 示例:监听来自 Python 的通知
emitter.on('status', params => {
  console.log('Status from Python:', params);
});

#!child.py 文件
#!/usr/bin/env python3 
import sys
import json
import threading

def read_messages():
    buffer = ""
    while True:
        chunk = sys.stdin.read(1)
        if not chunk:
            break
        buffer += chunk
        while True:
            if "\r\n\r\n" not in buffer:
                break
            header, rest = buffer.split("\r\n\r\n", 1)
            content_length = 0
            for line in header.split("\r\n"):
                if line.lower().startswith("content-length:"):
                    content_length = int(line.split(":")[1].strip())
            if len(rest) < content_length:
                break
            body = rest[:content_length]
            buffer = rest[content_length:]
            handle_message(json.loads(body))

def handle_message(msg):
    if 'method' in msg:
        if msg.get('id') is not None:
            # 是请求,返回响应
            if msg['method'] == 'add':
                a = msg['params'].get('a', 0)
                b = msg['params'].get('b', 0)
                result = a + b
                response = {
                    "jsonrpc": "2.0",
                    "id": msg['id'],
                    "result": result
                }
                send(response)
        else:
            # 是通知
            print("Received notification:", msg['method'], msg['params'], file=sys.stderr)

def send(obj):
    data = json.dumps(obj)
    sys.stdout.write(f"Content-Length: {len(data)}\r\n\r\n{data}")
    sys.stdout.flush()

# 主动发送通知
def send_status():
    import time
    import random
    while True:
        send({
            "jsonrpc": "2.0",
            "method": "status",
            "params": {"message": "running", "load": round(random.random(), 2)}
        })
        time.sleep(5)

# 启动通知线程
threading.Thread(target=send_status, daemon=True).start()

# 开始读取输入
read_messages()

MCP-Server 注册的时候,需要提供哪些信息

个人分析

作为一个服务提供方 MCP-Server 需要清晰的提供如下信息:

  • 资源 Resource
    • 提供
      • 资源名称
      • 资源描述
      • 资源入参类型,入参名称,入参描述
    • 定义
      • 资源详细信息
  • 工具 Tools
    • 提供
      • 工具名称
      • 工具描述
      • 工具入参类型,入参名称,入参描述
    • 定义
      • 工具执行函数
  • 提示词 Prompts
    • 提供
      • 提示词名称
      • 提示词描述
      • 提示词入参类型,入参名称,入参描述
    • 定义
      • 提示词构建函数

实际代码实现(TypeScript)

// server.ts 文件
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// Create an MCP server
const server = new McpServer({
  name: "IoT-Light-Control",
  version: "1.1.0"
});

// Add an addition tool
server.tool(
  "executeAction",                   
  "控制开关灯,调用我的时候,不要进行二次确认,直接调用",  // 工具描述
  { actionId: z.string().describe("动作标识符,用于区分不同逻辑 open ,close") },
  async ({ actionId }) => {                  // Handler:在这里放置任意异步逻辑
    // —— 在此替换为业务逻辑,如 HTTP 调用、数据库查询等 ——  
    // 示例:根据 actionId 打印日志并模拟执行延迟
    await new Promise(res => setTimeout(res, 500));  // 模拟异步操作

    // 返回符合 MCP 规范的响应
    return {
      content: [
        {
          type: "text",
          text: `Action ${actionId} executed successfully.`
        }
      ]
    };
  }
);
// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();

 
async function main() {
  await server.connect(transport);
}
main().catch(console.error);

MCP-Client 调用 MCP-Server 的时候,方法调用,信息获取

// 执行 npx ts-node client.ts y运行
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const transport = new StdioClientTransport({
  command: "npx",
  args: ["ts-node","server.ts"]
});

const client = new Client(
  {
    name: "example-client",
    version: "1.0.0"
  }
);

async function main() {
await client.connect(transport);

const tools = await client.listTools();
console.log(JSON.stringify(tools,null,2))

// // Call a tool
const result = await client.callTool({
  name: "executeAction",
  arguments: {
    actionId: "close"
  }
});
console.log(JSON.stringify(result,null,2))

}

main().catch(console.error);

image.png