Spring AI MCP集成实战:接入标准化工具服务
Spring AI MCP集成实战:接入标准化工具服务
零行Function Calling代码,让AI直接操作数据库
2026年2月,老张的技术群里有人发了一段让大家都沉默了的代码。
那是一段Spring AI MCP集成的配置文件,大概是这样的:
spring:
ai:
mcp:
client:
servers:
database:
command: java
args: ["-jar", "db-mcp-server.jar"]
transport: stdio
env:
DB_URL: jdbc:postgresql://localhost/mydb然后就这几行Java代码:
return chatClient.prompt()
.user("查一下过去7天每天的新增用户数")
.tools(mcpProvider.getToolCallbacks())
.call()
.content();就这样,AI就能直接查数据库了。没有定义任何@Tool方法,没有写任何JSON Schema,没有自己实现任何工具。
群里有人第一反应:"这是真的吗?"
群主回答:"我刚测了,是真的。"
然后群里安静了大概五分钟。
接下来是各种"我去"、"卧槽"、"这样也行?"。
老刘追问了一个细节:"那工具的权限怎么控制?要是让AI随便DELETE怎么办?"
群主回答说MCP Server可以配置只允许SELECT语句,而且工具描述里会明确说明支持什么操作。
老刘又问:"那跨库怎么办?我们有三个数据库。"
群主:"配三个MCP Server,AI自己会选对应的工具。"
沉默。
然后老刘说了一句话,让我印象特别深:
"我他妈写了三年的Function Calling,终于可以不写了。"
先说结论(TL;DR)
- Spring AI 1.0原生支持MCP Client,几行配置即可接入任意MCP Server
- 工具自动发现:AI自动知道MCP Server提供哪些工具,无需手动定义Schema
- 支持同时连接多个MCP Server,工具自动合并
- 两种连接方式:配置文件方式(推荐)和代码方式(灵活)
- 关键坑点:stdio模式需要安装Node.js/Java运行时,HTTP SSE模式需要处理重连
- 调试工具:MCP Inspector + Spring AI DEBUG日志
环境搭建:Spring AI MCP依赖配置
完整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>
<groupId>com.laozhang</groupId>
<artifactId>spring-ai-mcp-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring AI OpenAI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- Spring AI MCP Client(核心依赖) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Actuator(健康检查和指标) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Spring Boot WebFlux(HTTP SSE模式需要) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Resilience4j(熔断降级) -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.0</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Test -->
<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>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>连接MCP Server:配置文件方式和代码方式
方式一:配置文件方式(推荐)
# application.yml - 完整配置示例
spring:
application:
name: spring-ai-mcp-demo
ai:
openai:
api-key: ${OPENAI_API_KEY:your-key-here}
chat:
options:
model: gpt-4o
temperature: 0.1
mcp:
client:
enabled: true
request-timeout: PT30S # ISO 8601格式,30秒超时
initialization-timeout: PT10S
# 配置多个MCP Server
servers:
# ===== 文件系统工具(stdio方式) =====
filesystem:
command: npx
args:
- "-y"
- "@modelcontextprotocol/server-filesystem"
- "/Users/demo/workspace" # 允许访问的根目录
transport: stdio
# ===== SQLite数据库(stdio方式) =====
sqlite:
command: npx
args:
- "-y"
- "@modelcontextprotocol/server-sqlite"
- "--db-path"
- "/Users/demo/data/app.db"
transport: stdio
# ===== 网络搜索(stdio方式,需要API Key) =====
brave-search:
command: npx
args:
- "-y"
- "@modelcontextprotocol/server-brave-search"
transport: stdio
env:
BRAVE_API_KEY: ${BRAVE_API_KEY:your-brave-key}
# ===== 远程数据库服务(HTTP SSE方式) =====
remote-database:
url: http://localhost:9000/mcp
transport: http-sse
headers:
Authorization: "Bearer ${DB_MCP_TOKEN:dev-token}"
X-Tenant-Id: "company-123"
# 管理端点
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
# 日志
logging:
level:
com.laozhang: DEBUG
org.springframework.ai.mcp: DEBUG # MCP调试日志
org.springframework.ai.chat: INFO方式二:代码方式(动态配置)
// McpClientConfig.java - 代码方式配置MCP连接
package com.laozhang.mcp.config;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.ServerParameters;
import io.modelcontextprotocol.client.transport.StdioClientTransport;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import java.util.List;
import java.util.Map;
@Slf4j
@Configuration
@Profile("code-config") // 只在特定profile下激活
public class McpClientConfig {
/**
* 代码方式创建stdio MCP Client
* 适合需要动态配置参数的场景
*/
@Bean
public McpSyncClient filesystemMcpClient() {
ServerParameters serverParams = ServerParameters.builder("npx")
.args("-y", "@modelcontextprotocol/server-filesystem", "/tmp/workspace")
.build();
McpSyncClient client = McpClient.sync(
new StdioClientTransport(serverParams)
).build();
// 初始化连接
client.initialize();
log.info("Filesystem MCP Client已初始化");
return client;
}
/**
* 代码方式创建HTTP SSE MCP Client
*/
@Bean
public McpSyncClient remoteDatabaseMcpClient() {
// HTTP SSE方式
var transport = new io.modelcontextprotocol.client.transport.HttpClientSseClientTransport(
"http://localhost:9000/mcp"
);
McpSyncClient client = McpClient.sync(transport).build();
client.initialize();
log.info("Remote Database MCP Client已初始化");
return client;
}
/**
* 聚合所有MCP Client的工具
*/
@Bean
public SyncMcpToolCallbackProvider customMcpToolProvider(
McpSyncClient filesystemMcpClient,
McpSyncClient remoteDatabaseMcpClient) {
return new SyncMcpToolCallbackProvider(
List.of(filesystemMcpClient, remoteDatabaseMcpClient)
);
}
}调用内置MCP工具:文件系统、数据库、搜索
完整的工具使用服务
// McpToolsService.java
package com.laozhang.mcp.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class McpToolsService {
private final ChatClient.Builder chatClientBuilder;
private final SyncMcpToolCallbackProvider mcpToolCallbackProvider;
/**
* 文件系统操作:让AI读取和分析文件
*/
public String analyzeFile(String filePath, String analysisRequest) {
String prompt = String.format(
"请读取文件 %s,然后:%s",
filePath, analysisRequest
);
log.info("文件分析请求: path={}", filePath);
return chatClientBuilder.build()
.prompt()
.system("""
你可以读取文件系统中的文件。
读取文件后,按用户要求进行分析。
如果文件不存在,清楚地告知用户。
""")
.user(prompt)
.tools(mcpToolCallbackProvider.getToolCallbacks())
.call()
.content();
}
/**
* 数据库查询:自然语言转SQL查询
*/
public String queryDatabase(String naturalLanguageQuery) {
log.info("自然语言数据库查询: {}", naturalLanguageQuery);
return chatClientBuilder.build()
.prompt()
.system("""
你可以查询数据库。
先用list_tables工具查看可用的表,
再用read_query工具执行SELECT语句。
只能执行SELECT查询,不能执行任何修改操作。
用中文回答,结果要友好展示。
""")
.user(naturalLanguageQuery)
.tools(mcpToolCallbackProvider.getToolCallbacks())
.call()
.content();
}
/**
* 网络搜索:搜索最新信息
*/
public String searchAndSummarize(String topic) {
log.info("搜索请求: {}", topic);
return chatClientBuilder.build()
.prompt()
.system("""
你可以搜索网络获取最新信息。
搜索后对结果进行综合分析,
给出准确、客观的总结。
标注信息来源。
""")
.user("请搜索并总结:" + topic)
.tools(mcpToolCallbackProvider.getToolCallbacks())
.call()
.content();
}
/**
* 综合任务:同时使用多个工具
*/
public String executeComplexTask(String task) {
log.info("综合任务: {}", task);
return chatClientBuilder.build()
.prompt()
.system("""
你有以下工具可以使用:
1. 文件系统工具:读写文件、列出目录
2. 数据库工具:查询数据库中的数据
3. 搜索工具:搜索网络获取最新信息
根据任务需要,灵活组合使用这些工具。
步骤要清晰,结果要完整。
""")
.user(task)
.tools(mcpToolCallbackProvider.getToolCallbacks())
.call()
.content();
}
}动态工具发现:自动识别MCP Server提供的所有工具
// McpToolDiscovery.java - 运行时查看可用工具
package com.laozhang.mcp.discovery;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
@Slf4j
@Component
@RequiredArgsConstructor
public class McpToolDiscovery {
private final SyncMcpToolCallbackProvider mcpToolCallbackProvider;
private final List<McpSyncClient> mcpClients; // 所有已配置的MCP Client
/**
* 列出所有可用工具(用于UI展示或日志)
*/
public List<ToolInfo> listAllTools() {
List<ToolInfo> toolInfoList = new ArrayList<>();
ToolCallback[] callbacks = mcpToolCallbackProvider.getToolCallbacks();
for (ToolCallback callback : callbacks) {
ToolInfo info = ToolInfo.builder()
.name(callback.getName())
.description(callback.getDescription())
.build();
toolInfoList.add(info);
log.debug("可用工具: {} - {}", info.getName(),
info.getDescription().substring(0, Math.min(80, info.getDescription().length())));
}
log.info("共发现 {} 个MCP工具", toolInfoList.size());
return toolInfoList;
}
/**
* 从MCP Server直接获取工具详情(包含inputSchema)
*/
public List<McpSchema.Tool> listToolsWithSchema() {
List<McpSchema.Tool> allTools = new ArrayList<>();
for (McpSyncClient client : mcpClients) {
try {
McpSchema.ListToolsResult result = client.listTools();
if (result != null && result.tools() != null) {
allTools.addAll(result.tools());
log.debug("从MCP Client获取到 {} 个工具", result.tools().size());
}
} catch (Exception e) {
log.error("获取工具列表失败", e);
}
}
return allTools;
}
/**
* 按工具名前缀筛选(用于权限控制)
*/
public ToolCallback[] filterByPrefix(String prefix) {
return Arrays.stream(mcpToolCallbackProvider.getToolCallbacks())
.filter(t -> t.getName().startsWith(prefix))
.toArray(ToolCallback[]::new);
}
@lombok.Builder
@lombok.Data
public static class ToolInfo {
private String name;
private String description;
}
}
// McpToolDiscoveryController.java - 暴露工具发现API
@RestController
@RequestMapping("/api/mcp/tools")
@RequiredArgsConstructor
class McpToolDiscoveryController {
private final McpToolDiscovery toolDiscovery;
@GetMapping
public List<McpToolDiscovery.ToolInfo> listTools() {
return toolDiscovery.listAllTools();
}
@GetMapping("/with-schema")
public List<McpSchema.Tool> listToolsWithSchema() {
return toolDiscovery.listToolsWithSchema();
}
}多MCP Server:同时连接多个工具服务器
# application.yml - 多MCP Server完整配置
spring:
ai:
mcp:
client:
enabled: true
servers:
# 内部业务数据库
business-db:
command: java
args: ["-jar", "/opt/mcp/business-db-server.jar"]
transport: stdio
env:
DATASOURCE_URL: ${BUSINESS_DB_URL}
DATASOURCE_USERNAME: ${BUSINESS_DB_USER}
DATASOURCE_PASSWORD: ${BUSINESS_DB_PASSWORD}
ALLOWED_OPERATIONS: "SELECT" # 只允许查询
# 用户行为数据仓库
analytics-db:
command: java
args: ["-jar", "/opt/mcp/analytics-db-server.jar"]
transport: stdio
env:
DATASOURCE_URL: ${ANALYTICS_DB_URL}
DATASOURCE_USERNAME: ${ANALYTICS_DB_USER}
DATASOURCE_PASSWORD: ${ANALYTICS_DB_PASSWORD}
# 内部文档系统
docs:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/opt/company-docs"]
transport: stdio
# Confluence Wiki(远程)
confluence:
url: http://confluence-mcp:8080/mcp
transport: http-sse
headers:
Authorization: "Bearer ${CONFLUENCE_MCP_TOKEN}"
# GitHub代码仓库
github:
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]
transport: stdio
env:
GITHUB_PERSONAL_ACCESS_TOKEN: ${GITHUB_TOKEN}// MultiServerAssistant.java - 智能路由多个MCP Server
package com.laozhang.mcp.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class MultiServerAssistant {
private final ChatClient.Builder chatClientBuilder;
private final SyncMcpToolCallbackProvider mcpToolCallbackProvider;
/**
* 智能助手:AI自动从多个Server中选择合适的工具
*/
public String ask(String question) {
// 所有Server的工具都注入,AI自己决定用哪个
ToolCallback[] allTools = mcpToolCallbackProvider.getToolCallbacks();
log.info("多Server查询,可用工具数: {}", allTools.length);
return chatClientBuilder.build()
.prompt()
.system(buildMultiServerSystemPrompt())
.user(question)
.tools(allTools)
.call()
.content();
}
/**
* 针对特定场景,只使用部分Server的工具
*/
public String askWithFilteredTools(String question, String... serverPrefixes) {
// 按Server名称前缀过滤工具
// 工具命名规范:{server-name}_{tool-name}
ToolCallback[] filtered = Arrays.stream(mcpToolCallbackProvider.getToolCallbacks())
.filter(tool -> {
for (String prefix : serverPrefixes) {
if (tool.getName().startsWith(prefix + "_")) return true;
}
return false;
})
.toArray(ToolCallback[]::new);
log.info("过滤后可用工具数: {}/{}", filtered.length,
mcpToolCallbackProvider.getToolCallbacks().length);
return chatClientBuilder.build()
.prompt()
.user(question)
.tools(filtered)
.call()
.content();
}
private String buildMultiServerSystemPrompt() {
return """
你是一个企业级智能助手,可以访问多个数据源和服务:
1. **业务数据库**(business-db):查询订单、用户、产品等业务数据
2. **分析数据仓库**(analytics-db):查询用户行为、转化率等分析数据
3. **内部文档**(docs):读取公司内部文档、规范、流程
4. **Confluence Wiki**(confluence):搜索知识库文章
5. **GitHub**(github):查看代码仓库、Issue、PR
根据问题选择最合适的数据源。
如果需要综合多个数据源,分别查询后整合回答。
不确定用哪个工具时,优先查询文档获取指引。
""";
}
}工具调用结果处理
// McpResultProcessor.java - 处理MCP工具返回的结果
package com.laozhang.mcp.processor;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Component;
import java.util.*;
@Slf4j
@Component
@RequiredArgsConstructor
public class McpResultProcessor {
private final ObjectMapper objectMapper;
/**
* 解析数据库查询结果
* MCP database server通常返回JSON数组
*/
public List<Map<String, Object>> parseDbResult(String mcpResult) {
try {
if (mcpResult == null || mcpResult.trim().isEmpty()) {
return Collections.emptyList();
}
// 尝试解析JSON
JsonNode node = objectMapper.readTree(mcpResult);
if (node.isArray()) {
List<Map<String, Object>> rows = new ArrayList<>();
for (JsonNode row : node) {
rows.add(objectMapper.convertValue(row, Map.class));
}
return rows;
}
log.warn("MCP数据库结果不是JSON数组: {}", mcpResult.substring(0, Math.min(100, mcpResult.length())));
return Collections.emptyList();
} catch (Exception e) {
log.error("解析MCP数据库结果失败", e);
return Collections.emptyList();
}
}
/**
* 格式化查询结果为Markdown表格
*/
public String formatAsMarkdownTable(List<Map<String, Object>> rows) {
if (rows == null || rows.isEmpty()) return "(无数据)";
// 获取列名
Set<String> columns = rows.get(0).keySet();
StringBuilder sb = new StringBuilder();
// 表头
sb.append("| ").append(String.join(" | ", columns)).append(" |\n");
sb.append("| ").append("--- | ".repeat(columns.size())).append("\n");
// 数据行
for (Map<String, Object> row : rows) {
sb.append("| ");
for (String col : columns) {
Object val = row.get(col);
sb.append(val != null ? val.toString() : "NULL").append(" | ");
}
sb.append("\n");
}
return sb.toString();
}
/**
* 从ChatResponse中提取工具调用日志
*/
public List<ToolCallLog> extractToolCallLogs(ChatResponse response) {
List<ToolCallLog> logs = new ArrayList<>();
if (response == null || response.getResult() == null) return logs;
// Spring AI的ChatResponse包含工具调用信息
// 具体提取方式取决于Spring AI版本
log.debug("工具调用日志提取完成: {} 条", logs.size());
return logs;
}
@lombok.Builder
@lombok.Data
public static class ToolCallLog {
private String toolName;
private String input;
private String output;
private boolean success;
private long durationMs;
}
}错误处理:MCP Server不可用时的降级
// McpFallbackService.java - MCP Server不可用时的降级策略
package com.laozhang.mcp.fallback;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class McpFallbackService {
private final ChatClient.Builder chatClientBuilder;
private final SyncMcpToolCallbackProvider mcpToolCallbackProvider;
/**
* 带熔断的MCP工具调用
* 当MCP Server频繁失败时,自动切换到无工具模式
*/
@CircuitBreaker(name = "mcp-tools", fallbackMethod = "chatWithoutTools")
@Retry(name = "mcp-retry")
public String chatWithMcpTools(String userMessage) {
try {
ToolCallback[] tools = mcpToolCallbackProvider.getToolCallbacks();
if (tools.length == 0) {
log.warn("没有可用的MCP工具,降级到无工具模式");
return chatWithoutTools(userMessage, null);
}
return chatClientBuilder.build()
.prompt()
.user(userMessage)
.tools(tools)
.call()
.content();
} catch (Exception e) {
log.error("MCP工具调用失败: {}", e.getMessage());
throw e; // 让Resilience4j处理重试和熔断
}
}
/**
* 降级方法:MCP不可用时,AI仅凭自身知识回答
*/
public String chatWithoutTools(String userMessage, Exception ex) {
if (ex != null) {
log.warn("MCP服务降级,原因: {}", ex.getMessage());
}
return chatClientBuilder.build()
.prompt()
.system("""
注意:当前工具服务暂时不可用。
请基于你的知识尽力回答,并明确告知用户:
"由于工具服务暂时不可用,以下信息基于AI训练数据,可能不是最新的。"
""")
.user(userMessage)
.call()
.content();
}
}# application.yml 追加:Resilience4j配置
resilience4j:
circuitbreaker:
instances:
mcp-tools:
sliding-window-size: 10
minimum-number-of-calls: 5
failure-rate-threshold: 50 # 50%失败率触发熔断
wait-duration-in-open-state: 30s # 熔断30秒后尝试恢复
permitted-number-of-calls-in-half-open-state: 3
retry:
instances:
mcp-retry:
max-attempts: 3
wait-duration: 1s
exponential-backoff-multiplier: 2
retry-exceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException实战:用MCP让AI助手能查数据库、读文件、搜索网页
完整的企业AI助手
// EnterpriseAiAssistant.java - 完整的企业AI助手
package com.laozhang.mcp.assistant;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.stereotype.Service;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
public class EnterpriseAiAssistant {
private final ChatClient.Builder chatClientBuilder;
private final SyncMcpToolCallbackProvider mcpToolCallbackProvider;
/**
* 场景1:数据分析 - 让AI分析业务数据
* 示例:分析过去30天的销售情况,找出哪个产品类别增长最快
*/
public String analyzeBusinessData(String analysisRequest) {
return chatClientBuilder.build()
.prompt()
.system("""
你是一个数据分析师。请使用数据库工具查询相关数据,
进行深入分析,给出有业务价值的洞察。
数据分析步骤:
1. 先了解数据库结构(list_tables)
2. 查询所需数据(read_query)
3. 对数据进行分析和计算
4. 给出有业务价值的结论和建议
输出格式:
- 数据摘要
- 关键发现(3-5条)
- 行动建议
""")
.user(analysisRequest)
.tools(mcpToolCallbackProvider.getToolCallbacks())
.call()
.content();
}
/**
* 场景2:文档问答 - 让AI读取和理解内部文档
* 示例:搜索关于新员工入职流程的文档
*/
public String searchDocuments(String question) {
return chatClientBuilder.build()
.prompt()
.system("""
你是一个内部知识库助手。
可以读取公司内部文档目录下的文件。
步骤:
1. 先列出相关目录的文件(list_directory)
2. 读取最相关的文件(read_file)
3. 基于文档内容回答问题
4. 如果文档中没有答案,明确告知
始终告知信息来源于哪个文件。
""")
.user(question)
.tools(mcpToolCallbackProvider.getToolCallbacks())
.call()
.content();
}
/**
* 场景3:研究助手 - 结合搜索和数据库
* 示例:查找竞品价格信息,与我们的产品价格对比分析
*/
public String researchAndCompare(String researchRequest) {
return chatClientBuilder.build()
.prompt()
.system("""
你是一个市场研究助手,可以:
1. 搜索网络获取最新信息(brave_search)
2. 查询内部数据库获取我们的产品数据(read_query)
3. 读取竞品分析文档(read_file)
综合使用这些工具,给出全面的分析报告。
区分哪些信息来自网络搜索,哪些来自内部数据。
""")
.user(researchRequest)
.tools(mcpToolCallbackProvider.getToolCallbacks())
.call()
.content();
}
/**
* 场景4:代码助手 - 读取代码文件并分析
* 示例:分析UserService.java文件,找出潜在的性能问题
*/
public String analyzeCode(String codeAnalysisRequest) {
return chatClientBuilder.build()
.prompt()
.system("""
你是一个高级Java工程师。可以读取代码文件进行分析。
分析时重点关注:
- 性能问题(N+1查询、不必要的循环)
- 安全漏洞(SQL注入、参数未校验)
- 代码规范(命名、注释、异常处理)
- 可以改进的设计(设计模式、SOLID原则)
对每个问题提供具体的代码修改建议。
""")
.user(codeAnalysisRequest)
.tools(mcpToolCallbackProvider.getToolCallbacks())
.call()
.content();
}
}REST API层
// EnterpriseAssistantController.java
package com.laozhang.mcp.controller;
import com.laozhang.mcp.assistant.EnterpriseAiAssistant;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/assistant")
@RequiredArgsConstructor
@Tag(name = "企业AI助手")
public class EnterpriseAssistantController {
private final EnterpriseAiAssistant assistant;
@PostMapping("/analyze")
@Operation(summary = "数据分析", description = "使用自然语言分析业务数据")
public ResponseEntity<AssistantResponse> analyzeData(
@RequestBody AssistantRequest request) {
String result = assistant.analyzeBusinessData(request.query());
return ResponseEntity.ok(new AssistantResponse(result, "data-analysis"));
}
@PostMapping("/search-docs")
@Operation(summary = "文档搜索", description = "搜索内部文档")
public ResponseEntity<AssistantResponse> searchDocs(
@RequestBody AssistantRequest request) {
String result = assistant.searchDocuments(request.query());
return ResponseEntity.ok(new AssistantResponse(result, "document-search"));
}
@PostMapping("/research")
@Operation(summary = "市场研究", description = "结合网络搜索和内部数据进行研究")
public ResponseEntity<AssistantResponse> research(
@RequestBody AssistantRequest request) {
String result = assistant.researchAndCompare(request.query());
return ResponseEntity.ok(new AssistantResponse(result, "research"));
}
@PostMapping("/code-review")
@Operation(summary = "代码分析", description = "分析代码文件")
public ResponseEntity<AssistantResponse> codeReview(
@RequestBody AssistantRequest request) {
String result = assistant.analyzeCode(request.query());
return ResponseEntity.ok(new AssistantResponse(result, "code-analysis"));
}
record AssistantRequest(String query) {}
record AssistantResponse(String answer, String type) {}
}调试:MCP调用日志与问题排查
日志配置和分析
# 详细的MCP调试日志
logging:
level:
# MCP协议层日志(JSON-RPC消息)
org.springframework.ai.mcp: TRACE
# Spring AI工具调用日志
org.springframework.ai.chat: DEBUG
# 你自己的代码日志
com.laozhang.mcp: DEBUG
# 底层HTTP客户端(HTTP SSE模式)
reactor.netty: INFO// McpDebugInterceptor.java - 拦截并记录所有MCP调用
package com.laozhang.mcp.debug;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Slf4j
@Aspect
@Component
@ConditionalOnProperty(name = "mcp.debug.enabled", havingValue = "true")
public class McpDebugInterceptor {
/**
* 拦截所有MCP工具调用,记录详细日志
*/
@Around("execution(* org.springframework.ai.mcp..*(..))")
public Object interceptMcpCall(ProceedingJoinPoint pjp) throws Throwable {
String methodName = pjp.getSignature().toShortString();
Object[] args = pjp.getArgs();
log.debug("[MCP调试] >> 调用: {}", methodName);
if (args.length > 0) {
log.debug("[MCP调试] >> 参数: {}", Arrays.toString(args));
}
long start = System.currentTimeMillis();
try {
Object result = pjp.proceed();
long duration = System.currentTimeMillis() - start;
log.debug("[MCP调试] << 返回: {} ({}ms)", methodName, duration);
if (result != null) {
String resultStr = result.toString();
log.debug("[MCP调试] << 结果预览: {}",
resultStr.substring(0, Math.min(200, resultStr.length())));
}
return result;
} catch (Exception e) {
long duration = System.currentTimeMillis() - start;
log.error("[MCP调试] !! 异常: {} ({}ms) - {}", methodName, duration, e.getMessage());
throw e;
}
}
}常见问题排查指南
// McpHealthChecker.java - 检查MCP Server健康状态
package com.laozhang.mcp.health;
import io.modelcontextprotocol.client.McpSyncClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Component("mcpHealth")
@RequiredArgsConstructor
public class McpHealthChecker implements HealthIndicator {
private final List<McpSyncClient> mcpClients;
@Override
public Health health() {
Map<String, Object> details = new HashMap<>();
boolean allHealthy = true;
for (int i = 0; i < mcpClients.size(); i++) {
McpSyncClient client = mcpClients.get(i);
String clientId = "mcp-client-" + i;
try {
// 尝试列出工具(作为健康检查)
var result = client.listTools();
int toolCount = result != null && result.tools() != null
? result.tools().size() : 0;
details.put(clientId, Map.of(
"status", "UP",
"toolCount", toolCount
));
log.debug("MCP Client {} 健康,工具数: {}", clientId, toolCount);
} catch (Exception e) {
allHealthy = false;
details.put(clientId, Map.of(
"status", "DOWN",
"error", e.getMessage()
));
log.error("MCP Client {} 不健康: {}", clientId, e.getMessage());
}
}
return allHealthy
? Health.up().withDetails(details).build()
: Health.down().withDetails(details).build();
}
}调试检查清单
// McpTroubleshooter.java - MCP问题诊断工具
@Component
@RequiredArgsConstructor
@Slf4j
public class McpTroubleshooter {
private final SyncMcpToolCallbackProvider mcpToolCallbackProvider;
private final List<McpSyncClient> mcpClients;
/**
* 运行完整的MCP诊断
*/
public Map<String, Object> diagnose() {
Map<String, Object> report = new LinkedHashMap<>();
// 1. 检查工具是否加载
ToolCallback[] tools = mcpToolCallbackProvider.getToolCallbacks();
report.put("toolsLoaded", tools.length);
if (tools.length == 0) {
report.put("problem", "没有工具被加载,检查:\n" +
"1. application.yml中spring.ai.mcp.client.enabled=true\n" +
"2. MCP Server是否成功启动(检查启动日志)\n" +
"3. Node.js/Java是否已安装(stdio模式)\n" +
"4. npx命令是否可执行");
}
// 2. 列出所有工具名
List<String> toolNames = Arrays.stream(tools)
.map(ToolCallback::getName)
.collect(Collectors.toList());
report.put("toolNames", toolNames);
// 3. 检查每个Client的状态
List<Map<String, Object>> clientStatus = new ArrayList<>();
for (int i = 0; i < mcpClients.size(); i++) {
Map<String, Object> status = new HashMap<>();
try {
var result = mcpClients.get(i).listTools();
status.put("index", i);
status.put("healthy", true);
status.put("toolCount", result != null && result.tools() != null
? result.tools().size() : 0);
} catch (Exception e) {
status.put("index", i);
status.put("healthy", false);
status.put("error", e.getMessage());
}
clientStatus.add(status);
}
report.put("clientStatus", clientStatus);
return report;
}
}生产注意事项
1. Node.js依赖管理
stdio模式的MCP Server(@modelcontextprotocol/server-*)需要Node.js:
# 确认Node.js已安装
node --version # 要求 >= 18.x
# 确认npx可用
npx --version
# 预先安装MCP Server(避免首次启动慢)
npm install -g @modelcontextprotocol/server-filesystem
npm install -g @modelcontextprotocol/server-sqlite// NodejsChecker.java - 启动时检查Node.js依赖
@Component
@Slf4j
public class NodejsChecker implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
try {
Process p = Runtime.getRuntime().exec("node --version");
p.waitFor();
if (p.exitValue() == 0) {
log.info("Node.js可用,stdio MCP Server正常启动");
} else {
log.error("Node.js不可用!stdio模式的MCP Server将无法启动。请安装Node.js >= 18.x");
}
} catch (Exception e) {
log.error("Node.js检查失败,可能未安装: {}", e.getMessage());
}
}
}2. 工具调用幂等性
某些MCP工具有副作用(写文件、发消息),要防止重复调用:
// IdempotencyGuard.java
@Component
public class IdempotencyGuard {
private final Set<String> recentCallSignatures =
Collections.synchronizedSet(new LinkedHashSet<>() {
@Override
protected boolean removeEldestEntry(Map.Entry<String, ?> eldest) {
return size() > 1000; // 最多记录1000条
}
});
public boolean isDuplicate(String toolName, String input) {
String signature = toolName + ":" + input.hashCode();
boolean isDup = !recentCallSignatures.add(signature);
if (isDup) {
log.warn("检测到重复工具调用: {}", toolName);
}
return isDup;
}
}3. 生产环境MCP Server部署
# Dockerfile for Java MCP Server
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY db-mcp-server.jar .
COPY application-prod.yml .
# stdio MCP Server通过stdin/stdout通信,不需要暴露端口
# 父进程(Spring AI MCP Client)会启动此进程
ENTRYPOINT ["java", \
"-jar", "db-mcp-server.jar", \
"--spring.config.location=application-prod.yml"]常见问题解答
Q1:MCP Server启动很慢(npx每次都要下载),怎么优化?
A:三种方案:
- 全局安装:
npm install -g @modelcontextprotocol/server-filesystem,之后用node $(which filesystem-server)替代npx - 本地缓存:在项目中
npm install提前缓存,用node node_modules/.bin/xxx - 换Java版本的MCP Server:用Spring AI MCP Server框架自己实现,不依赖Node.js
Q2:HTTP SSE模式的MCP Server断线了怎么处理?
A:Spring AI MCP Client内置了重连机制。如果需要自定义重连策略:
spring.ai.mcp.client.servers.remote-db:
reconnect-delay: PT5S # 断线5秒后重连
max-reconnect-attempts: 10同时建议配置熔断器(Resilience4j),避免重连期间请求积压。
Q3:AI调用了不该调用的工具(误操作),怎么防止?
A:三层防护:
- 系统提示中明确说明工具使用限制
- MCP Server本身限制危险操作(数据库Server只允许SELECT)
- 代码层过滤工具:只注入当前场景需要的工具,不是所有工具
Q4:多个MCP Server工具名冲突怎么处理?
A:Spring AI MCP Client会自动处理命名冲突,通常通过给工具名加前缀(ServerName_ToolName)。也可以在配置中指定工具名前缀:
spring.ai.mcp.client.servers.database:
tool-name-prefix: "db_" # 该Server的所有工具名加db_前缀Q5:如何测试MCP集成,不想每次都启动真实的MCP Server?
A:两种方案:
- Mock MCP Server:实现一个内存版MCP Server用于测试
- 用MCP Inspector工具录制真实MCP Server的响应,回放Mock
测试代码示例:
@SpringBootTest
@TestPropertySource(properties = {
"spring.ai.mcp.client.enabled=false" // 测试时禁用MCP
})
class McpIntegrationTest {
// 在测试中注入Mock工具
}Q6:如何监控MCP工具的调用频率和错误率?
A:结合Spring Boot Actuator和Micrometer:
// 在工具调用时记录指标
@EventListener
public void onToolCall(ToolCallEvent event) {
meterRegistry.counter("mcp.tool.calls",
"tool", event.getToolName(),
"success", String.valueOf(event.isSuccess())
).increment();
}然后用Prometheus + Grafana监控,设置错误率告警(如某工具5分钟内错误率超过10%则告警)。
总结
MCP集成让AI工具接入从"每次都要写代码"变成了"配置即使用"。
可操作行动清单:
工具标准化了,剩下的就是发挥想象力。
