从源码分析 MCP 的实现和使用

开发 前端
MCP(Model Context Protocol)是最近非常热门的词,本文从实现和使用两个方面介绍 MCP 的内容。

MCP 是什么?

官方的定义如下:

MCP is an open protocol that standardizes how applications provide context to LLMs. Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect your devices to various peripherals and accessories, MCP provides a standardized way to connect AI models to different data sources and tools.

从实现来看,MCP 定义了一套通用的 API 用于 MCP 客户端和服务器通信,MCP 开发者只需要基于 MCP 提供的 Server 注册自己的工具、Prompt,就可以通过 MCP 提供的 Client 获取并调用这些能力,官网给的图如下。

MCP 提供的客户端和服务器实现了通用的能力,比如如何通信,如何获取工具等,具体有哪些能力(比如由哪里工具)是由 Server 开发者定义的,我们可以自己开发工具,也可以使用第三方开源的。但如何使用这些能力是用使用者决定的,比如我们开发一个基于 Redis 的 MCP Server,那么我们就可以通过 MCP Client 从这个 Server 从 Redis 里获取信息,但是如何使用这些信息是自己定义的,一般来说就是输入给大模型。

了解了 MCP Server 和 Client 架构后,接着看看 MCP 在实际场景中的使用架构。下图(来自《 MCP 是什么,现状和未来?)描述了 MCP 在用户和大模型通信过程中的位置和作用。

上图的流程大致如下。

  • 用户通过 MCP Client 从 MCP Server 获取可用的能力,比如工具、Prompt。
  • 把工具和用户输入传入大模型,大模型返回时会告诉用户可以使用哪些工具去获取更多信息。
  • 用户再次通过 MCP Client 请求 MCP Server,让 MCP Server 执行对应的工具获取数据。
  • 用户从 MCP Server 获取数据后,再次访问大模型,大模型就可以利用工具的信息更好地回解决用户的问题。

MCP 的使用例子

了解了 MCP 的一些基础概念后,来看一下 MCP 使用的例子。

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from 'express';
import { z } from 'zod';
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
const { Request, Response } = express;
const GetArgumentsSchema = z.object({
  key: z.string(),
});
// 模拟 redis
const memory = {"hello": "world"};
// 创建 MCP Server
const server = new Server({
  name: "example-server",
  version: "1.0.0",
},{
  capabilities: {
    tools: {},
  }
});
// 注册获取工具 API 路由
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
      tools: [
          {
              name: "get",
              description: "Get value by key from kv",
              inputSchema: {
                  type: "object",
                  properties: {
                      key: {
                          type: "string",
                          description: "",
                      },
                  },
                  required: ["key"],
              },
          },
      ],
  };
});
// 注册执行工具 API 路由
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  console.log(request.params)
  const { name, arguments: args } = request.params;
  const { key } = GetArgumentsSchema.parse(args);
  return {
    content: [
        {
            type: "text",
            text: `${memory[key]}`,
        },
    ],
};
});
// 启动 HTTP 服务器
const app = express();
const transports: {[sessionId: string]: SSEServerTransport} = {};
// SSE 路由
app.get("/sse", async (_: Request, res: Response) => {
  // 创建一个 SSE 通道,meesages 为后续通信的路由
  const transport = new SSEServerTransport('/messages', res);
  transports[transport.sessionId] = transport;
  res.on("close", () => {
    delete transports[transport.sessionId];
  });
  await server.connect(transport);
});
// 通信路由
app.post("/messages", async (req: Request, res: Response) => {
  const sessionId = req.query.sessionId as string;
  const transport = transports[sessionId];
  if (transport) {
    // 处理请求,处理完毕后通过上面的 sse res 进行响应
    await transport.handlePostMessage(req, res);
  } else {
    res.status(400).send('No transport found for sessionId');
  }
});
app.listen(3001);
  • 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.

