From 027c386d9c6cd809bfbc6a2a659b4a3cc3f981f1 Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Mon, 12 Oct 2026 16:39:50 -0700 Subject: [PATCH 1/6] test: reproduce rejection of stateless MCP 2026-07-28 requests RED: McpProtocolTest executes against MCP 1.13.0 and fails because the first tools/list POST returns HTTP 400 instead of 200. --- .../jsonapi/api/v1/mcp/McpProtocolTest.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpProtocolTest.java 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..44b23f1690 --- /dev/null +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpProtocolTest.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.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.restassured.http.ContentType; +import io.restassured.response.Response; +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 { + private static final String PROTOCOL_VERSION = "2026-07-28"; + + @Test + void listsToolsWithoutInitializationOrSession() { + // Each POST is independent. clientInfo, discovery and initialization are deliberately absent. + for (int id = 1; id <= 2; id++) { + Response response = request("tools/list", PROTOCOL_VERSION, id); + assertThat(response.statusCode()).as(response.asString()).isEqualTo(200); + assertThat(response.header("Mcp-Session-Id")).isNull(); + assertThat(response.jsonPath().getInt("id")).isEqualTo(id); + assertThat(response.jsonPath().getList("result.tools.name")).isNotEmpty(); + assertThat(response.jsonPath().getString("result.resultType")).isEqualTo("complete"); + } + } + + private Response request(String method, String version, int id) { + return given() + .contentType(ContentType.JSON) + .accept("application/json, text/event-stream") + // Existing header identity fixture only. OAuth admission is tracked separately in #2642. + .header("Token", "protocol-test-credential") + .header("MCP-Protocol-Version", version) + .header("Mcp-Method", method) + .body( + Map.of( + "jsonrpc", + "2.0", + "id", + id, + "method", + method, + "params", + Map.of( + "_meta", + Map.of( + "io.modelcontextprotocol/protocolVersion", + version, + "io.modelcontextprotocol/clientCapabilities", + Map.of())))) + .post("/v1/mcp"); + } +} From b2088a9bd442510a95b549ad335176547435e320 Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Mon, 12 Oct 2026 16:40:59 -0700 Subject: [PATCH 2/6] fix: upgrade MCP runtime for stateless protocol requests GREEN: the same raw HTTP McpProtocolTest passes with MCP BOM 2.0.3 on Quarkus 3.38.1 and JDK 21. Both independent tools/list requests return complete results without Mcp-Session-Id. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f8093ca942..9ab11340f1 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 From bdc563c29305de74340e23eecc6a85c680325ab1 Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Mon, 12 Oct 2026 17:14:41 -0700 Subject: [PATCH 3/6] test: expect framework server metadata in MCP responses Both DSE and HCD CI exposed four obsolete empty-metadata assertions after the framework upgrade. Require only serverInfo and validate its name and version. The two affected DSE integration classes pass locally: 17 tests, zero failures. --- .../api/v1/mcp/McpIntegrationTestBase.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) 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..e2f7addc46 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,23 @@ 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")).isNotBlank(); + } + /** - * Assert error response structure is valid (no meta_ and content), then apply additional - * assertions on the error JsonArray extracted from structuredContent. + * Assert error response structure is valid (server metadata and no text content), then apply + * additional assertions on 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 +205,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 data only response is valid (server metadata, no text content or error), then apply + * additional assertions on the structuredContent holds the data. * * @param dataAssertions additional assertions to run on the data JsonObject */ @@ -207,7 +214,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(); From 5fb06da01702eee857dc6521752a6b78178c6a5c Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Thu, 22 Oct 2026 10:30:31 -0700 Subject: [PATCH 4/6] Share MCP HTTP test support and run CI for stacked PRs --- .github/workflows/continuous-integration.yaml | 2 +- .../api/v1/mcp/McpHttpTestSupport.java | 59 +++++++++++++++++++ .../jsonapi/api/v1/mcp/McpProtocolTest.java | 46 ++++----------- 3 files changed, 70 insertions(+), 37 deletions(-) create mode 100644 src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpHttpTestSupport.java diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 2a3541648c..db25a40c5f 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -9,7 +9,7 @@ on: branches: [ "main", "feature/*" ] pull_request: - branches: [ "main", "feature/*" ] + branches: [ "main", "feature/*", "fix/mcp-*" ] workflow_dispatch: 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..8cbe11b44f --- /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 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", 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", + PROTOCOL_VERSION, + "io.modelcontextprotocol/clientCapabilities", + Map.of())); + return Map.of("jsonrpc", "2.0", "id", id, "method", method, "params", requestParams); + } + + static JsonObject completeResult(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 readableResult(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/McpProtocolTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpProtocolTest.java index 44b23f1690..3b96f6bd36 100644 --- 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 @@ -1,12 +1,12 @@ package io.stargate.sgv2.jsonapi.api.v1.mcp; -import static io.restassured.RestAssured.given; +import static io.stargate.sgv2.jsonapi.api.v1.mcp.McpHttpTestSupport.completeResult; +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.restassured.http.ContentType; -import io.restassured.response.Response; import io.stargate.sgv2.jsonapi.testresource.NoGlobalResourcesTestProfile; import java.util.Map; import org.junit.jupiter.api.Test; @@ -15,45 +15,19 @@ @QuarkusTest @TestProfile(NoGlobalResourcesTestProfile.Impl.class) class McpProtocolTest { - private static final String PROTOCOL_VERSION = "2026-07-28"; - @Test void listsToolsWithoutInitializationOrSession() { // Each POST is independent. clientInfo, discovery and initialization are deliberately absent. for (int id = 1; id <= 2; id++) { - Response response = request("tools/list", PROTOCOL_VERSION, id); - assertThat(response.statusCode()).as(response.asString()).isEqualTo(200); + var response = + request("tools/list") + // Existing header identity fixture only. OAuth admission is tracked in #2642. + .header("Token", "protocol-test-credential") + .body(rpcPayload(id, "tools/list", Map.of())) + .post("/v1/mcp"); + completeResult(response, id); assertThat(response.header("Mcp-Session-Id")).isNull(); - assertThat(response.jsonPath().getInt("id")).isEqualTo(id); assertThat(response.jsonPath().getList("result.tools.name")).isNotEmpty(); - assertThat(response.jsonPath().getString("result.resultType")).isEqualTo("complete"); } } - - private Response request(String method, String version, int id) { - return given() - .contentType(ContentType.JSON) - .accept("application/json, text/event-stream") - // Existing header identity fixture only. OAuth admission is tracked separately in #2642. - .header("Token", "protocol-test-credential") - .header("MCP-Protocol-Version", version) - .header("Mcp-Method", method) - .body( - Map.of( - "jsonrpc", - "2.0", - "id", - id, - "method", - method, - "params", - Map.of( - "_meta", - Map.of( - "io.modelcontextprotocol/protocolVersion", - version, - "io.modelcontextprotocol/clientCapabilities", - Map.of())))) - .post("/v1/mcp"); - } } From d3c2429b0fbb65c09442e4431c28cfa8bc96804a Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Fri, 23 Oct 2026 09:25:30 -0700 Subject: [PATCH 5/6] ci: restore the original pull request branch filter --- .github/workflows/continuous-integration.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index db25a40c5f..2a3541648c 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -9,7 +9,7 @@ on: branches: [ "main", "feature/*" ] pull_request: - branches: [ "main", "feature/*", "fix/mcp-*" ] + branches: [ "main", "feature/*" ] workflow_dispatch: From c6b41219bf32a74d88fffc493618c04e2e00caac Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Fri, 23 Oct 2026 13:00:19 -0700 Subject: [PATCH 6/6] Address MCP protocol test review comments --- pom.xml | 1 + .../sgv2/jsonapi/api/v1/mcp/McpHttpTestSupport.java | 10 +++++----- .../jsonapi/api/v1/mcp/McpIntegrationTestBase.java | 12 +++++++----- .../sgv2/jsonapi/api/v1/mcp/McpProtocolTest.java | 7 ++++--- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/pom.xml b/pom.xml index 9ab11340f1..f4559e379d 100644 --- a/pom.xml +++ b/pom.xml @@ -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 index 8cbe11b44f..737b565a0c 100644 --- 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 @@ -12,7 +12,7 @@ /** Shared wire format for raw HTTP tests that do not initialize an MCP client session. */ final class McpHttpTestSupport { - private static final String PROTOCOL_VERSION = "2026-07-28"; + private static final String CURRENT_PROTOCOL_VERSION = "2026-07-28"; private McpHttpTestSupport() {} @@ -20,7 +20,7 @@ static RequestSpecification request(String method) { return given() .contentType(ContentType.JSON) .accept("application/json, text/event-stream") - .header("MCP-Protocol-Version", PROTOCOL_VERSION) + .header("MCP-Protocol-Version", CURRENT_PROTOCOL_VERSION) .header("Mcp-Method", method); } @@ -34,13 +34,13 @@ static Map rpcPayload(int id, String method, Map "_meta", Map.of( "io.modelcontextprotocol/protocolVersion", - PROTOCOL_VERSION, + CURRENT_PROTOCOL_VERSION, "io.modelcontextprotocol/clientCapabilities", Map.of())); return Map.of("jsonrpc", "2.0", "id", id, "method", method, "params", requestParams); } - static JsonObject completeResult(Response response, int id) { + 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); @@ -50,7 +50,7 @@ static JsonObject completeResult(Response response, int id) { return result; } - static JsonObject readableResult(JsonObject result) { + static JsonObject assertResultReadable(JsonObject result) { var content = result.getJsonArray("content"); assertThat(content).hasSize(1); assertThat(content.getJsonObject(0).getString("type")).isEqualTo("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 e2f7addc46..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 @@ -180,12 +180,14 @@ 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")).isNotBlank(); + assertThat(serverInfo.getString("version")) + .isEqualTo( + ConfigProvider.getConfig().getValue("quarkus.application.version", String.class)); } /** - * Assert error response structure is valid (server metadata and no text 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 */ @@ -205,8 +207,8 @@ protected Consumer assertErrorOnly(Consumer errorsAsser } /** - * Assert data only response is valid (server metadata, no text content or 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 */ 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 index 3b96f6bd36..75a989ad23 100644 --- 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 @@ -1,6 +1,6 @@ package io.stargate.sgv2.jsonapi.api.v1.mcp; -import static io.stargate.sgv2.jsonapi.api.v1.mcp.McpHttpTestSupport.completeResult; +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; @@ -21,11 +21,12 @@ void listsToolsWithoutInitializationOrSession() { for (int id = 1; id <= 2; id++) { var response = request("tools/list") - // Existing header identity fixture only. OAuth admission is tracked in #2642. + // 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"); - completeResult(response, id); + assertResponseComplete(response, id); assertThat(response.header("Mcp-Session-Id")).isNull(); assertThat(response.jsonPath().getList("result.tools.name")).isNotEmpty(); }