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
12 changes: 10 additions & 2 deletions changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,16 @@ Changes log
2026-07-28 clients only, for `input_required` results and streamed progress. The
interceptors wrap each 2026-07-28 request and its final result, the tool filters and
the call limit apply to both revisions, and `setRequestStateKey()` shares the key
sealing `input_required` state between instances. Not yet supported for 2026-07-28:
stdio, `subscriptions/listen`, and the MCP client. The SDK's modern API is a preview.
sealing `input_required` state between instances. `getModernResources()`,
`getModernResourceTemplates()` and `getModernPrompts()` add resources, templates and
prompts served to 2026-07-28 clients only; a URI, URI template or prompt name may only
be used once across both revisions' lists. `subscriptions/listen` streams carry the
changes published through `getChanges()`; they have their own limit,
`setMaxListenStreams()`, no time limit, and end when the adapter stops or the client
goes away. Over stdio, a message whose `_meta` names 2026-07-28 goes to the modern
server: streamed messages are written as separate lines as they come, and
`notifications/cancelled` ends the request it names without a response. Not yet
supported for 2026-07-28: the MCP client. The SDK's modern API is a preview.
- Added experimental SDK-native gRPC hosting of unary, server-streaming,
client-streaming and bidirectional methods on a borrowed Restlet/Netty HTTP/2 listener.
Request messages are read as the application requests them, within the queue budget,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Copyright 2005-2026 Qlik
*<p>
* 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
*<p>
* Restlet is a registered trademark of QlikTech International AB.
*/
package org.restlet.ext.mcp;

import io.modelcontextprotocol.modern.McpSchema;
import io.modelcontextprotocol.modern.server.feature.McpAsyncResource;
import java.util.Objects;