MCP Server 并不是一个传统的 HTTP Server,它只是负责处理来自客户端的请求,并不处理 HTTP 协议本身,其中,Server 的 setRequestHandler API 用于注册路由和处理函数,第一个 setRequestHandler 注册了获取工具列表 API,第二个 setRequestHandler 注册了执行工具的 API,客户端一般先获取工具列表,然后再调 API 执行具体的工具。创建 MCP Server 后,还需要创建一个通信管道用户接收客户端的请求,这里是 SSE 通道,SSE 会接收到来自 HTTP 服务器的请求,然后传递给 MCP Server,MCP Server 再通过通道把处理结果返回给客户端。

接着看客户端的例子。

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
async function runClient() {
    const client = new Client(
      {
        name: "mcp-typescript test client",
        version: "0.1.0",
      },
    );
  
  
    const clientTransport = new SSEClientTransport(new URL("http://localhost:3001/sse"));
  
    await client.connect(clientTransport);
    const tools = await client.listTools()  
    console.log(tools)
    const resp = await client.callTool({
      name: "get",
      arguments: {
        key: "hello"
      }
    })
    console.log(resp);
    await client.close();
  }
  runClient();
  • 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.


执行上面代码会首先从 MCP 客户端获取工具列表,然后执行 get 命令获取数据,客户端和服务端的架构类似,就不再介绍。

图片

MCP 的实现

Transport

MCP 客户端和服务器需要通信来完成数据的交互,而通信就需要一个通道,所以先看一下通道的实现。MCP 支持通过本地、SSE、等方式来通信,通过 Transport 实现了抽象。

interface Transport {
  // 初始化通道
  start(): Promise<void>;
  // 发送信息或响应
  send(message: JSONRPCMessage): Promise<void>;
  
  close(): Promise<void>;
  onclose?: () => void;
  onerror?: (error: Error) => void;
  // 处理收到的请求或响应
  onmessage?: (message: JSONRPCMessage) => void;
  // 连接对应的回话 ID
  sessionId?: string;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

Transport 可以用于客户端或服务端,只要实现了上面的方法就行。

我们来看一个基于内存的通信管道的实现,类似 Unix 管道。

class InMemoryTransport implements Transport {
  private _otherTransport?: InMemoryTransport;
  private _messageQueue: JSONRPCMessage[] = [];
  onclose?: () => void;
  onerror?: (error: Error) => void;
  onmessage?: (message: JSONRPCMessage) => void;
  sessionId?: string;
  static createLinkedPair(): [InMemoryTransport, InMemoryTransport] {
    // 创建两个管道,互相关联
    const clientTransport = new InMemoryTransport();
    const serverTransport = new InMemoryTransport();
    clientTransport._otherTransport = serverTransport;
    serverTransport._otherTransport = clientTransport;
    return [clientTransport, serverTransport];
  }
  // 初始化管道,如果之前已经存在数据,则消费
  async start(): Promise<void> {
    while (this._messageQueue.length > 0) {
      const message = this._messageQueue.shift();
      if (message) {
        this.onmessage?.(message);
      }
    }
  }
  async close(): Promise<void> {
    const other = this._otherTransport;
    this._otherTransport = undefined;
    await other?.close();
    this.onclose?.();
  }
  // 发送数据
  async send(message: JSONRPCMessage): Promise<void> {
    if (!this._otherTransport) {
      throw new Error("Not connected");
    }
    // 如果还没有消费者,则先缓存
    if (this._otherTransport.onmessage) {
      this._otherTransport.onmessage(message);
    } else {
      this._otherTransport._messageQueue.push(message);
    }
  }
}
  • 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.

在实际场景中,我们一般使用 SSE 或 Websocket 来实现通信,下面看一下  Server SSE Transport的实现。

class SSEServerTransport implements Transport {
  private _sseResponse?: ServerResponse;
  private _sessionId: string;
  onclose?: () => void;
  onerror?: (error: Error) => void;
  // 上层设置
  onmessage?: (message: JSONRPCMessage) => void;
 
