所有文章 > AI驱动 > 从零开始教你打造一个MCP客户端
从零开始教你打造一个MCP客户端

从零开始教你打造一个MCP客户端

阿里妹导读Anthropic开源了一套MCP协议,它为连接AI系统与数据源提供了一个通用的、开放的标准,用单一协议取代了碎片化的集成方式。本文教你从零打造一个MCP客户端。

一、背景

如何让大语言模型与外部系统交互,一直是AI系统需要解决的问题:

  • Plugins:OpenAI推出ChatGPT Plugins,首次允许模型通过插件与外部应用交互。插件功能包括实时信息检索(如浏览器访问)、代码解释器(Code Interpreter)执行计算、第三方服务调用(如酒店预订、外卖服务等)

image

Function Calling:Function Calling技术逐步成熟,成为大模型与外部系统交互的核心方案。

image

Agent框架 Tools: 模型作为代理(Agent),动态选择工具完成任务,比如langchain的Tool。

image

一个企业,面对不同的框架或系统,可能都需要参考他们的协议,去开发对应Tool,这其实是一个非常重复的工作。面对这种问题,Anthropic开源了一套MCP协议(Model Context Protocol),https://www.anthropic.com/news/model-context-protocol https://modelcontextprotocol.io/introduction 它为连接AI系统与数据源提供了一个通用的、开放的标准,用单一协议取代了碎片化的集成方式。其结果是,能以更简单、更可靠的方式让人工智能系统获取所需数据。

二、架构

image

MCP Hosts:像 Claude Desktop、Cursor这样的程序,它们通过MCP访问数据。MCP Clients:与服务器保持 1:1 连接的协议客户端。MCP Servers:轻量级程序,每个程序都通过标准化的模型上下文协议公开特定功能。结合AI模型,以一个Java应用为例,架构是这样:

image

可以看到传输层有两类:StdioTransportHTTP SSE

image

三、实现MCP Server

首先看一个最简单的MCP Server例子:

`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: "Demo",
version: "1.0.0"
});

// Add an addition tool
server.tool(
"add",
"Add two numbers",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);

async function main() {
// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);
}

main();
`

代码头部和底部都是一些样板代码,主要变化的是在tool这块,这个声明了一个做加法的工具。这就是一个最简单的可运行的Server了。同时也可以使用官方的脚手架,来创建一个完整复杂的Server:

npx @modelcontextprotocol/create-server my-server

3.1 使用SDK从上面代码可以看到很多模块都是从@modelcontextprotocol/sdk 这个SDK里导出的。

image

SDK封装好了协议内部细节(JSON-RPC 2.0),包括架构分层,开发者直接写一些业务代码就可以了。https://github.com/modelcontextprotocol/typescript-sdk MCP服务器可以提供三种主要功能类型:Resources:可以由客户端读取的类似文件的数据(例如API响应或文件内容)Tools:LLM可以调用的功能(在用户批准下)Prompts:可帮助用户完成特定任务的预先编写的模板Resources和Prompts可以让客户端唤起,供用户选择,比如用户所有的笔记,或者最近订单。

image

重点在Tools,其他很多客户端都不支持。

image

3.2 调试如果写好了代码,怎么调试这个Server呢?官方提供了一个调试器:

npx @modelcontextprotocol/inspector

1.连接Server

image

2.获取工具

image

3.执行调试

image

3.3 在客户端使用如果运行结果没错,就可以上架到支持MCP协议的客户端使用了,比如Claude、Cursor,这里以Cursor为例:

image

在Cursor Composer中对话,会自动识别这个Tool,并寻求用户是否调用

image

点击运行,就可以调用执行:

image

3.4 HTTP SSE类型Server

`import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { z } from "zod";

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

server.tool(
"exchange",
"人民币汇率换算",
{ rmb: z.number() },
async ({ rmb }) => {
// 使用固定汇率进行演示,实际应该调用汇率API
const usdRate = 0.14; // 1人民币约等于0.14美元
const hkdRate = 1.09; // 1人民币约等于1.09港币

const usd = (rmb * usdRate).toFixed(2);
const hkd = (rmb * hkdRate).toFixed(2);

return {
content: [{
type: "text",
text: ${rmb}人民币等于:\n${usd}美元\n${hkd}港币 }] }

},
);

const app = express();
const sessions: Record<string, { transport: SSEServerTransport; response: express.Response }> = {};

app.get("/sse", async (req, res) => {
console.log(New SSE connection from ${req.ip});

const sseTransport = new SSEServerTransport("/messages", res);
const sessionId = sseTransport.sessionId;

if (sessionId) {
sessions[sessionId] = { transport: sseTransport, response: res }
}

await server.connect(sseTransport);
});

app.post("/messages", async (req, res) => {
const sessionId = req.query.sessionId as string;
const session = sessions[sessionId];

if (!session) {
res.status(404).send("Session not found");
return;
}

await session.transport.handlePostMessage(req, res);
});

app.listen(3001);
`

核心的差别在于需要提供一个sse服务,对于Tool基本一样,但是sse类型就可以部署在服务端了。上架也和command类型相似:

image

image

3.5 一个复杂一点的例子操作浏览器执行自动化流程。可以操作浏览器,Cursor秒变Devin。想象一下,写完代码,编辑器自动打开浏览器预览效果,然后截图给视觉模型,发现样式不对,自动修改。如果对接好内部系统,贴一个需求地址,自动连接浏览器,打开网页,分析需求,分析视觉稿,然后自己写代码,对比视觉稿,你就喝杯咖啡,静静的看着它工作。3.6 MCP Server资源有很多写好的Server,可以直接复用。https://github.com/modelcontextprotocol/servers https://github.com/punkpeye/awesome-mcp-servers/blob/main/README-zh.md

四、实现MCP Client

一般MCP Host以一个Chat box为入口,对话形式去调用。

image

那我们怎么在自己的应用里支持MCP协议呢?这里需要实现MCP Client。

4.1 配置文件

使用配置文件来标明有哪些MCP Server,以及类型。

const config = [
{
name: 'demo-stdio',
type: 'command',
command: 'node ~/code-open/cursor-toolkits/mcp/build/demo-stdio.js',

isOpen: true

},
{
name: 'weather-stdio',
type: 'command',
command: 'node ~/code-open/cursor-toolkits/mcp/build/weather-stdio.js',

isOpen: true

},
{
name: 'demo-sse',

type: 'sse',

url: 'http://localhost:3001/sse',

isOpen: false

}
];
export default config;

4.2 确认交互形态

MCP Client主要还是基于LLM,识别到需要调用外部系统,调用MCP Server提供的Tool,所以还是以对话为入口,可以方便一点,直接在terminal里对话,使用readline来读取用户输入。大模型可以直接使用openai,Tool的路由直接使用function calling。

4.3 编写Client

大致的逻辑:1.读取配置文件,运行所有Server,获取可用的Tools2.用户与LLM对话(附带所有Tools名称描述,参数定义)3.LLM识别到要执行某个Tool,返回名称和参数4.找到对应Server的Tool,调用执行,返回结果5.把工具执行结果提交给LLM6.LLM返回分析结果给用户使用SDK编写Client代码

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport, StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import OpenAI from "openai";
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import { ChatCompletionMessageParam } from "openai/resources/chat/completions.js";
import { createInterface } from "readline";
import { homedir } from 'os';
import config from "./mcp-server-config.js";

// 初始化环境变量

const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
if (!OPENAI_API_KEY) {
throw new Error("OPENAI_API_KEY environment variable is required");
}
interface MCPToolResult {
content: string;
}
interface ServerConfig {
name: string;
type: 'command' | 'sse';
command?: string;
url?: string;
isOpen?: boolean;
}
class MCPClient {
static getOpenServers(): string[] {
return config.filter(cfg => cfg.isOpen).map(cfg => cfg.name);
}
private sessions: Map = new Map();
private transports: Map = new Map();
private openai: OpenAI;
constructor() {
this.openai = new OpenAI({
apiKey: OPENAI_API_KEY
});
}
async connectToServer(serverName: string): Promise {
const serverConfig = config.find(cfg => cfg.name === serverName) as ServerConfig;
if (!serverConfig) {
throw new Error(Server configuration not found for: ${serverName});
}
let transport: StdioClientTransport | SSEClientTransport;
if (serverConfig.type === 'command' && serverConfig.command) {
transport = await this.createCommandTransport(serverConfig.command);
} else if (serverConfig.type === 'sse' && serverConfig.url) {
transport = await this.createSSETransport(serverConfig.url);
} else {
throw new Error(Invalid server configuration for: ${serverName});
}
const client = new Client(
{
name: "mcp-client",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);
await client.connect(transport);
this.sessions.set(serverName, client);
this.transports.set(serverName, transport);

// 列出可用工具

const response = await client.listTools();
console.log(`
Connected to server '${serverName}' with tools:`, response.tools.map((tool: Tool) => tool.name));
}
private async createCommandTransport(shell: string): Promise {
const [command, ...shellArgs] = shell.split(' ');
if (!command) {
throw new Error("Invalid shell command");
}

// 处理参数中的波浪号路径

const args = shellArgs.map(arg => {
if (arg.startsWith('~/')) {
return arg.replace('~', homedir());
}
return arg;
});
const serverParams: StdioServerParameters = {

command,
args,

env: Object.fromEntries(
Object.entries(process.env).filter(([_, v]) => v !== undefined)
) as Record
};
return new StdioClientTransport(serverParams);
}
private async createSSETransport(url: string): Promise {
return new SSEClientTransport(new URL(url));
}
async processQuery(query: string): Promise {
if (this.sessions.size === 0) {
throw new Error("Not connected to any server");
}
const messages: ChatCompletionMessageParam[] = [
{

`role:

原文转载自:https://mp.weixin.qq.com/s/zYgQEpdUC5C6WSpMXY8cxw

#你可能也喜欢这些API文章!