From b8ae1e39e03a03a680225b6bb5d1d1d7108e9e1a Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Tue, 11 Mar 2025 12:10:45 +0100 Subject: [PATCH 1/6] feat(client): Improve initialization state handling in McpAsyncClient - Add proper initialization state tracking using AtomicBoolean and Sinks - Implement timeout handling for requests requiring initialization - Ensure all client methods verify initialization state before proceeding - Fix rootsListChangedNotification to check initialization state - Improve error messages for uninitialized client operations Signed-off-by: Christian Tzolov --- .../client/McpAsyncClient.java | 186 +++++++++++------- 1 file changed, 117 insertions(+), 69 deletions(-) diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index e1bf574e..e9a424a1 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -9,6 +9,8 @@ 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.function.Function; import com.fasterxml.jackson.core.type.TypeReference; @@ -35,6 +37,7 @@ import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; /** * The Model Context Protocol (MCP) client implementation that provides asynchronous @@ -79,6 +82,12 @@ public class McpAsyncClient { private static TypeReference VOID_TYPE_REFERENCE = new TypeReference<>() { }; + protected final Sinks.One initializedSink = Sinks.one(); + + private AtomicBoolean initialized = new AtomicBoolean(false); + + private final Duration initializedTimeout; + /** * The MCP session implementation that manages bidirectional JSON-RPC communication * between clients and servers. @@ -149,6 +158,7 @@ public class McpAsyncClient { this.clientCapabilities = features.clientCapabilities(); this.transport = transport; this.roots = new ConcurrentHashMap<>(features.roots()); + this.initializedTimeout = requestTimeout.multipliedBy(2); // Request Handlers Map> requestHandlers = new HashMap<>(); @@ -253,8 +263,8 @@ public Mono initialize() { McpSchema.InitializeRequest initializeRequest = new McpSchema.InitializeRequest(// @formatter:off latestVersion, - this.clientCapabilities, - this.clientInfo); // @formatter:on + this.clientCapabilities, + this.clientInfo); // @formatter:on Mono result = this.mcpSession.sendRequest(McpSchema.METHOD_INITIALIZE, initializeRequest, new TypeReference() { @@ -273,10 +283,11 @@ public Mono initialize() { return Mono.error(new McpError( "Unsupported protocol version from the server: " + initializeResult.protocolVersion())); } - else { - return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_INITIALIZED, null) - .thenReturn(initializeResult); - } + + return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_INITIALIZED, null).doOnSuccess(v -> { + this.initialized.set(true); + this.initializedSink.tryEmitValue(initializeResult); + }).thenReturn(initializeResult); }); } @@ -301,7 +312,7 @@ public McpSchema.Implementation getServerInfo() { * @return true if the client-server connection is initialized */ public boolean isInitialized() { - return this.serverCapabilities != null; + return this.initialized.get(); } /** @@ -335,6 +346,26 @@ public Mono closeGracefully() { return this.mcpSession.closeGracefully(); } + // -------------------------- + // Utility Methods + // -------------------------- + + /** + * Utility method to handle the common pattern of checking initialization before + * executing an operation. + * @param The type of the result Mono + * @param errorMessage The error message to use if the client is not initialized + * @param operation The operation to execute if the client is initialized + * @return A Mono that completes with the result of the operation + */ + private Mono withInitializationCheck(String errorMessage, + Function> operation) { + return this.initializedSink.asMono() + .timeout(this.initializedTimeout) + .onErrorResume(TimeoutException.class, ex -> Mono.error(new McpError(errorMessage))) + .flatMap(operation); + } + // -------------------------- // Basic Utilites // -------------------------- @@ -344,8 +375,10 @@ public Mono closeGracefully() { * @return A Mono that completes with the server's ping response */ public Mono ping() { - return this.mcpSession.sendRequest(McpSchema.METHOD_PING, null, new TypeReference() { - }); + return withInitializationCheck("Client must be initialized before pinging the server", + initializedResult -> this.mcpSession.sendRequest(McpSchema.METHOD_PING, null, + new TypeReference() { + })); } // -------------------------- @@ -375,7 +408,12 @@ public Mono addRoot(Root root) { logger.debug("Added root: {}", root); if (this.clientCapabilities.roots().listChanged()) { - return this.rootsListChangedNotification(); + if (this.isInitialized()) { + return this.rootsListChangedNotification(); + } + else { + logger.warn("Client is not initialized, ignore sending a roots list changed notification"); + } } return Mono.empty(); } @@ -400,7 +438,13 @@ public Mono removeRoot(String rootUri) { if (removed != null) { logger.debug("Removed Root: {}", rootUri); if (this.clientCapabilities.roots().listChanged()) { - return this.rootsListChangedNotification(); + if (this.isInitialized()) { + return this.rootsListChangedNotification(); + } + else { + logger.warn("Client is not initialized, ignore sending a roots list changed notification"); + } + } return Mono.empty(); } @@ -413,7 +457,8 @@ public Mono removeRoot(String rootUri) { * @return A Mono that completes when the notification is sent */ public Mono rootsListChangedNotification() { - return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED); + return this.withInitializationCheck("Client must be initialized before sending roots list changed notification", + initResult -> this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED)); } private RequestHandler rootsListRequestHandler() { @@ -464,13 +509,12 @@ private RequestHandler samplingCreateMessageHandler() { * (false/absent) */ public Mono callTool(McpSchema.CallToolRequest callToolRequest) { - if (!this.isInitialized()) { - return Mono.error(new McpError("Client must be initialized before calling tools")); - } - if (this.serverCapabilities.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 withInitializationCheck("Client must be initialized before calling tools", initializedResult -> { + if (this.serverCapabilities.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); + }); } /** @@ -491,14 +535,13 @@ public Mono listTools() { * Optional cursor for pagination if more tools are available */ public Mono listTools(String cursor) { - if (!this.isInitialized()) { - return Mono.error(new McpError("Client must be initialized before listing tools")); - } - if (this.serverCapabilities.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 withInitializationCheck("Client must be initialized before listing tools", initializedResult -> { + if (this.serverCapabilities.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); + }); } /** @@ -516,13 +559,14 @@ public Mono listTools(String cursor) { private NotificationHandler asyncToolsChangeNotificationHandler( List, Mono>> toolsChangeConsumers) { // TODO: params are not used yet - return params -> listTools().flatMap(listToolsResult -> Flux.fromIterable(toolsChangeConsumers) - .flatMap(consumer -> consumer.apply(listToolsResult.tools())) - .onErrorResume(error -> { - logger.error("Error handling tools list change notification", error); - return Mono.empty(); - }) - .then()); + return params -> this.listTools() + .flatMap(listToolsResult -> Flux.fromIterable(toolsChangeConsumers) + .flatMap(consumer -> consumer.apply(listToolsResult.tools())) + .onErrorResume(error -> { + logger.error("Error handling tools list change notification", error); + return Mono.empty(); + }) + .then()); } // -------------------------- @@ -552,14 +596,13 @@ public Mono listResources() { * @return A Mono that completes with the list of resources result */ public Mono listResources(String cursor) { - if (!this.isInitialized()) { - return Mono.error(new McpError("Client must be initialized before listing resources")); - } - if (this.serverCapabilities.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 withInitializationCheck("Client must be initialized before listing resources", initializedResult -> { + if (this.serverCapabilities.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); + }); } /** @@ -577,14 +620,13 @@ public Mono readResource(McpSchema.Resource resour * @return A Mono that completes with the resource content */ public Mono readResource(McpSchema.ReadResourceRequest readResourceRequest) { - if (!this.isInitialized()) { - return Mono.error(new McpError("Client must be initialized before reading resources")); - } - if (this.serverCapabilities.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 withInitializationCheck("Client must be initialized before reading resources", initializedResult -> { + if (this.serverCapabilities.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); + }); } /** @@ -607,14 +649,14 @@ public Mono listResourceTemplates() { * @return A Mono that completes with the list of resource templates result */ public Mono listResourceTemplates(String cursor) { - if (!this.isInitialized()) { - return Mono.error(new McpError("Client must be initialized before listing resource templates")); - } - if (this.serverCapabilities.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 withInitializationCheck("Client must be initialized before listing resource templates", + initializedResult -> { + if (this.serverCapabilities.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); + }); } /** @@ -628,7 +670,9 @@ public Mono listResourceTemplates(String * @return A Mono that completes when the subscription is complete */ public Mono subscribeResource(McpSchema.SubscribeRequest subscribeRequest) { - return this.mcpSession.sendRequest(McpSchema.METHOD_RESOURCES_SUBSCRIBE, subscribeRequest, VOID_TYPE_REFERENCE); + return withInitializationCheck("Client must be initialized before subscribing to resources", + initializedResult -> this.mcpSession.sendRequest(McpSchema.METHOD_RESOURCES_SUBSCRIBE, subscribeRequest, + VOID_TYPE_REFERENCE)); } /** @@ -638,8 +682,9 @@ public Mono subscribeResource(McpSchema.SubscribeRequest subscribeRequest) * @return A Mono that completes when the unsubscription is complete */ public Mono unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) { - return this.mcpSession.sendRequest(McpSchema.METHOD_RESOURCES_UNSUBSCRIBE, unsubscribeRequest, - VOID_TYPE_REFERENCE); + return withInitializationCheck("Client must be initialized before unsubscribing from resources", + initializedResult -> this.mcpSession.sendRequest(McpSchema.METHOD_RESOURCES_UNSUBSCRIBE, + unsubscribeRequest, VOID_TYPE_REFERENCE)); } private NotificationHandler asyncResourcesChangeNotificationHandler( @@ -676,8 +721,9 @@ public Mono listPrompts() { * @return A Mono that completes with the list of prompts result */ public Mono listPrompts(String cursor) { - return this.mcpSession.sendRequest(McpSchema.METHOD_PROMPT_LIST, new PaginatedRequest(cursor), - LIST_PROMPTS_RESULT_TYPE_REF); + return withInitializationCheck("Client must be initialized before listing prompts", + initializedResult -> this.mcpSession.sendRequest(McpSchema.METHOD_PROMPT_LIST, + new PaginatedRequest(cursor), LIST_PROMPTS_RESULT_TYPE_REF)); } /** @@ -686,7 +732,9 @@ public Mono listPrompts(String cursor) { * @return A Mono that completes with the get prompt result */ public Mono getPrompt(GetPromptRequest getPromptRequest) { - return this.mcpSession.sendRequest(McpSchema.METHOD_PROMPT_GET, getPromptRequest, GET_PROMPT_RESULT_TYPE_REF); + return withInitializationCheck("Client must be initialized before getting prompts", + initializedResult -> this.mcpSession.sendRequest(McpSchema.METHOD_PROMPT_GET, getPromptRequest, + GET_PROMPT_RESULT_TYPE_REF)); } private NotificationHandler asyncPromptsChangeNotificationHandler( @@ -732,12 +780,12 @@ private NotificationHandler asyncLoggingNotificationHandler( public Mono setLoggingLevel(LoggingLevel loggingLevel) { Assert.notNull(loggingLevel, "Logging level must not be null"); - String levelName = this.transport.unmarshalFrom(loggingLevel, new TypeReference() { + return withInitializationCheck("Client must be initialized before setting logging level", initializedResult -> { + String levelName = this.transport.unmarshalFrom(loggingLevel, new TypeReference() { + }); + Map params = Map.of("level", levelName); + return this.mcpSession.sendNotification(McpSchema.METHOD_LOGGING_SET_LEVEL, params); }); - - Map params = Map.of("level", levelName); - - return this.mcpSession.sendNotification(McpSchema.METHOD_LOGGING_SET_LEVEL, params); } /** From ffe54489ae3f0ac7a31006b007ca38831fdc1583 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Tue, 11 Mar 2025 14:56:21 +0100 Subject: [PATCH 2/6] Address review comments and improve JavaDoc Signed-off-by: Christian Tzolov --- .../client/McpAsyncClient.java | 452 ++++++++++++------ 1 file changed, 307 insertions(+), 145 deletions(-) diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index e9a424a1..19c88282 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -86,7 +86,11 @@ public class McpAsyncClient { private AtomicBoolean initialized = new AtomicBoolean(false); - private final Duration initializedTimeout; + /** + * The max timeout to await for the client-server connection to be initialized. + * Usually x2 the request timeout. + */ + private final Duration initializationTimeout; /** * The MCP session implementation that manages bidirectional JSON-RPC communication @@ -158,7 +162,7 @@ public class McpAsyncClient { this.clientCapabilities = features.clientCapabilities(); this.transport = transport; this.roots = new ConcurrentHashMap<>(features.roots()); - this.initializedTimeout = requestTimeout.multipliedBy(2); + this.initializationTimeout = requestTimeout.multipliedBy(2); // Request Handlers Map> requestHandlers = new HashMap<>(); @@ -226,8 +230,63 @@ public class McpAsyncClient { } + /** + * Get the server capabilities that define the supported features and functionality. + * @return The server capabilities + */ + public McpSchema.ServerCapabilities getServerCapabilities() { + return this.serverCapabilities; + } + + /** + * Get the server implementation information. + * @return The server implementation details + */ + public McpSchema.Implementation getServerInfo() { + return this.serverInfo; + } + + /** + * Check if the client-server connection is initialized. + * @return true if the client-server connection is initialized + */ + public boolean isInitialized() { + return this.initialized.get(); + } + + /** + * Get the client capabilities that define the supported features and functionality. + * @return The client capabilities + */ + public ClientCapabilities getClientCapabilities() { + return this.clientCapabilities; + } + + /** + * Get the client implementation information. + * @return The client implementation details + */ + public McpSchema.Implementation getClientInfo() { + return this.clientInfo; + } + + /** + * Closes the client connection immediately. + */ + public void close() { + this.mcpSession.close(); + } + + /** + * Gracefully closes the client connection. + * @return A Mono that completes when the connection is closed + */ + public Mono closeGracefully() { + return this.mcpSession.closeGracefully(); + } + // -------------------------- - // Lifecycle + // Initialization // -------------------------- /** * The initialization phase MUST be the first interaction between client and server. @@ -291,78 +350,20 @@ public Mono initialize() { }); } - /** - * Get the server capabilities that define the supported features and functionality. - * @return The server capabilities - */ - public McpSchema.ServerCapabilities getServerCapabilities() { - return this.serverCapabilities; - } - - /** - * Get the server implementation information. - * @return The server implementation details - */ - public McpSchema.Implementation getServerInfo() { - return this.serverInfo; - } - - /** - * Check if the client-server connection is initialized. - * @return true if the client-server connection is initialized - */ - public boolean isInitialized() { - return this.initialized.get(); - } - - /** - * Get the client capabilities that define the supported features and functionality. - * @return The client capabilities - */ - public ClientCapabilities getClientCapabilities() { - return this.clientCapabilities; - } - - /** - * Get the client implementation information. - * @return The client implementation details - */ - public McpSchema.Implementation getClientInfo() { - return this.clientInfo; - } - - /** - * Closes the client connection immediately. - */ - public void close() { - this.mcpSession.close(); - } - - /** - * Gracefully closes the client connection. - * @return A Mono that completes when the connection is closed - */ - public Mono closeGracefully() { - return this.mcpSession.closeGracefully(); - } - - // -------------------------- - // Utility Methods - // -------------------------- - /** * Utility method to handle the common pattern of checking initialization before * executing an operation. * @param The type of the result Mono - * @param errorMessage The error message to use if the client is not initialized + * @param actionName The action to perform if the client is initialized * @param operation The operation to execute if the client is initialized * @return A Mono that completes with the result of the operation */ - private Mono withInitializationCheck(String errorMessage, + private Mono withInitializationCheck(String actionName, Function> operation) { return this.initializedSink.asMono() - .timeout(this.initializedTimeout) - .onErrorResume(TimeoutException.class, ex -> Mono.error(new McpError(errorMessage))) + .timeout(this.initializationTimeout) + .onErrorResume(TimeoutException.class, + ex -> Mono.error(new McpError("Client must be initialized before " + actionName))) .flatMap(operation); } @@ -375,10 +376,9 @@ private Mono withInitializationCheck(String errorMessage, * @return A Mono that completes with the server's ping response */ public Mono ping() { - return withInitializationCheck("Client must be initialized before pinging the server", - initializedResult -> this.mcpSession.sendRequest(McpSchema.METHOD_PING, null, - new TypeReference() { - })); + return this.withInitializationCheck("pinging the server", initializedResult -> this.mcpSession + .sendRequest(McpSchema.METHOD_PING, null, new TypeReference() { + })); } // -------------------------- @@ -457,7 +457,7 @@ public Mono removeRoot(String rootUri) { * @return A Mono that completes when the notification is sent */ public Mono rootsListChangedNotification() { - return this.withInitializationCheck("Client must be initialized before sending roots list changed notification", + return this.withInitializationCheck("sending roots list changed notification", initResult -> this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED)); } @@ -500,16 +500,25 @@ private RequestHandler samplingCreateMessageHandler() { * Calls a tool provided by the server. Tools enable servers to expose executable * functionality that can interact with external systems, perform computations, and * take actions in the real world. - * @param callToolRequest The request containing: - name: The name of the tool to call - * (must match a tool name from tools/list) - arguments: Arguments that conform to the - * tool's input schema - * @return A Mono that emits the tool execution result containing: - content: List of - * content items (text, images, or embedded resources) representing the tool's output - * - isError: Boolean indicating if the execution failed (true) or succeeded - * (false/absent) + * @param callToolRequest The request containing: + *
    + *
  • name: The name of the tool to call (must match a tool name from + * tools/list)
  • + *
  • arguments: Arguments that conform to the tool's input schema
  • + *
