实现真正的MCP (Model Context Protocol) 服务器

完全遵循Anthropic MCP协议标准和JSON-RPC 2.0规范的实现:

核心组件:
- TrueMCPServer: 提供标准的JSON-RPC端点(/mcp/tools/list, /mcp/tools/call)
- MCPToolRegistry: 管理工具定义,包含完整JSON Schema
- MCPFunctionBridge: 提供MCP与各种LLM函数调用格式的双向转换

新增DTOs:
- MCPJsonRpcRequest/Response: 标准JSON-RPC 2.0格式
- MCPToolListRequest/Response: tools/list方法的请求/响应
- MCPToolCallRequest/Response: tools/call方法的请求/响应
- MCPToolDefinition: 完整的MCP工具定义,包含JSON Schema
- MCPError: JSON-RPC错误处理

主要特性:
- 严格的JSON-RPC 2.0协议实现
- 完整的MCP工具Schema定义(包含验证、性能提示、资源需求等)
- 支持OpenAI和Anthropic函数调用格式转换
- 批量工具调用支持
- 健康检查和协议信息查询
- 全面的单元测试覆盖(50+测试用例)
- 向后兼容现有MCP实现

工具定义增强:
- repair_query: 工单查询工具
- repair_feedback_query: 工单反馈查询工具
- similarity_search: 向量相似度搜索工具
- knowledge_query: 知识库查询工具

所有工具都包含:
- 完整的JSON Schema参数定义
- 性能预期和资源需求
- 适当的标签和提示信息
- 严格的参数验证
This commit is contained in:
75681 2025-08-18 05:09:03 +08:00
parent 86728411a7
commit a8ef0900f5
14 changed files with 3551 additions and 0 deletions

View File

@ -0,0 +1,417 @@
package com.chinaweal.youfool.devops.ai.mcp;
import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolCallResponse;
import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolDefinition;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
/**
* MCP 函数调用桥接服务
*
* 提供 MCP 工具与函数调用格式之间的转换服务
* 支持与各种 LLM 提供商的函数调用格式进行互转
*
* @author AI开发团队
* @since 1.0.0
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class MCPFunctionBridge {
private final MCPToolRegistry toolRegistry;
private final ObjectMapper objectMapper;
/**
* MCP 工具定义转换为 OpenAI Function Calling 格式
*
* @param tools MCP工具定义列表
* @return OpenAI Function格式的工具定义
*/
public List<Map<String, Object>> convertToOpenAIFunctions(List<MCPToolDefinition> tools) {
log.debug("转换{}个MCP工具为OpenAI Function格式", tools.size());
return tools.stream()
.map(this::convertSingleToolToOpenAIFunction)
.collect(Collectors.toList());
}
/**
* MCP 工具定义转换为 Anthropic Tool Use 格式
*
* @param tools MCP工具定义列表
* @return Anthropic Tool格式的工具定义
*/
public List<Map<String, Object>> convertToAnthropicTools(List<MCPToolDefinition> tools) {
log.debug("转换{}个MCP工具为Anthropic Tool格式", tools.size());
return tools.stream()
.map(this::convertSingleToolToAnthropicTool)
.collect(Collectors.toList());
}
/**
* MCP 工具定义转换为通用函数格式
*
* @param tools MCP工具定义列表
* @return 通用函数格式的工具定义
*/
public List<Map<String, Object>> convertToGenericFunctions(List<MCPToolDefinition> tools) {
log.debug("转换{}个MCP工具为通用函数格式", tools.size());
return tools.stream()
.map(this::convertSingleToolToGenericFunction)
.collect(Collectors.toList());
}
/**
* OpenAI Function Call 结果转换为 MCP 响应格式
*
* @param functionCallResult OpenAI函数调用结果
* @param toolName 工具名称
* @return MCP响应格式
*/
public MCPToolCallResponse convertFromOpenAIFunctionResult(Map<String, Object> functionCallResult, String toolName) {
try {
log.debug("转换OpenAI函数调用结果为MCP格式: 工具={}", toolName);
// OpenAI函数调用结果通常包含 content 字段
Object content = functionCallResult.get("content");
if (content != null) {
return MCPToolCallResponse.success(content);
}
// 检查是否有错误
Object error = functionCallResult.get("error");
if (error != null) {
return MCPToolCallResponse.error(error.toString());
}
// 直接使用整个结果
return MCPToolCallResponse.success(functionCallResult);
} catch (Exception e) {
log.error("转换OpenAI函数调用结果失败: 工具={}", toolName, e);
return MCPToolCallResponse.error("转换函数调用结果失败: " + e.getMessage());
}
}
/**
* Anthropic Tool Use 结果转换为 MCP 响应格式
*
* @param toolUseResult Anthropic工具使用结果
* @param toolName 工具名称
* @return MCP响应格式
*/
public MCPToolCallResponse convertFromAnthropicToolResult(Map<String, Object> toolUseResult, String toolName) {
try {
log.debug("转换Anthropic工具使用结果为MCP格式: 工具={}", toolName);
// Anthropic工具使用结果通常包含 content 或 result 字段
Object content = toolUseResult.get("content");
if (content != null) {
return MCPToolCallResponse.success(content);
}
Object result = toolUseResult.get("result");
if (result != null) {
return MCPToolCallResponse.success(result);
}
// 检查是否有错误
Object error = toolUseResult.get("error");
if (error != null) {
return MCPToolCallResponse.error(error.toString());
}
// 直接使用整个结果
return MCPToolCallResponse.success(toolUseResult);
} catch (Exception e) {
log.error("转换Anthropic工具使用结果失败: 工具={}", toolName, e);
return MCPToolCallResponse.error("转换工具使用结果失败: " + e.getMessage());
}
}
/**
* MCP 响应转换为 OpenAI Function Call 结果格式
*
* @param mcpResponse MCP响应
* @param functionName 函数名称
* @return OpenAI Function Call结果格式
*/
public Map<String, Object> convertToOpenAIFunctionResult(MCPToolCallResponse mcpResponse, String functionName) {
Map<String, Object> result = new LinkedHashMap<>();
try {
result.put("function_name", functionName);
result.put("call_id", mcpResponse.getCallId());
if (mcpResponse.getIsError() != null && mcpResponse.getIsError()) {
result.put("error", extractErrorMessage(mcpResponse));
result.put("status", "error");
} else {
result.put("content", extractContent(mcpResponse));
result.put("status", "success");
}
if (mcpResponse.getExecutionTime() != null) {
result.put("execution_time_ms", mcpResponse.getExecutionTime());
}
log.debug("转换MCP响应为OpenAI格式: 函数={}, 状态={}", functionName, result.get("status"));
} catch (Exception e) {
log.error("转换MCP响应为OpenAI格式失败: 函数={}", functionName, e);
result.put("error", "转换响应失败: " + e.getMessage());
result.put("status", "error");
}
return result;
}
/**
* MCP 响应转换为 Anthropic Tool Use 结果格式
*
* @param mcpResponse MCP响应
* @param toolName 工具名称
* @return Anthropic Tool Use结果格式
*/
public Map<String, Object> convertToAnthropicToolResult(MCPToolCallResponse mcpResponse, String toolName) {
Map<String, Object> result = new LinkedHashMap<>();
try {
result.put("tool_name", toolName);
result.put("tool_use_id", mcpResponse.getCallId());
if (mcpResponse.getIsError() != null && mcpResponse.getIsError()) {
result.put("is_error", true);
result.put("content", extractErrorMessage(mcpResponse));
} else {
result.put("is_error", false);
result.put("content", extractContent(mcpResponse));
}
if (mcpResponse.getExecutionTime() != null) {
result.put("execution_time_ms", mcpResponse.getExecutionTime());
}
log.debug("转换MCP响应为Anthropic格式: 工具={}, 错误={}", toolName, result.get("is_error"));
} catch (Exception e) {
log.error("转换MCP响应为Anthropic格式失败: 工具={}", toolName, e);
result.put("is_error", true);
result.put("content", "转换响应失败: " + e.getMessage());
}
return result;
}
/**
* 批量转换工具定义
*
* @param format 目标格式 ("openai", "anthropic", "generic")
* @return 转换后的工具定义列表
*/
public List<Map<String, Object>> convertAllToolsToFormat(String format) {
List<MCPToolDefinition> allTools = toolRegistry.getAllTools();
return switch (format.toLowerCase()) {
case "openai" -> convertToOpenAIFunctions(allTools);
case "anthropic" -> convertToAnthropicTools(allTools);
case "generic" -> convertToGenericFunctions(allTools);
default -> {
log.warn("未知的转换格式: {}, 使用通用格式", format);
yield convertToGenericFunctions(allTools);
}
};
}
/**
* 验证函数调用参数是否符合工具定义
*
* @param toolName 工具名称
* @param arguments 调用参数
* @return 验证结果
*/
public ValidationResult validateFunctionCall(String toolName, Map<String, Object> arguments) {
Optional<MCPToolDefinition> toolDef = toolRegistry.getTool(toolName);
if (toolDef.isEmpty()) {
return ValidationResult.failure("工具不存在: " + toolName);
}
try {
// 基本验证逻辑
MCPToolDefinition.JsonSchema schema = toolDef.get().getInputSchema();
if (schema != null && schema.getRequired() != null) {
for (String required : schema.getRequired()) {
if (!arguments.containsKey(required)) {
return ValidationResult.failure("缺少必需参数: " + required);
}
}
}
return ValidationResult.success();
} catch (Exception e) {
log.error("验证函数调用参数失败: 工具={}", toolName, e);
return ValidationResult.failure("参数验证失败: " + e.getMessage());
}
}
// 私有辅助方法
/**
* 转换单个工具为 OpenAI Function 格式
*/
private Map<String, Object> convertSingleToolToOpenAIFunction(MCPToolDefinition tool) {
Map<String, Object> function = new LinkedHashMap<>();
function.put("name", tool.getName());
function.put("description", tool.getDescription());
if (tool.getInputSchema() != null) {
function.put("parameters", convertSchemaToOpenAIFormat(tool.getInputSchema()));
}
return Map.of("type", "function", "function", function);
}
/**
* 转换单个工具为 Anthropic Tool 格式
*/
private Map<String, Object> convertSingleToolToAnthropicTool(MCPToolDefinition tool) {
Map<String, Object> anthropicTool = new LinkedHashMap<>();
anthropicTool.put("name", tool.getName());
anthropicTool.put("description", tool.getDescription());
if (tool.getInputSchema() != null) {
anthropicTool.put("input_schema", convertSchemaToAnthropicFormat(tool.getInputSchema()));
}
return anthropicTool;
}
/**
* 转换单个工具为通用函数格式
*/
private Map<String, Object> convertSingleToolToGenericFunction(MCPToolDefinition tool) {
Map<String, Object> genericTool = new LinkedHashMap<>();
genericTool.put("name", tool.getName());
genericTool.put("description", tool.getDescription());
genericTool.put("type", tool.getType());
if (tool.getInputSchema() != null) {
genericTool.put("input_schema", objectMapper.convertValue(tool.getInputSchema(), Map.class));
}
// 添加额外的元数据
if (tool.getTags() != null) {
genericTool.put("tags", tool.getTags());
}
if (tool.getReadOnlyHint() != null) {
genericTool.put("read_only", tool.getReadOnlyHint());
}
if (tool.getDestructiveHint() != null) {
genericTool.put("destructive", tool.getDestructiveHint());
}
return genericTool;
}
/**
* 转换 Schema OpenAI 格式
*/
private Map<String, Object> convertSchemaToOpenAIFormat(MCPToolDefinition.JsonSchema schema) {
Map<String, Object> openaiSchema = new LinkedHashMap<>();
openaiSchema.put("type", schema.getType());
if (schema.getProperties() != null) {
openaiSchema.put("properties", objectMapper.convertValue(schema.getProperties(), Map.class));
}
if (schema.getRequired() != null) {
openaiSchema.put("required", schema.getRequired());
}
if (schema.getAdditionalProperties() != null) {
openaiSchema.put("additionalProperties", schema.getAdditionalProperties());
}
return openaiSchema;
}
/**
* 转换 Schema Anthropic 格式
*/
private Map<String, Object> convertSchemaToAnthropicFormat(MCPToolDefinition.JsonSchema schema) {
// Anthropic 格式与标准 JSON Schema 基本相同
return convertSchemaToOpenAIFormat(schema);
}
/**
* MCP 响应中提取内容
*/
private Object extractContent(MCPToolCallResponse mcpResponse) {
if (mcpResponse.getContent() == null || mcpResponse.getContent().isEmpty()) {
return null;
}
// 如果只有一个内容项,直接返回其数据
if (mcpResponse.getContent().size() == 1) {
MCPToolCallResponse.MCPContent content = mcpResponse.getContent().get(0);
if ("text".equals(content.getType())) {
return content.getText();
} else if ("data".equals(content.getType())) {
return content.getData();
}
}
// 多个内容项,返回完整列表
return mcpResponse.getContent();
}
/**
* MCP 响应中提取错误消息
*/
private String extractErrorMessage(MCPToolCallResponse mcpResponse) {
if (mcpResponse.getContent() != null && !mcpResponse.getContent().isEmpty()) {
MCPToolCallResponse.MCPContent content = mcpResponse.getContent().get(0);
if ("text".equals(content.getType())) {
return content.getText();
}
}
return "未知错误";
}
/**
* 验证结果类
*/
public static class ValidationResult {
private final boolean valid;
private final String message;
private ValidationResult(boolean valid, String message) {
this.valid = valid;
this.message = message;
}
public static ValidationResult success() {
return new ValidationResult(true, null);
}
public static ValidationResult failure(String message) {
return new ValidationResult(false, message);
}
public boolean isValid() {
return valid;
}
public String getMessage() {
return message;
}
}
}

