实用指南:大模型开发 - Spring AI MCP:构建通用UniversalToolInvoker实现服务端工具调用
文章目录

引言
Model Context Protocol (MCP) 是一种开放协议,用于在AI应用和工具之间建立标准化的通信接口。在基于Spring Boot的AI应用中,如何构建一个统一、安全、高效的工具调用机制是核心挑战。本文将介绍一个基于Spring AI 1.1.0的MCP客户端实现,通过UniversalToolInvoker模式实现通用工具调用,并集成细粒度的访问控制系统。
项目背景与技术栈
本项目是一个完整的Spring AI MCP集成示例,展示如何构建生产级的MCP服务器和客户端系统:
核心技术栈:
- Java 17+ / Spring Boot 3.5.7 / Spring AI 1.1.0
- WebFlux (Netty) 响应式编程模型
- Maven多模块架构
- DeepSeek AI集成
关键特性:
- 工具分组访问控制 (Tool Group Access Control)
- 多API Key认证机制
- 运行时权限验证
- 响应式SSE长连接支持
整体架构设计
系统架构概览
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Web Client │───▶│ mcp-tool-client │───▶│ mcp-server- │
│ (Port 8083) │ │ (UniversalTool- │ │ webflux (Port │
│ │ │ Invoker) │ │ 8086) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────┐ ┌──────────────┐
│ ChatClient │ │ Tool Facedes │
│ + AI Model │ │ + Services │
└─────────────┘ └──────────────┘
核心组件关系
- UniversalToolInvoker - 统一工具调用器
- UniversalToolCallbackProvider - 工具回调提供者
- ChatApiController - 聊天API控制器
- 访问控制层 - 多层权限验证机制
UniversalToolInvoker核心实现
1. 架构设计原理
UniversalToolInvoker采用装饰器模式,将所有MCP工具调用统一到一个入口,实现了以下设计目标:
- 统一接口:所有远程MCP工具通过单一
invokeTool方法调用 - 权限注入:自动注入
_toolName和_toolGroup权限元数据 - 安全隔离:保持工具发现/调用在MCP协议边界内
- 线程安全:使用ThreadLocal管理会话级别的工具分组
2. 核心实现代码
@Component
public class UniversalToolInvoker {
private static final Logger log = LoggerFactory.getLogger(UniversalToolInvoker.class);
private static final ObjectMapper objectMapper = new ObjectMapper();
private final SyncMcpToolCallbackProvider mcpToolProvider;
// ThreadLocal存储当前请求的工具分组
private static final ThreadLocal<String> currentToolGroup = new ThreadLocal<>();
/**
* 通用工具调用方法
* 调用格式示例:
* {
* "toolName": "findBooksByTitle",
* "parameters": {"title": "Spring"},
* "_toolGroup": "admin"
* }
*/
public String invokeTool(String requestJson) {
log.info("统一工具调用器接收请求: {}", requestJson);
try {
// 1. 解析请求JSON
@SuppressWarnings("unchecked")
Map<String, Object> request = objectMapper.readValue(requestJson, Map.class);
String toolName = (String) request.get("toolName");
@SuppressWarnings("unchecked")
Map<String, Object> parameters = (Map<String, Object>) request.get("parameters");
// 2. 参数校验
if (toolName == null || toolName.trim().isEmpty()) {
return buildErrorResponse("INVALID_TOOL_NAME", "工具名称不能为空");
}
// 3. 查找工具
ToolCallback tool = findTool(toolName);
if (tool == null) {
return buildErrorResponse("TOOL_NOT_FOUND", "工具 " + toolName + " 不存在");
}
// 4. 处理权限元数据注入
if (parameters == null) {
parameters = new java.util.HashMap<>();
}
// 工具分组处理逻辑
String toolGroupFromRequest = (String) request.get("_toolGroup");
if (toolGroupFromRequest == null || toolGroupFromRequest.trim().isEmpty()) {
String toolGroupFromThreadLocal = getToolGroup();
if (toolGroupFromThreadLocal != null) {
parameters.put("_toolGroup", toolGroupFromThreadLocal);
}
} else {
parameters.put("_toolGroup", toolGroupFromRequest);
}
// ===== 重要:添加工具名称到参数中供服务端权限校验使用 =====
parameters.put("_toolName", toolName);
log.info("添加工具名称到参数中供服务端权限校验: _toolName={}", toolName);
// 5. 调用工具(通过MCP协议)
String parametersJson = objectMapper.writeValueAsString(parameters);
String result = tool.call(parametersJson);
// 6. 返回成功响应
return objectMapper.writeValueAsString(Map.of(
"success", true,
"data", result
));
} catch (Exception e) {
log.error("调用工具时发生错误", e);
return buildErrorResponse("EXECUTION_ERROR", "工具执行失败: " + e.getMessage());
}
}
private ToolCallback findTool(String toolName) {
ToolCallback[] callbacks = mcpToolProvider.getToolCallbacks();
for (ToolCallback callback : callbacks) {
if (callback.getToolDefinition().name().equals(toolName)) {
return callback;
}
}
return null;
}
}
3. 关键设计特点
ThreadLocal上下文管理
// 设置当前线程的工具分组
public static void setToolGroup(String toolGroup) {
currentToolGroup.set(toolGroup);
log.debug("设置当前线程工具分组: {}", toolGroup);
}
// 清理ThreadLocal防止内存泄漏
public static void clearToolGroup() {
currentToolGroup.remove();
}
权限元数据自动注入
// 自动注入权限元数据
parameters.put("_toolName", toolName); // 服务端验证工具访问权限
parameters.put("_toolGroup", toolGroup); // 服务端检查组权限
UniversalToolCallbackProvider集成
工具回调包装器
UniversalToolCallbackProvider将UniversalToolInvoker包装为Spring AI可识别的ToolCallback:
@Component
public class UniversalToolCallbackProvider implements ToolCallbackProvider {
private final UniversalToolInvoker universalToolInvoker;
private final ToolCallback invokeToolCallback;
public UniversalToolCallbackProvider(UniversalToolInvoker universalToolInvoker) {
this.universalToolInvoker = universalToolInvoker;
// 创建ToolCallback包装器
this.invokeToolCallback = new ToolCallback() {
@Override
public String call(String argumentsJson) {
return universalToolInvoker.invokeTool(argumentsJson);
}
@Override
public ToolDefinition getToolDefinition() {
return ToolDefinition.builder()
.name("invokeTool")
.description("通用工具调用器,用于调用各种MCP工具")
.inputSchema("""
{
"type": "object",
"properties": {
"toolName": {
"type": "string",
"description": "要调用的工具名称"
},
"parameters": {
"type": "object",
"description": "工具参数"
},
"_toolGroup": {
"type": "string",
"description": "工具分组(可选),如admin或basic"
}
},
"required": ["toolName"]
}
""")
.build();
}
};
}
@Override
public ToolCallback[] getToolCallbacks() {
return new ToolCallback[]{invokeToolCallback};
}
}
ChatClient集成与控制流程
聊天控制器实现
ChatApiController展示了如何将UniversalToolInvoker集成到Spring AI ChatClient中:
@RestController
@RequestMapping("/api/chat")
public class ChatApiController {
private final ChatClient chatClient;
private final UniversalToolInvoker universalToolInvoker;
public ChatApiController(ChatClient.Builder chatClientBuilder,
UniversalToolInvoker universalToolInvoker) {
this.universalToolInvoker = universalToolInvoker;
// 使用UniversalToolCallbackProvider构建ChatClient
this.chatClient = chatClientBuilder
.defaultToolCallbacks(new UniversalToolCallbackProvider(universalToolInvoker))
.build();
}
@PostMapping("/call")
public List<Message> chatCall(@RequestParam("chatId") String chatId,
@RequestParam("message") String message,
@RequestParam(value = "toolGroup", defaultValue = "admin") String toolGroup) {
// 设置当前请求的工具分组
UniversalToolInvoker.setToolGroup(toolGroup);
try {
// 构建动态系统提示词
String systemPrompt = buildSystemPrompt(universalToolInvoker, toolGroup);
// 配置聊天选项,手动控制工具调用过程
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.internalToolExecutionEnabled(false) // 手动控制工具调用
.build();
// 构建聊天提示
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage(systemPrompt));
messages.addAll(chatMemory.get(chatId));
Prompt chatPrompt = new Prompt(messages, chatOptions);
// 手动工具调用循环
ChatResponse chatResponse = chatClient.prompt(chatPrompt).call().chatResponse();
while (chatResponse.hasToolCalls()) {
// 执行工具调用
ToolExecutionResult toolExecutionResult =
toolCallingManager.executeToolCalls(chatPrompt, chatResponse);
// 处理工具执行结果
List<Message> toolResultMessages = toolExecutionResult.conversationHistory();
processAddMessageToChatMemory(chatId, toolResultMessages);
// 继续对话
chatPrompt = new Prompt(chatMemory.get(chatId), chatOptions);
chatResponse = chatClient.prompt(chatPrompt).call().chatResponse();
}
return List.of(chatResponse.getResult().getOutput());
} finally {
// 清理工具分组ThreadLocal
UniversalToolInvoker.clearToolGroup();
}
}
}
动态系统提示词生成
private String buildSystemPrompt(UniversalToolInvoker universalToolInvoker, String toolGroup) {
StringBuilder prompt = new StringBuilder();
// 基础提示词
prompt.append(String.format("""
你是一个MCP小助手,可以使用invokeTool函数调用各种工具来帮助用户完成任务。
当前工具分组: %s
**严格限制**:
1. 你**只能**使用下面"可用工具列表"中明确列出的工具名称
2. **禁止**猜测、捏造或使用任何未在列表中的工具名
3. 在调用invokeTool时,必须在参数中包含"_toolGroup"字段,值为"%s"
调用格式示例:
```json
{
"toolName": "必须是下面列表中的确切工具名",
"parameters": {"参数名": "参数值"},
"_toolGroup": "%s"
}
```
""", toolGroup, toolGroup, toolGroup));
// 动态添加可用工具列表
prompt.append("## ⚠️ 可用工具列表(严格限制)\n\n");
ToolCallback[] tools = universalToolInvoker.getAvailableTools();
for (ToolCallback tool : tools) {
String toolName = tool.getToolDefinition().name();
String description = tool.getToolDefinition().description();
prompt.append(String.format("### `%s`\n", toolName));
prompt.append(String.format("**功能**: %s\n", description));
String inputSchema = tool.getToolDefinition().inputSchema();
if (inputSchema != null && !inputSchema.isEmpty()) {
prompt.append(String.format("**参数**: %s\n", inputSchema));
}
prompt.append("\n");
}
return prompt.toString();
}
服务端访问控制系统
多层权限验证架构
服务端实现了完整的访问控制系统,包含三个验证层次:
┌─────────────────────────────────────────────────────────────┐
│ 多层权限验证架构 │
├─────────────────────────────────────────────────────────────┤
│ 1. 请求级别验证 (ApiKeyWebFilter) │
│ - API Key验证 │
│ - 工具组映射 │
│ - 限流防护 │
├─────────────────────────────────────────────────────────────┤
│ 2. 工具注册级别验证 (GroupAwareToolCallbackProvider) │
│ - 预计算工具映射 │
│ - O(1)权限查找 │
│ - 动态工具过滤 │
├─────────────────────────────────────────────────────────────┤
│ 3. 执行级别验证 (ToolGroupExtractorCallback) │
│ - 实时权限校验 │
│ - 元数据提取 │
│ - 执行前验证 │
└─────────────────────────────────────────────────────────────┘
GroupAwareToolCallbackProvider
工具分组感知的ToolCallbackProvider,负责在工具注册阶段进行权限过滤:
@Slf4j
public class GroupAwareToolCallbackProvider implements ToolCallbackProvider {
// 预计算的工具映射:分组名 -> 工具名集合
private final Map<String, Set<String>> groupToToolsMap = new ConcurrentHashMap<>();
// 工具名 -> ToolCallback映射(快速查找)
private final Map<String, ToolCallback> toolNameToCallbackMap = new ConcurrentHashMap<>();
@Override
public ToolCallback[] getToolCallbacks() {
if (!toolGroupProperties.isEnabled()) {
return wrapWithExtractor(delegate.getToolCallbacks());
}
// 获取当前请求的工具分组
String currentGroup = ToolGroupContextHolder.getToolGroupOrDefault(
toolGroupProperties.getDefaultGroup());
// O(1)查找该分组允许的工具集合
Set<String> allowedTools = groupToToolsMap.getOrDefault(currentGroup, Collections.emptySet());
// 过滤工具列表
ToolCallback[] allCallbacks = delegate.getToolCallbacks();
List<ToolCallback> filteredCallbacks = new ArrayList<>();
for (ToolCallback callback : allCallbacks) {
if (allowedTools.contains(callback.getToolDefinition().name())) {
filteredCallbacks.add(callback);
}
}
// 用权限校验器包装每个工具
return wrapWithExtractor(filteredCallbacks.toArray(new ToolCallback[0]));
}
/**
* 验证工具是否在当前分组的权限范围内
*/
public boolean isToolAllowed(String toolName) {
if (!toolGroupProperties.isEnabled()) {
return true;
}
String currentGroup = ToolGroupContextHolder.getToolGroupOrDefault(
toolGroupProperties.getDefaultGroup());
Set<String> allowedTools = groupToToolsMap.getOrDefault(currentGroup, Collections.emptySet());
return allowedTools.contains(toolName);
}
}
ToolGroupExtractorCallback
工具调用拦截器,在执行前进行最终权限验证:
@Slf4j
public class ToolGroupExtractorCallback implements ToolCallback {
private final ToolCallback delegate;
private final ObjectMapper objectMapper = new ObjectMapper();
private GroupAwareToolCallbackProvider groupAwareToolCallbackProvider;
@Override
public String call(String toolInput) {
String extractedToolGroup = null;
String extractedToolName = null;
String cleanedToolInput = toolInput;
String actualToolName = getToolDefinition().name();
try {
// 解析工具输入参数
JsonNode inputNode = objectMapper.readTree(toolInput);
// 提取_toolGroup字段
if (inputNode.has("_toolGroup")) {
extractedToolGroup = inputNode.get("_toolGroup").asText();
// 从参数中移除_toolGroup字段
if (inputNode instanceof ObjectNode) {
((ObjectNode) inputNode).remove("_toolGroup");
}
}
// 提取_toolName字段
if (inputNode.has("_toolName")) {
extractedToolName = inputNode.get("_toolName").asText();
// 从参数中移除_toolName字段
if (inputNode instanceof ObjectNode) {
((ObjectNode) inputNode).remove("_toolName");
}
}
// 重新生成清理后的参数JSON
if (inputNode instanceof ObjectNode) {
cleanedToolInput = objectMapper.writeValueAsString(inputNode);
}
} catch (Exception e) {
log.warn("解析工具参数时发生错误,将使用原始参数: {}", e.getMessage());
}
// 设置工具分组上下文
if (extractedToolGroup != null) {
ToolGroupContextHolder.setToolGroup(extractedToolGroup);
}
try {
// ===== 权限校验 =====
String toolNameForValidation = extractedToolName != null ? extractedToolName : actualToolName;
if (!validateToolPermission(toolNameForValidation)) {
String currentGroup = ToolGroupContextHolder.getToolGroup();
throw new SecurityException(
String.format("工具 '%s' 在分组 '%s' 中无权限访问", toolNameForValidation, currentGroup));
}
// 验证工具名称匹配性
if (extractedToolName != null && !extractedToolName.equals(actualToolName)) {
log.warn("工具名称不匹配: requested={}, actual={}", extractedToolName, actualToolName);
}
// 调用实际工具(使用清理后的参数)
return delegate.call(cleanedToolInput);
} finally {
// 清理ThreadLocal
if (extractedToolGroup != null) {
ToolGroupContextHolder.clear();
}
}
}
private boolean validateToolPermission(String toolName) {
if (groupAwareToolCallbackProvider == null) {
return true; // 向后兼容
}
return groupAwareToolCallbackProvider.isToolAllowed(toolName);
}
}
配置
客户端配置
# application.yml
spring:
ai:
openai:
api-key: your-deepseek-api-key
base-url: https://api.deepseek.com
chat:
options:
model: deepseek-chat
# DeepSeek不提供embedding模型,需要禁用
embedding:
enabled: false
mcp:
client:
# 多API Key配置
api-key:
x-api-key: ${MCP_API_KEY:default-api-key}
another-x-api-key: ${MCP_ANOTHER_KEY:default-another-key}
# MCP服务器配置
servers:
- name: "mcp-server"
url: "http://localhost:8086/sse"
transport: "sse"
服务端访问控制配置
# application-auth.yml
mcp:
auth:
# 多API Key与组映射
api-keys:
- key: "${MCP_BASIC_KEY:basic-client-key-001}"
groups: ["basic"]
description: "Basic client (read-only)"
- key: "${MCP_ADMIN_KEY:admin-client-key-002}"
groups: ["basic", "admin"]
description: "Admin client (full access)"
header-name: X-Api-Key
tool-groups:
enabled: true
default-group: admin
group-header-name: X-Tool-Group
definitions:
basic:
tools: [findBooksByTitle, findBooksByISBN, getBookCategories]
description: "Basic query permissions"
admin:
tools: [findBooksByTitle, findBooksByAuthor, findBooksByCategory,
findBooksByISBN, findBooksByPublicationDateRange,
findBookRecommendations, getBookCategories, getPopularBooks]
description: "Full management permissions"
性能特点与安全性
性能优化
- 预计算映射:工具到组的映射在启动时预计算,实现O(1)查找
- ThreadLocal上下文:零分配的权限上下文传递
- 缓存验证结果:请求生命周期内的权限结果缓存
- 响应式模型:WebFlux事件循环支持高并发
// 性能指标
- 权限验证延迟: ~1-2ms
- 预计算耗时: 启动时一次性 ~50-100ms
- 内存开销: 每个API Key ~200-500bytes
- 并发支持: 10,000+ connections (WebFlux)
安全特性
- 多层验证:请求、注册、执行三层权限验证
- 元数据清理:权限参数在传递给业务逻辑前被移除
- ** Fail-Safe默认**:权限缺失时抛出安全异常,不静默跳过
- 工具名称验证:客户端请求的工具名与服务端实际执行的工具名进行一致性校验
// 安全检查流程
1. API Key验证 → ApiKeyWebFilter
2. 工具组映射 → ToolGroupContextHolder
3. 工具列表过滤 → GroupAwareToolCallbackProvider
4. 运行时权限校验 → ToolGroupExtractorCallback
5. 参数清理移除 → 移除_toolGroup, _toolName
代码示例与最佳实践
完整调用示例
// 1. 客户端发起调用
@PostMapping("/call")
public ResponseEntity<String> callTool(@RequestBody ToolRequest request) {
// 设置工具分组到ThreadLocal
UniversalToolInvoker.setToolGroup("admin");
try {
// 构建调用参数
Map<String, Object> parameters = Map.of(
"title", "Spring Boot",
"author", "Josh Long"
);
// 调用UniversalToolInvoker
String result = universalToolInvoker.invokeTool(
objectMapper.writeValueAsString(Map.of(
"toolName", "findBooksByTitle",
"parameters", parameters
))
);
return ResponseEntity.ok(result);
} finally {
// 清理ThreadLocal
UniversalToolInvoker.clearToolGroup();
}
}
// 2. 服务端权限验证链
// ApiKeyWebFilter → GroupAwareToolCallbackProvider → ToolGroupExtractorCallback
最佳实践建议
ThreadLocal管理:
- 始终在finally块中清理ThreadLocal
- 使用try-with-resources模式包装上下文
权限设计:
- 遵循最小权限原则
- 定期审计工具组权限配置
- 使用环境变量管理敏感配置
错误处理:
- 统一错误响应格式
- 记录详细的安全审计日志
- 对客户端隐藏内部实现细节
性能优化:
- 合理配置WebFlux线程池
- 监控权限验证延迟
- 定期清理过期的权限缓存
测试验证
权限控制测试
# 测试basic API Key(受限工具)
curl -H "X-Api-Key: basic-client-key-001" \
-H "X-Tool-Group: basic" \
-H "Content-Type: application/json" \
-d '{"toolName":"findBooksByTitle","parameters":{"title":"Spring"}}' \
http://localhost:8086/sse
# 测试admin API Key(完整权限)
curl -H "X-Api-Key: admin-client-key-002" \
-H "X-Tool-Group: admin" \
-H "Content-Type: application/json" \
-d '{"toolName":"getPopularBooks","parameters":{}}' \
http://localhost:8086/sse
# 测试权限违规(basic key调用admin工具)
# 应返回403 Forbidden
curl -H "X-Api-Key: basic-client-key-001" \
-H "X-Tool-Group: basic" \
-H "Content-Type: application/json" \
-d '{"toolName":"getPopularBooks","parameters":{}}' \
http://localhost:8086/sse
集成测试
@SpringBootTest
@ActiveProfiles("test")
class ToolGroupAccessControlIntegrationTest {
@Test
void testBasicGroupToolAccess() {
// 测试basic组只能访问指定工具
String result = universalToolInvoker.invokeTool("""
{
"toolName": "findBooksByTitle",
"parameters": {"title": "Spring"},
"_toolGroup": "basic"
}
""");
assertThat(result).contains("\"success\":true");
}
@Test
void testAdminGroupFullAccess() {
// 测试admin组可以访问所有工具
String result = universalToolInvoker.invokeTool("""
{
"toolName": "getPopularBooks",
"parameters": {},
"_toolGroup": "admin"
}
""");
assertThat(result).contains("\"success\":true");
}
@Test
void testUnauthorizedToolAccess() {
// 测试未授权工具访问应该失败
assertThatThrownBy(() -> {
universalToolInvoker.invokeTool("""
{
"toolName": "getPopularBooks",
"parameters": {},
"_toolGroup": "basic"
}
""");
}).isInstanceOf(SecurityException.class)
.hasMessageContaining("无权限访问");
}
}
总结与展望
技术价值
通过UniversalToolInvoker模式的实现,我们成功地:
- 统一了工具调用接口:所有MCP工具通过单一入口调用,简化了客户端集成复杂度
- 实现了细粒度权限控制:基于工具组的多层权限验证,确保安全的工具访问
- 提供了高性能实现:预计算映射和ThreadLocal上下文实现了毫秒级权限验证
- 保持了协议纯净性:所有工具发现和调用严格在MCP协议边界内
架构优势
- 可扩展性:新增工具只需在服务端添加@Tool注解方法
- 安全性:多层权限验证和元数据自动注入
- 可维护性:清晰的组件职责分离和统一的调用模式
- 性能:响应式架构和预计算优化支持高并发
未来扩展方向
- 动态工具发现:支持运行时工具注册/注销
- 权限继承机制:工具组的继承和权限传播
- 审计日志增强:详细的工具调用审计和分析
- 缓存策略优化:分布式权限缓存和失效机制
- 多租户支持:租户级别的工具隔离和权限管理
这个UniversalToolInvoker实现为Spring AI MCP应用提供了一个生产级的解决方案,不仅解决了工具调用的技术挑战,更重要的是建立了一套完整的访问控制体系,为AI应用的安全部署奠定了坚实基础。


浙公网安备 33010602011771号