  // 初始化时记录 SSE 连接对应的响应对象,以及用于通信的 endpoint 路由
  constructor(
    private _endpoint: string,
    private res: ServerResponse,
  ) {
    this._sessionId = randomUUID();
  }
  async start(): Promise<void> {
    // 返回 text/event-stream 类型的响应头,表示这是一个 SSE 连接,后续可以基于这个连接进行数据推送
    this.res.writeHead(200, {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    });
    // 返回 endpoint 信息,作为 POST 请求的 url 路径
    this.res.write(
      `event: endpoint\ndata: ${encodeURI(this._endpoint)}?sessinotallow=${this._sessionId}\n\n`,
    );
    // 记录 SSE 连接对应的响应对象
    this._sseResponse = this.res;
  }
  // 处理 POST 请求,解析请求体,将请求体并通过 SSE 响应对象把请求结果发送给客户端
  async handlePostMessage(
    req: IncomingMessage,
    res: ServerResponse,
    parsedBody?: unknown,
  ): Promise<void> {
    // 请求前置处理
    let body: string | unknown;
    try {
      const ct = contentType.parse(req.headers["content-type"] ?? "");
      if (ct.type !== "application/json") {
        throw new Error(`Unsupported content-type: ${ct}`);
      }
      body = parsedBody ?? await getRawBody(req, {
        limit: MAXIMUM_MESSAGE_SIZE,
        encoding: ct.parameters.charset ?? "utf-8",
      });
    } catch (error) {
      res.writeHead(400).end(String(error));
      this.onerror?.(error as Error);
      return;
    }
    try {
      // 具体的处理逻辑
      await this.handleMessage(typeof body === 'string' ? JSON.parse(body) : body);
    } catch {
      res.writeHead(400).end(`Invalid message: ${body}`);
      return;
    }
    // 先回复 202 Accepted,表示请求已经被接受,后续处理逻辑由上层处理
    res.writeHead(202).end("Accepted");
  }
  async handleMessage(message: unknown): Promise<void> {
    let parsedMessage: JSONRPCMessage;
    // 解析请求,通知上层处理
    try {
      parsedMessage = JSONRPCMessageSchema.parse(message);
    } catch (error) {
      this.onerror?.(error as Error);
      throw error;
    }
    this.onmessage?.(parsedMessage);
  }
  async send(message: JSONRPCMessage): Promise<void> {
    this._sseResponse.write(
      `event: message\ndata: ${JSON.stringify(message)}\n\n`,
    );
  }
  get sessionId(): string {
    return this._sessionId;
  }
}
  • 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.

SSE 的处理过程如下。

  1. 客户端发起 SSE 请求,服务器收到 SSE 请求时,记录对应的响应对象和设置通信路由,返回通信路由给客户端。
  2. 客户端收到 SSE 通信路径后,后续通过 HTTP POST 请求到服务端返回的路由。
  3. 服务端收到 HTTP Post 请求时,先返回 202 表示请求已被成功接收,然后通知上层,上册处理完毕后通过第一步保存的 SSE 响应对象推送处理结果给客户端。

接着看一下 Client 的 SSE Transport 的实现。

export class SSEClientTransport implements Transport {
  private _eventSource?: EventSource;
  private _endpoint?: URL;
  private _abortController?: AbortController;
  private _url: URL;
  private _eventSourceInit?: EventSourceInit;
  private _requestInit?: RequestInit;
  
