摘要:本文主要介绍springboot3+ spring ai实现mcp-server来增加我们的生产力;下文用一个温度的查询和数据库查询的案例来实现开发步骤。
核心概念
Spring AI 提供了与 Model Context Protocol (MCP) 的集成,这是一个新兴的协议,旨在为 AI 应用提供标准化工具和上下文管理方式。
MCP 是一个开放协议,让 AI 模型能够:
- 发现和使用工具
- 访问各种数据源
- 管理上下文信息
- 以标准化方式与外部系统交互
实战案例
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 https://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.5.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>sp3-mcp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>sp3-mcp</name>
<description>sp3-mcp</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.1.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
application.properties
# spring.main.web-application-type=none
# NOTE: You must disable the banner and the console logging
# to allow the STDIO transport to work !!!
spring.main.banner-mode=off
# logging.pattern.console=
# spring.ai.mcp.server.stdio=false
spring.ai.mcp.server.name=mcp-weather-server
spring.ai.mcp.server.request-timeout=1h
spring.ai.mcp.server.protocol=sse
logging.file.name=./mcp-weather-server/target/server.log
Application.java
@SpringBootApplication
public class Sp3McpApplication {
public static void main(String[] args) {
SpringApplication.run(Sp3McpApplication.class, args);
}
}
WeatherService.java
提供的温度查询的工具
@Service
public class WeatherService {
private final RestClient restClient = RestClient.create();
/**
* The response format from the Open-Meteo API
*/
public record WeatherResponse(Current current) {
public record Current(LocalDateTime time, int interval, double temperature_2m) {
}
}
@McpTool(description = "Get the temperature (in celsius) for a specific location")
public String getTemperature(McpSyncServerExchange exchange,
@McpToolParam(description = "The location latitude") double latitude,
@McpToolParam(description = "The location longitude") double longitude,
@McpProgressToken String progressToken) {
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.DEBUG)
.data("Call getTemperature Tool with latitude: " + latitude + " and longitude: " + longitude)
.meta(Map.of()) // non null meata as a workaround for bug: ...
.build());
// 0% progress
exchange.progressNotification(new ProgressNotification(progressToken, 0.0, 1.0, "Retrieving weather forecast"));
WeatherResponse weatherResponse = restClient.get()
.uri("https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}¤t=temperature_2m",
latitude, longitude)
.retrieve()
.body(WeatherResponse.class);
String epicPoem = "MCP client doesn't provide sampling capability.";
if (exchange.getClientCapabilities().sampling() != null) {
// 50% progress
exchange.progressNotification(new ProgressNotification(progressToken, 0.5, 1.0, "Start sampling"));
String samplingMessage = """
For a weather forecast (temperature is in Celsius): %s.
At location with latitude: %s and longitude: %s.
Please write an epic poem about this forecast using a Shakespearean style.
""".formatted(weatherResponse.current().temperature_2m(), latitude, longitude);
CreateMessageResult samplingResponse = exchange.createMessage(CreateMessageRequest.builder()
.systemPrompt("You are a poet!")
.messages(List.of(new SamplingMessage(Role.USER, new TextContent(samplingMessage))))
.modelPreferences(ModelPreferences.builder().addHint("anthropic").build())
.build());
epicPoem = ((TextContent) samplingResponse.content()).text();
}
// 100% progress
exchange.progressNotification(new ProgressNotification(progressToken, 1.0, 1.0, "Task completed"));
return """
Weather Poem2: %s
about the weather: %s°C at location with latitude: %s and longitude: %s
""".formatted(epicPoem, weatherResponse.current().temperature_2m(), latitude, longitude);
}
}
DatabaseService.java
数据库查询工具
@Service
public class DatabaseService {
private Connection connection;
private final String jdbcUrl;
private final String username;
private final String password;
public DatabaseService() {
// 从环境变量读取数据库配置
String host = "192.168.8.134";
String port = "30635";
String database = "test_1";
this.username = "root";
this.password = "xxxx";
this.jdbcUrl = String.format("jdbc:mysql://%s:%s/%s?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC",
host, port, database);
// 初始化数据库连接
initializeConnection();
}
private String getEnvOrDefault(String key, String defaultValue) {
String value = System.getenv(key);
return value != null ? value : defaultValue;
}
private String getRequiredEnv(String key) {
String value = System.getenv(key);
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("Required environment variable " + key + " is not set");
}
return value;
}
private void initializeConnection() {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
this.connection = DriverManager.getConnection(jdbcUrl, username, password);
} catch (ClassNotFoundException | SQLException e) {
throw new RuntimeException("Failed to initialize database connection: " + e.getMessage(), e);
}
}
private Connection getConnection() throws SQLException {
if (connection == null || connection.isClosed()) {
initializeConnection();
}
return connection;
}
@McpTool(description = "执行MySQL查询语句(SELECT)")
public String executeQuery(McpSyncServerExchange exchange,
@McpToolParam(description = "要执行的SQL查询语句") String sql,
@McpProgressToken String progressToken) {
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.DEBUG)
.data("Executing query: " + sql)
.meta(Map.of())
.build());
exchange.progressNotification(new ProgressNotification(progressToken, 0.0, 1.0, "开始执行查询"));
try (Connection conn = getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
exchange.progressNotification(new ProgressNotification(progressToken, 0.5, 1.0, "处理查询结果"));
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
StringBuilder result = new StringBuilder();
result.append("查询执行成功!\n\n");
result.append("列信息:\n");
for (int i = 1; i <= columnCount; i++) {
result.append(String.format(" %s (%s)\n",
metaData.getColumnName(i),
metaData.getColumnTypeName(i)));
}
result.append("\n查询结果:\n");
int rowCount = 0;
while (rs.next()) {
rowCount++;
result.append("行 ").append(rowCount).append(": ");
for (int i = 1; i <= columnCount; i++) {
Object value = rs.getObject(i);
result.append(metaData.getColumnName(i)).append("=").append(value);
if (i < columnCount) result.append(", ");
}
result.append("\n");
}
exchange.progressNotification(new ProgressNotification(progressToken, 1.0, 1.0, "查询完成"));
if (rowCount == 0) {
result.append("(查询未返回任何行)");
} else {
result.append("\n总计: ").append(rowCount).append(" 行");
}
return result.toString();
} catch (SQLException e) {
exchange.progressNotification(new ProgressNotification(progressToken, 1.0, 1.0, "查询失败"));
return "查询执行失败: " + e.getMessage();
}
}
@McpTool(description = "执行MySQL更新语句(INSERT, UPDATE, DELETE)")
public String executeUpdate(McpSyncServerExchange exchange,
@McpToolParam(description = "要执行的SQL更新语句") String sql,
@McpProgressToken String progressToken) {
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.DEBUG)
.data("Executing update: " + sql)
.meta(Map.of())
.build());
exchange.progressNotification(new ProgressNotification(progressToken, 0.0, 1.0, "开始执行更新"));
try (Connection conn = getConnection();
Statement stmt = conn.createStatement()) {
exchange.progressNotification(new ProgressNotification(progressToken, 0.5, 1.0, "执行更新语句"));
int affectedRows = stmt.executeUpdate(sql);
exchange.progressNotification(new ProgressNotification(progressToken, 1.0, 1.0, "更新完成"));
return String.format("更新执行成功! 影响了 %d 行。", affectedRows);
} catch (SQLException e) {
exchange.progressNotification(new ProgressNotification(progressToken, 1.0, 1.0, "更新失败"));
return "更新执行失败: " + e.getMessage();
}
}
@McpTool(description = "执行任意MySQL语句")
public String executeSql(McpSyncServerExchange exchange,
@McpToolParam(description = "要执行的SQL语句") String sql,
@McpProgressToken String progressToken) {
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.DEBUG)
.data("Executing SQL: " + sql)
.meta(Map.of())
.build());
exchange.progressNotification(new ProgressNotification(progressToken, 0.0, 1.0, "开始执行SQL"));
try (Connection conn = getConnection();
Statement stmt = conn.createStatement()) {
exchange.progressNotification(new ProgressNotification(progressToken, 0.5, 1.0, "执行SQL语句"));
boolean isResultSet = stmt.execute(sql);
StringBuilder result = new StringBuilder();
if (isResultSet) {
// 处理查询结果
try (ResultSet rs = stmt.getResultSet()) {
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
result.append("SQL执行成功 - 查询结果:\n\n");
result.append("列信息:\n");
for (int i = 1; i <= columnCount; i++) {
result.append(String.format(" %s (%s)\n",
metaData.getColumnName(i),
metaData.getColumnTypeName(i)));
}
result.append("\n数据:\n");
int rowCount = 0;
while (rs.next()) {
rowCount++;
result.append("行 ").append(rowCount).append(": ");
for (int i = 1; i <= columnCount; i++) {
Object value = rs.getObject(i);
result.append(metaData.getColumnName(i)).append("=").append(value);
if (i < columnCount) result.append(", ");
}
result.append("\n");
}
if (rowCount == 0) {
result.append("(查询未返回任何行)");
} else {
result.append("\n总计: ").append(rowCount).append(" 行");
}
}
} else {
// 处理更新结果
int affectedRows = stmt.getUpdateCount();
result.append("SQL执行成功 - 更新了 ").append(affectedRows).append(" 行。");
}
exchange.progressNotification(new ProgressNotification(progressToken, 1.0, 1.0, "SQL执行完成"));
return result.toString();
} catch (SQLException e) {
exchange.progressNotification(new ProgressNotification(progressToken, 1.0, 1.0, "SQL执行失败"));
return "SQL执行失败: " + e.getMessage();
}
}
@McpTool(description = "获取数据库表列表")
public String getTables(McpSyncServerExchange exchange,
@McpProgressToken String progressToken) {
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.DEBUG)
.data("Getting database tables")
.meta(Map.of())
.build());
exchange.progressNotification(new ProgressNotification(progressToken, 0.0, 1.0, "获取表列表"));
try {
return executeQuery(exchange, "SHOW TABLES", progressToken);
} catch (Exception e) {
exchange.progressNotification(new ProgressNotification(progressToken, 1.0, 1.0, "获取表列表失败"));
return "获取表列表失败: " + e.getMessage();
}
}
@McpTool(description = "获取指定表的结构信息")
public String getTableStructure(McpSyncServerExchange exchange,
@McpToolParam(description = "表名") String tableName,
@McpProgressToken String progressToken) {
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.DEBUG)
.data("Getting table structure: " + tableName)
.meta(Map.of())
.build());
exchange.progressNotification(new ProgressNotification(progressToken, 0.0, 1.0, "获取表结构"));
try {
String sql = "DESCRIBE " + tableName;
return executeQuery(exchange, sql, progressToken);
} catch (Exception e) {
exchange.progressNotification(new ProgressNotification(progressToken, 1.0, 1.0, "获取表结构失败"));
return "获取表结构失败: " + e.getMessage();
}
}
@McpTool(description = "测试数据库连接")
public String testConnection(McpSyncServerExchange exchange,
@McpProgressToken String progressToken) {
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.DEBUG)
.data("Testing database connection")
.meta(Map.of())
.build());
exchange.progressNotification(new ProgressNotification(progressToken, 0.5, 1.0, "测试连接"));
try {
Connection conn = getConnection();
if (conn != null && !conn.isClosed()) {
DatabaseMetaData metaData = conn.getMetaData();
String result = String.format(
"数据库连接测试成功!\n" +
"数据库产品: %s\n" +
"数据库版本: %s\n" +
"驱动名称: %s\n" +
"驱动版本: %s\n" +
"连接URL: %s",
metaData.getDatabaseProductName(),
metaData.getDatabaseProductVersion(),
metaData.getDriverName(),
metaData.getDriverVersion(),
jdbcUrl
);
exchange.progressNotification(new ProgressNotification(progressToken, 1.0, 1.0, "连接测试完成"));
return result;
} else {
exchange.progressNotification(new ProgressNotification(progressToken, 1.0, 1.0, "连接测试失败"));
return "数据库连接失败: 连接无效或已关闭";
}
} catch (SQLException e) {
exchange.progressNotification(new ProgressNotification(progressToken, 1.0, 1.0, "连接测试失败"));
return "数据库连接测试失败: " + e.getMessage();
}
}
}
启动日志
客户端配置MCP
{
"mcpServers": {
"天气服务": {
"disabled": false,
"disabledTools": [],
"url": "http://localhost:8080/sse"
}
}
}
- 然后就可以愉快使用了