MCP客户端开发实战:手把手教你打造一个MCP客户端,保姆级教程!

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

一、背景

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

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

img

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

img

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

img

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

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

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

https://modelcontextprotocol.io/introduction

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

二、架构

img

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

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

img

可以看到传输层有两类:

  • StdioTransport
  • HTTP SSE

img

三、实现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 serverconst server = new McpServer({  name: "Demo",  version: "1.0.0"});
// Add an addition toolserver.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里导出的。

img

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

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

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

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

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

img

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

img

3.2 调试

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

npx @modelcontextprotocol/inspector

1.连接Server

img

2.获取工具

img

3.执行调试

img

3.3 在客户端使用

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

img

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

img

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

img

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

img

img

3.5 一个复杂一点的例子

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

,时长00:56

可以操作浏览器,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为入口,对话形式去调用。

img

那我们怎么在自己的应用里支持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 可以忽略(不校验证书)

,时长00:55

4.5 时序图

img

五、总结

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

不足:

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

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

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

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

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

零基础如何学习AI大模型

领取方式在文末

为什么要学习大模型?

学习大模型课程的重要性在于它能够极大地促进个人在人工智能领域的专业发展。大模型技术,如自然语言处理和图像识别,正在推动着人工智能的新发展阶段。通过学习大模型课程,可以掌握设计和实现基于大模型的应用系统所需的基本原理和技术,从而提升自己在数据处理、分析和决策制定方面的能力。此外,大模型技术在多个行业中的应用日益增加,掌握这一技术将有助于提高就业竞争力,并为未来的创新创业提供坚实的基础。

大模型典型应用场景

AI+教育:智能教学助手和自动评分系统使个性化教育成为可能。通过AI分析学生的学习数据,提供量身定制的学习方案,提高学习效果。
AI+医疗:智能诊断系统和个性化医疗方案让医疗服务更加精准高效。AI可以分析医学影像,辅助医生进行早期诊断,同时根据患者数据制定个性化治疗方案。
AI+金融:智能投顾和风险管理系统帮助投资者做出更明智的决策,并实时监控金融市场,识别潜在风险。

这些案例表明,学习大模型课程不仅能够提升个人技能,还能为企业带来实际效益,推动行业创新发展。

大模型就业发展前景

根据脉脉发布的《2024年度人才迁徙报告》显示,AI相关岗位的需求在2024年就已经十分强劲,TOP20热招岗位中,有5个与AI相关。
在这里插入图片描述字节、阿里等多个头部公司AI人才紧缺,包括算法工程师、人工智能工程师、推荐算法、大模型算法以及自然语言处理等。
在这里插入图片描述
除了上述技术岗外,AI也催生除了一系列高薪非技术类岗位,如AI产品经理、产品主管等,平均月薪也达到了5-6万左右。
AI正在改变各行各业,行动力强的人,早已吃到了第一波红利。

最后

大模型很多技术干货,都可以共享给你们,如果你肯花时间沉下心去学习,它们一定能帮到你!

大模型全套学习资料领取

如果你对大模型感兴趣,可以看看我整合并且整理成了一份AI大模型资料包,需要的小伙伴文末免费领取哦,无偿分享!!!
vx扫描下方二维码即可
加上后会一个个给大家发

在这里插入图片描述

部分资料展示

一、 AI大模型学习路线图

整个学习分为7个阶段
在这里插入图片描述
在这里插入图片描述

二、AI大模型实战案例

涵盖AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,皆可用。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

三、视频和书籍PDF合集

从入门到进阶这里都有,跟着老师学习事半功倍。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

四、LLM面试题

在这里插入图片描述
在这里插入图片描述

五、AI产品经理面试题

在这里插入图片描述

六、deepseek部署包+技巧大全

在这里插入图片描述

😝朋友们如果有需要的话,可以V扫描下方二维码联系领取~
在这里插入图片描述

