Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
<dependency>
<groupId>io.quarkiverse.mcp</groupId>
<artifactId>quarkus-mcp-server-bom</artifactId>
<version>1.13.0</version>
<version>2.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Expand Down Expand Up @@ -485,6 +485,7 @@
</goals>
<configuration>
<systemPropertyVariables>
<quarkus.application.version>${project.version}</quarkus.application.version>
<!-- Pass JaCoCo agent args to Quarkus application JVM for @QuarkusIntegrationTest coverage -->
<quarkus.test.arg-line>${argLine}</quarkus.test.arg-line>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Object> toolPayload(int id, String tool, Map<String, Object> arguments) {
return rpcPayload(id, "tools/call", Map.of("name", tool, "arguments", arguments));
}

static Map<String, Object> rpcPayload(int id, String method, Map<String, Object> 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"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,16 +176,25 @@ protected Consumer<ToolResponse> assertStatusOnlyWithJson(Consumer<JsonObject> 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<ToolResponse> assertErrorOnly(Consumer<JsonArray> errorsAssertions) {
return response -> {
assertThat(response.content()).isEmpty();
assertThat(response._meta()).isEmpty();
assertServerInfoOnly(response);
assertTrue(response.isError());
assertThat(response.structuredContent()).isNotNull();

Expand All @@ -198,16 +207,16 @@ protected Consumer<ToolResponse> assertErrorOnly(Consumer<JsonArray> 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
*/
protected Consumer<ToolResponse> assertDataOnly(Consumer<JsonObject> dataAssertions) {
return response -> {
assertFalse(response.isError());
assertThat(response.content()).isEmpty();
assertThat(response._meta()).isEmpty();
assertServerInfoOnly(response);
assertNotNull(response.structuredContent());

var data = (JsonObject) response.structuredContent();
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
Comment thread
erichare marked this conversation as resolved.
Resolved
.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();
}
}
}
Loading