  // 上层定义
  onmessage?: (message: JSONRPCMessage) => void;
  constructor(
    url: URL,
    opts?: SSEClientTransportOptions,
  ) {
    this._url = url;
    this._eventSourceInit = opts?.eventSourceInit;
    this._requestInit = opts?.requestInit;
    // 身份验证提供者
    this._authProvider = opts?.authProvider;
  }
  async start() {
    return await this._startOrAuth();
  }
  private _startOrAuth(): Promise<void> {
    return new Promise((resolve, reject) => {
      // SSE 客户端
      this._eventSource = new EventSource(
        this._url.href,
        this._eventSourceInit ?? {
          // 自定义请求实现
          fetch: (url, init) => this._commonHeaders().then((headers) => fetch(url, {
            ...init,
            headers: {
              ...headers,
              Accept: "text/event-stream"
            }
          })),
        },
      );
      // 接收服务器的 endpoint,用于后续通信
      this._eventSource.addEventListener("endpoint", (event: Event) => {
        const messageEvent = event as MessageEvent;
        this._endpoint = new URL(messageEvent.data, this._url);
        resolve();
      });
      // 接收服务端消息
      this._eventSource.onmessage = (event: Event) => {
        const messageEvent = event as MessageEvent;
        let message: JSONRPCMessage;
        try {
          message = JSONRPCMessageSchema.parse(JSON.parse(messageEvent.data));
        } catch (error) {
          this.onerror?.(error as Error);
          return;
        }
        this.onmessage?.(message);
      };
    });
  }
  // 发送消息给客户端
  async send(message: JSONRPCMessage): Promise<void> {
    const headers = new Headers({ ...this._requestInit?.headers });
    headers.set("content-type", "application/json");
    const init = {
      ...this._requestInit,
      method: "POST",
      headers,
      body: JSON.stringify(message),
      signal: this._abortController?.signal,
    };
    const response = await fetch(this._endpoint, init);
  }
}
  • 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.

Server

了解了 Transport 后,继续看 Server 是如何处理来自 Transport 的请求的。

class Server extends Protocol {
  private _clientCapabilities?: ClientCapabilities;
  private _clientVersion?: Implementation;
  private _capabilities: ServerCapabilities;
  private _instructions?: string;
  constructor(
    private _serverInfo: Implementation,
    options?: ServerOptions,
  ) {
    super(options);
    // 支持的能力
    this._capabilities = options?.capabilities ?? {};
    this._instructions = options?.instructions;
    // 注册初始化请求路由和处理函数,该请求用户获取服务端的一些元信息,比如支持的能力
    this.setRequestHandler(InitializeRequestSchema, (request) =>
      this._oninitialize(request),
    );
  }
  private async _oninitialize(
    request: InitializeRequest,
  ): Promise<InitializeResult> {
    const requestedVersion = request.params.protocolVersion;
    this._clientCapabilities = request.params.capabilities;
    this._clientVersion = request.params.clientInfo;
    return {
      protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion)
        ? requestedVersion
        : LATEST_PROTOCOL_VERSION,
      capabilities: this.getCapabilities(),
      serverInfo: this._serverInfo,
      ...(this._instructions && { instructions: this._instructions }),
    };
  }
  // 注册 Server 支持的能力,比如工具、Prompt
  public registerCapabilities(capabilities: ServerCapabilities): void {
    this._capabilities = mergeCapabilities(this._capabilities, capabilities);
  }
}
  • 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.

Server 继承了 Protocol,本身处理了 InitializeRequestSchema 请求,用于返回服务端的元信息,接着看 Protocol。

Protocol


