增加 SuperAgent MCP 内嵌接口和配置文档

This commit is contained in:
andy
2026-07-09 17:49:53 +08:00
parent a2119a8d06
commit e12bfd77e1
25 changed files with 2236 additions and 0 deletions

View File

@@ -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"));
}
}

View File

@@ -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"))));
}
}