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

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

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

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

  • Plugins:OpenAI推出ChatGPT Plugins,首次允许模型通过插件与外部应用交互。插件功能包括实时信息检索(如浏览器访问)、代码解释器(Code Interpreter)执行计算、第三方服务调用(如酒店预订、外卖服务等)
从零开始教你打造一个MCP客户端
  • Function Calling:Function Calling技术逐步成熟,成为大模型与外部系统交互的核心方案。
从零开始教你打造一个MCP客户端
  • Agent框架 Tools: 模型作为代理(Agent),动态选择工具完成任务,比如langchain的Tool。
从零开始教你打造一个MCP客户端

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

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

anthropic.com/news/mode

modelcontextprotocol.io

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


从零开始教你打造一个MCP客户端
  • MCP Hosts:像 Claude Desktop、Cursor这样的程序,它们通过MCP访问数据。
  • MCP Clients:与服务器保持 1:1 连接的协议客户端。
  • MCP Servers:轻量级程序,每个程序都通过标准化的模型上下文协议公开特定功能。

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

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

可以看到传输层有两类:

  • StdioTransport
  • HTTP SSE
从零开始教你打造一个MCP客户端

首先看一个最简单的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

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

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

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

github.com/modelcontext

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

  • Resources:可以由客户端读取的类似文件的数据(例如API响应或文件内容)
  • Tools:LLM可以调用的功能(在用户批准下)
  • Prompts:可帮助用户完成特定任务的预先编写的模板

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

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

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

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

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

npx @modelcontextprotocol/inspector

1.连接Server

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

2.获取工具

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

3.执行调试

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

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

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

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

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

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

从零开始教你打造一个MCP客户端
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 
     
       = {} 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类型相似:

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

操作浏览器执行自动化流程。

可以操作浏览器,Cursor秒变Devin。想象一下,写完代码,编辑器自动打开浏览器预览效果,然后截图给视觉模型,发现样式不对,自动修改。

如果对接好内部系统,贴一个需求地址,自动连接浏览器,打开网页,分析需求,分析视觉稿,然后自己写代码,对比视觉稿,你就喝杯咖啡,静静的看着它工作。

有很多写好的Server,可以直接复用。

  • github.com/modelcontext
  • github.com/punkpeye/awe

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

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

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

使用配置文件来标明有哪些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;

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

大致的逻辑:

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 
      
        = 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(`\nConnected 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: "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 
             
               { 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 
              
                ((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 
               
                 { 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); 
                
               
              
             
            
           
          
         
        
      

NODE_TLS_REJECT_UNAUTHORIZED=0 node build/client.js

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

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

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

不足:

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

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

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

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

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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/239601.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月16日 上午9:17
下一篇 2026年3月16日 上午9:17


相关推荐

关注全栈程序员社区公众号