View File

@ -0,0 +1,305 @@
package com.chinaweal.youfool.devops.ai.mcp;
import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolDefinition;
import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolDefinition.JsonSchema;
import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolDefinition.PropertySchema;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.*;
/**
* MCP 工具注册表
*
* 管理所有可用的 MCP 工具定义提供符合 MCP 规范的工具描述
*
* @author AI开发团队
* @since 1.0.0
*/
@Slf4j
@Component
public class MCPToolRegistry {
private final Map<String, MCPToolDefinition> tools = new LinkedHashMap<>();
/**
* 构造函数初始化所有工具定义
*/
public MCPToolRegistry() {
initializeTools();
}
/**
* 初始化工具定义
*/
private void initializeTools() {
// 注册所有工具
registerRepairQueryTool();
registerRepairFeedbackQueryTool();
registerSimilaritySearchTool();
registerKnowledgeQueryTool();
log.info("MCP工具注册表初始化完成共注册 {} 个工具", tools.size());
}
/**
* 注册工单查询工具
*/
private void registerRepairQueryTool() {
MCPToolDefinition tool = new MCPToolDefinition();
tool.setName("repair_query");
tool.setDescription("根据工单ID查询工单详细信息包括标题、故障描述、业务类型、问题类型、优先级等");
tool.setType("function");
tool.setVersion("1.0.0");
tool.setAuthor("运维系统AI团队");
tool.setReadOnlyHint(true);
tool.setDestructiveHint(false);
tool.setTags(List.of("repair", "query", "readonly"));
// 创建输入 Schema
JsonSchema inputSchema = new JsonSchema();
inputSchema.setType("object");
inputSchema.setTitle("工单查询参数");
inputSchema.setDescription("查询工单详情所需的参数");
Map<String, PropertySchema> properties = new HashMap<>();
properties.put("repairId", PropertySchema.string("工单ID用于唯一标识一个工单")
.withLength(1, 50));
inputSchema.setProperties(properties);
inputSchema.setRequired(List.of("repairId"));
inputSchema.setAdditionalProperties(false);
tool.setInputSchema(inputSchema);
// 设置性能提示
MCPToolDefinition.ExecutionTimeHint executionTime = new MCPToolDefinition.ExecutionTimeHint();
executionTime.setMin(100L);
executionTime.setMax(3000L);
executionTime.setAverage(800L);
tool.setExpectedExecutionTime(executionTime);
// 设置资源需求
MCPToolDefinition.ResourceRequirements resources = new MCPToolDefinition.ResourceRequirements();
resources.setCpu("low");
resources.setMemory("low");
resources.setNetwork("low");
resources.setDatabase("medium");
tool.setResourceRequirements(resources);
tools.put(tool.getName(), tool);
}
/**
* 注册工单反馈查询工具
*/
private void registerRepairFeedbackQueryTool() {
MCPToolDefinition tool = new MCPToolDefinition();
tool.setName("repair_feedback_query");
tool.setDescription("查询工单的feedback步骤处理结果获取解决方案和处理记录");
tool.setType("function");
tool.setVersion("1.0.0");
tool.setAuthor("运维系统AI团队");
tool.setReadOnlyHint(true);
tool.setDestructiveHint(false);
tool.setTags(List.of("repair", "feedback", "readonly"));
// 创建输入 Schema
JsonSchema inputSchema = new JsonSchema();
inputSchema.setType("object");
inputSchema.setTitle("工单反馈查询参数");
inputSchema.setDescription("查询工单反馈记录所需的参数");
Map<String, PropertySchema> properties = new HashMap<>();
properties.put("repairId", PropertySchema.string("工单ID用于查询相关的反馈记录")
.withLength(1, 50));
inputSchema.setProperties(properties);
inputSchema.setRequired(List.of("repairId"));
inputSchema.setAdditionalProperties(false);
tool.setInputSchema(inputSchema);
// 设置性能提示
MCPToolDefinition.ExecutionTimeHint executionTime = new MCPToolDefinition.ExecutionTimeHint();
executionTime.setMin(200L);
executionTime.setMax(5000L);
executionTime.setAverage(1200L);
tool.setExpectedExecutionTime(executionTime);
// 设置资源需求
MCPToolDefinition.ResourceRequirements resources = new MCPToolDefinition.ResourceRequirements();
resources.setCpu("low");
resources.setMemory("low");
resources.setNetwork("low");
resources.setDatabase("medium");
tool.setResourceRequirements(resources);
tools.put(tool.getName(), tool);
}
/**
* 注册相似度搜索工具
*/
private void registerSimilaritySearchTool() {
MCPToolDefinition tool = new MCPToolDefinition();
tool.setName("similarity_search");
tool.setDescription("基于文本内容进行向量相似度检索,查找与查询文本相似的历史问题和解决方案");
tool.setType("function");
tool.setVersion("1.0.0");
tool.setAuthor("运维系统AI团队");
tool.setReadOnlyHint(true);
tool.setDestructiveHint(false);
tool.setTags(List.of("search", "similarity", "vector", "ai"));
// 创建输入 Schema
JsonSchema inputSchema = new JsonSchema();
inputSchema.setType("object");
inputSchema.setTitle("相似度搜索参数");
inputSchema.setDescription("执行向量相似度搜索所需的参数");
Map<String, PropertySchema> properties = new HashMap<>();
properties.put("queryText", PropertySchema.string("查询文本,用于向量化并进行相似度匹配")
.withLength(1, 1000));
properties.put("topK", PropertySchema.integer("返回结果数量限制返回最相似的K个结果")
.withDefault(5)
.withRange(1, 20));
properties.put("threshold", PropertySchema.number("相似度阈值,只返回相似度大于此值的结果")
.withDefault(0.3)
.withRange(0.0, 1.0));
inputSchema.setProperties(properties);
inputSchema.setRequired(List.of("queryText"));
inputSchema.setAdditionalProperties(false);
tool.setInputSchema(inputSchema);
// 设置性能提示
MCPToolDefinition.ExecutionTimeHint executionTime = new MCPToolDefinition.ExecutionTimeHint();
executionTime.setMin(1000L);
executionTime.setMax(15000L);
executionTime.setAverage(3500L);
tool.setExpectedExecutionTime(executionTime);
// 设置资源需求
MCPToolDefinition.ResourceRequirements resources = new MCPToolDefinition.ResourceRequirements();
resources.setCpu("high");
resources.setMemory("high");
resources.setNetwork("medium");
resources.setDatabase("high");
tool.setResourceRequirements(resources);
tools.put(tool.getName(), tool);
}
/**
* 注册知识库查询工具
*/
private void registerKnowledgeQueryTool() {
MCPToolDefinition tool = new MCPToolDefinition();
tool.setName("knowledge_query");
tool.setDescription("在知识库中精确匹配记录支持根据知识库ID或源工单ID进行查询");
tool.setType("function");
tool.setVersion("1.0.0");
tool.setAuthor("运维系统AI团队");
tool.setReadOnlyHint(true);
tool.setDestructiveHint(false);
tool.setTags(List.of("knowledge", "query", "readonly"));
// 创建输入 Schema
JsonSchema inputSchema = new JsonSchema();
inputSchema.setType("object");
inputSchema.setTitle("知识库查询参数");
inputSchema.setDescription("在知识库中查询记录所需的参数,至少需要提供一个查询条件");
Map<String, PropertySchema> properties = new HashMap<>();
properties.put("kbId", PropertySchema.string("知识库记录ID用于精确查找特定记录")
.withLength(1, 50));
properties.put("sourceRepairId", PropertySchema.string("源工单ID用于查找基于此工单创建的知识库记录")
.withLength(1, 50));
inputSchema.setProperties(properties);
inputSchema.setRequired(List.of()); // 至少需要一个参数,但不是所有都必需
inputSchema.setAdditionalProperties(false);
tool.setInputSchema(inputSchema);
// 设置性能提示
MCPToolDefinition.ExecutionTimeHint executionTime = new MCPToolDefinition.ExecutionTimeHint();
executionTime.setMin(150L);
executionTime.setMax(4000L);
executionTime.setAverage(1000L);
tool.setExpectedExecutionTime(executionTime);
// 设置资源需求
MCPToolDefinition.ResourceRequirements resources = new MCPToolDefinition.ResourceRequirements();
resources.setCpu("low");
resources.setMemory("medium");
resources.setNetwork("low");
resources.setDatabase("medium");
tool.setResourceRequirements(resources);
tools.put(tool.getName(), tool);
}
/**
* 获取所有工具定义
*/
public List<MCPToolDefinition> getAllTools() {
return new ArrayList<>(tools.values());
}
/**
* 根据名称获取工具定义
*/
public Optional<MCPToolDefinition> getTool(String name) {
return Optional.ofNullable(tools.get(name));
}
/**
* 根据类型过滤工具
*/
public List<MCPToolDefinition> getToolsByType(String type) {
return tools.values().stream()
.filter(tool -> type.equals(tool.getType()))
.toList();
}
/**
* 根据标签过滤工具
*/
public List<MCPToolDefinition> getToolsByTag(String tag) {
return tools.values().stream()
.filter(tool -> tool.getTags() != null && tool.getTags().contains(tag))
.toList();
}
/**
* 分页获取工具
*/
public List<MCPToolDefinition> getToolsPaginated(int offset, int limit) {
List<MCPToolDefinition> allTools = getAllTools();
int start = Math.min(offset, allTools.size());
int end = Math.min(start + limit, allTools.size());
return allTools.subList(start, end);
}
/**
* 验证工具名称是否存在
*/
public boolean haseTool(String name) {
return tools.containsKey(name);
}
/**
* 获取工具总数
*/
public int getToolCount() {
return tools.size();
}
}

View File

