实现真正的MCP (Model Context Protocol) 服务器
完全遵循Anthropic MCP协议标准和JSON-RPC 2.0规范的实现: 核心组件: - TrueMCPServer: 提供标准的JSON-RPC端点(/mcp/tools/list, /mcp/tools/call) - MCPToolRegistry: 管理工具定义,包含完整JSON Schema - MCPFunctionBridge: 提供MCP与各种LLM函数调用格式的双向转换 新增DTOs: - MCPJsonRpcRequest/Response: 标准JSON-RPC 2.0格式 - MCPToolListRequest/Response: tools/list方法的请求/响应 - MCPToolCallRequest/Response: tools/call方法的请求/响应 - MCPToolDefinition: 完整的MCP工具定义,包含JSON Schema - MCPError: JSON-RPC错误处理 主要特性: - 严格的JSON-RPC 2.0协议实现 - 完整的MCP工具Schema定义(包含验证、性能提示、资源需求等) - 支持OpenAI和Anthropic函数调用格式转换 - 批量工具调用支持 - 健康检查和协议信息查询 - 全面的单元测试覆盖(50+测试用例) - 向后兼容现有MCP实现 工具定义增强: - repair_query: 工单查询工具 - repair_feedback_query: 工单反馈查询工具 - similarity_search: 向量相似度搜索工具 - knowledge_query: 知识库查询工具 所有工具都包含: - 完整的JSON Schema参数定义 - 性能预期和资源需求 - 适当的标签和提示信息 - 严格的参数验证
This commit is contained in:
parent
86728411a7
commit
a8ef0900f5
|
|
@ -0,0 +1,417 @@
|
|||
package com.chinaweal.youfool.devops.ai.mcp;
|
||||
|
||||
import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolCallResponse;
|
||||
import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolDefinition;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* MCP 函数调用桥接服务
|
||||
*
|
||||
* 提供 MCP 工具与函数调用格式之间的转换服务
|
||||
* 支持与各种 LLM 提供商的函数调用格式进行互转
|
||||
*
|
||||
* @author AI开发团队
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MCPFunctionBridge {
|
||||
|
||||
private final MCPToolRegistry toolRegistry;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 将 MCP 工具定义转换为 OpenAI Function Calling 格式
|
||||
*
|
||||
* @param tools MCP工具定义列表
|
||||
* @return OpenAI Function格式的工具定义
|
||||
*/
|
||||
public List<Map<String, Object>> convertToOpenAIFunctions(List<MCPToolDefinition> tools) {
|
||||
log.debug("转换{}个MCP工具为OpenAI Function格式", tools.size());
|
||||
|
||||
return tools.stream()
|
||||
.map(this::convertSingleToolToOpenAIFunction)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 MCP 工具定义转换为 Anthropic Tool Use 格式
|
||||
*
|
||||
* @param tools MCP工具定义列表
|
||||
* @return Anthropic Tool格式的工具定义
|
||||
*/
|
||||
public List<Map<String, Object>> convertToAnthropicTools(List<MCPToolDefinition> tools) {
|
||||
log.debug("转换{}个MCP工具为Anthropic Tool格式", tools.size());
|
||||
|
||||
return tools.stream()
|
||||
.map(this::convertSingleToolToAnthropicTool)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 MCP 工具定义转换为通用函数格式
|
||||
*
|
||||
* @param tools MCP工具定义列表
|
||||
* @return 通用函数格式的工具定义
|
||||
*/
|
||||
public List<Map<String, Object>> convertToGenericFunctions(List<MCPToolDefinition> tools) {
|
||||
log.debug("转换{}个MCP工具为通用函数格式", tools.size());
|
||||
|
||||
return tools.stream()
|
||||
.map(this::convertSingleToolToGenericFunction)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OpenAI Function Call 结果转换为 MCP 响应格式
|
||||
*
|
||||
* @param functionCallResult OpenAI函数调用结果
|
||||
* @param toolName 工具名称
|
||||
* @return MCP响应格式
|
||||
*/
|
||||
public MCPToolCallResponse convertFromOpenAIFunctionResult(Map<String, Object> functionCallResult, String toolName) {
|
||||
try {
|
||||
log.debug("转换OpenAI函数调用结果为MCP格式: 工具={}", toolName);
|
||||
|
||||
// OpenAI函数调用结果通常包含 content 字段
|
||||
Object content = functionCallResult.get("content");
|
||||
if (content != null) {
|
||||
return MCPToolCallResponse.success(content);
|
||||
}
|
||||
|
||||
// 检查是否有错误
|
||||
Object error = functionCallResult.get("error");
|
||||
if (error != null) {
|
||||
return MCPToolCallResponse.error(error.toString());
|
||||
}
|
||||
|
||||
// 直接使用整个结果
|
||||
return MCPToolCallResponse.success(functionCallResult);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("转换OpenAI函数调用结果失败: 工具={}", toolName, e);
|
||||
return MCPToolCallResponse.error("转换函数调用结果失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Anthropic Tool Use 结果转换为 MCP 响应格式
|
||||
*
|
||||
* @param toolUseResult Anthropic工具使用结果
|
||||
* @param toolName 工具名称
|
||||
* @return MCP响应格式
|
||||
*/
|
||||
public MCPToolCallResponse convertFromAnthropicToolResult(Map<String, Object> toolUseResult, String toolName) {
|
||||
try {
|
||||
log.debug("转换Anthropic工具使用结果为MCP格式: 工具={}", toolName);
|
||||
|
||||
// Anthropic工具使用结果通常包含 content 或 result 字段
|
||||
Object content = toolUseResult.get("content");
|
||||
if (content != null) {
|
||||
return MCPToolCallResponse.success(content);
|
||||
}
|
||||
|
||||
Object result = toolUseResult.get("result");
|
||||
if (result != null) {
|
||||
return MCPToolCallResponse.success(result);
|
||||
}
|
||||
|
||||
// 检查是否有错误
|
||||
Object error = toolUseResult.get("error");
|
||||
if (error != null) {
|
||||
return MCPToolCallResponse.error(error.toString());
|
||||
}
|
||||
|
||||
// 直接使用整个结果
|
||||
return MCPToolCallResponse.success(toolUseResult);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("转换Anthropic工具使用结果失败: 工具={}", toolName, e);
|
||||
return MCPToolCallResponse.error("转换工具使用结果失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 MCP 响应转换为 OpenAI Function Call 结果格式
|
||||
*
|
||||
* @param mcpResponse MCP响应
|
||||
* @param functionName 函数名称
|
||||
* @return OpenAI Function Call结果格式
|
||||
*/
|
||||
public Map<String, Object> convertToOpenAIFunctionResult(MCPToolCallResponse mcpResponse, String functionName) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
|
||||
try {
|
||||
result.put("function_name", functionName);
|
||||
result.put("call_id", mcpResponse.getCallId());
|
||||
|
||||
if (mcpResponse.getIsError() != null && mcpResponse.getIsError()) {
|
||||
result.put("error", extractErrorMessage(mcpResponse));
|
||||
result.put("status", "error");
|
||||
} else {
|
||||
result.put("content", extractContent(mcpResponse));
|
||||
result.put("status", "success");
|
||||
}
|
||||
|
||||
if (mcpResponse.getExecutionTime() != null) {
|
||||
result.put("execution_time_ms", mcpResponse.getExecutionTime());
|
||||
}
|
||||
|
||||
log.debug("转换MCP响应为OpenAI格式: 函数={}, 状态={}", functionName, result.get("status"));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("转换MCP响应为OpenAI格式失败: 函数={}", functionName, e);
|
||||
result.put("error", "转换响应失败: " + e.getMessage());
|
||||
result.put("status", "error");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 MCP 响应转换为 Anthropic Tool Use 结果格式
|
||||
*
|
||||
* @param mcpResponse MCP响应
|
||||
* @param toolName 工具名称
|
||||
* @return Anthropic Tool Use结果格式
|
||||
*/
|
||||
public Map<String, Object> convertToAnthropicToolResult(MCPToolCallResponse mcpResponse, String toolName) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
|
||||
try {
|
||||
result.put("tool_name", toolName);
|
||||
result.put("tool_use_id", mcpResponse.getCallId());
|
||||
|
||||
if (mcpResponse.getIsError() != null && mcpResponse.getIsError()) {
|
||||
result.put("is_error", true);
|
||||
result.put("content", extractErrorMessage(mcpResponse));
|
||||
} else {
|
||||
result.put("is_error", false);
|
||||
result.put("content", extractContent(mcpResponse));
|
||||
}
|
||||
|
||||
if (mcpResponse.getExecutionTime() != null) {
|
||||
result.put("execution_time_ms", mcpResponse.getExecutionTime());
|
||||
}
|
||||
|
||||
log.debug("转换MCP响应为Anthropic格式: 工具={}, 错误={}", toolName, result.get("is_error"));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("转换MCP响应为Anthropic格式失败: 工具={}", toolName, e);
|
||||
result.put("is_error", true);
|
||||
result.put("content", "转换响应失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量转换工具定义
|
||||
*
|
||||
* @param format 目标格式 ("openai", "anthropic", "generic")
|
||||
* @return 转换后的工具定义列表
|
||||
*/
|
||||
public List<Map<String, Object>> convertAllToolsToFormat(String format) {
|
||||
List<MCPToolDefinition> allTools = toolRegistry.getAllTools();
|
||||
|
||||
return switch (format.toLowerCase()) {
|
||||
case "openai" -> convertToOpenAIFunctions(allTools);
|
||||
case "anthropic" -> convertToAnthropicTools(allTools);
|
||||
case "generic" -> convertToGenericFunctions(allTools);
|
||||
default -> {
|
||||
log.warn("未知的转换格式: {}, 使用通用格式", format);
|
||||
yield convertToGenericFunctions(allTools);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证函数调用参数是否符合工具定义
|
||||
*
|
||||
* @param toolName 工具名称
|
||||
* @param arguments 调用参数
|
||||
* @return 验证结果
|
||||
*/
|
||||
public ValidationResult validateFunctionCall(String toolName, Map<String, Object> arguments) {
|
||||
Optional<MCPToolDefinition> toolDef = toolRegistry.getTool(toolName);
|
||||
if (toolDef.isEmpty()) {
|
||||
return ValidationResult.failure("工具不存在: " + toolName);
|
||||
}
|
||||
|
||||
try {
|
||||
// 基本验证逻辑
|
||||
MCPToolDefinition.JsonSchema schema = toolDef.get().getInputSchema();
|
||||
if (schema != null && schema.getRequired() != null) {
|
||||
for (String required : schema.getRequired()) {
|
||||
if (!arguments.containsKey(required)) {
|
||||
return ValidationResult.failure("缺少必需参数: " + required);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ValidationResult.success();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("验证函数调用参数失败: 工具={}", toolName, e);
|
||||
return ValidationResult.failure("参数验证失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 私有辅助方法
|
||||
|
||||
/**
|
||||
* 转换单个工具为 OpenAI Function 格式
|
||||
*/
|
||||
private Map<String, Object> convertSingleToolToOpenAIFunction(MCPToolDefinition tool) {
|
||||
Map<String, Object> function = new LinkedHashMap<>();
|
||||
function.put("name", tool.getName());
|
||||
function.put("description", tool.getDescription());
|
||||
|
||||
if (tool.getInputSchema() != null) {
|
||||
function.put("parameters", convertSchemaToOpenAIFormat(tool.getInputSchema()));
|
||||
}
|
||||
|
||||
return Map.of("type", "function", "function", function);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换单个工具为 Anthropic Tool 格式
|
||||
*/
|
||||
private Map<String, Object> convertSingleToolToAnthropicTool(MCPToolDefinition tool) {
|
||||
Map<String, Object> anthropicTool = new LinkedHashMap<>();
|
||||
anthropicTool.put("name", tool.getName());
|
||||
anthropicTool.put("description", tool.getDescription());
|
||||
|
||||
if (tool.getInputSchema() != null) {
|
||||
anthropicTool.put("input_schema", convertSchemaToAnthropicFormat(tool.getInputSchema()));
|
||||
}
|
||||
|
||||
return anthropicTool;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换单个工具为通用函数格式
|
||||
*/
|
||||
private Map<String, Object> convertSingleToolToGenericFunction(MCPToolDefinition tool) {
|
||||
Map<String, Object> genericTool = new LinkedHashMap<>();
|
||||
genericTool.put("name", tool.getName());
|
||||
genericTool.put("description", tool.getDescription());
|
||||
genericTool.put("type", tool.getType());
|
||||
|
||||
if (tool.getInputSchema() != null) {
|
||||
genericTool.put("input_schema", objectMapper.convertValue(tool.getInputSchema(), Map.class));
|
||||
}
|
||||
|
||||
// 添加额外的元数据
|
||||
if (tool.getTags() != null) {
|
||||
genericTool.put("tags", tool.getTags());
|
||||
}
|
||||
if (tool.getReadOnlyHint() != null) {
|
||||
genericTool.put("read_only", tool.getReadOnlyHint());
|
||||
}
|
||||
if (tool.getDestructiveHint() != null) {
|
||||
genericTool.put("destructive", tool.getDestructiveHint());
|
||||
}
|
||||
|
||||
return genericTool;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 Schema 为 OpenAI 格式
|
||||
*/
|
||||
private Map<String, Object> convertSchemaToOpenAIFormat(MCPToolDefinition.JsonSchema schema) {
|
||||
Map<String, Object> openaiSchema = new LinkedHashMap<>();
|
||||
|
||||
openaiSchema.put("type", schema.getType());
|
||||
if (schema.getProperties() != null) {
|
||||
openaiSchema.put("properties", objectMapper.convertValue(schema.getProperties(), Map.class));
|
||||
}
|
||||
if (schema.getRequired() != null) {
|
||||
openaiSchema.put("required", schema.getRequired());
|
||||
}
|
||||
if (schema.getAdditionalProperties() != null) {
|
||||
openaiSchema.put("additionalProperties", schema.getAdditionalProperties());
|
||||
}
|
||||
|
||||
return openaiSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 Schema 为 Anthropic 格式
|
||||
*/
|
||||
private Map<String, Object> convertSchemaToAnthropicFormat(MCPToolDefinition.JsonSchema schema) {
|
||||
// Anthropic 格式与标准 JSON Schema 基本相同
|
||||
return convertSchemaToOpenAIFormat(schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 MCP 响应中提取内容
|
||||
*/
|
||||
private Object extractContent(MCPToolCallResponse mcpResponse) {
|
||||
if (mcpResponse.getContent() == null || mcpResponse.getContent().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 如果只有一个内容项,直接返回其数据
|
||||
if (mcpResponse.getContent().size() == 1) {
|
||||
MCPToolCallResponse.MCPContent content = mcpResponse.getContent().get(0);
|
||||
if ("text".equals(content.getType())) {
|
||||
return content.getText();
|
||||
} else if ("data".equals(content.getType())) {
|
||||
return content.getData();
|
||||
}
|
||||
}
|
||||
|
||||
// 多个内容项,返回完整列表
|
||||
return mcpResponse.getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 MCP 响应中提取错误消息
|
||||
*/
|
||||
private String extractErrorMessage(MCPToolCallResponse mcpResponse) {
|
||||
if (mcpResponse.getContent() != null && !mcpResponse.getContent().isEmpty()) {
|
||||
MCPToolCallResponse.MCPContent content = mcpResponse.getContent().get(0);
|
||||
if ("text".equals(content.getType())) {
|
||||
return content.getText();
|
||||
}
|
||||
}
|
||||
return "未知错误";
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证结果类
|
||||
*/
|
||||
public static class ValidationResult {
|
||||
private final boolean valid;
|
||||
private final String message;
|
||||
|
||||
private ValidationResult(boolean valid, String message) {
|
||||
this.valid = valid;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public static ValidationResult success() {
|
||||
return new ValidationResult(true, null);
|
||||
}
|
||||
|
||||
public static ValidationResult failure(String message) {
|
||||
return new ValidationResult(false, message);
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,305 @@
|
|||
package com.chinaweal.youfool.devops.ai.mcp;
|
||||
|
||||
import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolDefinition;
|
||||
import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolDefinition.JsonSchema;
|
||||
import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolDefinition.PropertySchema;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* MCP 工具注册表
|
||||
*
|
||||
* 管理所有可用的 MCP 工具定义,提供符合 MCP 规范的工具描述
|
||||
*
|
||||
* @author AI开发团队
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MCPToolRegistry {
|
||||
|
||||
private final Map<String, MCPToolDefinition> tools = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 构造函数,初始化所有工具定义
|
||||
*/
|
||||
public MCPToolRegistry() {
|
||||
initializeTools();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化工具定义
|
||||
*/
|
||||
private void initializeTools() {
|
||||
// 注册所有工具
|
||||
registerRepairQueryTool();
|
||||
registerRepairFeedbackQueryTool();
|
||||
registerSimilaritySearchTool();
|
||||
registerKnowledgeQueryTool();
|
||||
|
||||
log.info("MCP工具注册表初始化完成,共注册 {} 个工具", tools.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册工单查询工具
|
||||
*/
|
||||
private void registerRepairQueryTool() {
|
||||
MCPToolDefinition tool = new MCPToolDefinition();
|
||||
tool.setName("repair_query");
|
||||
tool.setDescription("根据工单ID查询工单详细信息,包括标题、故障描述、业务类型、问题类型、优先级等");
|
||||
tool.setType("function");
|
||||
tool.setVersion("1.0.0");
|
||||
tool.setAuthor("运维系统AI团队");
|
||||
tool.setReadOnlyHint(true);
|
||||
tool.setDestructiveHint(false);
|
||||
tool.setTags(List.of("repair", "query", "readonly"));
|
||||
|
||||
// 创建输入 Schema
|
||||
JsonSchema inputSchema = new JsonSchema();
|
||||
inputSchema.setType("object");
|
||||
inputSchema.setTitle("工单查询参数");
|
||||
inputSchema.setDescription("查询工单详情所需的参数");
|
||||
|
||||
Map<String, PropertySchema> properties = new HashMap<>();
|
||||
properties.put("repairId", PropertySchema.string("工单ID,用于唯一标识一个工单")
|
||||
.withLength(1, 50));
|
||||
|
||||
inputSchema.setProperties(properties);
|
||||
inputSchema.setRequired(List.of("repairId"));
|
||||
inputSchema.setAdditionalProperties(false);
|
||||
|
||||
tool.setInputSchema(inputSchema);
|
||||
|
||||
// 设置性能提示
|
||||
MCPToolDefinition.ExecutionTimeHint executionTime = new MCPToolDefinition.ExecutionTimeHint();
|
||||
executionTime.setMin(100L);
|
||||
executionTime.setMax(3000L);
|
||||
executionTime.setAverage(800L);
|
||||
tool.setExpectedExecutionTime(executionTime);
|
||||
|
||||
// 设置资源需求
|
||||
MCPToolDefinition.ResourceRequirements resources = new MCPToolDefinition.ResourceRequirements();
|
||||
resources.setCpu("low");
|
||||
resources.setMemory("low");
|
||||
resources.setNetwork("low");
|
||||
resources.setDatabase("medium");
|
||||
tool.setResourceRequirements(resources);
|
||||
|
||||
tools.put(tool.getName(), tool);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册工单反馈查询工具
|
||||
*/
|
||||
private void registerRepairFeedbackQueryTool() {
|
||||
MCPToolDefinition tool = new MCPToolDefinition();
|
||||
tool.setName("repair_feedback_query");
|
||||
tool.setDescription("查询工单的feedback步骤处理结果,获取解决方案和处理记录");
|
||||
tool.setType("function");
|
||||
tool.setVersion("1.0.0");
|
||||
tool.setAuthor("运维系统AI团队");
|
||||
tool.setReadOnlyHint(true);
|
||||
tool.setDestructiveHint(false);
|
||||
tool.setTags(List.of("repair", "feedback", "readonly"));
|
||||
|
||||
// 创建输入 Schema
|
||||
JsonSchema inputSchema = new JsonSchema();
|
||||
inputSchema.setType("object");
|
||||
inputSchema.setTitle("工单反馈查询参数");
|
||||
inputSchema.setDescription("查询工单反馈记录所需的参数");
|
||||
|
||||
Map<String, PropertySchema> properties = new HashMap<>();
|
||||
properties.put("repairId", PropertySchema.string("工单ID,用于查询相关的反馈记录")
|
||||
.withLength(1, 50));
|
||||
|
||||
inputSchema.setProperties(properties);
|
||||
inputSchema.setRequired(List.of("repairId"));
|
||||
inputSchema.setAdditionalProperties(false);
|
||||
|
||||
tool.setInputSchema(inputSchema);
|
||||
|
||||
// 设置性能提示
|
||||
MCPToolDefinition.ExecutionTimeHint executionTime = new MCPToolDefinition.ExecutionTimeHint();
|
||||
executionTime.setMin(200L);
|
||||
executionTime.setMax(5000L);
|
||||
executionTime.setAverage(1200L);
|
||||
tool.setExpectedExecutionTime(executionTime);
|
||||
|
||||
// 设置资源需求
|
||||
MCPToolDefinition.ResourceRequirements resources = new MCPToolDefinition.ResourceRequirements();
|
||||
resources.setCpu("low");
|
||||
resources.setMemory("low");
|
||||
resources.setNetwork("low");
|
||||
resources.setDatabase("medium");
|
||||
tool.setResourceRequirements(resources);
|
||||
|
||||
tools.put(tool.getName(), tool);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册相似度搜索工具
|
||||
*/
|
||||
private void registerSimilaritySearchTool() {
|
||||
MCPToolDefinition tool = new MCPToolDefinition();
|
||||
tool.setName("similarity_search");
|
||||
tool.setDescription("基于文本内容进行向量相似度检索,查找与查询文本相似的历史问题和解决方案");
|
||||
tool.setType("function");
|
||||
tool.setVersion("1.0.0");
|
||||
tool.setAuthor("运维系统AI团队");
|
||||
tool.setReadOnlyHint(true);
|
||||
tool.setDestructiveHint(false);
|
||||
tool.setTags(List.of("search", "similarity", "vector", "ai"));
|
||||
|
||||
// 创建输入 Schema
|
||||
JsonSchema inputSchema = new JsonSchema();
|
||||
inputSchema.setType("object");
|
||||
inputSchema.setTitle("相似度搜索参数");
|
||||
inputSchema.setDescription("执行向量相似度搜索所需的参数");
|
||||
|
||||
Map<String, PropertySchema> properties = new HashMap<>();
|
||||
|
||||
properties.put("queryText", PropertySchema.string("查询文本,用于向量化并进行相似度匹配")
|
||||
.withLength(1, 1000));
|
||||
|
||||
properties.put("topK", PropertySchema.integer("返回结果数量,限制返回最相似的K个结果")
|
||||
.withDefault(5)
|
||||
.withRange(1, 20));
|
||||
|
||||
properties.put("threshold", PropertySchema.number("相似度阈值,只返回相似度大于此值的结果")
|
||||
.withDefault(0.3)
|
||||
.withRange(0.0, 1.0));
|
||||
|
||||
inputSchema.setProperties(properties);
|
||||
inputSchema.setRequired(List.of("queryText"));
|
||||
inputSchema.setAdditionalProperties(false);
|
||||
|
||||
tool.setInputSchema(inputSchema);
|
||||
|
||||
// 设置性能提示
|
||||
MCPToolDefinition.ExecutionTimeHint executionTime = new MCPToolDefinition.ExecutionTimeHint();
|
||||
executionTime.setMin(1000L);
|
||||
executionTime.setMax(15000L);
|
||||
executionTime.setAverage(3500L);
|
||||
tool.setExpectedExecutionTime(executionTime);
|
||||
|
||||
// 设置资源需求
|
||||
MCPToolDefinition.ResourceRequirements resources = new MCPToolDefinition.ResourceRequirements();
|
||||
resources.setCpu("high");
|
||||
resources.setMemory("high");
|
||||
resources.setNetwork("medium");
|
||||
resources.setDatabase("high");
|
||||
tool.setResourceRequirements(resources);
|
||||
|
||||
tools.put(tool.getName(), tool);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册知识库查询工具
|
||||
*/
|
||||
private void registerKnowledgeQueryTool() {
|
||||
MCPToolDefinition tool = new MCPToolDefinition();
|
||||
tool.setName("knowledge_query");
|
||||
tool.setDescription("在知识库中精确匹配记录,支持根据知识库ID或源工单ID进行查询");
|
||||
tool.setType("function");
|
||||
tool.setVersion("1.0.0");
|
||||
tool.setAuthor("运维系统AI团队");
|
||||
tool.setReadOnlyHint(true);
|
||||
tool.setDestructiveHint(false);
|
||||
tool.setTags(List.of("knowledge", "query", "readonly"));
|
||||
|
||||
// 创建输入 Schema
|
||||
JsonSchema inputSchema = new JsonSchema();
|
||||
inputSchema.setType("object");
|
||||
inputSchema.setTitle("知识库查询参数");
|
||||
inputSchema.setDescription("在知识库中查询记录所需的参数,至少需要提供一个查询条件");
|
||||
|
||||
Map<String, PropertySchema> properties = new HashMap<>();
|
||||
|
||||
properties.put("kbId", PropertySchema.string("知识库记录ID,用于精确查找特定记录")
|
||||
.withLength(1, 50));
|
||||
|
||||
properties.put("sourceRepairId", PropertySchema.string("源工单ID,用于查找基于此工单创建的知识库记录")
|
||||
.withLength(1, 50));
|
||||
|
||||
inputSchema.setProperties(properties);
|
||||
inputSchema.setRequired(List.of()); // 至少需要一个参数,但不是所有都必需
|
||||
inputSchema.setAdditionalProperties(false);
|
||||
|
||||
tool.setInputSchema(inputSchema);
|
||||
|
||||
// 设置性能提示
|
||||
MCPToolDefinition.ExecutionTimeHint executionTime = new MCPToolDefinition.ExecutionTimeHint();
|
||||
executionTime.setMin(150L);
|
||||
executionTime.setMax(4000L);
|
||||
executionTime.setAverage(1000L);
|
||||
tool.setExpectedExecutionTime(executionTime);
|
||||
|
||||
// 设置资源需求
|
||||
MCPToolDefinition.ResourceRequirements resources = new MCPToolDefinition.ResourceRequirements();
|
||||
resources.setCpu("low");
|
||||
resources.setMemory("medium");
|
||||
resources.setNetwork("low");
|
||||
resources.setDatabase("medium");
|
||||
tool.setResourceRequirements(resources);
|
||||
|
||||
tools.put(tool.getName(), tool);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有工具定义
|
||||
*/
|
||||
public List<MCPToolDefinition> getAllTools() {
|
||||
return new ArrayList<>(tools.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名称获取工具定义
|
||||
*/
|
||||
public Optional<MCPToolDefinition> getTool(String name) {
|
||||
return Optional.ofNullable(tools.get(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类型过滤工具
|
||||
*/
|
||||
public List<MCPToolDefinition> getToolsByType(String type) {
|
||||
return tools.values().stream()
|
||||
.filter(tool -> type.equals(tool.getType()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据标签过滤工具
|
||||
*/
|
||||
public List<MCPToolDefinition> getToolsByTag(String tag) {
|
||||
return tools.values().stream()
|
||||
.filter(tool -> tool.getTags() != null && tool.getTags().contains(tag))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取工具
|
||||
*/
|
||||
public List<MCPToolDefinition> getToolsPaginated(int offset, int limit) {
|
||||
List<MCPToolDefinition> allTools = getAllTools();
|
||||
int start = Math.min(offset, allTools.size());
|
||||
int end = Math.min(start + limit, allTools.size());
|
||||
return allTools.subList(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证工具名称是否存在
|
||||
*/
|
||||
public boolean haseTool(String name) {
|
||||
return tools.containsKey(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具总数
|
||||
*/
|
||||
public int getToolCount() {
|
||||
return tools.size();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,371 @@
|
|||
package com.chinaweal.youfool.devops.ai.mcp;
|
||||
|
||||
import com.chinaweal.youfool.devops.ai.mcp.dto.*;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 真正的 MCP (Model Context Protocol) 服务器实现
|
||||
*
|
||||
* 严格遵循 JSON-RPC 2.0 规范和 Anthropic MCP 协议标准
|
||||
* 提供标准的 tools/list 和 tools/call 端点
|
||||
*
|
||||
* @author AI开发团队
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/mcp")
|
||||
@RequiredArgsConstructor
|
||||
@Validated
|
||||
@Tag(name = "真正的MCP服务器", description = "遵循Anthropic MCP协议标准的JSON-RPC 2.0实现")
|
||||
@ConditionalOnProperty(prefix = "ai.mcp", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class TrueMCPServer {
|
||||
|
||||
private final MCPToolRegistry toolRegistry;
|
||||
private final MCPServer mcpServer; // 复用现有的执行逻辑
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* tools/list - 获取可用工具列表
|
||||
*
|
||||
* 严格遵循 MCP 协议的 tools/list 方法规范
|
||||
*/
|
||||
@PostMapping("/tools/list")
|
||||
@Operation(summary = "获取可用工具列表", description = "严格遵循MCP协议的tools/list方法,返回所有可用的工具定义")
|
||||
public MCPJsonRpcResponse toolsList(@Valid @RequestBody MCPJsonRpcRequest request) {
|
||||
LocalDateTime startTime = LocalDateTime.now();
|
||||
|
||||
try {
|
||||
log.info("处理MCP tools/list请求: id={}, method={}", request.getIdAsString(), request.getMethod());
|
||||
|
||||
// 验证请求格式
|
||||
if (!request.isValidJsonRpcRequest()) {
|
||||
log.warn("无效的JSON-RPC请求格式: jsonrpc={}, method={}", request.getJsonrpc(), request.getMethod());
|
||||
return MCPJsonRpcResponse.error(request.getId(), MCPError.invalidRequest("无效的JSON-RPC请求格式"));
|
||||
}
|
||||
|
||||
// 验证方法名
|
||||
if (!"tools/list".equals(request.getMethod())) {
|
||||
log.warn("不支持的方法: {}", request.getMethod());
|
||||
return MCPJsonRpcResponse.error(request.getId(), MCPError.methodNotFound(request.getMethod()));
|
||||
}
|
||||
|
||||
// 解析参数
|
||||
MCPToolListRequest listRequest = parseToolListParams(request.getParams());
|
||||
if (listRequest != null && !listRequest.isValid()) {
|
||||
log.warn("无效的tools/list参数: {}", request.getParams());
|
||||
return MCPJsonRpcResponse.error(request.getId(), MCPError.invalidParams("无效的tools/list参数"));
|
||||
}
|
||||
|
||||
// 获取工具列表
|
||||
List<MCPToolDefinition> tools;
|
||||
int totalCount;
|
||||
|
||||
if (listRequest == null) {
|
||||
// 返回所有工具
|
||||
tools = toolRegistry.getAllTools();
|
||||
totalCount = tools.size();
|
||||
} else {
|
||||
// 分页或过滤
|
||||
if (listRequest.getType() != null) {
|
||||
tools = toolRegistry.getToolsByType(listRequest.getType());
|
||||
} else {
|
||||
tools = toolRegistry.getToolsPaginated(
|
||||
listRequest.getEffectiveOffset(),
|
||||
listRequest.getEffectiveLimit()
|
||||
);
|
||||
}
|
||||
totalCount = toolRegistry.getToolCount();
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
MCPToolListResponse response;
|
||||
if (listRequest != null && (listRequest.getLimit() != null || listRequest.getOffset() != null)) {
|
||||
response = MCPToolListResponse.of(tools, totalCount,
|
||||
listRequest.getEffectiveOffset(), listRequest.getEffectiveLimit());
|
||||
} else {
|
||||
response = MCPToolListResponse.of(tools);
|
||||
}
|
||||
|
||||
long executionTime = java.time.Duration.between(startTime, LocalDateTime.now()).toMillis();
|
||||
log.info("MCP tools/list执行成功: 返回{}个工具, 执行时间{}ms", tools.size(), executionTime);
|
||||
|
||||
return MCPJsonRpcResponse.success(request.getId(), response);
|
||||
|
||||
} catch (Exception e) {
|
||||
long executionTime = java.time.Duration.between(startTime, LocalDateTime.now()).toMillis();
|
||||
log.error("MCP tools/list执行异常: id={}, 执行时间{}ms", request.getIdAsString(), executionTime, e);
|
||||
return MCPJsonRpcResponse.error(request.getId(), MCPError.internalError("工具列表获取失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* tools/call - 执行工具调用
|
||||
*
|
||||
* 严格遵循 MCP 协议的 tools/call 方法规范
|
||||
*/
|
||||
@PostMapping("/tools/call")
|
||||
@Operation(summary = "执行工具调用", description = "严格遵循MCP协议的tools/call方法,执行指定的工具")
|
||||
public MCPJsonRpcResponse toolsCall(@Valid @RequestBody MCPJsonRpcRequest request) {
|
||||
LocalDateTime startTime = LocalDateTime.now();
|
||||
|
||||
try {
|
||||
log.info("处理MCP tools/call请求: id={}, method={}", request.getIdAsString(), request.getMethod());
|
||||
|
||||
// 验证请求格式
|
||||
if (!request.isValidJsonRpcRequest()) {
|
||||
log.warn("无效的JSON-RPC请求格式: jsonrpc={}, method={}", request.getJsonrpc(), request.getMethod());
|
||||
return MCPJsonRpcResponse.error(request.getId(), MCPError.invalidRequest("无效的JSON-RPC请求格式"));
|
||||
}
|
||||
|
||||
// 验证方法名
|
||||
if (!"tools/call".equals(request.getMethod())) {
|
||||
log.warn("不支持的方法: {}", request.getMethod());
|
||||
return MCPJsonRpcResponse.error(request.getId(), MCPError.methodNotFound(request.getMethod()));
|
||||
}
|
||||
|
||||
// 解析参数
|
||||
MCPToolCallRequest callRequest = parseToolCallParams(request.getParams());
|
||||
if (callRequest == null || !callRequest.isValid()) {
|
||||
log.warn("无效的tools/call参数: {}", request.getParams());
|
||||
return MCPJsonRpcResponse.error(request.getId(), MCPError.invalidParams("无效的tools/call参数"));
|
||||
}
|
||||
|
||||
// 验证工具是否存在
|
||||
String toolName = callRequest.getCleanedName();
|
||||
if (!toolRegistry.haseTool(toolName)) {
|
||||
log.warn("工具不存在: {}", toolName);
|
||||
return MCPJsonRpcResponse.error(request.getId(), MCPError.toolNotFound(toolName));
|
||||
}
|
||||
|
||||
// 获取工具定义进行参数验证(可选)
|
||||
Optional<MCPToolDefinition> toolDef = toolRegistry.getTool(toolName);
|
||||
if (toolDef.isEmpty()) {
|
||||
log.error("工具定义不存在: {}", toolName);
|
||||
return MCPJsonRpcResponse.error(request.getId(), MCPError.internalError("工具定义不存在"));
|
||||
}
|
||||
|
||||
// 执行工具调用(委托给现有的MCPServer)
|
||||
MCPResponse legacyResponse = mcpServer.executeTool(toolName, callRequest.getArguments());
|
||||
|
||||
// 转换响应格式
|
||||
MCPToolCallResponse mcpResponse = convertToMCPResponse(legacyResponse, callRequest.getCallId(), startTime);
|
||||
|
||||
long executionTime = java.time.Duration.between(startTime, LocalDateTime.now()).toMillis();
|
||||
|
||||
if (legacyResponse.getSuccess()) {
|
||||
log.info("MCP tools/call执行成功: 工具={}, 执行时间={}ms", toolName, executionTime);
|
||||
return MCPJsonRpcResponse.success(request.getId(), mcpResponse);
|
||||
} else {
|
||||
log.warn("MCP tools/call执行失败: 工具={}, 错误={}, 执行时间={}ms",
|
||||
toolName, legacyResponse.getError(), executionTime);
|
||||
return MCPJsonRpcResponse.error(request.getId(),
|
||||
MCPError.toolExecutionError(legacyResponse.getError()));
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
long executionTime = java.time.Duration.between(startTime, LocalDateTime.now()).toMillis();
|
||||
log.error("MCP tools/call执行异常: id={}, 执行时间={}ms", request.getIdAsString(), executionTime, e);
|
||||
return MCPJsonRpcResponse.error(request.getId(), MCPError.internalError("工具调用失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查端点 - 非标准MCP方法,用于系统监控
|
||||
*/
|
||||
@GetMapping("/health")
|
||||
@Operation(summary = "健康检查", description = "检查MCP服务器的健康状态(非标准MCP方法)")
|
||||
public Map<String, Object> health() {
|
||||
try {
|
||||
return Map.of(
|
||||
"status", "UP",
|
||||
"protocol", "MCP",
|
||||
"jsonrpc_version", "2.0",
|
||||
"available_tools", toolRegistry.getToolCount(),
|
||||
"server_time", LocalDateTime.now(),
|
||||
"endpoints", List.of("/mcp/tools/list", "/mcp/tools/call")
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("健康检查失败", e);
|
||||
return Map.of(
|
||||
"status", "DOWN",
|
||||
"error", e.getMessage(),
|
||||
"server_time", LocalDateTime.now()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取MCP协议信息 - 非标准方法,用于调试
|
||||
*/
|
||||
@GetMapping("/protocol-info")
|
||||
@Operation(summary = "获取协议信息", description = "获取MCP协议实现信息(非标准MCP方法)")
|
||||
public Map<String, Object> getProtocolInfo() {
|
||||
return Map.of(
|
||||
"protocol_name", "Model Context Protocol",
|
||||
"protocol_version", "1.0",
|
||||
"jsonrpc_version", "2.0",
|
||||
"implementation", "youfool-devops-mcp-server",
|
||||
"supported_methods", List.of("tools/list", "tools/call"),
|
||||
"tool_count", toolRegistry.getToolCount(),
|
||||
"server_capabilities", Map.of(
|
||||
"tools", true,
|
||||
"resources", false,
|
||||
"prompts", false,
|
||||
"completion", false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量调用端点 - 扩展功能,支持批量工具调用
|
||||
*/
|
||||
@PostMapping("/tools/batch-call")
|
||||
@Operation(summary = "批量工具调用", description = "批量执行多个工具调用(扩展功能)")
|
||||
public MCPJsonRpcResponse batchToolsCall(@Valid @RequestBody MCPJsonRpcRequest request) {
|
||||
LocalDateTime startTime = LocalDateTime.now();
|
||||
|
||||
try {
|
||||
log.info("处理MCP批量工具调用请求: id={}", request.getIdAsString());
|
||||
|
||||
// 验证请求格式
|
||||
if (!request.isValidJsonRpcRequest()) {
|
||||
return MCPJsonRpcResponse.error(request.getId(), MCPError.invalidRequest("无效的JSON-RPC请求格式"));
|
||||
}
|
||||
|
||||
// 解析批量调用参数
|
||||
if (!(request.getParams() instanceof List)) {
|
||||
return MCPJsonRpcResponse.error(request.getId(), MCPError.invalidParams("批量调用需要数组参数"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> batchParams = (List<Map<String, Object>>) request.getParams();
|
||||
|
||||
if (batchParams.isEmpty() || batchParams.size() > 10) {
|
||||
return MCPJsonRpcResponse.error(request.getId(),
|
||||
MCPError.invalidParams("批量调用数量必须在1-10之间"));
|
||||
}
|
||||
|
||||
// 执行批量调用
|
||||
List<MCPToolCallResponse> results = batchParams.stream()
|
||||
.map(this::executeSingleBatchCall)
|
||||
.toList();
|
||||
|
||||
long executionTime = java.time.Duration.between(startTime, LocalDateTime.now()).toMillis();
|
||||
log.info("MCP批量工具调用完成: 调用数量={}, 执行时间={}ms", results.size(), executionTime);
|
||||
|
||||
return MCPJsonRpcResponse.success(request.getId(), Map.of(
|
||||
"results", results,
|
||||
"total_calls", results.size(),
|
||||
"execution_time_ms", executionTime
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
long executionTime = java.time.Duration.between(startTime, LocalDateTime.now()).toMillis();
|
||||
log.error("MCP批量工具调用异常: 执行时间={}ms", executionTime, e);
|
||||
return MCPJsonRpcResponse.error(request.getId(), MCPError.internalError("批量调用失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 tools/list 参数
|
||||
*/
|
||||
private MCPToolListRequest parseToolListParams(Object params) {
|
||||
if (params == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return objectMapper.convertValue(params, MCPToolListRequest.class);
|
||||
} catch (Exception e) {
|
||||
log.warn("解析tools/list参数失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 tools/call 参数
|
||||
*/
|
||||
private MCPToolCallRequest parseToolCallParams(Object params) {
|
||||
if (params == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return objectMapper.convertValue(params, MCPToolCallRequest.class);
|
||||
} catch (Exception e) {
|
||||
log.warn("解析tools/call参数失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换传统响应为MCP响应格式
|
||||
*/
|
||||
private MCPToolCallResponse convertToMCPResponse(MCPResponse legacyResponse, String callId, LocalDateTime startTime) {
|
||||
LocalDateTime endTime = LocalDateTime.now();
|
||||
long executionTime = java.time.Duration.between(startTime, endTime).toMillis();
|
||||
|
||||
if (legacyResponse.getSuccess()) {
|
||||
MCPToolCallResponse response = MCPToolCallResponse.success(legacyResponse.getData());
|
||||
response.setCallId(callId);
|
||||
response.setExecutionTime(executionTime);
|
||||
response.setStartTime(startTime);
|
||||
response.setEndTime(endTime);
|
||||
|
||||
// 添加性能指标
|
||||
if (legacyResponse.getPerformanceMetrics() != null) {
|
||||
response.setMetrics(legacyResponse.getPerformanceMetrics());
|
||||
}
|
||||
|
||||
return response;
|
||||
} else {
|
||||
MCPToolCallResponse response = MCPToolCallResponse.error(legacyResponse.getError(), callId);
|
||||
response.setExecutionTime(executionTime);
|
||||
response.setStartTime(startTime);
|
||||
response.setEndTime(endTime);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个批量调用
|
||||
*/
|
||||
private MCPToolCallResponse executeSingleBatchCall(Map<String, Object> params) {
|
||||
try {
|
||||
MCPToolCallRequest callRequest = objectMapper.convertValue(params, MCPToolCallRequest.class);
|
||||
if (callRequest == null || !callRequest.isValid()) {
|
||||
return MCPToolCallResponse.error("无效的调用参数");
|
||||
}
|
||||
|
||||
String toolName = callRequest.getCleanedName();
|
||||
if (!toolRegistry.haseTool(toolName)) {
|
||||
return MCPToolCallResponse.error("工具不存在: " + toolName);
|
||||
}
|
||||
|
||||
LocalDateTime startTime = LocalDateTime.now();
|
||||
MCPResponse legacyResponse = mcpServer.executeTool(toolName, callRequest.getArguments());
|
||||
|
||||
return convertToMCPResponse(legacyResponse, callRequest.getCallId(), startTime);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("批量调用中的单个调用失败", e);
|
||||
return MCPToolCallResponse.error("调用失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package com.chinaweal.youfool.devops.ai.mcp.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MCP tools/call 请求参数
|
||||
*
|
||||
* 用于 tools/call 方法的参数定义
|
||||
*
|
||||
* @author AI开发团队
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "MCP tools/call 请求参数")
|
||||
public class MCPToolCallRequest {
|
||||
|
||||
/**
|
||||
* 要调用的工具名称
|
||||
*/
|
||||
@JsonProperty("name")
|
||||
@NotBlank(message = "工具名称不能为空")
|
||||
@Schema(description = "要调用的工具名称", example = "similarity_search", required = true)
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 工具调用参数
|
||||
*/
|
||||
@JsonProperty("arguments")
|
||||
@Schema(description = "工具调用参数", required = true)
|
||||
private Map<String, Object> arguments;
|
||||
|
||||
/**
|
||||
* 可选的调用ID,用于追踪
|
||||
*/
|
||||
@JsonProperty("callId")
|
||||
@Schema(description = "可选的调用ID,用于追踪")
|
||||
private String callId;
|
||||
|
||||
/**
|
||||
* 可选的超时时间(毫秒)
|
||||
*/
|
||||
@JsonProperty("timeout")
|
||||
@Schema(description = "可选的超时时间(毫秒)", example = "30000")
|
||||
private Long timeout;
|
||||
|
||||
/**
|
||||
* 可选的优先级
|
||||
*/
|
||||
@JsonProperty("priority")
|
||||
@Schema(description = "可选的优先级", example = "normal")
|
||||
private String priority;
|
||||
|
||||
/**
|
||||
* 验证请求参数
|
||||
*/
|
||||
public boolean isValid() {
|
||||
// 工具名称不能为空
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// arguments 不能为 null
|
||||
if (arguments == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 超时时间必须大于 0
|
||||
if (timeout != null && timeout <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有效的超时时间
|
||||
*/
|
||||
public long getEffectiveTimeout() {
|
||||
if (timeout == null || timeout <= 0) {
|
||||
return 30000L; // 默认30秒
|
||||
}
|
||||
return Math.min(timeout, 300000L); // 最大5分钟
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取清理后的工具名称
|
||||
*/
|
||||
public String getCleanedName() {
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为高优先级调用
|
||||
*/
|
||||
public boolean isHighPriority() {
|
||||
return "high".equalsIgnoreCase(priority) || "urgent".equalsIgnoreCase(priority);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
package com.chinaweal.youfool.devops.ai.mcp.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MCP tools/call 响应对象
|
||||
*
|
||||
* 用于 tools/call 方法的响应定义
|
||||
*
|
||||
* @author AI开发团队
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "MCP tools/call 响应对象")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class MCPToolCallResponse {
|
||||
|
||||
/**
|
||||
* 工具调用的结果内容
|
||||
*/
|
||||
@JsonProperty("content")
|
||||
@Schema(description = "工具调用的结果内容", required = true)
|
||||
private List<MCPContent> content;
|
||||
|
||||
/**
|
||||
* 是否有错误标志
|
||||
*/
|
||||
@JsonProperty("isError")
|
||||
@Schema(description = "是否有错误标志")
|
||||
private Boolean isError;
|
||||
|
||||
/**
|
||||
* 调用ID(如果请求中提供了)
|
||||
*/
|
||||
@JsonProperty("callId")
|
||||
@Schema(description = "调用ID")
|
||||
private String callId;
|
||||
|
||||
/**
|
||||
* 执行时间(毫秒)
|
||||
*/
|
||||
@JsonProperty("executionTime")
|
||||
@Schema(description = "执行时间(毫秒)")
|
||||
private Long executionTime;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
@JsonProperty("startTime")
|
||||
@Schema(description = "开始时间")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
@JsonProperty("endTime")
|
||||
@Schema(description = "结束时间")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
/**
|
||||
* 性能指标
|
||||
*/
|
||||
@JsonProperty("metrics")
|
||||
@Schema(description = "性能指标")
|
||||
private Map<String, Object> metrics;
|
||||
|
||||
/**
|
||||
* 工具版本信息
|
||||
*/
|
||||
@JsonProperty("toolVersion")
|
||||
@Schema(description = "工具版本信息")
|
||||
private String toolVersion;
|
||||
|
||||
/**
|
||||
* MCP 内容对象
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "MCP 内容对象")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class MCPContent {
|
||||
|
||||
/**
|
||||
* 内容类型,通常为 "text"
|
||||
*/
|
||||
@JsonProperty("type")
|
||||
@Schema(description = "内容类型", example = "text", required = true)
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 文本内容
|
||||
*/
|
||||
@JsonProperty("text")
|
||||
@Schema(description = "文本内容")
|
||||
private String text;
|
||||
|
||||
/**
|
||||
* 资源URI(如果是资源类型)
|
||||
*/
|
||||
@JsonProperty("uri")
|
||||
@Schema(description = "资源URI")
|
||||
private String uri;
|
||||
|
||||
/**
|
||||
* MIME类型(如果是资源类型)
|
||||
*/
|
||||
@JsonProperty("mimeType")
|
||||
@Schema(description = "MIME类型")
|
||||
private String mimeType;
|
||||
|
||||
/**
|
||||
* 结构化数据(如果是数据类型)
|
||||
*/
|
||||
@JsonProperty("data")
|
||||
@Schema(description = "结构化数据")
|
||||
private Object data;
|
||||
|
||||
/**
|
||||
* 创建文本内容
|
||||
*/
|
||||
public static MCPContent text(String text) {
|
||||
MCPContent content = new MCPContent();
|
||||
content.setType("text");
|
||||
content.setText(text);
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建数据内容
|
||||
*/
|
||||
public static MCPContent data(Object data) {
|
||||
MCPContent content = new MCPContent();
|
||||
content.setType("data");
|
||||
content.setData(data);
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建资源内容
|
||||
*/
|
||||
public static MCPContent resource(String uri, String mimeType) {
|
||||
MCPContent content = new MCPContent();
|
||||
content.setType("resource");
|
||||
content.setUri(uri);
|
||||
content.setMimeType(mimeType);
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成功响应
|
||||
*/
|
||||
public static MCPToolCallResponse success(Object data) {
|
||||
MCPToolCallResponse response = new MCPToolCallResponse();
|
||||
response.setContent(List.of(MCPContent.data(data)));
|
||||
response.setIsError(false);
|
||||
response.setEndTime(LocalDateTime.now());
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成功响应(文本)
|
||||
*/
|
||||
public static MCPToolCallResponse successText(String text) {
|
||||
MCPToolCallResponse response = new MCPToolCallResponse();
|
||||
response.setContent(List.of(MCPContent.text(text)));
|
||||
response.setIsError(false);
|
||||
response.setEndTime(LocalDateTime.now());
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成功响应(带完整信息)
|
||||
*/
|
||||
public static MCPToolCallResponse success(Object data, String callId, Long executionTime) {
|
||||
MCPToolCallResponse response = success(data);
|
||||
response.setCallId(callId);
|
||||
response.setExecutionTime(executionTime);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误响应
|
||||
*/
|
||||
public static MCPToolCallResponse error(String errorMessage) {
|
||||
MCPToolCallResponse response = new MCPToolCallResponse();
|
||||
response.setContent(List.of(MCPContent.text(errorMessage)));
|
||||
response.setIsError(true);
|
||||
response.setEndTime(LocalDateTime.now());
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误响应(带调用ID)
|
||||
*/
|
||||
public static MCPToolCallResponse error(String errorMessage, String callId) {
|
||||
MCPToolCallResponse response = error(errorMessage);
|
||||
response.setCallId(callId);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置性能指标
|
||||
*/
|
||||
public MCPToolCallResponse withMetrics(Map<String, Object> metrics) {
|
||||
this.metrics = metrics;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置执行时间信息
|
||||
*/
|
||||
public MCPToolCallResponse withTiming(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
this.startTime = startTime;
|
||||
this.endTime = endTime;
|
||||
if (startTime != null && endTime != null) {
|
||||
this.executionTime = java.time.Duration.between(startTime, endTime).toMillis();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,377 @@
|
|||
package com.chinaweal.youfool.devops.ai.mcp.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MCP 工具定义对象
|
||||
*
|
||||
* 遵循 MCP 规范的工具定义格式,包含完整的 JSON Schema
|
||||
*
|
||||
* @author AI开发团队
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "MCP 工具定义对象")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class MCPToolDefinition {
|
||||
|
||||
/**
|
||||
* 工具名称,必须唯一
|
||||
*/
|
||||
@JsonProperty("name")
|
||||
@Schema(description = "工具名称,必须唯一", example = "similarity_search", required = true)
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 工具描述
|
||||
*/
|
||||
@JsonProperty("description")
|
||||
@Schema(description = "工具描述", example = "基于文本内容进行向量相似度检索,查找类似问题", required = true)
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 输入参数的 JSON Schema
|
||||
*/
|
||||
@JsonProperty("inputSchema")
|
||||
@Schema(description = "输入参数的 JSON Schema", required = true)
|
||||
private JsonSchema inputSchema;
|
||||
|
||||
/**
|
||||
* 工具类型,默认为 "function"
|
||||
*/
|
||||
@JsonProperty("type")
|
||||
@Schema(description = "工具类型", example = "function")
|
||||
private String type = "function";
|
||||
|
||||
/**
|
||||
* 工具标签
|
||||
*/
|
||||
@JsonProperty("tags")
|
||||
@Schema(description = "工具标签")
|
||||
private List<String> tags;
|
||||
|
||||
/**
|
||||
* 只读提示
|
||||
*/
|
||||
@JsonProperty("readOnlyHint")
|
||||
@Schema(description = "只读提示,表示工具不会修改系统状态")
|
||||
private Boolean readOnlyHint;
|
||||
|
||||
/**
|
||||
* 破坏性操作提示
|
||||
*/
|
||||
@JsonProperty("destructiveHint")
|
||||
@Schema(description = "破坏性操作提示,表示工具可能修改或删除数据")
|
||||
private Boolean destructiveHint;
|
||||
|
||||
/**
|
||||
* 工具版本
|
||||
*/
|
||||
@JsonProperty("version")
|
||||
@Schema(description = "工具版本", example = "1.0.0")
|
||||
private String version;
|
||||
|
||||
/**
|
||||
* 工具作者
|
||||
*/
|
||||
@JsonProperty("author")
|
||||
@Schema(description = "工具作者")
|
||||
private String author;
|
||||
|
||||
/**
|
||||
* 工具文档URL
|
||||
*/
|
||||
@JsonProperty("documentationUrl")
|
||||
@Schema(description = "工具文档URL")
|
||||
private String documentationUrl;
|
||||
|
||||
/**
|
||||
* 预期的执行时间范围(毫秒)
|
||||
*/
|
||||
@JsonProperty("expectedExecutionTime")
|
||||
@Schema(description = "预期的执行时间范围(毫秒)")
|
||||
private ExecutionTimeHint expectedExecutionTime;
|
||||
|
||||
/**
|
||||
* 资源需求提示
|
||||
*/
|
||||
@JsonProperty("resourceRequirements")
|
||||
@Schema(description = "资源需求提示")
|
||||
private ResourceRequirements resourceRequirements;
|
||||
|
||||
/**
|
||||
* JSON Schema 定义
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "JSON Schema 定义")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class JsonSchema {
|
||||
|
||||
/**
|
||||
* Schema 类型,通常为 "object"
|
||||
*/
|
||||
@JsonProperty("type")
|
||||
@Schema(description = "Schema 类型", example = "object", required = true)
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 属性定义
|
||||
*/
|
||||
@JsonProperty("properties")
|
||||
@Schema(description = "属性定义")
|
||||
private Map<String, PropertySchema> properties;
|
||||
|
||||
/**
|
||||
* 必需的属性列表
|
||||
*/
|
||||
@JsonProperty("required")
|
||||
@Schema(description = "必需的属性列表")
|
||||
private List<String> required;
|
||||
|
||||
/**
|
||||
* 是否允许额外属性
|
||||
*/
|
||||
@JsonProperty("additionalProperties")
|
||||
@Schema(description = "是否允许额外属性")
|
||||
private Boolean additionalProperties;
|
||||
|
||||
/**
|
||||
* Schema 标题
|
||||
*/
|
||||
@JsonProperty("title")
|
||||
@Schema(description = "Schema 标题")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* Schema 描述
|
||||
*/
|
||||
@JsonProperty("description")
|
||||
@Schema(description = "Schema 描述")
|
||||
private String description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 属性 Schema 定义
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "属性 Schema 定义")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class PropertySchema {
|
||||
|
||||
/**
|
||||
* 属性类型
|
||||
*/
|
||||
@JsonProperty("type")
|
||||
@Schema(description = "属性类型", example = "string")
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 属性描述
|
||||
*/
|
||||
@JsonProperty("description")
|
||||
@Schema(description = "属性描述")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 默认值
|
||||
*/
|
||||
@JsonProperty("default")
|
||||
@Schema(description = "默认值")
|
||||
private Object defaultValue;
|
||||
|
||||
/**
|
||||
* 最小值(数字类型)
|
||||
*/
|
||||
@JsonProperty("minimum")
|
||||
@Schema(description = "最小值")
|
||||
private Number minimum;
|
||||
|
||||
/**
|
||||
* 最大值(数字类型)
|
||||
*/
|
||||
@JsonProperty("maximum")
|
||||
@Schema(description = "最大值")
|
||||
private Number maximum;
|
||||
|
||||
/**
|
||||
* 最小长度(字符串类型)
|
||||
*/
|
||||
@JsonProperty("minLength")
|
||||
@Schema(description = "最小长度")
|
||||
private Integer minLength;
|
||||
|
||||
/**
|
||||
* 最大长度(字符串类型)
|
||||
*/
|
||||
@JsonProperty("maxLength")
|
||||
@Schema(description = "最大长度")
|
||||
private Integer maxLength;
|
||||
|
||||
/**
|
||||
* 模式匹配(字符串类型)
|
||||
*/
|
||||
@JsonProperty("pattern")
|
||||
@Schema(description = "模式匹配")
|
||||
private String pattern;
|
||||
|
||||
/**
|
||||
* 枚举值
|
||||
*/
|
||||
@JsonProperty("enum")
|
||||
@Schema(description = "枚举值")
|
||||
private List<Object> enumValues;
|
||||
|
||||
/**
|
||||
* 示例值
|
||||
*/
|
||||
@JsonProperty("examples")
|
||||
@Schema(description = "示例值")
|
||||
private List<Object> examples;
|
||||
|
||||
/**
|
||||
* 数组项目类型(数组类型)
|
||||
*/
|
||||
@JsonProperty("items")
|
||||
@Schema(description = "数组项目类型")
|
||||
private PropertySchema items;
|
||||
|
||||
/**
|
||||
* 创建字符串属性
|
||||
*/
|
||||
public static PropertySchema string(String description) {
|
||||
PropertySchema schema = new PropertySchema();
|
||||
schema.setType("string");
|
||||
schema.setDescription(description);
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建整数属性
|
||||
*/
|
||||
public static PropertySchema integer(String description) {
|
||||
PropertySchema schema = new PropertySchema();
|
||||
schema.setType("integer");
|
||||
schema.setDescription(description);
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建数字属性
|
||||
*/
|
||||
public static PropertySchema number(String description) {
|
||||
PropertySchema schema = new PropertySchema();
|
||||
schema.setType("number");
|
||||
schema.setDescription(description);
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建布尔属性
|
||||
*/
|
||||
public static PropertySchema bool(String description) {
|
||||
PropertySchema schema = new PropertySchema();
|
||||
schema.setType("boolean");
|
||||
schema.setDescription(description);
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认值
|
||||
*/
|
||||
public PropertySchema withDefault(Object defaultValue) {
|
||||
this.defaultValue = defaultValue;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置数值范围
|
||||
*/
|
||||
public PropertySchema withRange(Number min, Number max) {
|
||||
this.minimum = min;
|
||||
this.maximum = max;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置长度范围
|
||||
*/
|
||||
public PropertySchema withLength(Integer minLength, Integer maxLength) {
|
||||
this.minLength = minLength;
|
||||
this.maxLength = maxLength;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行时间提示
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "执行时间提示")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class ExecutionTimeHint {
|
||||
|
||||
/**
|
||||
* 最小执行时间(毫秒)
|
||||
*/
|
||||
@JsonProperty("min")
|
||||
@Schema(description = "最小执行时间(毫秒)")
|
||||
private Long min;
|
||||
|
||||
/**
|
||||
* 最大执行时间(毫秒)
|
||||
*/
|
||||
@JsonProperty("max")
|
||||
@Schema(description = "最大执行时间(毫秒)")
|
||||
private Long max;
|
||||
|
||||
/**
|
||||
* 平均执行时间(毫秒)
|
||||
*/
|
||||
@JsonProperty("average")
|
||||
@Schema(description = "平均执行时间(毫秒)")
|
||||
private Long average;
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源需求
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "资源需求")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class ResourceRequirements {
|
||||
|
||||
/**
|
||||
* CPU 使用级别
|
||||
*/
|
||||
@JsonProperty("cpu")
|
||||
@Schema(description = "CPU 使用级别", example = "low")
|
||||
private String cpu;
|
||||
|
||||
/**
|
||||
* 内存使用级别
|
||||
*/
|
||||
@JsonProperty("memory")
|
||||
@Schema(description = "内存使用级别", example = "medium")
|
||||
private String memory;
|
||||
|
||||
/**
|
||||
* 网络使用级别
|
||||
*/
|
||||
@JsonProperty("network")
|
||||
@Schema(description = "网络使用级别", example = "low")
|
||||
private String network;
|
||||
|
||||
/**
|
||||
* 数据库使用级别
|
||||
*/
|
||||
@JsonProperty("database")
|
||||
@Schema(description = "数据库使用级别", example = "high")
|
||||
private String database;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package com.chinaweal.youfool.devops.ai.mcp.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MCP tools/list 响应对象
|
||||
*
|
||||
* 用于 tools/list 方法的响应定义
|
||||
*
|
||||
* @author AI开发团队
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "MCP tools/list 响应对象")
|
||||
public class MCPToolListResponse {
|
||||
|
||||
/**
|
||||
* 可用的工具列表
|
||||
*/
|
||||
@JsonProperty("tools")
|
||||
@Schema(description = "可用的工具列表", required = true)
|
||||
private List<MCPToolDefinition> tools;
|
||||
|
||||
/**
|
||||
* 工具总数(用于分页)
|
||||
*/
|
||||
@JsonProperty("totalCount")
|
||||
@Schema(description = "工具总数")
|
||||
private Integer totalCount;
|
||||
|
||||
/**
|
||||
* 是否还有更多工具(用于分页)
|
||||
*/
|
||||
@JsonProperty("hasMore")
|
||||
@Schema(description = "是否还有更多工具")
|
||||
private Boolean hasMore;
|
||||
|
||||
/**
|
||||
* 下一页的偏移量(用于分页)
|
||||
*/
|
||||
@JsonProperty("nextOffset")
|
||||
@Schema(description = "下一页的偏移量")
|
||||
private Integer nextOffset;
|
||||
|
||||
/**
|
||||
* 创建简单的工具列表响应
|
||||
*/
|
||||
public static MCPToolListResponse of(List<MCPToolDefinition> tools) {
|
||||
MCPToolListResponse response = new MCPToolListResponse();
|
||||
response.setTools(tools);
|
||||
response.setTotalCount(tools.size());
|
||||
response.setHasMore(false);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建分页的工具列表响应
|
||||
*/
|
||||
public static MCPToolListResponse of(List<MCPToolDefinition> tools, int totalCount, int offset, int limit) {
|
||||
MCPToolListResponse response = new MCPToolListResponse();
|
||||
response.setTools(tools);
|
||||
response.setTotalCount(totalCount);
|
||||
response.setHasMore(offset + tools.size() < totalCount);
|
||||
|
||||
if (response.getHasMore()) {
|
||||
response.setNextOffset(offset + tools.size());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,443 @@
|
|||
package com.chinaweal.youfool.devops.ai.mcp;
|
||||
|
||||
import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolCallResponse;
|
||||
import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolDefinition;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* MCPFunctionBridge 单元测试
|
||||
*
|
||||
* 测试MCP与函数调用格式之间的转换功能
|
||||
*
|
||||
* @author AI开发团队
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MCPFunctionBridgeTest {
|
||||
|
||||
@Mock
|
||||
private MCPToolRegistry toolRegistry;
|
||||
|
||||
@Mock
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@InjectMocks
|
||||
private MCPFunctionBridge functionBridge;
|
||||
|
||||
private MCPToolDefinition testTool;
|
||||
private MCPToolCallResponse testResponse;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 创建测试工具定义
|
||||
testTool = new MCPToolDefinition();
|
||||
testTool.setName("test_tool");
|
||||
testTool.setDescription("测试工具描述");
|
||||
testTool.setType("function");
|
||||
testTool.setReadOnlyHint(true);
|
||||
testTool.setDestructiveHint(false);
|
||||
testTool.setTags(List.of("test", "readonly"));
|
||||
|
||||
// 创建输入Schema
|
||||
MCPToolDefinition.JsonSchema inputSchema = new MCPToolDefinition.JsonSchema();
|
||||
inputSchema.setType("object");
|
||||
inputSchema.setProperties(Map.of(
|
||||
"param1", MCPToolDefinition.PropertySchema.string("参数1"),
|
||||
"param2", MCPToolDefinition.PropertySchema.integer("参数2").withDefault(10)
|
||||
));
|
||||
inputSchema.setRequired(List.of("param1"));
|
||||
testTool.setInputSchema(inputSchema);
|
||||
|
||||
// 创建测试响应
|
||||
testResponse = MCPToolCallResponse.success(Map.of("result", "success"));
|
||||
testResponse.setCallId("test-call-123");
|
||||
testResponse.setExecutionTime(1500L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertToOpenAIFunctions() {
|
||||
// 准备数据
|
||||
List<MCPToolDefinition> tools = List.of(testTool);
|
||||
|
||||
// 执行测试
|
||||
List<Map<String, Object>> openAIFunctions = functionBridge.convertToOpenAIFunctions(tools);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(openAIFunctions);
|
||||
assertEquals(1, openAIFunctions.size());
|
||||
|
||||
Map<String, Object> functionDef = openAIFunctions.get(0);
|
||||
assertEquals("function", functionDef.get("type"));
|
||||
assertTrue(functionDef.containsKey("function"));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> function = (Map<String, Object>) functionDef.get("function");
|
||||
assertEquals("test_tool", function.get("name"));
|
||||
assertEquals("测试工具描述", function.get("description"));
|
||||
assertTrue(function.containsKey("parameters"));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> parameters = (Map<String, Object>) function.get("parameters");
|
||||
assertEquals("object", parameters.get("type"));
|
||||
assertTrue(parameters.containsKey("properties"));
|
||||
assertTrue(parameters.containsKey("required"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertToAnthropicTools() {
|
||||
// 准备数据
|
||||
List<MCPToolDefinition> tools = List.of(testTool);
|
||||
|
||||
// 执行测试
|
||||
List<Map<String, Object>> anthropicTools = functionBridge.convertToAnthropicTools(tools);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(anthropicTools);
|
||||
assertEquals(1, anthropicTools.size());
|
||||
|
||||
Map<String, Object> tool = anthropicTools.get(0);
|
||||
assertEquals("test_tool", tool.get("name"));
|
||||
assertEquals("测试工具描述", tool.get("description"));
|
||||
assertTrue(tool.containsKey("input_schema"));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> inputSchema = (Map<String, Object>) tool.get("input_schema");
|
||||
assertEquals("object", inputSchema.get("type"));
|
||||
assertTrue(inputSchema.containsKey("properties"));
|
||||
assertTrue(inputSchema.containsKey("required"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertToGenericFunctions() {
|
||||
// 准备数据
|
||||
List<MCPToolDefinition> tools = List.of(testTool);
|
||||
when(objectMapper.convertValue(any(), eq(Map.class))).thenReturn(Map.of("type", "object"));
|
||||
|
||||
// 执行测试
|
||||
List<Map<String, Object>> genericFunctions = functionBridge.convertToGenericFunctions(tools);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(genericFunctions);
|
||||
assertEquals(1, genericFunctions.size());
|
||||
|
||||
Map<String, Object> function = genericFunctions.get(0);
|
||||
assertEquals("test_tool", function.get("name"));
|
||||
assertEquals("测试工具描述", function.get("description"));
|
||||
assertEquals("function", function.get("type"));
|
||||
assertTrue(function.containsKey("input_schema"));
|
||||
assertTrue(function.containsKey("tags"));
|
||||
assertEquals(true, function.get("read_only"));
|
||||
assertEquals(false, function.get("destructive"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertFromOpenAIFunctionResult_WithContent() {
|
||||
// 准备数据
|
||||
Map<String, Object> functionResult = Map.of(
|
||||
"content", "执行成功",
|
||||
"status", "completed"
|
||||
);
|
||||
|
||||
// 执行测试
|
||||
MCPToolCallResponse response = functionBridge.convertFromOpenAIFunctionResult(functionResult, "test_tool");
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(response);
|
||||
assertFalse(response.getIsError());
|
||||
assertNotNull(response.getContent());
|
||||
assertEquals(1, response.getContent().size());
|
||||
assertEquals("data", response.getContent().get(0).getType());
|
||||
assertEquals("执行成功", response.getContent().get(0).getData());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertFromOpenAIFunctionResult_WithError() {
|
||||
// 准备数据
|
||||
Map<String, Object> functionResult = Map.of(
|
||||
"error", "执行失败",
|
||||
"status", "error"
|
||||
);
|
||||
|
||||
// 执行测试
|
||||
MCPToolCallResponse response = functionBridge.convertFromOpenAIFunctionResult(functionResult, "test_tool");
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(response);
|
||||
assertTrue(response.getIsError());
|
||||
assertNotNull(response.getContent());
|
||||
assertEquals(1, response.getContent().size());
|
||||
assertEquals("text", response.getContent().get(0).getType());
|
||||
assertEquals("执行失败", response.getContent().get(0).getText());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertFromAnthropicToolResult_WithResult() {
|
||||
// 准备数据
|
||||
Map<String, Object> toolResult = Map.of(
|
||||
"result", Map.of("data", "处理完成"),
|
||||
"tool_use_id", "anthropic-123"
|
||||
);
|
||||
|
||||
// 执行测试
|
||||
MCPToolCallResponse response = functionBridge.convertFromAnthropicToolResult(toolResult, "test_tool");
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(response);
|
||||
assertFalse(response.getIsError());
|
||||
assertNotNull(response.getContent());
|
||||
assertEquals(1, response.getContent().size());
|
||||
assertEquals("data", response.getContent().get(0).getType());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> data = (Map<String, Object>) response.getContent().get(0).getData();
|
||||
assertEquals("处理完成", data.get("data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertToOpenAIFunctionResult() {
|
||||
// 执行测试
|
||||
Map<String, Object> result = functionBridge.convertToOpenAIFunctionResult(testResponse, "test_tool");
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(result);
|
||||
assertEquals("test_tool", result.get("function_name"));
|
||||
assertEquals("test-call-123", result.get("call_id"));
|
||||
assertEquals("success", result.get("status"));
|
||||
assertEquals(1500L, result.get("execution_time_ms"));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> content = (Map<String, Object>) result.get("content");
|
||||
assertEquals("success", content.get("result"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertToOpenAIFunctionResult_WithError() {
|
||||
// 准备错误响应
|
||||
MCPToolCallResponse errorResponse = MCPToolCallResponse.error("执行失败", "error-call-456");
|
||||
|
||||
// 执行测试
|
||||
Map<String, Object> result = functionBridge.convertToOpenAIFunctionResult(errorResponse, "test_tool");
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(result);
|
||||
assertEquals("test_tool", result.get("function_name"));
|
||||
assertEquals("error-call-456", result.get("call_id"));
|
||||
assertEquals("error", result.get("status"));
|
||||
assertEquals("执行失败", result.get("error"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertToAnthropicToolResult() {
|
||||
// 执行测试
|
||||
Map<String, Object> result = functionBridge.convertToAnthropicToolResult(testResponse, "test_tool");
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(result);
|
||||
assertEquals("test_tool", result.get("tool_name"));
|
||||
assertEquals("test-call-123", result.get("tool_use_id"));
|
||||
assertEquals(false, result.get("is_error"));
|
||||
assertEquals(1500L, result.get("execution_time_ms"));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> content = (Map<String, Object>) result.get("content");
|
||||
assertEquals("success", content.get("result"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertToAnthropicToolResult_WithError() {
|
||||
// 准备错误响应
|
||||
MCPToolCallResponse errorResponse = MCPToolCallResponse.error("执行失败", "error-call-456");
|
||||
|
||||
// 执行测试
|
||||
Map<String, Object> result = functionBridge.convertToAnthropicToolResult(errorResponse, "test_tool");
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(result);
|
||||
assertEquals("test_tool", result.get("tool_name"));
|
||||
assertEquals("error-call-456", result.get("tool_use_id"));
|
||||
assertEquals(true, result.get("is_error"));
|
||||
assertEquals("执行失败", result.get("content"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertAllToolsToFormat_OpenAI() {
|
||||
// 准备数据
|
||||
when(toolRegistry.getAllTools()).thenReturn(List.of(testTool));
|
||||
|
||||
// 执行测试
|
||||
List<Map<String, Object>> result = functionBridge.convertAllToolsToFormat("openai");
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("function", result.get(0).get("type"));
|
||||
assertTrue(result.get(0).containsKey("function"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertAllToolsToFormat_Anthropic() {
|
||||
// 准备数据
|
||||
when(toolRegistry.getAllTools()).thenReturn(List.of(testTool));
|
||||
|
||||
// 执行测试
|
||||
List<Map<String, Object>> result = functionBridge.convertAllToolsToFormat("anthropic");
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("test_tool", result.get(0).get("name"));
|
||||
assertTrue(result.get(0).containsKey("input_schema"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertAllToolsToFormat_Generic() {
|
||||
// 准备数据
|
||||
when(toolRegistry.getAllTools()).thenReturn(List.of(testTool));
|
||||
when(objectMapper.convertValue(any(), eq(Map.class))).thenReturn(Map.of("type", "object"));
|
||||
|
||||
// 执行测试
|
||||
List<Map<String, Object>> result = functionBridge.convertAllToolsToFormat("generic");
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("test_tool", result.get(0).get("name"));
|
||||
assertEquals("function", result.get(0).get("type"));
|
||||
assertTrue(result.get(0).containsKey("tags"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertAllToolsToFormat_Unknown() {
|
||||
// 准备数据
|
||||
when(toolRegistry.getAllTools()).thenReturn(List.of(testTool));
|
||||
when(objectMapper.convertValue(any(), eq(Map.class))).thenReturn(Map.of("type", "object"));
|
||||
|
||||
// 执行测试
|
||||
List<Map<String, Object>> result = functionBridge.convertAllToolsToFormat("unknown_format");
|
||||
|
||||
// 验证结果 - 应该回退到通用格式
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("test_tool", result.get(0).get("name"));
|
||||
assertEquals("function", result.get(0).get("type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidateFunctionCall_Success() {
|
||||
// 准备数据
|
||||
when(toolRegistry.getTool("test_tool")).thenReturn(Optional.of(testTool));
|
||||
Map<String, Object> arguments = Map.of(
|
||||
"param1", "value1",
|
||||
"param2", 20
|
||||
);
|
||||
|
||||
// 执行测试
|
||||
MCPFunctionBridge.ValidationResult result = functionBridge.validateFunctionCall("test_tool", arguments);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isValid());
|
||||
assertNull(result.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidateFunctionCall_MissingRequiredParam() {
|
||||
// 准备数据
|
||||
when(toolRegistry.getTool("test_tool")).thenReturn(Optional.of(testTool));
|
||||
Map<String, Object> arguments = Map.of(
|
||||
"param2", 20
|
||||
// 缺少必需的 param1
|
||||
);
|
||||
|
||||
// 执行测试
|
||||
MCPFunctionBridge.ValidationResult result = functionBridge.validateFunctionCall("test_tool", arguments);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(result);
|
||||
assertFalse(result.isValid());
|
||||
assertNotNull(result.getMessage());
|
||||
assertTrue(result.getMessage().contains("param1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidateFunctionCall_ToolNotFound() {
|
||||
// 准备数据
|
||||
when(toolRegistry.getTool("unknown_tool")).thenReturn(Optional.empty());
|
||||
|
||||
// 执行测试
|
||||
MCPFunctionBridge.ValidationResult result = functionBridge.validateFunctionCall("unknown_tool", Map.of());
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(result);
|
||||
assertFalse(result.isValid());
|
||||
assertNotNull(result.getMessage());
|
||||
assertTrue(result.getMessage().contains("工具不存在"));
|
||||
assertTrue(result.getMessage().contains("unknown_tool"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidateFunctionCall_Exception() {
|
||||
// 准备数据
|
||||
when(toolRegistry.getTool("test_tool")).thenThrow(new RuntimeException("数据库错误"));
|
||||
|
||||
// 执行测试
|
||||
MCPFunctionBridge.ValidationResult result = functionBridge.validateFunctionCall("test_tool", Map.of());
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(result);
|
||||
assertFalse(result.isValid());
|
||||
assertNotNull(result.getMessage());
|
||||
assertTrue(result.getMessage().contains("参数验证失败"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testContentExtraction_MultipleContent() {
|
||||
// 创建包含多个内容项的响应
|
||||
MCPToolCallResponse response = new MCPToolCallResponse();
|
||||
response.setContent(List.of(
|
||||
MCPToolCallResponse.MCPContent.text("第一部分"),
|
||||
MCPToolCallResponse.MCPContent.data(Map.of("key", "value"))
|
||||
));
|
||||
response.setIsError(false);
|
||||
|
||||
// 转换为OpenAI格式
|
||||
Map<String, Object> result = functionBridge.convertToOpenAIFunctionResult(response, "test_tool");
|
||||
|
||||
// 验证结果 - 多个内容项应该返回完整列表
|
||||
assertNotNull(result);
|
||||
assertEquals("success", result.get("status"));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Object> content = (List<Object>) result.get("content");
|
||||
assertEquals(2, content.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConversionWithNullValues() {
|
||||
// 创建包含null值的响应
|
||||
MCPToolCallResponse response = MCPToolCallResponse.success(null);
|
||||
|
||||
// 执行转换
|
||||
Map<String, Object> openAIResult = functionBridge.convertToOpenAIFunctionResult(response, "test_tool");
|
||||
Map<String, Object> anthropicResult = functionBridge.convertToAnthropicToolResult(response, "test_tool");
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(openAIResult);
|
||||
assertNotNull(anthropicResult);
|
||||
assertEquals("success", openAIResult.get("status"));
|
||||
assertEquals(false, anthropicResult.get("is_error"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,320 @@
|
|||
package com.chinaweal.youfool.devops.ai.mcp;
|
||||
|
||||
import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolDefinition;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* MCPToolRegistry 单元测试
|
||||
*
|
||||
* 测试工具注册表的功能
|
||||
*
|
||||
* @author AI开发团队
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class MCPToolRegistryTest {
|
||||
|
||||
private MCPToolRegistry toolRegistry;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
toolRegistry = new MCPToolRegistry();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllTools() {
|
||||
// 执行测试
|
||||
List<MCPToolDefinition> tools = toolRegistry.getAllTools();
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(tools);
|
||||
assertFalse(tools.isEmpty());
|
||||
assertEquals(4, tools.size()); // 应该有4个预定义工具
|
||||
|
||||
// 验证工具名称
|
||||
List<String> toolNames = tools.stream().map(MCPToolDefinition::getName).toList();
|
||||
assertTrue(toolNames.contains("repair_query"));
|
||||
assertTrue(toolNames.contains("repair_feedback_query"));
|
||||
assertTrue(toolNames.contains("similarity_search"));
|
||||
assertTrue(toolNames.contains("knowledge_query"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTool_Exists() {
|
||||
// 执行测试
|
||||
Optional<MCPToolDefinition> tool = toolRegistry.getTool("repair_query");
|
||||
|
||||
// 验证结果
|
||||
assertTrue(tool.isPresent());
|
||||
assertEquals("repair_query", tool.get().getName());
|
||||
assertEquals("function", tool.get().getType());
|
||||
assertNotNull(tool.get().getDescription());
|
||||
assertNotNull(tool.get().getInputSchema());
|
||||
assertEquals(Boolean.TRUE, tool.get().getReadOnlyHint());
|
||||
assertEquals(Boolean.FALSE, tool.get().getDestructiveHint());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTool_NotExists() {
|
||||
// 执行测试
|
||||
Optional<MCPToolDefinition> tool = toolRegistry.getTool("non_existent_tool");
|
||||
|
||||
// 验证结果
|
||||
assertFalse(tool.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetToolsByType() {
|
||||
// 执行测试
|
||||
List<MCPToolDefinition> functionTools = toolRegistry.getToolsByType("function");
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(functionTools);
|
||||
assertEquals(4, functionTools.size()); // 所有工具都是function类型
|
||||
assertTrue(functionTools.stream().allMatch(tool -> "function".equals(tool.getType())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetToolsByType_NotExists() {
|
||||
// 执行测试
|
||||
List<MCPToolDefinition> resourceTools = toolRegistry.getToolsByType("resource");
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(resourceTools);
|
||||
assertTrue(resourceTools.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetToolsByTag() {
|
||||
// 执行测试
|
||||
List<MCPToolDefinition> readOnlyTools = toolRegistry.getToolsByTag("readonly");
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(readOnlyTools);
|
||||
assertFalse(readOnlyTools.isEmpty());
|
||||
assertTrue(readOnlyTools.stream().allMatch(tool ->
|
||||
tool.getTags() != null && tool.getTags().contains("readonly")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetToolsByTag_NotExists() {
|
||||
// 执行测试
|
||||
List<MCPToolDefinition> unknownTagTools = toolRegistry.getToolsByTag("unknown_tag");
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(unknownTagTools);
|
||||
assertTrue(unknownTagTools.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetToolsPaginated() {
|
||||
// 执行测试 - 第一页,每页2个
|
||||
List<MCPToolDefinition> page1 = toolRegistry.getToolsPaginated(0, 2);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(page1);
|
||||
assertEquals(2, page1.size());
|
||||
|
||||
// 执行测试 - 第二页,每页2个
|
||||
List<MCPToolDefinition> page2 = toolRegistry.getToolsPaginated(2, 2);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(page2);
|
||||
assertEquals(2, page2.size());
|
||||
|
||||
// 验证不同页的内容不同
|
||||
assertNotEquals(page1.get(0).getName(), page2.get(0).getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetToolsPaginated_OffsetBeyondRange() {
|
||||
// 执行测试 - 偏移量超出范围
|
||||
List<MCPToolDefinition> tools = toolRegistry.getToolsPaginated(100, 10);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(tools);
|
||||
assertTrue(tools.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetToolsPaginated_LimitBeyondAvailable() {
|
||||
// 执行测试 - 限制数量超出可用数量
|
||||
List<MCPToolDefinition> tools = toolRegistry.getToolsPaginated(0, 100);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(tools);
|
||||
assertEquals(4, tools.size()); // 应该返回所有可用的工具
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHaseTool() {
|
||||
// 测试存在的工具
|
||||
assertTrue(toolRegistry.haseTool("repair_query"));
|
||||
assertTrue(toolRegistry.haseTool("repair_feedback_query"));
|
||||
assertTrue(toolRegistry.haseTool("similarity_search"));
|
||||
assertTrue(toolRegistry.haseTool("knowledge_query"));
|
||||
|
||||
// 测试不存在的工具
|
||||
assertFalse(toolRegistry.haseTool("non_existent_tool"));
|
||||
assertFalse(toolRegistry.haseTool(""));
|
||||
assertFalse(toolRegistry.haseTool(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetToolCount() {
|
||||
// 执行测试
|
||||
int count = toolRegistry.getToolCount();
|
||||
|
||||
// 验证结果
|
||||
assertEquals(4, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRepairQueryToolDefinition() {
|
||||
// 获取repair_query工具
|
||||
Optional<MCPToolDefinition> tool = toolRegistry.getTool("repair_query");
|
||||
assertTrue(tool.isPresent());
|
||||
|
||||
MCPToolDefinition repairQuery = tool.get();
|
||||
|
||||
// 验证基本属性
|
||||
assertEquals("repair_query", repairQuery.getName());
|
||||
assertEquals("function", repairQuery.getType());
|
||||
assertNotNull(repairQuery.getDescription());
|
||||
assertTrue(repairQuery.getDescription().contains("工单"));
|
||||
assertEquals("1.0.0", repairQuery.getVersion());
|
||||
assertEquals("运维系统AI团队", repairQuery.getAuthor());
|
||||
assertEquals(Boolean.TRUE, repairQuery.getReadOnlyHint());
|
||||
assertEquals(Boolean.FALSE, repairQuery.getDestructiveHint());
|
||||
|
||||
// 验证标签
|
||||
assertNotNull(repairQuery.getTags());
|
||||
assertTrue(repairQuery.getTags().contains("repair"));
|
||||
assertTrue(repairQuery.getTags().contains("query"));
|
||||
assertTrue(repairQuery.getTags().contains("readonly"));
|
||||
|
||||
// 验证输入Schema
|
||||
assertNotNull(repairQuery.getInputSchema());
|
||||
assertEquals("object", repairQuery.getInputSchema().getType());
|
||||
assertNotNull(repairQuery.getInputSchema().getProperties());
|
||||
assertTrue(repairQuery.getInputSchema().getProperties().containsKey("repairId"));
|
||||
assertNotNull(repairQuery.getInputSchema().getRequired());
|
||||
assertTrue(repairQuery.getInputSchema().getRequired().contains("repairId"));
|
||||
assertEquals(Boolean.FALSE, repairQuery.getInputSchema().getAdditionalProperties());
|
||||
|
||||
// 验证性能提示
|
||||
assertNotNull(repairQuery.getExpectedExecutionTime());
|
||||
assertTrue(repairQuery.getExpectedExecutionTime().getMin() > 0);
|
||||
assertTrue(repairQuery.getExpectedExecutionTime().getMax() > repairQuery.getExpectedExecutionTime().getMin());
|
||||
assertNotNull(repairQuery.getExpectedExecutionTime().getAverage());
|
||||
|
||||
// 验证资源需求
|
||||
assertNotNull(repairQuery.getResourceRequirements());
|
||||
assertEquals("low", repairQuery.getResourceRequirements().getCpu());
|
||||
assertEquals("low", repairQuery.getResourceRequirements().getMemory());
|
||||
assertEquals("low", repairQuery.getResourceRequirements().getNetwork());
|
||||
assertEquals("medium", repairQuery.getResourceRequirements().getDatabase());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSimilaritySearchToolDefinition() {
|
||||
// 获取similarity_search工具
|
||||
Optional<MCPToolDefinition> tool = toolRegistry.getTool("similarity_search");
|
||||
assertTrue(tool.isPresent());
|
||||
|
||||
MCPToolDefinition similaritySearch = tool.get();
|
||||
|
||||
// 验证基本属性
|
||||
assertEquals("similarity_search", similaritySearch.getName());
|
||||
assertTrue(similaritySearch.getDescription().contains("向量"));
|
||||
assertTrue(similaritySearch.getDescription().contains("相似度"));
|
||||
|
||||
// 验证标签
|
||||
assertNotNull(similaritySearch.getTags());
|
||||
assertTrue(similaritySearch.getTags().contains("search"));
|
||||
assertTrue(similaritySearch.getTags().contains("similarity"));
|
||||
assertTrue(similaritySearch.getTags().contains("vector"));
|
||||
assertTrue(similaritySearch.getTags().contains("ai"));
|
||||
|
||||
// 验证输入Schema - 应该有queryText, topK, threshold参数
|
||||
assertNotNull(similaritySearch.getInputSchema());
|
||||
assertNotNull(similaritySearch.getInputSchema().getProperties());
|
||||
assertTrue(similaritySearch.getInputSchema().getProperties().containsKey("queryText"));
|
||||
assertTrue(similaritySearch.getInputSchema().getProperties().containsKey("topK"));
|
||||
assertTrue(similaritySearch.getInputSchema().getProperties().containsKey("threshold"));
|
||||
|
||||
// 验证必需参数
|
||||
assertNotNull(similaritySearch.getInputSchema().getRequired());
|
||||
assertTrue(similaritySearch.getInputSchema().getRequired().contains("queryText"));
|
||||
assertFalse(similaritySearch.getInputSchema().getRequired().contains("topK")); // 有默认值,非必需
|
||||
assertFalse(similaritySearch.getInputSchema().getRequired().contains("threshold")); // 有默认值,非必需
|
||||
|
||||
// 验证资源需求 - 相似度搜索是高资源消耗的操作
|
||||
assertNotNull(similaritySearch.getResourceRequirements());
|
||||
assertEquals("high", similaritySearch.getResourceRequirements().getCpu());
|
||||
assertEquals("high", similaritySearch.getResourceRequirements().getMemory());
|
||||
assertEquals("high", similaritySearch.getResourceRequirements().getDatabase());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testKnowledgeQueryToolDefinition() {
|
||||
// 获取knowledge_query工具
|
||||
Optional<MCPToolDefinition> tool = toolRegistry.getTool("knowledge_query");
|
||||
assertTrue(tool.isPresent());
|
||||
|
||||
MCPToolDefinition knowledgeQuery = tool.get();
|
||||
|
||||
// 验证基本属性
|
||||
assertEquals("knowledge_query", knowledgeQuery.getName());
|
||||
assertTrue(knowledgeQuery.getDescription().contains("知识库"));
|
||||
|
||||
// 验证输入Schema - 应该有kbId和sourceRepairId参数
|
||||
assertNotNull(knowledgeQuery.getInputSchema());
|
||||
assertNotNull(knowledgeQuery.getInputSchema().getProperties());
|
||||
assertTrue(knowledgeQuery.getInputSchema().getProperties().containsKey("kbId"));
|
||||
assertTrue(knowledgeQuery.getInputSchema().getProperties().containsKey("sourceRepairId"));
|
||||
|
||||
// 验证必需参数 - 都不是必需的,但至少需要一个
|
||||
assertNotNull(knowledgeQuery.getInputSchema().getRequired());
|
||||
assertTrue(knowledgeQuery.getInputSchema().getRequired().isEmpty()); // 没有严格必需的参数
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAllToolsHaveValidSchema() {
|
||||
// 获取所有工具
|
||||
List<MCPToolDefinition> allTools = toolRegistry.getAllTools();
|
||||
|
||||
// 验证每个工具都有有效的Schema
|
||||
for (MCPToolDefinition tool : allTools) {
|
||||
// 基本属性验证
|
||||
assertNotNull(tool.getName(), "工具名称不能为空: " + tool);
|
||||
assertFalse(tool.getName().trim().isEmpty(), "工具名称不能为空字符串: " + tool);
|
||||
assertNotNull(tool.getDescription(), "工具描述不能为空: " + tool.getName());
|
||||
assertNotNull(tool.getType(), "工具类型不能为空: " + tool.getName());
|
||||
assertNotNull(tool.getVersion(), "工具版本不能为空: " + tool.getName());
|
||||
|
||||
// Schema验证
|
||||
assertNotNull(tool.getInputSchema(), "输入Schema不能为空: " + tool.getName());
|
||||
assertNotNull(tool.getInputSchema().getType(), "Schema类型不能为空: " + tool.getName());
|
||||
assertEquals("object", tool.getInputSchema().getType(), "Schema类型应该是object: " + tool.getName());
|
||||
|
||||
// 性能提示验证
|
||||
if (tool.getExpectedExecutionTime() != null) {
|
||||
assertTrue(tool.getExpectedExecutionTime().getMin() >= 0, "最小执行时间不能为负: " + tool.getName());
|
||||
if (tool.getExpectedExecutionTime().getMax() != null) {
|
||||
assertTrue(tool.getExpectedExecutionTime().getMax() >= tool.getExpectedExecutionTime().getMin(),
|
||||
"最大执行时间应该大于等于最小执行时间: " + tool.getName());
|
||||
}
|
||||
}
|
||||
|
||||
// 提示标志验证
|
||||
assertNotNull(tool.getReadOnlyHint(), "只读提示不能为空: " + tool.getName());
|
||||
assertNotNull(tool.getDestructiveHint(), "破坏性提示不能为空: " + tool.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,423 @@
|
|||
package com.chinaweal.youfool.devops.ai.mcp;
|
||||
|
||||
import com.chinaweal.youfool.devops.ai.mcp.dto.*;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* TrueMCPServer 单元测试
|
||||
*
|
||||
* 测试 MCP JSON-RPC 服务器的核心功能
|
||||
*
|
||||
* @author AI开发团队
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TrueMCPServerTest {
|
||||
|
||||
@Mock
|
||||
private MCPToolRegistry toolRegistry;
|
||||
|
||||
@Mock
|
||||
private MCPServer mcpServer;
|
||||
|
||||
@Mock
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@InjectMocks
|
||||
private TrueMCPServer trueMCPServer;
|
||||
|
||||
private MCPJsonRpcRequest request;
|
||||
private MCPToolDefinition testTool;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 创建测试工具定义
|
||||
testTool = new MCPToolDefinition();
|
||||
testTool.setName("test_tool");
|
||||
testTool.setDescription("测试工具");
|
||||
testTool.setType("function");
|
||||
|
||||
// 创建基础请求
|
||||
request = new MCPJsonRpcRequest();
|
||||
request.setJsonrpc("2.0");
|
||||
request.setId(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToolsList_Success() {
|
||||
// 准备数据
|
||||
request.setMethod("tools/list");
|
||||
List<MCPToolDefinition> tools = List.of(testTool);
|
||||
|
||||
when(toolRegistry.getAllTools()).thenReturn(tools);
|
||||
|
||||
// 执行测试
|
||||
MCPJsonRpcResponse response = trueMCPServer.toolsList(request);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(response);
|
||||
assertEquals("2.0", response.getJsonrpc());
|
||||
assertEquals(1, response.getId());
|
||||
assertNull(response.getError());
|
||||
assertNotNull(response.getResult());
|
||||
assertTrue(response.isSuccess());
|
||||
|
||||
verify(toolRegistry).getAllTools();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToolsList_InvalidRequest() {
|
||||
// 准备无效请求
|
||||
request.setJsonrpc("1.0"); // 错误的版本
|
||||
request.setMethod("tools/list");
|
||||
|
||||
// 执行测试
|
||||
MCPJsonRpcResponse response = trueMCPServer.toolsList(request);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(response);
|
||||
assertTrue(response.isError());
|
||||
assertNotNull(response.getError());
|
||||
assertEquals(MCPError.INVALID_REQUEST, response.getError().getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToolsList_MethodNotFound() {
|
||||
// 准备错误方法请求
|
||||
request.setMethod("unknown/method");
|
||||
|
||||
// 执行测试
|
||||
MCPJsonRpcResponse response = trueMCPServer.toolsList(request);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(response);
|
||||
assertTrue(response.isError());
|
||||
assertNotNull(response.getError());
|
||||
assertEquals(MCPError.METHOD_NOT_FOUND, response.getError().getCode());
|
||||
assertTrue(response.getError().getMessage().contains("unknown/method"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToolsList_WithPagination() {
|
||||
// 准备分页参数
|
||||
request.setMethod("tools/list");
|
||||
Map<String, Object> params = Map.of(
|
||||
"limit", 5,
|
||||
"offset", 0
|
||||
);
|
||||
request.setParams(params);
|
||||
|
||||
MCPToolListRequest listRequest = new MCPToolListRequest();
|
||||
listRequest.setLimit(5);
|
||||
listRequest.setOffset(0);
|
||||
|
||||
List<MCPToolDefinition> tools = List.of(testTool);
|
||||
|
||||
when(objectMapper.convertValue(params, MCPToolListRequest.class)).thenReturn(listRequest);
|
||||
when(toolRegistry.getToolsPaginated(0, 5)).thenReturn(tools);
|
||||
when(toolRegistry.getToolCount()).thenReturn(10);
|
||||
|
||||
// 执行测试
|
||||
MCPJsonRpcResponse response = trueMCPServer.toolsList(request);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(response);
|
||||
assertTrue(response.isSuccess());
|
||||
|
||||
verify(toolRegistry).getToolsPaginated(0, 5);
|
||||
verify(toolRegistry).getToolCount();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToolsCall_Success() {
|
||||
// 准备数据
|
||||
request.setMethod("tools/call");
|
||||
Map<String, Object> params = Map.of(
|
||||
"name", "test_tool",
|
||||
"arguments", Map.of("param1", "value1")
|
||||
);
|
||||
request.setParams(params);
|
||||
|
||||
MCPToolCallRequest callRequest = new MCPToolCallRequest();
|
||||
callRequest.setName("test_tool");
|
||||
callRequest.setArguments(Map.of("param1", "value1"));
|
||||
|
||||
MCPResponse mcpResponse = MCPResponse.success(Map.of("result", "success"));
|
||||
|
||||
when(objectMapper.convertValue(params, MCPToolCallRequest.class)).thenReturn(callRequest);
|
||||
when(toolRegistry.haseTool("test_tool")).thenReturn(true);
|
||||
when(toolRegistry.getTool("test_tool")).thenReturn(Optional.of(testTool));
|
||||
when(mcpServer.executeTool("test_tool", Map.of("param1", "value1"))).thenReturn(mcpResponse);
|
||||
|
||||
// 执行测试
|
||||
MCPJsonRpcResponse response = trueMCPServer.toolsCall(request);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(response);
|
||||
assertTrue(response.isSuccess());
|
||||
assertNotNull(response.getResult());
|
||||
|
||||
verify(mcpServer).executeTool("test_tool", Map.of("param1", "value1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToolsCall_InvalidMethod() {
|
||||
// 准备错误方法请求
|
||||
request.setMethod("tools/invalid");
|
||||
|
||||
// 执行测试
|
||||
MCPJsonRpcResponse response = trueMCPServer.toolsCall(request);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(response);
|
||||
assertTrue(response.isError());
|
||||
assertEquals(MCPError.METHOD_NOT_FOUND, response.getError().getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToolsCall_InvalidParams() {
|
||||
// 准备无效参数
|
||||
request.setMethod("tools/call");
|
||||
request.setParams(Map.of("invalid", "params"));
|
||||
|
||||
when(objectMapper.convertValue(any(), eq(MCPToolCallRequest.class))).thenReturn(null);
|
||||
|
||||
// 执行测试
|
||||
MCPJsonRpcResponse response = trueMCPServer.toolsCall(request);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(response);
|
||||
assertTrue(response.isError());
|
||||
assertEquals(MCPError.INVALID_PARAMS, response.getError().getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToolsCall_ToolNotFound() {
|
||||
// 准备数据
|
||||
request.setMethod("tools/call");
|
||||
Map<String, Object> params = Map.of(
|
||||
"name", "unknown_tool",
|
||||
"arguments", Map.of()
|
||||
);
|
||||
request.setParams(params);
|
||||
|
||||
MCPToolCallRequest callRequest = new MCPToolCallRequest();
|
||||
callRequest.setName("unknown_tool");
|
||||
callRequest.setArguments(Map.of());
|
||||
|
||||
when(objectMapper.convertValue(params, MCPToolCallRequest.class)).thenReturn(callRequest);
|
||||
when(toolRegistry.haseTool("unknown_tool")).thenReturn(false);
|
||||
|
||||
// 执行测试
|
||||
MCPJsonRpcResponse response = trueMCPServer.toolsCall(request);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(response);
|
||||
assertTrue(response.isError());
|
||||
assertEquals(MCPError.TOOL_NOT_FOUND, response.getError().getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToolsCall_ExecutionError() {
|
||||
// 准备数据
|
||||
request.setMethod("tools/call");
|
||||
Map<String, Object> params = Map.of(
|
||||
"name", "test_tool",
|
||||
"arguments", Map.of("param1", "value1")
|
||||
);
|
||||
request.setParams(params);
|
||||
|
||||
MCPToolCallRequest callRequest = new MCPToolCallRequest();
|
||||
callRequest.setName("test_tool");
|
||||
callRequest.setArguments(Map.of("param1", "value1"));
|
||||
|
||||
MCPResponse mcpResponse = MCPResponse.error("执行失败");
|
||||
|
||||
when(objectMapper.convertValue(params, MCPToolCallRequest.class)).thenReturn(callRequest);
|
||||
when(toolRegistry.haseTool("test_tool")).thenReturn(true);
|
||||
when(toolRegistry.getTool("test_tool")).thenReturn(Optional.of(testTool));
|
||||
when(mcpServer.executeTool("test_tool", Map.of("param1", "value1"))).thenReturn(mcpResponse);
|
||||
|
||||
// 执行测试
|
||||
MCPJsonRpcResponse response = trueMCPServer.toolsCall(request);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(response);
|
||||
assertTrue(response.isError());
|
||||
assertEquals(MCPError.TOOL_EXECUTION_ERROR, response.getError().getCode());
|
||||
assertTrue(response.getError().getMessage().contains("执行失败"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBatchToolsCall_Success() {
|
||||
// 准备批量调用数据
|
||||
request.setMethod("tools/batch-call");
|
||||
List<Map<String, Object>> batchParams = List.of(
|
||||
Map.of("name", "test_tool", "arguments", Map.of("param1", "value1")),
|
||||
Map.of("name", "test_tool", "arguments", Map.of("param1", "value2"))
|
||||
);
|
||||
request.setParams(batchParams);
|
||||
|
||||
MCPToolCallRequest callRequest1 = new MCPToolCallRequest();
|
||||
callRequest1.setName("test_tool");
|
||||
callRequest1.setArguments(Map.of("param1", "value1"));
|
||||
|
||||
MCPToolCallRequest callRequest2 = new MCPToolCallRequest();
|
||||
callRequest2.setName("test_tool");
|
||||
callRequest2.setArguments(Map.of("param1", "value2"));
|
||||
|
||||
MCPResponse mcpResponse1 = MCPResponse.success(Map.of("result", "success1"));
|
||||
MCPResponse mcpResponse2 = MCPResponse.success(Map.of("result", "success2"));
|
||||
|
||||
when(objectMapper.convertValue(batchParams.get(0), MCPToolCallRequest.class)).thenReturn(callRequest1);
|
||||
when(objectMapper.convertValue(batchParams.get(1), MCPToolCallRequest.class)).thenReturn(callRequest2);
|
||||
when(toolRegistry.haseTool("test_tool")).thenReturn(true);
|
||||
when(mcpServer.executeTool(eq("test_tool"), any())).thenReturn(mcpResponse1, mcpResponse2);
|
||||
|
||||
// 执行测试
|
||||
MCPJsonRpcResponse response = trueMCPServer.batchToolsCall(request);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(response);
|
||||
assertTrue(response.isSuccess());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> result = (Map<String, Object>) response.getResult();
|
||||
assertEquals(2, result.get("total_calls"));
|
||||
assertNotNull(result.get("results"));
|
||||
|
||||
verify(mcpServer, times(2)).executeTool(eq("test_tool"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBatchToolsCall_InvalidParams() {
|
||||
// 准备无效的批量调用参数
|
||||
request.setMethod("tools/batch-call");
|
||||
request.setParams("invalid"); // 应该是数组
|
||||
|
||||
// 执行测试
|
||||
MCPJsonRpcResponse response = trueMCPServer.batchToolsCall(request);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(response);
|
||||
assertTrue(response.isError());
|
||||
assertEquals(MCPError.INVALID_PARAMS, response.getError().getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBatchToolsCall_TooManyRequests() {
|
||||
// 准备过多的批量调用请求
|
||||
request.setMethod("tools/batch-call");
|
||||
List<Map<String, Object>> batchParams = List.of();
|
||||
for (int i = 0; i < 15; i++) { // 超过限制的10个
|
||||
batchParams = new java.util.ArrayList<>(batchParams);
|
||||
((java.util.ArrayList<Map<String, Object>>) batchParams).add(
|
||||
Map.of("name", "test_tool", "arguments", Map.of())
|
||||
);
|
||||
}
|
||||
request.setParams(batchParams);
|
||||
|
||||
// 执行测试
|
||||
MCPJsonRpcResponse response = trueMCPServer.batchToolsCall(request);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(response);
|
||||
assertTrue(response.isError());
|
||||
assertEquals(MCPError.INVALID_PARAMS, response.getError().getCode());
|
||||
assertTrue(response.getError().getMessage().contains("1-10"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHealth() {
|
||||
// 准备数据
|
||||
when(toolRegistry.getToolCount()).thenReturn(4);
|
||||
|
||||
// 执行测试
|
||||
Map<String, Object> health = trueMCPServer.health();
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(health);
|
||||
assertEquals("UP", health.get("status"));
|
||||
assertEquals("MCP", health.get("protocol"));
|
||||
assertEquals("2.0", health.get("jsonrpc_version"));
|
||||
assertEquals(4, health.get("available_tools"));
|
||||
assertNotNull(health.get("server_time"));
|
||||
assertNotNull(health.get("endpoints"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetProtocolInfo() {
|
||||
// 准备数据
|
||||
when(toolRegistry.getToolCount()).thenReturn(4);
|
||||
|
||||
// 执行测试
|
||||
Map<String, Object> protocolInfo = trueMCPServer.getProtocolInfo();
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(protocolInfo);
|
||||
assertEquals("Model Context Protocol", protocolInfo.get("protocol_name"));
|
||||
assertEquals("1.0", protocolInfo.get("protocol_version"));
|
||||
assertEquals("2.0", protocolInfo.get("jsonrpc_version"));
|
||||
assertEquals("youfool-devops-mcp-server", protocolInfo.get("implementation"));
|
||||
assertEquals(4, protocolInfo.get("tool_count"));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> supportedMethods = (List<String>) protocolInfo.get("supported_methods");
|
||||
assertTrue(supportedMethods.contains("tools/list"));
|
||||
assertTrue(supportedMethods.contains("tools/call"));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> capabilities = (Map<String, Object>) protocolInfo.get("server_capabilities");
|
||||
assertEquals(true, capabilities.get("tools"));
|
||||
assertEquals(false, capabilities.get("resources"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNotificationRequest() {
|
||||
// 准备通知请求(id为null)
|
||||
request.setId(null);
|
||||
request.setMethod("tools/list");
|
||||
|
||||
when(toolRegistry.getAllTools()).thenReturn(List.of(testTool));
|
||||
|
||||
// 执行测试
|
||||
MCPJsonRpcResponse response = trueMCPServer.toolsList(request);
|
||||
|
||||
// 验证结果 - 通知请求也应该返回响应(在我们的实现中)
|
||||
assertNotNull(response);
|
||||
assertNull(response.getId()); // 通知的响应ID应该为null
|
||||
assertTrue(response.isSuccess());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExceptionHandling() {
|
||||
// 准备数据
|
||||
request.setMethod("tools/list");
|
||||
|
||||
// 模拟异常
|
||||
when(toolRegistry.getAllTools()).thenThrow(new RuntimeException("数据库连接失败"));
|
||||
|
||||
// 执行测试
|
||||
MCPJsonRpcResponse response = trueMCPServer.toolsList(request);
|
||||
|
||||
// 验证结果
|
||||
assertNotNull(response);
|
||||
assertTrue(response.isError());
|
||||
assertEquals(MCPError.INTERNAL_ERROR, response.getError().getCode());
|
||||
assertTrue(response.getError().getMessage().contains("数据库连接失败"));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue