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 的处理过程如下。
- 客户端发起 SSE 请求,服务器收到 SSE 请求时,记录对应的响应对象和设置通信路由,返回通信路由给客户端。
- 客户端收到 SSE 通信路径后,后续通过 HTTP POST 请求到服务端返回的路由。
- 服务端收到 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/。