@ -0,0 +1,371 @@
package com.chinaweal.youfool.devops.ai.mcp;
import com.chinaweal.youfool.devops.ai.mcp.dto.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* 真正的 MCP (Model Context Protocol) 服务器实现
*
* 严格遵循 JSON-RPC 2.0 规范和 Anthropic MCP 协议标准
* 提供标准的 tools/list tools/call 端点
*
* @author AI开发团队
* @since 1.0.0
*/
@Slf4j
@RestController
@RequestMapping("/mcp")
@RequiredArgsConstructor
@Validated
@Tag(name = "真正的MCP服务器", description = "遵循Anthropic MCP协议标准的JSON-RPC 2.0实现")
@ConditionalOnProperty(prefix = "ai.mcp", name = "enabled", havingValue = "true", matchIfMissing = true)
public class TrueMCPServer {
private final MCPToolRegistry toolRegistry;
private final MCPServer mcpServer; // 复用现有的执行逻辑
private final ObjectMapper objectMapper;
/**
* tools/list - 获取可用工具列表
*
* 严格遵循 MCP 协议的 tools/list 方法规范
*/
@PostMapping("/tools/list")
@Operation(summary = "获取可用工具列表", description = "严格遵循MCP协议的tools/list方法返回所有可用的工具定义")
public MCPJsonRpcResponse toolsList(@Valid @RequestBody MCPJsonRpcRequest request) {
LocalDateTime startTime = LocalDateTime.now();
try {
log.info("处理MCP tools/list请求: id={}, method={}", request.getIdAsString(), request.getMethod());
// 验证请求格式
if (!request.isValidJsonRpcRequest()) {
log.warn("无效的JSON-RPC请求格式: jsonrpc={}, method={}", request.getJsonrpc(), request.getMethod());
return MCPJsonRpcResponse.error(request.getId(), MCPError.invalidRequest("无效的JSON-RPC请求格式"));
}
// 验证方法名
if (!"tools/list".equals(request.getMethod())) {
log.warn("不支持的方法: {}", request.getMethod());
return MCPJsonRpcResponse.error(request.getId(), MCPError.methodNotFound(request.getMethod()));
}
// 解析参数
MCPToolListRequest listRequest = parseToolListParams(request.getParams());
if (listRequest != null && !listRequest.isValid()) {
log.warn("无效的tools/list参数: {}", request.getParams());
return MCPJsonRpcResponse.error(request.getId(), MCPError.invalidParams("无效的tools/list参数"));
}
// 获取工具列表
List<MCPToolDefinition> tools;
int totalCount;
if (listRequest == null) {
// 返回所有工具
tools = toolRegistry.getAllTools();
totalCount = tools.size();
} else {
// 分页或过滤
if (listRequest.getType() != null) {
tools = toolRegistry.getToolsByType(listRequest.getType());
} else {
tools = toolRegistry.getToolsPaginated(
listRequest.getEffectiveOffset(),
listRequest.getEffectiveLimit()
);
}
totalCount = toolRegistry.getToolCount();
}
// 构建响应
MCPToolListResponse response;
if (listRequest != null && (listRequest.getLimit() != null || listRequest.getOffset() != null)) {
response = MCPToolListResponse.of(tools, totalCount,
listRequest.getEffectiveOffset(), listRequest.getEffectiveLimit());
} else {
response = MCPToolListResponse.of(tools);
}
long executionTime = java.time.Duration.between(startTime, LocalDateTime.now()).toMillis();
log.info("MCP tools/list执行成功: 返回{}个工具, 执行时间{}ms", tools.size(), executionTime);
return MCPJsonRpcResponse.success(request.getId(), response);
} catch (Exception e) {
long executionTime = java.time.Duration.between(startTime, LocalDateTime.now()).toMillis();
log.error("MCP tools/list执行异常: id={}, 执行时间{}ms", request.getIdAsString(), executionTime, e);
return MCPJsonRpcResponse.error(request.getId(), MCPError.internalError("工具列表获取失败: " + e.getMessage()));
}
}
/**
* tools/call - 执行工具调用
*
* 严格遵循 MCP 协议的 tools/call 方法规范
*/
@PostMapping("/tools/call")
@Operation(summary = "执行工具调用", description = "严格遵循MCP协议的tools/call方法执行指定的工具")
public MCPJsonRpcResponse toolsCall(@Valid @RequestBody MCPJsonRpcRequest request) {
LocalDateTime startTime = LocalDateTime.now();
try {
log.info("处理MCP tools/call请求: id={}, method={}", request.getIdAsString(), request.getMethod());
// 验证请求格式
if (!request.isValidJsonRpcRequest()) {
log.warn("无效的JSON-RPC请求格式: jsonrpc={}, method={}", request.getJsonrpc(), request.getMethod());
return MCPJsonRpcResponse.error(request.getId(), MCPError.invalidRequest("无效的JSON-RPC请求格式"));
}
// 验证方法名
if (!"tools/call".equals(request.getMethod())) {
log.warn("不支持的方法: {}", request.getMethod());
return MCPJsonRpcResponse.error(request.getId(), MCPError.methodNotFound(request.getMethod()));
}
// 解析参数
MCPToolCallRequest callRequest = parseToolCallParams(request.getParams());
if (callRequest == null || !callRequest.isValid()) {
log.warn("无效的tools/call参数: {}", request.getParams());
return MCPJsonRpcResponse.error(request.getId(), MCPError.invalidParams("无效的tools/call参数"));
}
// 验证工具是否存在
String toolName = callRequest.getCleanedName();
if (!toolRegistry.haseTool(toolName)) {
log.warn("工具不存在: {}", toolName);
return MCPJsonRpcResponse.error(request.getId(), MCPError.toolNotFound(toolName));
}
// 获取工具定义进行参数验证(可选)
Optional<MCPToolDefinition> toolDef = toolRegistry.getTool(toolName);
if (toolDef.isEmpty()) {
log.error("工具定义不存在: {}", toolName);
return MCPJsonRpcResponse.error(request.getId(), MCPError.internalError("工具定义不存在"));
}
// 执行工具调用委托给现有的MCPServer
MCPResponse legacyResponse = mcpServer.executeTool(toolName, callRequest.getArguments());
// 转换响应格式
MCPToolCallResponse mcpResponse = convertToMCPResponse(legacyResponse, callRequest.getCallId(), startTime);
long executionTime = java.time.Duration.between(startTime, LocalDateTime.now()).toMillis();
if (legacyResponse.getSuccess()) {
log.info("MCP tools/call执行成功: 工具={}, 执行时间={}ms", toolName, executionTime);
return MCPJsonRpcResponse.success(request.getId(), mcpResponse);
} else {
log.warn("MCP tools/call执行失败: 工具={}, 错误={}, 执行时间={}ms",
toolName, legacyResponse.getError(), executionTime);
return MCPJsonRpcResponse.error(request.getId(),
MCPError.toolExecutionError(legacyResponse.getError()));
}
} catch (Exception e) {
long executionTime = java.time.Duration.between(startTime, LocalDateTime.now()).toMillis();
log.error("MCP tools/call执行异常: id={}, 执行时间={}ms", request.getIdAsString(), executionTime, e);
return MCPJsonRpcResponse.error(request.getId(), MCPError.internalError("工具调用失败: " + e.getMessage()));
}
}
/**
* 健康检查端点 - 非标准MCP方法用于系统监控
*/
@GetMapping("/health")
@Operation(summary = "健康检查", description = "检查MCP服务器的健康状态非标准MCP方法")
public Map<String, Object> health() {
try {
return Map.of(
"status", "UP",
"protocol", "MCP",
"jsonrpc_version", "2.0",
"available_tools", toolRegistry.getToolCount(),
"server_time", LocalDateTime.now(),
"endpoints", List.of("/mcp/tools/list", "/mcp/tools/call")
);
} catch (Exception e) {
log.error("健康检查失败", e);
return Map.of(
"status", "DOWN",
"error", e.getMessage(),
"server_time", LocalDateTime.now()
);
}
}
/**
* 获取MCP协议信息 - 非标准方法用于调试
*/
@GetMapping("/protocol-info")
@Operation(summary = "获取协议信息", description = "获取MCP协议实现信息非标准MCP方法")
public Map<String, Object> getProtocolInfo() {
return Map.of(
"protocol_name", "Model Context Protocol",
"protocol_version", "1.0",
"jsonrpc_version", "2.0",
"implementation", "youfool-devops-mcp-server",
"supported_methods", List.of("tools/list", "tools/call"),
"tool_count", toolRegistry.getToolCount(),
"server_capabilities", Map.of(
"tools", true,
"resources", false,
"prompts", false,
"completion", false
)
);
}
/**
* 批量调用端点 - 扩展功能支持批量工具调用
*/
@PostMapping("/tools/batch-call")
@Operation(summary = "批量工具调用", description = "批量执行多个工具调用(扩展功能)")
public MCPJsonRpcResponse batchToolsCall(@Valid @RequestBody MCPJsonRpcRequest request) {
LocalDateTime startTime = LocalDateTime.now();
try {
log.info("处理MCP批量工具调用请求: id={}", request.getIdAsString());
// 验证请求格式
if (!request.isValidJsonRpcRequest()) {
return MCPJsonRpcResponse.error(request.getId(), MCPError.invalidRequest("无效的JSON-RPC请求格式"));
}
// 解析批量调用参数
if (!(request.getParams() instanceof List)) {
return MCPJsonRpcResponse.error(request.getId(), MCPError.invalidParams("批量调用需要数组参数"));
}
@SuppressWarnings("unchecked")
List<Map<String, Object>> batchParams = (List<Map<String, Object>>) request.getParams();
if (batchParams.isEmpty() || batchParams.size() > 10) {
return MCPJsonRpcResponse.error(request.getId(),
MCPError.invalidParams("批量调用数量必须在1-10之间"));
}
// 执行批量调用
List<MCPToolCallResponse> results = batchParams.stream()
.map(this::executeSingleBatchCall)
.toList();
long executionTime = java.time.Duration.between(startTime, LocalDateTime.now()).toMillis();
log.info("MCP批量工具调用完成: 调用数量={}, 执行时间={}ms", results.size(), executionTime);
return MCPJsonRpcResponse.success(request.getId(), Map.of(
"results", results,
"total_calls", results.size(),
"execution_time_ms", executionTime
));
} catch (Exception e) {
long executionTime = java.time.Duration.between(startTime, LocalDateTime.now()).toMillis();
log.error("MCP批量工具调用异常: 执行时间={}ms", executionTime, e);
return MCPJsonRpcResponse.error(request.getId(), MCPError.internalError("批量调用失败: " + e.getMessage()));
}
}
/**
* 解析 tools/list 参数
*/
private MCPToolListRequest parseToolListParams(Object params) {
if (params == null) {
return null;
}
try {
return objectMapper.convertValue(params, MCPToolListRequest.class);
} catch (Exception e) {
log.warn("解析tools/list参数失败: {}", e.getMessage());
return null;
}
}
/**
* 解析 tools/call 参数
*/
private MCPToolCallRequest parseToolCallParams(Object params) {
if (params == null) {
return null;
}
try {
return objectMapper.convertValue(params, MCPToolCallRequest.class);
} catch (Exception e) {
log.warn("解析tools/call参数失败: {}", e.getMessage());
return null;
}
}
/**
* 转换传统响应为MCP响应格式
*/
private MCPToolCallResponse convertToMCPResponse(MCPResponse legacyResponse, String callId, LocalDateTime startTime) {
LocalDateTime endTime = LocalDateTime.now();
long executionTime = java.time.Duration.between(startTime, endTime).toMillis();
if (legacyResponse.getSuccess()) {
MCPToolCallResponse response = MCPToolCallResponse.success(legacyResponse.getData());
response.setCallId(callId);
response.setExecutionTime(executionTime);
response.setStartTime(startTime);
response.setEndTime(endTime);
// 添加性能指标
if (legacyResponse.getPerformanceMetrics() != null) {
response.setMetrics(legacyResponse.getPerformanceMetrics());
}
return response;
} else {
MCPToolCallResponse response = MCPToolCallResponse.error(legacyResponse.getError(), callId);
response.setExecutionTime(executionTime);
response.setStartTime(startTime);
response.setEndTime(endTime);
return response;
}
}
/**
* 执行单个批量调用
*/
private MCPToolCallResponse executeSingleBatchCall(Map<String, Object> params) {
try {
MCPToolCallRequest callRequest = objectMapper.convertValue(params, MCPToolCallRequest.class);
if (callRequest == null || !callRequest.isValid()) {
return MCPToolCallResponse.error("无效的调用参数");
}
String toolName = callRequest.getCleanedName();
if (!toolRegistry.haseTool(toolName)) {
return MCPToolCallResponse.error("工具不存在: " + toolName);
}
LocalDateTime startTime = LocalDateTime.now();
MCPResponse legacyResponse = mcpServer.executeTool(toolName, callRequest.getArguments());
return convertToMCPResponse(legacyResponse, callRequest.getCallId(), startTime);
} catch (Exception e) {
log.error("批量调用中的单个调用失败", e);
return MCPToolCallResponse.error("调用失败: " + e.getMessage());
}
}
}

View File

