一个简单的java基于spring-ai构建mcp server以及mcp client案例

 

  简介

  • 本文主要基于spring-ai 框架构建,目前mcp官方的示例中对于mcp相关的java实现,也是用的spring-ai举例,由此可见java这边使用spring实现应该是标准了。
  • 当然官方也有单独的sdk提供,感兴趣的可以用这个sdk自己实现
  • 官方MCP简介https://mcp-docs.cn/introduction  可以找到里面的快速开始-面向服务器开发者/面向客户端开发者,查看详细代码示例,本文主要偏向于应用,要了解概念等可以参考该网站
  • 一般来说mcp server分为stdio和sse,本文主要用sse作为案例,结尾也有个简单的stdio案例
    • stdio一般和客户端在同一个机器上启动,用来提供一些本地功能例如根据需求生成文件等,当然也可以作为远程调用的agent,client和server通过标准的输入输出流进行交互
    • sse一般用来提供一些远程服务,比如实时查询天气状况,股票情况,应用数据等实时数据或者提供公共服务,client和server通过http的sse进行交互

 

    

    

      

      

  准备

  • springboot3

  • java17 (springboot3依赖)

  • deepchat (测试mcp使用 https://deepchat.thinkinai.xyz/)

  • 模型 base_url,api_key (例如deepseek https://api-docs.deepseek.com/zh-cn/)   

  • 本项目包含一个父项目和两个子项目mcp-server和mcp-client

 

 

  创建父项目(整个项目结构结尾有)

    父项目pom.xml如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.4</version>
        <relativePath/>
    </parent>
    <groupId>org.example</groupId>
    <artifactId>zm-mcp</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <modules>
        <module>mcp-server</module>
        <module>mcp-client</module>
    </modules>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>1.0.0-M7</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <repositories>
        <repository>
            <id>alimaven</id>
            <name>aliyun maven</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
        </repository>
    </repositories>
</project>

 

  创建子项目MCP-SERVER

    该项目作为mcp的server端,对外提供mcp功能

    1. pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.example</groupId>
        <artifactId>zm-mcp</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>mcp-server</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
        </dependency>
    </dependencies>
</project>

    2.定义application.yml

server:
  port: 9999
spring:
  ai:
    mcp:
      server:
        type: async

    3.定义工具

package com.zm.ai.server.compnent;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

import java.sql.SQLException;
import java.util.Map;

@Component
public class ToolComponent {

    @Tool(description = "可以获取到某个城市的天气信息")
    public Map<String, String> getWeather(@ToolParam(required = true,description = "入参城市,请传入城市拼音") String city) throws SQLException {
        System.out.println("获取到了城市信息:" + city);
        return Map.of("datas",city + "的天气为大雪,温度为零下5度到零下10度");
    }
}

    4.注册工具

package com.zm.ai.server.compnent;

import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class ToolRegister {
    
    @Bean
    public ToolCallbackProvider mcpTools(ToolComponent component){
        return MethodToolCallbackProvider.builder().toolObjects(component).build();
    }

}

    5.springboot启动类

package com.zm.ai.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServerApplication.class,args);
    }
}

    6.启动并关注日志(Registered tools: 1, notification: true)

2025-06-06T11:07:20.661+08:00  INFO 19876 --- [           main] com.zm.ai.server.ServerApplication       : No active profile set, falling back to 1 default profile: "default"
2025-06-06T11:07:21.448+08:00  INFO 19876 --- [           main] o.s.a.m.s.a.McpServerAutoConfiguration   : Registered tools: 1, notification: true
2025-06-06T11:07:21.587+08:00  INFO 19876 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port 9999 (http)
2025-06-06T11:07:21.592+08:00  INFO 19876 --- [           main] com.zm.ai.server.ServerApplication       : Started ServerApplication in 1.219 seconds (process running for 1.587)

    

    7.使用mcp-sdk测试改mcp是否发布成功

    sdk的包

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

    测试代码

public class MCPClientStarter {

