MCP 探索与实践

Mar 16 · 20min

最近在学习 AI 领域相关知识,接触到了 MCP(Model Context Protocol),感觉很有意思,所以做了一些探索和实践,这里做下记录和分享。PS:文中关于 MCP 的概念介绍主要参考了 Akshay 在 X 上的分享 Model Context Protocol (MCP), clearly explained,基于此做了翻译和补充。

什么是 MCP ?

MCP(Model Context Protocol,模型上下文协议)是一种通信协议,旨在解决大型语言模型(LLM)与外部数据源及工具之间的无缝集成。它允许 AI 模型获取更丰富的上下文信息,并调用外部工具执行操作,从而提供更准确、更有价值的响应。

MCP 就像 AI 应用的 USB-C 接口。正如 USB-C 为设备与各类配件提供了标准化的连接方式,MCP 也为 AI 应用与不同数据源和工具之间的连接实现了标准化。

MCP 架构

MCP 核心遵循客户端-服务端架构,在这种架构下,一个主机应用程序可以连接到多个服务器。核心组件包含:HostClientServer

MCP Host 组件指的是一些 AI 应用程序,例如 Claude APP、Cursor,它为 AI 交互提供环境,能够访问工具和数据,并且运行 MCP ClientMCP Client 组件在 MCP Host 内部运行,用于实现与 MCP Server 的通信。

MCP Server 组件主要是暴露具体特定功能的实现,例如访问 API、Database、File System 等。MCP Server 提供了三项关键能力:

  • Tools:为 LLM 提供一些特定工具,允许 LLM 通过工具调用执行特定任务。
  • Resources:为 LLM 提供一些数据和内容。
  • Prompts:为 LLM 提供可复用的提示词模版和工作流。

MCP 通信

Capability Exchange(能力交换)

在给定的上下文中,客户端向服务器发送初始化请求以了解服务器的能力。服务器则回应其能力的详细信息。例如,一个 Weather API 服务器提供可用的 Tools 来调用 API,Prompts 以及将 API 文档作为 Resource。这里的 "能力交换" 指的就是 ClientServer 之间关于 MCP Server 功能和资源的信息交互过程。

Message Exchange(消息交换)

MCP ClientMCP Server 确认连接成功后,就可以继续进一步的消息交换。与传统的单向 API 通信不同的是,MCP 通信是双向的,这意味着 ClientServer 可以同时发送和接收消息。如果需要,客户端也可以允许 MCP Server 在不需要 API 密钥的情况下利用 MCP Host 的 AI 能力(e.g. LLM completions or generations),而 MCP Client 则维护对模型访问和权限的控制。

MCP 消息格式

MCP 消息格式采用 JSON-RPC 2.0 标准,主要有三种消息类型:

1. Requests:这些请求需要响应。它们包含 method和可选的 params

{
  jsonrpc: "2.0",
  id: number | string,
  method: string,
  params?: object
}

2. Responses:响应请求时发送的响应。响应包含 result(成功) 或 error(失败)

{
  jsonrpc: "2.0",
  id: number | string,
  result?: object,
  error?: {
    code: number,
    message: string,
    data?: unknown
  }
}

3. Notifications:这些是单向消息,不需要响应。与请求一样,它们包含 method 和可选的 params

{
  jsonrpc: "2.0",
  method: string,
  params?: object
}

MCP 传输机制

MCP 支持不同的传输机制进行通信。其中有两种内置的传输机制:

1. Standard Input/Output (stdio)

这种传输机制使用标准输入输出流进行通信,适用于本地集成和命令行工具。

// example (Server)

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'

const server = new McpServer({
  name: 'example-server',
  version: '1.0.0'
}, { capabilities: {} })

const transport = new StdioServerTransport()
await server.connect(transport)
// example (Client)

import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'

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

const transport = new StdioClientTransport({
  command: './server', //  Path to your server executable
  args: ['--option', 'value'] // Optional arguments
})
await client.connect(transport)

