阿里开发者

MCP客户端调用看这一篇就够了(Java版)

Image

如果没有MCP

MCP协议的初衷是希望能将大模型的工具调用来做统一,对于 MCP 的原理介绍的文章已经随处可见,相信大家都有自己的见解,这里简单介绍一些没有MCP之前的痛点问题,帮助大家理解为何需要MCP。

1.客户端:每个工具暴露出来的对接方式都不一样,客户端为了去对接各类工具,需要做很多开发,比如getWeather工具是一个http服务,getLocation是一个HSF服务,并且两种入参,出参的数据结构都不一样,那这时候的对接开发成本就会很高;

2.服务端:A平台Agent和B平台Agent所需要服务的约定不一致,同一个服务需要考虑客户端的约定,开发两套接口,开发成本和维护成本都较高;

因此,MCP的出现约定了在AI开发领域客户端和服务端的对接规范,当然未来也许会有更好用的协议也会替代MCP成为一种新的规范。

大模型调用MCP姿势

框架篇

Spring-AI

引包

这里使用最新的M7版本(此前M6版本中的MCP包里面有部分问题,会将SSE类型的服务端url做改写,导致某些SSE服务调用报错 ,社区在M7版本中已经做了依赖升级)。

<dependency>    <groupId>org.springframework.ai</groupId>    <artifactId>spring-ai-starter-mcp-client</artifactId>    <version>1.0.0-M7</version></dependency>

配置LLM接口

Spring-AI支持的LLM类型有很多,基本涵盖了各个平台的LLM接口规范,具体可以参考官方文档中介绍[1]。

Image

我们平常用OpenAI的规范多一些,并且Idealab中也提供了开放接口,这里我们采用OpenAI接口作为chatModel,当然大家也可自行封装自己的API为OpenAI规范,如Whale上部署的模型也都是支持OpenAI协议的。

#配置chatModel的域名,这里我们使用的idealabspring.ai.openai.base-url=https://idealab.alibaba-inc.com#配置chat的akspring.ai.openai.api-key=脱敏#配置chant的接口路径spring.ai.openai.chat.completions-path=api/openai/v1/chat/completions#配置模型spring.ai.openai.chat.options.model=gpt-4o-0513-global#其他参数配置spring.ai.openai.chat.options.temperature=0.1

模型配置完以后开始注册我们的chatModel:

@ConfigurationpublicclassChatClientConfig {    @Autowired    private ToolCallbackProvider tools;    @Autowired    OpenAiChatModel chatModel;    @Bean    public CommandLineRunner predefinedQuestions(            ConfigurableApplicationContext context) {        return args -> {            // 构建ChatClient,此时不注入任何工具            var chatClient = ChatClient.builder(chatModel)                    .build();            String userInput = "帮我将这个网页内容进行抓取 https://www.shuaijiao.cn/news/view/68320.html";            System.out.println("\n>>> QUESTION: " + userInput);            System.out.println("\n>>> ASSISTANT: " + chatClient.prompt().user(userInput).call().content());
            context.close();        };    }

此时我们先不配置chatModel的工具,只是作为一个最基本的LLM利用Spring-AI配置化框架进行调用看看效果。

Image

此时的回答就是没有任何工具的一个最基础的LLM能力,接下来我们开始为这个chatModel上添加MCP工具,利用框架去直接让大模型调用MCP服务。

配置MCP服务

上述配置chatClient的过程中我们发现,使用框架提供的能力去调用LLM的API比我们自己去写客户端对接API来的方便。

而调用MCP的过程,使用框架的提效会更明显,直接给chatClient注入工具即可,不用我们去手动写functionCall的组装,以及获取到结果后再去调用对应服务。

1.服务端为SSE方式提供

a.设置配置文件

spring.ai.mcp.client.name=ai-demospring.ai.mcp.client.type=SYNCspring.ai.mcp.client.toolcallback.enabled=truespring.ai.mcp.client.request-timeout=30000spring.ai.mcp.client.enabled=true# 配置mcp的服务端sse地址,这里选用了一个开源的抓取网站内容的工具spring.ai.mcp.client.sse.connections.server1.url=https://mcp-09724909-442f-4b85.api-inference.modelscope.cn

b.给chatModel注入工具

Image

运行结果:

Image

此时可以发现,我们的chatModel不再是一个原生的LLM接口,已经可以根据用户意图来自主调用我们的MCP工具。

当然我们也可以看出,利用Spring-AI做MCP调用是非常简单,只需配置好LLM的调用接口和MCP工具地址即可。

作为Demo此时没有任何问题,但如果作为工程实现,比如我们要去做一个助理平台,这时候其实每个助理所绑定的MCP工具是动态的,而非像上述这样在应用启动时初始化好的Bean,这种场景也是可以实现的,Spring-AI也支持在调用过程中动态封装工具,这些工具可以是MCP,也可以是程序中的Bean,或者是某些HTTP、RCP接口等。

Image

2.服务端为stdio方式提供

a.配置Properties

Stdio和SSE配置的区别是将调用方式由显示的服务地址换成npm、java、python等脚本命令直接执行的远程包或本地包。

这里采用本地配置文件的方式去配置Stdio,即spring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-servers-config.json。

spring.ai.mcp.client.name=ai-demo#spring.ai.mcp.client.type=SYNCspring.ai.mcp.client.toolcallback.enabled=truespring.ai.mcp.client.request-timeout=30000spring.ai.mcp.client.enabled=truespring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-servers-config.json

b.配置config.json

Image

这里注入了两个比较经典的MCP服务,百度地图和新闻热点服务:

{  "mcpServers": {    "baidu-map": {      "command": "npx",      "args": [        "-y",        "@baidumap/mcp-server-baidu-map"      ],      "env": {        "BAIDU_MAP_API_KEY": "Qr0GV6v4krVPlIJkupyPpi63d1zXh0Ko"      }    },    "mcp-server-hotnews": {      "command": "npx",      "args": [        "-y",        "@wopal/mcp-server-hotnews"      ]    }  } }

c.给chatModel注入工具

@ConfigurationpublicclassChatClientConfig {    @Autowired    private ToolCallbackProvider tools;    @Autowired    OpenAiChatModel chatModel;    @Bean    public CommandLineRunner predefinedQuestions(            ConfigurableApplicationContext context) {        return args -> {            // 构建ChatClient,注入mcp工具            var chatClient = ChatClient.builder(chatModel).defaultTools(tools.getToolCallbacks())                    .build();
            // 使用ChatClient与LLM交互            String userInput = "帮我查找今天的知乎热帖";            System.out.println("\n>>> QUESTION: " + userInput);            System.out.println("\n>>> ASSISTANT: " + chatClient.prompt().user(userInput).call().content());
            context.close();        };    }}

d.让LLM自主选择工具调用

用户输入为热点查询的提问时:

Image

用户输入为地点检索时:

Image

通过上述例子可以发现,我们在初始化时配置了两个MCP服务,LLM可以根据用户不同提问来自行选择不同的工具去调用,方式也比较简单。

Spring-AI-Alibaba

上述介绍了Spring-AI框架对于MCP调用的支持和使用方式,但在集团内部,Spring-AI-Alibaba项目在此基础上也做了很多封装,更适合集团技术栈和内部中间件的无缝衔接,以及基于Spring-AI项目扩展了很多example项目可以参阅学习。如:Stramable HTTP 方式的MCP调用、OpenManus的实现等等,可以帮助开发者最更上层的封装调用[2]。

Image

基于Spring-AI-Alibaba做MCP客户端的实现与Spring-AI框架基本相似,一些差一点主要有:

1.引入依赖时,在Spring-AI-MCP客户端包的基础上,需要新增com.alibaba.cloud.ai的依赖,如下所示:

<dependency>    <groupId>org.springframework.ai</groupId>    <artifactId>spring-ai-starter-mcp-client</artifactId>    <version>1.0.0-M7</version></dependency><dependency>    <groupId>com.alibaba.cloud.ai</groupId>    <artifactId>spring-ai-alibaba-starter</artifactId>    <version>1.0.0-M6.1</version></dependency>

2.配置chatModel时,Spring-AI-Alibaba支持百炼平台上的模型及API,即

spring:  ai:    dashscope:      # 配置通义千问API密钥      api-key: ${DASH_SCOPE_API_KEY}

3.支持Stramable HTTP 模式的MCP调用,配置方式与Spring-AI的SSE客户端配置方式一致,但Spring-AI-Alibaba底层是自主实现了Stramable HTTP的get和post请求并集成在了Spring-AI框架中,具体可参考[3]。

自研篇

io.modelcontextprotocol.sdk+functionCall

通过上述介绍,我们发现既然框架层面已经为我们封装好了很规范的MCP调用方式,通过简单配置即可实现MCP客户端,本章节将介绍原生的MCP SDK调用方式,对于平台类研发可能更有帮助。

1. 引入依赖

<dependency>    <groupId>io.modelcontextprotocol.sdk</groupId>    <artifactId>mcp</artifactId>    <version>0.9.0</version></dependency>

2. 获取该MCP服务中的所资源(方法、入参、描述等)

private McpSyncClient mcpClient;
    privatestaticfinal String sseServerUrl = "https://mcp-09724909-442f-4b85.api-inference.modelscope.cn";
    @PostConstruct    publicvoidinit(){        try {            McpClientTransport transport = new HttpClientSseClientTransport(sseServerUrl);            mcpClient = McpClient.sync(transport)                    .requestTimeout(Duration.ofSeconds(20L))                    .capabilities(ClientCapabilities.builder()                                          .roots(true)                                          .sampling()                                          .build())                    .build();            mcpClient.initialize();        } catch (Exception e) {            thrownew RuntimeException("初始化MCP客户端对象失败", e);        }    }
    @PreDestroy    publicvoiddestroy(){        if (mcpClient != null) {            mcpClient.closeGracefully();        }    }
    @Bean    public String getToolList(){        ListToolsResult toolsResult = mcpClient.listTools();        for (Tool tool:toolsResult.tools()) {            System.out.println(tool.name());            System.out.println(tool.description());            System.out.println(tool.inputSchema());        }        return null;    }

即初始化构建了MCP客户端和实例关闭动作,通过listTools即可得到该MCP-Server中的所有资源,如下示例:

Image

其中inputSchema即用来作为functionCall中tools中的对象,获取到该描述后,我们就可以在调用functionCall的时候这样填写tools中每个方法中的properties。

Image

3. McpSyncClient来发起调用

@Bean    public String getToolList(){        ListToolsResult toolsResult = mcpClient.listTools();        for (Tool tool:toolsResult.tools()) {            System.out.println(tool.name());            System.out.println(tool.description());            System.out.println(tool.inputSchema());
            Map<String, Object> schemaMap = new HashMap<>();            schemaMap.put("type", "object");            Map<String, Object> properties = new HashMap<>();            if (tool.inputSchema().properties() != null) {                tool.inputSchema().properties().forEach((key, value) -> {                    properties.put(key, value);                });            }            schemaMap.put("properties", properties);
            if (tool.inputSchema().required() != null && !tool.inputSchema().required().isEmpty()) {                schemaMap.put("required", tool.inputSchema().required());            }            Map<String,Object> parameters = new HashMap<>();            parameters.put("url","https://www.shuaijiao.cn/news/view/68320.html");            CallToolResult toolResult = mcpClient.callTool(new CallToolRequest("fetch", parameters));            System.out.println(extractTextContent(toolResult));
        }        return null;    }
    private String extractTextContent(CallToolResult toolResult){        StringBuilder resultText = new StringBuilder();        toolResult.content().forEach(content -> {            if (content instanceof TextContent) {                resultText.append(((TextContent) content).text());            }        });        return resultText.toString();    }

上述示例中,通过mock了一个parameters作为functionCall返回的格式,传给CallToolRequest中,并且显示的指定调用的方法是fetch方法,运行结果如下:

Image

通过上述过程发现,其实调用MCP的SDK去做封装和开发也是比较简单的,只需调用资源获取接口,拿到工具列表传给functionCall,并且将LLM分析出的结果拿到后来调用MCP的call方法即可。

踩坑记录

1. 1.0.0-M6版本SSE方式报错Caused by: java.lang.IllegalStateException: Multiple tools with the same name

解决方式:在启动类上排除SseHttpClientTransportAutoConfiguration 即可。(换成1.0.0-M7版本后没有出现)。

Image

2. SSE方式不带/SSE的时候报错 服务找不到,本质原因是地址url被改写

Image

解决方式:Spring-AI的话升级1.0.0-M6到1.0.0-M7版本,并且单独引入io.modelcontextprotocol.sdk 0.9的版本。

3. 启动时报错MCP服务连接超时

试了很久发现,本身一些MCP服务是做了IP安全证书等等,localhost本身ping不通,可尝试换一些公开可访问的url尝试,如本文示例中的fetch网页内容的url。

思考

本文针对框架和原生SDK的调用都做了总结,那如何选型,或者说如何取舍两种不同技术路线来支撑平台型研发呢?

如果我们自己开发一套这样的流程,其中包括意图识别、工具描述获取、functionCall入参拼接、LLM调用结果获取、工具调用、上下文记忆配置......岂不是需要投入很多的人力成本?甚至说是Spring-AI-Alibaba开源的openManus,如果我们自己开发工作量可想而知,从使用者的视角出发,采用框架固然可以很快的构建出自己的个人超级Agent,但从平台开发视角出发,我们仍然需要调用底层sdk去更好的服务上层,比如现在很多的AI助理平台中,一个Agent不仅仅是支持调用工具,还支持其他Agent的嵌套调用、如RAG知识检索、工作流调用等等,如果采用框架来开发。

一方面需要很大的成本将自己平台的调用姿势对接成框架底层的调用,如将平台某个Agent关联的其他Agent及工具都要抽象成适合框架的工具,才能发起调用;另一方面,框架调用会屏蔽很多处理细节,比如一次规划中到底使用了哪些工具、当前执行到哪个工具,此工具的输出是什么等等,都是需要展示给用户的,框架层面难以将很多个性化的细节一一暴露给用户。

因此面向AI平台研发工程,个人觉得还是需要用原生的方式去打磨平台能力,一些如openMauns等复杂流程,如果在框架中后续暴露出接口,可以结合框架做调用,而不是完全依赖框架。

总结

本文针对MCP客户端的开发,详细介绍了如何通过Spring-AI框架和原生SDK调用MCP服务,并对使用过程中遇到的一些问题进行了记录。整体来说,框架调用简单快捷,适合快速构建应用;而原生SDK则提供更灵活的控制,适合平台级开发。

本文所实践的示例仅基于一次调用过程,但MCP真正发挥其“链接模型和数据”意义可能需要体现在规划反思类场景中,最近也在做此类场景的研发模式探索,大致上有几个方向:1. 提示词中做规划打标,每次结束后重新思考需要调用什么工具  2. 规划本身作为一个工具去每次调用 3. Spring-AI-Alibaba发布的openManus开源代码的设计。后续将在新的文章中做分享。

参考资料

[1]https://docs.spring.io/spring-ai/reference/api/index.html

[2]https://java2ai.com/docs/1.0.0-M6.1/overview/?spm=0.29160081.0.0.79fa20f6jEvcRH

[3]https://java2ai.com/blog/spring-ai-alibaba-mcp-streamable-http/?spm=0.29160081.0.0.1cb05b624WFIFp#%E9%9B%86%E6%88%90%E5%88%B0-spring-ai-%E6%A1%86%E6%9E%B6

[4]https://java2ai.com/docs/1.0.0-M6.1/overview/?spm=0.29160081.0.0.1cb05b624WFIFp

[5]https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html#_standard_mcp_client

[6]https://github.com/springaialibaba/spring-ai-alibaba-examples/blob/main/spring-ai-alibaba-mcp-example/starter-example/client/starter-default-client/pom.xml

[7]https://bailian.console.aliyun.com/?spm=5176.29015046.0.0.1dd4778bb93yqh&tab=app#/app-market

[8]https://openlm.alibaba-inc.com/web/workbench/chatOs/public-tools?projectName=alsc_eleme_iic&pathname=%2Fpublic-tools

[9]https://java2ai.com/mcp/?spm=0.29160081.0.0.1cb05b624WFIFp

OpenLake大数据&AI一体化解决方案

本方案是基于开放可控数据湖仓构建的大数据/搜索/AI一体化解决方案。通过元数据管理平台DLF管理结构化和半/非结构化数据,提供湖仓数据表和文件的安全访问及IO加速。支持多引擎对接和平权协同计算,通过DataWorks统一开发,并保障大规模任务调度。    

点击阅读原文查看详情。