export abstract class Protocol {
  private _transport?: Transport;
  private _requestMessageId = 0;
  private _requestHandlers = new Map();
  private _responseHandlers = new Map();
  // 初始化实例,保存 transport,后续用 transport 和对象通信
  async connect(transport: Transport): Promise<void> {
    this._transport = transport;
    this._transport.onmessage = (message) => {
      // 根据 message 的类型,分别处理请求、响应、通知,因为该类会被 Clien 和 Server 继承
      if (!("method" in message)) {
        this._onresponse(message);
      } else if ("id" in message) {
        this._onrequest(message);
      } else {
        this._onnotification(message);
      }
    };
    await this._transport.start();
  }
  private _onrequest(request: JSONRPCRequest): void {
    const handler = this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler;
    const extra = {
      sessionId: this._transport?.sessionId,
    };
    Promise.resolve()
      .then(() => handler(request, extra)) // 处理请求
      .then(
        (result) => {
          // 发送处理请求的结果
          return this._transport?.send({
            result,
            jsonrpc: "2.0",
            id: request.id,
          });
        })
  }
  private _onresponse(response: JSONRPCResponse | JSONRPCError): void {
    const messageId = Number(response.id);
    const handler = this._responseHandlers.get(messageId);
    this._responseHandlers.delete(messageId);
    handler(response);
  }
  request<T extends ZodType<object>>(
    request: SendRequestT,
    resultSchema: T,
    options?: RequestOptions,
  ): Promise<z.infer<T>> {
    return new Promise((resolve, reject) => {
      // 请求 ID 递增
      const messageId = this._requestMessageId++;
      const jsonrpcRequest: JSONRPCRequest = {
        ...request,
        jsonrpc: "2.0",
        id: messageId,
      };
      // 设置请求和处理函数的映射,在 _onresponse 处理响应
      this._responseHandlers.set(messageId, (response) => {
        try {
          const result = resultSchema.parse(response.result);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      });
      // 发送请求
      this._transport.send(jsonrpcRequest);
    });
  }
  // 注册路由和处理函数
  setRequestHandler<
    T extends ZodObject<{
      method: ZodLiteral<string>;
    }>,
  >(
    requestSchema: T,
    handler: (
      request: z.infer<T>,
      extra: RequestHandlerExtra,
    ) => SendResultT | Promise<SendResultT>,
  ): void {
    const method = requestSchema.shape.method.value;
    // 存入 map 
    this._requestHandlers.set(method, (request, extra) =>
      Promise.resolve(handler(requestSchema.parse(request), extra)),
    );
  }
}
  • 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.

Protocol 基于 Transport 实现了对请求和响应的封装,具体请求和响应的处理由上层的 Client 或 Server 执行。

MCP Server

通过前面的分析可以知道,已经实现了数据通信、路由注册和处理,请求和响应的封装,接着看 MCP Server 的实现,MCP Server 在之前的基础上实现了一系列对外的 API 和提供注册工具、Prompt 等能力。

export class McpServer {
  public readonly server: Server;
  private _registeredTools = {};
  constructor(serverInfo: Implementation, options?: ServerOptions) {
  private _registeredTools: { [name: string]: Tool } = {};
  }
  async connect(transport: Transport): Promise<void> {
    return await this.server.connect(transport);
  }
  private setToolRequestHandlers() {
    // 表示 Server 支持工具
    this.server.registerCapabilities({
      tools: {},
    });
    // 注册获取工具列表路由
    this.server.setRequestHandler(
      ListToolsRequestSchema,
      (): ListToolsResult => ({
        tools: Object.entries(this._registeredTools).map(
          ([name, tool]): Tool => {
            return {
              name,
              description: tool.description,
              inputSchema: tool.inputSchema
                ? (zodToJsonSchema(tool.inputSchema, {
                    strictUnions: true,
                  }) as Tool["inputSchema"])
                : EMPTY_OBJECT_JSON_SCHEMA,
            };
          },
        ),
      }),
    );
    // 注册调用工具路由
    this.server.setRequestHandler(
      CallToolRequestSchema,
      async (request, extra): Promise<CallToolResult> => {
        // 获取对应的工具
        const tool = this._registeredTools[request.params.name];
        const parseResult = await tool.inputSchema.safeParseAsync(
          request.params.arguments,
        );
        // 解析参数
        const args = parseResult.data;
        const cb = tool.callback as ToolCallback<ZodRawShape>;
        return await Promise.resolve(cb(args, extra));
      },
    );
  }
  // 注册工具,name为工具名,description为工具描述,paramsSchema为参数的zod schema,cb为工具的回调函数
  tool(name: string, ...rest: unknown[]): void {
    if (this._registeredTools[name]) {
      throw new Error(`Tool ${name} is already registered`);
    }
    let description;
    if (typeof rest[0] === "string") {
      description = rest.shift() as string;
    }
    let paramsSchema;
    if (rest.length > 1) {
      paramsSchema = rest.shift() as ZodRawShape;
    }
    const cb = rest[0];
    // 记录工具信息
    this._registeredTools[name] = {
      description,
      inputSchema:
        paramsSchema === undefined ? undefined : z.object(paramsSchema),
      callback: cb,
    };
    // 给 MCP Server 注册路由,这样就可以处理客户端的工具相关请求
    this.setToolRequestHandlers();
  }
}
  • 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.

MCP Server 主要提供了一些 API 方便用户注册工具、Prompt 等,如果注册了相关的能力,那么 MCP Server 就会注册对应的路由,这样客户端就可以访问这些路由获取相关能力。

Client

Client 继承了 Protocol 并通过 Transport 实现和 MCP Server 的通信 。

export class Client extends Protocol {
  private _serverCapabilities?: ServerCapabilities;
  private _serverVersion?: Implementation;
  private _capabilities: ClientCapabilities;
  private _instructions?: string;
  constructor(
    private _clientInfo: Implementation,
    options?: ClientOptions,
  ) {
    super(options);
    // 客户端支持的能力
    this._capabilities = options?.capabilities ?? {};
  }
  public registerCapabilities(capabilities: ClientCapabilities): void {
    this._capabilities = mergeCapabilities(this._capabilities, capabilities);
  }
  override async connect(transport: Transport): Promise<void> {
    // 建立和服务器的连接和身份验证,由 Transport 实现提供
    await super.connect(transport);
    try {
      // 发送初始化请求获取服务器元信息,比如支持的能力
      const result = await this.request(
        {
          method: "initialize",
          params: {
            protocolVersion: LATEST_PROTOCOL_VERSION,
            capabilities: this._capabilities,
            clientInfo: this._clientInfo,
          },
        },
        InitializeResultSchema,
      );
      if (result === undefined) {
        throw new Error(`Server sent invalid initialize result: ${result}`);
      }
      if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
        throw new Error(
          `Server's protocol version is not supported: ${result.protocolVersion}`,
        );
      }
      this._serverCapabilities = result.capabilities;
      this._serverVersion = result.serverInfo;
      this._instructions = result.instructions;
      await this.notification({
        method: "notifications/initialized",
      });
    } catch (error) {
      // Disconnect if initialization fails.
      void this.close();
      throw error;
    }
  }
  // 调用工具
  async callTool(
    params: CallToolRequest["params"],
    resultSchema,
    options?: RequestOptions,
  ) {
    return this.request(
      { method: "tools/call", params },
      resultSchema,
      options,
    );
  }
  // 获取工具列表
  async listTools(
    params?: ListToolsRequest["params"],
    options?: RequestOptions,
  ) {
    return this.request(
      { method: "tools/list", params },
      ListToolsResultSchema,
      options,
    );
  }
}
  • 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.

