Springboot + MCP + JUnit 模板项目

前往原站点查看

2025-03-21 23:03:45

    本文快速介绍一下springboot新出的框架内容mcp,通过springboot+mcp+junit,来快速构建一个模板项目。

引流

    先挂一下几个链接,首先是我的模板仓库地址:

        gitee: https://gitee.com/dreamcenter/springboot-mcpserver-junit
        github: https://github.com/dreamcenter/springboot-MCPserver-JUnit

    接着是我的B站视频解说:

        

    接着是参考文档地址:

     MCP官网  、  Spring官网

搭建mcp服务

    首先引入spring-ai-mcp的依赖项

    <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
            <version>${mcp.verison}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

    接着,在application.properties中,一定要做如下配置,否则后续启动会失败:

spring.main.banner-mode=off
logging.pattern.console=

    这样基础的环境就已经搭建完毕了。我们需要创建一个服务,服务加上@Tool注解,这样才能被识别为mcp函数,对于函数的参数也可以加上@ToolParam注解,表明是mcp函数的参数。

package top.dreamcenter.mcp.service;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;

@Service
public class WeatherService {

    @Tool(description = "通过城市名字获取温度[伪]")
    public String getWeather(String cityName) {
        return cityName + "今天的温度是" + cityName.length()  * 10;
    }

}

    接着需要注册provider到容器中,我这里放在confiuration中集中注册了:

@Configuration
public class ToolCallbackProviderRegister {

    @Bean
    public ToolCallbackProvider weatherTools(WeatherService weatherService) {
        return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
    }

}

    这时候其实就已经完整的搭建起来了,我们只需要 clean + install 即可获得一个含有mcp协议服务的jar包。

通过CherryStudio测试

    打开一个支持mcp的客户端,如我这里使用cherrystudio,找到并且配置上我们刚刚写的jar包,确认如果没有出错,那么就成功了:

    接着我们找到一个带小扳手的模型(即支持mcp的大模型)。

    工具中启用mcp的demo服务,询问温度。



    然后,就得到利用mcp函数的结果啦!

JUnit利用MCPClient测试

    当然,实际开发中肯定不是用cherrystudio来测试,我们需要引入junit,调用McpClient来测试该函数是否有效。

    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    引入依赖后,即可写测试代码,首先构建McpClient客户端,接着通过该客户端来调用函数得到结果。

  @Test
    public void myTest() {

        ServerParameters parameters = ServerParameters.builder("java")
                .args("-jar", "D:\\CODE\\codings\\springboot\\demo\\target\\demo-0.0.1-SNAPSHOT.jar").build();

        McpSyncClient mcpClient = McpClient.sync(new StdioClientTransport(parameters)).build();

        McpSchema.CallToolResult callToolResult = mcpClient.callTool(new McpSchema.CallToolRequest("getWeather",
                Map.of("city", "南京")));


        List<McpSchema.Content> content = callToolResult.content();

        Assertions.assertNotNull(content);
        Assertions.assertEquals(content.size(), 1);

        McpSchema.TextContent result =  (McpSchema.TextContent)content.get(0);

        Assertions.assertEquals(result.text(), "\"南京的温度是2\"");

        mcpClient.closeGracefully();

    }

    当然,上面的只是一个简单的单元测试,如果对每一个接口都CV上面的这一大串代码肯定不方便,而且jar包随着版本迭代,文件名称可能也会变动,不适合单元测试,所以我封装了一个测试模板。首先进入单元任务的时候,BeforeAll查找jar包文件路径,接着在每个测试单元开始前创建一个MCPClient,并且在单元结束后自动closeGracefully进行关闭。

package top.dreamcenter.mcp;

import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.ServerParameters;
import io.modelcontextprotocol.client.transport.StdioClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import org.junit.jupiter.api.*;

import java.io.File;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;

public class CommonTest {

    private static String jarFilePath;
    private McpSyncClient mcpClient;

    @BeforeAll
    public static void init() throws URISyntaxException {
        // find jar file path

        URL resource = CommonTest.class.getClassLoader().getResource("locate.txt");
        Assertions.assertNotNull(resource, "load junit classLoader context fail");

        Path targetPath = Paths.get(resource.toURI()).getParent().getParent();

        File files = targetPath.toFile();
        File[] files1 = files.listFiles(file -> (file.getName().endsWith(".jar")));

        Assertions.assertNotNull(files1, "Can not find the generated jar file under [target] directory");
        Assertions.assertEquals(files1.length, 1, "Find more then one jar file under [target] directory");

        jarFilePath = files1[0].getAbsolutePath();
    }

    @BeforeEach
    public void initClient() {
        ServerParameters serverParameters = ServerParameters.builder("java")
                .args("-jar", jarFilePath).build();
        mcpClient = McpClient.sync(new StdioClientTransport(serverParameters)).build();
    }

    @AfterEach
    public void close() {
        if (mcpClient != null) {
            mcpClient.closeGracefully();
        }
    }


    @Test
    public void testWeatherMCP() {

        var weather = quickCall("getWeather", Map.of("cityName", "南京"));

        List<McpSchema.Content> contentList = weather.content();
        String res = easyTextContent(contentList);

        Assertions.assertEquals("\"南京今天的温度是20\"", res);
    }

    @Test
    public void testNumMCP() {

        var num = quickCall("judgeIfOdd", Map.of("num", "1"));

        List<McpSchema.Content> contentList = num.content();
        String res = easyTextContent(contentList);

        Assertions.assertEquals("\"1不是双数\"", res);
    }

    private McpSchema.CallToolResult quickCall(String name, Map<String, Object> arguments) {
        return mcpClient.callTool(new McpSchema.CallToolRequest(name,arguments));
    }

    private String easyTextContent(List<McpSchema.Content> contentList) {
        assert contentList != null && contentList.size() == 1;
        McpSchema.TextContent textContent = (McpSchema.TextContent) contentList.get(0);
        return textContent.text();
    }
}

    

    ok,以上就是本次的博客日记啦!嘻嘻QAQ



上一篇: RClone同步文件夹到webdav
下一篇: 代码开源协议一图流 | 搬运