Skip to content
Open
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
2 changes: 1 addition & 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>2.0.3</version>
<version>2.1.0.Beta1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Expand Down
17 changes: 12 additions & 5 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,20 @@ quarkus:
# MCP (Model Context Protocol) server configuration
mcp:
server:
# Admit and advertise only this revision. A stateless-only list also disables initialize,
# sessions and the legacy HTTP+SSE endpoints.
# https://docs.quarkiverse.io/quarkus-mcp-server/dev/concepts-transports.html
supported-protocol-versions: "2026-07-28"
# Results may depend on the caller's authorization, so keep them private and immediately
# stale until a cache policy is reviewed.
discover:
ttl-ms: 0
cache-scope: private
tools:
ttl-ms: 0
cache-scope: private
http:
root-path: /v1/mcp
streamable:
# It performs an automatic initialization when the first message from the client is not initialize.
# A new MCP session is created for each request.
# https://docs.quarkiverse.io/quarkus-mcp-server/1.13.x/reference-configuration.html#quarkus-mcp-server-http_quarkus-mcp-server-http-streamable-auto-init
auto-init: true
traffic-logging:
enabled: true
text-limit: 1000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,30 @@ void findEmbeddingProvidersToolCall() {
}));
}

@Test
void successfulToolCallIsTaggedCompleteOverRawHttp() {
var response = rawToolCall(1, CommandName.Names.FIND_KEYSPACES, Map.of());

var result = McpHttpTestSupport.assertResponseComplete(response, 1);
assertFalse(result.getBoolean("isError", false));
assertNull(response.header("Mcp-Session-Id"));
assertNotNull(result.getJsonObject("_meta").getJsonObject(McpHttpTestSupport.SERVER_INFO));
}

@Test
void packagedServerDoesNotAdvertiseToolListChanges() {
// The tool list never changes at runtime, and subscriptions are not part of this launch.
var response =
rawRequest("server/discover")
.body(McpHttpTestSupport.rpcPayload(1, "server/discover", Map.of()))
.post(MCP_PATH);

var result = McpHttpTestSupport.assertResponseComplete(response, 1);
var tools = result.getJsonObject("capabilities").getJsonObject("tools");
assertNotNull(tools);
assertFalse(tools.containsKey("listChanged"));
}

@Test
void findRerankingProvidersToolCall() {
callToolAndAssert(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,23 @@ public void failToolCallWithMCPDisabled() {
}));
}

@Test
public void toolErrorIsTaggedCompleteOverRawHttp() {
var response = rawToolCall(1, CommandName.Names.FIND_KEYSPACES, Map.of());

// One JSON body rather than an SSE stream, so there are no event IDs to resume.
assertThat(response.contentType()).startsWith("application/json");
assertNull(response.header("Mcp-Session-Id"));
var result = McpHttpTestSupport.assertResponseComplete(response, 1);
assertTrue(result.getBoolean("isError"));
assertNotNull(result.getJsonObject("_meta").getJsonObject(McpHttpTestSupport.SERVER_INFO));
}