    public static void main(String[] args) {
        HttpClientSseClientTransport transport = HttpClientSseClientTransport.builder("http://10.181.20.3:9999").build();
        McpSyncClient client = McpClient.sync(transport).build();

        client.initialize();
        System.out.println("是否已经初始化:" + client.isInitialized());

        McpSchema.ListToolsResult listToolsResult = client.listTools();
        List<McpSchema.Tool> tools = listToolsResult.tools();
        System.out.println("获取到的tools:" + tools.stream().map(McpSchema.Tool::name).collect(Collectors.joining(",")));
        for (McpSchema.Tool tool : tools) {
            McpSchema.JsonSchema jsonSchema = tool.inputSchema();
            System.out.println(jsonSchema);
            McpSchema.CallToolResult callToolResult = client.callTool(new McpSchema.CallToolRequest(tool.name(), Map.of("city", "北京")));

            System.out.println("获取到的结果为:==========");
            callToolResult.content().forEach(System.out::println);
        }

    }
}

    输出结果为

是否已经初始化:true
获取到的tools:getWeather
JsonSchema[type=object, properties={city={type=string, description=入参城市,请传入城市拼音}}, required=[city], additionalProperties=false, defs=null, definitions=null]
获取到的结果为:==========
TextContent[audience=null, priority=null, text={"datas":"北京的天气为大雪,温度为零下5度到零下10度"}]

 

    8.使用deepchat测试该mcp是否正常,并配合大模型使用

      假设已经安装好deepchat,并已经配置了base_urlapi_key,我们先来问deepchat一个天气问题,案例此处使用deepseek-r1模型

    

 

      可以看到deepseek无法回答出天气,因为模型中并不提供实时天气信息查询的功能

      此时我们添加mcp,点击会话下面的锤子,或者右上角的设置->MCP设置,均可添加mcp工具,我们添加刚启动的工具

 

   注意服务器类型选sse,url即上面的服务器的url,此处注意如果工具不通,可以尝试把localhost更换为本机局域网ip

   保存成功后我们在mcp工具中进行测试

    启动该mcpserver并重新询问天气,模型已经成功调用mcp server处理了

 

 

 

    

 

  创建子项目MCP-CLIENT

    某些时候我们可能需要自己开发一个ai-agent,而不是用deepchat这种东西,那我们怎么使用mcp呢

    1. pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.example</groupId>
        <artifactId>zm-mcp</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>mcp-client</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-autoconfigure-model-tool</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-client-chat</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-openai</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
    </dependencies>
</project>

    2. application.yml  本案例使用deepseek,api-keybase-url记得配置自己的

server:
  port: 8888
spring:
  ai:
    mcp:
      client:
        toolcallback:
          enabled: true
        enabled: true
        type: ASYNC
        sse:
          connections:
            weather:
              url: http://localhost:9999
    openai:
      api-key: ***your api key***
      base-url: ***base_url***
      chat:
        options:
          model: deepseek-r1

    3.新增一个模型调用工具 

package com.zm.ai.client.component;

import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.stereotype.Service;

@Service
public class ChatTestTool {

    @Resource
    private OpenAiChatModel openAiChatModel;

    @Resource
    private ToolCallbackProvider callbackProvider;

    public void call(){
        ChatClient client = ChatClient.builder(openAiChatModel).defaultTools(callbackProvider).defaultSystem("你是一个很可爱的智能助手,不论回答什么问题,都请用比较可爱的语气回答").build();

        String  res = client.prompt("能帮我查询一下北京的天气吗").call().content();

        System.out.println("获取到结果res===========");
        System.out.println(res);
    }

}

    4.application启动

package com.zm.ai.client;