Client 封装了 MCP 协议定义的一系列 API,比如获取工具、Prompt 列表,调用工具等。下面是 MCP 客户端和服务器一次请求的通信过程。

MCP 在 OpenManus 中使用

下面以 OpenManus 为例,看看 MCP 是如何和大模型结合使用的。下面是 run_mcp.py 的代码。

async def run_mcp() -> None:
    """Main entry point for the MCP runner."""
    args = parse_args()
    runner = MCPRunner()
    await runner.initialize(args.connection, args.server_url)
    await runner.run_default()
if __name__ == "__main__":
    asyncio.run(run_mcp())
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

上面的代码中创建了一个 MCPRunner 对象,然后执行它的 initialize 和 run_default 方法,看看 MCPRunner 的实现。

class MCPRunner:
    def __init__(self):
        self.agent = MCPAgent()
    async def initialize(
        self,
        connection_type: str,
        server_url: str | None = None,
    ) -> None:
        // 建立和 MCP 服务器的连接
        await self.agent.initialize(connection_type="sse", server_url=server_url)
    async def run_default(self) -> None:
        # 提示用户输入问题
        prompt = input("Enter your prompt: ")
        # 开始处理用户输入的问题
        await self.agent.run(prompt)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

MCPRunner 是对 MCPAgent 封装,并最终调了 MCPAgent 的 initialize 和 run 方法。接着看 MCPAgent 的实现。