@ -0,0 +1,189 @@
package com.chinaweal.youfool.devops.ai.mcp.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
/**
* MCP JSON-RPC 错误对象
*
* 遵循 JSON-RPC 2.0 规范的错误格式
*
* @author AI开发团队
* @since 1.0.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "MCP JSON-RPC 错误对象")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MCPError {
/**
* 错误代码
*
* JSON-RPC 预定义错误代码
* -32700: Parse error (解析错误)
* -32600: Invalid Request (无效请求)
* -32601: Method not found (方法未找到)
* -32602: Invalid params (参数无效)
* -32603: Internal error (内部错误)
* -32000 to -32099: Server error (服务器错误保留给实现定义)
*/
@JsonProperty("code")
@Schema(description = "错误代码", example = "-32601", required = true)
private int code;
/**
* 错误消息
*/
@JsonProperty("message")
@Schema(description = "错误消息", example = "Method not found", required = true)
private String message;
/**
* 错误数据可选
* 包含关于错误的额外信息
*/
@JsonProperty("data")
@Schema(description = "错误数据,包含关于错误的额外信息")
private Object data;
/**
* 创建基本错误
*/
public MCPError(int code, String message) {
this.code = code;
this.message = message;
}
// JSON-RPC 2.0 预定义错误代码常量
public static final int PARSE_ERROR = -32700;
public static final int INVALID_REQUEST = -32600;
public static final int METHOD_NOT_FOUND = -32601;
public static final int INVALID_PARAMS = -32602;
public static final int INTERNAL_ERROR = -32603;
// MCP 特定错误代码 (使用 -32000 到 -32099 范围)
public static final int TOOL_NOT_FOUND = -32001;
public static final int TOOL_EXECUTION_ERROR = -32002;
public static final int VALIDATION_ERROR = -32003;
public static final int SECURITY_ERROR = -32004;
public static final int RATE_LIMIT_ERROR = -32005;
public static final int TIMEOUT_ERROR = -32006;
/**
* 创建解析错误
*/
public static MCPError parseError() {
return new MCPError(PARSE_ERROR, "Parse error");
}
/**
* 创建解析错误带消息
*/
public static MCPError parseError(String message) {
return new MCPError(PARSE_ERROR, message);
}
/**
* 创建无效请求错误
*/
public static MCPError invalidRequest() {
return new MCPError(INVALID_REQUEST, "Invalid Request");
}
/**
* 创建无效请求错误带消息
*/
public static MCPError invalidRequest(String message) {
return new MCPError(INVALID_REQUEST, message);
}
/**
* 创建方法未找到错误
*/
public static MCPError methodNotFound() {
return new MCPError(METHOD_NOT_FOUND, "Method not found");
}
/**
* 创建方法未找到错误带消息
*/
public static MCPError methodNotFound(String method) {
return new MCPError(METHOD_NOT_FOUND, "Method not found: " + method);
}
/**
* 创建参数无效错误
*/
public static MCPError invalidParams() {
return new MCPError(INVALID_PARAMS, "Invalid params");
}
/**
* 创建参数无效错误带消息
*/
public static MCPError invalidParams(String message) {
return new MCPError(INVALID_PARAMS, message);
}
/**
* 创建内部错误
*/
public static MCPError internalError() {
return new MCPError(INTERNAL_ERROR, "Internal error");
}
/**
* 创建内部错误带消息
*/
public static MCPError internalError(String message) {
return new MCPError(INTERNAL_ERROR, message);
}
/**
* 创建工具未找到错误
*/
public static MCPError toolNotFound(String toolName) {
return new MCPError(TOOL_NOT_FOUND, "Tool not found: " + toolName);
}
/**
* 创建工具执行错误
*/
public static MCPError toolExecutionError(String message) {
return new MCPError(TOOL_EXECUTION_ERROR, "Tool execution error: " + message);
}
/**
* 创建验证错误
*/
public static MCPError validationError(String message) {
return new MCPError(VALIDATION_ERROR, "Validation error: " + message);
}
/**
* 创建安全错误
*/
public static MCPError securityError(String message) {
return new MCPError(SECURITY_ERROR, "Security error: " + message);
}
/**
* 创建速率限制错误
*/
public static MCPError rateLimitError() {
return new MCPError(RATE_LIMIT_ERROR, "Rate limit exceeded");
}
/**
* 创建超时错误
*/
public static MCPError timeoutError() {
return new MCPError(TIMEOUT_ERROR, "Request timeout");
}
}

View File

@ -0,0 +1,85 @@
package com.chinaweal.youfool.devops.ai.mcp.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* MCP JSON-RPC 请求对象
*
* 遵循 JSON-RPC 2.0 规范用于 MCP 协议通信
*
* @author AI开发团队
* @since 1.0.0
*/
@Data
@Schema(description = "MCP JSON-RPC 请求对象")
public class MCPJsonRpcRequest {
/**
* JSON-RPC 版本必须为 "2.0"
*/
@JsonProperty("jsonrpc")
@NotBlank(message = "JSON-RPC版本不能为空")
@Schema(description = "JSON-RPC版本", example = "2.0", required = true)
private String jsonrpc = "2.0";
/**
* 请求ID用于标识请求和响应的对应关系
* 可以是字符串数字或null
*/
@JsonProperty("id")
@Schema(description = "请求ID用于标识请求", example = "1")
private Object id;
/**
* 调用的方法名
* MCP协议支持的方法tools/list, tools/call
*/
@JsonProperty("method")
@NotBlank(message = "方法名不能为空")
@Schema(description = "调用的方法名", example = "tools/list", required = true)
private String method;
/**
* 方法参数可以是对象或数组
*/
@JsonProperty("params")
@Schema(description = "方法参数")
private Object params;
/**
* 验证 JSON-RPC 请求的基本格式
*/
public boolean isValidJsonRpcRequest() {
return "2.0".equals(jsonrpc) && method != null && !method.trim().isEmpty();
}
/**
* 判断是否为通知请求无需响应的请求
* 通知请求的 id null
*/
public boolean isNotification() {
return id == null;
}
/**
* 获取字符串形式的ID
*/
public String getIdAsString() {
if (id == null) {
return null;
}
return id.toString();
}
/**
* 设置ID通用方法
*/
public void setId(Object id) {
this.id = id;
}
}

View File

@ -0,0 +1,128 @@
package com.chinaweal.youfool.devops.ai.mcp.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* MCP JSON-RPC 响应对象
*
* 遵循 JSON-RPC 2.0 规范用于 MCP 协议通信
*
* @author AI开发团队
* @since 1.0.0
*/
@Data
@Schema(description = "MCP JSON-RPC 响应对象")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MCPJsonRpcResponse {
/**
* JSON-RPC 版本必须为 "2.0"
*/
@JsonProperty("jsonrpc")
@Schema(description = "JSON-RPC版本", example = "2.0", required = true)
private String jsonrpc = "2.0";
/**
* 请求ID与请求中的ID相同
* 通知请求的响应不包含此字段
*/
@JsonProperty("id")
@Schema(description = "请求ID与请求中的ID相同")
private Object id;
/**
* 成功响应的结果数据
* error 字段互斥只能存在其中一个
*/
@JsonProperty("result")
@Schema(description = "成功响应的结果数据")
private Object result;
/**
* 错误响应的错误信息
* result 字段互斥只能存在其中一个
*/
@JsonProperty("error")
@Schema(description = "错误响应的错误信息")
private MCPError error;
/**
* 创建成功响应
*
* @param id 请求ID
* @param result 结果数据
* @return 成功响应对象
*/
public static MCPJsonRpcResponse success(Object id, Object result) {
MCPJsonRpcResponse response = new MCPJsonRpcResponse();
response.setId(id);
response.setResult(result);
return response;
}
/**
* 创建错误响应
*
* @param id 请求ID
* @param error 错误信息
* @return 错误响应对象
*/
public static MCPJsonRpcResponse error(Object id, MCPError error) {
MCPJsonRpcResponse response = new MCPJsonRpcResponse();
response.setId(id);
response.setError(error);
return response;
}
/**
* 创建错误响应简化版本
*
* @param id 请求ID
* @param code 错误代码
* @param message 错误消息
* @return 错误响应对象
*/
public static MCPJsonRpcResponse error(Object id, int code, String message) {
return error(id, new MCPError(code, message));
}
/**
* 创建错误响应带数据
*
* @param id 请求ID
* @param code 错误代码
* @param message 错误消息
* @param data 错误数据
* @return 错误响应对象
*/
public static MCPJsonRpcResponse error(Object id, int code, String message, Object data) {
return error(id, new MCPError(code, message, data));
}
/**
* 判断是否为成功响应
*/
public boolean isSuccess() {
return error == null && result != null;
}
/**
* 判断是否为错误响应
*/
public boolean isError() {
return error != null;
}
/**
* 获取字符串形式的ID
*/
public String getIdAsString() {
if (id == null) {
return null;
}
return id.toString();
}
}

View File

@ -0,0 +1,106 @@
package com.chinaweal.youfool.devops.ai.mcp.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.util.Map;
/**
* MCP tools/call 请求参数
*
* 用于 tools/call 方法的参数定义
*
* @author AI开发团队
* @since 1.0.0
*/
@Data
@Schema(description = "MCP tools/call 请求参数")
public class MCPToolCallRequest {
/**
* 要调用的工具名称
*/
@JsonProperty("name")
@NotBlank(message = "工具名称不能为空")
@Schema(description = "要调用的工具名称", example = "similarity_search", required = true)
private String name;
/**
* 工具调用参数
*/
@JsonProperty("arguments")
@Schema(description = "工具调用参数", required = true)
private Map<String, Object> arguments;
/**
* 可选的调用ID用于追踪
*/
@JsonProperty("callId")
@Schema(description = "可选的调用ID用于追踪")
private String callId;
/**
* 可选的超时时间毫秒
*/
@JsonProperty("timeout")
@Schema(description = "可选的超时时间(毫秒)", example = "30000")
private Long timeout;
/**
* 可选的优先级
*/
@JsonProperty("priority")
@Schema(description = "可选的优先级", example = "normal")
private String priority;
/**
* 验证请求参数
*/
public boolean isValid() {
// 工具名称不能为空
if (name == null || name.trim().isEmpty()) {
return false;
}
// arguments 不能为 null
if (arguments == null) {
return false;
}
// 超时时间必须大于 0
if (timeout != null && timeout <= 0) {
return false;
}
return true;
}
/**
* 获取有效的超时时间
*/
public long getEffectiveTimeout() {
if (timeout == null || timeout <= 0) {
return 30000L; // 默认30秒
}
return Math.min(timeout, 300000L); // 最大5分钟
}
/**
* 获取清理后的工具名称
*/
public String getCleanedName() {
if (name == null) {
return null;
}
return name.trim();
}
/**
* 判断是否为高优先级调用
*/
public boolean isHighPriority() {
return "high".equalsIgnoreCase(priority) || "urgent".equalsIgnoreCase(priority);
}
}

View File

@ -0,0 +1,227 @@
package com.chinaweal.youfool.devops.ai.mcp.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* MCP tools/call 响应对象
*
* 用于 tools/call 方法的响应定义
*
* @author AI开发团队
* @since 1.0.0
*/
@Data
@Schema(description = "MCP tools/call 响应对象")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MCPToolCallResponse {
/**
* 工具调用的结果内容
*/
@JsonProperty("content")
@Schema(description = "工具调用的结果内容", required = true)
private List<MCPContent> content;
/**
* 是否有错误标志
*/
@JsonProperty("isError")
@Schema(description = "是否有错误标志")
private Boolean isError;
/**
* 调用ID如果请求中提供了
*/
@JsonProperty("callId")
@Schema(description = "调用ID")
private String callId;
/**
* 执行时间毫秒
*/
@JsonProperty("executionTime")
@Schema(description = "执行时间(毫秒)")
private Long executionTime;
/**
* 开始时间
*/
@JsonProperty("startTime")
@Schema(description = "开始时间")
private LocalDateTime startTime;
/**
* 结束时间
*/
@JsonProperty("endTime")
@Schema(description = "结束时间")
private LocalDateTime endTime;
/**
* 性能指标
*/
@JsonProperty("metrics")
@Schema(description = "性能指标")
private Map<String, Object> metrics;
/**
* 工具版本信息
*/
@JsonProperty("toolVersion")
@Schema(description = "工具版本信息")
private String toolVersion;
/**
* MCP 内容对象
*/
@Data
@Schema(description = "MCP 内容对象")
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class MCPContent {
/**
* 内容类型通常为 "text"
*/
@JsonProperty("type")
@Schema(description = "内容类型", example = "text", required = true)
private String type;
/**
* 文本内容
*/
@JsonProperty("text")
@Schema(description = "文本内容")
private String text;
/**
* 资源URI如果是资源类型
*/
@JsonProperty("uri")
@Schema(description = "资源URI")
private String uri;
/**
* MIME类型如果是资源类型
*/
@JsonProperty("mimeType")
@Schema(description = "MIME类型")
private String mimeType;
/**
* 结构化数据如果是数据类型
*/
@JsonProperty("data")
@Schema(description = "结构化数据")
private Object data;
/**
* 创建文本内容
*/
public static MCPContent text(String text) {
MCPContent content = new MCPContent();
content.setType("text");
content.setText(text);
return content;
}
/**
* 创建数据内容
*/
public static MCPContent data(Object data) {
MCPContent content = new MCPContent();
content.setType("data");
content.setData(data);
return content;
}
/**
* 创建资源内容
*/
public static MCPContent resource(String uri, String mimeType) {
MCPContent content = new MCPContent();
content.setType("resource");
content.setUri(uri);
content.setMimeType(mimeType);
return content;
}
}
/**
* 创建成功响应
*/
public static MCPToolCallResponse success(Object data) {
MCPToolCallResponse response = new MCPToolCallResponse();
response.setContent(List.of(MCPContent.data(data)));
response.setIsError(false);
response.setEndTime(LocalDateTime.now());
return response;
}
/**
* 创建成功响应文本
*/
public static MCPToolCallResponse successText(String text) {
MCPToolCallResponse response = new MCPToolCallResponse();
response.setContent(List.of(MCPContent.text(text)));
response.setIsError(false);
response.setEndTime(LocalDateTime.now());
return response;
}
/**
* 创建成功响应带完整信息
*/
public static MCPToolCallResponse success(Object data, String callId, Long executionTime) {
MCPToolCallResponse response = success(data);
response.setCallId(callId);
response.setExecutionTime(executionTime);
return response;
}
/**
* 创建错误响应
*/
public static MCPToolCallResponse error(String errorMessage) {
MCPToolCallResponse response = new MCPToolCallResponse();
response.setContent(List.of(MCPContent.text(errorMessage)));
response.setIsError(true);
response.setEndTime(LocalDateTime.now());
return response;
}
/**
* 创建错误响应带调用ID
*/
public static MCPToolCallResponse error(String errorMessage, String callId) {
MCPToolCallResponse response = error(errorMessage);
response.setCallId(callId);
return response;
}
/**
* 设置性能指标
*/
public MCPToolCallResponse withMetrics(Map<String, Object> metrics) {
this.metrics = metrics;
return this;
}
/**
* 设置执行时间信息
*/
public MCPToolCallResponse withTiming(LocalDateTime startTime, LocalDateTime endTime) {
this.startTime = startTime;
this.endTime = endTime;
if (startTime != null && endTime != null) {
this.executionTime = java.time.Duration.between(startTime, endTime).toMillis();
}
return this;
}
}