使用 stdio 传输机制 时:

  • MCP Client将 MCP Server 作为子进程启动。
  • 服务器通过 stdin 接收 JSON-RPC 消息并通过 stdout 做出响应。
  • 消息必须以换行符分隔。
  • MCP Server 可能用 stderr 记录日志。
  • 重要提示:MCP Server 不得向 stdout 写入任何无效的 MCP 消息,MCP Client 也不得向 MCP Server 的 stdin 写入任何无效的 MCP 消息。

2. Server-Sent Events (SSE)

这种传输机制使用 HTTP POST 请求进行客户端到服务器通信,并使用 Server-Sent Events 进行服务器到客户端流式传输。

// example (Server)

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import express, { Response } from 'express';

const server = new McpServer({
  name: "example-server",
  version: "1.0.0"
}, { capabilities: {} });

const app = express();
app.use(express.json());

app.get("/sse", async (req, res) => {
  const transport = new SSEServerTransport("/messages", res as Response);
  await server.connect(transport);
  // Store the transport instance for later use. For simplicity, we assume a single client here.
  app.locals.transport = transport;
});

app.post("/messages", async (req, res) => {
  const transport = app.locals.transport;
  await transport.handlePostMessage(req, res);
});

app.listen(3000, () => {
  console.log('Server listening on port 3000');
});

// Note: For simplicity, this example doesn't handle routing for multiple connections.
// In a production environment, you'd need to route messages to the correct transport instance
// based on some identifier (e.g., a session ID).
// example (Client)

import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'

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

const transport = new SSEClientTransport(
  new URL('http://localhost:3000/sse')
)
await client.connect(transport)

3. Custom Transport

MCP 支持自定义传输机制,自定义传输机制需要实现 Transport 接口:

interface Transport {
  // Start processing messages
  start(): Promise<void>;

  // Send a JSON-RPC message
  send(message: JSONRPCMessage): Promise<void>;

  // Close the connection
  close(): Promise<void>;

  // Callbacks
  onclose?: () => void;
  onerror?: (error: Error) => void;
  onmessage?: (message: JSONRPCMessage) => void;
}

MCP Server 实践

在对 MCP 有了基本了解后,我分别在工作和开源中做了两个 MCP Server 的实践,下面跟大家做下简单的分享:

BFF MCP Server

公司团队内部有一个 BFF 服务,作为前端和后端之间的桥梁,主要负责面向后端进行微服务聚合和数据编排、面向前端提供视图层数据处理。

在日常的前后端 API 联调过程中,API 文档无疑是最重要的资源。如果能让前端开发人员通过 Chat with AI 直接获取 API 文档的详细内容,将极大提升 API 对接效率。在我们团队中,我们主要使用 Apifox 来维护 API 文档。因此,我们只需创建一个 MCP Server 并定义相应的 Tools 与 Apifox API 交互,即可实现通过 Chat with AI 查询 API 文档的功能。

在 BFF架构中,一个 API 可能会聚合多个微服务及其相关接口。当后端开发人员排查问题时,往往需要查看 BFF API 所依赖的微服务信息。在我们现有的 BFF 实现中,我们已通过特定技术方案生成了 API 依赖关系图。同样地,我们只需定义一个 Tools 来动态获取这些依赖关系图,就能让后端开发人员通过 Chat with AI 快速获取所需信息,有效解决他们在开发过程中遇到的问题。

Vue MCP Server

在 Vue 应用中,我们同样可以实现一个 MCP Server,来实现通过 Chat with AI 查询 Vue 应用中的数据信息,例如组件树、组件数据、路由信息等。

在 Vue MCP Server 的实现中,我主要利用了 Vite Dev Server 来搭建 MCP Server 的 SSE 通信层。通过集成 Vue DevTools Kit,我们可以获取 Vue 应用的运行时数据。同时,借助 Vite Dev Server 提供的通信能力,我们能够与 Vue 应用进行实时交互,从而实现一个功能完备的 Vue MCP Server。具体实现流程如下图所示:

接下来我们来看一下 Vue MCP Server 提供的能力:

获取 Vue 应用组件树

获取 Vue 应用组件数据

获取 Vue 应用路由信息

获取 Pinia Store 树

获取 Pinia Store 数据

Vue MCP 目前已在 Github 上开源,如果你对它感兴趣,可以在这里查看更多信息。

CC BY-NC-SA 4.0 2024-PRESENT © Arlo