@Test
public void okToolCallWithHeaderEnableMCP() {
var mcpClientWithHeaderEnable =
McpAssured.newStreamableClient()
.setStateless()
.setBaseUri(URI.create(MCP_HOSTNAME + getTestPort()))
.setMcpPath(MCP_PATH)
.setAdditionalHeaders(msg -> authHeaders().add(ApiFeature.MCP.httpHeaderName(), "true"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@

/** 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";
static final String CURRENT_PROTOCOL_VERSION = "2026-07-28";
static final String SERVER_INFO = "io.modelcontextprotocol/serverInfo";

private McpHttpTestSupport() {}

/** A POST with the Accept and Content-Type headers but none of the MCP request headers. */
static RequestSpecification bareRequest() {
return given().contentType(ContentType.JSON).accept("application/json, text/event-stream");
}

static RequestSpecification request(String method) {
return given()
.contentType(ContentType.JSON)
.accept("application/json, text/event-stream")
return bareRequest()
.header("MCP-Protocol-Version", CURRENT_PROTOCOL_VERSION)
.header("Mcp-Method", method);
}
Expand All @@ -29,17 +33,39 @@ static Map<String, Object> toolPayload(int id, String tool, Map<String, Object>
}

static Map<String, Object> rpcPayload(int id, String method, Map<String, Object> params) {
return rpcPayload(id, method, params, requestMeta(CURRENT_PROTOCOL_VERSION));
}

/** Builds a request with the given {@code _meta}, or without one when {@code meta} is null. */
static Map<String, Object> rpcPayload(
int id, String method, Map<String, Object> params, Map<String, Object> meta) {
var requestParams = new HashMap<>(params);
requestParams.put(
"_meta",
Map.of(
"io.modelcontextprotocol/protocolVersion",
CURRENT_PROTOCOL_VERSION,
"io.modelcontextprotocol/clientCapabilities",
Map.of()));
if (meta != null) {
requestParams.put("_meta", meta);
}
return Map.of("jsonrpc", "2.0", "id", id, "method", method, "params", requestParams);
}

/** A notification has no id, so the server must not answer it with a result. */
static Map<String, Object> notificationPayload(String method) {
return Map.of(
"jsonrpc",
"2.0",
"method",
method,
"params",
Map.of("_meta", requestMeta(CURRENT_PROTOCOL_VERSION)));
}

/** The required per-request fields, with clientInfo omitted because it is optional. */
static Map<String, Object> requestMeta(String protocolVersion) {
return Map.of(
"io.modelcontextprotocol/protocolVersion",
protocolVersion,
"io.modelcontextprotocol/clientCapabilities",
Map.of());
}

static JsonObject assertResponseComplete(Response response, int id) {
assertThat(response.statusCode()).as(response.asString()).isEqualTo(200);
var body = new JsonObject(response.asString());
Expand All @@ -50,6 +76,16 @@ static JsonObject assertResponseComplete(Response response, int id) {
return result;
}

/** Asserts a JSON-RPC error envelope with the given HTTP status and error code. */
static JsonObject assertRpcError(Response response, int httpStatus, int code) {
assertThat(response.statusCode()).as(response.asString()).isEqualTo(httpStatus);
var body = new JsonObject(response.asString());
assertThat(body.containsKey("result")).isFalse();
var error = body.getJsonObject("error");
assertThat(error.getInteger("code")).as(error.encode()).isEqualTo(code);
return error;
}

static JsonObject assertResultReadable(JsonObject result) {
var content = result.getJsonArray("content");
assertThat(content).hasSize(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import io.quarkiverse.mcp.server.ToolResponse;
import io.quarkiverse.mcp.server.test.McpAssured;
import io.quarkiverse.mcp.server.test.McpAssured.McpStreamableTestClient;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
import io.stargate.sgv2.jsonapi.api.model.command.CommandName;
import io.stargate.sgv2.jsonapi.config.constants.HttpConstants;
import io.vertx.core.MultiMap;
Expand Down Expand Up @@ -64,6 +66,8 @@ void setUpMcpClient() {

mcpClient =
McpAssured.newStreamableClient()
// The server admits only 2026-07-28, which has no initialize handshake or session.
.setStateless()
.setBaseUri(URI.create(MCP_HOSTNAME + getTestPort()))
.setMcpPath(MCP_PATH)
.setAdditionalHeaders(msg -> authHeaders())
Expand Down Expand Up @@ -99,6 +103,23 @@ protected MultiMap authHeaders() {
.add(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, credential);
}

/**
* Starts a raw HTTP request with the same credentials, so a mistake shared by the test client and
* the server cannot hide a wire problem.
*/
protected RequestSpecification rawRequest(String method) {
var request = McpHttpTestSupport.request(method);
authHeaders().forEach(header -> request.header(header.getKey(), header.getValue()));
return request;
}

protected Response rawToolCall(int id, String tool, Map<String, Object> arguments) {
return rawRequest("tools/call")
.header("Mcp-Name", tool)
.body(McpHttpTestSupport.toolPayload(id, tool, arguments))
.post(MCP_PATH);
}

/** Create a keyspace via the MCP createKeyspace tool. */
protected void createKeyspace(String keyspace) {
callToolAndAssert("createKeyspace", Map.of("name", keyspace), assertStatusOnlyOk());
Expand Down
Loading
Loading