View File

@ -0,0 +1,377 @@
package com.chinaweal.youfool.devops.ai.mcp.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Map;
import java.util.List;
/**
* MCP 工具定义对象
*
* 遵循 MCP 规范的工具定义格式包含完整的 JSON Schema
*
* @author AI开发团队
* @since 1.0.0
*/
@Data
@Schema(description = "MCP 工具定义对象")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MCPToolDefinition {
/**
* 工具名称必须唯一
*/
@JsonProperty("name")
@Schema(description = "工具名称,必须唯一", example = "similarity_search", required = true)
private String name;
/**
* 工具描述
*/
@JsonProperty("description")
@Schema(description = "工具描述", example = "基于文本内容进行向量相似度检索,查找类似问题", required = true)
private String description;
/**
* 输入参数的 JSON Schema
*/
@JsonProperty("inputSchema")
@Schema(description = "输入参数的 JSON Schema", required = true)
private JsonSchema inputSchema;
/**
* 工具类型默认为 "function"
*/
@JsonProperty("type")
@Schema(description = "工具类型", example = "function")
private String type = "function";
/**
* 工具标签
*/
@JsonProperty("tags")
@Schema(description = "工具标签")
private List<String> tags;
/**
* 只读提示
*/
@JsonProperty("readOnlyHint")
@Schema(description = "只读提示,表示工具不会修改系统状态")
private Boolean readOnlyHint;
/**
* 破坏性操作提示
*/
@JsonProperty("destructiveHint")
@Schema(description = "破坏性操作提示,表示工具可能修改或删除数据")
private Boolean destructiveHint;
/**
* 工具版本
*/
@JsonProperty("version")
@Schema(description = "工具版本", example = "1.0.0")
private String version;
/**
* 工具作者
*/
@JsonProperty("author")
@Schema(description = "工具作者")
private String author;
/**
* 工具文档URL
*/
@JsonProperty("documentationUrl")
@Schema(description = "工具文档URL")
private String documentationUrl;
/**
* 预期的执行时间范围毫秒
*/
@JsonProperty("expectedExecutionTime")
@Schema(description = "预期的执行时间范围(毫秒)")
private ExecutionTimeHint expectedExecutionTime;
/**
* 资源需求提示
*/
@JsonProperty("resourceRequirements")
@Schema(description = "资源需求提示")
private ResourceRequirements resourceRequirements;
/**
* JSON Schema 定义
*/
@Data
@Schema(description = "JSON Schema 定义")
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class JsonSchema {
/**
* Schema 类型通常为 "object"
*/
@JsonProperty("type")
@Schema(description = "Schema 类型", example = "object", required = true)
private String type;
/**
* 属性定义
*/
@JsonProperty("properties")
@Schema(description = "属性定义")
private Map<String, PropertySchema> properties;
/**
* 必需的属性列表
*/
@JsonProperty("required")
@Schema(description = "必需的属性列表")
private List<String> required;
/**
* 是否允许额外属性
*/
@JsonProperty("additionalProperties")
@Schema(description = "是否允许额外属性")
private Boolean additionalProperties;
/**
* Schema 标题
*/
@JsonProperty("title")
@Schema(description = "Schema 标题")
private String title;
/**
* Schema 描述
*/
@JsonProperty("description")
@Schema(description = "Schema 描述")
private String description;
}
/**
* 属性 Schema 定义
*/
@Data
@Schema(description = "属性 Schema 定义")
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class PropertySchema {
/**
* 属性类型
*/
@JsonProperty("type")
@Schema(description = "属性类型", example = "string")
private String type;
/**
* 属性描述
*/
@JsonProperty("description")
@Schema(description = "属性描述")
private String description;
/**
* 默认值
*/
@JsonProperty("default")
@Schema(description = "默认值")
private Object defaultValue;
/**
* 最小值数字类型
*/
@JsonProperty("minimum")
@Schema(description = "最小值")
private Number minimum;
/**
* 最大值数字类型
*/
@JsonProperty("maximum")
@Schema(description = "最大值")
private Number maximum;
/**
* 最小长度字符串类型
*/
@JsonProperty("minLength")
@Schema(description = "最小长度")
private Integer minLength;
/**
* 最大长度字符串类型
*/
@JsonProperty("maxLength")
@Schema(description = "最大长度")
private Integer maxLength;
/**
* 模式匹配字符串类型
*/
@JsonProperty("pattern")
@Schema(description = "模式匹配")
private String pattern;
/**
* 枚举值
*/
@JsonProperty("enum")
@Schema(description = "枚举值")
private List<Object> enumValues;
/**
* 示例值
*/
@JsonProperty("examples")
@Schema(description = "示例值")
private List<Object> examples;
/**
* 数组项目类型数组类型
*/
@JsonProperty("items")
@Schema(description = "数组项目类型")
private PropertySchema items;
/**
* 创建字符串属性
*/
public static PropertySchema string(String description) {
PropertySchema schema = new PropertySchema();
schema.setType("string");
schema.setDescription(description);
return schema;
}
/**
* 创建整数属性
*/
public static PropertySchema integer(String description) {
PropertySchema schema = new PropertySchema();
schema.setType("integer");
schema.setDescription(description);
return schema;
}
/**
* 创建数字属性
*/
public static PropertySchema number(String description) {
PropertySchema schema = new PropertySchema();
schema.setType("number");
schema.setDescription(description);
return schema;
}
/**
* 创建布尔属性
*/
public static PropertySchema bool(String description) {
PropertySchema schema = new PropertySchema();
schema.setType("boolean");
schema.setDescription(description);
return schema;
}
/**
* 设置默认值
*/
public PropertySchema withDefault(Object defaultValue) {
this.defaultValue = defaultValue;
return this;
}
/**
* 设置数值范围
*/
public PropertySchema withRange(Number min, Number max) {
this.minimum = min;
this.maximum = max;
return this;
}
/**
* 设置长度范围
*/
public PropertySchema withLength(Integer minLength, Integer maxLength) {
this.minLength = minLength;
this.maxLength = maxLength;
return this;
}
}
/**
* 执行时间提示
*/
@Data
@Schema(description = "执行时间提示")
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class ExecutionTimeHint {
/**
* 最小执行时间毫秒
*/
@JsonProperty("min")
@Schema(description = "最小执行时间(毫秒)")
private Long min;
/**
* 最大执行时间毫秒
*/
@JsonProperty("max")
@Schema(description = "最大执行时间(毫秒)")
private Long max;
/**
* 平均执行时间毫秒
*/
@JsonProperty("average")
@Schema(description = "平均执行时间(毫秒)")
private Long average;
}
/**
* 资源需求
*/
@Data
@Schema(description = "资源需求")
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class ResourceRequirements {
/**
* CPU 使用级别
*/
@JsonProperty("cpu")
@Schema(description = "CPU 使用级别", example = "low")
private String cpu;
/**
* 内存使用级别
*/
@JsonProperty("memory")
@Schema(description = "内存使用级别", example = "medium")
private String memory;
/**
* 网络使用级别
*/
@JsonProperty("network")
@Schema(description = "网络使用级别", example = "low")
private String network;
/**
* 数据库使用级别
*/
@JsonProperty("database")
@Schema(description = "数据库使用级别", example = "high")
private String database;
}
}

View File

@ -0,0 +1,85 @@
package com.chinaweal.youfool.devops.ai.mcp.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* MCP tools/list 请求参数
*
* 用于 tools/list 方法的参数定义
*
* @author AI开发团队
* @since 1.0.0
*/
@Data
@Schema(description = "MCP tools/list 请求参数")
public class MCPToolListRequest {
/**
* 可选的工具类型过滤器
* 例如function, resource
*/
@JsonProperty("type")
@Schema(description = "工具类型过滤器", example = "function")
private String type;
/**
* 可选的标签过滤器
* 用于筛选特定标签的工具
*/
@JsonProperty("tags")
@Schema(description = "标签过滤器")
private String[] tags;
/**
* 可选的分页参数 - 限制返回的工具数量
*/
@JsonProperty("limit")
@Schema(description = "限制返回的工具数量", example = "10")
private Integer limit;
/**
* 可选的分页参数 - 偏移量
*/
@JsonProperty("offset")
@Schema(description = "偏移量", example = "0")
private Integer offset;
/**
* 验证请求参数
*/
public boolean isValid() {
// limit 必须大于 0
if (limit != null && limit <= 0) {
return false;
}
// offset 必须大于等于 0
if (offset != null && offset < 0) {
return false;
}
return true;
}
/**
* 获取有效的 limit
*/
public int getEffectiveLimit() {
if (limit == null || limit <= 0) {
return 100; // 默认限制
}
return Math.min(limit, 1000); // 最大限制
}
/**
* 获取有效的 offset
*/
public int getEffectiveOffset() {
if (offset == null || offset < 0) {
return 0;
}
return offset;
}
}

View File

@ -0,0 +1,75 @@
package com.chinaweal.youfool.devops.ai.mcp.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* MCP tools/list 响应对象
*
* 用于 tools/list 方法的响应定义
*
* @author AI开发团队
* @since 1.0.0
*/
@Data
@Schema(description = "MCP tools/list 响应对象")
public class MCPToolListResponse {
/**
* 可用的工具列表
*/
@JsonProperty("tools")
@Schema(description = "可用的工具列表", required = true)
private List<MCPToolDefinition> tools;
/**
* 工具总数用于分页
*/
@JsonProperty("totalCount")
@Schema(description = "工具总数")
private Integer totalCount;
/**
* 是否还有更多工具用于分页
*/
@JsonProperty("hasMore")
@Schema(description = "是否还有更多工具")
private Boolean hasMore;
/**
* 下一页的偏移量用于分页
*/
@JsonProperty("nextOffset")
@Schema(description = "下一页的偏移量")
private Integer nextOffset;
/**
* 创建简单的工具列表响应
*/
public static MCPToolListResponse of(List<MCPToolDefinition> tools) {
MCPToolListResponse response = new MCPToolListResponse();
response.setTools(tools);
response.setTotalCount(tools.size());
response.setHasMore(false);
return response;
}
/**
* 创建分页的工具列表响应
*/
public static MCPToolListResponse of(List<MCPToolDefinition> tools, int totalCount, int offset, int limit) {
MCPToolListResponse response = new MCPToolListResponse();
response.setTools(tools);
response.setTotalCount(totalCount);
response.setHasMore(offset + tools.size() < totalCount);
if (response.getHasMore()) {
response.setNextOffset(offset + tools.size());
}
return response;
}
}

View File

@ -0,0 +1,443 @@
package com.chinaweal.youfool.devops.ai.mcp;
import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolCallResponse;
import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolDefinition;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* MCPFunctionBridge 单元测试
*
* 测试MCP与函数调用格式之间的转换功能
*
* @author AI开发团队
* @since 1.0.0
*/
@ExtendWith(MockitoExtension.class)
class MCPFunctionBridgeTest {
@Mock
private MCPToolRegistry toolRegistry;
@Mock
private ObjectMapper objectMapper;
@InjectMocks
private MCPFunctionBridge functionBridge;
private MCPToolDefinition testTool;
private MCPToolCallResponse testResponse;
@BeforeEach
void setUp() {
// 创建测试工具定义
testTool = new MCPToolDefinition();
testTool.setName("test_tool");
testTool.setDescription("测试工具描述");
testTool.setType("function");
testTool.setReadOnlyHint(true);
testTool.setDestructiveHint(false);
testTool.setTags(List.of("test", "readonly"));
// 创建输入Schema
MCPToolDefinition.JsonSchema inputSchema = new MCPToolDefinition.JsonSchema();
inputSchema.setType("object");
inputSchema.setProperties(Map.of(
"param1", MCPToolDefinition.PropertySchema.string("参数1"),
"param2", MCPToolDefinition.PropertySchema.integer("参数2").withDefault(10)
));
inputSchema.setRequired(List.of("param1"));
testTool.setInputSchema(inputSchema);
// 创建测试响应
testResponse = MCPToolCallResponse.success(Map.of("result", "success"));
testResponse.setCallId("test-call-123");
testResponse.setExecutionTime(1500L);
}
@Test
void testConvertToOpenAIFunctions() {
// 准备数据
List<MCPToolDefinition> tools = List.of(testTool);
// 执行测试
List<Map<String, Object>> openAIFunctions = functionBridge.convertToOpenAIFunctions(tools);
// 验证结果
assertNotNull(openAIFunctions);
assertEquals(1, openAIFunctions.size());
Map<String, Object> functionDef = openAIFunctions.get(0);
assertEquals("function", functionDef.get("type"));
assertTrue(functionDef.containsKey("function"));
@SuppressWarnings("unchecked")
Map<String, Object> function = (Map<String, Object>) functionDef.get("function");
assertEquals("test_tool", function.get("name"));
assertEquals("测试工具描述", function.get("description"));
assertTrue(function.containsKey("parameters"));
@SuppressWarnings("unchecked")
Map<String, Object> parameters = (Map<String, Object>) function.get("parameters");
assertEquals("object", parameters.get("type"));
assertTrue(parameters.containsKey("properties"));
assertTrue(parameters.containsKey("required"));
}
@Test
void testConvertToAnthropicTools() {
// 准备数据
List<MCPToolDefinition> tools = List.of(testTool);
// 执行测试
List<Map<String, Object>> anthropicTools = functionBridge.convertToAnthropicTools(tools);
// 验证结果
assertNotNull(anthropicTools);
assertEquals(1, anthropicTools.size());
Map<String, Object> tool = anthropicTools.get(0);
assertEquals("test_tool", tool.get("name"));
assertEquals("测试工具描述", tool.get("description"));
assertTrue(tool.containsKey("input_schema"));
@SuppressWarnings("unchecked")
Map<String, Object> inputSchema = (Map<String, Object>) tool.get("input_schema");
assertEquals("object", inputSchema.get("type"));
assertTrue(inputSchema.containsKey("properties"));
assertTrue(inputSchema.containsKey("required"));
}
@Test
void testConvertToGenericFunctions() {
// 准备数据
List<MCPToolDefinition> tools = List.of(testTool);
when(objectMapper.convertValue(any(), eq(Map.class))).thenReturn(Map.of("type", "object"));
// 执行测试
List<Map<String, Object>> genericFunctions = functionBridge.convertToGenericFunctions(tools);
// 验证结果
assertNotNull(genericFunctions);
assertEquals(1, genericFunctions.size());
Map<String, Object> function = genericFunctions.get(0);
assertEquals("test_tool", function.get("name"));
assertEquals("测试工具描述", function.get("description"));
assertEquals("function", function.get("type"));
assertTrue(function.containsKey("input_schema"));
assertTrue(function.containsKey("tags"));
assertEquals(true, function.get("read_only"));
assertEquals(false, function.get("destructive"));
}
@Test
void testConvertFromOpenAIFunctionResult_WithContent() {
// 准备数据
Map<String, Object> functionResult = Map.of(
"content", "执行成功",
"status", "completed"
);
// 执行测试
MCPToolCallResponse response = functionBridge.convertFromOpenAIFunctionResult(functionResult, "test_tool");
// 验证结果
assertNotNull(response);
assertFalse(response.getIsError());
assertNotNull(response.getContent());
assertEquals(1, response.getContent().size());
assertEquals("data", response.getContent().get(0).getType());
assertEquals("执行成功", response.getContent().get(0).getData());
}
@Test
void testConvertFromOpenAIFunctionResult_WithError() {
// 准备数据
Map<String, Object> functionResult = Map.of(
"error", "执行失败",
"status", "error"
);
// 执行测试
MCPToolCallResponse response = functionBridge.convertFromOpenAIFunctionResult(functionResult, "test_tool");
// 验证结果
assertNotNull(response);
assertTrue(response.getIsError());
assertNotNull(response.getContent());
assertEquals(1, response.getContent().size());
assertEquals("text", response.getContent().get(0).getType());
assertEquals("执行失败", response.getContent().get(0).getText());
}
@Test
void testConvertFromAnthropicToolResult_WithResult() {
// 准备数据
Map<String, Object> toolResult = Map.of(
"result", Map.of("data", "处理完成"),
"tool_use_id", "anthropic-123"
);
// 执行测试
MCPToolCallResponse response = functionBridge.convertFromAnthropicToolResult(toolResult, "test_tool");
// 验证结果
assertNotNull(response);
assertFalse(response.getIsError());
assertNotNull(response.getContent());
assertEquals(1, response.getContent().size());
assertEquals("data", response.getContent().get(0).getType());
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) response.getContent().get(0).getData();
assertEquals("处理完成", data.get("data"));
}
@Test
void testConvertToOpenAIFunctionResult() {
// 执行测试
Map<String, Object> result = functionBridge.convertToOpenAIFunctionResult(testResponse, "test_tool");
// 验证结果
assertNotNull(result);
assertEquals("test_tool", result.get("function_name"));
assertEquals("test-call-123", result.get("call_id"));
assertEquals("success", result.get("status"));
assertEquals(1500L, result.get("execution_time_ms"));
@SuppressWarnings("unchecked")
Map<String, Object> content = (Map<String, Object>) result.get("content");
assertEquals("success", content.get("result"));
}
@Test
void testConvertToOpenAIFunctionResult_WithError() {
// 准备错误响应
MCPToolCallResponse errorResponse = MCPToolCallResponse.error("执行失败", "error-call-456");
// 执行测试
Map<String, Object> result = functionBridge.convertToOpenAIFunctionResult(errorResponse, "test_tool");
// 验证结果
assertNotNull(result);
assertEquals("test_tool", result.get("function_name"));
assertEquals("error-call-456", result.get("call_id"));
assertEquals("error", result.get("status"));
assertEquals("执行失败", result.get("error"));
}
@Test
void testConvertToAnthropicToolResult() {
// 执行测试
Map<String, Object> result = functionBridge.convertToAnthropicToolResult(testResponse, "test_tool");
// 验证结果
assertNotNull(result);
assertEquals("test_tool", result.get("tool_name"));
assertEquals("test-call-123", result.get("tool_use_id"));
assertEquals(false, result.get("is_error"));
assertEquals(1500L, result.get("execution_time_ms"));
@SuppressWarnings("unchecked")
Map<String, Object> content = (Map<String, Object>) result.get("content");
assertEquals("success", content.get("result"));
}
@Test
void testConvertToAnthropicToolResult_WithError() {
// 准备错误响应
MCPToolCallResponse errorResponse = MCPToolCallResponse.error("执行失败", "error-call-456");
// 执行测试
Map<String, Object> result = functionBridge.convertToAnthropicToolResult(errorResponse, "test_tool");
// 验证结果
assertNotNull(result);
assertEquals("test_tool", result.get("tool_name"));
assertEquals("error-call-456", result.get("tool_use_id"));
assertEquals(true, result.get("is_error"));
assertEquals("执行失败", result.get("content"));
}
@Test
void testConvertAllToolsToFormat_OpenAI() {
// 准备数据
when(toolRegistry.getAllTools()).thenReturn(List.of(testTool));
// 执行测试
List<Map<String, Object>> result = functionBridge.convertAllToolsToFormat("openai");
// 验证结果
assertNotNull(result);
assertEquals(1, result.size());
assertEquals("function", result.get(0).get("type"));
assertTrue(result.get(0).containsKey("function"));
}
@Test
void testConvertAllToolsToFormat_Anthropic() {
// 准备数据
when(toolRegistry.getAllTools()).thenReturn(List.of(testTool));
// 执行测试
List<Map<String, Object>> result = functionBridge.convertAllToolsToFormat("anthropic");
// 验证结果
assertNotNull(result);
assertEquals(1, result.size());
assertEquals("test_tool", result.get(0).get("name"));
assertTrue(result.get(0).containsKey("input_schema"));
}
@Test
void testConvertAllToolsToFormat_Generic() {
// 准备数据
when(toolRegistry.getAllTools()).thenReturn(List.of(testTool));
when(objectMapper.convertValue(any(), eq(Map.class))).thenReturn(Map.of("type", "object"));
// 执行测试
List<Map<String, Object>> result = functionBridge.convertAllToolsToFormat("generic");
// 验证结果
assertNotNull(result);
assertEquals(1, result.size());
assertEquals("test_tool", result.get(0).get("name"));
assertEquals("function", result.get(0).get("type"));
assertTrue(result.get(0).containsKey("tags"));
}
@Test
void testConvertAllToolsToFormat_Unknown() {
// 准备数据
when(toolRegistry.getAllTools()).thenReturn(List.of(testTool));
when(objectMapper.convertValue(any(), eq(Map.class))).thenReturn(Map.of("type", "object"));
// 执行测试
List<Map<String, Object>> result = functionBridge.convertAllToolsToFormat("unknown_format");
// 验证结果 - 应该回退到通用格式
assertNotNull(result);
assertEquals(1, result.size());
assertEquals("test_tool", result.get(0).get("name"));
assertEquals("function", result.get(0).get("type"));
}
@Test
void testValidateFunctionCall_Success() {
// 准备数据
when(toolRegistry.getTool("test_tool")).thenReturn(Optional.of(testTool));
Map<String, Object> arguments = Map.of(
"param1", "value1",
"param2", 20
);
// 执行测试
MCPFunctionBridge.ValidationResult result = functionBridge.validateFunctionCall("test_tool", arguments);
// 验证结果
assertNotNull(result);
assertTrue(result.isValid());
assertNull(result.getMessage());
}
@Test
void testValidateFunctionCall_MissingRequiredParam() {
// 准备数据
when(toolRegistry.getTool("test_tool")).thenReturn(Optional.of(testTool));
Map<String, Object> arguments = Map.of(
"param2", 20
// 缺少必需的 param1
);
// 执行测试
MCPFunctionBridge.ValidationResult result = functionBridge.validateFunctionCall("test_tool", arguments);
// 验证结果
assertNotNull(result);
assertFalse(result.isValid());
assertNotNull(result.getMessage());
assertTrue(result.getMessage().contains("param1"));
}
@Test
void testValidateFunctionCall_ToolNotFound() {
// 准备数据
when(toolRegistry.getTool("unknown_tool")).thenReturn(Optional.empty());
// 执行测试
MCPFunctionBridge.ValidationResult result = functionBridge.validateFunctionCall("unknown_tool", Map.of());
// 验证结果
assertNotNull(result);
assertFalse(result.isValid());
assertNotNull(result.getMessage());
assertTrue(result.getMessage().contains("工具不存在"));
assertTrue(result.getMessage().contains("unknown_tool"));
}
@Test
void testValidateFunctionCall_Exception() {
// 准备数据
when(toolRegistry.getTool("test_tool")).thenThrow(new RuntimeException("数据库错误"));
// 执行测试
MCPFunctionBridge.ValidationResult result = functionBridge.validateFunctionCall("test_tool", Map.of());
// 验证结果
assertNotNull(result);
assertFalse(result.isValid());
assertNotNull(result.getMessage());
assertTrue(result.getMessage().contains("参数验证失败"));
}
@Test
void testContentExtraction_MultipleContent() {
// 创建包含多个内容项的响应
MCPToolCallResponse response = new MCPToolCallResponse();
response.setContent(List.of(
MCPToolCallResponse.MCPContent.text("第一部分"),
MCPToolCallResponse.MCPContent.data(Map.of("key", "value"))
));
response.setIsError(false);
// 转换为OpenAI格式
Map<String, Object> result = functionBridge.convertToOpenAIFunctionResult(response, "test_tool");
// 验证结果 - 多个内容项应该返回完整列表
assertNotNull(result);
assertEquals("success", result.get("status"));
@SuppressWarnings("unchecked")
List<Object> content = (List<Object>) result.get("content");
assertEquals(2, content.size());
}
@Test
void testConversionWithNullValues() {
// 创建包含null值的响应
MCPToolCallResponse response = MCPToolCallResponse.success(null);
// 执行转换
Map<String, Object> openAIResult = functionBridge.convertToOpenAIFunctionResult(response, "test_tool");
Map<String, Object> anthropicResult = functionBridge.convertToAnthropicToolResult(response, "test_tool");
// 验证结果
assertNotNull(openAIResult);
assertNotNull(anthropicResult);
assertEquals("success", openAIResult.get("status"));
assertEquals(false, anthropicResult.get("is_error"));
}
}

