diff --git a/org.restlet.ext.mcp/src/test/java/org/restlet/ext/mcp/McpModernSpikeTestCase.java b/org.restlet.ext.mcp/src/test/java/org/restlet/ext/mcp/McpModernSpikeTestCase.java
new file mode 100644
index 0000000000..0e41253838
--- /dev/null
+++ b/org.restlet.ext.mcp/src/test/java/org/restlet/ext/mcp/McpModernSpikeTestCase.java
@@ -0,0 +1,570 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.ext.mcp;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.json.McpJsonDefaults;
+import io.modelcontextprotocol.json.McpJsonMapper;
+import io.modelcontextprotocol.json.TypeRef;
+import io.modelcontextprotocol.modern.JsonRpc;
+import io.modelcontextprotocol.modern.JsonRpc.JSONRPCMessage;
+import io.modelcontextprotocol.modern.JsonRpc.JSONRPCNotification;
+import io.modelcontextprotocol.modern.JsonRpc.JSONRPCRequest;
+import io.modelcontextprotocol.modern.JsonRpc.JSONRPCResponse;
+import io.modelcontextprotocol.modern.JsonRpc.JSONRPCResponse.JSONRPCError;
+import io.modelcontextprotocol.modern.McpSchema;
+import io.modelcontextprotocol.modern.McpSchema.CallToolOutcome;
+import io.modelcontextprotocol.modern.McpSchema.CallToolResult;
+import io.modelcontextprotocol.modern.McpSchema.ErrorCodes;
+import io.modelcontextprotocol.modern.McpSchema.Implementation;
+import io.modelcontextprotocol.modern.McpSchema.MetaKeys;
+import io.modelcontextprotocol.modern.McpSchema.TextContent;
+import io.modelcontextprotocol.modern.McpSchema.Tool;
+import io.modelcontextprotocol.modern.server.McpRequestContext;
+import io.modelcontextprotocol.modern.server.McpServer;
+import io.modelcontextprotocol.modern.server.McpSyncResponse;
+import io.modelcontextprotocol.modern.server.McpTransportResponse;
+import io.modelcontextprotocol.modern.server.feature.McpSyncTool;
+import io.modelcontextprotocol.modern.server.feature.McpSyncToolRepository;
+import io.modelcontextprotocol.modern.server.feature.ToolsFeature;
+import io.modelcontextprotocol.modern.server.feature.ToolsPage;
+import io.modelcontextprotocol.modern.server.transport.McpHeaderDecoder;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Base64;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.Server;
+import org.restlet.data.MediaType;
+import org.restlet.data.Method;
+import org.restlet.data.Protocol;
+import org.restlet.data.ServerSentEvent;
+import org.restlet.data.Status;
+import org.restlet.representation.EventStreamRepresentation;
+import org.restlet.representation.StringRepresentation;
+
+/**
+ * R6b gate 0 for MCP 2026-07-28: the MCP SDK's modern stateless server (mcp-core 2.2.0-M2, a
+ * preview) behind a test-only Restlet transport, mirroring the SDK's own servlet transport, and
+ * called as the Ikanos client calls a server. Test-only: no production API is implied.
+ */
+class McpModernSpikeTestCase {
+ private static final Duration TIMEOUT = Duration.ofSeconds(10);
+ private static final McpJsonMapper MAPPER = McpJsonDefaults.getMapper();
+ private static final String VERSION = McpSchema.LATEST_PROTOCOL_VERSION;
+
+ private static final Tool ECHO =
+ Tool.builder(
+ "echo",
+ Map.of(
+ "type",
+ "object",
+ "properties",
+ Map.of("text", Map.of("type", "string"))))
+ .description("Echoes text")
+ .build();
+
+ private static final Tool PROGRESS =
+ Tool.builder("progress", Map.of("type", "object"))
+ .description("Reports progress, then answers")
+ .build();
+
+ private McpServer sdkServer;
+ private Server server;
+ private HttpClient http;
+ private String target;
+
+ @BeforeEach
+ void start() throws Exception {
+ McpSyncToolRepository tools =
+ new McpSyncToolRepository() {
+ @Override
+ public ToolsPage list(McpRequestContext ctx, String cursor) {
+ return ToolsPage.of(List.of(ECHO, PROGRESS));
+ }
+
+ @Override
+ public McpSyncTool find(McpRequestContext ctx, String name) {
+ if (ECHO.name().equals(name)) {
+ return McpSyncTool.of(
+ ECHO,
+ (context, request) ->
+ McpSyncResponse.result(
+ text(
+ String.valueOf(
+ request.arguments()
+ .get("text")))));
+ }
+ if (PROGRESS.name().equals(name)) {
+ // A streaming reply: a progress notification, then the result.
+ return McpSyncTool.of(
+ PROGRESS,
+ (context, request) ->
+ McpSyncResponse.streaming(
+ notifier -> {
+ notifier.progress(1, 2.0, "half");
+ return text("done");
+ }));
+ }
+ return null;
+ }
+ };
+ sdkServer =
+ McpServer.builder()
+ .serverInfo(Implementation.builder("restlet-modern-spike", "1").build())
+ .feature(ToolsFeature.ofSync(tools))
+ .build();
+ server = new Server(new Context(), Protocol.HTTP, 0, new ModernHost(sdkServer));
+ server.start();
+ target = "http://localhost:" + server.getActualPort() + "/mcp";
+ http = HttpClient.newHttpClient();
+ }
+
+ @AfterEach
+ void stop() throws Exception {
+ try {
+ if (http != null) {
+ http.close();
+ }
+ } finally {
+ try {
+ if (sdkServer != null) {
+ sdkServer.closeGracefully();
+ }
+ } finally {
+ if (server != null) {
+ server.stop();
+ }
+ }
+ }
+ }
+
+ /** Discovery replaces initialize: versions, capabilities and the server identity. */
+ @Test
+ void serverDiscoverAdvertisesTheRevision() throws Exception {
+ Reply reply = call("server/discover", Map.of(), null);
+ assertEquals(200, reply.status, reply.body);
+ Map result = reply.result();
+ assertEquals(List.of(VERSION), result.get("supportedVersions"));
+ assertEquals("complete", result.get("resultType"));
+ assertTrue(((Map, ?>) result.get("capabilities")).containsKey("tools"), reply.body);
+ assertEquals(
+ "restlet-modern-spike",
+ ((Map, ?>) ((Map, ?>) result.get("_meta")).get(MetaKeys.SERVER_INFO))
+ .get("name"));
+ }
+
+ /** A tool is listed and called as Ikanos calls one, with a plain or base64 Mcp-Name. */
+ @Test
+ void toolsAreListedAndCalledAsIkanosCallsThem() throws Exception {
+ Reply list = call("tools/list", Map.of(), null);
+ assertEquals(200, list.status, list.body);
+ assertEquals(
+ "echo", ((Map, ?>) ((List>) list.result().get("tools")).get(0)).get("name"));
+ assertNotNull(list.result().get("ttlMs"), list.body);
+ assertNotNull(list.result().get("cacheScope"), list.body);
+
+ Map params = Map.of("name", "echo", "arguments", Map.of("text", "hello"));
+ Reply plain = call("tools/call", params, "echo");
+ assertEquals(200, plain.status, plain.body);
+ assertEquals("hello", firstText(plain.result()));
+ assertEquals("complete", plain.result().get("resultType"));
+
+ String encoded =
+ "=?base64?"
+ + Base64.getEncoder()
+ .encodeToString("echo".getBytes(StandardCharsets.UTF_8))
+ + "?=";
+ Reply base64 = call("tools/call", params, encoded);
+ assertEquals(200, base64.status, base64.body);
+ assertEquals("hello", firstText(base64.result()));
+ }
+
+ /**
+ * A streaming reply comes as an SSE stream over core SSE: the progress notification, then the
+ * result answering the request, then the end of the stream.
+ */
+ @Test
+ void aStreamingReplyCarriesProgressThenTheResult() throws Exception {
+ String request =
+ body(
+ "tools/call",
+ Map.of("name", "progress", "arguments", Map.of()),
+ Map.of(
+ MetaKeys.PROTOCOL_VERSION,
+ VERSION,
+ MetaKeys.CLIENT_CAPABILITIES,
+ Map.of(),
+ MetaKeys.PROGRESS_TOKEN,
+ "p1"));
+ HttpRequest.Builder post =
+ HttpRequest.newBuilder(URI.create(target))
+ .timeout(TIMEOUT)
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(request));
+ headers("tools/call", VERSION, "progress").forEach(post::header);
+ // Returns once the stream has ended.
+ HttpResponse response =
+ http.send(post.build(), HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(200, response.statusCode(), response.body());
+ assertTrue(
+ response.headers()
+ .firstValue("Content-Type")
+ .orElse("")
+ .startsWith("text/event-stream"),
+ response.headers().toString());
+ List