03.使用spring-ai玩转MCP
接着上篇:https://blog.csdn.net/sinat_15906013/article/details/147052013,我们介绍了,什么是MCP?使用cline插件/cherry-studio安装了Mcp Server,本篇我们要借助spring-ai实现MCP Client和Server。
使用spring-ai的话,需要spring-boot3和JDK17。
MCP Server
- 引入mcp-server依赖
<properties><java.version>17</java.version><spring-ai.version>1.0.0-M7</spring-ai.version>
</properties>
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
<dependencyManagement><dependencies><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>${spring-ai.version}</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>
- 使用@Tool声明方法,使用@ToolParam声明参数
@Service
public class EchoService {/*** echo** @param msg* @return String*/@Tool(description = "echo")public String echo(@ToolParam(description = "消息") String msg) {return msg;}
}
- 将EchoService注入到ToolCallbackProvider
@Configuration
public class McpServerConfig {@Beanpublic ToolCallbackProvider allTools(EchoService echoService) {return MethodToolCallbackProvider.builder().toolObjects(echoService).build();}
}
- 单元测试
class ToolTest {private final static String url = "http://localhost:8899";private static McpSyncClient client;@BeforeAllpublic static void init() {var transport = new WebFluxSseClientTransport(WebClient.builder().baseUrl(url));client = McpClient.sync(transport).build();client.initialize();client.ping();}@Testpublic void test() {// List and demonstrate toolsListToolsResult toolsList = client.listTools();System.out.println("Available Tools = " + toolsList);CallToolResult echo = client.callTool(new CallToolRequest("echo",Map.of("msg", "hello world")));System.out.println("echo: " + echo);client.closeGracefully();}
}
- 扩展mcp-server不仅提供了tool,还有prompt和resource
@Configuration
public class PromptManagement {@Beanpublic List<McpServerFeatures.SyncPromptSpecification> myPrompts() {var prompt = new McpSchema.Prompt("greeting", "A friendly greeting prompt",List.of(new McpSchema.PromptArgument("name", "The name to greet", true)));var promptSpecification = new McpServerFeatures.SyncPromptSpecification(prompt, (exchange, getPromptRequest) -> {String nameArgument = (String) getPromptRequest.arguments().get("name");if (nameArgument == null) { nameArgument = "friend"; }var userMessage = new McpSchema.PromptMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello " + nameArgument + "! How can I assist you today?"));return new McpSchema.GetPromptResult("A personalized greeting message", List.of(userMessage));});return List.of(promptSpecification);}
}
@Configuration
public class ResourceManagement {@Beanpublic List<McpServerFeatures.SyncResourceSpecification> myResources() {var systemInfoResource = new McpSchema.Resource("file:///logs/app.log", "Application Logs", "Application Logs", "text/plain",new McpSchema.Annotations(List.of(McpSchema.Role.USER), 1.0));var resourceSpecification = new McpServerFeatures.SyncResourceSpecification(systemInfoResource, (exchange, request) -> {try {var systemInfo = Map.of();String jsonContent = new ObjectMapper().writeValueAsString(systemInfo);return new McpSchema.ReadResourceResult(List.of(new McpSchema.TextResourceContents(request.uri(), "application/json", jsonContent)));} catch (Exception e) {throw new RuntimeException("Failed to generate system info", e);}});return List.of(resourceSpecification);}
}
参考前面的单元测试:client.listTools()
,下面两种应该也不在话下。
MCP Client
- 引入mcp-client依赖
<properties><java.version>17</java.version><spring-ai.version>1.0.0-M7</spring-ai.version>
</properties><!-- web&webflux start -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- web&webflux end --><!-- model start -->
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.ai</groupId>-->
<!-- <artifactId>spring-ai-starter-model-openai</artifactId>-->
<!-- </dependency>-->
<!-- model end --><!-- MCP start -->
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
</dependency>
<!-- MCP end --><dependencyManagement><dependencies><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>${spring-ai.version}</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>
- 在application.properties配置stdio方式的mcp-server
spring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-servers-config.json
发没发现,配合和之前的客户端一致,mcp-servers-config.json如下:
{"mcpServers": {"time": {"command": "cmd","args": ["/c","uvx","mcp-server-time","--local-timezone=Asia/Shanghai"]},"filesystem": {"command": "cmd","args": ["/c","npx","-y","@modelcontextprotocol/server-filesystem","D:\\SourceCode\\bsp-mcp"]}}
}
- 在application.yaml配置模型和sse方式的mcp-server
spring:ai:ollama:base-url: http://192.168.199.255:11434chat:options:model: qwen2.5:32bmcp:client:toolcallback:enabled: truesse:connections:amap:url: https://mcp.amap.com?key=xxxmy:url: http://localhost:8899
- 配置模型集成MCP
ChatModel是对话模型,常见的还有Embedding是关于RAG的。
OllamaChatModel是支持function call的,我们就以它为例。
来自:https://docs.spring.io/spring-ai/reference/api/index.html#_ai_model_api
@Configuration
public class ChatClientConfig {@Beanpublic ChatClient client(OllamaChatModel ollamaChatModel,SyncMcpToolCallbackProvider syncMcpToolCallbackProvider) {return ChatClient.builder(ollamaChatModel).defaultTools(syncMcpToolCallbackProvider).build();}
}
- 对外提供API
@RequiredArgsConstructor
@RestController
public class MCPController {private final ChatClient client;private final List<McpSyncClient> mcpClientList;@GetMapping("/ai/mcp/generate")public Map<String, String> generate(@RequestParam(value = "message", defaultValue = "几点了?") String message) {return Map.of("generation", client.prompt(message).call().content());}@GetMapping("/ai/mcp/servers")public List<McpSyncClient> servers() {return mcpClientList;}@GetMapping("/ai/mcp/tools")public McpSchema.ListToolsResult tools(String name) {return mcpClientList.stream().filter(mcpSyncClient ->StringUtils.endsWithIgnoreCase(name, mcpSyncClient.getServerInfo().name())).findFirst().get().listTools();}@GetMapping("/ai/mcp/resources")public McpSchema.ListResourcesResult resources(String name) {return mcpClientList.stream().filter(mcpSyncClient ->StringUtils.endsWithIgnoreCase(name, mcpSyncClient.getServerInfo().name())).findFirst().get().listResources();}@GetMapping("/ai/mcp/prompts")public McpSchema.ListPromptsResult prompts(String name) {return mcpClientList.stream().filter(mcpSyncClient ->StringUtils.endsWithIgnoreCase(name, mcpSyncClient.getServerInfo().name())).findFirst().get().listPrompts();}
}
抓包分析交互流程
来自:https://docs.spring.io/spring-ai/reference/api/index.html#_tool_calling_api