<think>好的,我现在需要解决用户在使用langchain4j的MCP客户端接收SSE事件时遇到的NullPointerException问题,具体错误是“Cannot invoke 'onEvent' method”。首先,我应该回忆一下相关的知识,并结合提供的引用内容来分析可能的原因。 根据引用1,MCP工具提供者需要构造器传入Builder,并且在提供工具时会遍历mcpClients,然后每个客户端列出工具,构建对应的执行器。这说明MCP客户端在执行工具时涉及到多个步骤,可能涉及到初始化或配置的问题。如果某个环节没有正确配置,可能会导致空指针异常。 用户的问题发生在处理SSE事件时,特别是调用onEvent方法时出现空指针。可能的几个原因:事件监听器未正确注册、回调函数未初始化、或者MCP客户端配置不正确导致相关对象未被创建。 首先,检查事件监听器的注册。如果用户没有正确设置事件处理回调,比如onEvent方法未被实现或传入,那么在事件到达时,试图调用一个空对象的方法就会抛出NPE。这时候需要确保在创建执行请求时,正确设置了事件监听器。 其次,查看MCP客户端的初始化过程。根据引用1,MCP客户端需要正确构建,可能涉及到Builder模式。如果构建过程中遗漏了必要的配置,比如未设置回调处理器,或者未正确传递参数,可能导致执行时所需的回调对象为null。 另外,考虑到SSE是流式处理,可能需要使用StreamingChatLanguageModel,如引用2提到的,langchain4j区分了ChatLanguageModel和StreamingChatLanguageModel。如果用户错误地使用了非流式模型来处理流式事件,可能导致相关处理对象不存在,从而引发空指针。需要确认是否选择了正确的模型接口。 可能的解决方案包括: 1. 确保在创建ExecutionRequest时,正确设置事件监听器,比如通过ExecutionRequest.Builder的onEvent方法注册回调。 2. 检查MCP客户端的配置,确保所有必要的参数都已设置,特别是与SSE处理相关的部分。 3. 确认使用的是支持流式处理的StreamingChatLanguageModel,并正确初始化相关组件。 用户可能需要检查代码中构建ExecutionRequest的部分,是否有类似下面的代码: ExecutionRequest request = ExecutionRequest.builder() .toolName("toolName") .onEvent(event -> { ... }) // 确保这里设置了onEvent回调 .build(); 如果没有设置onEvent,或者传入的lambda表达式有误,就会导致NPE。同时,检查MCP客户端的构建过程,确保所有必需的参数如builder、mcpClients等正确配置,避免工具执行时客户端未初始化。 另外,引用3提到LangChain4J适合与企业系统集成,模块化设计可能要求各个组件正确连接。如果某个模块如事件处理器未正确注入,也可能导致问题。需要确认依赖注入或手动配置是否正确。 总结可能的原因和解决步骤后,再将这些整理成用户容易理解的解决方案,分步骤说明,并给出代码示例,帮助用户检查和修复问题。</think>针对使用LangChain4J MCP客户端接收SSE事件时出现的`NullPointerException: "Cannot invoke 'onEvent' method"`问题,以下是分步解决方案: --- ### **1. 核心原因分析** 该异常通常由以下原因导致: - **未正确注册事件回调**:在构建`ExecutionRequest`时,未通过`.onEvent()`方法设置事件处理器[^1] - **流式模型未初始化**:使用了非流式接口(如`ChatLanguageModel`)处理SSE事件,而非专用的`StreamingChatLanguageModel`[^2] - **MCP客户端配置缺失**:`McpClient`未正确配置执行工具所需参数(如回调处理器或流式支持) --- ### **2. 解决方案步骤** #### **(1) 检查事件回调注册** 确保在构建`ExecutionRequest`时显式定义事件处理器: ```java ExecutionRequest request = ExecutionRequest.builder() .toolName("your_tool_name") .parameters(params) .onEvent(event -> { // 关键点:必须注册回调 System.out.println("Received event: " + event.getContent()); }) .build(); ``` #### **(2) 确认使用流式模型** - 在工具执行时,需通过`StreamingChatLanguageModel`接口处理流式响应 - 检查`McpToolProvider`初始化代码: ```java // 正确示例:使用流式模型构建客户端 McpClient mcpClient = McpClient.builder() .streamingChatLanguageModel(streamingModel) // 注入流式模型 .build(); ``` #### **(3) 验证MCP客户端配置** 检查`McpToolProvider`构建过程是否关联了正确客户端: ```java McpToolProvider provider = new McpToolProvider.Builder() .mcpClients(List.of(mcpClient)) // 确保客户端列表非空 .build(); ``` --- ### **3. 完整代码示例** ```java // 1. 初始化流式模型 StreamingChatLanguageModel streamingModel = ...; // 2. 构建MCP客户端 McpClient mcpClient = McpClient.builder() .streamingChatLanguageModel(streamingModel) .build(); // 3. 配置工具提供者 McpToolProvider provider = new McpToolProvider.Builder() .mcpClients(List.of(mcpClient)) .build(); // 4. 执行工具时注册事件回调 ToolExecutor executor = provider.getToolExecutor("toolName"); executor.execute( ExecutionRequest.builder() .toolName("toolName") .onEvent(event -> { // 明确设置事件处理器 handleSSEEvent(event); }) .build() ); ``` --- ### **4. 补充验证** - **空值检查**:在回调方法中添加`if (event != null)`验证 - **日志调试**:在`onEvent`回调中打印日志,确认事件触发机制正常 - **依赖版本**:检查`langchain4j-mcp`与核心库版本兼容性 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值