View File

@ -0,0 +1,320 @@
package com.chinaweal.youfool.devops.ai.mcp;
import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolDefinition;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
/**
* MCPToolRegistry 单元测试
*
* 测试工具注册表的功能
*
* @author AI开发团队
* @since 1.0.0
*/
class MCPToolRegistryTest {
private MCPToolRegistry toolRegistry;
@BeforeEach
void setUp() {
toolRegistry = new MCPToolRegistry();
}
@Test
void testGetAllTools() {
// 执行测试
List<MCPToolDefinition> tools = toolRegistry.getAllTools();
// 验证结果
assertNotNull(tools);
assertFalse(tools.isEmpty());
assertEquals(4, tools.size()); // 应该有4个预定义工具
// 验证工具名称
List<String> toolNames = tools.stream().map(MCPToolDefinition::getName).toList();
assertTrue(toolNames.contains("repair_query"));
assertTrue(toolNames.contains("repair_feedback_query"));
assertTrue(toolNames.contains("similarity_search"));
assertTrue(toolNames.contains("knowledge_query"));
}
@Test
void testGetTool_Exists() {
// 执行测试
Optional<MCPToolDefinition> tool = toolRegistry.getTool("repair_query");
// 验证结果
assertTrue(tool.isPresent());
assertEquals("repair_query", tool.get().getName());
assertEquals("function", tool.get().getType());
assertNotNull(tool.get().getDescription());
assertNotNull(tool.get().getInputSchema());
assertEquals(Boolean.TRUE, tool.get().getReadOnlyHint());
assertEquals(Boolean.FALSE, tool.get().getDestructiveHint());
}
@Test
void testGetTool_NotExists() {
// 执行测试
Optional<MCPToolDefinition> tool = toolRegistry.getTool("non_existent_tool");
// 验证结果
assertFalse(tool.isPresent());
}
@Test
void testGetToolsByType() {
// 执行测试
List<MCPToolDefinition> functionTools = toolRegistry.getToolsByType("function");
// 验证结果
assertNotNull(functionTools);
assertEquals(4, functionTools.size()); // 所有工具都是function类型
assertTrue(functionTools.stream().allMatch(tool -> "function".equals(tool.getType())));
}
@Test
void testGetToolsByType_NotExists() {
// 执行测试
List<MCPToolDefinition> resourceTools = toolRegistry.getToolsByType("resource");
// 验证结果
assertNotNull(resourceTools);
assertTrue(resourceTools.isEmpty());
}
@Test
void testGetToolsByTag() {
// 执行测试
List<MCPToolDefinition> readOnlyTools = toolRegistry.getToolsByTag("readonly");
// 验证结果
assertNotNull(readOnlyTools);
assertFalse(readOnlyTools.isEmpty());
assertTrue(readOnlyTools.stream().allMatch(tool ->
tool.getTags() != null && tool.getTags().contains("readonly")));
}
@Test
void testGetToolsByTag_NotExists() {
// 执行测试
List<MCPToolDefinition> unknownTagTools = toolRegistry.getToolsByTag("unknown_tag");
// 验证结果
assertNotNull(unknownTagTools);
assertTrue(unknownTagTools.isEmpty());
}
@Test
void testGetToolsPaginated() {
// 执行测试 - 第一页每页2个
List<MCPToolDefinition> page1 = toolRegistry.getToolsPaginated(0, 2);
// 验证结果
assertNotNull(page1);
assertEquals(2, page1.size());
// 执行测试 - 第二页每页2个
List<MCPToolDefinition> page2 = toolRegistry.getToolsPaginated(2, 2);
// 验证结果
assertNotNull(page2);
assertEquals(2, page2.size());
// 验证不同页的内容不同
assertNotEquals(page1.get(0).getName(), page2.get(0).getName());
}
@Test
void testGetToolsPaginated_OffsetBeyondRange() {
// 执行测试 - 偏移量超出范围
List<MCPToolDefinition> tools = toolRegistry.getToolsPaginated(100, 10);
// 验证结果
assertNotNull(tools);
assertTrue(tools.isEmpty());
}
@Test
void testGetToolsPaginated_LimitBeyondAvailable() {
// 执行测试 - 限制数量超出可用数量
List<MCPToolDefinition> tools = toolRegistry.getToolsPaginated(0, 100);
// 验证结果
assertNotNull(tools);
assertEquals(4, tools.size()); // 应该返回所有可用的工具
}
@Test
void testHaseTool() {
// 测试存在的工具
assertTrue(toolRegistry.haseTool("repair_query"));
assertTrue(toolRegistry.haseTool("repair_feedback_query"));
assertTrue(toolRegistry.haseTool("similarity_search"));
assertTrue(toolRegistry.haseTool("knowledge_query"));
// 测试不存在的工具
assertFalse(toolRegistry.haseTool("non_existent_tool"));
assertFalse(toolRegistry.haseTool(""));
assertFalse(toolRegistry.haseTool(null));
}
@Test
void testGetToolCount() {
// 执行测试
int count = toolRegistry.getToolCount();
// 验证结果
assertEquals(4, count);
}
@Test
void testRepairQueryToolDefinition() {
// 获取repair_query工具
Optional<MCPToolDefinition> tool = toolRegistry.getTool("repair_query");
assertTrue(tool.isPresent());
MCPToolDefinition repairQuery = tool.get();
// 验证基本属性
assertEquals("repair_query", repairQuery.getName());
assertEquals("function", repairQuery.getType());
assertNotNull(repairQuery.getDescription());
assertTrue(repairQuery.getDescription().contains("工单"));
assertEquals("1.0.0", repairQuery.getVersion());
assertEquals("运维系统AI团队", repairQuery.getAuthor());
assertEquals(Boolean.TRUE, repairQuery.getReadOnlyHint());
assertEquals(Boolean.FALSE, repairQuery.getDestructiveHint());
// 验证标签
assertNotNull(repairQuery.getTags());
assertTrue(repairQuery.getTags().contains("repair"));
assertTrue(repairQuery.getTags().contains("query"));
assertTrue(repairQuery.getTags().contains("readonly"));
// 验证输入Schema
assertNotNull(repairQuery.getInputSchema());
assertEquals("object", repairQuery.getInputSchema().getType());
assertNotNull(repairQuery.getInputSchema().getProperties());
assertTrue(repairQuery.getInputSchema().getProperties().containsKey("repairId"));
assertNotNull(repairQuery.getInputSchema().getRequired());
assertTrue(repairQuery.getInputSchema().getRequired().contains("repairId"));
assertEquals(Boolean.FALSE, repairQuery.getInputSchema().getAdditionalProperties());
// 验证性能提示
assertNotNull(repairQuery.getExpectedExecutionTime());
assertTrue(repairQuery.getExpectedExecutionTime().getMin() > 0);
assertTrue(repairQuery.getExpectedExecutionTime().getMax() > repairQuery.getExpectedExecutionTime().getMin());
assertNotNull(repairQuery.getExpectedExecutionTime().getAverage());
// 验证资源需求
assertNotNull(repairQuery.getResourceRequirements());
assertEquals("low", repairQuery.getResourceRequirements().getCpu());
assertEquals("low", repairQuery.getResourceRequirements().getMemory());
assertEquals("low", repairQuery.getResourceRequirements().getNetwork());
assertEquals("medium", repairQuery.getResourceRequirements().getDatabase());
}
@Test
void testSimilaritySearchToolDefinition() {
// 获取similarity_search工具
Optional<MCPToolDefinition> tool = toolRegistry.getTool("similarity_search");
assertTrue(tool.isPresent());
MCPToolDefinition similaritySearch = tool.get();
// 验证基本属性
assertEquals("similarity_search", similaritySearch.getName());
assertTrue(similaritySearch.getDescription().contains("向量"));
assertTrue(similaritySearch.getDescription().contains("相似度"));
// 验证标签
assertNotNull(similaritySearch.getTags());
assertTrue(similaritySearch.getTags().contains("search"));
assertTrue(similaritySearch.getTags().contains("similarity"));
assertTrue(similaritySearch.getTags().contains("vector"));
assertTrue(similaritySearch.getTags().contains("ai"));
// 验证输入Schema - 应该有queryText, topK, threshold参数
assertNotNull(similaritySearch.getInputSchema());
assertNotNull(similaritySearch.getInputSchema().getProperties());
assertTrue(similaritySearch.getInputSchema().getProperties().containsKey("queryText"));
assertTrue(similaritySearch.getInputSchema().getProperties().containsKey("topK"));
assertTrue(similaritySearch.getInputSchema().getProperties().containsKey("threshold"));
// 验证必需参数
assertNotNull(similaritySearch.getInputSchema().getRequired());
assertTrue(similaritySearch.getInputSchema().getRequired().contains("queryText"));
assertFalse(similaritySearch.getInputSchema().getRequired().contains("topK")); // 有默认值,非必需
assertFalse(similaritySearch.getInputSchema().getRequired().contains("threshold")); // 有默认值,非必需
// 验证资源需求 - 相似度搜索是高资源消耗的操作
assertNotNull(similaritySearch.getResourceRequirements());
assertEquals("high", similaritySearch.getResourceRequirements().getCpu());
assertEquals("high", similaritySearch.getResourceRequirements().getMemory());
assertEquals("high", similaritySearch.getResourceRequirements().getDatabase());
}
@Test
void testKnowledgeQueryToolDefinition() {
// 获取knowledge_query工具
Optional<MCPToolDefinition> tool = toolRegistry.getTool("knowledge_query");
assertTrue(tool.isPresent());
MCPToolDefinition knowledgeQuery = tool.get();
// 验证基本属性
assertEquals("knowledge_query", knowledgeQuery.getName());
assertTrue(knowledgeQuery.getDescription().contains("知识库"));
// 验证输入Schema - 应该有kbId和sourceRepairId参数
assertNotNull(knowledgeQuery.getInputSchema());
assertNotNull(knowledgeQuery.getInputSchema().getProperties());
assertTrue(knowledgeQuery.getInputSchema().getProperties().containsKey("kbId"));
assertTrue(knowledgeQuery.getInputSchema().getProperties().containsKey("sourceRepairId"));
// 验证必需参数 - 都不是必需的,但至少需要一个
assertNotNull(knowledgeQuery.getInputSchema().getRequired());
assertTrue(knowledgeQuery.getInputSchema().getRequired().isEmpty()); // 没有严格必需的参数
}
@Test
void testAllToolsHaveValidSchema() {
// 获取所有工具
List<MCPToolDefinition> allTools = toolRegistry.getAllTools();
// 验证每个工具都有有效的Schema
for (MCPToolDefinition tool : allTools) {
// 基本属性验证
assertNotNull(tool.getName(), "工具名称不能为空: " + tool);
assertFalse(tool.getName().trim().isEmpty(), "工具名称不能为空字符串: " + tool);
assertNotNull(tool.getDescription(), "工具描述不能为空: " + tool.getName());
assertNotNull(tool.getType(), "工具类型不能为空: " + tool.getName());
assertNotNull(tool.getVersion(), "工具版本不能为空: " + tool.getName());
// Schema验证
assertNotNull(tool.getInputSchema(), "输入Schema不能为空: " + tool.getName());
assertNotNull(tool.getInputSchema().getType(), "Schema类型不能为空: " + tool.getName());
assertEquals("object", tool.getInputSchema().getType(), "Schema类型应该是object: " + tool.getName());
// 性能提示验证
if (tool.getExpectedExecutionTime() != null) {
assertTrue(tool.getExpectedExecutionTime().getMin() >= 0, "最小执行时间不能为负: " + tool.getName());
if (tool.getExpectedExecutionTime().getMax() != null) {
assertTrue(tool.getExpectedExecutionTime().getMax() >= tool.getExpectedExecutionTime().getMin(),
"最大执行时间应该大于等于最小执行时间: " + tool.getName());
}
}
// 提示标志验证
assertNotNull(tool.getReadOnlyHint(), "只读提示不能为空: " + tool.getName());
assertNotNull(tool.getDestructiveHint(), "破坏性提示不能为空: " + tool.getName());
}
}
}

View File