import com.zm.ai.client.component.ChatTestTool;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class ServerApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(ServerApplication.class, args);

        ChatTestTool testTool = context.getBean(ChatTestTool.class);
        testTool.call();

    }
}

  5.得到结果

 

  总结

    到此,我们成功的创建了mcp server服务,并且使用java sdk,deepchat工具进行了调试和使用。

    也成功的创建了一个ai-agent整合了mcp功能,如果追踪源代码我们可以得知,在mcp-client中,openai-model实现自动调用mcp服务的流程也比较易懂。通过mcp协议调用server的tools/list获取到tools信息,将tools的name,description和input-schema转换为标准的llm的tool-message信息。通过llm的回复递归决定是否调用tools,直到最终不需要调用tools后进行返回。

    这个流程可以参考上一篇文章https://www.cnblogs.com/hetutu-5238/p/18904347

    当然应该也有不支持tools的chatmodel,比如我引入了百度千帆的依赖,发现在调用call方法时,并没有把mcp提供的tool带入模型或者调用tool,个人猜测spring-ai在编写各个平台的chatmodel时应该也是根据该模型平台是否支持tools来决定是否加入tools相关代码的  

    个人猜测如果我们在对方不不支持tools的情况下,想要利用,可能要自己增加上下文提示,让模型输出中包含工具调用相关结构化内容,我们自己解析然后决定是否要调用工具

    个人感觉mcp的功能更像是整合了外部工具的使用标准,和模型上下文。这样不用再自己手动根据结果解析是否调用工具以及编写参数逻辑。这样工具提供方可以只按照mcp协议制作工具即可直接给外部的ai应用使用。

   

   附录

    附录1:stdio形式的server使用

    stdio形式的server,pom如下 主要增加了个打包插件,这个等会儿需要打jar包

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.example</groupId>
        <artifactId>zm-mcp</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>mcp-server-stdio</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
        </dependency>
    </dependencies>

    <build>
        <finalName>mcp-server-stdio</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

    yml如下,对stdio做修改

spring:
  ai:
    mcp:
      server:
        type: async
        stdio: true
  main:
    banner-mode: off
logging:
  pattern:
    console:

    对tool稍作修改,测试下来发现只有返回string可以正常处理,其他类均和上面的普通server一致

package com.zm.ai.server.stdio.compnent;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

import java.sql.SQLException;
import java.util.Map;

@Component
public class ToolComponent {

    @Tool(description = "可以获取到某个城市的天气信息")
    public String getWeather(@ToolParam(required = true,description = "入参城市,请传入城市拼音") String city) throws SQLException {
        return city + "的天气为大雪,温度为零下5度到零下10度";
    }
}

    打成jar包后,用mcp-sdk进行简单测试,注意代码的jar包位置要替换为自己打好的jar包

package com.zm.ai.tool;

import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
import io.modelcontextprotocol.client.transport.ServerParameters;
import io.modelcontextprotocol.client.transport.StdioClientTransport;
import io.modelcontextprotocol.spec.McpSchema;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class MCPClientStdioStarter {

    public static void main(String[] args) {
        StdioClientTransport clientTransport = new StdioClientTransport(ServerParameters
                .builder("java")
                .arg("-jar")
                .arg("D:/***/mcp-server-stdio.jar")
                .build());
        McpSyncClient client = McpClient.sync(clientTransport).build();

        client.initialize();
        System.out.println("是否已经初始化:" + client.isInitialized());

        McpSchema.ListToolsResult listToolsResult = client.listTools();
        List<McpSchema.Tool> tools = listToolsResult.tools();
        System.out.println("获取到的tools:" + tools.stream().map(McpSchema.Tool::name).collect(Collectors.joining(",")));
        for (McpSchema.Tool tool : tools) {
            McpSchema.JsonSchema jsonSchema = tool.inputSchema();
            System.out.println(jsonSchema);
            McpSchema.CallToolResult callToolResult = client.callTool(new McpSchema.CallToolRequest(tool.name(), Map.of("city", "beijing")));

            System.out.println("获取到的结果为:==========");
            callToolResult.content().forEach(System.out::println);
        }

    }
}

    可得到结果

是否已经初始化:true
获取到的tools:getWeather
JsonSchema[type=object, properties={city={type=string, description=入参城市,请传入城市拼音}}, required=[city], additionalProperties=false, defs=null, definitions=null]
获取到的结果为:==========
TextContent[audience=null, priority=null, text="beijing的天气为大雪,温度为零下5度到零下10度"]

    当然也可以放在spring-ai-client使用,形式和这个差不多就不再赘述了。

 

 

  

    附录2:项目结构

 

posted @ 2025-06-06 14:28  雨落寒沙  阅读(5612)  评论(0)    收藏  举报