From 586f132f466b96ec625a0906658bebc2eb5aea94 Mon Sep 17 00:00:00 2001 From: Mark Pollack Date: Tue, 10 Dec 2024 23:49:24 -0500 Subject: [PATCH] Update copyright headers and add javaformat plugin --- pom.xml | 14 ++ .../docs/spring-ai-mcp-uml-classdiagram.svg | 4 +- spring-ai-mcp-core/pom.xml | 2 +- .../ai/mcp/client/McpAsyncClient.java | 79 +++++---- .../ai/mcp/client/McpClient.java | 143 +++++++-------- .../ai/mcp/client/McpSyncClient.java | 59 ++++--- .../ai/mcp/client/stdio/ServerParameters.java | 45 +++-- .../client/stdio/ServerParametersParser.java | 32 ++-- .../client/stdio/StdioServerTransport.java | 162 ++++++++++------- .../ai/mcp/client/util/Assert.java | 81 ++++----- .../ai/mcp/spec/AbstractMcpTransport.java | 44 +++-- .../ai/mcp/spec/DefaultMcpSession.java | 56 ++++-- .../ai/mcp/spec/McpSchema.java | 120 ++++++------- .../ai/mcp/spec/McpSession.java | 110 ++++++------ .../ai/mcp/spec/McpTransport.java | 163 ++++++++---------- .../ai/mcp/attic/ClientSessionTests2.java | 38 ++-- .../ai/mcp/attic/ClientSessionTests3.java | 38 ++-- .../ai/mcp/client/McpAsyncClientTests.java | 42 ++--- .../ai/mcp/client/McpSyncClientTests.java | 8 +- .../stdio/StdioServerTransportTests.java | 6 +- .../ai/mcp/spec/DefaultMcpTransportTests.java | 69 +++----- spring-ai-mcp-spring/pom.xml | 2 +- .../ai/mcp/spring/McpFunctionCallback.java | 29 ++-- 23 files changed, 706 insertions(+), 640 deletions(-) diff --git a/pom.xml b/pom.xml index 05236a7..050fbb1 100644 --- a/pom.xml +++ b/pom.xml @@ -97,6 +97,20 @@ + + io.spring.javaformat + spring-javaformat-maven-plugin + ${spring-javaformat-maven-plugin.version} + + + validate + true + + validate + + + + org.apache.maven.plugins maven-site-plugin diff --git a/spring-ai-mcp-core/docs/spring-ai-mcp-uml-classdiagram.svg b/spring-ai-mcp-core/docs/spring-ai-mcp-uml-classdiagram.svg index 5ff2925..a46137e 100644 --- a/spring-ai-mcp-core/docs/spring-ai-mcp-uml-classdiagram.svg +++ b/spring-ai-mcp-core/docs/spring-ai-mcp-uml-classdiagram.svg @@ -1 +1,3 @@ -McpTransportvoid start()void close()Mono<Void> closeGracefully()void setInboudMessageHandler()void setInboundErrorHandler()Mono<Void> sendMessage()AbstractMcpTransportObjectMapper objectMappergetInboundSink()getOutboundSink()getErrorSink()McpSession<T> Mono<T> sendRequest()Mono<Void> sendNotification()Mono<Void> closeGracefully()DefaultMcpSessionMcpTransport transportObjectMapper objectMapperMcpClientasync()sync()McpAsyncClientMono<InitializeResult> initialize()Mono<CallToolResult> callTool()Mono<ListToolsResult> listTools()Mono<ListResourcesResult> listResources()Mono<ReadResourceResult> readResource()Mono<ListPromptsResult> listPrompts()Mono<GetPromptResult> getPrompt()McpSyncClientInitializeResult initialize()CallToolResult callTool()ListToolsResult listTools()ListResourcesResult listResources()ReadResourceResult readResource()ListPromptsResult listPrompts()GetPromptResult getPrompt()StdioServerTransportProcess serverProcessServerParameters parametersMcpSchemaclass ErrorCodesinterface Requestinterface JSONRPCMessageinterface ResourceContentsinterface ContentMcpErrorint codeString messagecreatescreatesusesthrows \ No newline at end of file +McpTransportvoid start()void close()Mono<Void> closeGracefully()void setInboudMessageHandler()void setInboundErrorHandler()Mono<Void> sendMessage()AbstractMcpTransportObjectMapper objectMappergetInboundSink()getOutboundSink()getErrorSink()McpSession<T> Mono<T> sendRequest()Mono<Void> sendNotification()Mono<Void> closeGracefully()DefaultMcpSessionMcpTransport transportObjectMapper objectMapperMcpClientasync()sync()McpAsyncClientMono<InitializeResult> initialize()Mono<CallToolResult> callTool()Mono<ListToolsResult> listTools()Mono<ListResourcesResult> listResources()Mono<ReadResourceResult> readResource()Mono<ListPromptsResult> listPrompts()Mono<GetPromptResult> getPrompt()McpSyncClientInitializeResult initialize()CallToolResult callTool()ListToolsResult listTools()ListResourcesResult listResources()ReadResourceResult readResource()ListPromptsResult listPrompts()GetPromptResult getPrompt()StdioServerTransportProcess serverProcessServerParameters parametersMcpSchemaclass ErrorCodesinterface Requestinterface JSONRPCMessageinterface ResourceContentsinterface ContentMcpErrorint codeString messagecreatescreatesusesthrows \ No newline at end of file diff --git a/spring-ai-mcp-core/pom.xml b/spring-ai-mcp-core/pom.xml index eb7692b..4c3c808 100644 --- a/spring-ai-mcp-core/pom.xml +++ b/spring-ai-mcp-core/pom.xml @@ -6,7 +6,7 @@ org.springframework.experimental spring-ai-mcp - 0.0.1-SNAPSHOT + 0.1.0-SNAPSHOT spring-ai-mcp-core jar diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpAsyncClient.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpAsyncClient.java index 14328cc..7c6c896 100644 --- a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpAsyncClient.java +++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpAsyncClient.java @@ -1,3 +1,18 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.ai.mcp.client; import java.time.Duration; @@ -5,14 +20,15 @@ import java.time.Duration; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import reactor.core.publisher.Mono; + import org.springframework.ai.mcp.spec.DefaultMcpSession; -import org.springframework.ai.mcp.spec.McpTransport; import org.springframework.ai.mcp.spec.McpError; import org.springframework.ai.mcp.spec.McpSchema; import org.springframework.ai.mcp.spec.McpSchema.GetPromptRequest; import org.springframework.ai.mcp.spec.McpSchema.GetPromptResult; import org.springframework.ai.mcp.spec.McpSchema.ListPromptsResult; import org.springframework.ai.mcp.spec.McpSchema.PaginatedRequest; +import org.springframework.ai.mcp.spec.McpTransport; /** * @author Dariusz Jędrzejczyk @@ -32,8 +48,7 @@ public class McpAsyncClient extends DefaultMcpSession { } /** - * The initialization phase MUST be the first interaction between client and - * server. + * The initialization phase MUST be the first interaction between client and server. * During this phase, the client and server: *
    *
  • Establish protocol version compatibility
  • @@ -41,8 +56,7 @@ public class McpAsyncClient extends DefaultMcpSession { *
  • Share implementation details
  • *
*
- * The client MUST initiate this phase by sending an initialize request - * containing: + * The client MUST initiate this phase by sending an initialize request containing: *
    *
  • The protocol version the client supports
  • *
  • The client's capabilities
  • @@ -51,8 +65,7 @@ public class McpAsyncClient extends DefaultMcpSession { * * The server MUST respond with its own capabilities and information: * {@link McpSchema.ServerCapabilities}.
    - * After successful initialization, the client MUST send an initialized - * notification + * After successful initialization, the client MUST send an initialized notification * to indicate it is ready to begin normal operations. * *
    @@ -60,7 +73,6 @@ public class McpAsyncClient extends DefaultMcpSession { * Initialization * Spec - * * @return the initialize result. */ public Mono initialize() { @@ -75,12 +87,11 @@ public class McpAsyncClient extends DefaultMcpSession { return result.flatMap(initializeResult -> { if (!McpSchema.LATEST_PROTOCOL_VERSION.equals(initializeResult.protocolVersion())) { - return Mono.error( - new McpError("Unsupported protocol version from the server: " - + initializeResult.protocolVersion())); - } else { - return this.sendNotification("notifications/initialized", null) - .thenReturn(initializeResult); + return Mono.error(new McpError( + "Unsupported protocol version from the server: " + initializeResult.protocolVersion())); + } + else { + return this.sendNotification("notifications/initialized", null).thenReturn(initializeResult); } }); } @@ -104,12 +115,12 @@ public class McpAsyncClient extends DefaultMcpSession { // -------------------------- private static TypeReference CALL_TOOL_RESULT_TYPE_REF = new TypeReference() { }; + private static TypeReference LIST_TOOLS_RESULT_TYPE_REF = new TypeReference() { }; /** * Send a tools/call request. - * * @param callToolRequest the call tool request. * @return the call tool result. */ @@ -119,7 +130,6 @@ public class McpAsyncClient extends DefaultMcpSession { /** * Send a tools/list request. - * * @return the list of tools result. */ public Mono listTools(String cursor) { @@ -132,14 +142,15 @@ public class McpAsyncClient extends DefaultMcpSession { private static TypeReference LIST_RESOURCES_RESULT_TYPE_REF = new TypeReference() { }; + private static TypeReference READ_RESOURCE_RESULT_TYPE_REF = new TypeReference() { }; + private static TypeReference LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF = new TypeReference() { }; /** * Send a resources/list request. - * * @param cursor the cursor * @return the list of resources result. */ @@ -150,7 +161,6 @@ public class McpAsyncClient extends DefaultMcpSession { /** * Send a resources/read request. - * * @param resource the resource to read * @return the resource content. */ @@ -160,7 +170,6 @@ public class McpAsyncClient extends DefaultMcpSession { /** * Send a resources/read request. - * * @param readResourceRequest the read resource request. * @return the resource content. */ @@ -173,7 +182,6 @@ public class McpAsyncClient extends DefaultMcpSession { * templates. Arguments may be auto-completed through the completion API. * * Request a list of resource templates the server has. - * * @param cursor the cursor * @return the list of resource templates result. */ @@ -183,23 +191,21 @@ public class McpAsyncClient extends DefaultMcpSession { } /** - * List Changed Notification. When the list of available resources changes, - * servers that declared the listChanged capability SHOULD send a notification: + * List Changed Notification. When the list of available resources changes, servers + * that declared the listChanged capability SHOULD send a notification: */ public Mono sendResourcesListChanged() { return this.sendNotification("notifications/resources/list_changed"); } /** - * Subscriptions. The protocol supports optional subscriptions to resource - * changes. - * Clients can subscribe to specific resources and receive notifications when - * they change. + * Subscriptions. The protocol supports optional subscriptions to resource changes. + * Clients can subscribe to specific resources and receive notifications when they + * change. * * Send a resources/subscribe request. - * - * @param subscribeRequest the subscribe request contains the uri of the - * resource to subscribe to. + * @param subscribeRequest the subscribe request contains the uri of the resource to + * subscribe to. */ public Mono subscribeResource(McpSchema.SubscribeRequest subscribeRequest) { return this.sendRequest("resources/subscribe", subscribeRequest, VOID_TYPE_REFERENCE); @@ -207,9 +213,8 @@ public class McpAsyncClient extends DefaultMcpSession { /** * Send a resources/unsubscribe request. - * - * @param unsubscribeRequest the unsubscribe request contains the uri of the - * resource to unsubscribe from. + * @param unsubscribeRequest the unsubscribe request contains the uri of the resource + * to unsubscribe from. */ public Mono unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) { return this.sendRequest("resources/unsubscribe", unsubscribeRequest, VOID_TYPE_REFERENCE); @@ -220,12 +225,12 @@ public class McpAsyncClient extends DefaultMcpSession { // -------------------------- private static TypeReference LIST_PROMPTS_RESULT_TYPE_REF = new TypeReference() { }; + private static TypeReference GET_PROMPT_RESULT_TYPE_REF = new TypeReference() { }; public Mono listPrompts(String cursor) { - return this - .sendRequest("prompts/list", new PaginatedRequest(cursor), LIST_PROMPTS_RESULT_TYPE_REF); + return this.sendRequest("prompts/list", new PaginatedRequest(cursor), LIST_PROMPTS_RESULT_TYPE_REF); } public Mono getPrompt(GetPromptRequest getPromptRequest) { @@ -233,10 +238,8 @@ public class McpAsyncClient extends DefaultMcpSession { } /** - * (Server) An optional notification from the server to the client, informing it - * that - * the list of prompts it offers has changed. This may be issued by servers - * without + * (Server) An optional notification from the server to the client, informing it that + * the list of prompts it offers has changed. This may be issued by servers without * any previous subscription from the client. */ public Mono promptListChangedNotification() { diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpClient.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpClient.java index a67bc5d..6f12f96 100644 --- a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpClient.java +++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpClient.java @@ -1,11 +1,11 @@ /* - * Copyright 2024 - 2024 the original author or authors. + * Copyright 2024-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,93 +13,96 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.ai.mcp.client; import java.time.Duration; import com.fasterxml.jackson.databind.ObjectMapper; + import org.springframework.ai.mcp.spec.McpTransport; /** - * Factory class providing static methods for creating Model Context Protocol (MCP) clients. - * This class serves as the main entry point for establishing connections with MCP servers, - * offering both synchronous and asynchronous client implementations. - * - *

    The class provides factory methods to create either: + * Factory class providing static methods for creating Model Context Protocol (MCP) + * clients. This class serves as the main entry point for establishing connections with + * MCP servers, offering both synchronous and asynchronous client implementations. + * + *

    + * The class provides factory methods to create either: *

      - *
    • {@link McpAsyncClient} for non-blocking operations - *
    • {@link McpSyncClient} for blocking operations + *
    • {@link McpAsyncClient} for non-blocking operations + *
    • {@link McpSyncClient} for blocking operations *
    - * - *

    Each client type can be instantiated with default settings or with custom configuration + * + *

    + * Each client type can be instantiated with default settings or with custom configuration * including request timeout and JSON object mapping. - * - *

    Future implementations will introduce a builder pattern for more flexible client configuration. - * + * + *

    + * Future implementations will introduce a builder pattern for more flexible client + * configuration. + * * @author Christian Tzolov * @author Dariusz Jędrzejczyk */ public class McpClient { - /** - * Private constructor to prevent instantiation as this is a utility class - * containing only static factory methods. - */ - private McpClient() { - } + /** + * Private constructor to prevent instantiation as this is a utility class containing + * only static factory methods. + */ + private McpClient() { + } - // TODO: introduce a builder like: - // McpClient.using(transport) - // .withRequestTimeout(Duration.ofSeconds(5)) - // .withObjectMapper(objectMapper); <- or even a more sophisticated - // JSONtoPOJOCodec type - // .sync(); + // TODO: introduce a builder like: + // McpClient.using(transport) + // .withRequestTimeout(Duration.ofSeconds(5)) + // .withObjectMapper(objectMapper); <- or even a more sophisticated + // JSONtoPOJOCodec type + // .sync(); - /** - * Creates an asynchronous MCP client with default configuration. - * - * @param transport The transport layer implementation for MCP communication - * @return A new instance of {@link McpAsyncClient} - */ - public static McpAsyncClient async(McpTransport transport) { - return new McpAsyncClient(transport); - } + /** + * Creates an asynchronous MCP client with default configuration. + * @param transport The transport layer implementation for MCP communication + * @return A new instance of {@link McpAsyncClient} + */ + public static McpAsyncClient async(McpTransport transport) { + return new McpAsyncClient(transport); + } - /** - * Creates an asynchronous MCP client with custom configuration. - * - * @param transport The transport layer implementation for MCP communication - * @param requestTimeout The duration to wait before timing out requests - * @param objectMapper The Jackson object mapper for JSON serialization/deserialization - * @return A new instance of {@link McpAsyncClient} - */ - public static McpAsyncClient async(McpTransport transport, Duration requestTimeout, - ObjectMapper objectMapper) { - return new McpAsyncClient(transport, requestTimeout, objectMapper); - } + /** + * Creates an asynchronous MCP client with custom configuration. + * @param transport The transport layer implementation for MCP communication + * @param requestTimeout The duration to wait before timing out requests + * @param objectMapper The Jackson object mapper for JSON + * serialization/deserialization + * @return A new instance of {@link McpAsyncClient} + */ + public static McpAsyncClient async(McpTransport transport, Duration requestTimeout, ObjectMapper objectMapper) { + return new McpAsyncClient(transport, requestTimeout, objectMapper); + } - /** - * Creates a synchronous MCP client with default configuration. - * This method wraps an asynchronous client to provide synchronous operations. - * - * @param transport The transport layer implementation for MCP communication - * @return A new instance of {@link McpSyncClient} - */ - public static McpSyncClient sync(McpTransport transport) { - return new McpSyncClient(async(transport)); - } + /** + * Creates a synchronous MCP client with default configuration. This method wraps an + * asynchronous client to provide synchronous operations. + * @param transport The transport layer implementation for MCP communication + * @return A new instance of {@link McpSyncClient} + */ + public static McpSyncClient sync(McpTransport transport) { + return new McpSyncClient(async(transport)); + } + + /** + * Creates a synchronous MCP client with custom configuration. This method wraps an + * asynchronous client to provide synchronous operations. + * @param transport The transport layer implementation for MCP communication + * @param requestTimeout The duration to wait before timing out requests + * @param objectMapper The Jackson object mapper for JSON + * serialization/deserialization + * @return A new instance of {@link McpSyncClient} + */ + public static McpSyncClient sync(McpTransport transport, Duration requestTimeout, ObjectMapper objectMapper) { + return new McpSyncClient(async(transport, requestTimeout, objectMapper)); + } - /** - * Creates a synchronous MCP client with custom configuration. - * This method wraps an asynchronous client to provide synchronous operations. - * - * @param transport The transport layer implementation for MCP communication - * @param requestTimeout The duration to wait before timing out requests - * @param objectMapper The Jackson object mapper for JSON serialization/deserialization - * @return A new instance of {@link McpSyncClient} - */ - public static McpSyncClient sync(McpTransport transport, Duration requestTimeout, - ObjectMapper objectMapper) { - return new McpSyncClient(async(transport, requestTimeout, objectMapper)); - } } diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpSyncClient.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpSyncClient.java index d96c5ce..596eda9 100644 --- a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpSyncClient.java +++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpSyncClient.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.ai.mcp.client; import java.time.Duration; @@ -32,8 +48,7 @@ public class McpSyncClient implements AutoCloseable { } /** - * The initialization phase MUST be the first interaction between client and - * server. + * The initialization phase MUST be the first interaction between client and server. * During this phase, the client and server: *

      *
    • Establish protocol version compatibility
    • @@ -41,8 +56,7 @@ public class McpSyncClient implements AutoCloseable { *
    • Share implementation details
    • *
    *
    - * The client MUST initiate this phase by sending an initialize request - * containing: + * The client MUST initiate this phase by sending an initialize request containing: *
      *
    • The protocol version the client supports
    • *
    • The client's capabilities
    • @@ -51,8 +65,7 @@ public class McpSyncClient implements AutoCloseable { * * The server MUST respond with its own capabilities and information: * {@link McpSchema.ServerCapabilities}.
      - * After successful initialization, the client MUST send an initialized - * notification + * After successful initialization, the client MUST send an initialized notification * to indicate it is ready to begin normal operations. * *
      @@ -60,7 +73,6 @@ public class McpSyncClient implements AutoCloseable { * Initialization * Spec - * * @return the initialize result. */ public McpSchema.InitializeResult initialize() { @@ -88,7 +100,6 @@ public class McpSyncClient implements AutoCloseable { // -------------------------- /** * Send a tools/call request. - * * @param callToolRequest the call tool request. * @return the call tool result. */ @@ -98,7 +109,6 @@ public class McpSyncClient implements AutoCloseable { /** * Send a tools/list request. - * * @return the list of tools result. */ public McpSchema.ListToolsResult listTools(String cursor) { @@ -111,7 +121,6 @@ public class McpSyncClient implements AutoCloseable { /** * Send a resources/list request. - * * @param cursor the cursor * @return the list of resources result. */ @@ -121,7 +130,6 @@ public class McpSyncClient implements AutoCloseable { /** * Send a resources/read request. - * * @param resource the resource to read * @return the resource content. */ @@ -131,7 +139,6 @@ public class McpSyncClient implements AutoCloseable { /** * Send a resources/read request. - * * @param readResourceRequest the read resource request. * @return the resource content. */ @@ -144,7 +151,6 @@ public class McpSyncClient implements AutoCloseable { * templates. Arguments may be auto-completed through the completion API. * * Request a list of resource templates the server has. - * * @param cursor the cursor * @return the list of resource templates result. */ @@ -153,8 +159,7 @@ public class McpSyncClient implements AutoCloseable { } /** - * List Changed Notification. When the list of available resources changes, - * servers + * List Changed Notification. When the list of available resources changes, servers * that declared the listChanged capability SHOULD send a notification: */ public void sendResourcesListChanged() { @@ -162,14 +167,13 @@ public class McpSyncClient implements AutoCloseable { } /** - * Subscriptions. The protocol supports optional subscriptions to resource - * changes. Clients can subscribe to specific resources and receive - * notifications when they change. + * Subscriptions. The protocol supports optional subscriptions to resource changes. + * Clients can subscribe to specific resources and receive notifications when they + * change. * * Send a resources/subscribe request. - * - * @param subscribeRequest the subscribe request contains the uri of the - * resource to subscribe to. + * @param subscribeRequest the subscribe request contains the uri of the resource to + * subscribe to. */ public void subscribeResource(McpSchema.SubscribeRequest subscribeRequest) { this.delegate.subscribeResource(subscribeRequest).block(); @@ -177,10 +181,8 @@ public class McpSyncClient implements AutoCloseable { /** * Send a resources/unsubscribe request. - * - * @param unsubscribeRequest the unsubscribe request contains the uri of the - * resource - * to unsubscribe from. + * @param unsubscribeRequest the unsubscribe request contains the uri of the resource + * to unsubscribe from. */ public void unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) { this.delegate.unsubscribeResource(unsubscribeRequest).block(); @@ -198,11 +200,12 @@ public class McpSyncClient implements AutoCloseable { } /** - * (Server) An optional notification from the server to the client, informing it - * that the list of prompts it offers has changed. This may be issued by servers - * without any previous subscription from the client. + * (Server) An optional notification from the server to the client, informing it that + * the list of prompts it offers has changed. This may be issued by servers without + * any previous subscription from the client. */ public void promptListChangedNotification() { this.delegate.promptListChangedNotification().block(); } + } diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/stdio/ServerParameters.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/stdio/ServerParameters.java index cf2c8bb..8c01c86 100644 --- a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/stdio/ServerParameters.java +++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/stdio/ServerParameters.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.ai.mcp.client.stdio; import java.util.ArrayList; @@ -9,11 +25,12 @@ import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; + import org.springframework.ai.mcp.client.util.Assert; /** * Server parameters for stdio client. - * + * * @author Christian Tzolov * @author Dariusz Jędrzejczyk */ @@ -22,11 +39,11 @@ public class ServerParameters { // Environment variables to inherit by default private static final List DEFAULT_INHERITED_ENV_VARS = System.getProperty("os.name") - .toLowerCase() - .contains("win") - ? Arrays.asList("APPDATA", "HOMEDRIVE", "HOMEPATH", "LOCALAPPDATA", "PATH", - "PROCESSOR_ARCHITECTURE", "SYSTEMDRIVE", "SYSTEMROOT", "TEMP", "USERNAME", "USERPROFILE") - : Arrays.asList("HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER"); + .toLowerCase() + .contains("win") + ? Arrays.asList("APPDATA", "HOMEDRIVE", "HOMEPATH", "LOCALAPPDATA", "PATH", "PROCESSOR_ARCHITECTURE", + "SYSTEMDRIVE", "SYSTEMROOT", "TEMP", "USERNAME", "USERPROFILE") + : Arrays.asList("HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER"); @JsonProperty("command") private String command; @@ -117,17 +134,17 @@ public class ServerParameters { } /** - * Returns a default environment object including only environment variables - * deemed safe to inherit. + * Returns a default environment object including only environment variables deemed + * safe to inherit. */ private static Map getDefaultEnvironment() { return System.getenv() - .entrySet() - .stream() - .filter(entry -> DEFAULT_INHERITED_ENV_VARS.contains(entry.getKey())) - .filter(entry -> entry.getValue() != null) - .filter(entry -> !entry.getValue().startsWith("()")) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + .entrySet() + .stream() + .filter(entry -> DEFAULT_INHERITED_ENV_VARS.contains(entry.getKey())) + .filter(entry -> entry.getValue() != null) + .filter(entry -> !entry.getValue().startsWith("()")) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } } \ No newline at end of file diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/stdio/ServerParametersParser.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/stdio/ServerParametersParser.java index b625b72..723dde2 100644 --- a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/stdio/ServerParametersParser.java +++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/stdio/ServerParametersParser.java @@ -1,26 +1,28 @@ /* -* Copyright 2024 - 2024 the original author or authors. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* https://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.ai.mcp.client.stdio; -import java.util.Map; import java.util.List; +import java.util.Map; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; + import org.springframework.ai.mcp.client.stdio.ServerParametersParser.McpServerConfigurations.McpServerConfiguration; /** diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/stdio/StdioServerTransport.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/stdio/StdioServerTransport.java index a95ec5e..9ec3a8f 100644 --- a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/stdio/StdioServerTransport.java +++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/stdio/StdioServerTransport.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.ai.mcp.client.stdio; import java.io.BufferedReader; @@ -13,6 +29,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; + import org.springframework.ai.mcp.client.util.Assert; import org.springframework.ai.mcp.spec.AbstractMcpTransport; import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage; @@ -22,7 +39,7 @@ import org.springframework.ai.mcp.spec.McpSchema.JSONRPCResponse; /** * Stdio client for communicating with a server process. - * + * * @author Christian Tzolov * @author Dariusz Jędrzejczyk */ @@ -34,11 +51,15 @@ public class StdioServerTransport extends AbstractMcpTransport { private Process process; private BufferedReader processErrorReader; + private BufferedReader processReader; + private BufferedWriter processWriter; private Scheduler inboundScheduler; + private Scheduler outboundScheduler; + private Scheduler errorScheduler; volatile boolean isRunning; @@ -59,12 +80,9 @@ public class StdioServerTransport extends AbstractMcpTransport { this.params = params; // Start threads - this.inboundScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), - "inbound"); - this.outboundScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), - "outbound"); - this.errorScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), - "error"); + this.inboundScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), "inbound"); + this.outboundScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), "outbound"); + this.errorScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), "error"); } @Override @@ -81,7 +99,8 @@ public class StdioServerTransport extends AbstractMcpTransport { // Start the process try { this.process = processBuilder.start(); - } catch (IOException e) { + } + catch (IOException e) { throw new RuntimeException("Failed to start process with command: " + fullCommand, e); } @@ -110,7 +129,8 @@ public class StdioServerTransport extends AbstractMcpTransport { public void awaitForExit() { try { this.process.waitFor(); - } catch (InterruptedException e) { + } + catch (InterruptedException e) { throw new RuntimeException("Process interrupted", e); } } @@ -124,15 +144,18 @@ public class StdioServerTransport extends AbstractMcpTransport { System.out.println("Received error line: " + line); // TODO: handle errors, etc. this.getErrorSink().tryEmitNext(line); - } catch (Exception e) { + } + catch (Exception e) { throw new RuntimeException(e); } } - } catch (IOException e) { + } + catch (IOException e) { if (this.isRunning) { throw new RuntimeException(e); } - } finally { + } + finally { this.isRunning = false; } }); @@ -149,15 +172,18 @@ public class StdioServerTransport extends AbstractMcpTransport { // TODO: Back off, reschedule, give up? throw new RuntimeException("Failed to enqueue message"); } - } catch (Exception e) { + } + catch (Exception e) { throw new RuntimeException(e); } } - } catch (IOException e) { + } + catch (IOException e) { if (isRunning) { throw new RuntimeException(e); } - } finally { + } + finally { isRunning = false; } }); @@ -170,9 +196,11 @@ public class StdioServerTransport extends AbstractMcpTransport { // Determine message type based on specific JSON structure if (map.containsKey("method") && map.containsKey("id")) { return this.objectMapper.convertValue(map, JSONRPCRequest.class); - } else if (map.containsKey("method") && !map.containsKey("id")) { + } + else if (map.containsKey("method") && !map.containsKey("id")) { return this.objectMapper.convertValue(map, JSONRPCNotification.class); - } else if (map.containsKey("result") || map.containsKey("error")) { + } + else if (map.containsKey("result") || map.containsKey("error")) { return this.objectMapper.convertValue(map, JSONRPCResponse.class); } @@ -181,23 +209,24 @@ public class StdioServerTransport extends AbstractMcpTransport { private void startOutboundProcessing() { this.getOutboundSink() - .asFlux() - // this bit is important since writes come from user threads and we - // want to ensure that the actual writing happens on a dedicated thread - .publishOn(outboundScheduler) - .handle((message, s) -> { - if (message != null) { - try { - this.processWriter.write(objectMapper.writeValueAsString(message)); - this.processWriter.newLine(); - this.processWriter.flush(); - s.next(message); - } catch (IOException e) { - s.error(new RuntimeException(e)); - } + .asFlux() + // this bit is important since writes come from user threads and we + // want to ensure that the actual writing happens on a dedicated thread + .publishOn(outboundScheduler) + .handle((message, s) -> { + if (message != null) { + try { + this.processWriter.write(objectMapper.writeValueAsString(message)); + this.processWriter.newLine(); + this.processWriter.flush(); + s.next(message); } - }) - .subscribe(); + catch (IOException e) { + s.error(new RuntimeException(e)); + } + } + }) + .subscribe(); } @Override @@ -205,41 +234,40 @@ public class StdioServerTransport extends AbstractMcpTransport { this.isRunning = false; - return Mono.whenDelayError( - Mono.fromRunnable(() -> { - try { - this.processErrorReader.close(); - } catch (IOException e) { - throw new RuntimeException(e); + return Mono.whenDelayError(Mono.fromRunnable(() -> { + try { + this.processErrorReader.close(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + }).subscribeOn(errorScheduler), Mono.fromRunnable(() -> { + try { + this.processReader.close(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + }).subscribeOn(inboundScheduler), Mono.fromRunnable(() -> { + try { + this.processWriter.close(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + }).subscribeOn(outboundScheduler)) + .then(Mono.whenDelayError(inboundScheduler.disposeGracefully(), outboundScheduler.disposeGracefully(), + errorScheduler.disposeGracefully())) + .then(Mono.fromRunnable(() -> { + if (this.process != null) { + var process = this.process.destroyForcibly(); + if (process.exitValue() != 0) { + throw new RuntimeException("Failed to destroy process"); } - }).subscribeOn(errorScheduler), - Mono.fromRunnable(() -> { - try { - this.processReader.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - }).subscribeOn(inboundScheduler), - Mono.fromRunnable(() -> { - try { - this.processWriter.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - }).subscribeOn(outboundScheduler)) - .then(Mono.whenDelayError(inboundScheduler.disposeGracefully(), - outboundScheduler.disposeGracefully(), - errorScheduler.disposeGracefully())) - .then(Mono.fromRunnable(() -> { - if (this.process != null) { - var process = this.process.destroyForcibly(); - if (process.exitValue() != 0) { - throw new RuntimeException("Failed to destroy process"); - } - } - })) - .then() - .subscribeOn(Schedulers.boundedElastic()); + } + })) + .then() + .subscribeOn(Schedulers.boundedElastic()); } } \ No newline at end of file diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/util/Assert.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/util/Assert.java index 1b4fdfc..4804020 100644 --- a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/util/Assert.java +++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/util/Assert.java @@ -1,18 +1,19 @@ -/* -* Copyright 2024 - 2024 the original author or authors. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* https://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.ai.mcp.client.util; import reactor.util.annotation.Nullable; @@ -23,26 +24,26 @@ import reactor.util.annotation.Nullable; */ public class Assert { - /** - * Assert that an object is not {@code null}. - * - *
      -     * Assert.notNull(clazz, "The class must not be null");
      -     * 
      - * - * @param object the object to check - * @param message the exception message to use if the assertion fails - * @throws IllegalArgumentException if the object is {@code null} - */ - public static void notNull(@Nullable Object object, String message) { - if (object == null) { - throw new IllegalArgumentException(message); - } - } - /** - * Assert that the given String contains valid text content; that is, it must not - * be {@code null} and must contain at least one non-whitespace character. + /** + * Assert that an object is not {@code null}. + * + *
      +	 * Assert.notNull(clazz, "The class must not be null");
      +	 * 
      + * @param object the object to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object is {@code null} + */ + public static void notNull(@Nullable Object object, String message) { + if (object == null) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given String contains valid text content; that is, it must not be + * {@code null} and must contain at least one non-whitespace character. *
      Assert.hasText(name, "'name' must not be empty");
      * @param text the String to check * @param message the exception message to use if the assertion fails @@ -57,12 +58,13 @@ public class Assert { /** * Check whether the given {@code String} contains actual text. - *

      More specifically, this method returns {@code true} if the - * {@code String} is not {@code null}, its length is greater than 0, - * and it contains at least one non-whitespace character. + *

      + * More specifically, this method returns {@code true} if the {@code String} is not + * {@code null}, its length is greater than 0, and it contains at least one + * non-whitespace character. * @param str the {@code String} to check (may be {@code null}) - * @return {@code true} if the {@code String} is not {@code null}, its - * length is greater than 0, and it does not contain whitespace only + * @return {@code true} if the {@code String} is not {@code null}, its length is + * greater than 0, and it does not contain whitespace only * @see #hasText(CharSequence) * @see #hasLength(String) * @see Character#isWhitespace @@ -71,5 +73,4 @@ public class Assert { return (str != null && !str.isBlank()); } - } diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/AbstractMcpTransport.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/AbstractMcpTransport.java index 7769923..6c09c85 100644 --- a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/AbstractMcpTransport.java +++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/AbstractMcpTransport.java @@ -1,18 +1,19 @@ /* -* Copyright 2024 - 2024 the original author or authors. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* https://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.ai.mcp.spec; import java.util.function.Consumer; @@ -20,6 +21,7 @@ import java.util.function.Consumer; import com.fasterxml.jackson.databind.ObjectMapper; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; + import org.springframework.ai.mcp.client.util.Assert; import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage; @@ -32,14 +34,17 @@ public abstract class AbstractMcpTransport implements McpTransport { protected final ObjectMapper objectMapper; private final Sinks.Many errorSink; + private final Sinks.Many inboundSink; + private final Sinks.Many outboundSink; private Consumer inboundMessageHandler = message -> System.out - .println("Received message: " + message); + .println("Received message: " + message); + private Consumer errorHandler = error -> System.err.println("Received error: " + error); - public AbstractMcpTransport() { + public AbstractMcpTransport() { this(new ObjectMapper()); } @@ -66,8 +71,7 @@ public abstract class AbstractMcpTransport implements McpTransport { } private void handleIncomingMessages() { - this.inboundSink.asFlux() - .subscribe(message -> this.inboundMessageHandler.accept(message)); + this.inboundSink.asFlux().subscribe(message -> this.inboundMessageHandler.accept(message)); } private void handleIncomingErrors() { @@ -112,8 +116,10 @@ public abstract class AbstractMcpTransport implements McpTransport { // In this approach we delegate the retry and the backpressure onto the // caller. This might be enough for most cases. return Mono.empty(); - } else { + } + else { return Mono.error(new RuntimeException("Failed to enqueue message")); } } + } diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/DefaultMcpSession.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/DefaultMcpSession.java index dbddc87..f754289 100644 --- a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/DefaultMcpSession.java +++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/DefaultMcpSession.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.ai.mcp.spec; import java.time.Duration; @@ -9,11 +25,12 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoSink; + import org.springframework.ai.mcp.client.util.Assert; /** * Implementation of the MCP client session. - * + * * @author Christian Tzolov * @author Dariusz Jędrzejczyk */ @@ -39,16 +56,19 @@ public class DefaultMcpSession implements McpSession { this.transport.setInboudMessageHandler(message -> { - if ( message instanceof McpSchema.JSONRPCResponse response) { + if (message instanceof McpSchema.JSONRPCResponse response) { var sink = pendingResponses.remove(response.id()); if (sink == null) { System.out.println("Unexpected response for unkown id " + response.id()); - } else { + } + else { sink.success(response); } - } else if ( message instanceof McpSchema.JSONRPCRequest request) { + } + else if (message instanceof McpSchema.JSONRPCRequest request) { System.out.println("Client does not yet support server requests"); - } else if ( message instanceof McpSchema.JSONRPCNotification notification) { + } + else if (message instanceof McpSchema.JSONRPCNotification notification) { System.out.println("Notifications not yet supported"); } }); @@ -70,23 +90,26 @@ public class DefaultMcpSession implements McpSession { // TODO: This is non-blocking, but it's actually a synchronous call, // perhaps there's no need to make it return Mono? this.transport.sendMessage(jsonrpcRequest) - // TODO: It's most efficient to create a dedicated - // Subscriber here - .subscribe(v -> { - }, e -> { - this.pendingResponses.remove(requestId); - sink.error(e); - }); - } catch (Exception e) { + // TODO: It's most efficient to create a dedicated + // Subscriber here + .subscribe(v -> { + }, e -> { + this.pendingResponses.remove(requestId); + sink.error(e); + }); + } + catch (Exception e) { sink.error(e); } }).timeout(this.requestTimeout).handle((jsonRpcResponse, s) -> { if (jsonRpcResponse.error() != null) { s.error(new McpError(jsonRpcResponse.error())); - } else { + } + else { if (typeRef.getType().getTypeName().equals("java.lang.Void")) { s.complete(); - } else { + } + else { s.next(this.objectMapper.convertValue(jsonRpcResponse.result(), typeRef)); } } @@ -99,7 +122,8 @@ public class DefaultMcpSession implements McpSession { method, params); try { this.transport.sendMessage(jsonrpcNotification); - } catch (Exception e) { + } + catch (Exception e) { return Mono.error(new McpError(e)); } return Mono.empty(); diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/McpSchema.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/McpSchema.java index b0843b3..e78fa2d 100644 --- a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/McpSchema.java +++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/McpSchema.java @@ -1,18 +1,19 @@ /* -* Copyright 2024 - 2024 the original author or authors. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* https://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.ai.mcp.spec; import java.util.List; @@ -57,7 +58,8 @@ public class McpSchema { } - public sealed interface Request permits InitializeRequest, CallToolRequest, CreateMessageRequest, CompleteRequest, GetPromptRequest { + public sealed interface Request + permits InitializeRequest, CallToolRequest, CreateMessageRequest, CompleteRequest, GetPromptRequest { } @@ -178,8 +180,7 @@ public class McpSchema { // Resource Interfaces // --------------------------- /** - * Base for objects that include optional annotations for the client. The client - * can + * Base for objects that include optional annotations for the client. The client can * use annotations to inform how objects are used or displayed */ public interface Annotated { @@ -189,18 +190,13 @@ public class McpSchema { } /** - * @param audience Describes who the intended customer of this object or data - * is. It - * can include multiple entries to indicate content useful for - * multiple audiences - * (e.g., `["user", "assistant"]`). - * @param priority Describes how important this data is for operating the - * server. A - * value of 1 means "most important," and indicates that the - * data is effectively - * required, while 0 means "least important," and indicates that - * the data is entirely - * optional. It is a number between 0 and 1. + * @param audience Describes who the intended customer of this object or data is. It + * can include multiple entries to indicate content useful for multiple audiences + * (e.g., `["user", "assistant"]`). + * @param priority Describes how important this data is for operating the server. A + * value of 1 means "most important," and indicates that the data is effectively + * required, while 0 means "least important," and indicates that the data is entirely + * optional. It is a number between 0 and 1. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) public record Annotations( // @formatter:off @@ -211,16 +207,13 @@ public class McpSchema { /** * A known resource that the server is capable of reading. * - * @param uri the URI of the resource. - * @param name A human-readable name for this resource. This can be used - * by clients to - * populate UI elements. - * @param description A description of what this resource represents. This can - * be used - * by clients to improve the LLM's understanding of available - * resources. It can be - * thought of like a "hint" to the model. - * @param mimeType The MIME type of this resource, if known. + * @param uri the URI of the resource. + * @param name A human-readable name for this resource. This can be used by clients to + * populate UI elements. + * @param description A description of what this resource represents. This can be used + * by clients to improve the LLM's understanding of available resources. It can be + * thought of like a "hint" to the model. + * @param mimeType The MIME type of this resource, if known. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) @@ -264,13 +257,11 @@ public class McpSchema { } // @formatter:on /** - * Sent from the client to request resources/updated notifications from the - * server + * Sent from the client to request resources/updated notifications from the server * whenever a particular resource changes. * - * @param uri the URI of the resource to subscribe to. The URI can use any - * protocol; - * it is up to the server how to interpret it. + * @param uri the URI of the resource to subscribe to. The URI can use any protocol; + * it is up to the server how to interpret it. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) public record SubscribeRequest( // @formatter:off @@ -292,14 +283,12 @@ public class McpSchema { /** * The URI of this resource. - * * @return the URI of this resource. */ String uri(); /** * The MIME type of this resource. - * * @return the MIME type of this resource. */ String mimeType(); @@ -309,11 +298,10 @@ public class McpSchema { /** * Text contents of a resource. * - * @param uri the URI of this resource. + * @param uri the URI of this resource. * @param mimeType the MIME type of this resource. - * @param text the text of the resource. This must only be set if the - * resource can - * actually be represented as text (not binary data). + * @param text the text of the resource. This must only be set if the resource can + * actually be represented as text (not binary data). */ @JsonInclude(JsonInclude.Include.NON_ABSENT) public record TextResourceContents( // @formatter:off @@ -325,13 +313,11 @@ public class McpSchema { /** * Binary contents of a resource. * - * @param uri the URI of this resource. + * @param uri the URI of this resource. * @param mimeType the MIME type of this resource. - * @param blob a base64-encoded string representing the binary data of the - * resource. - * This must only be set if the resource can actually be - * represented as binary data - * (not text). + * @param blob a base64-encoded string representing the binary data of the resource. + * This must only be set if the resource can actually be represented as binary data + * (not text). */ @JsonInclude(JsonInclude.Include.NON_ABSENT) public record BlobResourceContents( // @formatter:off @@ -346,9 +332,9 @@ public class McpSchema { /** * A prompt or prompt template that the server offers. * - * @param name The name of the prompt or prompt template. + * @param name The name of the prompt or prompt template. * @param description An optional description of what this prompt provides. - * @param arguments A list of arguments to use for templating the prompt. + * @param arguments A list of arguments to use for templating the prompt. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) public record Prompt( // @formatter:off @@ -360,9 +346,9 @@ public class McpSchema { /** * Describes an argument that a prompt can accept. * - * @param name The name of the argument. + * @param name The name of the argument. * @param description A human-readable description of the argument. - * @param required Whether this argument must be provided. + * @param required Whether this argument must be provided. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) public record PromptArgument( // @formatter:off @@ -374,12 +360,10 @@ public class McpSchema { /** * Describes a message returned as part of a prompt. * - * This is similar to `SamplingMessage`, but also supports the embedding of - * resources + * This is similar to `SamplingMessage`, but also supports the embedding of resources * from the MCP server. - * - * @param role The sender or recipient of messages and data in a - * conversation. + * + * @param role The sender or recipient of messages and data in a conversation. * @param content The content of the message of type {@link Content}. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @@ -399,8 +383,8 @@ public class McpSchema { /** * Used by the client to get a prompt provided by the server. - * - * @param name The name of the prompt or prompt template. + * + * @param name The name of the prompt or prompt template. * @param arguments Arguments to use for templating the prompt. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @@ -411,7 +395,7 @@ public class McpSchema { /** * The server's response to a prompts/get request from the client. - * + * * @param description An optional description for the prompt. * @param messages A list of messages to display as part of the prompt. */ diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/McpSession.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/McpSession.java index fd7fab5..7c0e48e 100644 --- a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/McpSession.java +++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/McpSession.java @@ -1,11 +1,11 @@ /* - * Copyright 2024 - 2024 the original author or authors. + * Copyright 2024-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.ai.mcp.spec; import java.time.Duration; @@ -23,65 +24,72 @@ import reactor.core.publisher.Mono; /** * Represents a Model Control Protocol (MCP) session that handles communication between - * clients and the server. This interface provides methods for sending requests - * and notifications, as well as managing the session lifecycle. - * - *

      The session operates asynchronously using Project Reactor's {@link Mono} type - * for non-blocking operations. It supports both request-response patterns and - * one-way notifications.

      + * clients and the server. This interface provides methods for sending requests and + * notifications, as well as managing the session lifecycle. + * + *

      + * The session operates asynchronously using Project Reactor's {@link Mono} type for + * non-blocking operations. It supports both request-response patterns and one-way + * notifications. + *

      * * @author Christian Tzolov * @author Dariusz Jędrzejczyk */ public interface McpSession { - /** - * Sends a request to the model server and expects a response of type T. - * - *

      This method handles the request-response pattern where a response is expected - * from the server. The response type is determined by the provided TypeReference.

      - * - * @param the type of the expected response - * @param method the name of the method to be called on the server - * @param requestParams the parameters to be sent with the request - * @param typeRef the TypeReference describing the expected response type - * @return a Mono that will emit the response when received - */ - Mono sendRequest(String method, Object requestParams, TypeReference typeRef); + /** + * Sends a request to the model server and expects a response of type T. + * + *

      + * This method handles the request-response pattern where a response is expected from + * the server. The response type is determined by the provided TypeReference. + *

      + * @param the type of the expected response + * @param method the name of the method to be called on the server + * @param requestParams the parameters to be sent with the request + * @param typeRef the TypeReference describing the expected response type + * @return a Mono that will emit the response when received + */ + Mono sendRequest(String method, Object requestParams, TypeReference typeRef); - /** - * Sends a notification to the model server without parameters. - * - *

      This method implements the notification pattern where no response is expected - * from the server. It's useful for fire-and-forget scenarios.

      - * - * @param method the name of the notification method to be called on the server - * @return a Mono that completes when the notification has been sent - */ + /** + * Sends a notification to the model server without parameters. + * + *

      + * This method implements the notification pattern where no response is expected from + * the server. It's useful for fire-and-forget scenarios. + *

      + * @param method the name of the notification method to be called on the server + * @return a Mono that completes when the notification has been sent + */ default Mono sendNotification(String method) { return sendNotification(method, null); } - /** - * Sends a notification to the model server with parameters. - * - *

      Similar to {@link #sendNotification(String)} but allows sending additional - * parameters with the notification.

      - * - * @param method the name of the notification method to be called on the server - * @param params a map of parameters to be sent with the notification - * @return a Mono that completes when the notification has been sent - */ - Mono sendNotification(String method, Map params); + /** + * Sends a notification to the model server with parameters. + * + *

      + * Similar to {@link #sendNotification(String)} but allows sending additional + * parameters with the notification. + *

      + * @param method the name of the notification method to be called on the server + * @param params a map of parameters to be sent with the notification + * @return a Mono that completes when the notification has been sent + */ + Mono sendNotification(String method, Map params); + + /** + * Gracefully closes the session with a specified timeout. + * + *

      + * This method attempts to close the session cleanly, waiting for any pending + * operations to complete up to the specified timeout duration. + *

      + * @param timeout the maximum duration to wait for the session to close + * @return a Mono that completes when the session has been closed + */ + Mono closeGracefully(Duration timeout); - /** - * Gracefully closes the session with a specified timeout. - * - *

      This method attempts to close the session cleanly, waiting for any pending - * operations to complete up to the specified timeout duration.

      - * - * @param timeout the maximum duration to wait for the session to close - * @return a Mono that completes when the session has been closed - */ - Mono closeGracefully(Duration timeout); } diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/McpTransport.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/McpTransport.java index 3e91c9d..3d444bc 100644 --- a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/McpTransport.java +++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/spec/McpTransport.java @@ -1,38 +1,37 @@ -/* - * Copyright 2024 - 2024 the original author or authors. - * +/* + * Copyright 2024-2024 the original author or authors. + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * + * + * https://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.ai.mcp.spec; import java.util.function.Consumer; import reactor.core.publisher.Mono; + import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage; /** - * Defines the asynchronous transport layer for the Model Context Protocol - * (MCP). - * + * Defines the asynchronous transport layer for the Model Context Protocol (MCP). + * *

      - * The McpTransport interface provides the foundation for implementing custom - * transport - * mechanisms in the Model Context Protocol. It handles the bidirectional - * communication between - * the client and server components, supporting asynchronous message exchange + * The McpTransport interface provides the foundation for implementing custom transport + * mechanisms in the Model Context Protocol. It handles the bidirectional communication + * between the client and server components, supporting asynchronous message exchange * using JSON-RPC format. *

      - * + * *

      * Implementations of this interface are responsible for: *

      @@ -41,11 +40,10 @@ import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage; *
    • Handling incoming messages and errors from the server
    • *
    • Sending outbound messages to the server
    • *
    - * + * *

    * The transport layer is designed to be protocol-agnostic, allowing for various - * implementations - * such as WebSocket, HTTP, or custom protocols. + * implementations such as WebSocket, HTTP, or custom protocols. *

    * * @author Christian Tzolov @@ -53,78 +51,69 @@ import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage; */ public interface McpTransport { - /** - * Initializes and starts the transport connection. - * - *

    - * This method should be called before any message exchange can occur. It sets - * up - * the necessary resources and establishes the connection to the server. - *

    - */ - void start(); + /** + * Initializes and starts the transport connection. + * + *

    + * This method should be called before any message exchange can occur. It sets up the + * necessary resources and establishes the connection to the server. + *

    + */ + void start(); - /** - * Closes the transport connection and releases any associated resources. - * - *

    - * This method ensures proper cleanup of resources when the transport is no - * longer needed. - * It should handle the graceful shutdown of any active connections. - *

    - */ - default void close() { - this.closeGracefully().subscribe(); - } + /** + * Closes the transport connection and releases any associated resources. + * + *

    + * This method ensures proper cleanup of resources when the transport is no longer + * needed. It should handle the graceful shutdown of any active connections. + *

    + */ + default void close() { + this.closeGracefully().subscribe(); + } - /** - * Closes the transport connection and releases any associated resources - * asynchronously. - * - * @return a {@link Mono} that completes when the connection has been - * closed. - */ - Mono closeGracefully(); + /** + * Closes the transport connection and releases any associated resources + * asynchronously. + * @return a {@link Mono} that completes when the connection has been closed. + */ + Mono closeGracefully(); - /** - * Sets the handler for processing incoming messages from the server. - * - *

    - * The provided handler will be called whenever a new message is received from - * the server. - * Messages are expected to follow the JSON-RPC format as defined in the MCP - * specification. - *

    - * - * @param inboundMessageHandler a consumer that processes incoming - * {@link JSONRPCMessage}s - */ - void setInboudMessageHandler(Consumer inboundMessageHandler); + /** + * Sets the handler for processing incoming messages from the server. + * + *

    + * The provided handler will be called whenever a new message is received from the + * server. Messages are expected to follow the JSON-RPC format as defined in the MCP + * specification. + *

    + * @param inboundMessageHandler a consumer that processes incoming + * {@link JSONRPCMessage}s + */ + void setInboudMessageHandler(Consumer inboundMessageHandler); - /** - * Sets the handler for processing transport-level errors. - * - *

    - * The provided handler will be called when errors occur during transport - * operations, - * such as connection failures or protocol violations. - *

    - * - * @param inboundErrorHandler a consumer that processes error messages - */ - void setInboundErrorHandler(Consumer inboundErrorHandler); + /** + * Sets the handler for processing transport-level errors. + * + *

    + * The provided handler will be called when errors occur during transport operations, + * such as connection failures or protocol violations. + *

    + * @param inboundErrorHandler a consumer that processes error messages + */ + void setInboundErrorHandler(Consumer inboundErrorHandler); + + /** + * Sends a message to the server asynchronously. + * + *

    + * This method handles the transmission of messages to the server in an asynchronous + * manner. Messages are sent in JSON-RPC format as specified by the MCP protocol. + *

    + * @param message the {@link JSONRPCMessage} to be sent to the server + * @return a {@link Mono} that completes when the message has been sent + */ + Mono sendMessage(JSONRPCMessage message); - /** - * Sends a message to the server asynchronously. - * - *

    - * This method handles the transmission of messages to the server in an - * asynchronous manner. - * Messages are sent in JSON-RPC format as specified by the MCP protocol. - *

    - * - * @param message the {@link JSONRPCMessage} to be sent to the server - * @return a {@link Mono} that completes when the message has been sent - */ - Mono sendMessage(JSONRPCMessage message); } diff --git a/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/attic/ClientSessionTests2.java b/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/attic/ClientSessionTests2.java index 55b60b0..57a4ba3 100644 --- a/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/attic/ClientSessionTests2.java +++ b/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/attic/ClientSessionTests2.java @@ -1,23 +1,25 @@ /* -* Copyright 2024 - 2024 the original author or authors. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* https://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.ai.mcp.attic; import java.time.Duration; import com.fasterxml.jackson.databind.ObjectMapper; + import org.springframework.ai.mcp.client.McpClient; import org.springframework.ai.mcp.client.McpSyncClient; import org.springframework.ai.mcp.client.stdio.ServerParameters; @@ -35,13 +37,11 @@ public class ClientSessionTests2 { public static void main(String[] args) { var stdioParams = ServerParameters.builder("uv") - .args("--directory", "dir", "run", "mcp-server-sqlite", - "--db-path", "~/test.db") + .args("--directory", "dir", "run", "mcp-server-sqlite", "--db-path", "~/test.db") .build(); - try (McpSyncClient clientSession = - McpClient.sync(new StdioServerTransport(stdioParams), - Duration.ofSeconds(10), new ObjectMapper())) { + try (McpSyncClient clientSession = McpClient.sync(new StdioServerTransport(stdioParams), Duration.ofSeconds(10), + new ObjectMapper())) { clientSession.initialize(); diff --git a/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/attic/ClientSessionTests3.java b/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/attic/ClientSessionTests3.java index 59ec596..e4bd3e1 100644 --- a/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/attic/ClientSessionTests3.java +++ b/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/attic/ClientSessionTests3.java @@ -1,23 +1,25 @@ /* -* Copyright 2024 - 2024 the original author or authors. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* https://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.ai.mcp.attic; import java.time.Duration; import com.fasterxml.jackson.databind.ObjectMapper; + import org.springframework.ai.mcp.client.McpClient; import org.springframework.ai.mcp.client.McpSyncClient; import org.springframework.ai.mcp.client.stdio.ServerParameters; @@ -35,16 +37,14 @@ public class ClientSessionTests3 { public static void main(String[] args) { var stdioParams = ServerParameters.builder("uv") - .args("--directory", "dir", "run", "mcp-server-sqlite", - "--db-path", "~/test.db") + .args("--directory", "dir", "run", "mcp-server-sqlite", "--db-path", "~/test.db") .build(); - McpSyncClient clientSession = null; try { - clientSession = McpClient.sync(new StdioServerTransport(stdioParams), - Duration.ofSeconds(10), new ObjectMapper()); + clientSession = McpClient.sync(new StdioServerTransport(stdioParams), Duration.ofSeconds(10), + new ObjectMapper()); clientSession.initialize(); diff --git a/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/client/McpAsyncClientTests.java b/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/client/McpAsyncClientTests.java index bd10cc2..70fc8d5 100644 --- a/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/client/McpAsyncClientTests.java +++ b/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/client/McpAsyncClientTests.java @@ -1,11 +1,11 @@ /* - * Copyright 2024 - 2024 the original author or authors. + * Copyright 2024-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.ai.mcp.client; import java.time.Duration; @@ -23,6 +24,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; + import org.springframework.ai.mcp.client.stdio.ServerParameters; import org.springframework.ai.mcp.client.stdio.StdioServerTransport; import org.springframework.ai.mcp.spec.McpSchema.CallToolRequest; @@ -54,8 +56,8 @@ class McpAsyncClientTests { @BeforeEach void setUp() { stdioParams = ServerParameters.builder("npx") - .args("-y", "@modelcontextprotocol/server-everything", "dir") - .build(); + .args("-y", "@modelcontextprotocol/server-everything", "dir") + .build(); assertThatCode(() -> { mcpAsyncClient = McpClient.async(new StdioServerTransport(stdioParams), TIMEOUT, new ObjectMapper()); @@ -73,13 +75,13 @@ class McpAsyncClientTests { @Test void testConstructorWithInvalidArguments() { assertThatThrownBy(() -> McpClient.sync(null, TIMEOUT, new ObjectMapper())) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(IllegalArgumentException.class); assertThatThrownBy(() -> McpClient.sync(new StdioServerTransport(stdioParams), null, new ObjectMapper())) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(IllegalArgumentException.class); assertThatThrownBy(() -> McpClient.sync(new StdioServerTransport(stdioParams), TIMEOUT, null)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(IllegalArgumentException.class); } @Test @@ -152,16 +154,15 @@ class McpAsyncClientTests { @Timeout(15) void testListPrompts() { mcpAsyncClient.listPrompts(null).subscribe(prompts -> { - assertThat(prompts).isNotNull() - .satisfies(result -> { - assertThat(result.prompts()).isNotNull(); + assertThat(prompts).isNotNull().satisfies(result -> { + assertThat(result.prompts()).isNotNull(); - if (!result.prompts().isEmpty()) { - Prompt firstPrompt = result.prompts().get(0); - assertThat(firstPrompt.name()).isNotNull(); - assertThat(firstPrompt.description()).isNotNull(); - } - }); + if (!result.prompts().isEmpty()) { + Prompt firstPrompt = result.prompts().get(0); + assertThat(firstPrompt.name()).isNotNull(); + assertThat(firstPrompt.description()).isNotNull(); + } + }); }); } @@ -169,11 +170,10 @@ class McpAsyncClientTests { @Timeout(15) void testGetPrompt() { mcpAsyncClient.getPrompt(new GetPromptRequest("simple_prompt", Map.of())).subscribe(prompt -> { - assertThat(prompt).isNotNull() - .satisfies(result -> { - assertThat(result.description()).isNotNull(); - assertThat(result.messages()).isNotEmpty(); - }); + assertThat(prompt).isNotNull().satisfies(result -> { + assertThat(result.description()).isNotNull(); + assertThat(result.messages()).isNotEmpty(); + }); }); } diff --git a/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/client/McpSyncClientTests.java b/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/client/McpSyncClientTests.java index bb47407..0a352cb 100644 --- a/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/client/McpSyncClientTests.java +++ b/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/client/McpSyncClientTests.java @@ -1,11 +1,11 @@ /* - * Copyright 2024 - 2024 the original author or authors. + * Copyright 2024-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.ai.mcp.client; import java.time.Duration; @@ -23,6 +24,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; + import org.springframework.ai.mcp.client.stdio.ServerParameters; import org.springframework.ai.mcp.client.stdio.StdioServerTransport; import org.springframework.ai.mcp.spec.McpSchema.CallToolRequest; @@ -104,7 +106,7 @@ class McpSyncClientTests { CallToolResult toolResult = mcpSyncClient.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4))); assertThat(toolResult).isNotNull().satisfies(result -> { - + assertThat(result.content()).hasSize(1); TextContent content = (TextContent) result.content().get(0); diff --git a/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/client/stdio/StdioServerTransportTests.java b/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/client/stdio/StdioServerTransportTests.java index 0eb80cb..9b36dfe 100644 --- a/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/client/stdio/StdioServerTransportTests.java +++ b/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/client/stdio/StdioServerTransportTests.java @@ -1,11 +1,11 @@ /* - * Copyright 2024 - 2024 the original author or authors. + * Copyright 2024-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.ai.mcp.client.stdio; import java.io.BufferedReader; @@ -129,4 +130,5 @@ public class StdioServerTransportTests { assertThat(exception.getMessage()).contains("Process input or output stream is null"); verify(mockProcess).destroy(); } + } \ No newline at end of file diff --git a/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/spec/DefaultMcpTransportTests.java b/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/spec/DefaultMcpTransportTests.java index 197f7c4..449ee3c 100644 --- a/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/spec/DefaultMcpTransportTests.java +++ b/spring-ai-mcp-core/src/test/java/org/springframework/ai/mcp/spec/DefaultMcpTransportTests.java @@ -1,11 +1,11 @@ /* - * Copyright 2024 - 2024 the original author or authors. + * Copyright 2024-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.ai.mcp.spec; import java.util.concurrent.atomic.AtomicReference; @@ -22,6 +23,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; + import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage; import org.springframework.ai.mcp.spec.McpSchema.JSONRPCRequest; @@ -31,7 +33,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; class DefaultMcpTransportTests { private AbstractMcpTransport transport; - + private ObjectMapper objectMapper; @BeforeEach @@ -52,9 +54,7 @@ class DefaultMcpTransportTests { public Mono closeGracefully() { return null; } - }) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("ObjectMapper must not be null"); + }).isInstanceOf(IllegalArgumentException.class).hasMessage("ObjectMapper must not be null"); } @Test @@ -75,26 +75,19 @@ class DefaultMcpTransportTests { @Test void sendMessageShouldEmitToOutboundSink() { - JSONRPCRequest message = new JSONRPCRequest( - "2.0", - "test", - 1, - null); + JSONRPCRequest message = new JSONRPCRequest("2.0", "test", 1, null); AtomicReference receivedMessage = new AtomicReference<>(); transport.getOutboundSink().asFlux().subscribe(receivedMessage::set); Mono result = transport.sendMessage(message); - StepVerifier.create(result) - .verifyComplete(); + StepVerifier.create(result).verifyComplete(); - assertThat(receivedMessage.get()) - .isNotNull() - .satisfies(msg -> { - assertThat(((JSONRPCRequest) msg).jsonrpc()).isEqualTo("2.0"); - assertThat(((JSONRPCRequest) msg).method()).isEqualTo("test"); - }); + assertThat(receivedMessage.get()).isNotNull().satisfies(msg -> { + assertThat(((JSONRPCRequest) msg).jsonrpc()).isEqualTo("2.0"); + assertThat(((JSONRPCRequest) msg).method()).isEqualTo("test"); + }); } @Test @@ -103,20 +96,14 @@ class DefaultMcpTransportTests { transport.setInboudMessageHandler(receivedMessage::set); transport.start(); - JSONRPCRequest message = new JSONRPCRequest( - "2.0", - "test", - 1, - null); + JSONRPCRequest message = new JSONRPCRequest("2.0", "test", 1, null); transport.getInboundSink().tryEmitNext(message); - assertThat(receivedMessage.get()) - .isNotNull() - .satisfies(msg -> { - assertThat(((JSONRPCRequest) msg).jsonrpc()).isEqualTo("2.0"); - assertThat(((JSONRPCRequest) msg).method()).isEqualTo("test"); - }); + assertThat(receivedMessage.get()).isNotNull().satisfies(msg -> { + assertThat(((JSONRPCRequest) msg).jsonrpc()).isEqualTo("2.0"); + assertThat(((JSONRPCRequest) msg).method()).isEqualTo("test"); + }); } @Test @@ -128,9 +115,7 @@ class DefaultMcpTransportTests { String errorMessage = "Test error"; transport.getErrorSink().tryEmitNext(errorMessage); - assertThat(receivedError.get()) - .isNotNull() - .isEqualTo(errorMessage); + assertThat(receivedError.get()).isNotNull().isEqualTo(errorMessage); } @Test @@ -142,11 +127,7 @@ class DefaultMcpTransportTests { transport.setInboundErrorHandler(receivedError::set); transport.start(); - JSONRPCRequest message = new JSONRPCRequest( - "2.0", - "test", - 1, - null); + JSONRPCRequest message = new JSONRPCRequest("2.0", "test", 1, null); String errorMessage = "Test error"; @@ -159,19 +140,15 @@ class DefaultMcpTransportTests { @Test void sendMessageShouldReturnErrorWhenSinkFails() { - JSONRPCRequest message = new JSONRPCRequest( - "2.0", - "test", - 1, - null); + JSONRPCRequest message = new JSONRPCRequest("2.0", "test", 1, null); // Close the sink to simulate failure transport.getOutboundSink().tryEmitComplete(); Mono result = transport.sendMessage(message); StepVerifier.create(result) - .verifyErrorSatisfies(error -> assertThat(error) - .isInstanceOf(RuntimeException.class) - .hasMessage("Failed to enqueue message")); + .verifyErrorSatisfies(error -> assertThat(error).isInstanceOf(RuntimeException.class) + .hasMessage("Failed to enqueue message")); } + } diff --git a/spring-ai-mcp-spring/pom.xml b/spring-ai-mcp-spring/pom.xml index 5d840c2..0553d4b 100644 --- a/spring-ai-mcp-spring/pom.xml +++ b/spring-ai-mcp-spring/pom.xml @@ -6,7 +6,7 @@ org.springframework.experimental spring-ai-mcp - 0.0.1-SNAPSHOT + 0.1.0-SNAPSHOT spring-ai-mcp-spring Spring AI MCP Spring diff --git a/spring-ai-mcp-spring/src/main/java/org/springframework/ai/mcp/spring/McpFunctionCallback.java b/spring-ai-mcp-spring/src/main/java/org/springframework/ai/mcp/spring/McpFunctionCallback.java index c307039..8f965b5 100644 --- a/spring-ai-mcp-spring/src/main/java/org/springframework/ai/mcp/spring/McpFunctionCallback.java +++ b/spring-ai-mcp-spring/src/main/java/org/springframework/ai/mcp/spring/McpFunctionCallback.java @@ -1,18 +1,19 @@ /* -* Copyright 2024 - 2024 the original author or authors. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* https://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.ai.mcp.spring; import java.util.Map;