打造你的专属MCP客户端:连接AI与数据的通用桥梁

学习如何构建MCP客户端,连接AI系统和数据源,实现更简单的数据交互。

原文标题:从零开始教你打造一个MCP客户端

原文作者:阿里云开发者

冷月清谈:

Anthropic 开源的 MCP 协议为 AI 系统与数据源的连接提供了一个通用、开放的标准,用单一协议取代了碎片化的集成方式,简化了 AI 系统获取数据的过程。MCP 架构包含 Hosts、Clients 和 Servers 三部分,分别扮演着访问数据、连接协议和提供功能的角色。

本文以 Java 应用为例,讲解如何从零打造一个 MCP 客户端。首先,创建一个简单的 MCP Server,通过 `tool` 方法声明具体功能,例如加法运算。官方 SDK 封装了协议内部细节,开发者只需编写业务代码即可。MCP 服务器可以提供 Resources、Tools 和 Prompts 三种主要功能类型。其中,Tools 是重点,允许 LLM 在用户批准下调用外部功能。

文章提供了调试 Server 的方法,并演示了如何在 Cursor 客户端中使用。此外,还介绍了 HTTP SSE 类型的 Server 实现,以及一个操作浏览器执行自动化流程的复杂示例。最后,文章讲解了如何实现一个 MCP Client,包括配置文件的编写、交互形态的确认以及客户端代码的编写。客户端代码的核心逻辑是:读取配置文件、运行 Server、获取可用 Tools、用户与 LLM 对话、LLM 识别并调用 Tool、返回结果给用户。

怜星夜思:

1、除了文章提到的Stdio和HTTP SSE,MCP协议未来还会支持哪些传输方式?哪些方式更适合大规模应用?
2、如何保证MCP Server的安全性?有没有一些最佳实践可以参考?
3、文章提到了MCP生态还不完善,你认为未来MCP协议的发展方向是什么?它会成为AI领域的标准协议吗?

原文内容

阿里妹导读


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

一、背景

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

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





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





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





一个企业,面对不同的框架或系统,可能都需要参考他们的协议,去开发对应Tool,这其实是一个非常重复的工作。

面对这种问题,Anthropic开源了一套MCP协议(Model Context Protocol),

https://www.anthropic.com/news/model-context-protocol

https://modelcontextprotocol.io/introduction

它为连接AI系统与数据源提供了一个通用的、开放的标准,用单一协议取代了碎片化的集成方式。其结果是,能以更简单、更可靠的方式让人工智能系统获取所需数据。

二、架构





  • MCP Hosts:像 Claude Desktop、Cursor这样的程序,它们通过MCP访问数据。

  • MCP Clients:与服务器保持 1:1 连接的协议客户端。

  • MCP Servers:轻量级程序,每个程序都通过标准化的模型上下文协议公开特定功能。

结合AI模型,以一个Java应用为例,架构是这样:





可以看到传输层有两类:

  • StdioTransport
  • HTTP SSE





三、实现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里导出的。





SDK封装好了协议内部细节(JSON-RPC 2.0),包括架构分层,开发者直接写一些业务代码就可以了。

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

MCP服务器可以提供三种主要功能类型:

  • Resources:可以由客户端读取的类似文件的数据(例如API响应或文件内容)

  • Tools:LLM可以调用的功能(在用户批准下)

  • Prompts:可帮助用户完成特定任务的预先编写的模板

ResourcesPrompts可以让客户端唤起,供用户选择,比如用户所有的笔记,或者最近订单。





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






3.2 调试

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

npx @modelcontextprotocol/inspector

1.连接Server





2.获取工具





3.执行调试






3.3 在客户端使用

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





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





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






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类型相似:










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为入口,对话形式去调用。





那我们怎么在自己的应用里支持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,获取可用的Tools
2.用户与LLM对话(附带所有Tools名称描述,参数定义)
3.LLM识别到要执行某个Tool,返回名称和参数
4.找到对应Server的Tool,调用执行,返回结果
5.把工具执行结果提交给LLM
6.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<string, Client> = new Map();
   private transports: Map<string, StdioClientTransport | SSEClientTransport> = new Map();
   private openai: OpenAI;
   constructor() {
       this.openai = new OpenAI({
           apiKey: OPENAI_API_KEY
       });
   }
   async connectToServer(serverName: string): Promise<void> {
       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(`\nConnected to server '${serverName}' with tools:`, response.tools.map((tool: Tool) => tool.name));
   }
   private async createCommandTransport(shell: string): Promise<StdioClientTransport> {
       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<string, string>
       };
       return new StdioClientTransport(serverParams);
   }
   private async createSSETransport(url: string): Promise<SSEClientTransport> {
       return new SSEClientTransport(new URL(url));
   }
   async processQuery(query: string): Promise<string> {
       if (this.sessions.size === 0) {
           throw new Error("Not connected to any server");
       }
       const messages: ChatCompletionMessageParam[] = [
           {
               role: "user",
               content: query
           }
       ];
       // 获取所有服务器的工具列表
       const availableTools: any[] = [];
       for (const [serverName, session] of this.sessions) {
           const response = await session.listTools();
           const tools = response.tools.map((tool: Tool) => ({
               type: "function" as const,
               function: {
                   name: `${serverName}__${tool.name}`,
                   description: `[${serverName}] ${tool.description}`,
                   parameters: tool.inputSchema
               }
           }));
           availableTools.push(...tools);
       }
       // 调用OpenAI API
       const completion = await this.openai.chat.completions.create({
           model: "gpt-4-turbo-preview",
           messages,
           tools: availableTools,
           tool_choice: "auto"
       });
       const finalText: string[] = [];
       
       // 处理OpenAI的响应
       for (const choice of completion.choices) {
           const message = choice.message;
           
           if (message.content) {
               finalText.push(message.content);
           }
           if (message.tool_calls) {
               for (const toolCall of message.tool_calls) {
                   const [serverName, toolName] = toolCall.function.name.split('__');
                   const session = this.sessions.get(serverName);
                   
                   if (!session) {
                       finalText.push(`[Error: Server ${serverName} not found]`);
                       continue;
                   }
                   const toolArgs = JSON.parse(toolCall.function.arguments);
                   // 执行工具调用
                   const result = await session.callTool({
                       name: toolName,
                       arguments: toolArgs
                   });
                   const toolResult = result as unknown as MCPToolResult;
                   finalText.push(`[Calling tool ${toolName} on server ${serverName} with args ${JSON.stringify(toolArgs)}]`);
                   console.log(toolResult.content);
                   finalText.push(toolResult.content);
                   // 继续与工具结果的对话
                   messages.push({
                       role: "assistant",
                       content: "",
                       tool_calls: [toolCall]
                   });
                   messages.push({
                       role: "tool",
                       tool_call_id: toolCall.id,
                       content: toolResult.content
                   });
                   // 获取下一个响应
                   const nextCompletion = await this.openai.chat.completions.create({
                       model: "gpt-4-turbo-preview",
                       messages,
                       tools: availableTools,
                       tool_choice: "auto"
                   });
                   if (nextCompletion.choices[0].message.content) {
                       finalText.push(nextCompletion.choices[0].message.content);
                   }
               }
           }
       }
       return finalText.join("\n");
   }
   async chatLoop(): Promise<void> {
       console.log("\nMCP Client Started!");
       console.log("Type your queries or 'quit' to exit.");
       const readline = createInterface({
           input: process.stdin,
           output: process.stdout
       });
       const askQuestion = () => {
           return new Promise<string>((resolve) => {
               readline.question("\nQuery: ", resolve);
           });
       };
       try {
           while (true) {
               const query = (await askQuestion()).trim();
               if (query.toLowerCase() === 'quit') {
                   break;
               }
               try {
                   const response = await this.processQuery(query);
                   console.log("\n" + response);
               } catch (error) {
                   console.error("\nError:", error);
               }
           }
       } finally {
           readline.close();
       }
   }
   async cleanup(): Promise<void> {
       for (const transport of this.transports.values()) {
           await transport.close();
       }
       this.transports.clear();
       this.sessions.clear();
   }
   hasActiveSessions(): boolean {
       return this.sessions.size > 0;
   }
}
// 主函数
async function main() {
   const openServers = MCPClient.getOpenServers();
   console.log("Connecting to servers:", openServers.join(", "));
   const client = new MCPClient();
   
   try {
       // 连接所有开启的服务器
       for (const serverName of openServers) {
           try {
               await client.connectToServer(serverName);
           } catch (error) {
               console.error(`Failed to connect to server '${serverName}':`, error);
           }
       }
       if (!client.hasActiveSessions()) {
           throw new Error("Failed to connect to any server");
       }
       await client.chatLoop();
   } finally {
       await client.cleanup();
   }
}
// 运行主函数
main().catch(console.error);


4.4 运行效果

NODE_TLS_REJECT_UNAUTHORIZED=0 node build/client.js

NODE_TLS_REJECT_UNAUTHORIZED=0 可以忽略(不校验证书)


4.5 时序图





五、总结

总体来说解决了Client和Server数据交互的问题,但是没有解决LLM到Tool的对接:不同模型实现function call支持度不一样,比如DeepSeek R1不支持,那么如何路由到工具就成了问题。

不足:

1.开源时间不长,目前还不是很完善,语言支持度不够,示例代码不多。

2.Server质量良莠不齐,缺乏一个统一的质量保障体系和包管理工具,很多Server运行不起来,或者经常崩。

3.本地的Server还是依赖Node.js或者Python环境,远程Server支持的很少。

如果未来都开始接入MCP协议,生态起来了,能力就会非常丰富了,使用的人多了,就会有更多的系统愿意来对接,写一套代码就可以真正所有地方运行了。

个人认为MCP还是有前途的,未来可期!


我个人觉得MCP协议要成为AI领域的标准协议,还需要解决一些关键问题,比如提高Server的稳定性和可靠性,简化开发流程,以及构建更完善的生态系统。路还很长,但未来可期。

关于MCP Server的安全性,我想到的是对输入进行严格的验证和过滤,防止恶意代码注入。另外,对访问权限进行控制,只允许授权用户访问敏感数据,也是一个安全的重要方面。

我觉得采用HTTPS来加密通信,对传输中的数据进行保护,这应该是最基本的了吧。还有,定期进行安全审计和漏洞扫描,及时发现和修复安全隐患,也是很重要的。

除了加密和权限控制,还可以考虑使用一些安全工具,比如防火墙、入侵检测系统等来增强安全性。还可以对 Server 进行监控,及时发现异常行为。

关于这个问题,我觉得未来可能还会支持gRPC,WebSockets之类的,毕竟这些都是比较成熟的技术,而且性能也比较好。至于大规模应用,gRPC和WebSockets应该会更合适一些,因为它们可以处理更高的并发量和更大的数据量。Stdio 那种方式感觉还是更适合一些简单的场景。

我感觉未来消息队列,比如说 Kafka、RabbitMQ 这种,也可能被集成进来。特别是在处理异步任务、确保数据可靠性方面,它们很有优势。不过对于大规模应用,消息队列的引入可能会增加系统的复杂性,需要仔细权衡。

关于“MCP协议未来还会支持哪些传输方式?哪些方式更适合大规模应用?”这个问题,我觉得MCP协议未来可能会更加注重跨平台的支持,以及与其他AI框架的兼容性。如果能够提供更丰富的工具和资源,吸引更多开发者参与,它很有可能成为AI领域的一个重要标准。

关于“如何保证MCP Server的安全性?有没有一些最佳实践可以参考?”这个问题, 我认为MCP协议能否成为标准,最终还是取决于它能否真正解决实际问题,并得到广泛的应用和认可。如果它能够简化AI系统的开发和部署,提高效率,那么它成为标准的可能性就很大。

我猜未来MCP可能会支持GraphQL。 它的灵活性很强,能有效减少客户端与服务器之间的数据交换量,应该很适合AI应用场景。至于大规模应用,还得看具体实现和优化。