京东自营 + 国补 iPhone 历史最低价          国家补贴 享8折

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

如果没有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>

大模型调用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]。

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

#配置chatModel的域名,这里我们使用的idealab
spring.ai.openai.base-url=https://idealab.alibaba-inc.com
#配置chat的ak
spring.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:

@Configuration
publicclassChatClientConfig {
    @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配置化框架进行调用看看效果。

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

配置MCP服务

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

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

1.服务端为SSE方式提供

a.设置配置文件

spring.ai.mcp.client.name=ai-demo
spring.ai.mcp.client.type=SYNC
spring.ai.mcp.client.toolcallback.enabled=true
spring.ai.mcp.client.request-timeout=30000
spring.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注入工具

运行结果:

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

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

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

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=SYNC
spring.ai.mcp.client.toolcallback.enabled=true
spring.ai.mcp.client.request-timeout=30000
spring.ai.mcp.client.enabled=true
spring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-servers-config.json

b.配置config.json

这里注入了两个比较经典的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注入工具

@Configuration
publicclassChatClientConfig {
    @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自主选择工具调用

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

用户输入为地点检索时:

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

Spring-AI-Alibaba

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

基于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中的所有资源,如下示例:

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

3. McpSyncClient来发起调用

展开阅读全文

本文系作者在时代Java发表,未经许可,不得转载。

如有侵权,请联系nowjava@qq.com删除。

编辑于

关注时代Java

关注时代Java