diff --git a/pom.xml b/pom.xml index f8093ca942..f4559e379d 100644 --- a/pom.xml +++ b/pom.xml @@ -86,7 +86,7 @@ io.quarkiverse.mcp quarkus-mcp-server-bom - 1.13.0 + 2.0.3 pom import @@ -485,6 +485,7 @@ + ${project.version} ${argLine} org.jboss.logmanager.LogManager diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpHttpTestSupport.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpHttpTestSupport.java new file mode 100644 index 0000000000..737b565a0c --- /dev/null +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpHttpTestSupport.java @@ -0,0 +1,59 @@ +package io.stargate.sgv2.jsonapi.api.v1.mcp; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; + +import io.restassured.http.ContentType; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; +import io.vertx.core.json.JsonObject; +import java.util.HashMap; +import java.util.Map; + +/** Shared wire format for raw HTTP tests that do not initialize an MCP client session. */ +final class McpHttpTestSupport { + private static final String CURRENT_PROTOCOL_VERSION = "2026-07-28"; + + private McpHttpTestSupport() {} + + static RequestSpecification request(String method) { + return given() + .contentType(ContentType.JSON) + .accept("application/json, text/event-stream") + .header("MCP-Protocol-Version", CURRENT_PROTOCOL_VERSION) + .header("Mcp-Method", method); + } + + static Map toolPayload(int id, String tool, Map arguments) { + return rpcPayload(id, "tools/call", Map.of("name", tool, "arguments", arguments)); + } + + static Map rpcPayload(int id, String method, Map params) { + var requestParams = new HashMap<>(params); + requestParams.put( + "_meta", + Map.of( + "io.modelcontextprotocol/protocolVersion", + CURRENT_PROTOCOL_VERSION, + "io.modelcontextprotocol/clientCapabilities", + Map.of())); + return Map.of("jsonrpc", "2.0", "id", id, "method", method, "params", requestParams); + } + + static JsonObject assertResponseComplete(Response response, int id) { + assertThat(response.statusCode()).as(response.asString()).isEqualTo(200); + var body = new JsonObject(response.asString()); + assertThat(body.getInteger("id")).isEqualTo(id); + assertThat(body.containsKey("error")).isFalse(); + var result = body.getJsonObject("result"); + assertThat(result.getString("resultType")).isEqualTo("complete"); + return result; + } + + static JsonObject assertResultReadable(JsonObject result) { + var content = result.getJsonArray("content"); + assertThat(content).hasSize(1); + assertThat(content.getJsonObject(0).getString("type")).isEqualTo("text"); + return new JsonObject(content.getJsonObject(0).getString("text")); + } +} diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpIntegrationTestBase.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpIntegrationTestBase.java index 392bf69c8f..03a16ae209 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpIntegrationTestBase.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpIntegrationTestBase.java @@ -176,16 +176,25 @@ protected Consumer assertStatusOnlyWithJson(Consumer s }; } + private static void assertServerInfoOnly(ToolResponse response) { + assertThat(response._meta()).containsOnlyKeys(MetaKey.SERVER_INFO); + var serverInfo = (JsonObject) response._meta().get(MetaKey.SERVER_INFO); + assertThat(serverInfo.getString("name")).isEqualTo("Stargate DATA API"); + assertThat(serverInfo.getString("version")) + .isEqualTo( + ConfigProvider.getConfig().getValue("quarkus.application.version", String.class)); + } + /** - * Assert error response structure is valid (no meta_ and content), then apply additional - * assertions on the error JsonArray extracted from structuredContent. + * Assert that an error response has server metadata and an empty content field. Then apply + * additional assertions to the error JsonArray extracted from structuredContent. * * @param errorsAssertions additional assertions to run on the error JsonArray */ protected Consumer assertErrorOnly(Consumer errorsAssertions) { return response -> { assertThat(response.content()).isEmpty(); - assertThat(response._meta()).isEmpty(); + assertServerInfoOnly(response); assertTrue(response.isError()); assertThat(response.structuredContent()).isNotNull(); @@ -198,8 +207,8 @@ protected Consumer assertErrorOnly(Consumer errorsAsser } /** - * Assert data only response is valid (no meta_, content and error), then apply additional - * assertions on the structuredContent holds the data. + * Assert that a data response has server metadata, an empty content field, and no error. Then + * apply additional assertions to the data held in structuredContent. * * @param dataAssertions additional assertions to run on the data JsonObject */ @@ -207,7 +216,7 @@ protected Consumer assertDataOnly(Consumer dataAsserti return response -> { assertFalse(response.isError()); assertThat(response.content()).isEmpty(); - assertThat(response._meta()).isEmpty(); + assertServerInfoOnly(response); assertNotNull(response.structuredContent()); var data = (JsonObject) response.structuredContent(); diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpProtocolTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpProtocolTest.java new file mode 100644 index 0000000000..75a989ad23 --- /dev/null +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpProtocolTest.java @@ -0,0 +1,34 @@ +package io.stargate.sgv2.jsonapi.api.v1.mcp; + +import static io.stargate.sgv2.jsonapi.api.v1.mcp.McpHttpTestSupport.assertResponseComplete; +import static io.stargate.sgv2.jsonapi.api.v1.mcp.McpHttpTestSupport.request; +import static io.stargate.sgv2.jsonapi.api.v1.mcp.McpHttpTestSupport.rpcPayload; +import static org.assertj.core.api.Assertions.assertThat; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.stargate.sgv2.jsonapi.testresource.NoGlobalResourcesTestProfile; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** Exercises the HTTP wire contract without a client that might initialize a session for us. */ +@QuarkusTest +@TestProfile(NoGlobalResourcesTestProfile.Impl.class) +class McpProtocolTest { + @Test + void listsToolsWithoutInitializationOrSession() { + // Each POST is independent. clientInfo, discovery and initialization are deliberately absent. + for (int id = 1; id <= 2; id++) { + var response = + request("tools/list") + // Header auth checks only that a token is present. Database requests validate it, + // but tools/list returns metadata without accessing the database. + .header("Token", "protocol-test-credential") + .body(rpcPayload(id, "tools/list", Map.of())) + .post("/v1/mcp"); + assertResponseComplete(response, id); + assertThat(response.header("Mcp-Session-Id")).isNull(); + assertThat(response.jsonPath().getList("result.tools.name")).isNotEmpty(); + } + } +}