增加 SuperAgent MCP 内嵌接口和配置文档
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.common.request;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/**
|
||||
* SuperAgent MCP JSON-RPC 请求。MCP endpoint 只读取方法名、请求 ID 和参数节点。
|
||||
*
|
||||
* @param jsonrpc JSON-RPC 版本
|
||||
* @param id 请求 ID,可能是字符串或数字
|
||||
* @param method MCP 方法名
|
||||
* @param params 方法参数
|
||||
*/
|
||||
public record SuperAgentMcpJsonRpcRequest(
|
||||
String jsonrpc,
|
||||
JsonNode id,
|
||||
String method,
|
||||
JsonNode params
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.common.request;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/**
|
||||
* MCP tools/call 参数。name 决定工具路由,arguments 透传为具体工具入参。
|
||||
*
|
||||
* @param name 工具名称
|
||||
* @param arguments 工具入参
|
||||
*/
|
||||
public record SuperAgentMcpToolCallParams(
|
||||
String name,
|
||||
JsonNode arguments
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.common.result;
|
||||
|
||||
/**
|
||||
* MCP tool result 中的文本内容块。
|
||||
*
|
||||
* @param type 内容类型
|
||||
* @param text 文本内容
|
||||
*/
|
||||
public record SuperAgentMcpContentItem(
|
||||
String type,
|
||||
String text
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.common.result;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MCP JSON-RPC 错误对象。data.code 保留稳定业务错误码,便于 SuperAgent 判断原因。
|
||||
*
|
||||
* @param code JSON-RPC 错误码
|
||||
* @param message 中文错误说明
|
||||
* @param data 扩展错误数据
|
||||
*/
|
||||
public record SuperAgentMcpJsonRpcError(
|
||||
int code,
|
||||
String message,
|
||||
Map<String, Object> data
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* SuperAgent MCP JSON-RPC 响应。成功时返回 result,失败时返回 error。
|
||||
*
|
||||
* @param jsonrpc JSON-RPC 版本
|
||||
* @param id 请求 ID
|
||||
* @param result 成功结果
|
||||
* @param error 错误结果
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record SuperAgentMcpJsonRpcResponse(
|
||||
String jsonrpc,
|
||||
JsonNode id,
|
||||
Object result,
|
||||
SuperAgentMcpJsonRpcError error
|
||||
) {
|
||||
|
||||
/**
|
||||
* 构造 JSON-RPC 成功响应。
|
||||
*/
|
||||
public static SuperAgentMcpJsonRpcResponse success(JsonNode id, Object result) {
|
||||
return new SuperAgentMcpJsonRpcResponse("2.0", id, result, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 JSON-RPC 错误响应。
|
||||
*/
|
||||
public static SuperAgentMcpJsonRpcResponse error(JsonNode id, int rpcCode, String errorCode, String message) {
|
||||
return new SuperAgentMcpJsonRpcResponse(
|
||||
"2.0",
|
||||
id,
|
||||
null,
|
||||
new SuperAgentMcpJsonRpcError(rpcCode, message, Map.of("code", errorCode)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.common.result;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MCP tools/call 结果。content 给模型简短文本,structuredContent 给模型结构化业务数据。
|
||||
*
|
||||
* @param content 文本内容块
|
||||
* @param structuredContent 结构化结果
|
||||
* @param isError 是否工具级错误
|
||||
*/
|
||||
public record SuperAgentMcpToolCallResult(
|
||||
List<SuperAgentMcpContentItem> content,
|
||||
Object structuredContent,
|
||||
boolean isError
|
||||
) {
|
||||
|
||||
/**
|
||||
* 构造成功工具调用结果。
|
||||
*/
|
||||
public static SuperAgentMcpToolCallResult success(String summary, Object structuredContent) {
|
||||
return new SuperAgentMcpToolCallResult(
|
||||
List.of(new SuperAgentMcpContentItem("text", summary)),
|
||||
structuredContent,
|
||||
false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造工具级错误结果。MCP 协议仍返回 JSON-RPC 成功,错误放在 tool result 内。
|
||||
*/
|
||||
public static SuperAgentMcpToolCallResult error(String summary, Object structuredContent) {
|
||||
return new SuperAgentMcpToolCallResult(
|
||||
List.of(new SuperAgentMcpContentItem("text", summary)),
|
||||
structuredContent,
|
||||
true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.common.result;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MCP tool 定义。包含工具名、中文说明、输入 JSON Schema 和工具安全提示。
|
||||
*
|
||||
* @param name 工具名称
|
||||
* @param description 工具说明
|
||||
* @param inputSchema 输入 JSON Schema
|
||||
* @param annotations MCP 工具注解
|
||||
*/
|
||||
public record SuperAgentMcpToolDefinition(
|
||||
String name,
|
||||
String description,
|
||||
Map<String, Object> inputSchema,
|
||||
Map<String, Object> annotations
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.common.result;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MCP tools/list 响应。
|
||||
*
|
||||
* @param tools 可用工具列表
|
||||
*/
|
||||
public record SuperAgentMcpToolsListResult(
|
||||
List<SuperAgentMcpToolDefinition> tools
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.control;
|
||||
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.request.SuperAgentMcpJsonRpcRequest;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcpJsonRpcResponse;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.service.SuperAgentMcpService;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.service.impl.SuperAgentMcpProperties;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* SuperAgent 内嵌 MCP endpoint。只处理 MCP 鉴权、JSON-RPC 解析和服务分发。
|
||||
*/
|
||||
@RestController
|
||||
@ConditionalOnProperty(prefix = "mcp", name = "enabled", havingValue = "true")
|
||||
public class SuperAgentMcpController {
|
||||
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
|
||||
private final SuperAgentMcpService mcpService;
|
||||
private final SuperAgentMcpProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 注入 MCP 服务、配置和 JSON 解析器。
|
||||
*/
|
||||
public SuperAgentMcpController(
|
||||
SuperAgentMcpService mcpService,
|
||||
SuperAgentMcpProperties properties,
|
||||
ObjectMapper objectMapper) {
|
||||
this.mcpService = mcpService;
|
||||
this.properties = properties;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP HTTP 入口。当前实现支持单个 JSON-RPC 请求。
|
||||
*/
|
||||
@PostMapping(
|
||||
value = "${mcp.http-path:/mcp}",
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<SuperAgentMcpJsonRpcResponse> handle(
|
||||
@RequestBody(required = false) String rawBody,
|
||||
@RequestHeader(value = "Authorization", required = false) String authorization) {
|
||||
String requestBody = rawBody == null ? "" : rawBody;
|
||||
ResponseEntity<SuperAgentMcpJsonRpcResponse> bodyLimitFailure = rejectBodyWhenTooLarge(requestBody);
|
||||
if (bodyLimitFailure != null) {
|
||||
return bodyLimitFailure;
|
||||
}
|
||||
if (!validAuthorization(authorization)) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(SuperAgentMcpJsonRpcResponse.error(
|
||||
null,
|
||||
-32001,
|
||||
"MCP_AUTH_INVALID",
|
||||
"MCP 鉴权失败。"));
|
||||
}
|
||||
SuperAgentMcpJsonRpcRequest request = readRequest(requestBody);
|
||||
SuperAgentMcpJsonRpcResponse response = mcpService.handle(request);
|
||||
if (response == null) {
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 SuperAgent 到 MCP endpoint 的 Bearer token。
|
||||
*/
|
||||
private boolean validAuthorization(String authorization) {
|
||||
String authToken = properties.getAuthToken();
|
||||
if (!StringUtils.hasText(authToken) || !StringUtils.hasText(authorization)) {
|
||||
return false;
|
||||
}
|
||||
String expected = BEARER_PREFIX + authToken;
|
||||
return MessageDigest.isEqual(
|
||||
expected.getBytes(StandardCharsets.UTF_8),
|
||||
authorization.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* 限制 MCP 请求体大小,避免 SuperAgent 外部输入占用过多后端资源。
|
||||
*/
|
||||
private ResponseEntity<SuperAgentMcpJsonRpcResponse> rejectBodyWhenTooLarge(String rawBody) {
|
||||
long maxBodyBytes = properties.getMaxBodyBytes();
|
||||
int actualBytes = rawBody.getBytes(StandardCharsets.UTF_8).length;
|
||||
if (maxBodyBytes >= 0 && actualBytes > maxBodyBytes) {
|
||||
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
|
||||
.body(SuperAgentMcpJsonRpcResponse.error(
|
||||
null,
|
||||
-32002,
|
||||
"MCP_REQUEST_BODY_TOO_LARGE",
|
||||
"MCP 请求体超过允许大小。"));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JSON-RPC 请求体。
|
||||
*/
|
||||
private SuperAgentMcpJsonRpcRequest readRequest(String rawBody) {
|
||||
try {
|
||||
return objectMapper.readValue(rawBody, SuperAgentMcpJsonRpcRequest.class);
|
||||
} catch (JsonProcessingException exception) {
|
||||
throw new SuperAgentMcpRequestException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 MCP JSON 解析错误转换为稳定 JSON-RPC parse error。
|
||||
*/
|
||||
@ExceptionHandler(SuperAgentMcpRequestException.class)
|
||||
public ResponseEntity<SuperAgentMcpJsonRpcResponse> handleRequestException() {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(SuperAgentMcpJsonRpcResponse.error(
|
||||
null,
|
||||
-32700,
|
||||
"MCP_REQUEST_INVALID",
|
||||
"MCP 请求 JSON 不合法。"));
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP 请求 JSON 不合法时使用稳定异常,交给本 Controller 内部 advice 转换。
|
||||
*/
|
||||
private static class SuperAgentMcpRequestException extends RuntimeException {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.service;
|
||||
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.request.SuperAgentMcpJsonRpcRequest;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcpJsonRpcResponse;
|
||||
|
||||
/**
|
||||
* SuperAgent MCP 服务。负责 MCP 方法分发和工具调用,不直接暴露业务持久化细节。
|
||||
*/
|
||||
public interface SuperAgentMcpService {
|
||||
|
||||
/**
|
||||
* 处理单个 JSON-RPC 请求。MCP notification 不需要响应时返回 null。
|
||||
*/
|
||||
SuperAgentMcpJsonRpcResponse handle(SuperAgentMcpJsonRpcRequest request);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.service.impl;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* SuperAgent MCP 配置。MCP Auth Token 只能来自环境变量或部署平台 Secret。
|
||||
*/
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "mcp")
|
||||
public class SuperAgentMcpProperties {
|
||||
|
||||
/** 是否启用内嵌 MCP endpoint。 */
|
||||
private boolean enabled = false;
|
||||
/** MCP HTTP path,默认 /mcp。 */
|
||||
private String httpPath = "/mcp";
|
||||
/** SuperAgent 到 MCP endpoint 的 Bearer Token。 */
|
||||
private String authToken = "";
|
||||
/** 是否允许 MCP 写入工具提交任务结果。 */
|
||||
private boolean enableSubmitTaskResults = false;
|
||||
/** MCP 单次请求体最大字节数,默认 10MB。 */
|
||||
private long maxBodyBytes = 10_485_760L;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getHttpPath() {
|
||||
return httpPath;
|
||||
}
|
||||
|
||||
public void setHttpPath(String httpPath) {
|
||||
this.httpPath = httpPath;
|
||||
}
|
||||
|
||||
public String getAuthToken() {
|
||||
return authToken;
|
||||
}
|
||||
|
||||
public void setAuthToken(String authToken) {
|
||||
this.authToken = authToken;
|
||||
}
|
||||
|
||||
public boolean isEnableSubmitTaskResults() {
|
||||
return enableSubmitTaskResults;
|
||||
}
|
||||
|
||||
public void setEnableSubmitTaskResults(boolean enableSubmitTaskResults) {
|
||||
this.enableSubmitTaskResults = enableSubmitTaskResults;
|
||||
}
|
||||
|
||||
public long getMaxBodyBytes() {
|
||||
return maxBodyBytes;
|
||||
}
|
||||
|
||||
public void setMaxBodyBytes(long maxBodyBytes) {
|
||||
this.maxBodyBytes = maxBodyBytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.integrations.ai.superagent.service.impl.SuperAgentTaskResultException;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.request.SuperAgentMcpJsonRpcRequest;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.request.SuperAgentMcpToolCallParams;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcpJsonRpcResponse;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcpToolCallResult;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcpToolDefinition;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.common.result.SuperAgentMcpToolsListResult;
|
||||
import cn.nianxx.thhotel.integrations.mcp.superagent.service.SuperAgentMcpService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationAiCaseContextQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationAiObjectDetailQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationMessageConversationQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationAiQueryErrorResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationAiQueryResponse;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.SuperAgentTaskResultResponse;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationAiQueryService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationAiTaskIntakeService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.impl.ReservationAiQueryException;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.impl.ReservationAiTaskIntakeException;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* SuperAgent MCP 服务实现。只做 MCP 协议适配,业务查询和写入委托已有 Reservation 服务。
|
||||
*/
|
||||
@Service
|
||||
public class SuperAgentMcpServiceImpl implements SuperAgentMcpService {
|
||||
|
||||
private static final String METHOD_INITIALIZE = "initialize";
|
||||
private static final String METHOD_NOTIFICATIONS_INITIALIZED = "notifications/initialized";
|
||||
private static final String METHOD_TOOLS_LIST = "tools/list";
|
||||
private static final String METHOD_TOOLS_CALL = "tools/call";
|
||||
private static final String TOOL_QUERY_CASE_CONTEXT = "th_hotel_query_case_context";
|
||||
private static final String TOOL_QUERY_OBJECT_DETAIL = "th_hotel_query_object_detail";
|
||||
private static final String TOOL_LIST_CONVERSATION_TASKS = "th_hotel_list_message_conversation_tasks";
|
||||
private static final String TOOL_LIST_CONVERSATION_MESSAGES = "th_hotel_list_message_conversation_messages";
|
||||
private static final String TOOL_SUBMIT_TASK_RESULTS = "th_hotel_submit_task_results";
|
||||
private static final String MCP_CLIENT_ID = "superagent-mcp";
|
||||
|
||||
private final ReservationAiQueryService aiQueryService;
|
||||
private final ReservationAiTaskIntakeService intakeService;
|
||||
private final SuperAgentMcpProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 注入已有业务服务和 JSON 工具,MCP 层不直接访问 Mapper 或数据库。
|
||||
*/
|
||||
public SuperAgentMcpServiceImpl(
|
||||
ReservationAiQueryService aiQueryService,
|
||||
ReservationAiTaskIntakeService intakeService,
|
||||
SuperAgentMcpProperties properties,
|
||||
ObjectMapper objectMapper) {
|
||||
this.aiQueryService = aiQueryService;
|
||||
this.intakeService = intakeService;
|
||||
this.properties = properties;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分发 MCP JSON-RPC 方法。
|
||||
*/
|
||||
@Override
|
||||
public SuperAgentMcpJsonRpcResponse handle(SuperAgentMcpJsonRpcRequest request) {
|
||||
if (request == null || !StringUtils.hasText(request.method())) {
|
||||
return SuperAgentMcpJsonRpcResponse.error(null, -32600, "MCP_REQUEST_INVALID", "MCP 请求缺少 method。");
|
||||
}
|
||||
return switch (request.method()) {
|
||||
case METHOD_INITIALIZE -> SuperAgentMcpJsonRpcResponse.success(request.id(), initializeResult());
|
||||
case METHOD_NOTIFICATIONS_INITIALIZED -> null;
|
||||
case METHOD_TOOLS_LIST -> SuperAgentMcpJsonRpcResponse.success(
|
||||
request.id(),
|
||||
new SuperAgentMcpToolsListResult(toolDefinitions()));
|
||||
case METHOD_TOOLS_CALL -> callTool(request);
|
||||
default -> SuperAgentMcpJsonRpcResponse.error(
|
||||
request.id(),
|
||||
-32601,
|
||||
"MCP_METHOD_NOT_FOUND",
|
||||
"MCP 方法不存在。");
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 MCP 初始化信息。当前服务只声明 tools 能力。
|
||||
*/
|
||||
private Map<String, Object> initializeResult() {
|
||||
return Map.of(
|
||||
"protocolVersion", "2025-06-18",
|
||||
"capabilities", Map.of("tools", Map.of()),
|
||||
"serverInfo", Map.of(
|
||||
"name", "th-hotel-simple-superagent-mcp",
|
||||
"version", "0.1.0"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 tools/call。业务异常转换为 tool result,协议参数错误转换为 JSON-RPC error。
|
||||
*/
|
||||
private SuperAgentMcpJsonRpcResponse callTool(SuperAgentMcpJsonRpcRequest request) {
|
||||
SuperAgentMcpToolCallParams params;
|
||||
try {
|
||||
params = objectMapper.treeToValue(request.params(), SuperAgentMcpToolCallParams.class);
|
||||
} catch (JsonProcessingException | IllegalArgumentException exception) {
|
||||
return SuperAgentMcpJsonRpcResponse.error(
|
||||
request.id(),
|
||||
-32602,
|
||||
"MCP_TOOL_PARAMS_INVALID",
|
||||
"MCP 工具调用参数不合法。");
|
||||
}
|
||||
if (params == null || !StringUtils.hasText(params.name())) {
|
||||
return SuperAgentMcpJsonRpcResponse.error(
|
||||
request.id(),
|
||||
-32602,
|
||||
"MCP_TOOL_NAME_REQUIRED",
|
||||
"MCP 工具名称不能为空。");
|
||||
}
|
||||
|
||||
SuperAgentMcpToolCallResult result = dispatchTool(params.name(), safeArguments(params.arguments()));
|
||||
return SuperAgentMcpJsonRpcResponse.success(request.id(), result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按工具名分发到已有业务服务。
|
||||
*/
|
||||
private SuperAgentMcpToolCallResult dispatchTool(String toolName, JsonNode arguments) {
|
||||
try {
|
||||
return switch (toolName) {
|
||||
case TOOL_QUERY_CASE_CONTEXT -> callQueryCaseContext(arguments);
|
||||
case TOOL_QUERY_OBJECT_DETAIL -> callQueryObjectDetail(arguments);
|
||||
case TOOL_LIST_CONVERSATION_TASKS -> callListConversationTasks(arguments);
|
||||
case TOOL_LIST_CONVERSATION_MESSAGES -> callListConversationMessages(arguments);
|
||||
case TOOL_SUBMIT_TASK_RESULTS -> callSubmitTaskResults(arguments);
|
||||
default -> SuperAgentMcpToolCallResult.error(
|
||||
"MCP 工具不存在:" + toolName,
|
||||
errorStructuredContent("MCP_TOOL_NOT_FOUND", "MCP 工具不存在。", Map.of("tool", toolName)));
|
||||
};
|
||||
} catch (ReservationAiQueryException exception) {
|
||||
return SuperAgentMcpToolCallResult.error(
|
||||
"TH Hotel 查询失败:" + exception.getMessage(),
|
||||
queryFailure(exception));
|
||||
} catch (SuperAgentTaskResultException exception) {
|
||||
return SuperAgentMcpToolCallResult.error(
|
||||
"TH Hotel 写入失败:" + exception.getMessage(),
|
||||
errorStructuredContent(exception.getErrorCode(), exception.getMessage(), Map.of()));
|
||||
} catch (ReservationAiTaskIntakeException exception) {
|
||||
return SuperAgentMcpToolCallResult.error(
|
||||
"TH Hotel 写入失败:" + exception.getMessage(),
|
||||
errorStructuredContent(
|
||||
exception.getErrorCode(),
|
||||
exception.getMessage(),
|
||||
Map.of("http_status", exception.getStatus().value())));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return SuperAgentMcpToolCallResult.error(
|
||||
"MCP 工具参数不合法。",
|
||||
errorStructuredContent("MCP_TOOL_ARGUMENTS_INVALID", "MCP 工具参数不合法。", Map.of()));
|
||||
} catch (JsonProcessingException exception) {
|
||||
return SuperAgentMcpToolCallResult.error(
|
||||
"MCP 工具参数无法序列化。",
|
||||
errorStructuredContent("MCP_TOOL_ARGUMENTS_INVALID", "MCP 工具参数无法序列化。", Map.of()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用订单上下文查询工具。
|
||||
*/
|
||||
private SuperAgentMcpToolCallResult callQueryCaseContext(JsonNode arguments) {
|
||||
ReservationAiCaseContextQueryRequest request = objectMapper.convertValue(
|
||||
arguments,
|
||||
ReservationAiCaseContextQueryRequest.class);
|
||||
ReservationAiQueryResponse<?> response = ReservationAiQueryResponse.success(
|
||||
null,
|
||||
null,
|
||||
aiQueryService.queryCaseContext(request),
|
||||
List.of());
|
||||
return SuperAgentMcpToolCallResult.success(TOOL_QUERY_CASE_CONTEXT + " 调用成功。", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用对象详情查询工具。
|
||||
*/
|
||||
private SuperAgentMcpToolCallResult callQueryObjectDetail(JsonNode arguments) {
|
||||
ReservationAiObjectDetailQueryRequest request = objectMapper.convertValue(
|
||||
arguments,
|
||||
ReservationAiObjectDetailQueryRequest.class);
|
||||
ReservationAiQueryResponse<?> response = ReservationAiQueryResponse.success(
|
||||
null,
|
||||
null,
|
||||
aiQueryService.queryObjectDetail(request),
|
||||
List.of());
|
||||
return SuperAgentMcpToolCallResult.success(TOOL_QUERY_OBJECT_DETAIL + " 调用成功。", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用邮件会话任务查询工具。
|
||||
*/
|
||||
private SuperAgentMcpToolCallResult callListConversationTasks(JsonNode arguments) {
|
||||
ReservationMessageConversationQueryRequest request = objectMapper.convertValue(
|
||||
arguments,
|
||||
ReservationMessageConversationQueryRequest.class);
|
||||
ReservationAiQueryResponse<?> response = ReservationAiQueryResponse.success(
|
||||
null,
|
||||
null,
|
||||
aiQueryService.queryMessageConversationTasks(request),
|
||||
List.of());
|
||||
return SuperAgentMcpToolCallResult.success(TOOL_LIST_CONVERSATION_TASKS + " 调用成功。", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用邮件会话受控正文查询工具。
|
||||
*/
|
||||
private SuperAgentMcpToolCallResult callListConversationMessages(JsonNode arguments) {
|
||||
ReservationMessageConversationQueryRequest request = objectMapper.convertValue(
|
||||
arguments,
|
||||
ReservationMessageConversationQueryRequest.class);
|
||||
ReservationAiQueryResponse<?> response = ReservationAiQueryResponse.success(
|
||||
null,
|
||||
null,
|
||||
aiQueryService.queryMessageConversationMessages(request),
|
||||
List.of());
|
||||
return SuperAgentMcpToolCallResult.success(TOOL_LIST_CONVERSATION_MESSAGES + " 调用成功。", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用任务结果写入工具。生产是否启用由 MCP 独立开关控制。
|
||||
*/
|
||||
private SuperAgentMcpToolCallResult callSubmitTaskResults(JsonNode arguments) throws JsonProcessingException {
|
||||
if (!properties.isEnableSubmitTaskResults()) {
|
||||
return SuperAgentMcpToolCallResult.error(
|
||||
TOOL_SUBMIT_TASK_RESULTS + " 当前未启用。",
|
||||
errorStructuredContent(
|
||||
"MCP_TOOL_DISABLED",
|
||||
"MCP 写入工具未启用。",
|
||||
Map.of("tool", TOOL_SUBMIT_TASK_RESULTS)));
|
||||
}
|
||||
String rawBody = objectMapper.writeValueAsString(arguments);
|
||||
SuperAgentTaskResultResponse response = intakeService.accept(rawBody, MCP_CLIENT_ID, null);
|
||||
return SuperAgentMcpToolCallResult.success(TOOL_SUBMIT_TASK_RESULTS + " 调用成功。", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将查询异常转换为查询接口统一响应结构,便于 SuperAgent 复用既有解析逻辑。
|
||||
*/
|
||||
private ReservationAiQueryResponse<?> queryFailure(ReservationAiQueryException exception) {
|
||||
return ReservationAiQueryResponse.failure(
|
||||
null,
|
||||
null,
|
||||
new ReservationAiQueryErrorResult(
|
||||
exception.getErrorCode(),
|
||||
exception.getMessage(),
|
||||
exception.getDetails()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 MCP 工具错误结构。
|
||||
*/
|
||||
private Map<String, Object> errorStructuredContent(String code, String message, Map<String, Object> details) {
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"error", Map.of(
|
||||
"code", code,
|
||||
"message", message,
|
||||
"details", details == null ? Map.of() : details));
|
||||
}
|
||||
|
||||
/**
|
||||
* arguments 为空时按空对象处理,避免工具实现处理 null 节点。
|
||||
*/
|
||||
private JsonNode safeArguments(JsonNode arguments) {
|
||||
if (arguments == null || arguments.isNull()) {
|
||||
return objectMapper.createObjectNode();
|
||||
}
|
||||
return arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP tools/list 工具定义。
|
||||
*/
|
||||
private List<SuperAgentMcpToolDefinition> toolDefinitions() {
|
||||
return List.of(
|
||||
new SuperAgentMcpToolDefinition(
|
||||
TOOL_QUERY_CASE_CONTEXT,
|
||||
"查询订单上下文,用于判断当前邮件是否匹配已有订单、任务或需要人工复核。",
|
||||
caseContextSchema(),
|
||||
readOnlyAnnotations()),
|
||||
new SuperAgentMcpToolDefinition(
|
||||
TOOL_QUERY_OBJECT_DETAIL,
|
||||
"查询指定订单对象详情,第一版主要支持 ORDER:{order_id}。",
|
||||
objectDetailSchema(),
|
||||
readOnlyAnnotations()),
|
||||
new SuperAgentMcpToolDefinition(
|
||||
TOOL_LIST_CONVERSATION_TASKS,
|
||||
"查询邮件会话下已有任务,按邮件接收时间和任务创建时间正序返回。",
|
||||
conversationQuerySchema(),
|
||||
readOnlyAnnotations()),
|
||||
new SuperAgentMcpToolDefinition(
|
||||
TOOL_LIST_CONVERSATION_MESSAGES,
|
||||
"查询邮件会话下受控正文,只返回清洗后的正文并触发后端访问审计。",
|
||||
conversationQuerySchema(),
|
||||
readOnlyAnnotations()),
|
||||
new SuperAgentMcpToolDefinition(
|
||||
TOOL_SUBMIT_TASK_RESULTS,
|
||||
"提交 SuperAgent AI 任务结果,会写入 AI 过渡层、订单、任务和任务卡。",
|
||||
submitTaskResultsSchema(),
|
||||
writeAnnotations()));
|
||||
}
|
||||
|
||||
private Map<String, Object> caseContextSchema() {
|
||||
Map<String, Object> propertiesMap = new LinkedHashMap<>();
|
||||
propertiesMap.put("hotel_id", stringField("酒店上下文 ID"));
|
||||
propertiesMap.put("group_code", nullableStringField("Group / Allotment 查询 key"));
|
||||
propertiesMap.put("confirmation_number", nullableStringField("FIT confirmation number 查询 key"));
|
||||
propertiesMap.put("reservation_no", nullableStringField("OPERA reservation no"));
|
||||
propertiesMap.put("object_type_hint", nullableStringField("调用方推测的对象类型"));
|
||||
propertiesMap.put("target_key_source", nullableStringField("key 来源,例如 body_current"));
|
||||
propertiesMap.put("body_thread_used_only_as_evidence", Map.of("type", "boolean", "description", "历史线程 key 是否仅作为证据"));
|
||||
return objectSchema(propertiesMap, List.of("hotel_id"));
|
||||
}
|
||||
|
||||
private Map<String, Object> objectDetailSchema() {
|
||||
Map<String, Object> propertiesMap = new LinkedHashMap<>();
|
||||
propertiesMap.put("hotel_id", stringField("酒店上下文 ID"));
|
||||
propertiesMap.put("object_id", stringField("查询对象 ID,第一版支持 ORDER:{order_id}"));
|
||||
propertiesMap.put("object_type", nullableStringField("调用方对象类型提示"));
|
||||
return objectSchema(propertiesMap, List.of("hotel_id", "object_id"));
|
||||
}
|
||||
|
||||
private Map<String, Object> conversationQuerySchema() {
|
||||
Map<String, Object> propertiesMap = new LinkedHashMap<>();
|
||||
propertiesMap.put("hotel_id", stringField("酒店上下文 ID"));
|
||||
propertiesMap.put("source_provider", nullableStringField("来源提供方,默认 AGENTBUS"));
|
||||
propertiesMap.put("source_channel", nullableStringField("来源渠道,默认 EMAIL"));
|
||||
propertiesMap.put("external_conversation_id", nullableStringField("外部邮件会话 ID"));
|
||||
propertiesMap.put("source_message_id", nullableStringField("外部来源消息 ID,可作为锚点反查会话"));
|
||||
return objectSchema(propertiesMap, List.of("hotel_id"));
|
||||
}
|
||||
|
||||
private Map<String, Object> submitTaskResultsSchema() {
|
||||
Map<String, Object> propertiesMap = new LinkedHashMap<>();
|
||||
propertiesMap.put("hotel_id", stringField("酒店上下文 ID"));
|
||||
propertiesMap.put("source_provider", nullableStringField("来源提供方,默认 AGENTBUS"));
|
||||
propertiesMap.put("source_channel", nullableStringField("来源渠道,默认 EMAIL"));
|
||||
propertiesMap.put("source_message_id", stringField("外部来源消息 ID,对应 AgentBus source.external_message_id"));
|
||||
propertiesMap.put("ai_task_results", Map.of(
|
||||
"type", "array",
|
||||
"description", "AI 拆分出的任务结果,必须保留数组顺序",
|
||||
"items", Map.of("type", "object")));
|
||||
propertiesMap.put("extraction_warnings", Map.of(
|
||||
"type", "array",
|
||||
"description", "AI 抽取警告",
|
||||
"items", Map.of("type", "object")));
|
||||
return objectSchema(propertiesMap, List.of("hotel_id", "source_message_id", "ai_task_results"));
|
||||
}
|
||||
|
||||
private Map<String, Object> objectSchema(Map<String, Object> propertiesMap, List<String> required) {
|
||||
return Map.of(
|
||||
"type", "object",
|
||||
"additionalProperties", false,
|
||||
"properties", propertiesMap,
|
||||
"required", required);
|
||||
}
|
||||
|
||||
private Map<String, Object> stringField(String description) {
|
||||
return Map.of("type", "string", "description", description);
|
||||
}
|
||||
|
||||
private Map<String, Object> nullableStringField(String description) {
|
||||
return Map.of("type", List.of("string", "null"), "description", description);
|
||||
}
|
||||
|
||||
private Map<String, Object> readOnlyAnnotations() {
|
||||
return Map.of(
|
||||
"readOnlyHint", true,
|
||||
"destructiveHint", false,
|
||||
"openWorldHint", false);
|
||||
}
|
||||
|
||||
private Map<String, Object> writeAnnotations() {
|
||||
return Map.of(
|
||||
"readOnlyHint", false,
|
||||
"destructiveHint", true,
|
||||
"openWorldHint", false);
|
||||
}
|
||||
}
|
||||
@@ -27,5 +27,13 @@ superagent:
|
||||
nonce-ttl-seconds: 600
|
||||
max-body-bytes: 1048576
|
||||
|
||||
mcp:
|
||||
# SuperAgent MCP 默认关闭;启用时必须通过部署环境配置高熵 Bearer Token。
|
||||
enabled: ${MCP_ENABLED:false}
|
||||
http-path: ${MCP_HTTP_PATH:/mcp}
|
||||
auth-token: ${MCP_AUTH_TOKEN:}
|
||||
enable-submit-task-results: ${MCP_ENABLE_SUBMIT_TASK_RESULTS:false}
|
||||
max-body-bytes: ${MCP_MAX_BODY_BYTES:10485760}
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.control;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import cn.nianxx.thhotel.ThHotelApplication;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@SpringBootTest(
|
||||
classes = ThHotelApplication.class,
|
||||
properties = {
|
||||
"mcp.enabled=true",
|
||||
"mcp.auth-token=test-mcp-token",
|
||||
"mcp.enable-submit-task-results=false",
|
||||
"mcp.max-body-bytes=12000"
|
||||
})
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class SuperAgentMcpControllerTest {
|
||||
|
||||
private static final String ENDPOINT = "/mcp";
|
||||
private static final String AUTHORIZATION = "Bearer test-mcp-token";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
void shouldRejectMcpRequestWithoutBearerToken() throws Exception {
|
||||
String body = """
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "mcp-auth-001",
|
||||
"method": "tools/list",
|
||||
"params": {}
|
||||
}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post(ENDPOINT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(body))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.jsonrpc").value("2.0"))
|
||||
.andExpect(jsonPath("$.error.data.code").value("MCP_AUTH_INVALID"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnJsonRpcParseErrorWhenRequestBodyInvalid() throws Exception {
|
||||
mockMvc.perform(post(ENDPOINT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", AUTHORIZATION)
|
||||
.content("{invalid-json"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.jsonrpc").value("2.0"))
|
||||
.andExpect(jsonPath("$.error.code").value(-32700))
|
||||
.andExpect(jsonPath("$.error.data.code").value("MCP_REQUEST_INVALID"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectMcpRequestWhenBodyLargerThanConfiguredLimit() throws Exception {
|
||||
String body = """
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "mcp-body-too-large-001",
|
||||
"method": "tools/list",
|
||||
"params": {
|
||||
"padding": "%s"
|
||||
}
|
||||
}
|
||||
""".formatted("x".repeat(12_100));
|
||||
|
||||
mockMvc.perform(post(ENDPOINT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", AUTHORIZATION)
|
||||
.content(body))
|
||||
.andExpect(status().isPayloadTooLarge())
|
||||
.andExpect(jsonPath("$.jsonrpc").value("2.0"))
|
||||
.andExpect(jsonPath("$.error.data.code").value("MCP_REQUEST_BODY_TOO_LARGE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptInitializedNotificationWithoutJsonRpcResponse() throws Exception {
|
||||
String body = """
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/initialized",
|
||||
"params": {}
|
||||
}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post(ENDPOINT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", AUTHORIZATION)
|
||||
.content(body))
|
||||
.andExpect(status().isAccepted())
|
||||
.andExpect(content().string(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldListFiveSuperAgentMcpTools() throws Exception {
|
||||
String body = """
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "mcp-tools-001",
|
||||
"method": "tools/list",
|
||||
"params": {}
|
||||
}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post(ENDPOINT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", AUTHORIZATION)
|
||||
.content(body))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.jsonrpc").value("2.0"))
|
||||
.andExpect(jsonPath("$.id").value("mcp-tools-001"))
|
||||
.andExpect(jsonPath("$.result.tools.length()").value(5))
|
||||
.andExpect(jsonPath("$.result.tools[0].name").value("th_hotel_query_case_context"))
|
||||
.andExpect(jsonPath("$.result.tools[0].annotations.readOnlyHint").value(true))
|
||||
.andExpect(jsonPath("$.result.tools[3].name").value("th_hotel_list_message_conversation_messages"))
|
||||
.andExpect(jsonPath("$.result.tools[3].annotations.readOnlyHint").value(true))
|
||||
.andExpect(jsonPath("$.result.tools[4].name").value("th_hotel_submit_task_results"))
|
||||
.andExpect(jsonPath("$.result.tools[4].annotations.readOnlyHint").value(false))
|
||||
.andExpect(jsonPath("$.result.tools[4].annotations.destructiveHint").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCallCaseContextToolThroughEmbeddedMcpEndpoint() throws Exception {
|
||||
String body = """
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "mcp-call-case-context-001",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "th_hotel_query_case_context",
|
||||
"arguments": {
|
||||
"hotel_id": "HOTEL-TEST",
|
||||
"group_code": "GRP-MCP-NOT-FOUND"
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post(ENDPOINT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", AUTHORIZATION)
|
||||
.content(body))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.jsonrpc").value("2.0"))
|
||||
.andExpect(jsonPath("$.id").value("mcp-call-case-context-001"))
|
||||
.andExpect(jsonPath("$.result.isError").value(false))
|
||||
.andExpect(jsonPath("$.result.structuredContent.success").value(true))
|
||||
.andExpect(jsonPath("$.result.structuredContent.data.matched_order_records.length()").value(0))
|
||||
.andExpect(jsonPath("$.result.structuredContent.data.target_object_validation.status").value("none"))
|
||||
.andExpect(content().string(containsString("th_hotel_query_case_context")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectSubmitTaskResultsToolWhenWriteToolDisabled() throws Exception {
|
||||
String body = """
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "mcp-submit-disabled-001",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "th_hotel_submit_task_results",
|
||||
"arguments": {
|
||||
"hotel_id": "HOTEL-TEST",
|
||||
"source_message_id": "mail-mcp-disabled-001",
|
||||
"ai_task_results": []
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post(ENDPOINT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", AUTHORIZATION)
|
||||
.content(body))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.jsonrpc").value("2.0"))
|
||||
.andExpect(jsonPath("$.id").value("mcp-submit-disabled-001"))
|
||||
.andExpect(jsonPath("$.result.isError").value(true))
|
||||
.andExpect(jsonPath("$.result.structuredContent.error.code").value("MCP_TOOL_DISABLED"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package cn.nianxx.thhotel.integrations.mcp.superagent.control;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import cn.nianxx.thhotel.ThHotelApplication;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@SpringBootTest(
|
||||
classes = ThHotelApplication.class,
|
||||
properties = {
|
||||
"mcp.enabled=true",
|
||||
"mcp.auth-token=test-mcp-token",
|
||||
"mcp.enable-submit-task-results=true",
|
||||
"mcp.max-body-bytes=12000"
|
||||
})
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class SuperAgentMcpSubmitEnabledControllerTest {
|
||||
|
||||
private static final String ENDPOINT = "/mcp";
|
||||
private static final String AUTHORIZATION = "Bearer test-mcp-token";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
void shouldDelegateSubmitTaskResultsToolWhenWriteToolEnabled() throws Exception {
|
||||
String body = """
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "mcp-submit-enabled-001",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "th_hotel_submit_task_results",
|
||||
"arguments": {
|
||||
"hotel_id": "HOTEL-TEST",
|
||||
"source_message_id": "mail-mcp-enabled-missing-001",
|
||||
"ai_task_results": [
|
||||
{
|
||||
"source_event_index": 1,
|
||||
"catalog_code": "S01",
|
||||
"skill_id": "S01_new_booking_skill",
|
||||
"result_type": "normal_task",
|
||||
"task_type": "NEW_BOOKING"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
mockMvc.perform(post(ENDPOINT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", AUTHORIZATION)
|
||||
.content(body))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.jsonrpc").value("2.0"))
|
||||
.andExpect(jsonPath("$.id").value("mcp-submit-enabled-001"))
|
||||
.andExpect(jsonPath("$.result.isError").value(true))
|
||||
.andExpect(jsonPath("$.result.structuredContent.error.code").value("SOURCE_MESSAGE_NOT_FOUND"))
|
||||
.andExpect(content().string(not(containsString("MCP_TOOL_DISABLED"))));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user