/**
* A resource served to MCP {@link McpProtocol#MODERN_VERSION} clients only, for {@link
* McpServerAdapter#getModernResources()}: its definition, listed, and its read handler, which may
* answer {@code input_required}. Experimental: the SDK's modern API is a preview.
*
* @param definition The resource, with its URI.
* @param resource The read handler.
*/
public record McpModernResource(McpSchema.Resource definition, McpAsyncResource resource) {

/**
* Creates a modern resource.
*
* @param definition The resource, with its URI.
* @param resource The read handler.
*/
public McpModernResource {
Objects.requireNonNull(definition, "definition");
Objects.requireNonNull(resource, "resource");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright 2005-2026 Qlik
*<p>
* 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
*<p>
* Restlet is a registered trademark of QlikTech International AB.
*/
package org.restlet.ext.mcp;

import io.modelcontextprotocol.modern.McpSchema;
import io.modelcontextprotocol.modern.server.feature.McpAsyncResource;
import java.util.Objects;

/**
* A resource template served to MCP {@link McpProtocol#MODERN_VERSION} clients only, for {@link
* McpServerAdapter#getModernResourceTemplates()}: its definition, listed, and the read handler of
* the URIs it matches. Experimental: the SDK's modern API is a preview.
*
* @param definition The template, with its URI template.
* @param resource The read handler of the matching URIs.
*/
public record McpModernResourceTemplate(
McpSchema.ResourceTemplate definition, McpAsyncResource resource) {

/**
* Creates a modern resource template.
*
* @param definition The template, with its URI template.
* @param resource The read handler of the matching URIs.
*/
public McpModernResourceTemplate {
Objects.requireNonNull(definition, "definition");
Objects.requireNonNull(resource, "resource");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ public final class McpProtocol {
public static final int DEFAULT_MAX_MESSAGE_BYTES = 1024 * 1024;
public static final int DEFAULT_MAX_CONCURRENT_CALLS = 128;

/** The default number of open MCP 2026-07-28 {@code subscriptions/listen} streams. */
public static final int DEFAULT_MAX_LISTEN_STREAMS = 64;

private McpProtocol() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.json.jackson2.JacksonMcpJsonMapper;
import io.modelcontextprotocol.modern.server.HmacRequestStateCodec;
import io.modelcontextprotocol.modern.server.feature.McpAsyncPrompt;
import io.modelcontextprotocol.modern.server.feature.McpAsyncTool;
import io.modelcontextprotocol.modern.server.feature.McpChangeBroadcaster;
import io.modelcontextprotocol.modern.server.feature.McpChangeFeed;
import io.modelcontextprotocol.server.McpAsyncListFilter;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpStatelessAsyncServer;
Expand Down Expand Up @@ -61,6 +64,9 @@
public final class McpServerAdapter extends Restlet {
private static final McpJsonMapper MAPPER = initMapper();

/** How long stopping waits for listen streams to end with their result. */
private static final Duration LISTEN_DRAIN = Duration.ofSeconds(2);

private static McpJsonMapper initMapper() {
McpJsonMapper mapper = McpJsonDefaults.getMapper();
if (mapper instanceof JacksonMcpJsonMapper jacksonMapper) {
Expand All @@ -82,7 +88,13 @@ private static McpJsonMapper initMapper() {
new CopyOnWriteArrayList<>();
private final List<McpInterceptor> interceptors = new CopyOnWriteArrayList<>();
private final List<McpAsyncTool> modernTools = new CopyOnWriteArrayList<>();
private final List<McpModernResource> modernResources = new CopyOnWriteArrayList<>();
private final List<McpModernResourceTemplate> modernResourceTemplates =
new CopyOnWriteArrayList<>();
private final List<McpAsyncPrompt> modernPrompts = new CopyOnWriteArrayList<>();
private final McpChangeFeed changes = new McpChangeFeed();
private byte[] requestStateKey;
private int maxListenStreams = McpProtocol.DEFAULT_MAX_LISTEN_STREAMS;
private final Set<String> allowedOrigins = ConcurrentHashMap.newKeySet();
private McpSchema.Implementation serverInfo =
McpSchema.Implementation.builder("restlet", "3.0").build();
Expand All @@ -94,6 +106,7 @@ private static McpJsonMapper initMapper() {
private volatile McpStatelessAsyncServer sdkServer;
private volatile io.modelcontextprotocol.modern.server.McpServer modernServer;
private volatile McpModernExchange modernExchange;
private volatile McpWorkers listeners;
private volatile McpWorkers workers;
private volatile boolean accepting;
private volatile Sinks.One<Boolean> stopping;
Expand Down Expand Up @@ -147,6 +160,70 @@ public List<McpAsyncTool> getModernTools() {
return modernTools;
}

/**
* Resources served to MCP {@link McpProtocol#MODERN_VERSION} clients only, whose read may
* answer {@code input_required}. The resources of {@link #getResources()} are served to both
* revisions; a URI may only be used once across both lists. Experimental.
*
* @return The modern resources, modifiable until the adapter starts.
*/
public List<McpModernResource> getModernResources() {
return modernResources;
}

/**
* Resource templates served to MCP {@link McpProtocol#MODERN_VERSION} clients only. The
* templates of {@link #getResourceTemplates()} are served to both revisions; a URI template may
* only be used once across both lists. Experimental.
*
* @return The modern resource templates, modifiable until the adapter starts.
*/
public List<McpModernResourceTemplate> getModernResourceTemplates() {
return modernResourceTemplates;
}

/**
* Prompts served to MCP {@link McpProtocol#MODERN_VERSION} clients only, on the SDK's modern
* types. The prompts of {@link #getPrompts()} are served to both revisions; a name may only be
* used once across both lists. Experimental.
*
* @return The modern prompts, modifiable until the adapter starts.
*/
public List<McpAsyncPrompt> getModernPrompts() {
return modernPrompts;
}

/**
* Publishes changes to the MCP {@link McpProtocol#MODERN_VERSION} clients listening through
* {@code subscriptions/listen}: tools, prompts and resources list changes, and resource
* updates. A change nobody listens to is dropped. Experimental.
*
* @return The change broadcaster, usable at any time.
*/
public McpChangeBroadcaster getChanges() {
return changes;
}

public synchronized int getMaxListenStreams() {
return maxListenStreams;
}

/**
* Sets how many {@code subscriptions/listen} streams may be open at once, {@value
* McpProtocol#DEFAULT_MAX_LISTEN_STREAMS} by default. Listen streams have no time limit and
* don't count against {@link #getMaxConcurrentCalls()}; they end when the adapter stops or the
* client goes away, and carry heartbeats.
*
* @param value The number of streams, positive.
*/
public synchronized void setMaxListenStreams(int value) {
requireConfigurable();
if (value <= 0) {
throw new IllegalArgumentException("maxListenStreams must be positive");
}
maxListenStreams = value;
}

/**
* Sets the key sealing the {@code requestState} of {@code input_required} results, at least 32
* bytes. Without it, each adapter generates its own random key, so a retry must reach the
Expand Down Expand Up @@ -278,13 +355,15 @@ public List<String> protocolVersions() {
.toolFilters(filters -> filters.addAll(toolFilters))
.build();
workers = new McpWorkers(maxConcurrentCalls, requestTimeout);
listeners = new McpWorkers(maxListenStreams, null, requestTimeout);
stopping = Sinks.one();
modernServer = modern;
modernExchange =
new McpModernExchange(
modernServer,
MAPPER,
workers,
listeners,
requestTimeout,
maxMessageBytes,
(message, context, terminal) -> invoke(0, message, context, terminal),
Expand Down Expand Up @@ -316,7 +395,11 @@ private io.modelcontextprotocol.modern.server.McpServer modernServer() {
List.copyOf(resourceTemplates),
List.copyOf(prompts),
List.copyOf(toolFilters),
List.copyOf(modernTools))));
List.copyOf(modernTools),
List.copyOf(modernResources),
List.copyOf(modernResourceTemplates),
List.copyOf(modernPrompts))))
.subscriptions(changes);
var codec =
HmacRequestStateCodec.builder()
.principalExtractor(McpServerAdapter::principal)
Expand Down Expand Up @@ -356,18 +439,26 @@ public synchronized void stop() throws Exception {
return;
}
accepting = false;
// Listen streams end with their result first; what remains is then cut short.
modernServer.closeGracefully();
try {
listeners.awaitIdle(
requestTimeout.compareTo(LISTEN_DRAIN) < 0 ? requestTimeout : LISTEN_DRAIN);
} catch (InterruptedException _) {
Thread.currentThread().interrupt();
}
stopping.tryEmitValue(Boolean.TRUE);
try {
workers.close();
try {
workers.close();
} finally {
listeners.close();
}
} finally {
try {
sdkServer.closeGracefully().block(requestTimeout);
} finally {
try {
modernServer.closeGracefully();
} finally {
super.stop();
}
super.stop();
}
}
}
Expand Down
Loading
Loading