class MCPAgent(ToolCallAgent):
    mcp_clients: MCPClients = Field(default_factory=MCPClients)
    
    async def initialize(
        self,
        connection_type: Optional[str] = None,
        server_url: Optional[str] = None,
        command: Optional[str] = None,
        args: Optional[List[str]] = None,
    ) -> None:
        
        # 初始化和 MCP 服务器的连接,并获取工具列表
        await self.mcp_clients.connect_sse(server_url=server_url)
        # 保存工具相关的实例到 available_tools
        self.available_tools = self.mcp_clients
    async def run(self, request: Optional[str] = None) -> str:
        result = await super().run(request)
        return result
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

MCPAgent 通过 MCPClients 建立和 MCP Server 的连接,然后获取工具列表。看看 MCP Client 的实现。

class MCPClients(ToolCollection):
    session: Optional[ClientSession] = None
    exit_stack: AsyncExitStack = None
    description: str = "MCP client tools for server interaction"
    def __init__(self):
        super().__init__()  # Initialize with empty tools list
        self.name = "mcp"  # Keep name for backward compatibility
        self.exit_stack = AsyncExitStack()
    async def connect_sse(self, server_url: str) -> None:
        streams_context = sse_client(url=server_url)
        streams = await self.exit_stack.enter_async_context(streams_context)
        self.session = await self.exit_stack.enter_async_context(
            ClientSession(*streams)
        )
        await self._initialize_and_list_tools()
    # 从 MCP Server 获取工具列表
    async def _initialize_and_list_tools(self) -> None:
        
        await self.session.initialize()
        # 从 MCP Server 获取工具列表
        response = await self.session.list_tools()
        # 存起来
        self.tools = tuple()
        self.tool_map = {}
        # Create proper tool objects for each server tool
        for tool in response.tools:
            server_tool = MCPClientTool(
                name=tool.name,
                descriptinotallow=tool.description,
                parameters=tool.inputSchema,
                sessinotallow=self.session,
            )
            self.tool_map[tool.name] = server_tool
        self.tools = tuple(self.tool_map.values())
  • 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.

获取工具列表后,就执行 MCPAgent 的 run 启动。

async def run(self, request: Optional[str] = None) -> str:
        
        if self.state != AgentState.IDLE:
            raise RuntimeError(f"Cannot run agent from state: {self.state}")
        if request:
            self.update_memory("user", request)
        results: List[str] = []
        # 循环调大模型
        async with self.state_context(AgentState.RUNNING):
            while (
                self.current_step < self.max_steps and self.state != AgentState.FINISHED
            ):
                self.current_step += 1
                logger.info(f"Executing step {self.current_step}/{self.max_steps}")
                # 不断调 step,子类实现
                step_result = await self.step()
                results.append(f"Step {self.current_step}: {step_result}")
            if self.current_step >= self.max_steps:
                self.current_step = 0
                self.state = AgentState.IDLE
                results.append(f"Terminated: Reached max steps ({self.max_steps})")
        await SANDBOX_CLIENT.cleanup()
        return "\n".join(results) if results else "No steps executed"
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

run 不断调 step 方法,step 方法由子类实现。通过继承链可以看到 MCPAgent 继承 ToolCallAgent,ToolCallAgent 继承 ReActAgent,ReActAgent 实现了 step。

async def step(self) -> str:
        """Execute a single step: think and act."""
        should_act = await self.think()
        if not should_act:
            return "Thinking complete - no action needed"
        return await self.act()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

step 中首先调 think 然后再调 act。这两个方法由 ToolCallAgent 实现。