+ * @return A Mono that emits the tool execution result containing: + *
    + *
  • content: List of content items (text, images, or embedded resources) + * representing the tool's output
  • + *
  • isError: Boolean indicating if the execution failed (true) or succeeded + * (false/absent)
  • + *
+ * @see McpSchema.CallToolRequest + * @see McpSchema.CallToolResult + * @see #listTools() */ public Mono callTool(McpSchema.CallToolRequest callToolRequest) { - return withInitializationCheck("Client must be initialized before calling tools", initializedResult -> { + return this.withInitializationCheck("calling tools", initializedResult -> { if (this.serverCapabilities.tools() == null) { return Mono.error(new McpError("Server does not provide tools capability")); } @@ -535,7 +544,7 @@ public Mono listTools() { * Optional cursor for pagination if more tools are available */ public Mono listTools(String cursor) { - return withInitializationCheck("Client must be initialized before listing tools", initializedResult -> { + return this.withInitializationCheck("listing tools", initializedResult -> { if (this.serverCapabilities.tools() == null) { return Mono.error(new McpError("Server does not provide tools capability")); } @@ -583,20 +592,40 @@ private NotificationHandler asyncToolsChangeNotificationHandler( }; /** - * Send a resources/list request. - * @return A Mono that completes with the list of resources result + * Retrieves the list of all resources provided by the server. Resources represent any + * kind of UTF-8 encoded data that an MCP server makes available to clients, such as + * database records, API responses, log files, and more. + * @return A Mono that completes with the list of resources result containing: + *
    + *
  • resources: List of available resources, each with a URI, name, and + * optional description
  • + *
  • nextCursor: Optional cursor for pagination if more resources are + * available
  • + *
+ * @see McpSchema.ListResourcesResult + * @see #readResource(McpSchema.Resource) */ public Mono listResources() { return this.listResources(null); } /** - * Send a resources/list request. - * @param cursor the cursor for pagination - * @return A Mono that completes with the list of resources result + * Retrieves a paginated list of resources provided by the server. Resources represent + * any kind of UTF-8 encoded data that an MCP server makes available to clients, such + * as database records, API responses, log files, and more. + * @param cursor Optional pagination cursor from a previous list request + * @return A Mono that completes with the list of resources result containing: + *
    + *
  • resources: List of available resources, each with a URI, name, and + * optional description
  • + *
  • nextCursor: Optional cursor for pagination if more resources are + * available
  • + *
+ * @see McpSchema.ListResourcesResult + * @see #readResource(McpSchema.Resource) */ public Mono listResources(String cursor) { - return withInitializationCheck("Client must be initialized before listing resources", initializedResult -> { + return this.withInitializationCheck("listing resources", initializedResult -> { if (this.serverCapabilities.resources() == null) { return Mono.error(new McpError("Server does not provide the resources capability")); } @@ -606,21 +635,36 @@ public Mono listResources(String cursor) { } /** - * Send a resources/read request. - * @param resource the resource to read - * @return A Mono that completes with the resource content + * Reads the content of a specific resource identified by the provided Resource + * object. This method fetches the actual data that the resource represents. + * @param resource The resource to read, containing the URI that identifies the + * resource + * @return A Mono that completes with the resource content containing: + *
    + *
  • contents: List of content items, each with a URI, MIME type, and the + * actual text content
  • + *
