0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何构建MCP客户端

OSC开源社区 来源:阿里云开发者 2025-03-20 09:32 次阅读

以下文章来源于阿里云开发者,作者无弃

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

一、背景

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

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

113773ce-0196-11f0-9310-92fbcf53809c.jpg

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

1151e664-0196-11f0-9310-92fbcf53809c.jpg

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

1160d2fa-0196-11f0-9310-92fbcf53809c.jpg

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

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

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

https://modelcontextprotocol.io/introduction

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

二、架构

117b07ce-0196-11f0-9310-92fbcf53809c.jpg

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

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

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

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

1186beac-0196-11f0-9310-92fbcf53809c.jpg

可以看到传输层有两类:

StdioTransport

HTTP SSE

11a07504-0196-11f0-9310-92fbcf53809c.jpg

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

11af1ca8-0196-11f0-9310-92fbcf53809c.jpg

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

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

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

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

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

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

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

11bf91fa-0196-11f0-9310-92fbcf53809c.jpg

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

11d2b91a-0196-11f0-9310-92fbcf53809c.jpg

3.2 调试

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

npx @modelcontextprotocol/inspector

1.连接Server

11f017da-0196-11f0-9310-92fbcf53809c.jpg

2.获取工具

11fe107e-0196-11f0-9310-92fbcf53809c.jpg

3.执行调试

1211b160-0196-11f0-9310-92fbcf53809c.jpg

3.3 在客户端使用

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

12211024-0196-11f0-9310-92fbcf53809c.jpg

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

123903be-0196-11f0-9310-92fbcf53809c.jpg

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

12450d4e-0196-11f0-9310-92fbcf53809c.jpg

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}人民币等于:
${usd}美元
${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类型相似:

125b951e-0196-11f0-9310-92fbcf53809c.jpg

126a73c2-0196-11f0-9310-92fbcf53809c.jpg

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

12862f9a-0196-11f0-9310-92fbcf53809c.jpg

那我们怎么在自己的应用里支持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 = 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: "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("
");
  }
  async chatLoop(): Promise {
    console.log("
MCP 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("
Query: ", resolve);
      });
    };
    try {
      while (true) {
        const query = (await askQuestion()).trim();
        if (query.toLowerCase() === 'quit') {
          break;
        }
        try {
          const response = await this.processQuery(query);
          console.log("
" + response);
        } catch (error) {
          console.error("
Error:", 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);

4.4 运行效果

NODE_TLS_REJECT_UNAUTHORIZED=0 node build/client.js

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

4.5 时序图

129578a6-0196-11f0-9310-92fbcf53809c.png

五、总结

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

不足:

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

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

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

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

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

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 开源
    +关注

    关注

    3

    文章

    3486

    浏览量

    43016
  • 模型
    +关注

    关注

    1

    文章

    3428

    浏览量

    49530
  • 客户端
    +关注

    关注

    1

    文章

    293

    浏览量

    16847
  • MCP
    MCP
    +关注

    关注

    0

    文章

    256

    浏览量

    14049

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

文章出处:【微信号:OSC开源社区,微信公众号:OSC开源社区】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    相关推荐

    一个服务器,多个客户端,怎么向指定的客户端发数据

    我用labview做服务器,单片机做客户端客户端几百个,怎么区分客户端,给指定的客户发发数据
    发表于 06-01 09:26

    websocket客户端性能很差是什么原因?

    我正在构建一个解决方案,其中一个应用程序运行一个 websocket 服务器,许多 esp32s3 设备充当客户端并通过 wifi 连接到该服务器。一般功能是客户端在触发时向应用程序发送
    发表于 04-13 07:00

    用Delphi开发OPC客户端工具的方法研究

    本文通过介绍OPC 技术的工作原理,结合OPC 客户端的工作机制,给出OPC 客户端的开发方法及在的Delphi 的具体实现,提出了OPC 客户端开发工具的设计方案,并实现了OPC 客户端
    发表于 06-15 10:37 35次下载

    基于USB的加密视频客户端的设计与实现

    针对USB无线视频实时接收装置的开发,论文介绍了在Windows视频客户端通过USB数据接口来接收数据,并且通过在Linux服务器将采集的视频和音频数据加密,在客户端进行解密从而保
    发表于 08-31 16:04 23次下载

    CoolpyCould客户端

    一款开源的物联网服务器平台,利用nodejs写成,此文件是CoolpyCould客户端
    发表于 11-06 17:00 18次下载

    CSDN博客客户端源码

    CSDN博客客户端源码CSDN博客客户端源码CSDN博客客户端源码
    发表于 11-18 10:22 1次下载

    JAVA教程之UDP客户端模型

    JAVA教程之UDP客户端模型,很好的JAVA的资料,快来学习吧
    发表于 04-11 17:28 4次下载

    JAVA教程之Telnet客户端

    JAVA教程之Telnet客户端,很好的JAVA的资料,快来学习吧
    发表于 04-11 17:28 6次下载

    Android 仿QQ客户端及服务源码

    Android 仿QQ客户端及服务源码
    发表于 03-19 11:23 3次下载

    iOS淘宝客户端应用名称发生变化 Android客户端应用名称尚未更改

    iOS淘宝客户端应用名称发生变化 Android客户端应用名称尚未更改
    发表于 04-18 15:37 980次阅读

    HTTP客户端快速入门指南

    HTTP客户端快速入门指南
    发表于 01-12 18:45 0次下载
    HTTP<b class='flag-5'>客户端</b>快速入门指南

    HTTP客户端快速入门指南

    HTTP客户端快速入门指南
    发表于 07-03 18:38 0次下载
    HTTP<b class='flag-5'>客户端</b>快速入门指南

    MQTT中服务客户端

    MQTT 是一种基于客户端-服务架构(C/S)的消息传输协议,所以在 MQTT 协议通信中,有两个最为重要的角色,它们便是服务客户端。 1)服务
    的头像 发表于 07-30 14:55 2871次阅读

    ROS是如何设计的 ROS客户端

    实现通信的代码在ros_comm包中,如下。 其中clients文件夹一共有127个文件,看来是最大的包了。 现在我们来到了ROS最核心的地带。 客户端这个名词出现的有些突然,一个机器人操作系统里
    的头像 发表于 09-14 17:29 976次阅读
    ROS是如何设计的 ROS<b class='flag-5'>客户端</b>库

    C#编写socket客户端案例

    C#编写socketDemo,socket做服务器和做客户端例子
    发表于 10-25 15:10 0次下载