diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPFunctionBridge.java b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPFunctionBridge.java new file mode 100644 index 0000000..83369ac --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPFunctionBridge.java @@ -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> convertToOpenAIFunctions(List 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> convertToAnthropicTools(List tools) { + log.debug("转换{}个MCP工具为Anthropic Tool格式", tools.size()); + + return tools.stream() + .map(this::convertSingleToolToAnthropicTool) + .collect(Collectors.toList()); + } + + /** + * 将 MCP 工具定义转换为通用函数格式 + * + * @param tools MCP工具定义列表 + * @return 通用函数格式的工具定义 + */ + public List> convertToGenericFunctions(List 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 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 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 convertToOpenAIFunctionResult(MCPToolCallResponse mcpResponse, String functionName) { + Map 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 convertToAnthropicToolResult(MCPToolCallResponse mcpResponse, String toolName) { + Map 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> convertAllToolsToFormat(String format) { + List 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 arguments) { + Optional 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 convertSingleToolToOpenAIFunction(MCPToolDefinition tool) { + Map 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 convertSingleToolToAnthropicTool(MCPToolDefinition tool) { + Map 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 convertSingleToolToGenericFunction(MCPToolDefinition tool) { + Map 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 convertSchemaToOpenAIFormat(MCPToolDefinition.JsonSchema schema) { + Map 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 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; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPToolRegistry.java b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPToolRegistry.java new file mode 100644 index 0000000..6bfd7c0 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/MCPToolRegistry.java @@ -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 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 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 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 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 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 getAllTools() { + return new ArrayList<>(tools.values()); + } + + /** + * 根据名称获取工具定义 + */ + public Optional getTool(String name) { + return Optional.ofNullable(tools.get(name)); + } + + /** + * 根据类型过滤工具 + */ + public List getToolsByType(String type) { + return tools.values().stream() + .filter(tool -> type.equals(tool.getType())) + .toList(); + } + + /** + * 根据标签过滤工具 + */ + public List getToolsByTag(String tag) { + return tools.values().stream() + .filter(tool -> tool.getTags() != null && tool.getTags().contains(tag)) + .toList(); + } + + /** + * 分页获取工具 + */ + public List getToolsPaginated(int offset, int limit) { + List 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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/mcp/TrueMCPServer.java b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/TrueMCPServer.java new file mode 100644 index 0000000..eefda73 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/TrueMCPServer.java @@ -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 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 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 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 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> batchParams = (List>) request.getParams(); + + if (batchParams.isEmpty() || batchParams.size() > 10) { + return MCPJsonRpcResponse.error(request.getId(), + MCPError.invalidParams("批量调用数量必须在1-10之间")); + } + + // 执行批量调用 + List 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 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()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPError.java b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPError.java new file mode 100644 index 0000000..3a06332 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPError.java @@ -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"); + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPJsonRpcRequest.java b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPJsonRpcRequest.java new file mode 100644 index 0000000..fd3f0f4 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPJsonRpcRequest.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPJsonRpcResponse.java b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPJsonRpcResponse.java new file mode 100644 index 0000000..180dc29 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPJsonRpcResponse.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPToolCallRequest.java b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPToolCallRequest.java new file mode 100644 index 0000000..98eaa2d --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPToolCallRequest.java @@ -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 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); + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPToolCallResponse.java b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPToolCallResponse.java new file mode 100644 index 0000000..61f3980 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPToolCallResponse.java @@ -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 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 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 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; + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPToolDefinition.java b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPToolDefinition.java new file mode 100644 index 0000000..ef5d731 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPToolDefinition.java @@ -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 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 properties; + + /** + * 必需的属性列表 + */ + @JsonProperty("required") + @Schema(description = "必需的属性列表") + private List 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 enumValues; + + /** + * 示例值 + */ + @JsonProperty("examples") + @Schema(description = "示例值") + private List 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; + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPToolListRequest.java b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPToolListRequest.java new file mode 100644 index 0000000..1f00b64 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPToolListRequest.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPToolListResponse.java b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPToolListResponse.java new file mode 100644 index 0000000..6524827 --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/mcp/dto/MCPToolListResponse.java @@ -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 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 tools) { + MCPToolListResponse response = new MCPToolListResponse(); + response.setTools(tools); + response.setTotalCount(tools.size()); + response.setHasMore(false); + return response; + } + + /** + * 创建分页的工具列表响应 + */ + public static MCPToolListResponse of(List 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; + } +} \ No newline at end of file diff --git a/src/test/java/com/chinaweal/youfool/devops/ai/mcp/MCPFunctionBridgeTest.java b/src/test/java/com/chinaweal/youfool/devops/ai/mcp/MCPFunctionBridgeTest.java new file mode 100644 index 0000000..1e50fcb --- /dev/null +++ b/src/test/java/com/chinaweal/youfool/devops/ai/mcp/MCPFunctionBridgeTest.java @@ -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 tools = List.of(testTool); + + // 执行测试 + List> openAIFunctions = functionBridge.convertToOpenAIFunctions(tools); + + // 验证结果 + assertNotNull(openAIFunctions); + assertEquals(1, openAIFunctions.size()); + + Map functionDef = openAIFunctions.get(0); + assertEquals("function", functionDef.get("type")); + assertTrue(functionDef.containsKey("function")); + + @SuppressWarnings("unchecked") + Map function = (Map) functionDef.get("function"); + assertEquals("test_tool", function.get("name")); + assertEquals("测试工具描述", function.get("description")); + assertTrue(function.containsKey("parameters")); + + @SuppressWarnings("unchecked") + Map parameters = (Map) function.get("parameters"); + assertEquals("object", parameters.get("type")); + assertTrue(parameters.containsKey("properties")); + assertTrue(parameters.containsKey("required")); + } + + @Test + void testConvertToAnthropicTools() { + // 准备数据 + List tools = List.of(testTool); + + // 执行测试 + List> anthropicTools = functionBridge.convertToAnthropicTools(tools); + + // 验证结果 + assertNotNull(anthropicTools); + assertEquals(1, anthropicTools.size()); + + Map tool = anthropicTools.get(0); + assertEquals("test_tool", tool.get("name")); + assertEquals("测试工具描述", tool.get("description")); + assertTrue(tool.containsKey("input_schema")); + + @SuppressWarnings("unchecked") + Map inputSchema = (Map) tool.get("input_schema"); + assertEquals("object", inputSchema.get("type")); + assertTrue(inputSchema.containsKey("properties")); + assertTrue(inputSchema.containsKey("required")); + } + + @Test + void testConvertToGenericFunctions() { + // 准备数据 + List tools = List.of(testTool); + when(objectMapper.convertValue(any(), eq(Map.class))).thenReturn(Map.of("type", "object")); + + // 执行测试 + List> genericFunctions = functionBridge.convertToGenericFunctions(tools); + + // 验证结果 + assertNotNull(genericFunctions); + assertEquals(1, genericFunctions.size()); + + Map 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 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 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 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 data = (Map) response.getContent().get(0).getData(); + assertEquals("处理完成", data.get("data")); + } + + @Test + void testConvertToOpenAIFunctionResult() { + // 执行测试 + Map 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 content = (Map) result.get("content"); + assertEquals("success", content.get("result")); + } + + @Test + void testConvertToOpenAIFunctionResult_WithError() { + // 准备错误响应 + MCPToolCallResponse errorResponse = MCPToolCallResponse.error("执行失败", "error-call-456"); + + // 执行测试 + Map 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 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 content = (Map) result.get("content"); + assertEquals("success", content.get("result")); + } + + @Test + void testConvertToAnthropicToolResult_WithError() { + // 准备错误响应 + MCPToolCallResponse errorResponse = MCPToolCallResponse.error("执行失败", "error-call-456"); + + // 执行测试 + Map 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> 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> 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> 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> 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 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 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 result = functionBridge.convertToOpenAIFunctionResult(response, "test_tool"); + + // 验证结果 - 多个内容项应该返回完整列表 + assertNotNull(result); + assertEquals("success", result.get("status")); + + @SuppressWarnings("unchecked") + List content = (List) result.get("content"); + assertEquals(2, content.size()); + } + + @Test + void testConversionWithNullValues() { + // 创建包含null值的响应 + MCPToolCallResponse response = MCPToolCallResponse.success(null); + + // 执行转换 + Map openAIResult = functionBridge.convertToOpenAIFunctionResult(response, "test_tool"); + Map anthropicResult = functionBridge.convertToAnthropicToolResult(response, "test_tool"); + + // 验证结果 + assertNotNull(openAIResult); + assertNotNull(anthropicResult); + assertEquals("success", openAIResult.get("status")); + assertEquals(false, anthropicResult.get("is_error")); + } +} \ No newline at end of file diff --git a/src/test/java/com/chinaweal/youfool/devops/ai/mcp/MCPToolRegistryTest.java b/src/test/java/com/chinaweal/youfool/devops/ai/mcp/MCPToolRegistryTest.java new file mode 100644 index 0000000..be395c1 --- /dev/null +++ b/src/test/java/com/chinaweal/youfool/devops/ai/mcp/MCPToolRegistryTest.java @@ -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 tools = toolRegistry.getAllTools(); + + // 验证结果 + assertNotNull(tools); + assertFalse(tools.isEmpty()); + assertEquals(4, tools.size()); // 应该有4个预定义工具 + + // 验证工具名称 + List 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 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 tool = toolRegistry.getTool("non_existent_tool"); + + // 验证结果 + assertFalse(tool.isPresent()); + } + + @Test + void testGetToolsByType() { + // 执行测试 + List 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 resourceTools = toolRegistry.getToolsByType("resource"); + + // 验证结果 + assertNotNull(resourceTools); + assertTrue(resourceTools.isEmpty()); + } + + @Test + void testGetToolsByTag() { + // 执行测试 + List 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 unknownTagTools = toolRegistry.getToolsByTag("unknown_tag"); + + // 验证结果 + assertNotNull(unknownTagTools); + assertTrue(unknownTagTools.isEmpty()); + } + + @Test + void testGetToolsPaginated() { + // 执行测试 - 第一页,每页2个 + List page1 = toolRegistry.getToolsPaginated(0, 2); + + // 验证结果 + assertNotNull(page1); + assertEquals(2, page1.size()); + + // 执行测试 - 第二页,每页2个 + List 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 tools = toolRegistry.getToolsPaginated(100, 10); + + // 验证结果 + assertNotNull(tools); + assertTrue(tools.isEmpty()); + } + + @Test + void testGetToolsPaginated_LimitBeyondAvailable() { + // 执行测试 - 限制数量超出可用数量 + List 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 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 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 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 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()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/chinaweal/youfool/devops/ai/mcp/TrueMCPServerTest.java b/src/test/java/com/chinaweal/youfool/devops/ai/mcp/TrueMCPServerTest.java new file mode 100644 index 0000000..13c1f9c --- /dev/null +++ b/src/test/java/com/chinaweal/youfool/devops/ai/mcp/TrueMCPServerTest.java @@ -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 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 params = Map.of( + "limit", 5, + "offset", 0 + ); + request.setParams(params); + + MCPToolListRequest listRequest = new MCPToolListRequest(); + listRequest.setLimit(5); + listRequest.setOffset(0); + + List 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 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 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 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> 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 result = (Map) 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> batchParams = List.of(); + for (int i = 0; i < 15; i++) { // 超过限制的10个 + batchParams = new java.util.ArrayList<>(batchParams); + ((java.util.ArrayList>) 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 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 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 supportedMethods = (List) protocolInfo.get("supported_methods"); + assertTrue(supportedMethods.contains("tools/list")); + assertTrue(supportedMethods.contains("tools/call")); + + @SuppressWarnings("unchecked") + Map capabilities = (Map) 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("数据库连接失败")); + } +} \ No newline at end of file