+ * @see McpSchema.Resource + * @see McpSchema.ReadResourceResult */ public Mono readResource(McpSchema.Resource resource) { return this.readResource(new McpSchema.ReadResourceRequest(resource.uri())); } /** - * Send a resources/read request. - * @param readResourceRequest the read resource request - * @return A Mono that completes with the resource content + * Reads the content of a specific resource identified by the provided request. This + * method fetches the actual data that the resource represents. + * @param readResourceRequest The request containing the URI of the resource to read + * @return A Mono that completes with the resource content containing: + *
    + *
  • contents: List of content items, each with a URI, MIME type, and the + * actual text content
  • + *
+ * @see McpSchema.ReadResourceRequest + * @see McpSchema.ReadResourceResult */ public Mono readResource(McpSchema.ReadResourceRequest readResourceRequest) { - return withInitializationCheck("Client must be initialized before reading resources", initializedResult -> { + return this.withInitializationCheck("reading resources", initializedResult -> { if (this.serverCapabilities.resources() == null) { return Mono.error(new McpError("Server does not provide the resources capability")); } @@ -630,63 +674,112 @@ public Mono readResource(McpSchema.ReadResourceReq } /** - * Resource templates allow servers to expose parameterized resources using URI - * templates. Arguments may be auto-completed through the completion API. + * Retrieves the list of all resource templates provided by the server. Resource + * templates allow servers to expose parameterized resources using URI templates, + * enabling dynamic resource access based on variable parameters. * - * Request a list of resource templates the server has. + *

+ * For example, a template like "weather://{city}/forecast" allows clients to access + * weather forecasts for different cities by substituting the {city} parameter. * @return A Mono that completes with the list of resource templates result + * containing: + *

    + *
  • resourceTemplates: List of available resource templates, each with a URI + * template, name, and optional description
  • + *
  • nextCursor: Optional cursor for pagination if more templates are + * available
  • + *
+ * @see McpSchema.ListResourceTemplatesResult */ public Mono listResourceTemplates() { return this.listResourceTemplates(null); } /** - * Resource templates allow servers to expose parameterized resources using URI - * templates. Arguments may be auto-completed through the completion API. + * Retrieves a paginated list of resource templates provided by the server. Resource + * templates allow servers to expose parameterized resources using URI templates, + * enabling dynamic resource access based on variable parameters. * - * Request a list of resource templates the server has. - * @param cursor the cursor for pagination + *

+ * For example, a template like "weather://{city}/forecast" allows clients to access + * weather forecasts for different cities by substituting the {city} parameter. + * @param cursor Optional pagination cursor from a previous list request * @return A Mono that completes with the list of resource templates result + * containing: + *

    + *
  • resourceTemplates: List of available resource templates, each with a URI + * template, name, and optional description
  • + *
  • nextCursor: Optional cursor for pagination if more templates are + * available
  • + *
+ * @see McpSchema.ListResourceTemplatesResult */ public Mono listResourceTemplates(String cursor) { - return withInitializationCheck("Client must be initialized before listing resource templates", - initializedResult -> { - if (this.serverCapabilities.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 this.withInitializationCheck("listing resource templates", initializedResult -> { + if (this.serverCapabilities.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); + }); } /** - * Subscriptions. The protocol supports optional subscriptions to resource changes. - * Clients can subscribe to specific resources and receive notifications when they - * change. + * Subscribes to changes in a specific resource. When the resource changes on the + * server, the client will receive notifications through the resources change + * notification handler. * - * Send a resources/subscribe request. - * @param subscribeRequest the subscribe request contains the uri of the resource to - * subscribe to + *

+ * Resource subscriptions enable real-time updates for dynamic resources that may + * change over time, such as system status information, live data feeds, or + * collaborative documents. + * @param subscribeRequest The request containing: + *

    + *
  • uri: The URI of the resource to subscribe to
  • + *
* @return A Mono that completes when the subscription is complete + * @see McpSchema.SubscribeRequest + * @see #unsubscribeResource(McpSchema.UnsubscribeRequest) */ public Mono subscribeResource(McpSchema.SubscribeRequest subscribeRequest) { - return withInitializationCheck("Client must be initialized before subscribing to resources", - initializedResult -> this.mcpSession.sendRequest(McpSchema.METHOD_RESOURCES_SUBSCRIBE, subscribeRequest, - VOID_TYPE_REFERENCE)); + return this.withInitializationCheck("subscribing to resources", initializedResult -> this.mcpSession + .sendRequest(McpSchema.METHOD_RESOURCES_SUBSCRIBE, subscribeRequest, VOID_TYPE_REFERENCE)); } /** - * Send a resources/unsubscribe request. - * @param unsubscribeRequest the unsubscribe request contains the uri of the resource - * to unsubscribe from + * Cancels an existing subscription to a resource. After unsubscribing, the client + * will no longer receive notifications when the resource changes. + * + *

+ * This method should be called when the client is no longer interested in receiving + * updates for a particular resource, to reduce unnecessary network traffic and + * processing. + * @param unsubscribeRequest The request containing: + *

    + *
  • uri: The URI of the resource to unsubscribe from
  • + *
* @return A Mono that completes when the unsubscription is complete + * @see McpSchema.UnsubscribeRequest + * @see #subscribeResource(McpSchema.SubscribeRequest) */ public Mono unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) { - return withInitializationCheck("Client must be initialized before unsubscribing from resources", - initializedResult -> this.mcpSession.sendRequest(McpSchema.METHOD_RESOURCES_UNSUBSCRIBE, - unsubscribeRequest, VOID_TYPE_REFERENCE)); + return this.withInitializationCheck("unsubscribing from resources", initializedResult -> this.mcpSession + .sendRequest(McpSchema.METHOD_RESOURCES_UNSUBSCRIBE, unsubscribeRequest, VOID_TYPE_REFERENCE)); } + /** + * Creates a notification handler for resources/list_changed notifications from the + * server. When the server's available resources change, it sends a notification to + * inform connected clients. This handler automatically fetches the updated resource + * list and distributes it to all registered consumers. + * @param resourcesChangeConsumers List of consumers that will be notified when the + * resources list changes. Each consumer receives the complete updated list of + * resources. + * @return A NotificationHandler that processes resources/list_changed notifications + * by: 1. Fetching the current list of resources from the server 2. Distributing the + * updated list to all registered consumers 3. Handling any errors that occur during + * this process + */ private NotificationHandler asyncResourcesChangeNotificationHandler( List, Mono>> resourcesChangeConsumers) { return params -> listResources().flatMap(listResourcesResult -> Flux.fromIterable(resourcesChangeConsumers) @@ -708,35 +801,81 @@ private NotificationHandler asyncResourcesChangeNotificationHandler( }; /** - * List all available prompts. - * @return A Mono that completes with the list of prompts result + * Retrieves the list of all prompts provided by the server. Prompts are templates + * that define structured interactions with language models, allowing servers to + * request specific types of AI-generated content. + * @return A Mono that completes with the list of prompts result containing: + *
    + *
  • prompts: List of available prompts, each with an ID, name, and + * description
  • + *
  • nextCursor: Optional cursor for pagination if more prompts are + * available
  • + *
+ * @see McpSchema.ListPromptsResult + * @see #getPrompt(GetPromptRequest) */ public Mono listPrompts() { return this.listPrompts(null); } /** - * List all available prompts. - * @param cursor the cursor for pagination - * @return A Mono that completes with the list of prompts result + * Retrieves a paginated list of prompts provided by the server. Prompts are templates + * that define structured interactions with language models, allowing servers to + * request specific types of AI-generated content. + * @param cursor Optional pagination cursor from a previous list request + * @return A Mono that completes with the list of prompts result containing: + *
    + *
  • prompts: List of available prompts, each with an ID, name, and + * description
  • + *
  • nextCursor: Optional cursor for pagination if more prompts are + * available
  • + *
+ * @see McpSchema.ListPromptsResult + * @see #getPrompt(GetPromptRequest) */ public Mono listPrompts(String cursor) { - return withInitializationCheck("Client must be initialized before listing prompts", - initializedResult -> this.mcpSession.sendRequest(McpSchema.METHOD_PROMPT_LIST, - new PaginatedRequest(cursor), LIST_PROMPTS_RESULT_TYPE_REF)); + return this.withInitializationCheck("listing prompts", initializedResult -> this.mcpSession + .sendRequest(McpSchema.METHOD_PROMPT_LIST, new PaginatedRequest(cursor), LIST_PROMPTS_RESULT_TYPE_REF)); } /** - * Get a prompt by its id. - * @param getPromptRequest the get prompt request - * @return A Mono that completes with the get prompt result + * Retrieves a specific prompt by its ID. This provides the complete prompt template + * including all parameters and instructions for generating AI content. + * + *

+ * Prompts define structured interactions with language models, allowing servers to + * request specific types of AI-generated content with consistent formatting and + * behavior. + * @param getPromptRequest The request containing: + *

    + *
  • id: The unique identifier of the prompt to retrieve
  • + *
+ * @return A Mono that completes with the prompt result containing: + *
    + *
  • prompt: The complete prompt details including ID, name, description, and + * template
  • + *
+ * @see McpSchema.GetPromptRequest + * @see McpSchema.GetPromptResult + * @see #listPrompts() */ public Mono getPrompt(GetPromptRequest getPromptRequest) { - return withInitializationCheck("Client must be initialized before getting prompts", - initializedResult -> this.mcpSession.sendRequest(McpSchema.METHOD_PROMPT_GET, getPromptRequest, - GET_PROMPT_RESULT_TYPE_REF)); + return this.withInitializationCheck("getting prompts", initializedResult -> this.mcpSession + .sendRequest(McpSchema.METHOD_PROMPT_GET, getPromptRequest, GET_PROMPT_RESULT_TYPE_REF)); } + /** + * Creates a notification handler for prompts/list_changed notifications from the + * server. When the server's available prompts change, it sends a notification to + * inform connected clients. This handler automatically fetches the updated prompt + * list and distributes it to all registered consumers. + * @param promptsChangeConsumers List of consumers that will be notified when the + * prompts list changes. Each consumer receives the complete updated list of prompts. + * @return A NotificationHandler that processes prompts/list_changed notifications by: + * 1. Fetching the current list of prompts from the server 2. Distributing the updated + * list to all registered consumers 3. Handling any errors that occur during this + * process + */ private NotificationHandler asyncPromptsChangeNotificationHandler( List, Mono>> promptsChangeConsumers) { return params -> listPrompts().flatMap(listPromptsResult -> Flux.fromIterable(promptsChangeConsumers) @@ -752,12 +891,22 @@ private NotificationHandler asyncPromptsChangeNotificationHandler( // Logging // -------------------------- /** - * Create a notification handler for logging notifications from the server. This - * handler automatically distributes logging messages to all registered consumers. + * Creates a notification handler for logging messages from the server. This handler + * automatically parses incoming log messages and distributes them to all registered + * consumers based on the current logging level. + * + *

+ * The server can send log messages with different severity levels (DEBUG, INFO, WARN, + * ERROR), and the client can filter these messages using the + * {@link #setLoggingLevel(LoggingLevel)} method. * @param loggingConsumers List of consumers that will be notified when a logging - * message is received. Each consumer receives the logging message notification. - * @return A NotificationHandler that processes log notifications by distributing the - * message to all registered consumers + * message is received. Each consumer receives the complete logging message + * notification. + * @return A NotificationHandler that processes logging notifications by: 1. Parsing + * the incoming log message 2. Distributing the message to all registered consumers 3. + * Handling any errors that occur during this process + * @see McpSchema.LoggingMessageNotification + * @see #setLoggingLevel(LoggingLevel) */ private NotificationHandler asyncLoggingNotificationHandler( List>> loggingConsumers) { @@ -774,13 +923,26 @@ private NotificationHandler asyncLoggingNotificationHandler( } /** - * Client can set the minimum logging level it wants to receive from the server. - * @param loggingLevel the min logging level + * Sets the minimum logging level for messages received from the server. The client + * will only receive log messages at or above the specified severity level. + * + *

+ * This allows clients to control the verbosity of server logging, filtering out less + * important messages while still receiving critical information. + * @param loggingLevel The minimum logging level to receive, one of: + *

    + *
  • DEBUG: Detailed information for debugging purposes
  • + *
  • INFO: General information about normal operation
  • + *
  • WARN: Potential issues that don't prevent normal operation
  • + *
  • ERROR: Errors that prevent normal operation
  • + *
+ * @return A Mono that completes when the logging level is set + * @see McpSchema.LoggingLevel */ public Mono setLoggingLevel(LoggingLevel loggingLevel) { Assert.notNull(loggingLevel, "Logging level must not be null"); - return withInitializationCheck("Client must be initialized before setting logging level", initializedResult -> { + return this.withInitializationCheck("setting logging level", initializedResult -> { String levelName = this.transport.unmarshalFrom(loggingLevel, new TypeReference() { }); Map params = Map.of("level", levelName); From 1970cc6c0130378d541498b36ce6165d0618c07a Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Wed, 12 Mar 2025 11:51:47 +0100 Subject: [PATCH 3/6] Clean and purne javadoc Signed-off-by: Christian Tzolov --- .../client/McpAsyncClient.java | 218 ++++-------------- 1 file changed, 41 insertions(+), 177 deletions(-) diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index 19c88282..7cfb94ff 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -88,7 +88,7 @@ public class McpAsyncClient { /** * The max timeout to await for the client-server connection to be initialized. - * Usually x2 the request timeout. + * Usually x2 the request timeout. // TODO should we make it configurable? */ private final Duration initializationTimeout; @@ -298,23 +298,17 @@ public Mono closeGracefully() { * *
* The client MUST initiate this phase by sending an initialize request containing: - *
    - *
  • The protocol version the client supports
  • - *
  • The client's capabilities
  • - *
  • Client implementation information
  • - *
- * - * The server MUST respond with its own capabilities and information: - * {@link McpSchema.ServerCapabilities}.
+ * The protocol version the client supports, client's capabilities and clients + * implementation information. + *

+ * The server MUST respond with its own capabilities and information. + *

* After successful initialization, the client MUST send an initialized notification * to indicate it is ready to begin normal operations. - * - *
- * - * Initialization - * Spec * @return the initialize result. + * @see MCP + * Initialization Spec */ public Mono initialize() { @@ -386,8 +380,8 @@ public Mono ping() { // -------------------------- /** * Adds a new root to the client's root list. - * @param root The root to add - * @return A Mono that completes when the root is added and notifications are sent + * @param root The root to add. + * @return A Mono that completes when the root is added and notifications are sent. */ public Mono addRoot(Root root) { @@ -420,8 +414,8 @@ public Mono addRoot(Root root) { /** * Removes a root from the client's root list. - * @param rootUri The URI of the root to remove - * @return A Mono that completes when the root is removed and notifications are sent + * @param rootUri The URI of the root to remove. + * @return A Mono that completes when the root is removed and notifications are sent. */ public Mono removeRoot(String rootUri) { @@ -453,8 +447,9 @@ public Mono removeRoot(String rootUri) { /** * Manually sends a roots/list_changed notification. The addRoot and removeRoot - * methods automatically send the roots/list_changed notification. - * @return A Mono that completes when the notification is sent + * methods automatically send the roots/list_changed notification if the client is in + * an initialized state. + * @return A Mono that completes when the notification is sent. */ public Mono rootsListChangedNotification() { return this.withInitializationCheck("sending roots list changed notification", @@ -500,19 +495,9 @@ private RequestHandler samplingCreateMessageHandler() { * Calls a tool provided by the server. Tools enable servers to expose executable * functionality that can interact with external systems, perform computations, and * take actions in the real world. - * @param callToolRequest The request containing: - *
    - *
  • name: The name of the tool to call (must match a tool name from - * tools/list)
  • - *
  • arguments: Arguments that conform to the tool's input schema
  • - *
- * @return A Mono that emits the tool execution result containing: - *
    - *
  • content: List of content items (text, images, or embedded resources) - * representing the tool's output
  • - *
  • isError: Boolean indicating if the execution failed (true) or succeeded - * (false/absent)
  • - *
+ * @param callToolRequest The request containing the tool name and input parameters + * @return A Mono that emits the result of the tool call, including the output and any + * errors * @see McpSchema.CallToolRequest * @see McpSchema.CallToolResult * @see #listTools() @@ -528,9 +513,7 @@ public Mono callTool(McpSchema.CallToolRequest callToo /** * Retrieves the list of all tools provided by the server. - * @return A Mono that emits the list of tools result containing: - tools: List of - * available tools, each with a name, description, and input schema - nextCursor: - * Optional cursor for pagination if more tools are available + * @return A Mono that emits the list of tools result. */ public Mono listTools() { return this.listTools(null); @@ -539,9 +522,7 @@ public Mono listTools() { /** * Retrieves a paginated list of tools provided by the server. * @param cursor Optional pagination cursor from a previous list request - * @return A Mono that emits the list of tools result containing: - tools: List of - * available tools, each with a name, description, and input schema - nextCursor: - * Optional cursor for pagination if more tools are available + * @return A Mono that emits the list of tools result */ public Mono listTools(String cursor) { return this.withInitializationCheck("listing tools", initializedResult -> { @@ -553,18 +534,6 @@ public Mono listTools(String cursor) { }); } - /** - * Creates a notification handler for tools/list_changed notifications from the - * server. When the server's available tools change, it sends a notification to inform - * connected clients. This handler automatically fetches the updated tool list and - * distributes it to all registered consumers. - * @param toolsChangeConsumers List of consumers that will be notified when the tools - * list changes. Each consumer receives the complete updated list of tools. - * @return A NotificationHandler that processes tools/list_changed notifications by: - * 1. Fetching the current list of tools from the server 2. Distributing the updated - * list to all registered consumers 3. Handling any errors that occur during this - * process - */ private NotificationHandler asyncToolsChangeNotificationHandler( List, Mono>> toolsChangeConsumers) { // TODO: params are not used yet @@ -595,13 +564,7 @@ private NotificationHandler asyncToolsChangeNotificationHandler( * Retrieves the list of all resources provided by the server. Resources represent any * kind of UTF-8 encoded data that an MCP server makes available to clients, such as * database records, API responses, log files, and more. - * @return A Mono that completes with the list of resources result containing: - *
    - *
  • resources: List of available resources, each with a URI, name, and - * optional description
  • - *
  • nextCursor: Optional cursor for pagination if more resources are - * available
  • - *
+ * @return A Mono that completes with the list of resources result. * @see McpSchema.ListResourcesResult * @see #readResource(McpSchema.Resource) */ @@ -613,14 +576,8 @@ public Mono listResources() { * Retrieves a paginated list of resources provided by the server. Resources represent * any kind of UTF-8 encoded data that an MCP server makes available to clients, such * as database records, API responses, log files, and more. - * @param cursor Optional pagination cursor from a previous list request - * @return A Mono that completes with the list of resources result containing: - *
    - *
  • resources: List of available resources, each with a URI, name, and - * optional description
  • - *
  • nextCursor: Optional cursor for pagination if more resources are - * available
  • - *
+ * @param cursor Optional pagination cursor from a previous list request. + * @return A Mono that completes with the list of resources result. * @see McpSchema.ListResourcesResult * @see #readResource(McpSchema.Resource) */ @@ -638,12 +595,8 @@ public Mono listResources(String cursor) { * Reads the content of a specific resource identified by the provided Resource * object. This method fetches the actual data that the resource represents. * @param resource The resource to read, containing the URI that identifies the - * resource - * @return A Mono that completes with the resource content containing: - *
    - *
  • contents: List of content items, each with a URI, MIME type, and the - * actual text content
  • - *
+ * resource. + * @return A Mono that completes with the resource content. * @see McpSchema.Resource * @see McpSchema.ReadResourceResult */ @@ -655,11 +608,7 @@ public Mono readResource(McpSchema.Resource resour * Reads the content of a specific resource identified by the provided request. This * method fetches the actual data that the resource represents. * @param readResourceRequest The request containing the URI of the resource to read - * @return A Mono that completes with the resource content containing: - *
    - *
  • contents: List of content items, each with a URI, MIME type, and the - * actual text content
  • - *
+ * @return A Mono that completes with the resource content. * @see McpSchema.ReadResourceRequest * @see McpSchema.ReadResourceResult */ @@ -681,14 +630,7 @@ public Mono readResource(McpSchema.ReadResourceReq *

* For example, a template like "weather://{city}/forecast" allows clients to access * weather forecasts for different cities by substituting the {city} parameter. - * @return A Mono that completes with the list of resource templates result - * containing: - *

    - *
  • resourceTemplates: List of available resource templates, each with a URI - * template, name, and optional description
  • - *
  • nextCursor: Optional cursor for pagination if more templates are - * available
  • - *
+ * @return A Mono that completes with the list of resource templates result. * @see McpSchema.ListResourceTemplatesResult */ public Mono listResourceTemplates() { @@ -703,15 +645,8 @@ public Mono listResourceTemplates() { *

* For example, a template like "weather://{city}/forecast" allows clients to access * weather forecasts for different cities by substituting the {city} parameter. - * @param cursor Optional pagination cursor from a previous list request - * @return A Mono that completes with the list of resource templates result - * containing: - *

    - *
  • resourceTemplates: List of available resource templates, each with a URI - * template, name, and optional description
  • - *
  • nextCursor: Optional cursor for pagination if more templates are - * available
  • - *
+ * @param cursor Optional pagination cursor from a previous list request. + * @return A Mono that completes with the list of resource templates result. * @see McpSchema.ListResourceTemplatesResult */ public Mono listResourceTemplates(String cursor) { @@ -733,11 +668,8 @@ public Mono listResourceTemplates(String * Resource subscriptions enable real-time updates for dynamic resources that may * change over time, such as system status information, live data feeds, or * collaborative documents. - * @param subscribeRequest The request containing: - *
    - *
  • uri: The URI of the resource to subscribe to
  • - *
- * @return A Mono that completes when the subscription is complete + * @param subscribeRequest The subscribe request containing the URI of the resource. + * @return A Mono that completes when the subscription is complete. * @see McpSchema.SubscribeRequest * @see #unsubscribeResource(McpSchema.UnsubscribeRequest) */ @@ -754,11 +686,9 @@ public Mono subscribeResource(McpSchema.SubscribeRequest subscribeRequest) * This method should be called when the client is no longer interested in receiving * updates for a particular resource, to reduce unnecessary network traffic and * processing. - * @param unsubscribeRequest The request containing: - *
    - *
  • uri: The URI of the resource to unsubscribe from
  • - *
- * @return A Mono that completes when the unsubscription is complete + * @param unsubscribeRequest The unsubscribe request containing the URI of the + * resource. + * @return A Mono that completes when the unsubscription is complete. * @see McpSchema.UnsubscribeRequest * @see #subscribeResource(McpSchema.SubscribeRequest) */ @@ -767,19 +697,6 @@ public Mono unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRe .sendRequest(McpSchema.METHOD_RESOURCES_UNSUBSCRIBE, unsubscribeRequest, VOID_TYPE_REFERENCE)); } - /** - * Creates a notification handler for resources/list_changed notifications from the - * server. When the server's available resources change, it sends a notification to - * inform connected clients. This handler automatically fetches the updated resource - * list and distributes it to all registered consumers. - * @param resourcesChangeConsumers List of consumers that will be notified when the - * resources list changes. Each consumer receives the complete updated list of - * resources. - * @return A NotificationHandler that processes resources/list_changed notifications - * by: 1. Fetching the current list of resources from the server 2. Distributing the - * updated list to all registered consumers 3. Handling any errors that occur during - * this process - */ private NotificationHandler asyncResourcesChangeNotificationHandler( List, Mono>> resourcesChangeConsumers) { return params -> listResources().flatMap(listResourcesResult -> Flux.fromIterable(resourcesChangeConsumers) @@ -801,16 +718,8 @@ private NotificationHandler asyncResourcesChangeNotificationHandler( }; /** - * Retrieves the list of all prompts provided by the server. Prompts are templates - * that define structured interactions with language models, allowing servers to - * request specific types of AI-generated content. - * @return A Mono that completes with the list of prompts result containing: - *
    - *
  • prompts: List of available prompts, each with an ID, name, and - * description
  • - *
  • nextCursor: Optional cursor for pagination if more prompts are - * available
  • - *
+ * Retrieves the list of all prompts provided by the server. + * @return A Mono that completes with the list of prompts result. * @see McpSchema.ListPromptsResult * @see #getPrompt(GetPromptRequest) */ @@ -819,17 +728,9 @@ public Mono listPrompts() { } /** - * Retrieves a paginated list of prompts provided by the server. Prompts are templates - * that define structured interactions with language models, allowing servers to - * request specific types of AI-generated content. + * Retrieves a paginated list of prompts provided by the server. * @param cursor Optional pagination cursor from a previous list request - * @return A Mono that completes with the list of prompts result containing: - *
    - *
  • prompts: List of available prompts, each with an ID, name, and - * description
  • - *
  • nextCursor: Optional cursor for pagination if more prompts are - * available
  • - *
+ * @return A Mono that completes with the list of prompts result. * @see McpSchema.ListPromptsResult * @see #getPrompt(GetPromptRequest) */ @@ -846,15 +747,8 @@ public Mono listPrompts(String cursor) { * Prompts define structured interactions with language models, allowing servers to * request specific types of AI-generated content with consistent formatting and * behavior. - * @param getPromptRequest The request containing: - *
    - *
  • id: The unique identifier of the prompt to retrieve
  • - *
- * @return A Mono that completes with the prompt result containing: - *
    - *
  • prompt: The complete prompt details including ID, name, description, and - * template
  • - *
+ * @param getPromptRequest The request containing the ID of the prompt to retrieve + * @return A Mono that completes with the prompt result. * @see McpSchema.GetPromptRequest * @see McpSchema.GetPromptResult * @see #listPrompts() @@ -864,18 +758,6 @@ public Mono getPrompt(GetPromptRequest getPromptRequest) { .sendRequest(McpSchema.METHOD_PROMPT_GET, getPromptRequest, GET_PROMPT_RESULT_TYPE_REF)); } - /** - * Creates a notification handler for prompts/list_changed notifications from the - * server. When the server's available prompts change, it sends a notification to - * inform connected clients. This handler automatically fetches the updated prompt - * list and distributes it to all registered consumers. - * @param promptsChangeConsumers List of consumers that will be notified when the - * prompts list changes. Each consumer receives the complete updated list of prompts. - * @return A NotificationHandler that processes prompts/list_changed notifications by: - * 1. Fetching the current list of prompts from the server 2. Distributing the updated - * list to all registered consumers 3. Handling any errors that occur during this - * process - */ private NotificationHandler asyncPromptsChangeNotificationHandler( List, Mono>> promptsChangeConsumers) { return params -> listPrompts().flatMap(listPromptsResult -> Flux.fromIterable(promptsChangeConsumers) @@ -890,24 +772,6 @@ private NotificationHandler asyncPromptsChangeNotificationHandler( // -------------------------- // Logging // -------------------------- - /** - * Creates a notification handler for logging messages from the server. This handler - * automatically parses incoming log messages and distributes them to all registered - * consumers based on the current logging level. - * - *

- * The server can send log messages with different severity levels (DEBUG, INFO, WARN, - * ERROR), and the client can filter these messages using the - * {@link #setLoggingLevel(LoggingLevel)} method. - * @param loggingConsumers List of consumers that will be notified when a logging - * message is received. Each consumer receives the complete logging message - * notification. - * @return A NotificationHandler that processes logging notifications by: 1. Parsing - * the incoming log message 2. Distributing the message to all registered consumers 3. - * Handling any errors that occur during this process - * @see McpSchema.LoggingMessageNotification - * @see #setLoggingLevel(LoggingLevel) - */ private NotificationHandler asyncLoggingNotificationHandler( List>> loggingConsumers) { From 8f0b24bedfcd18cbb35df731210ebdef7b558935 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Wed, 12 Mar 2025 11:56:44 +0100 Subject: [PATCH 4/6] Another round of javadoc prunning Signed-off-by: Christian Tzolov --- .../client/McpAsyncClient.java | 43 +++---------------- 1 file changed, 5 insertions(+), 38 deletions(-) diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index 7cfb94ff..b301aa93 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -495,9 +495,9 @@ private RequestHandler samplingCreateMessageHandler() { * Calls a tool provided by the server. Tools enable servers to expose executable * functionality that can interact with external systems, perform computations, and * take actions in the real world. - * @param callToolRequest The request containing the tool name and input parameters + * @param callToolRequest The request containing the tool name and input parameters. * @return A Mono that emits the result of the tool call, including the output and any - * errors + * errors. * @see McpSchema.CallToolRequest * @see McpSchema.CallToolResult * @see #listTools() @@ -626,10 +626,6 @@ public Mono readResource(McpSchema.ReadResourceReq * Retrieves the list of all resource templates provided by the server. Resource * templates allow servers to expose parameterized resources using URI templates, * enabling dynamic resource access based on variable parameters. - * - *

- * For example, a template like "weather://{city}/forecast" allows clients to access - * weather forecasts for different cities by substituting the {city} parameter. * @return A Mono that completes with the list of resource templates result. * @see McpSchema.ListResourceTemplatesResult */ @@ -641,10 +637,6 @@ public Mono listResourceTemplates() { * Retrieves a paginated list of resource templates provided by the server. Resource * templates allow servers to expose parameterized resources using URI templates, * enabling dynamic resource access based on variable parameters. - * - *

- * For example, a template like "weather://{city}/forecast" allows clients to access - * weather forecasts for different cities by substituting the {city} parameter. * @param cursor Optional pagination cursor from a previous list request. * @return A Mono that completes with the list of resource templates result. * @see McpSchema.ListResourceTemplatesResult @@ -663,11 +655,6 @@ public Mono listResourceTemplates(String * Subscribes to changes in a specific resource. When the resource changes on the * server, the client will receive notifications through the resources change * notification handler. - * - *

- * Resource subscriptions enable real-time updates for dynamic resources that may - * change over time, such as system status information, live data feeds, or - * collaborative documents. * @param subscribeRequest The subscribe request containing the URI of the resource. * @return A Mono that completes when the subscription is complete. * @see McpSchema.SubscribeRequest @@ -681,11 +668,6 @@ public Mono subscribeResource(McpSchema.SubscribeRequest subscribeRequest) /** * Cancels an existing subscription to a resource. After unsubscribing, the client * will no longer receive notifications when the resource changes. - * - *

- * This method should be called when the client is no longer interested in receiving - * updates for a particular resource, to reduce unnecessary network traffic and - * processing. * @param unsubscribeRequest The unsubscribe request containing the URI of the * resource. * @return A Mono that completes when the unsubscription is complete. @@ -742,12 +724,7 @@ public Mono listPrompts(String cursor) { /** * Retrieves a specific prompt by its ID. This provides the complete prompt template * including all parameters and instructions for generating AI content. - * - *

- * Prompts define structured interactions with language models, allowing servers to - * request specific types of AI-generated content with consistent formatting and - * behavior. - * @param getPromptRequest The request containing the ID of the prompt to retrieve + * @param getPromptRequest The request containing the ID of the prompt to retrieve. * @return A Mono that completes with the prompt result. * @see McpSchema.GetPromptRequest * @see McpSchema.GetPromptResult @@ -789,18 +766,8 @@ private NotificationHandler asyncLoggingNotificationHandler( /** * Sets the minimum logging level for messages received from the server. The client * will only receive log messages at or above the specified severity level. - * - *

- * This allows clients to control the verbosity of server logging, filtering out less - * important messages while still receiving critical information. - * @param loggingLevel The minimum logging level to receive, one of: - *

    - *
  • DEBUG: Detailed information for debugging purposes
  • - *
  • INFO: General information about normal operation
  • - *
  • WARN: Potential issues that don't prevent normal operation
  • - *
  • ERROR: Errors that prevent normal operation
  • - *
- * @return A Mono that completes when the logging level is set + * @param loggingLevel The minimum logging level to receive. + * @return A Mono that completes when the logging level is set. * @see McpSchema.LoggingLevel */ public Mono setLoggingLevel(LoggingLevel loggingLevel) { From 9105c31a41867e7b53b608d23ea47551262f00df Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Thu, 13 Mar 2025 11:29:07 +0100 Subject: [PATCH 5/6] feat: Add tests to verify proper error handling for uninitialized clients Replace hardcoded timeout constants with configurable getTimeoutDuration() method Remove automatic initialization in setUp methods to allow explicit testing Signed-off-by: Christian Tzolov --- .../client/WebFluxSseMcpAsyncClientTests.java | 7 ++ .../client/WebFluxSseMcpSyncClientTests.java | 7 ++ .../client/AbstractMcpAsyncClientTests.java | 98 +++++++++++++++++-- .../client/AbstractMcpSyncClientTests.java | 85 ++++++++++++++-- .../client/AbstractMcpAsyncClientTests.java | 98 +++++++++++++++++-- .../client/AbstractMcpSyncClientTests.java | 85 ++++++++++++++-- .../client/ServletSseMcpAsyncClientTests.java | 7 ++ .../client/ServletSseMcpSyncClientTests.java | 7 ++ 8 files changed, 364 insertions(+), 30 deletions(-) diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpAsyncClientTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpAsyncClientTests.java index 021ce465..6cd74631 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpAsyncClientTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpAsyncClientTests.java @@ -4,6 +4,8 @@ package io.modelcontextprotocol.client; +import java.time.Duration; + import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; import io.modelcontextprotocol.spec.ClientMcpTransport; import org.junit.jupiter.api.Timeout; @@ -46,4 +48,9 @@ public void onClose() { container.stop(); } + @Override + protected Duration getTimeoutDuration() { + return Duration.ofMillis(300); + } + } diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpSyncClientTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpSyncClientTests.java index 20eeb1d5..6b980da4 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpSyncClientTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebFluxSseMcpSyncClientTests.java @@ -4,6 +4,8 @@ package io.modelcontextprotocol.client; +import java.time.Duration; + import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; import io.modelcontextprotocol.spec.ClientMcpTransport; import org.junit.jupiter.api.Timeout; @@ -46,4 +48,9 @@ protected void onClose() { container.stop(); } + @Override + protected Duration getTimeoutDuration() { + return Duration.ofMillis(300); + } + } diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index 092d5afe..50ebc744 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -10,6 +10,7 @@ import java.util.function.Function; import io.modelcontextprotocol.spec.ClientMcpTransport; +import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; @@ -47,7 +48,7 @@ public abstract class AbstractMcpAsyncClientTests { protected ClientMcpTransport mcpTransport; - private static final Duration TIMEOUT = Duration.ofSeconds(20); + // private static final Duration TIMEOUT = Duration.ofMillis(1000); private static final String ECHO_TEST_MESSAGE = "Hello MCP Spring AI!"; @@ -59,6 +60,10 @@ protected void onStart() { protected void onClose() { } + protected Duration getTimeoutDuration() { + return Duration.ofSeconds(2); + } + @BeforeEach void setUp() { onStart(); @@ -66,10 +71,9 @@ void setUp() { assertThatCode(() -> { mcpAsyncClient = McpClient.async(mcpTransport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .capabilities(ClientCapabilities.builder().roots(true).build()) .build(); - mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); }).doesNotThrowAnyException(); } @@ -92,8 +96,16 @@ void testConstructorWithInvalidArguments() { .hasMessage("Request timeout must not be null"); } + @Test + void testListToolsWithoutInitialization() { + assertThatThrownBy(() -> mcpAsyncClient.listTools(null).block()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before listing tools"); + } + @Test void testListTools() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + StepVerifier.create(mcpAsyncClient.listTools(null)).consumeNextWith(result -> { assertThat(result.tools()).isNotNull().isNotEmpty(); @@ -103,13 +115,30 @@ void testListTools() { }).verifyComplete(); } + @Test + void testPingWithoutInitialization() { + assertThatThrownBy(() -> mcpAsyncClient.ping().block()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before pinging the server"); + } + @Test void testPing() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); assertThatCode(() -> mcpAsyncClient.ping().block()).doesNotThrowAnyException(); } + @Test + void testCallToolWithoutInitialization() { + CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE)); + + assertThatThrownBy(() -> mcpAsyncClient.callTool(callToolRequest).block()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before calling tools"); + } + @Test void testCallTool() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE)); StepVerifier.create(mcpAsyncClient.callTool(callToolRequest)).consumeNextWith(callToolResult -> { @@ -122,13 +151,23 @@ void testCallTool() { @Test void testCallToolWithInvalidTool() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool", Map.of("message", ECHO_TEST_MESSAGE)); assertThatThrownBy(() -> mcpAsyncClient.callTool(invalidRequest).block()).isInstanceOf(Exception.class); } + @Test + void testListResourcesWithoutInitialization() { + assertThatThrownBy(() -> mcpAsyncClient.listResources(null).block()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before listing resources"); + } + @Test void testListResources() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + StepVerifier.create(mcpAsyncClient.listResources(null)).consumeNextWith(resources -> { assertThat(resources).isNotNull().satisfies(result -> { assertThat(result.resources()).isNotNull(); @@ -147,8 +186,16 @@ void testMcpAsyncClientState() { assertThat(mcpAsyncClient).isNotNull(); } + @Test + void testListPromptsWithoutInitialization() { + assertThatThrownBy(() -> mcpAsyncClient.listPrompts(null).block()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before listing prompts"); + } + @Test void testListPrompts() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + StepVerifier.create(mcpAsyncClient.listPrompts(null)).consumeNextWith(prompts -> { assertThat(prompts).isNotNull().satisfies(result -> { assertThat(result.prompts()).isNotNull(); @@ -162,8 +209,18 @@ void testListPrompts() { }).verifyComplete(); } + @Test + void testGetPromptWithoutInitialization() { + GetPromptRequest request = new GetPromptRequest("simple_prompt", Map.of()); + + assertThatThrownBy(() -> mcpAsyncClient.getPrompt(request).block()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before getting prompts"); + } + @Test void testGetPrompt() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + StepVerifier.create(mcpAsyncClient.getPrompt(new GetPromptRequest("simple_prompt", Map.of()))) .consumeNextWith(prompt -> { assertThat(prompt).isNotNull().satisfies(result -> { @@ -174,8 +231,16 @@ void testGetPrompt() { .verifyComplete(); } + @Test + void testRootsListChangedWithoutInitialization() { + assertThatThrownBy(() -> mcpAsyncClient.rootsListChangedNotification().block()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before sending roots list changed notification"); + } + @Test void testRootsListChanged() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + assertThatCode(() -> mcpAsyncClient.rootsListChangedNotification().block()).doesNotThrowAnyException(); } @@ -184,7 +249,7 @@ void testInitializeWithRootsListProviders() { var transport = createMcpTransport(); var client = McpClient.async(transport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .roots(new Root("file:///test/path", "test-root")) .build(); @@ -233,8 +298,16 @@ void testReadResource() { }).verifyComplete(); } + @Test + void testListResourceTemplatesWithoutInitialization() { + assertThatThrownBy(() -> mcpAsyncClient.listResourceTemplates().block()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before listing resource templates"); + } + @Test void testListResourceTemplates() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + StepVerifier.create(mcpAsyncClient.listResourceTemplates()).consumeNextWith(result -> { assertThat(result).isNotNull(); assertThat(result.resourceTemplates()).isNotNull(); @@ -266,7 +339,7 @@ void testNotificationHandlers() { var transport = createMcpTransport(); var client = McpClient.async(transport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .toolsChangeConsumer(tools -> Mono.fromRunnable(() -> toolsNotificationReceived.set(true))) .resourcesChangeConsumer(resources -> Mono.fromRunnable(() -> resourcesNotificationReceived.set(true))) .promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> promptsNotificationReceived.set(true))) @@ -285,7 +358,7 @@ void testInitializeWithSamplingCapability() { var capabilities = ClientCapabilities.builder().sampling().build(); var client = McpClient.async(transport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .capabilities(capabilities) .sampling(request -> Mono.just(CreateMessageResult.builder().message("test").model("test-model").build())) .build(); @@ -309,7 +382,7 @@ void testInitializeWithAllCapabilities() { Function> samplingHandler = request -> Mono .just(CreateMessageResult.builder().message("test").model("test-model").build()); var client = McpClient.async(transport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .capabilities(capabilities) .sampling(samplingHandler) .build(); @@ -326,8 +399,17 @@ void testInitializeWithAllCapabilities() { // Logging Tests // --------------------------------------- + @Test + void testLoggingLevelsWithoutInitialization() { + assertThatThrownBy(() -> mcpAsyncClient.setLoggingLevel(McpSchema.LoggingLevel.DEBUG).block()) + .isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before setting logging level"); + } + @Test void testLoggingLevels() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + // Test all logging levels for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) { StepVerifier.create(mcpAsyncClient.setLoggingLevel(level)).verifyComplete(); @@ -340,7 +422,7 @@ void testLoggingConsumer() { var transport = createMcpTransport(); var client = McpClient.async(transport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .loggingConsumer(notification -> Mono.fromRunnable(() -> logReceived.set(true))) .build(); diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java index 796dd5ac..aeed06cb 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java @@ -9,6 +9,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import io.modelcontextprotocol.spec.ClientMcpTransport; +import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; @@ -41,8 +42,6 @@ public abstract class AbstractMcpSyncClientTests { private McpSyncClient mcpSyncClient; - private static final Duration TIMEOUT = Duration.ofSeconds(10); - private static final String TEST_MESSAGE = "Hello MCP Spring AI!"; protected ClientMcpTransport mcpTransport; @@ -53,6 +52,10 @@ public abstract class AbstractMcpSyncClientTests { abstract protected void onClose(); + protected Duration getTimeoutDuration() { + return Duration.ofSeconds(2); + } + @BeforeEach void setUp() { onStart(); @@ -60,10 +63,9 @@ void setUp() { assertThatCode(() -> { mcpSyncClient = McpClient.sync(mcpTransport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .capabilities(ClientCapabilities.builder().roots(true).build()) .build(); - mcpSyncClient.initialize(); }).doesNotThrowAnyException(); } @@ -85,8 +87,15 @@ void testConstructorWithInvalidArguments() { .hasMessage("Request timeout must not be null"); } + @Test + void testListToolsWithoutInitialization() { + assertThatThrownBy(() -> mcpSyncClient.listTools(null)).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before listing tools"); + } + @Test void testListTools() { + mcpSyncClient.initialize(); ListToolsResult tools = mcpSyncClient.listTools(null); assertThat(tools).isNotNull().satisfies(result -> { @@ -98,8 +107,16 @@ void testListTools() { }); } + @Test + void testCallToolsWithoutInitialization() { + assertThatThrownBy(() -> mcpSyncClient.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4)))) + .isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before calling tools"); + } + @Test void testCallTools() { + mcpSyncClient.initialize(); CallToolResult toolResult = mcpSyncClient.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4))); assertThat(toolResult).isNotNull().satisfies(result -> { @@ -114,13 +131,29 @@ void testCallTools() { }); } + @Test + void testPingWithoutInitialization() { + assertThatThrownBy(() -> mcpSyncClient.ping()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before pinging the server"); + } + @Test void testPing() { + mcpSyncClient.initialize(); assertThatCode(() -> mcpSyncClient.ping()).doesNotThrowAnyException(); } + @Test + void testCallToolWithoutInitialization() { + CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE)); + + assertThatThrownBy(() -> mcpSyncClient.callTool(callToolRequest)).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before calling tools"); + } + @Test void testCallTool() { + mcpSyncClient.initialize(); CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE)); CallToolResult callToolResult = mcpSyncClient.callTool(callToolRequest); @@ -138,13 +171,27 @@ void testCallToolWithInvalidTool() { assertThatThrownBy(() -> mcpSyncClient.callTool(invalidRequest)).isInstanceOf(Exception.class); } + @Test + void testRootsListChangedWithoutInitialization() { + assertThatThrownBy(() -> mcpSyncClient.rootsListChangedNotification()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before sending roots list changed notification"); + } + @Test void testRootsListChanged() { + mcpSyncClient.initialize(); assertThatCode(() -> mcpSyncClient.rootsListChangedNotification()).doesNotThrowAnyException(); } + @Test + void testListResourcesWithoutInitialization() { + assertThatThrownBy(() -> mcpSyncClient.listResources(null)).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before listing resources"); + } + @Test void testListResources() { + mcpSyncClient.initialize(); ListResourcesResult resources = mcpSyncClient.listResources(null); assertThat(resources).isNotNull().satisfies(result -> { @@ -168,7 +215,7 @@ void testInitializeWithRootsListProviders() { var transport = createMcpTransport(); var client = McpClient.sync(transport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .roots(new Root("file:///test/path", "test-root")) .build(); @@ -204,8 +251,17 @@ void testRemoveNonExistentRoot() { .hasMessageContaining("Root with uri 'nonexistent-uri' not found"); } + @Test + void testReadResourceWithoutInitialization() { + assertThatThrownBy(() -> { + Resource resource = new Resource("test://uri", "Test Resource", null, null, null); + mcpSyncClient.readResource(resource); + }).isInstanceOf(McpError.class).hasMessage("Client must be initialized before reading resources"); + } + @Test void testReadResource() { + mcpSyncClient.initialize(); ListResourcesResult resources = mcpSyncClient.listResources(null); if (!resources.resources().isEmpty()) { @@ -217,8 +273,15 @@ void testReadResource() { } } + @Test + void testListResourceTemplatesWithoutInitialization() { + assertThatThrownBy(() -> mcpSyncClient.listResourceTemplates(null)).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before listing resource templates"); + } + @Test void testListResourceTemplates() { + mcpSyncClient.initialize(); ListResourceTemplatesResult result = mcpSyncClient.listResourceTemplates(null); assertThat(result).isNotNull(); @@ -250,7 +313,7 @@ void testNotificationHandlers() { var transport = createMcpTransport(); var client = McpClient.sync(transport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .toolsChangeConsumer(tools -> toolsNotificationReceived.set(true)) .resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true)) .promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true)) @@ -266,8 +329,16 @@ void testNotificationHandlers() { // Logging Tests // --------------------------------------- + @Test + void testLoggingLevelsWithoutInitialization() { + assertThatThrownBy(() -> mcpSyncClient.setLoggingLevel(McpSchema.LoggingLevel.DEBUG)) + .isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before setting logging level"); + } + @Test void testLoggingLevels() { + mcpSyncClient.initialize(); // Test all logging levels for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) { assertThatCode(() -> mcpSyncClient.setLoggingLevel(level)).doesNotThrowAnyException(); @@ -280,7 +351,7 @@ void testLoggingConsumer() { var transport = createMcpTransport(); var client = McpClient.sync(transport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .loggingConsumer(notification -> logReceived.set(true)) .build(); diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index d32b1910..ca4438fe 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -10,6 +10,7 @@ import java.util.function.Function; import io.modelcontextprotocol.spec.ClientMcpTransport; +import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; @@ -48,7 +49,7 @@ public abstract class AbstractMcpAsyncClientTests { protected ClientMcpTransport mcpTransport; - private static final Duration TIMEOUT = Duration.ofSeconds(20); + // private static final Duration TIMEOUT = Duration.ofMillis(1000); private static final String ECHO_TEST_MESSAGE = "Hello MCP Spring AI!"; @@ -60,6 +61,10 @@ protected void onStart() { protected void onClose() { } + protected Duration getTimeoutDuration() { + return Duration.ofSeconds(2); + } + @BeforeEach void setUp() { onStart(); @@ -67,10 +72,9 @@ void setUp() { assertThatCode(() -> { mcpAsyncClient = McpClient.async(mcpTransport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .capabilities(ClientCapabilities.builder().roots(true).build()) .build(); - mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); }).doesNotThrowAnyException(); } @@ -93,8 +97,16 @@ void testConstructorWithInvalidArguments() { .hasMessage("Request timeout must not be null"); } + @Test + void testListToolsWithoutInitialization() { + assertThatThrownBy(() -> mcpAsyncClient.listTools(null).block()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before listing tools"); + } + @Test void testListTools() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + StepVerifier.create(mcpAsyncClient.listTools(null)).consumeNextWith(result -> { assertThat(result.tools()).isNotNull().isNotEmpty(); @@ -104,13 +116,30 @@ void testListTools() { }).verifyComplete(); } + @Test + void testPingWithoutInitialization() { + assertThatThrownBy(() -> mcpAsyncClient.ping().block()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before pinging the server"); + } + @Test void testPing() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); assertThatCode(() -> mcpAsyncClient.ping().block()).doesNotThrowAnyException(); } + @Test + void testCallToolWithoutInitialization() { + CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE)); + + assertThatThrownBy(() -> mcpAsyncClient.callTool(callToolRequest).block()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before calling tools"); + } + @Test void testCallTool() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE)); StepVerifier.create(mcpAsyncClient.callTool(callToolRequest)).consumeNextWith(callToolResult -> { @@ -123,13 +152,23 @@ void testCallTool() { @Test void testCallToolWithInvalidTool() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool", Map.of("message", ECHO_TEST_MESSAGE)); assertThatThrownBy(() -> mcpAsyncClient.callTool(invalidRequest).block()).isInstanceOf(Exception.class); } + @Test + void testListResourcesWithoutInitialization() { + assertThatThrownBy(() -> mcpAsyncClient.listResources(null).block()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before listing resources"); + } + @Test void testListResources() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + StepVerifier.create(mcpAsyncClient.listResources(null)).consumeNextWith(resources -> { assertThat(resources).isNotNull().satisfies(result -> { assertThat(result.resources()).isNotNull(); @@ -148,8 +187,16 @@ void testMcpAsyncClientState() { assertThat(mcpAsyncClient).isNotNull(); } + @Test + void testListPromptsWithoutInitialization() { + assertThatThrownBy(() -> mcpAsyncClient.listPrompts(null).block()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before listing prompts"); + } + @Test void testListPrompts() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + StepVerifier.create(mcpAsyncClient.listPrompts(null)).consumeNextWith(prompts -> { assertThat(prompts).isNotNull().satisfies(result -> { assertThat(result.prompts()).isNotNull(); @@ -163,8 +210,18 @@ void testListPrompts() { }).verifyComplete(); } + @Test + void testGetPromptWithoutInitialization() { + GetPromptRequest request = new GetPromptRequest("simple_prompt", Map.of()); + + assertThatThrownBy(() -> mcpAsyncClient.getPrompt(request).block()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before getting prompts"); + } + @Test void testGetPrompt() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + StepVerifier.create(mcpAsyncClient.getPrompt(new GetPromptRequest("simple_prompt", Map.of()))) .consumeNextWith(prompt -> { assertThat(prompt).isNotNull().satisfies(result -> { @@ -175,8 +232,16 @@ void testGetPrompt() { .verifyComplete(); } + @Test + void testRootsListChangedWithoutInitialization() { + assertThatThrownBy(() -> mcpAsyncClient.rootsListChangedNotification().block()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before sending roots list changed notification"); + } + @Test void testRootsListChanged() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + assertThatCode(() -> mcpAsyncClient.rootsListChangedNotification().block()).doesNotThrowAnyException(); } @@ -185,7 +250,7 @@ void testInitializeWithRootsListProviders() { var transport = createMcpTransport(); var client = McpClient.async(transport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .roots(new Root("file:///test/path", "test-root")) .build(); @@ -234,8 +299,16 @@ void testReadResource() { }).verifyComplete(); } + @Test + void testListResourceTemplatesWithoutInitialization() { + assertThatThrownBy(() -> mcpAsyncClient.listResourceTemplates().block()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before listing resource templates"); + } + @Test void testListResourceTemplates() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + StepVerifier.create(mcpAsyncClient.listResourceTemplates()).consumeNextWith(result -> { assertThat(result).isNotNull(); assertThat(result.resourceTemplates()).isNotNull(); @@ -267,7 +340,7 @@ void testNotificationHandlers() { var transport = createMcpTransport(); var client = McpClient.async(transport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .toolsChangeConsumer(tools -> Mono.fromRunnable(() -> toolsNotificationReceived.set(true))) .resourcesChangeConsumer(resources -> Mono.fromRunnable(() -> resourcesNotificationReceived.set(true))) .promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> promptsNotificationReceived.set(true))) @@ -286,7 +359,7 @@ void testInitializeWithSamplingCapability() { var capabilities = ClientCapabilities.builder().sampling().build(); var client = McpClient.async(transport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .capabilities(capabilities) .sampling(request -> Mono.just(CreateMessageResult.builder().message("test").model("test-model").build())) .build(); @@ -310,7 +383,7 @@ void testInitializeWithAllCapabilities() { Function> samplingHandler = request -> Mono .just(CreateMessageResult.builder().message("test").model("test-model").build()); var client = McpClient.async(transport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .capabilities(capabilities) .sampling(samplingHandler) .build(); @@ -327,8 +400,17 @@ void testInitializeWithAllCapabilities() { // Logging Tests // --------------------------------------- + @Test + void testLoggingLevelsWithoutInitialization() { + assertThatThrownBy(() -> mcpAsyncClient.setLoggingLevel(McpSchema.LoggingLevel.DEBUG).block()) + .isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before setting logging level"); + } + @Test void testLoggingLevels() { + mcpAsyncClient.initialize().block(Duration.ofSeconds(10)); + // Test all logging levels for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) { StepVerifier.create(mcpAsyncClient.setLoggingLevel(level)).verifyComplete(); @@ -341,7 +423,7 @@ void testLoggingConsumer() { var transport = createMcpTransport(); var client = McpClient.async(transport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .loggingConsumer(notification -> Mono.fromRunnable(() -> logReceived.set(true))) .build(); diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java index cbad5419..6f8cf198 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java @@ -9,6 +9,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import io.modelcontextprotocol.spec.ClientMcpTransport; +import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; @@ -42,8 +43,6 @@ public abstract class AbstractMcpSyncClientTests { private McpSyncClient mcpSyncClient; - private static final Duration TIMEOUT = Duration.ofSeconds(10); - private static final String TEST_MESSAGE = "Hello MCP Spring AI!"; protected ClientMcpTransport mcpTransport; @@ -54,6 +53,10 @@ public abstract class AbstractMcpSyncClientTests { abstract protected void onClose(); + protected Duration getTimeoutDuration() { + return Duration.ofSeconds(2); + } + @BeforeEach void setUp() { onStart(); @@ -61,10 +64,9 @@ void setUp() { assertThatCode(() -> { mcpSyncClient = McpClient.sync(mcpTransport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .capabilities(ClientCapabilities.builder().roots(true).build()) .build(); - mcpSyncClient.initialize(); }).doesNotThrowAnyException(); } @@ -86,8 +88,15 @@ void testConstructorWithInvalidArguments() { .hasMessage("Request timeout must not be null"); } + @Test + void testListToolsWithoutInitialization() { + assertThatThrownBy(() -> mcpSyncClient.listTools(null)).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before listing tools"); + } + @Test void testListTools() { + mcpSyncClient.initialize(); ListToolsResult tools = mcpSyncClient.listTools(null); assertThat(tools).isNotNull().satisfies(result -> { @@ -99,8 +108,16 @@ void testListTools() { }); } + @Test + void testCallToolsWithoutInitialization() { + assertThatThrownBy(() -> mcpSyncClient.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4)))) + .isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before calling tools"); + } + @Test void testCallTools() { + mcpSyncClient.initialize(); CallToolResult toolResult = mcpSyncClient.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4))); assertThat(toolResult).isNotNull().satisfies(result -> { @@ -115,13 +132,29 @@ void testCallTools() { }); } + @Test + void testPingWithoutInitialization() { + assertThatThrownBy(() -> mcpSyncClient.ping()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before pinging the server"); + } + @Test void testPing() { + mcpSyncClient.initialize(); assertThatCode(() -> mcpSyncClient.ping()).doesNotThrowAnyException(); } + @Test + void testCallToolWithoutInitialization() { + CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE)); + + assertThatThrownBy(() -> mcpSyncClient.callTool(callToolRequest)).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before calling tools"); + } + @Test void testCallTool() { + mcpSyncClient.initialize(); CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE)); CallToolResult callToolResult = mcpSyncClient.callTool(callToolRequest); @@ -139,13 +172,27 @@ void testCallToolWithInvalidTool() { assertThatThrownBy(() -> mcpSyncClient.callTool(invalidRequest)).isInstanceOf(Exception.class); } + @Test + void testRootsListChangedWithoutInitialization() { + assertThatThrownBy(() -> mcpSyncClient.rootsListChangedNotification()).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before sending roots list changed notification"); + } + @Test void testRootsListChanged() { + mcpSyncClient.initialize(); assertThatCode(() -> mcpSyncClient.rootsListChangedNotification()).doesNotThrowAnyException(); } + @Test + void testListResourcesWithoutInitialization() { + assertThatThrownBy(() -> mcpSyncClient.listResources(null)).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before listing resources"); + } + @Test void testListResources() { + mcpSyncClient.initialize(); ListResourcesResult resources = mcpSyncClient.listResources(null); assertThat(resources).isNotNull().satisfies(result -> { @@ -169,7 +216,7 @@ void testInitializeWithRootsListProviders() { var transport = createMcpTransport(); var client = McpClient.sync(transport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .roots(new Root("file:///test/path", "test-root")) .build(); @@ -205,8 +252,17 @@ void testRemoveNonExistentRoot() { .hasMessageContaining("Root with uri 'nonexistent-uri' not found"); } + @Test + void testReadResourceWithoutInitialization() { + assertThatThrownBy(() -> { + Resource resource = new Resource("test://uri", "Test Resource", null, null, null); + mcpSyncClient.readResource(resource); + }).isInstanceOf(McpError.class).hasMessage("Client must be initialized before reading resources"); + } + @Test void testReadResource() { + mcpSyncClient.initialize(); ListResourcesResult resources = mcpSyncClient.listResources(null); if (!resources.resources().isEmpty()) { @@ -218,8 +274,15 @@ void testReadResource() { } } + @Test + void testListResourceTemplatesWithoutInitialization() { + assertThatThrownBy(() -> mcpSyncClient.listResourceTemplates(null)).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before listing resource templates"); + } + @Test void testListResourceTemplates() { + mcpSyncClient.initialize(); ListResourceTemplatesResult result = mcpSyncClient.listResourceTemplates(null); assertThat(result).isNotNull(); @@ -251,7 +314,7 @@ void testNotificationHandlers() { var transport = createMcpTransport(); var client = McpClient.sync(transport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .toolsChangeConsumer(tools -> toolsNotificationReceived.set(true)) .resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true)) .promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true)) @@ -267,8 +330,16 @@ void testNotificationHandlers() { // Logging Tests // --------------------------------------- + @Test + void testLoggingLevelsWithoutInitialization() { + assertThatThrownBy(() -> mcpSyncClient.setLoggingLevel(McpSchema.LoggingLevel.DEBUG)) + .isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before setting logging level"); + } + @Test void testLoggingLevels() { + mcpSyncClient.initialize(); // Test all logging levels for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) { assertThatCode(() -> mcpSyncClient.setLoggingLevel(level)).doesNotThrowAnyException(); @@ -281,7 +352,7 @@ void testLoggingConsumer() { var transport = createMcpTransport(); var client = McpClient.sync(transport) - .requestTimeout(TIMEOUT) + .requestTimeout(getTimeoutDuration()) .loggingConsumer(notification -> logReceived.set(true)) .build(); diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/ServletSseMcpAsyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/ServletSseMcpAsyncClientTests.java index 56ac8f50..7cc673fa 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/ServletSseMcpAsyncClientTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/ServletSseMcpAsyncClientTests.java @@ -4,6 +4,8 @@ package io.modelcontextprotocol.client; +import java.time.Duration; + import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.spec.ClientMcpTransport; import org.junit.jupiter.api.Timeout; @@ -44,4 +46,9 @@ protected void onClose() { container.stop(); } + @Override + protected Duration getTimeoutDuration() { + return Duration.ofMillis(300); + } + } diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/ServletSseMcpSyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/ServletSseMcpSyncClientTests.java index d76d0bfe..2b8af41a 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/ServletSseMcpSyncClientTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/ServletSseMcpSyncClientTests.java @@ -4,6 +4,8 @@ package io.modelcontextprotocol.client; +import java.time.Duration; + import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.spec.ClientMcpTransport; import org.junit.jupiter.api.Timeout; @@ -44,4 +46,9 @@ protected void onClose() { container.stop(); } + @Override + protected Duration getTimeoutDuration() { + return Duration.ofMillis(300); + } + } From 6345bf4939ce95d54d307a7585024bb028c3a2a7 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Thu, 13 Mar 2025 11:32:48 +0100 Subject: [PATCH 6/6] minor code clening Signed-off-by: Christian Tzolov --- .../client/AbstractMcpAsyncClientTests.java | 2 -- .../client/AbstractMcpAsyncClientTests.java | 2 -- 2 files changed, 4 deletions(-) diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index 50ebc744..cdcba4d1 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -48,8 +48,6 @@ public abstract class AbstractMcpAsyncClientTests { protected ClientMcpTransport mcpTransport; - // private static final Duration TIMEOUT = Duration.ofMillis(1000); - private static final String ECHO_TEST_MESSAGE = "Hello MCP Spring AI!"; abstract protected ClientMcpTransport createMcpTransport(); diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index ca4438fe..661c629e 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -49,8 +49,6 @@ public abstract class AbstractMcpAsyncClientTests { protected ClientMcpTransport mcpTransport; - // private static final Duration TIMEOUT = Duration.ofMillis(1000); - private static final String ECHO_TEST_MESSAGE = "Hello MCP Spring AI!"; abstract protected ClientMcpTransport createMcpTransport();