async def think(self) -> bool:
        
        try:
            # 传入工具调大模型,询问需要执行的工具
            response = await self.llm.ask_tool(
                messages=self.messages,
                # 传入可用的工具
                tools=self.available_tools.to_params(),
                tool_choice=self.tool_choices,
            )
        
        self.tool_calls = tool_calls = (
            response.tool_calls if response and response.tool_calls else []
        )
    async def act(self) -> str:
        results = []
        # 执行模型返回的工具列表
        for command in self.tool_calls:
            result = await self.execute_tool(command)
            results.append(result)
        return "\n\n".join(results)
    # 执行某个工具
    async def execute_tool(self, command: ToolCall) -> str:
       
        name = command.function.name
        if name not in self.available_tools.tool_map:
            return f"Error: Unknown tool '{name}'"
        try:
            # 解析大模型提取的参数
            args = json.loads(command.function.arguments or "{}")
            # 执行工具
            result = await self.available_tools.execute(name=name, tool_input=args)
            # Format result for display (standard case)
            observation = (
                f"Observed output of cmd `{name}` executed:\n{str(result)}"
                if result
                else f"Cmd `{name}` completed with no output"
            )
            return observation
  • 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.

这里以 OpenManus 提供的 Bash 工具为例。

class _BashSession:
    command: str = "/bin/bash"
    async def start(self):
        # 创建一个 bash 进程
        self._process = await asyncio.create_subprocess_shell(
            self.command,
            preexec_fn=os.setsid,
            shell=True,
            bufsize=0,
            stdin=asyncio.subprocess.PIPE,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
    async def run(self, command: str):
        # 让 bash 执行命令
        self._process.stdin.write(
            command.encode() + f"; echo '{self._sentinel}'\n".encode()
        )
        await self._process.stdin.drain()
        # 获取输出
class Bash(BaseTool):
    """A tool for executing bash commands"""
    name: str = "bash"
    description: str = _BASH_DESCRIPTION
    parameters: dict = {
        "type": "object",
        "properties": {
            "command": {
                "type": "string",
                "description": "The bash command to execute. Can be empty to view additional logs when previous exit code is `-1`. Can be `ctrl+c` to interrupt the currently running process.",
            },
        },
        "required": ["command"],
    }
    _session: Optional[_BashSession] = None
    async def execute(
        self, command: str | None = None, restart: bool = False, **kwargs
    ) -> CLIResult:
        if restart:
            if self._session:
                self._session.stop()
            self._session = _BashSession()
            await self._session.start()
            return CLIResult(system="tool has been restarted.")
        if self._session is None:
            self._session = _BashSession()
            await self._session.start()
        if command is not None:
            return await self._session.run(command)
        raise ToolError("no command provided.")
  • 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.

Bash 工具或创建一个子进程并执行大模型从用户输入中提取的命令。从代码来看 OpenManus 只是收集并展示工具的输出,而没有再次把工具的输出传给模型进行下一步的查询。


参考资料:

https://github.com/mannaandpoem/OpenManus/tree/main。

https://github.com/modelcontextprotocol/typescript-sdk。

https://github.com/modelcontextprotocol/servers。

Introduction - Model Context Protocol。

https://onevcat.com/2025/02/mcp/。

责任编辑:姜华 来源: 编程杂技
相关推荐

2021-03-26 22:23:13

Python算法底层

2020-12-17 08:03:57

LinkedList面试源码

2020-12-14 08:03:52

ArrayList面试源码

2025-03-12 00:45:25

MCPJavaSDK

2025-04-01 08:45:56

2025-04-02 10:06:00

2021-03-16 21:45:59

Python Resize机制

2017-04-05 20:00:32

ChromeObjectJS代码

2024-12-23 14:12:41

2020-08-26 14:00:37

C++string语言

2011-06-08 09:22:54

Samba

2021-08-12 07:01:23

FlutterRouter Android

2023-12-25 11:18:12

OpenTeleme应用日志Loki

2020-10-09 14:13:04

Zookeeper Z

2021-04-15 09:07:52

hotspotJavaC++

2024-04-01 00:07:20

LinuxeBPF源码

2023-12-04 07:31:41

Golangwebsocket

2018-10-31 15:54:47

Java线程池源码

2024-11-04 06:00:00

redis双向链表

2023-08-11 08:42:49

泛型工厂继承配置
点赞
收藏

51CTO技术栈公众号