@ -0,0 +1,423 @@
package com.chinaweal.youfool.devops.ai.mcp;
import com.chinaweal.youfool.devops.ai.mcp.dto.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* TrueMCPServer 单元测试
*
* 测试 MCP JSON-RPC 服务器的核心功能
*
* @author AI开发团队
* @since 1.0.0
*/
@ExtendWith(MockitoExtension.class)
class TrueMCPServerTest {
@Mock
private MCPToolRegistry toolRegistry;
@Mock
private MCPServer mcpServer;
@Mock
private ObjectMapper objectMapper;
@InjectMocks
private TrueMCPServer trueMCPServer;
private MCPJsonRpcRequest request;
private MCPToolDefinition testTool;
@BeforeEach
void setUp() {
// 创建测试工具定义
testTool = new MCPToolDefinition();
testTool.setName("test_tool");
testTool.setDescription("测试工具");
testTool.setType("function");
// 创建基础请求
request = new MCPJsonRpcRequest();
request.setJsonrpc("2.0");
request.setId(1);
}
@Test
void testToolsList_Success() {
// 准备数据
request.setMethod("tools/list");
List<MCPToolDefinition> tools = List.of(testTool);
when(toolRegistry.getAllTools()).thenReturn(tools);
// 执行测试
MCPJsonRpcResponse response = trueMCPServer.toolsList(request);
// 验证结果
assertNotNull(response);
assertEquals("2.0", response.getJsonrpc());
assertEquals(1, response.getId());
assertNull(response.getError());
assertNotNull(response.getResult());
assertTrue(response.isSuccess());
verify(toolRegistry).getAllTools();
}
@Test
void testToolsList_InvalidRequest() {
// 准备无效请求
request.setJsonrpc("1.0"); // 错误的版本
request.setMethod("tools/list");
// 执行测试
MCPJsonRpcResponse response = trueMCPServer.toolsList(request);
// 验证结果
assertNotNull(response);
assertTrue(response.isError());
assertNotNull(response.getError());
assertEquals(MCPError.INVALID_REQUEST, response.getError().getCode());
}
@Test
void testToolsList_MethodNotFound() {
// 准备错误方法请求
request.setMethod("unknown/method");
// 执行测试
MCPJsonRpcResponse response = trueMCPServer.toolsList(request);
// 验证结果
assertNotNull(response);
assertTrue(response.isError());
assertNotNull(response.getError());
assertEquals(MCPError.METHOD_NOT_FOUND, response.getError().getCode());
assertTrue(response.getError().getMessage().contains("unknown/method"));
}
@Test
void testToolsList_WithPagination() {
// 准备分页参数
request.setMethod("tools/list");
Map<String, Object> params = Map.of(
"limit", 5,
"offset", 0
);
request.setParams(params);
MCPToolListRequest listRequest = new MCPToolListRequest();
listRequest.setLimit(5);
listRequest.setOffset(0);
List<MCPToolDefinition> tools = List.of(testTool);
when(objectMapper.convertValue(params, MCPToolListRequest.class)).thenReturn(listRequest);
when(toolRegistry.getToolsPaginated(0, 5)).thenReturn(tools);
when(toolRegistry.getToolCount()).thenReturn(10);
// 执行测试
MCPJsonRpcResponse response = trueMCPServer.toolsList(request);
// 验证结果
assertNotNull(response);
assertTrue(response.isSuccess());
verify(toolRegistry).getToolsPaginated(0, 5);
verify(toolRegistry).getToolCount();
}
@Test
void testToolsCall_Success() {
// 准备数据
request.setMethod("tools/call");
Map<String, Object> params = Map.of(
"name", "test_tool",
"arguments", Map.of("param1", "value1")
);
request.setParams(params);
MCPToolCallRequest callRequest = new MCPToolCallRequest();
callRequest.setName("test_tool");
callRequest.setArguments(Map.of("param1", "value1"));
MCPResponse mcpResponse = MCPResponse.success(Map.of("result", "success"));
when(objectMapper.convertValue(params, MCPToolCallRequest.class)).thenReturn(callRequest);
when(toolRegistry.haseTool("test_tool")).thenReturn(true);
when(toolRegistry.getTool("test_tool")).thenReturn(Optional.of(testTool));
when(mcpServer.executeTool("test_tool", Map.of("param1", "value1"))).thenReturn(mcpResponse);
// 执行测试
MCPJsonRpcResponse response = trueMCPServer.toolsCall(request);
// 验证结果
assertNotNull(response);
assertTrue(response.isSuccess());
assertNotNull(response.getResult());
verify(mcpServer).executeTool("test_tool", Map.of("param1", "value1"));
}
@Test
void testToolsCall_InvalidMethod() {
// 准备错误方法请求
request.setMethod("tools/invalid");
// 执行测试
MCPJsonRpcResponse response = trueMCPServer.toolsCall(request);
// 验证结果
assertNotNull(response);
assertTrue(response.isError());
assertEquals(MCPError.METHOD_NOT_FOUND, response.getError().getCode());
}
@Test
void testToolsCall_InvalidParams() {
// 准备无效参数
request.setMethod("tools/call");
request.setParams(Map.of("invalid", "params"));
when(objectMapper.convertValue(any(), eq(MCPToolCallRequest.class))).thenReturn(null);
// 执行测试
MCPJsonRpcResponse response = trueMCPServer.toolsCall(request);
// 验证结果
assertNotNull(response);
assertTrue(response.isError());
assertEquals(MCPError.INVALID_PARAMS, response.getError().getCode());
}
@Test
void testToolsCall_ToolNotFound() {
// 准备数据
request.setMethod("tools/call");
Map<String, Object> params = Map.of(
"name", "unknown_tool",
"arguments", Map.of()
);
request.setParams(params);
MCPToolCallRequest callRequest = new MCPToolCallRequest();
callRequest.setName("unknown_tool");
callRequest.setArguments(Map.of());
when(objectMapper.convertValue(params, MCPToolCallRequest.class)).thenReturn(callRequest);
when(toolRegistry.haseTool("unknown_tool")).thenReturn(false);
// 执行测试
MCPJsonRpcResponse response = trueMCPServer.toolsCall(request);
// 验证结果
assertNotNull(response);
assertTrue(response.isError());
assertEquals(MCPError.TOOL_NOT_FOUND, response.getError().getCode());
}
@Test
void testToolsCall_ExecutionError() {
// 准备数据
request.setMethod("tools/call");
Map<String, Object> params = Map.of(
"name", "test_tool",
"arguments", Map.of("param1", "value1")
);
request.setParams(params);
MCPToolCallRequest callRequest = new MCPToolCallRequest();
callRequest.setName("test_tool");
callRequest.setArguments(Map.of("param1", "value1"));
MCPResponse mcpResponse = MCPResponse.error("执行失败");
when(objectMapper.convertValue(params, MCPToolCallRequest.class)).thenReturn(callRequest);
when(toolRegistry.haseTool("test_tool")).thenReturn(true);
when(toolRegistry.getTool("test_tool")).thenReturn(Optional.of(testTool));
when(mcpServer.executeTool("test_tool", Map.of("param1", "value1"))).thenReturn(mcpResponse);
// 执行测试
MCPJsonRpcResponse response = trueMCPServer.toolsCall(request);
// 验证结果
assertNotNull(response);
assertTrue(response.isError());
assertEquals(MCPError.TOOL_EXECUTION_ERROR, response.getError().getCode());
assertTrue(response.getError().getMessage().contains("执行失败"));
}
@Test
void testBatchToolsCall_Success() {
// 准备批量调用数据
request.setMethod("tools/batch-call");
List<Map<String, Object>> batchParams = List.of(
Map.of("name", "test_tool", "arguments", Map.of("param1", "value1")),
Map.of("name", "test_tool", "arguments", Map.of("param1", "value2"))
);
request.setParams(batchParams);
MCPToolCallRequest callRequest1 = new MCPToolCallRequest();
callRequest1.setName("test_tool");
callRequest1.setArguments(Map.of("param1", "value1"));
MCPToolCallRequest callRequest2 = new MCPToolCallRequest();
callRequest2.setName("test_tool");
callRequest2.setArguments(Map.of("param1", "value2"));
MCPResponse mcpResponse1 = MCPResponse.success(Map.of("result", "success1"));
MCPResponse mcpResponse2 = MCPResponse.success(Map.of("result", "success2"));
when(objectMapper.convertValue(batchParams.get(0), MCPToolCallRequest.class)).thenReturn(callRequest1);
when(objectMapper.convertValue(batchParams.get(1), MCPToolCallRequest.class)).thenReturn(callRequest2);
when(toolRegistry.haseTool("test_tool")).thenReturn(true);
when(mcpServer.executeTool(eq("test_tool"), any())).thenReturn(mcpResponse1, mcpResponse2);
// 执行测试
MCPJsonRpcResponse response = trueMCPServer.batchToolsCall(request);
// 验证结果
assertNotNull(response);
assertTrue(response.isSuccess());
@SuppressWarnings("unchecked")
Map<String, Object> result = (Map<String, Object>) response.getResult();
assertEquals(2, result.get("total_calls"));
assertNotNull(result.get("results"));
verify(mcpServer, times(2)).executeTool(eq("test_tool"), any());
}
@Test
void testBatchToolsCall_InvalidParams() {
// 准备无效的批量调用参数
request.setMethod("tools/batch-call");
request.setParams("invalid"); // 应该是数组
// 执行测试
MCPJsonRpcResponse response = trueMCPServer.batchToolsCall(request);
// 验证结果
assertNotNull(response);
assertTrue(response.isError());
assertEquals(MCPError.INVALID_PARAMS, response.getError().getCode());
}
@Test
void testBatchToolsCall_TooManyRequests() {
// 准备过多的批量调用请求
request.setMethod("tools/batch-call");
List<Map<String, Object>> batchParams = List.of();
for (int i = 0; i < 15; i++) { // 超过限制的10个
batchParams = new java.util.ArrayList<>(batchParams);
((java.util.ArrayList<Map<String, Object>>) batchParams).add(
Map.of("name", "test_tool", "arguments", Map.of())
);
}
request.setParams(batchParams);
// 执行测试
MCPJsonRpcResponse response = trueMCPServer.batchToolsCall(request);
// 验证结果
assertNotNull(response);
assertTrue(response.isError());
assertEquals(MCPError.INVALID_PARAMS, response.getError().getCode());
assertTrue(response.getError().getMessage().contains("1-10"));
}
@Test
void testHealth() {
// 准备数据
when(toolRegistry.getToolCount()).thenReturn(4);
// 执行测试
Map<String, Object> health = trueMCPServer.health();
// 验证结果
assertNotNull(health);
assertEquals("UP", health.get("status"));
assertEquals("MCP", health.get("protocol"));
assertEquals("2.0", health.get("jsonrpc_version"));
assertEquals(4, health.get("available_tools"));
assertNotNull(health.get("server_time"));
assertNotNull(health.get("endpoints"));
}
@Test
void testGetProtocolInfo() {
// 准备数据
when(toolRegistry.getToolCount()).thenReturn(4);
// 执行测试
Map<String, Object> protocolInfo = trueMCPServer.getProtocolInfo();
// 验证结果
assertNotNull(protocolInfo);
assertEquals("Model Context Protocol", protocolInfo.get("protocol_name"));
assertEquals("1.0", protocolInfo.get("protocol_version"));
assertEquals("2.0", protocolInfo.get("jsonrpc_version"));
assertEquals("youfool-devops-mcp-server", protocolInfo.get("implementation"));
assertEquals(4, protocolInfo.get("tool_count"));
@SuppressWarnings("unchecked")
List<String> supportedMethods = (List<String>) protocolInfo.get("supported_methods");
assertTrue(supportedMethods.contains("tools/list"));
assertTrue(supportedMethods.contains("tools/call"));
@SuppressWarnings("unchecked")
Map<String, Object> capabilities = (Map<String, Object>) protocolInfo.get("server_capabilities");
assertEquals(true, capabilities.get("tools"));
assertEquals(false, capabilities.get("resources"));
}
@Test
void testNotificationRequest() {
// 准备通知请求id为null
request.setId(null);
request.setMethod("tools/list");
when(toolRegistry.getAllTools()).thenReturn(List.of(testTool));
// 执行测试
MCPJsonRpcResponse response = trueMCPServer.toolsList(request);
// 验证结果 - 通知请求也应该返回响应(在我们的实现中)
assertNotNull(response);
assertNull(response.getId()); // 通知的响应ID应该为null
assertTrue(response.isSuccess());
}
@Test
void testExceptionHandling() {
// 准备数据
request.setMethod("tools/list");
// 模拟异常
when(toolRegistry.getAllTools()).thenThrow(new RuntimeException("数据库连接失败"));
// 执行测试
MCPJsonRpcResponse response = trueMCPServer.toolsList(request);
// 验证结果
assertNotNull(response);
assertTrue(response.isError());
assertEquals(MCPError.INTERNAL_ERROR, response.getError().getCode());
assertTrue(response.getError().getMessage().contains("数据库连接失败"));
}
}