一、Spring AI MCP Client
MCP
由 Anthropic
推出的一种开放标准协议,旨在统一大模型(LLM
)与外部数据源和工具之间的通信方式。通过 MCP
协议,开发者可以更高效地实现 AI
模型与外部资源的集成,从而提升应用的智能化和上下文感知能力。
上篇文章介绍了关于 Spring AI MCP Server + Cline
的方式实现了数据库 ChatBi
助手,但MCP
客户端的能力是基于 Cline
进行实现的,本篇文章继续进行深入,通过 Spring AI MCP Client
端框架调用MCP Server
端的 Tools
,AIGC
能力使用 deepseek-r1
进行完成。
上篇文章在实现 MCP Server
端时采用的 stdio
模式进行的接入,本篇文章也继续扩展重新采用 SSE
模式进行实现,同样是实现一个 ChatBi
数据查询助手 ,不过关于表结构的创建,还是请参考上篇文章中的介绍:
关于AIGC
能力端的说明,本次使用的 OpenRouter
平台提供的大模型能力,非局限于当前平台你可以使用任何支持 tools
调用的AIGC
平台。
Postman
测试调用Client
端接口效果如下:
Spring AI MCP Client 端的官方介绍文档如下:
https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html
二、Spring AI MCP Server SSE 模式搭建
1. 创建父 Maven
创建父 Maven
项目,在 pom
中加入如下全局声明,这里 Java
使用17
版本,SpringBoot
选用 3.3.0
版本,Spring AI
选用 1.0.0-SNAPSHOT
版本。
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>mcp-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mcp-demo</name>
<description>mcp-demo</description>
<packaging>pom</packaging>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>3.3.0</spring-boot.version>
<spring-ai.version>1.0.0-SNAPSHOT</spring-ai.version>
</properties>
<modules>
<module>mcp-server</module>
<module>mcp-client</module>
</modules>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<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>
<repositories>
<repository>
<name>Central Portal Snapshots</name>
<id>central-portal-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
</project>
2. 创建 mcp-server 子模块
在父依赖创建SpringBoot
子模块,修改pom
依赖如下:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.example</groupId>
<artifactId>mcp-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mcp-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mcp-server</name>
<description>mcp-server</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
application.yml
配置:
server:
port: 8010
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/langchain?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
ai:
mcp:
server:
name: mymcp
type: SYNC
sse-message-endpoint: /mcp/messages
3. 创建 MCP Tools
创建三个 MCP Tools ,实现 获取可用表名、根据表名获取表结构、执行SQL 三个功能,这点和上篇文章中 stdio
模式是一致的 :
@Component
public class DBTool {
@Resource
private JdbcTemplate jdbcTemplate;
private final String sql = "SELECT TABLE_NAME, TABLE_COMMENT FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'langchain'";
private final String schemaSql = "SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT FROM INFORMATION_SCHEMA.COLUMNS " +
"WHERE TABLE_SCHEMA = 'langchain' AND TABLE_NAME = ?";
@Tool(description = "获取所有可用的表名")
public List<String> getTables() {
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
return maps.stream().map(map -> {
String tableName = String.valueOf(map.get("TABLE_NAME"));
String tableComment = String.valueOf(map.get("TABLE_COMMENT"));
return tableName + " COMMENT " + tableComment;
}).collect(Collectors.toList());
}
@Tool(description = "根据表名获取Schema")
public String getTableSchema(@ToolParam(description = "表名") List<String> tables) {
return tables.stream().filter(t -> !t.isBlank()).map(tableName -> {
List<Map<String, Object>> columns = jdbcTemplate.queryForList(schemaSql, tableName);
String tablePrompt = columns.stream().map(map -> {
String name = String.valueOf(map.get("COLUMN_NAME"));
String type = String.valueOf(map.get("DATA_TYPE"));
String comment = String.valueOf(map.get("COLUMN_COMMENT"));
return String.format("%s (%s) COMMENT %s", name, type, comment);
}).collect(Collectors.joining(", \n"));
return String.format("Table: %s (%s)\n", tableName, tablePrompt);
}).collect(Collectors.joining("\n"));
}
@Tool(description = "执行SQL查询结果")
public List<Map<String, Object>> runSql(@ToolParam(description = "sql") String sql) {
try {
if (sql.contains("DELETE") || sql.contains("UPDATE") || sql.contains("INSERT")) {
throw new RuntimeException("执行SQL仅限于查询语句!");
}
return jdbcTemplate.queryForList(sql);
} catch (RuntimeException e) {
return Collections.singletonList(Map.of("执行SQL异常",e.getMessage()));
}
}
}
4. 注册 MCP Tools
:
@Configuration
public class MCPConfig {
@Bean
public List<ToolCallback> tools(DBTool dbTool) {
return new java.util.ArrayList<>(List.of(ToolCallbacks.from(dbTool)));
}
}
5. 启动服务
启动后可以看到日志中打印注册了三个tools:
三、MCP Client 端搭建
1. 创建 mcp-client 子模块
在父依赖创建SpringBoot
子模块,修改pom
依赖如下:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.example</groupId>
<artifactId>mcp-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mcp-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mcp-client</name>
<description>mcp-client</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<version>${spring-ai.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.example.mcpclient.McpClientApplication</mainClass>
<skip>true</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
application.yml
配置如下,主要声明出大模型的链接和MCPServer
端的连接信息。
server:
port: 8020
spring:
ai:
openai:
base-url: https://openrouter.ai/api/
api-key: sk-or-v1-xxxxxxxx ## 修改为你的Key
chat:
enabled: true
options:
model: deepseek/deepseek-r1
mcp:
client:
enabled: true
name: mymcp
version: 1.0.0
type: SYNC
request-timeout: 300s
sse:
connections:
url: http://localhost:8010
2. 不依赖大模型,使用MCP API 的方式调用SSE远程Tools, 验证连接
首先实验下不依赖大模型,使用 MCP API
的方式测试调用远程Tools
,验证连接是否正常。
@SpringBootTest
public class MCPSSEClientSpringTest {
@Resource
List<McpSyncClient> clients;
@Test
public void test() {
clients.stream()
.filter(Objects::nonNull)
.filter(m -> Objects.equals(m.getServerInfo().name(), "mymcp"))
.findFirst()
.ifPresent(client -> {
// 获取所有的 MCP Tools
McpSchema.ListToolsResult toolsList = client.listTools();
System.out.println("==============MCP Tools===============");
toolsList.tools().forEach(t -> System.out.println(t.toString()));
// 调用工具,获取全部的表
McpSchema.CallToolResult tables = client.callTool(
new McpSchema.CallToolRequest(
"getTables",
Collections.emptyMap()
)
);
System.out.println("==============getTables===============");
System.out.println("getTables" + tables.content().get(0));
// 调用工具,获取表的Schema
McpSchema.CallToolResult tableSchema = client.callTool(
new McpSchema.CallToolRequest(
"getTableSchema",
Map.of("arg0", Arrays.asList("user", "role"))
)
);
System.out.println("==============getTableSchema===============");
System.out.println("getTableSchema" + tableSchema.content().get(0));
// 调用工具,执行SQL
McpSchema.CallToolResult runSQL = client.callTool(
new McpSchema.CallToolRequest(
"runSql",
Map.of("arg0", "select count(id) from user")
)
);
System.out.println("==============runSql===============");
System.out.println("runSql" + runSQL.content().get(0));
});
}
}
运行后看到 Tools
列表和调用 Tool
成功即表示连接正常。
3. 结合 DeepSeek 自动 Tools
调用
这里临时使用 Map
存放多轮对话,通过 userId
区分具体用户。关于 Tools
的组装,
SpringAI
已经自动将 MCP Tools
放入了 ToolCallbackProvider
中,因此可直接将该对象提供给 ChatClient
即可。
这部分的内容可参考:
完整实现逻辑如下:
@RestController
public class ChatController {
private final ChatClient chatClient;
// 存放会话,临时使用Map, 实际使用请考虑持久化
private final Map<String, List<Message>> history;
public ChatController(ChatClient.Builder chatClientBuilder, ToolCallbackProvider toolCallbackProvider) {
this.chatClient = chatClientBuilder
.defaultTools(toolCallbackProvider)
.build();
history = new ConcurrentHashMap<>();
}
@GetMapping("/chat")
public String mcpChat(@RequestParam(name = "userId", required = true) String userId,
@RequestParam(name = "msg", required = true) String msg) {
if (!history.containsKey(userId)) {
history.put(userId, new ArrayList<>());
history.get(userId).add(new SystemMessage("You are an AI assistant that helps people find information."));
}
history.get(userId).add(new UserMessage(msg));
String result = chatClient
.prompt(new Prompt(history.get(userId)))
.call().content();
history.get(userId).add(new AssistantMessage(result));
return result;
}
}
启动服务,下面进行测试。