feat: WebClient Streamable HTTP support (#292)
An implementation of Streamable HTTP Client with WebFlux WebClient. Aside from implementing the specification, several improvements have been incorporated throughout the client-side of the architecture. The changes cover: - resilience tests using toxiproxy in testcontainers - integration tests using updated everything-server with streamableHttp support - improved logging - session invalidation handling (both transport session and JSON-RPC concept of session) - implicit initialization and burst protection (in case of concurrent `Mcp(Sync|Async)Client` use - more logging, e.g. stdio process lifecycle logs Related #72, #273, #253, #107, #105 Signed-off-by: Dariusz Jędrzejczyk <dariusz.jedrzejczyk@broadcom.com>
This commit is contained in:
parent
8a5a591d39
commit
2f944349cc
10 changed files with 1501 additions and 242 deletions
|
|
@ -9,9 +9,9 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import io.modelcontextprotocol.spec.McpClientSession;
|
||||
|
|
@ -32,7 +32,7 @@ import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
|||
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
||||
import io.modelcontextprotocol.spec.McpSchema.PaginatedRequest;
|
||||
import io.modelcontextprotocol.spec.McpSchema.Root;
|
||||
import io.modelcontextprotocol.spec.McpTransport;
|
||||
import io.modelcontextprotocol.spec.McpTransportSessionNotFoundException;
|
||||
import io.modelcontextprotocol.util.Assert;
|
||||
import io.modelcontextprotocol.util.Utils;
|
||||
import org.slf4j.Logger;
|
||||
|
|
@ -77,29 +77,37 @@ import reactor.core.publisher.Sinks;
|
|||
* @see McpClient
|
||||
* @see McpSchema
|
||||
* @see McpClientSession
|
||||
* @see McpClientTransport
|
||||
*/
|
||||
public class McpAsyncClient {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(McpAsyncClient.class);
|
||||
|
||||
private static TypeReference<Void> VOID_TYPE_REFERENCE = new TypeReference<>() {
|
||||
private static final TypeReference<Void> VOID_TYPE_REFERENCE = new TypeReference<>() {
|
||||
};
|
||||
|
||||
protected final Sinks.One<McpSchema.InitializeResult> initializedSink = Sinks.one();
|
||||
public static final TypeReference<Object> OBJECT_TYPE_REF = new TypeReference<>() {
|
||||
};
|
||||
|
||||
private AtomicBoolean initialized = new AtomicBoolean(false);
|
||||
public static final TypeReference<PaginatedRequest> PAGINATED_REQUEST_TYPE_REF = new TypeReference<>() {
|
||||
};
|
||||
|
||||
public static final TypeReference<McpSchema.InitializeResult> INITIALIZE_RESULT_TYPE_REF = new TypeReference<>() {
|
||||
};
|
||||
|
||||
public static final TypeReference<CreateMessageRequest> CREATE_MESSAGE_REQUEST_TYPE_REF = new TypeReference<>() {
|
||||
};
|
||||
|
||||
public static final TypeReference<LoggingMessageNotification> LOGGING_MESSAGE_NOTIFICATION_TYPE_REF = new TypeReference<>() {
|
||||
};
|
||||
|
||||
private final AtomicReference<Initialization> initializationRef = new AtomicReference<>();
|
||||
|
||||
/**
|
||||
* The max timeout to await for the client-server connection to be initialized.
|
||||
*/
|
||||
private final Duration initializationTimeout;
|
||||
|
||||
/**
|
||||
* The MCP session implementation that manages bidirectional JSON-RPC communication
|
||||
* between clients and servers.
|
||||
*/
|
||||
private final McpClientSession mcpSession;
|
||||
|
||||
/**
|
||||
* Client capabilities.
|
||||
*/
|
||||
|
|
@ -110,21 +118,6 @@ public class McpAsyncClient {
|
|||
*/
|
||||
private final McpSchema.Implementation clientInfo;
|
||||
|
||||
/**
|
||||
* Server capabilities.
|
||||
*/
|
||||
private McpSchema.ServerCapabilities serverCapabilities;
|
||||
|
||||
/**
|
||||
* Server instructions.
|
||||
*/
|
||||
private String serverInstructions;
|
||||
|
||||
/**
|
||||
* Server implementation information.
|
||||
*/
|
||||
private McpSchema.Implementation serverInfo;
|
||||
|
||||
/**
|
||||
* Roots define the boundaries of where servers can operate within the filesystem,
|
||||
* allowing them to understand which directories and files they have access to.
|
||||
|
|
@ -155,13 +148,19 @@ public class McpAsyncClient {
|
|||
/**
|
||||
* Client transport implementation.
|
||||
*/
|
||||
private final McpTransport transport;
|
||||
private final McpClientTransport transport;
|
||||
|
||||
/**
|
||||
* Supported protocol versions.
|
||||
*/
|
||||
private List<String> protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
|
||||
|
||||
/**
|
||||
* The MCP session supplier that manages bidirectional JSON-RPC communication between
|
||||
* clients and servers.
|
||||
*/
|
||||
private final Supplier<McpClientSession> sessionSupplier;
|
||||
|
||||
/**
|
||||
* Create a new McpAsyncClient with the given transport and session request-response
|
||||
* timeout.
|
||||
|
|
@ -254,16 +253,38 @@ public class McpAsyncClient {
|
|||
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_MESSAGE,
|
||||
asyncLoggingNotificationHandler(loggingConsumersFinal));
|
||||
|
||||
this.mcpSession = new McpClientSession(requestTimeout, transport, requestHandlers, notificationHandlers);
|
||||
this.transport.setExceptionHandler(this::handleException);
|
||||
this.sessionSupplier = () -> new McpClientSession(requestTimeout, transport, requestHandlers,
|
||||
notificationHandlers);
|
||||
|
||||
}
|
||||
|
||||
private void handleException(Throwable t) {
|
||||
logger.warn("Handling exception", t);
|
||||
if (t instanceof McpTransportSessionNotFoundException) {
|
||||
Initialization previous = this.initializationRef.getAndSet(null);
|
||||
if (previous != null) {
|
||||
previous.close();
|
||||
}
|
||||
// Providing an empty operation since we are only interested in triggering the
|
||||
// implicit initialization step.
|
||||
withSession("re-initializing", result -> Mono.empty()).subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
private McpSchema.InitializeResult currentInitializationResult() {
|
||||
Initialization current = this.initializationRef.get();
|
||||
McpSchema.InitializeResult initializeResult = current != null ? current.result.get() : null;
|
||||
return initializeResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the server capabilities that define the supported features and functionality.
|
||||
* @return The server capabilities
|
||||
*/
|
||||
public McpSchema.ServerCapabilities getServerCapabilities() {
|
||||
return this.serverCapabilities;
|
||||
McpSchema.InitializeResult initializeResult = currentInitializationResult();
|
||||
return initializeResult != null ? initializeResult.capabilities() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -272,7 +293,8 @@ public class McpAsyncClient {
|
|||
* @return The server instructions
|
||||
*/
|
||||
public String getServerInstructions() {
|
||||
return this.serverInstructions;
|
||||
McpSchema.InitializeResult initializeResult = currentInitializationResult();
|
||||
return initializeResult != null ? initializeResult.instructions() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -280,7 +302,8 @@ public class McpAsyncClient {
|
|||
* @return The server implementation details
|
||||
*/
|
||||
public McpSchema.Implementation getServerInfo() {
|
||||
return this.serverInfo;
|
||||
McpSchema.InitializeResult initializeResult = currentInitializationResult();
|
||||
return initializeResult != null ? initializeResult.serverInfo() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -288,7 +311,8 @@ public class McpAsyncClient {
|
|||
* @return true if the client-server connection is initialized
|
||||
*/
|
||||
public boolean isInitialized() {
|
||||
return this.initialized.get();
|
||||
Initialization current = this.initializationRef.get();
|
||||
return current != null && (current.result.get() != null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -311,7 +335,11 @@ public class McpAsyncClient {
|
|||
* Closes the client connection immediately.
|
||||
*/
|
||||
public void close() {
|
||||
this.mcpSession.close();
|
||||
Initialization current = this.initializationRef.getAndSet(null);
|
||||
if (current != null) {
|
||||
current.close();
|
||||
}
|
||||
this.transport.close();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -319,14 +347,21 @@ public class McpAsyncClient {
|
|||
* @return A Mono that completes when the connection is closed
|
||||
*/
|
||||
public Mono<Void> closeGracefully() {
|
||||
return this.mcpSession.closeGracefully();
|
||||
return Mono.defer(() -> {
|
||||
Initialization current = this.initializationRef.getAndSet(null);
|
||||
Mono<?> sessionClose = current != null ? current.closeGracefully() : Mono.empty();
|
||||
return sessionClose.then(transport.closeGracefully());
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------
|
||||
// Initialization
|
||||
// --------------------------
|
||||
/**
|
||||
* The initialization phase MUST be the first interaction between client and server.
|
||||
* The initialization phase should be the first interaction between client and server.
|
||||
* The client will ensure it happens in case it has not been explicitly called and in
|
||||
* case of transport session invalidation.
|
||||
* <p>
|
||||
* During this phase, the client and server:
|
||||
* <ul>
|
||||
* <li>Establish protocol version compatibility</li>
|
||||
|
|
@ -346,9 +381,13 @@ public class McpAsyncClient {
|
|||
* @see <a href=
|
||||
* "https://github.com/modelcontextprotocol/specification/blob/main/docs/specification/basic/lifecycle.md#initialization">MCP
|
||||
* Initialization Spec</a>
|
||||
* </p>
|
||||
*/
|
||||
public Mono<McpSchema.InitializeResult> initialize() {
|
||||
return withSession("by explicit API call", init -> Mono.just(init.get()));
|
||||
}
|
||||
|
||||
private Mono<McpSchema.InitializeResult> doInitialize(McpClientSession mcpClientSession) {
|
||||
String latestVersion = this.protocolVersions.get(this.protocolVersions.size() - 1);
|
||||
|
||||
McpSchema.InitializeRequest initializeRequest = new McpSchema.InitializeRequest(// @formatter:off
|
||||
|
|
@ -356,16 +395,10 @@ public class McpAsyncClient {
|
|||
this.clientCapabilities,
|
||||
this.clientInfo); // @formatter:on
|
||||
|
||||
Mono<McpSchema.InitializeResult> result = this.mcpSession.sendRequest(McpSchema.METHOD_INITIALIZE,
|
||||
initializeRequest, new TypeReference<McpSchema.InitializeResult>() {
|
||||
});
|
||||
Mono<McpSchema.InitializeResult> result = mcpClientSession.sendRequest(McpSchema.METHOD_INITIALIZE,
|
||||
initializeRequest, INITIALIZE_RESULT_TYPE_REF);
|
||||
|
||||
return result.flatMap(initializeResult -> {
|
||||
|
||||
this.serverCapabilities = initializeResult.capabilities();
|
||||
this.serverInstructions = initializeResult.instructions();
|
||||
this.serverInfo = initializeResult.serverInfo();
|
||||
|
||||
logger.info("Server response with Protocol: {}, Capabilities: {}, Info: {} and Instructions {}",
|
||||
initializeResult.protocolVersion(), initializeResult.capabilities(), initializeResult.serverInfo(),
|
||||
initializeResult.instructions());
|
||||
|
|
@ -375,28 +408,93 @@ public class McpAsyncClient {
|
|||
"Unsupported protocol version from the server: " + initializeResult.protocolVersion()));
|
||||
}
|
||||
|
||||
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_INITIALIZED, null).doOnSuccess(v -> {
|
||||
this.initialized.set(true);
|
||||
this.initializedSink.tryEmitValue(initializeResult);
|
||||
}).thenReturn(initializeResult);
|
||||
return mcpClientSession.sendNotification(McpSchema.METHOD_NOTIFICATION_INITIALIZED, null)
|
||||
.thenReturn(initializeResult);
|
||||
});
|
||||
}
|
||||
|
||||
private static class Initialization {
|
||||
|
||||
private final Sinks.One<McpSchema.InitializeResult> initSink = Sinks.one();
|
||||
|
||||
private final AtomicReference<McpSchema.InitializeResult> result = new AtomicReference<>();
|
||||
|
||||
private final AtomicReference<McpClientSession> mcpClientSession = new AtomicReference<>();
|
||||
|
||||
static Initialization create() {
|
||||
return new Initialization();
|
||||
}
|
||||
|
||||
void setMcpClientSession(McpClientSession mcpClientSession) {
|
||||
this.mcpClientSession.set(mcpClientSession);
|
||||
}
|
||||
|
||||
McpClientSession mcpSession() {
|
||||
return this.mcpClientSession.get();
|
||||
}
|
||||
|
||||
McpSchema.InitializeResult get() {
|
||||
return this.result.get();
|
||||
}
|
||||
|
||||
Mono<McpSchema.InitializeResult> await() {
|
||||
return this.initSink.asMono();
|
||||
}
|
||||
|
||||
void complete(McpSchema.InitializeResult initializeResult) {
|
||||
// first ensure the result is cached
|
||||
this.result.set(initializeResult);
|
||||
// inform all the subscribers waiting for the initialization
|
||||
this.initSink.emitValue(initializeResult, Sinks.EmitFailureHandler.FAIL_FAST);
|
||||
}
|
||||
|
||||
void error(Throwable t) {
|
||||
this.initSink.emitError(t, Sinks.EmitFailureHandler.FAIL_FAST);
|
||||
}
|
||||
|
||||
void close() {
|
||||
this.mcpSession().close();
|
||||
}
|
||||
|
||||
Mono<Void> closeGracefully() {
|
||||
return this.mcpSession().closeGracefully();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to handle the common pattern of checking initialization before
|
||||
* Utility method to handle the common pattern of ensuring initialization before
|
||||
* executing an operation.
|
||||
* @param <T> The type of the result Mono
|
||||
* @param actionName The action to perform if the client is initialized
|
||||
* @param operation The operation to execute if the client is initialized
|
||||
* @param actionName The action to perform when the client is initialized
|
||||
* @param operation The operation to execute when the client is initialized
|
||||
* @return A Mono that completes with the result of the operation
|
||||
*/
|
||||
private <T> Mono<T> withInitializationCheck(String actionName,
|
||||
Function<McpSchema.InitializeResult, Mono<T>> operation) {
|
||||
return this.initializedSink.asMono()
|
||||
.timeout(this.initializationTimeout)
|
||||
.onErrorResume(TimeoutException.class,
|
||||
ex -> Mono.error(new McpError("Client must be initialized before " + actionName)))
|
||||
.flatMap(operation);
|
||||
private <T> Mono<T> withSession(String actionName, Function<Initialization, Mono<T>> operation) {
|
||||
return Mono.defer(() -> {
|
||||
Initialization newInit = Initialization.create();
|
||||
Initialization previous = this.initializationRef.compareAndExchange(null, newInit);
|
||||
|
||||
boolean needsToInitialize = previous == null;
|
||||
logger.debug(needsToInitialize ? "Initialization process started" : "Joining previous initialization");
|
||||
if (needsToInitialize) {
|
||||
newInit.setMcpClientSession(this.sessionSupplier.get());
|
||||
}
|
||||
|
||||
Mono<McpSchema.InitializeResult> initializationJob = needsToInitialize
|
||||
? doInitialize(newInit.mcpSession()).doOnNext(newInit::complete).onErrorResume(ex -> {
|
||||
newInit.error(ex);
|
||||
return Mono.error(ex);
|
||||
}) : previous.await();
|
||||
|
||||
return initializationJob.map(initializeResult -> this.initializationRef.get())
|
||||
.timeout(this.initializationTimeout)
|
||||
.onErrorResume(ex -> {
|
||||
logger.warn("Failed to initialize", ex);
|
||||
return Mono.error(new McpError("Client failed to initialize " + actionName));
|
||||
})
|
||||
.flatMap(operation);
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------
|
||||
|
|
@ -408,9 +506,8 @@ public class McpAsyncClient {
|
|||
* @return A Mono that completes with the server's ping response
|
||||
*/
|
||||
public Mono<Object> ping() {
|
||||
return this.withInitializationCheck("pinging the server", initializedResult -> this.mcpSession
|
||||
.sendRequest(McpSchema.METHOD_PING, null, new TypeReference<Object>() {
|
||||
}));
|
||||
return this.withSession("pinging the server",
|
||||
init -> init.mcpSession().sendRequest(McpSchema.METHOD_PING, null, OBJECT_TYPE_REF));
|
||||
}
|
||||
|
||||
// --------------------------
|
||||
|
|
@ -490,16 +587,14 @@ public class McpAsyncClient {
|
|||
* @return A Mono that completes when the notification is sent.
|
||||
*/
|
||||
public Mono<Void> rootsListChangedNotification() {
|
||||
return this.withInitializationCheck("sending roots list changed notification",
|
||||
initResult -> this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED));
|
||||
return this.withSession("sending roots list changed notification",
|
||||
init -> init.mcpSession().sendNotification(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED));
|
||||
}
|
||||
|
||||
private RequestHandler<McpSchema.ListRootsResult> rootsListRequestHandler() {
|
||||
return params -> {
|
||||
@SuppressWarnings("unused")
|
||||
McpSchema.PaginatedRequest request = transport.unmarshalFrom(params,
|
||||
new TypeReference<McpSchema.PaginatedRequest>() {
|
||||
});
|
||||
McpSchema.PaginatedRequest request = transport.unmarshalFrom(params, PAGINATED_REQUEST_TYPE_REF);
|
||||
|
||||
List<Root> roots = this.roots.values().stream().toList();
|
||||
|
||||
|
|
@ -512,9 +607,7 @@ public class McpAsyncClient {
|
|||
// --------------------------
|
||||
private RequestHandler<CreateMessageResult> samplingCreateMessageHandler() {
|
||||
return params -> {
|
||||
McpSchema.CreateMessageRequest request = transport.unmarshalFrom(params,
|
||||
new TypeReference<McpSchema.CreateMessageRequest>() {
|
||||
});
|
||||
McpSchema.CreateMessageRequest request = transport.unmarshalFrom(params, CREATE_MESSAGE_REQUEST_TYPE_REF);
|
||||
|
||||
return this.samplingHandler.apply(request);
|
||||
};
|
||||
|
|
@ -553,11 +646,12 @@ public class McpAsyncClient {
|
|||
* @see #listTools()
|
||||
*/
|
||||
public Mono<McpSchema.CallToolResult> callTool(McpSchema.CallToolRequest callToolRequest) {
|
||||
return this.withInitializationCheck("calling tools", initializedResult -> {
|
||||
if (this.serverCapabilities.tools() == null) {
|
||||
return this.withSession("calling tools", init -> {
|
||||
if (init.get().capabilities().tools() == null) {
|
||||
return Mono.error(new McpError("Server does not provide tools capability"));
|
||||
}
|
||||
return this.mcpSession.sendRequest(McpSchema.METHOD_TOOLS_CALL, callToolRequest, CALL_TOOL_RESULT_TYPE_REF);
|
||||
return init.mcpSession()
|
||||
.sendRequest(McpSchema.METHOD_TOOLS_CALL, callToolRequest, CALL_TOOL_RESULT_TYPE_REF);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -575,12 +669,13 @@ public class McpAsyncClient {
|
|||
* @return A Mono that emits the list of tools result
|
||||
*/
|
||||
public Mono<McpSchema.ListToolsResult> listTools(String cursor) {
|
||||
return this.withInitializationCheck("listing tools", initializedResult -> {
|
||||
if (this.serverCapabilities.tools() == null) {
|
||||
return this.withSession("listing tools", init -> {
|
||||
if (init.get().capabilities().tools() == null) {
|
||||
return Mono.error(new McpError("Server does not provide tools capability"));
|
||||
}
|
||||
return this.mcpSession.sendRequest(McpSchema.METHOD_TOOLS_LIST, new McpSchema.PaginatedRequest(cursor),
|
||||
LIST_TOOLS_RESULT_TYPE_REF);
|
||||
return init.mcpSession()
|
||||
.sendRequest(McpSchema.METHOD_TOOLS_LIST, new McpSchema.PaginatedRequest(cursor),
|
||||
LIST_TOOLS_RESULT_TYPE_REF);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -632,12 +727,13 @@ public class McpAsyncClient {
|
|||
* @see #readResource(McpSchema.Resource)
|
||||
*/
|
||||
public Mono<McpSchema.ListResourcesResult> listResources(String cursor) {
|
||||
return this.withInitializationCheck("listing resources", initializedResult -> {
|
||||
if (this.serverCapabilities.resources() == null) {
|
||||
return this.withSession("listing resources", init -> {
|
||||
if (init.get().capabilities().resources() == null) {
|
||||
return Mono.error(new McpError("Server does not provide the resources capability"));
|
||||
}
|
||||
return this.mcpSession.sendRequest(McpSchema.METHOD_RESOURCES_LIST, new McpSchema.PaginatedRequest(cursor),
|
||||
LIST_RESOURCES_RESULT_TYPE_REF);
|
||||
return init.mcpSession()
|
||||
.sendRequest(McpSchema.METHOD_RESOURCES_LIST, new McpSchema.PaginatedRequest(cursor),
|
||||
LIST_RESOURCES_RESULT_TYPE_REF);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -663,12 +759,12 @@ public class McpAsyncClient {
|
|||
* @see McpSchema.ReadResourceResult
|
||||
*/
|
||||
public Mono<McpSchema.ReadResourceResult> readResource(McpSchema.ReadResourceRequest readResourceRequest) {
|
||||
return this.withInitializationCheck("reading resources", initializedResult -> {
|
||||
if (this.serverCapabilities.resources() == null) {
|
||||
return this.withSession("reading resources", init -> {
|
||||
if (init.get().capabilities().resources() == null) {
|
||||
return Mono.error(new McpError("Server does not provide the resources capability"));
|
||||
}
|
||||
return this.mcpSession.sendRequest(McpSchema.METHOD_RESOURCES_READ, readResourceRequest,
|
||||
READ_RESOURCE_RESULT_TYPE_REF);
|
||||
return init.mcpSession()
|
||||
.sendRequest(McpSchema.METHOD_RESOURCES_READ, readResourceRequest, READ_RESOURCE_RESULT_TYPE_REF);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -692,12 +788,13 @@ public class McpAsyncClient {
|
|||
* @see McpSchema.ListResourceTemplatesResult
|
||||
*/
|
||||
public Mono<McpSchema.ListResourceTemplatesResult> listResourceTemplates(String cursor) {
|
||||
return this.withInitializationCheck("listing resource templates", initializedResult -> {
|
||||
if (this.serverCapabilities.resources() == null) {
|
||||
return this.withSession("listing resource templates", init -> {
|
||||
if (init.get().capabilities().resources() == null) {
|
||||
return Mono.error(new McpError("Server does not provide the resources capability"));
|
||||
}
|
||||
return this.mcpSession.sendRequest(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST,
|
||||
new McpSchema.PaginatedRequest(cursor), LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF);
|
||||
return init.mcpSession()
|
||||
.sendRequest(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, new McpSchema.PaginatedRequest(cursor),
|
||||
LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -711,7 +808,7 @@ public class McpAsyncClient {
|
|||
* @see #unsubscribeResource(McpSchema.UnsubscribeRequest)
|
||||
*/
|
||||
public Mono<Void> subscribeResource(McpSchema.SubscribeRequest subscribeRequest) {
|
||||
return this.withInitializationCheck("subscribing to resources", initializedResult -> this.mcpSession
|
||||
return this.withSession("subscribing to resources", init -> init.mcpSession()
|
||||
.sendRequest(McpSchema.METHOD_RESOURCES_SUBSCRIBE, subscribeRequest, VOID_TYPE_REFERENCE));
|
||||
}
|
||||
|
||||
|
|
@ -725,7 +822,7 @@ public class McpAsyncClient {
|
|||
* @see #subscribeResource(McpSchema.SubscribeRequest)
|
||||
*/
|
||||
public Mono<Void> unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) {
|
||||
return this.withInitializationCheck("unsubscribing from resources", initializedResult -> this.mcpSession
|
||||
return this.withSession("unsubscribing from resources", init -> init.mcpSession()
|
||||
.sendRequest(McpSchema.METHOD_RESOURCES_UNSUBSCRIBE, unsubscribeRequest, VOID_TYPE_REFERENCE));
|
||||
}
|
||||
|
||||
|
|
@ -767,7 +864,7 @@ public class McpAsyncClient {
|
|||
* @see #getPrompt(GetPromptRequest)
|
||||
*/
|
||||
public Mono<ListPromptsResult> listPrompts(String cursor) {
|
||||
return this.withInitializationCheck("listing prompts", initializedResult -> this.mcpSession
|
||||
return this.withSession("listing prompts", init -> init.mcpSession()
|
||||
.sendRequest(McpSchema.METHOD_PROMPT_LIST, new PaginatedRequest(cursor), LIST_PROMPTS_RESULT_TYPE_REF));
|
||||
}
|
||||
|
||||
|
|
@ -781,7 +878,7 @@ public class McpAsyncClient {
|
|||
* @see #listPrompts()
|
||||
*/
|
||||
public Mono<GetPromptResult> getPrompt(GetPromptRequest getPromptRequest) {
|
||||
return this.withInitializationCheck("getting prompts", initializedResult -> this.mcpSession
|
||||
return this.withSession("getting prompts", init -> init.mcpSession()
|
||||
.sendRequest(McpSchema.METHOD_PROMPT_GET, getPromptRequest, GET_PROMPT_RESULT_TYPE_REF));
|
||||
}
|
||||
|
||||
|
|
@ -812,8 +909,7 @@ public class McpAsyncClient {
|
|||
|
||||
return params -> {
|
||||
McpSchema.LoggingMessageNotification loggingMessageNotification = transport.unmarshalFrom(params,
|
||||
new TypeReference<McpSchema.LoggingMessageNotification>() {
|
||||
});
|
||||
LOGGING_MESSAGE_NOTIFICATION_TYPE_REF);
|
||||
|
||||
return Flux.fromIterable(loggingConsumers)
|
||||
.flatMap(consumer -> consumer.apply(loggingMessageNotification))
|
||||
|
|
@ -833,10 +929,9 @@ public class McpAsyncClient {
|
|||
return Mono.error(new McpError("Logging level must not be null"));
|
||||
}
|
||||
|
||||
return this.withInitializationCheck("setting logging level", initializedResult -> {
|
||||
return this.withSession("setting logging level", init -> {
|
||||
var params = new McpSchema.SetLevelRequest(loggingLevel);
|
||||
return this.mcpSession.sendRequest(McpSchema.METHOD_LOGGING_SET_LEVEL, params, new TypeReference<Object>() {
|
||||
}).then();
|
||||
return init.mcpSession().sendRequest(McpSchema.METHOD_LOGGING_SET_LEVEL, params, OBJECT_TYPE_REF).then();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -866,7 +961,7 @@ public class McpAsyncClient {
|
|||
* @see McpSchema.CompleteResult
|
||||
*/
|
||||
public Mono<McpSchema.CompleteResult> completeCompletion(McpSchema.CompleteRequest completeRequest) {
|
||||
return this.withInitializationCheck("complete completions", initializedResult -> this.mcpSession
|
||||
return this.withSession("complete completions", init -> init.mcpSession()
|
||||
.sendRequest(McpSchema.METHOD_COMPLETION_COMPLETE, completeRequest, COMPLETION_COMPLETE_RESULT_TYPE_REF));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ public class StdioClientTransport implements McpClientTransport {
|
|||
@Override
|
||||
public Mono<Void> connect(Function<Mono<JSONRPCMessage>, Mono<JSONRPCMessage>> handler) {
|
||||
return Mono.<Void>fromRunnable(() -> {
|
||||
logger.info("MCP server starting.");
|
||||
handleIncomingMessages(handler);
|
||||
handleIncomingErrors();
|
||||
|
||||
|
|
@ -142,6 +143,7 @@ public class StdioClientTransport implements McpClientTransport {
|
|||
startInboundProcessing();
|
||||
startOutboundProcessing();
|
||||
startErrorProcessing();
|
||||
logger.info("MCP server started");
|
||||
}).subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
|
|
@ -366,6 +368,9 @@ public class StdioClientTransport implements McpClientTransport {
|
|||
if (process.exitValue() != 0) {
|
||||
logger.warn("Process terminated with code " + process.exitValue());
|
||||
}
|
||||
else {
|
||||
logger.info("MCP server process stopped");
|
||||
}
|
||||
}).then(Mono.fromRunnable(() -> {
|
||||
try {
|
||||
// The Threads are blocked on readLine so disposeGracefully would not
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
package io.modelcontextprotocol.spec;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.Disposables;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link McpTransportSession} which manages the open
|
||||
* connections using tye {@link Disposable} type and allows to perform clean up using the
|
||||
* {@link Disposable#dispose()} method.
|
||||
*
|
||||
* @author Dariusz Jędrzejczyk
|
||||
*/
|
||||
public class DefaultMcpTransportSession implements McpTransportSession<Disposable> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DefaultMcpTransportSession.class);
|
||||
|
||||
private final Disposable.Composite openConnections = Disposables.composite();
|
||||
|
||||
private final AtomicBoolean initialized = new AtomicBoolean(false);
|
||||
|
||||
private final AtomicReference<String> sessionId = new AtomicReference<>();
|
||||
|
||||
private final Supplier<Publisher<Void>> onClose;
|
||||
|
||||
public DefaultMcpTransportSession(Supplier<Publisher<Void>> onClose) {
|
||||
this.onClose = onClose;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> sessionId() {
|
||||
return Optional.ofNullable(this.sessionId.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markInitialized(String sessionId) {
|
||||
boolean flipped = this.initialized.compareAndSet(false, true);
|
||||
if (flipped) {
|
||||
this.sessionId.set(sessionId);
|
||||
logger.debug("Established session with id {}", sessionId);
|
||||
}
|
||||
else {
|
||||
if (sessionId != null && !sessionId.equals(this.sessionId.get())) {
|
||||
logger.warn("Different session id provided in response. Expecting {} but server returned {}",
|
||||
this.sessionId.get(), sessionId);
|
||||
}
|
||||
}
|
||||
return flipped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addConnection(Disposable connection) {
|
||||
this.openConnections.add(connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeConnection(Disposable connection) {
|
||||
this.openConnections.remove(connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
this.closeGracefully().subscribe();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> closeGracefully() {
|
||||
return Mono.from(this.onClose.get()).then(Mono.fromRunnable(this.openConnections::dispose));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package io.modelcontextprotocol.spec;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.function.Tuple2;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* An implementation of {@link McpTransportStream} using Project Reactor types.
|
||||
*
|
||||
* @param <CONNECTION> the resource serving the stream
|
||||
* @author Dariusz Jędrzejczyk
|
||||
*/
|
||||
public class DefaultMcpTransportStream<CONNECTION> implements McpTransportStream<CONNECTION> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DefaultMcpTransportStream.class);
|
||||
|
||||
private static final AtomicLong counter = new AtomicLong();
|
||||
|
||||
private final AtomicReference<String> lastId = new AtomicReference<>();
|
||||
|
||||
// Used only for internal accounting
|
||||
private final long streamId;
|
||||
|
||||
private final boolean resumable;
|
||||
|
||||
private final Function<McpTransportStream<CONNECTION>, Publisher<CONNECTION>> reconnect;
|
||||
|
||||
/**
|
||||
* Constructs a new instance representing a particular stream that can resume using
|
||||
* the provided reconnect mechanism.
|
||||
* @param resumable whether the stream is resumable and should try to reconnect
|
||||
* @param reconnect the mechanism to use in case an error is observed on the current
|
||||
* event stream to asynchronously kick off a resumed stream consumption, potentially
|
||||
* using the stored {@link #lastId()}.
|
||||
*/
|
||||
public DefaultMcpTransportStream(boolean resumable,
|
||||
Function<McpTransportStream<CONNECTION>, Publisher<CONNECTION>> reconnect) {
|
||||
this.reconnect = reconnect;
|
||||
this.streamId = counter.getAndIncrement();
|
||||
this.resumable = resumable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> lastId() {
|
||||
return Optional.ofNullable(this.lastId.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long streamId() {
|
||||
return this.streamId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher<McpSchema.JSONRPCMessage> consumeSseStream(
|
||||
Publisher<Tuple2<Optional<String>, Iterable<McpSchema.JSONRPCMessage>>> eventStream) {
|
||||
return Flux.deferContextual(ctx -> Flux.from(eventStream).doOnError(e -> {
|
||||
if (resumable && !(e instanceof McpTransportSessionNotFoundException)) {
|
||||
Mono.from(reconnect.apply(this)).contextWrite(ctx).subscribe();
|
||||
}
|
||||
}).doOnNext(idAndMessage -> idAndMessage.getT1().ifPresent(id -> {
|
||||
String previousId = this.lastId.getAndSet(id);
|
||||
logger.debug("Updating last id {} -> {} for stream {}", previousId, id, this.streamId);
|
||||
})).flatMapIterable(Tuple2::getT2));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,20 +4,19 @@
|
|||
|
||||
package io.modelcontextprotocol.spec;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import io.modelcontextprotocol.util.Assert;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.MonoSink;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import io.modelcontextprotocol.util.Assert;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.MonoSink;
|
||||
|
||||
/**
|
||||
* Default implementation of the MCP (Model Context Protocol) session that manages
|
||||
* bidirectional JSON-RPC communication between clients and servers. This implementation
|
||||
|
|
@ -37,7 +36,6 @@ import reactor.core.publisher.MonoSink;
|
|||
*/
|
||||
public class McpClientSession implements McpSession {
|
||||
|
||||
/** Logger for this class */
|
||||
private static final Logger logger = LoggerFactory.getLogger(McpClientSession.class);
|
||||
|
||||
/** Duration to wait for request responses before timing out */
|
||||
|
|
@ -61,8 +59,6 @@ public class McpClientSession implements McpSession {
|
|||
/** Atomic counter for generating unique request IDs */
|
||||
private final AtomicLong requestCounter = new AtomicLong(0);
|
||||
|
||||
private final Disposable connection;
|
||||
|
||||
/**
|
||||
* Functional interface for handling incoming JSON-RPC requests. Implementations
|
||||
* should process the request parameters and return a response.
|
||||
|
|
@ -117,12 +113,15 @@ public class McpClientSession implements McpSession {
|
|||
this.requestHandlers.putAll(requestHandlers);
|
||||
this.notificationHandlers.putAll(notificationHandlers);
|
||||
|
||||
// TODO: consider mono.transformDeferredContextual where the Context contains
|
||||
// the
|
||||
// Observation associated with the individual message - it can be used to
|
||||
// create child Observation and emit it together with the message to the
|
||||
// consumer
|
||||
this.connection = this.transport.connect(mono -> mono.doOnNext(this::handle)).subscribe();
|
||||
this.transport.connect(mono -> mono.doOnNext(this::handle)).subscribe();
|
||||
}
|
||||
|
||||
private void dismissPendingResponses() {
|
||||
this.pendingResponses.forEach((id, sink) -> {
|
||||
logger.warn("Abruptly terminating exchange for request {}", id);
|
||||
sink.error(new RuntimeException("MCP session with server terminated"));
|
||||
});
|
||||
this.pendingResponses.clear();
|
||||
}
|
||||
|
||||
private void handle(McpSchema.JSONRPCMessage message) {
|
||||
|
|
@ -231,17 +230,15 @@ public class McpClientSession implements McpSession {
|
|||
String requestId = this.generateRequestId();
|
||||
|
||||
return Mono.deferContextual(ctx -> Mono.<McpSchema.JSONRPCResponse>create(sink -> {
|
||||
logger.debug("Sending message for method {}", method);
|
||||
this.pendingResponses.put(requestId, sink);
|
||||
McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, method,
|
||||
requestId, requestParams);
|
||||
this.transport.sendMessage(jsonrpcRequest)
|
||||
.contextWrite(ctx)
|
||||
// TODO: It's most efficient to create a dedicated Subscriber here
|
||||
.subscribe(v -> {
|
||||
}, error -> {
|
||||
this.pendingResponses.remove(requestId);
|
||||
sink.error(error);
|
||||
});
|
||||
this.transport.sendMessage(jsonrpcRequest).contextWrite(ctx).subscribe(v -> {
|
||||
}, error -> {
|
||||
this.pendingResponses.remove(requestId);
|
||||
sink.error(error);
|
||||
});
|
||||
})).timeout(this.requestTimeout).handle((jsonRpcResponse, sink) -> {
|
||||
if (jsonRpcResponse.error() != null) {
|
||||
logger.error("Error handling request: {}", jsonRpcResponse.error());
|
||||
|
|
@ -277,10 +274,7 @@ public class McpClientSession implements McpSession {
|
|||
*/
|
||||
@Override
|
||||
public Mono<Void> closeGracefully() {
|
||||
return Mono.defer(() -> {
|
||||
this.connection.dispose();
|
||||
return transport.closeGracefully();
|
||||
});
|
||||
return Mono.fromRunnable(this::dismissPendingResponses);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -288,8 +282,7 @@ public class McpClientSession implements McpSession {
|
|||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
this.connection.dispose();
|
||||
transport.close();
|
||||
dismissPendingResponses();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,18 +3,38 @@
|
|||
*/
|
||||
package io.modelcontextprotocol.spec;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Marker interface for the client-side MCP transport.
|
||||
* Interface for the client side of the {@link McpTransport}. It allows setting handlers
|
||||
* for messages that are incoming from the MCP server and hooking in to exceptions raised
|
||||
* on the transport layer.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Dariusz Jędrzejczyk
|
||||
*/
|
||||
public interface McpClientTransport extends McpTransport {
|
||||
|
||||
/**
|
||||
* Used to register the incoming messages' handler and potentially (eagerly) connect
|
||||
* to the server.
|
||||
* @param handler a transformer for incoming messages
|
||||
* @return a {@link Mono} that terminates upon successful client setup. It can mean
|
||||
* establishing a connection (which can be later disposed) but it doesn't have to,
|
||||
* depending on the transport type. The successful termination of the returned
|
||||
* {@link Mono} simply means the client can now be used. An error can be retried
|
||||
* according to the application requirements.
|
||||
*/
|
||||
Mono<Void> connect(Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler);
|
||||
|
||||
/**
|
||||
* Sets the exception handler for exceptions raised on the transport layer.
|
||||
* @param handler Allows reacting to transport level exceptions by the higher layers
|
||||
*/
|
||||
default void setExceptionHandler(Consumer<Throwable> handler) {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -184,6 +184,8 @@ public final class McpSchema {
|
|||
|
||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
// TODO: batching support
|
||||
// @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||
public record JSONRPCRequest( // @formatter:off
|
||||
@JsonProperty("jsonrpc") String jsonrpc,
|
||||
@JsonProperty("method") String method,
|
||||
|
|
@ -193,6 +195,8 @@ public final class McpSchema {
|
|||
|
||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
// TODO: batching support
|
||||
// @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||
public record JSONRPCNotification( // @formatter:off
|
||||
@JsonProperty("jsonrpc") String jsonrpc,
|
||||
@JsonProperty("method") String method,
|
||||
|
|
@ -201,6 +205,8 @@ public final class McpSchema {
|
|||
|
||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
// TODO: batching support
|
||||
// @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||
public record JSONRPCResponse( // @formatter:off
|
||||
@JsonProperty("jsonrpc") String jsonrpc,
|
||||
@JsonProperty("id") Object id,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
package io.modelcontextprotocol.spec;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* An abstraction of the session as perceived from the MCP transport layer. Not to be
|
||||
* confused with the {@link McpSession} type that operates at the level of the JSON-RPC
|
||||
* communication protocol and matches asynchronous responses with previously issued
|
||||
* requests.
|
||||
*
|
||||
* @param <CONNECTION> the resource representing the connection that the transport
|
||||
* manages.
|
||||
* @author Dariusz Jędrzejczyk
|
||||
*/
|
||||
public interface McpTransportSession<CONNECTION> {
|
||||
|
||||
/**
|
||||
* In case of stateful MCP servers, the value is present and contains the String
|
||||
* identifier for the transport-level session.
|
||||
* @return optional session id
|
||||
*/
|
||||
Optional<String> sessionId();
|
||||
|
||||
/**
|
||||
* Stateful operation that flips the un-initialized state to initialized if this is
|
||||
* the first call. If the transport provides a session id for the communication,
|
||||
* argument should not be null to record the current identifier.
|
||||
* @param sessionId session identifier as provided by the server
|
||||
* @return if successful, this method returns {@code true} and means that a
|
||||
* post-initialization step can be performed
|
||||
*/
|
||||
boolean markInitialized(String sessionId);
|
||||
|
||||
/**
|
||||
* Adds a resource that this transport session can monitor and dismiss when needed.
|
||||
* @param connection the managed resource
|
||||
*/
|
||||
void addConnection(CONNECTION connection);
|
||||
|
||||
/**
|
||||
* Called when the resource is terminating by itself and the transport session does
|
||||
* not need to track it anymore.
|
||||
* @param connection the resource to remove from the monitored collection
|
||||
*/
|
||||
void removeConnection(CONNECTION connection);
|
||||
|
||||
/**
|
||||
* Close and clear the monitored resources. Potentially asynchronous.
|
||||
*/
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Close and clear the monitored resources in a graceful manner.
|
||||
* @return completes once all resources have been dismissed
|
||||
*/
|
||||
Publisher<Void> closeGracefully();
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package io.modelcontextprotocol.spec;
|
||||
|
||||
/**
|
||||
* Exception that signifies that the server does not recognize the connecting client via
|
||||
* the presented transport session identifier.
|
||||
*
|
||||
* @author Dariusz Jędrzejczyk
|
||||
*/
|
||||
public class McpTransportSessionNotFoundException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Construct an instance with a known {@link Exception cause}.
|
||||
* @param sessionId transport session identifier
|
||||
* @param cause the cause that was identified as a session not found error
|
||||
*/
|
||||
public McpTransportSessionNotFoundException(String sessionId, Exception cause) {
|
||||
super("Session " + sessionId + " not found on the server", cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an instance with the session identifier but without a {@link Exception
|
||||
* cause}.
|
||||
* @param sessionId transport session identifier
|
||||
*/
|
||||
public McpTransportSessionNotFoundException(String sessionId) {
|
||||
super("Session " + sessionId + " not found on the server");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package io.modelcontextprotocol.spec;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.util.function.Tuple2;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A representation of a stream at the transport layer of the MCP protocol. In particular,
|
||||
* it is currently used in the Streamable HTTP implementation to potentially be able to
|
||||
* resume a broken connection from where it left off by optionally keeping track of
|
||||
* attached SSE event ids.
|
||||
*
|
||||
* @param <CONNECTION> the resource on which the stream is being served and consumed via
|
||||
* this mechanism
|
||||
* @author Dariusz Jędrzejczyk
|
||||
*/
|
||||
public interface McpTransportStream<CONNECTION> {
|
||||
|
||||
/**
|
||||
* The last observed event identifier.
|
||||
* @return if not empty, contains the most recent event that was consumed
|
||||
*/
|
||||
Optional<String> lastId();
|
||||
|
||||
/**
|
||||
* An internal stream identifier used to distinguish streams while debugging.
|
||||
* @return a {@code long} stream identifier value
|
||||
*/
|
||||
long streamId();
|
||||
|
||||
/**
|
||||
* Allows keeping track of the transport stream of events (currently an SSE stream
|
||||
* from Streamable HTTP specification) and enable resumability and reconnects in case
|
||||
* of stream errors.
|
||||
* @param eventStream a {@link Publisher} of tuples (pairs) of an optional identifier
|
||||
* associated with a collection of messages
|
||||
* @return a flattened {@link Publisher} of
|
||||
* {@link io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage JSON-RPC messages}
|
||||
* with the identifier stripped away
|
||||
*/
|
||||
Publisher<McpSchema.JSONRPCMessage> consumeSseStream(
|
||||
Publisher<Tuple2<Optional<String>, Iterable<McpSchema.JSONRPCMessage>>> eventStream);
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue