Update copyright headers and add javaformat plugin

This commit is contained in:
Mark Pollack 2024-12-10 23:49:24 -05:00
commit 586f132f46
23 changed files with 708 additions and 642 deletions

14
pom.xml
View file

@ -97,6 +97,20 @@
<build> <build>
<plugins> <plugins>
<plugin>
<groupId>io.spring.javaformat</groupId>
<artifactId>spring-javaformat-maven-plugin</artifactId>
<version>${spring-javaformat-maven-plugin.version}</version>
<executions>
<execution>
<phase>validate</phase>
<inherited>true</inherited>
<goals>
<goal>validate</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId> <artifactId>maven-site-plugin</artifactId>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Before After
Before After

View file

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>org.springframework.experimental</groupId> <groupId>org.springframework.experimental</groupId>
<artifactId>spring-ai-mcp</artifactId> <artifactId>spring-ai-mcp</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.1.0-SNAPSHOT</version>
</parent> </parent>
<artifactId>spring-ai-mcp-core</artifactId> <artifactId>spring-ai-mcp-core</artifactId>
<packaging>jar</packaging> <packaging>jar</packaging>

View file

@ -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; package org.springframework.ai.mcp.client;
import java.time.Duration; import java.time.Duration;
@ -5,14 +20,15 @@ import java.time.Duration;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.ai.mcp.spec.DefaultMcpSession; 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.McpError;
import org.springframework.ai.mcp.spec.McpSchema; import org.springframework.ai.mcp.spec.McpSchema;
import org.springframework.ai.mcp.spec.McpSchema.GetPromptRequest; import org.springframework.ai.mcp.spec.McpSchema.GetPromptRequest;
import org.springframework.ai.mcp.spec.McpSchema.GetPromptResult; import org.springframework.ai.mcp.spec.McpSchema.GetPromptResult;
import org.springframework.ai.mcp.spec.McpSchema.ListPromptsResult; import org.springframework.ai.mcp.spec.McpSchema.ListPromptsResult;
import org.springframework.ai.mcp.spec.McpSchema.PaginatedRequest; import org.springframework.ai.mcp.spec.McpSchema.PaginatedRequest;
import org.springframework.ai.mcp.spec.McpTransport;
/** /**
* @author Dariusz Jędrzejczyk * @author Dariusz Jędrzejczyk
@ -32,8 +48,7 @@ public class McpAsyncClient extends DefaultMcpSession {
} }
/** /**
* The initialization phase MUST be the first interaction between client and * The initialization phase MUST be the first interaction between client and server.
* server.
* During this phase, the client and server: * During this phase, the client and server:
* <ul> * <ul>
* <li>Establish protocol version compatibility</li> * <li>Establish protocol version compatibility</li>
@ -41,8 +56,7 @@ public class McpAsyncClient extends DefaultMcpSession {
* <li>Share implementation details</li> * <li>Share implementation details</li>
* </ul> * </ul>
* <br/> * <br/>
* The client MUST initiate this phase by sending an initialize request * The client MUST initiate this phase by sending an initialize request containing:
* containing:
* <ul> * <ul>
* <li>The protocol version the client supports</li> * <li>The protocol version the client supports</li>
* <li>The client's capabilities</li> * <li>The client's capabilities</li>
@ -51,8 +65,7 @@ public class McpAsyncClient extends DefaultMcpSession {
* *
* The server MUST respond with its own capabilities and information: * The server MUST respond with its own capabilities and information:
* {@link McpSchema.ServerCapabilities}. <br/> * {@link McpSchema.ServerCapabilities}. <br/>
* After successful initialization, the client MUST send an initialized * After successful initialization, the client MUST send an initialized notification
* notification
* to indicate it is ready to begin normal operations. * to indicate it is ready to begin normal operations.
* *
* <br/> * <br/>
@ -60,7 +73,6 @@ public class McpAsyncClient extends DefaultMcpSession {
* <a href= * <a href=
* "https://github.com/modelcontextprotocol/specification/blob/main/docs/specification/basic/lifecycle.md#initialization">Initialization * "https://github.com/modelcontextprotocol/specification/blob/main/docs/specification/basic/lifecycle.md#initialization">Initialization
* Spec</a> * Spec</a>
*
* @return the initialize result. * @return the initialize result.
*/ */
public Mono<McpSchema.InitializeResult> initialize() { public Mono<McpSchema.InitializeResult> initialize() {
@ -75,12 +87,11 @@ public class McpAsyncClient extends DefaultMcpSession {
return result.flatMap(initializeResult -> { return result.flatMap(initializeResult -> {
if (!McpSchema.LATEST_PROTOCOL_VERSION.equals(initializeResult.protocolVersion())) { if (!McpSchema.LATEST_PROTOCOL_VERSION.equals(initializeResult.protocolVersion())) {
return Mono.error( return Mono.error(new McpError(
new McpError("Unsupported protocol version from the server: " "Unsupported protocol version from the server: " + initializeResult.protocolVersion()));
+ initializeResult.protocolVersion())); }
} else { else {
return this.sendNotification("notifications/initialized", null) return this.sendNotification("notifications/initialized", null).thenReturn(initializeResult);
.thenReturn(initializeResult);
} }
}); });
} }
@ -104,12 +115,12 @@ public class McpAsyncClient extends DefaultMcpSession {
// -------------------------- // --------------------------
private static TypeReference<McpSchema.CallToolResult> CALL_TOOL_RESULT_TYPE_REF = new TypeReference<McpSchema.CallToolResult>() { private static TypeReference<McpSchema.CallToolResult> CALL_TOOL_RESULT_TYPE_REF = new TypeReference<McpSchema.CallToolResult>() {
}; };
private static TypeReference<McpSchema.ListToolsResult> LIST_TOOLS_RESULT_TYPE_REF = new TypeReference<McpSchema.ListToolsResult>() { private static TypeReference<McpSchema.ListToolsResult> LIST_TOOLS_RESULT_TYPE_REF = new TypeReference<McpSchema.ListToolsResult>() {
}; };
/** /**
* Send a tools/call request. * Send a tools/call request.
*
* @param callToolRequest the call tool request. * @param callToolRequest the call tool request.
* @return the call tool result. * @return the call tool result.
*/ */
@ -119,7 +130,6 @@ public class McpAsyncClient extends DefaultMcpSession {
/** /**
* Send a tools/list request. * Send a tools/list request.
*
* @return the list of tools result. * @return the list of tools result.
*/ */
public Mono<McpSchema.ListToolsResult> listTools(String cursor) { public Mono<McpSchema.ListToolsResult> listTools(String cursor) {
@ -132,14 +142,15 @@ public class McpAsyncClient extends DefaultMcpSession {
private static TypeReference<McpSchema.ListResourcesResult> LIST_RESOURCES_RESULT_TYPE_REF = new TypeReference<McpSchema.ListResourcesResult>() { private static TypeReference<McpSchema.ListResourcesResult> LIST_RESOURCES_RESULT_TYPE_REF = new TypeReference<McpSchema.ListResourcesResult>() {
}; };
private static TypeReference<McpSchema.ReadResourceResult> READ_RESOURCE_RESULT_TYPE_REF = new TypeReference<McpSchema.ReadResourceResult>() { private static TypeReference<McpSchema.ReadResourceResult> READ_RESOURCE_RESULT_TYPE_REF = new TypeReference<McpSchema.ReadResourceResult>() {
}; };
private static TypeReference<McpSchema.ListResourceTemplatesResult> LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF = new TypeReference<McpSchema.ListResourceTemplatesResult>() { private static TypeReference<McpSchema.ListResourceTemplatesResult> LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF = new TypeReference<McpSchema.ListResourceTemplatesResult>() {
}; };
/** /**
* Send a resources/list request. * Send a resources/list request.
*
* @param cursor the cursor * @param cursor the cursor
* @return the list of resources result. * @return the list of resources result.
*/ */
@ -150,7 +161,6 @@ public class McpAsyncClient extends DefaultMcpSession {
/** /**
* Send a resources/read request. * Send a resources/read request.
*
* @param resource the resource to read * @param resource the resource to read
* @return the resource content. * @return the resource content.
*/ */
@ -160,7 +170,6 @@ public class McpAsyncClient extends DefaultMcpSession {
/** /**
* Send a resources/read request. * Send a resources/read request.
*
* @param readResourceRequest the read resource request. * @param readResourceRequest the read resource request.
* @return the resource content. * @return the resource content.
*/ */
@ -173,7 +182,6 @@ public class McpAsyncClient extends DefaultMcpSession {
* templates. Arguments may be auto-completed through the completion API. * templates. Arguments may be auto-completed through the completion API.
* *
* Request a list of resource templates the server has. * Request a list of resource templates the server has.
*
* @param cursor the cursor * @param cursor the cursor
* @return the list of resource templates result. * @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, * List Changed Notification. When the list of available resources changes, servers
* servers that declared the listChanged capability SHOULD send a notification: * that declared the listChanged capability SHOULD send a notification:
*/ */
public Mono<Void> sendResourcesListChanged() { public Mono<Void> sendResourcesListChanged() {
return this.sendNotification("notifications/resources/list_changed"); return this.sendNotification("notifications/resources/list_changed");
} }
/** /**
* Subscriptions. The protocol supports optional subscriptions to resource * Subscriptions. The protocol supports optional subscriptions to resource changes.
* changes. * Clients can subscribe to specific resources and receive notifications when they
* Clients can subscribe to specific resources and receive notifications when * change.
* they change.
* *
* Send a resources/subscribe request. * Send a resources/subscribe request.
* * @param subscribeRequest the subscribe request contains the uri of the resource to
* @param subscribeRequest the subscribe request contains the uri of the * subscribe to.
* resource to subscribe to.
*/ */
public Mono<Void> subscribeResource(McpSchema.SubscribeRequest subscribeRequest) { public Mono<Void> subscribeResource(McpSchema.SubscribeRequest subscribeRequest) {
return this.sendRequest("resources/subscribe", subscribeRequest, VOID_TYPE_REFERENCE); return this.sendRequest("resources/subscribe", subscribeRequest, VOID_TYPE_REFERENCE);
@ -207,9 +213,8 @@ public class McpAsyncClient extends DefaultMcpSession {
/** /**
* Send a resources/unsubscribe request. * Send a resources/unsubscribe request.
* * @param unsubscribeRequest the unsubscribe request contains the uri of the resource
* @param unsubscribeRequest the unsubscribe request contains the uri of the * to unsubscribe from.
* resource to unsubscribe from.
*/ */
public Mono<Void> unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) { public Mono<Void> unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) {
return this.sendRequest("resources/unsubscribe", unsubscribeRequest, VOID_TYPE_REFERENCE); return this.sendRequest("resources/unsubscribe", unsubscribeRequest, VOID_TYPE_REFERENCE);
@ -220,12 +225,12 @@ public class McpAsyncClient extends DefaultMcpSession {
// -------------------------- // --------------------------
private static TypeReference<McpSchema.ListPromptsResult> LIST_PROMPTS_RESULT_TYPE_REF = new TypeReference<McpSchema.ListPromptsResult>() { private static TypeReference<McpSchema.ListPromptsResult> LIST_PROMPTS_RESULT_TYPE_REF = new TypeReference<McpSchema.ListPromptsResult>() {
}; };
private static TypeReference<McpSchema.GetPromptResult> GET_PROMPT_RESULT_TYPE_REF = new TypeReference<McpSchema.GetPromptResult>() { private static TypeReference<McpSchema.GetPromptResult> GET_PROMPT_RESULT_TYPE_REF = new TypeReference<McpSchema.GetPromptResult>() {
}; };
public Mono<ListPromptsResult> listPrompts(String cursor) { public Mono<ListPromptsResult> listPrompts(String cursor) {
return this return this.sendRequest("prompts/list", new PaginatedRequest(cursor), LIST_PROMPTS_RESULT_TYPE_REF);
.sendRequest("prompts/list", new PaginatedRequest(cursor), LIST_PROMPTS_RESULT_TYPE_REF);
} }
public Mono<GetPromptResult> getPrompt(GetPromptRequest getPromptRequest) { public Mono<GetPromptResult> getPrompt(GetPromptRequest getPromptRequest) {
@ -233,10 +238,8 @@ public class McpAsyncClient extends DefaultMcpSession {
} }
/** /**
* (Server) An optional notification from the server to the client, informing it * (Server) An optional notification from the server to the client, informing it that
* that * the list of prompts it offers has changed. This may be issued by servers without
* the list of prompts it offers has changed. This may be issued by servers
* without
* any previous subscription from the client. * any previous subscription from the client.
*/ */
public Mono<Void> promptListChangedNotification() { public Mono<Void> promptListChangedNotification() {

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * 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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.ai.mcp.client; package org.springframework.ai.mcp.client;
import java.time.Duration; import java.time.Duration;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.mcp.spec.McpTransport; import org.springframework.ai.mcp.spec.McpTransport;
/** /**
* Factory class providing static methods for creating Model Context Protocol (MCP) clients. * Factory class providing static methods for creating Model Context Protocol (MCP)
* This class serves as the main entry point for establishing connections with MCP servers, * clients. This class serves as the main entry point for establishing connections with
* offering both synchronous and asynchronous client implementations. * MCP servers, offering both synchronous and asynchronous client implementations.
* *
* <p>The class provides factory methods to create either: * <p>
* The class provides factory methods to create either:
* <ul> * <ul>
* <li>{@link McpAsyncClient} for non-blocking operations * <li>{@link McpAsyncClient} for non-blocking operations
* <li>{@link McpSyncClient} for blocking operations * <li>{@link McpSyncClient} for blocking operations
* </ul> * </ul>
* *
* <p>Each client type can be instantiated with default settings or with custom configuration * <p>
* Each client type can be instantiated with default settings or with custom configuration
* including request timeout and JSON object mapping. * including request timeout and JSON object mapping.
* *
* <p>Future implementations will introduce a builder pattern for more flexible client configuration. * <p>
* Future implementations will introduce a builder pattern for more flexible client
* configuration.
* *
* @author Christian Tzolov * @author Christian Tzolov
* @author Dariusz Jędrzejczyk * @author Dariusz Jędrzejczyk
*/ */
public class McpClient { public class McpClient {
/** /**
* Private constructor to prevent instantiation as this is a utility class * Private constructor to prevent instantiation as this is a utility class containing
* containing only static factory methods. * only static factory methods.
*/ */
private McpClient() { private McpClient() {
} }
// TODO: introduce a builder like: // TODO: introduce a builder like:
// McpClient.using(transport) // McpClient.using(transport)
// .withRequestTimeout(Duration.ofSeconds(5)) // .withRequestTimeout(Duration.ofSeconds(5))
// .withObjectMapper(objectMapper); <- or even a more sophisticated // .withObjectMapper(objectMapper); <- or even a more sophisticated
// JSONtoPOJOCodec type // JSONtoPOJOCodec type
// .sync(); // .sync();
/** /**
* Creates an asynchronous MCP client with default configuration. * Creates an asynchronous MCP client with default configuration.
* * @param transport The transport layer implementation for MCP communication
* @param transport The transport layer implementation for MCP communication * @return A new instance of {@link McpAsyncClient}
* @return A new instance of {@link McpAsyncClient} */
*/ public static McpAsyncClient async(McpTransport transport) {
public static McpAsyncClient async(McpTransport transport) { return new McpAsyncClient(transport);
return new McpAsyncClient(transport); }
}
/** /**
* Creates an asynchronous MCP client with custom configuration. * Creates an asynchronous MCP client with custom configuration.
* * @param transport The transport layer implementation for MCP communication
* @param transport The transport layer implementation for MCP communication * @param requestTimeout The duration to wait before timing out requests
* @param requestTimeout The duration to wait before timing out requests * @param objectMapper The Jackson object mapper for JSON
* @param objectMapper The Jackson object mapper for JSON serialization/deserialization * serialization/deserialization
* @return A new instance of {@link McpAsyncClient} * @return A new instance of {@link McpAsyncClient}
*/ */
public static McpAsyncClient async(McpTransport transport, Duration requestTimeout, public static McpAsyncClient async(McpTransport transport, Duration requestTimeout, ObjectMapper objectMapper) {
ObjectMapper objectMapper) { return new McpAsyncClient(transport, requestTimeout, objectMapper);
return new McpAsyncClient(transport, requestTimeout, objectMapper); }
}
/** /**
* Creates a synchronous MCP client with default configuration. * Creates a synchronous MCP client with default configuration. This method wraps an
* This method wraps an asynchronous client to provide synchronous operations. * asynchronous client to provide synchronous operations.
* * @param transport The transport layer implementation for MCP communication
* @param transport The transport layer implementation for MCP communication * @return A new instance of {@link McpSyncClient}
* @return A new instance of {@link McpSyncClient} */
*/ public static McpSyncClient sync(McpTransport transport) {
public static McpSyncClient sync(McpTransport transport) { return new McpSyncClient(async(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));
}
} }

View file

@ -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; package org.springframework.ai.mcp.client;
import java.time.Duration; import java.time.Duration;
@ -32,8 +48,7 @@ public class McpSyncClient implements AutoCloseable {
} }
/** /**
* The initialization phase MUST be the first interaction between client and * The initialization phase MUST be the first interaction between client and server.
* server.
* During this phase, the client and server: * During this phase, the client and server:
* <ul> * <ul>
* <li>Establish protocol version compatibility</li> * <li>Establish protocol version compatibility</li>
@ -41,8 +56,7 @@ public class McpSyncClient implements AutoCloseable {
* <li>Share implementation details</li> * <li>Share implementation details</li>
* </ul> * </ul>
* <br/> * <br/>
* The client MUST initiate this phase by sending an initialize request * The client MUST initiate this phase by sending an initialize request containing:
* containing:
* <ul> * <ul>
* <li>The protocol version the client supports</li> * <li>The protocol version the client supports</li>
* <li>The client's capabilities</li> * <li>The client's capabilities</li>
@ -51,8 +65,7 @@ public class McpSyncClient implements AutoCloseable {
* *
* The server MUST respond with its own capabilities and information: * The server MUST respond with its own capabilities and information:
* {@link McpSchema.ServerCapabilities}. <br/> * {@link McpSchema.ServerCapabilities}. <br/>
* After successful initialization, the client MUST send an initialized * After successful initialization, the client MUST send an initialized notification
* notification
* to indicate it is ready to begin normal operations. * to indicate it is ready to begin normal operations.
* *
* <br/> * <br/>
@ -60,7 +73,6 @@ public class McpSyncClient implements AutoCloseable {
* <a href= * <a href=
* "https://github.com/modelcontextprotocol/specification/blob/main/docs/specification/basic/lifecycle.md#initialization">Initialization * "https://github.com/modelcontextprotocol/specification/blob/main/docs/specification/basic/lifecycle.md#initialization">Initialization
* Spec</a> * Spec</a>
*
* @return the initialize result. * @return the initialize result.
*/ */
public McpSchema.InitializeResult initialize() { public McpSchema.InitializeResult initialize() {
@ -88,7 +100,6 @@ public class McpSyncClient implements AutoCloseable {
// -------------------------- // --------------------------
/** /**
* Send a tools/call request. * Send a tools/call request.
*
* @param callToolRequest the call tool request. * @param callToolRequest the call tool request.
* @return the call tool result. * @return the call tool result.
*/ */
@ -98,7 +109,6 @@ public class McpSyncClient implements AutoCloseable {
/** /**
* Send a tools/list request. * Send a tools/list request.
*
* @return the list of tools result. * @return the list of tools result.
*/ */
public McpSchema.ListToolsResult listTools(String cursor) { public McpSchema.ListToolsResult listTools(String cursor) {
@ -111,7 +121,6 @@ public class McpSyncClient implements AutoCloseable {
/** /**
* Send a resources/list request. * Send a resources/list request.
*
* @param cursor the cursor * @param cursor the cursor
* @return the list of resources result. * @return the list of resources result.
*/ */
@ -121,7 +130,6 @@ public class McpSyncClient implements AutoCloseable {
/** /**
* Send a resources/read request. * Send a resources/read request.
*
* @param resource the resource to read * @param resource the resource to read
* @return the resource content. * @return the resource content.
*/ */
@ -131,7 +139,6 @@ public class McpSyncClient implements AutoCloseable {
/** /**
* Send a resources/read request. * Send a resources/read request.
*
* @param readResourceRequest the read resource request. * @param readResourceRequest the read resource request.
* @return the resource content. * @return the resource content.
*/ */
@ -144,7 +151,6 @@ public class McpSyncClient implements AutoCloseable {
* templates. Arguments may be auto-completed through the completion API. * templates. Arguments may be auto-completed through the completion API.
* *
* Request a list of resource templates the server has. * Request a list of resource templates the server has.
*
* @param cursor the cursor * @param cursor the cursor
* @return the list of resource templates result. * @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, * List Changed Notification. When the list of available resources changes, servers
* servers
* that declared the listChanged capability SHOULD send a notification: * that declared the listChanged capability SHOULD send a notification:
*/ */
public void sendResourcesListChanged() { public void sendResourcesListChanged() {
@ -162,14 +167,13 @@ public class McpSyncClient implements AutoCloseable {
} }
/** /**
* Subscriptions. The protocol supports optional subscriptions to resource * Subscriptions. The protocol supports optional subscriptions to resource changes.
* changes. Clients can subscribe to specific resources and receive * Clients can subscribe to specific resources and receive notifications when they
* notifications when they change. * change.
* *
* Send a resources/subscribe request. * Send a resources/subscribe request.
* * @param subscribeRequest the subscribe request contains the uri of the resource to
* @param subscribeRequest the subscribe request contains the uri of the * subscribe to.
* resource to subscribe to.
*/ */
public void subscribeResource(McpSchema.SubscribeRequest subscribeRequest) { public void subscribeResource(McpSchema.SubscribeRequest subscribeRequest) {
this.delegate.subscribeResource(subscribeRequest).block(); this.delegate.subscribeResource(subscribeRequest).block();
@ -177,10 +181,8 @@ public class McpSyncClient implements AutoCloseable {
/** /**
* Send a resources/unsubscribe request. * Send a resources/unsubscribe request.
* * @param unsubscribeRequest the unsubscribe request contains the uri of the resource
* @param unsubscribeRequest the unsubscribe request contains the uri of the * to unsubscribe from.
* resource
* to unsubscribe from.
*/ */
public void unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) { public void unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) {
this.delegate.unsubscribeResource(unsubscribeRequest).block(); 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 * (Server) An optional notification from the server to the client, informing it that
* that the list of prompts it offers has changed. This may be issued by servers * the list of prompts it offers has changed. This may be issued by servers without
* without any previous subscription from the client. * any previous subscription from the client.
*/ */
public void promptListChangedNotification() { public void promptListChangedNotification() {
this.delegate.promptListChangedNotification().block(); this.delegate.promptListChangedNotification().block();
} }
} }

View file

@ -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; package org.springframework.ai.mcp.client.stdio;
import java.util.ArrayList; import java.util.ArrayList;
@ -9,6 +25,7 @@ import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.mcp.client.util.Assert; import org.springframework.ai.mcp.client.util.Assert;
/** /**
@ -22,11 +39,11 @@ public class ServerParameters {
// Environment variables to inherit by default // Environment variables to inherit by default
private static final List<String> DEFAULT_INHERITED_ENV_VARS = System.getProperty("os.name") private static final List<String> DEFAULT_INHERITED_ENV_VARS = System.getProperty("os.name")
.toLowerCase() .toLowerCase()
.contains("win") .contains("win")
? Arrays.asList("APPDATA", "HOMEDRIVE", "HOMEPATH", "LOCALAPPDATA", "PATH", ? Arrays.asList("APPDATA", "HOMEDRIVE", "HOMEPATH", "LOCALAPPDATA", "PATH", "PROCESSOR_ARCHITECTURE",
"PROCESSOR_ARCHITECTURE", "SYSTEMDRIVE", "SYSTEMROOT", "TEMP", "USERNAME", "USERPROFILE") "SYSTEMDRIVE", "SYSTEMROOT", "TEMP", "USERNAME", "USERPROFILE")
: Arrays.asList("HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER"); : Arrays.asList("HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER");
@JsonProperty("command") @JsonProperty("command")
private String command; private String command;
@ -117,17 +134,17 @@ public class ServerParameters {
} }
/** /**
* Returns a default environment object including only environment variables * Returns a default environment object including only environment variables deemed
* deemed safe to inherit. * safe to inherit.
*/ */
private static Map<String, String> getDefaultEnvironment() { private static Map<String, String> getDefaultEnvironment() {
return System.getenv() return System.getenv()
.entrySet() .entrySet()
.stream() .stream()
.filter(entry -> DEFAULT_INHERITED_ENV_VARS.contains(entry.getKey())) .filter(entry -> DEFAULT_INHERITED_ENV_VARS.contains(entry.getKey()))
.filter(entry -> entry.getValue() != null) .filter(entry -> entry.getValue() != null)
.filter(entry -> !entry.getValue().startsWith("()")) .filter(entry -> !entry.getValue().startsWith("()"))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
} }
} }

View file

@ -1,26 +1,28 @@
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.ai.mcp.client.stdio; package org.springframework.ai.mcp.client.stdio;
import java.util.Map;
import java.util.List; import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.mcp.client.stdio.ServerParametersParser.McpServerConfigurations.McpServerConfiguration; import org.springframework.ai.mcp.client.stdio.ServerParametersParser.McpServerConfigurations.McpServerConfiguration;
/** /**

View file

@ -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; package org.springframework.ai.mcp.client.stdio;
import java.io.BufferedReader; import java.io.BufferedReader;
@ -13,6 +29,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers; import reactor.core.scheduler.Schedulers;
import org.springframework.ai.mcp.client.util.Assert; import org.springframework.ai.mcp.client.util.Assert;
import org.springframework.ai.mcp.spec.AbstractMcpTransport; import org.springframework.ai.mcp.spec.AbstractMcpTransport;
import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage; import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage;
@ -34,11 +51,15 @@ public class StdioServerTransport extends AbstractMcpTransport {
private Process process; private Process process;
private BufferedReader processErrorReader; private BufferedReader processErrorReader;
private BufferedReader processReader; private BufferedReader processReader;
private BufferedWriter processWriter; private BufferedWriter processWriter;
private Scheduler inboundScheduler; private Scheduler inboundScheduler;
private Scheduler outboundScheduler; private Scheduler outboundScheduler;
private Scheduler errorScheduler; private Scheduler errorScheduler;
volatile boolean isRunning; volatile boolean isRunning;
@ -59,12 +80,9 @@ public class StdioServerTransport extends AbstractMcpTransport {
this.params = params; this.params = params;
// Start threads // Start threads
this.inboundScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), this.inboundScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), "inbound");
"inbound"); this.outboundScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), "outbound");
this.outboundScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), this.errorScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), "error");
"outbound");
this.errorScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(),
"error");
} }
@Override @Override
@ -81,7 +99,8 @@ public class StdioServerTransport extends AbstractMcpTransport {
// Start the process // Start the process
try { try {
this.process = processBuilder.start(); this.process = processBuilder.start();
} catch (IOException e) { }
catch (IOException e) {
throw new RuntimeException("Failed to start process with command: " + fullCommand, e); throw new RuntimeException("Failed to start process with command: " + fullCommand, e);
} }
@ -110,7 +129,8 @@ public class StdioServerTransport extends AbstractMcpTransport {
public void awaitForExit() { public void awaitForExit() {
try { try {
this.process.waitFor(); this.process.waitFor();
} catch (InterruptedException e) { }
catch (InterruptedException e) {
throw new RuntimeException("Process interrupted", e); throw new RuntimeException("Process interrupted", e);
} }
} }
@ -124,15 +144,18 @@ public class StdioServerTransport extends AbstractMcpTransport {
System.out.println("Received error line: " + line); System.out.println("Received error line: " + line);
// TODO: handle errors, etc. // TODO: handle errors, etc.
this.getErrorSink().tryEmitNext(line); this.getErrorSink().tryEmitNext(line);
} catch (Exception e) { }
catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
} catch (IOException e) { }
catch (IOException e) {
if (this.isRunning) { if (this.isRunning) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} finally { }
finally {
this.isRunning = false; this.isRunning = false;
} }
}); });
@ -149,15 +172,18 @@ public class StdioServerTransport extends AbstractMcpTransport {
// TODO: Back off, reschedule, give up? // TODO: Back off, reschedule, give up?
throw new RuntimeException("Failed to enqueue message"); throw new RuntimeException("Failed to enqueue message");
} }
} catch (Exception e) { }
catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
} catch (IOException e) { }
catch (IOException e) {
if (isRunning) { if (isRunning) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} finally { }
finally {
isRunning = false; isRunning = false;
} }
}); });
@ -170,9 +196,11 @@ public class StdioServerTransport extends AbstractMcpTransport {
// Determine message type based on specific JSON structure // Determine message type based on specific JSON structure
if (map.containsKey("method") && map.containsKey("id")) { if (map.containsKey("method") && map.containsKey("id")) {
return this.objectMapper.convertValue(map, JSONRPCRequest.class); 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); 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); return this.objectMapper.convertValue(map, JSONRPCResponse.class);
} }
@ -181,23 +209,24 @@ public class StdioServerTransport extends AbstractMcpTransport {
private void startOutboundProcessing() { private void startOutboundProcessing() {
this.getOutboundSink() this.getOutboundSink()
.asFlux() .asFlux()
// this bit is important since writes come from user threads and we // this bit is important since writes come from user threads and we
// want to ensure that the actual writing happens on a dedicated thread // want to ensure that the actual writing happens on a dedicated thread
.publishOn(outboundScheduler) .publishOn(outboundScheduler)
.handle((message, s) -> { .handle((message, s) -> {
if (message != null) { if (message != null) {
try { try {
this.processWriter.write(objectMapper.writeValueAsString(message)); this.processWriter.write(objectMapper.writeValueAsString(message));
this.processWriter.newLine(); this.processWriter.newLine();
this.processWriter.flush(); this.processWriter.flush();
s.next(message); s.next(message);
} catch (IOException e) {
s.error(new RuntimeException(e));
}
} }
}) catch (IOException e) {
.subscribe(); s.error(new RuntimeException(e));
}
}
})
.subscribe();
} }
@Override @Override
@ -205,41 +234,40 @@ public class StdioServerTransport extends AbstractMcpTransport {
this.isRunning = false; this.isRunning = false;
return Mono.whenDelayError( return Mono.whenDelayError(Mono.fromRunnable(() -> {
Mono.fromRunnable(() -> { try {
try { this.processErrorReader.close();
this.processErrorReader.close(); }
} catch (IOException e) { catch (IOException e) {
throw new RuntimeException(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 { .then()
this.processReader.close(); .subscribeOn(Schedulers.boundedElastic());
} 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());
} }
} }

View file

@ -1,18 +1,19 @@
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.ai.mcp.client.util; package org.springframework.ai.mcp.client.util;
import reactor.util.annotation.Nullable; import reactor.util.annotation.Nullable;
@ -23,26 +24,26 @@ import reactor.util.annotation.Nullable;
*/ */
public class Assert { public class Assert {
/**
* Assert that an object is not {@code null}.
*
* <pre class="code">
* Assert.notNull(clazz, "The class must not be null");
* </pre>
*
* @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 * Assert that an object is not {@code null}.
* be {@code null} and must contain at least one non-whitespace character. *
* <pre class="code">
* Assert.notNull(clazz, "The class must not be null");
* </pre>
* @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.
* <pre class="code">Assert.hasText(name, "'name' must not be empty");</pre> * <pre class="code">Assert.hasText(name, "'name' must not be empty");</pre>
* @param text the String to check * @param text the String to check
* @param message the exception message to use if the assertion fails * @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 <em>text</em>. * Check whether the given {@code String} contains actual <em>text</em>.
* <p>More specifically, this method returns {@code true} if the * <p>
* {@code String} is not {@code null}, its length is greater than 0, * More specifically, this method returns {@code true} if the {@code String} is not
* and it contains at least one non-whitespace character. * {@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}) * @param str the {@code String} to check (may be {@code null})
* @return {@code true} if the {@code String} is not {@code null}, its * @return {@code true} if the {@code String} is not {@code null}, its length is
* length is greater than 0, and it does not contain whitespace only * greater than 0, and it does not contain whitespace only
* @see #hasText(CharSequence) * @see #hasText(CharSequence)
* @see #hasLength(String) * @see #hasLength(String)
* @see Character#isWhitespace * @see Character#isWhitespace
@ -71,5 +73,4 @@ public class Assert {
return (str != null && !str.isBlank()); return (str != null && !str.isBlank());
} }
} }

View file

@ -1,18 +1,19 @@
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.ai.mcp.spec; package org.springframework.ai.mcp.spec;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -20,6 +21,7 @@ import java.util.function.Consumer;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks; import reactor.core.publisher.Sinks;
import org.springframework.ai.mcp.client.util.Assert; import org.springframework.ai.mcp.client.util.Assert;
import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage; import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage;
@ -32,11 +34,14 @@ public abstract class AbstractMcpTransport implements McpTransport {
protected final ObjectMapper objectMapper; protected final ObjectMapper objectMapper;
private final Sinks.Many<String> errorSink; private final Sinks.Many<String> errorSink;
private final Sinks.Many<JSONRPCMessage> inboundSink; private final Sinks.Many<JSONRPCMessage> inboundSink;
private final Sinks.Many<JSONRPCMessage> outboundSink; private final Sinks.Many<JSONRPCMessage> outboundSink;
private Consumer<JSONRPCMessage> inboundMessageHandler = message -> System.out private Consumer<JSONRPCMessage> inboundMessageHandler = message -> System.out
.println("Received message: " + message); .println("Received message: " + message);
private Consumer<String> errorHandler = error -> System.err.println("Received error: " + error); private Consumer<String> errorHandler = error -> System.err.println("Received error: " + error);
public AbstractMcpTransport() { public AbstractMcpTransport() {
@ -66,8 +71,7 @@ public abstract class AbstractMcpTransport implements McpTransport {
} }
private void handleIncomingMessages() { private void handleIncomingMessages() {
this.inboundSink.asFlux() this.inboundSink.asFlux().subscribe(message -> this.inboundMessageHandler.accept(message));
.subscribe(message -> this.inboundMessageHandler.accept(message));
} }
private void handleIncomingErrors() { 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 // In this approach we delegate the retry and the backpressure onto the
// caller. This might be enough for most cases. // caller. This might be enough for most cases.
return Mono.empty(); return Mono.empty();
} else { }
else {
return Mono.error(new RuntimeException("Failed to enqueue message")); return Mono.error(new RuntimeException("Failed to enqueue message"));
} }
} }
} }

View file

@ -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; package org.springframework.ai.mcp.spec;
import java.time.Duration; import java.time.Duration;
@ -9,6 +25,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink; import reactor.core.publisher.MonoSink;
import org.springframework.ai.mcp.client.util.Assert; import org.springframework.ai.mcp.client.util.Assert;
/** /**
@ -39,16 +56,19 @@ public class DefaultMcpSession implements McpSession {
this.transport.setInboudMessageHandler(message -> { this.transport.setInboudMessageHandler(message -> {
if ( message instanceof McpSchema.JSONRPCResponse response) { if (message instanceof McpSchema.JSONRPCResponse response) {
var sink = pendingResponses.remove(response.id()); var sink = pendingResponses.remove(response.id());
if (sink == null) { if (sink == null) {
System.out.println("Unexpected response for unkown id " + response.id()); System.out.println("Unexpected response for unkown id " + response.id());
} else { }
else {
sink.success(response); 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"); 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"); 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, // TODO: This is non-blocking, but it's actually a synchronous call,
// perhaps there's no need to make it return Mono? // perhaps there's no need to make it return Mono?
this.transport.sendMessage(jsonrpcRequest) this.transport.sendMessage(jsonrpcRequest)
// TODO: It's most efficient to create a dedicated // TODO: It's most efficient to create a dedicated
// Subscriber here // Subscriber here
.subscribe(v -> { .subscribe(v -> {
}, e -> { }, e -> {
this.pendingResponses.remove(requestId); this.pendingResponses.remove(requestId);
sink.error(e); sink.error(e);
}); });
} catch (Exception e) { }
catch (Exception e) {
sink.error(e); sink.error(e);
} }
}).timeout(this.requestTimeout).handle((jsonRpcResponse, s) -> { }).timeout(this.requestTimeout).handle((jsonRpcResponse, s) -> {
if (jsonRpcResponse.error() != null) { if (jsonRpcResponse.error() != null) {
s.error(new McpError(jsonRpcResponse.error())); s.error(new McpError(jsonRpcResponse.error()));
} else { }
else {
if (typeRef.getType().getTypeName().equals("java.lang.Void")) { if (typeRef.getType().getTypeName().equals("java.lang.Void")) {
s.complete(); s.complete();
} else { }
else {
s.next(this.objectMapper.convertValue(jsonRpcResponse.result(), typeRef)); s.next(this.objectMapper.convertValue(jsonRpcResponse.result(), typeRef));
} }
} }
@ -99,7 +122,8 @@ public class DefaultMcpSession implements McpSession {
method, params); method, params);
try { try {
this.transport.sendMessage(jsonrpcNotification); this.transport.sendMessage(jsonrpcNotification);
} catch (Exception e) { }
catch (Exception e) {
return Mono.error(new McpError(e)); return Mono.error(new McpError(e));
} }
return Mono.empty(); return Mono.empty();

View file

@ -1,18 +1,19 @@
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.ai.mcp.spec; package org.springframework.ai.mcp.spec;
import java.util.List; 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 // Resource Interfaces
// --------------------------- // ---------------------------
/** /**
* Base for objects that include optional annotations for the client. The client * Base for objects that include optional annotations for the client. The client can
* can
* use annotations to inform how objects are used or displayed * use annotations to inform how objects are used or displayed
*/ */
public interface Annotated { public interface Annotated {
@ -189,18 +190,13 @@ public class McpSchema {
} }
/** /**
* @param audience Describes who the intended customer of this object or data * @param audience Describes who the intended customer of this object or data is. It
* is. It * can include multiple entries to indicate content useful for multiple audiences
* can include multiple entries to indicate content useful for * (e.g., `["user", "assistant"]`).
* multiple audiences * @param priority Describes how important this data is for operating the server. A
* (e.g., `["user", "assistant"]`). * value of 1 means "most important," and indicates that the data is effectively
* @param priority Describes how important this data is for operating the * required, while 0 means "least important," and indicates that the data is entirely
* server. A * optional. It is a number between 0 and 1.
* 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) @JsonInclude(JsonInclude.Include.NON_ABSENT)
public record Annotations( // @formatter:off public record Annotations( // @formatter:off
@ -211,16 +207,13 @@ public class McpSchema {
/** /**
* A known resource that the server is capable of reading. * A known resource that the server is capable of reading.
* *
* @param uri the URI of the resource. * @param uri the URI of the resource.
* @param name A human-readable name for this resource. This can be used * @param name A human-readable name for this resource. This can be used by clients to
* by clients to * populate UI elements.
* populate UI elements. * @param description A description of what this resource represents. This can be used
* @param description A description of what this resource represents. This can * by clients to improve the LLM's understanding of available resources. It can be
* be used * thought of like a "hint" to the model.
* by clients to improve the LLM's understanding of available * @param mimeType The MIME type of this resource, if known.
* 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) @JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@ -264,13 +257,11 @@ public class McpSchema {
} // @formatter:on } // @formatter:on
/** /**
* Sent from the client to request resources/updated notifications from the * Sent from the client to request resources/updated notifications from the server
* server
* whenever a particular resource changes. * whenever a particular resource changes.
* *
* @param uri the URI of the resource to subscribe to. The URI can use any * @param uri the URI of the resource to subscribe to. The URI can use any protocol;
* protocol; * it is up to the server how to interpret it.
* it is up to the server how to interpret it.
*/ */
@JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonInclude(JsonInclude.Include.NON_ABSENT)
public record SubscribeRequest( // @formatter:off public record SubscribeRequest( // @formatter:off
@ -292,14 +283,12 @@ public class McpSchema {
/** /**
* The URI of this resource. * The URI of this resource.
*
* @return the URI of this resource. * @return the URI of this resource.
*/ */
String uri(); String uri();
/** /**
* The MIME type of this resource. * The MIME type of this resource.
*
* @return the MIME type of this resource. * @return the MIME type of this resource.
*/ */
String mimeType(); String mimeType();
@ -309,11 +298,10 @@ public class McpSchema {
/** /**
* Text contents of a resource. * 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 mimeType the MIME type of this resource.
* @param text the text of the resource. This must only be set if the * @param text the text of the resource. This must only be set if the resource can
* resource can * actually be represented as text (not binary data).
* actually be represented as text (not binary data).
*/ */
@JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonInclude(JsonInclude.Include.NON_ABSENT)
public record TextResourceContents( // @formatter:off public record TextResourceContents( // @formatter:off
@ -325,13 +313,11 @@ public class McpSchema {
/** /**
* Binary contents of a resource. * 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 mimeType the MIME type of this resource.
* @param blob a base64-encoded string representing the binary data of the * @param blob a base64-encoded string representing the binary data of the resource.
* resource. * This must only be set if the resource can actually be represented as binary data
* This must only be set if the resource can actually be * (not text).
* represented as binary data
* (not text).
*/ */
@JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonInclude(JsonInclude.Include.NON_ABSENT)
public record BlobResourceContents( // @formatter:off public record BlobResourceContents( // @formatter:off
@ -346,9 +332,9 @@ public class McpSchema {
/** /**
* A prompt or prompt template that the server offers. * 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 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) @JsonInclude(JsonInclude.Include.NON_ABSENT)
public record Prompt( // @formatter:off public record Prompt( // @formatter:off
@ -360,9 +346,9 @@ public class McpSchema {
/** /**
* Describes an argument that a prompt can accept. * 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 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) @JsonInclude(JsonInclude.Include.NON_ABSENT)
public record PromptArgument( // @formatter:off public record PromptArgument( // @formatter:off
@ -374,12 +360,10 @@ public class McpSchema {
/** /**
* Describes a message returned as part of a prompt. * Describes a message returned as part of a prompt.
* *
* This is similar to `SamplingMessage`, but also supports the embedding of * This is similar to `SamplingMessage`, but also supports the embedding of resources
* resources
* from the MCP server. * from the MCP server.
* *
* @param role The sender or recipient of messages and data in a * @param role The sender or recipient of messages and data in a conversation.
* conversation.
* @param content The content of the message of type {@link Content}. * @param content The content of the message of type {@link Content}.
*/ */
@JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonInclude(JsonInclude.Include.NON_ABSENT)
@ -400,7 +384,7 @@ public class McpSchema {
/** /**
* Used by the client to get a prompt provided by the server. * 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. * @param arguments Arguments to use for templating the prompt.
*/ */
@JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonInclude(JsonInclude.Include.NON_ABSENT)

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * 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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.ai.mcp.spec; package org.springframework.ai.mcp.spec;
import java.time.Duration; import java.time.Duration;
@ -23,65 +24,72 @@ import reactor.core.publisher.Mono;
/** /**
* Represents a Model Control Protocol (MCP) session that handles communication between * Represents a Model Control Protocol (MCP) session that handles communication between
* clients and the server. This interface provides methods for sending requests * clients and the server. This interface provides methods for sending requests and
* and notifications, as well as managing the session lifecycle. * notifications, as well as managing the session lifecycle.
* *
* <p>The session operates asynchronously using Project Reactor's {@link Mono} type * <p>
* for non-blocking operations. It supports both request-response patterns and * The session operates asynchronously using Project Reactor's {@link Mono} type for
* one-way notifications.</p> * non-blocking operations. It supports both request-response patterns and one-way
* notifications.
* </p>
* *
* @author Christian Tzolov * @author Christian Tzolov
* @author Dariusz Jędrzejczyk * @author Dariusz Jędrzejczyk
*/ */
public interface McpSession { public interface McpSession {
/** /**
* Sends a request to the model server and expects a response of type T. * Sends a request to the model server and expects a response of type T.
* *
* <p>This method handles the request-response pattern where a response is expected * <p>
* from the server. The response type is determined by the provided TypeReference.</p> * 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 <T> the type of the expected response * </p>
* @param method the name of the method to be called on the server * @param <T> the type of the expected response
* @param requestParams the parameters to be sent with the request * @param method the name of the method to be called on the server
* @param typeRef the TypeReference describing the expected response type * @param requestParams the parameters to be sent with the request
* @return a Mono that will emit the response when received * @param typeRef the TypeReference describing the expected response type
*/ * @return a Mono that will emit the response when received
<T> Mono<T> sendRequest(String method, Object requestParams, TypeReference<T> typeRef); */
<T> Mono<T> sendRequest(String method, Object requestParams, TypeReference<T> typeRef);
/** /**
* Sends a notification to the model server without parameters. * Sends a notification to the model server without parameters.
* *
* <p>This method implements the notification pattern where no response is expected * <p>
* from the server. It's useful for fire-and-forget scenarios.</p> * 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 * </p>
* @return a Mono that completes when the notification has been sent * @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<Void> sendNotification(String method) { default Mono<Void> sendNotification(String method) {
return sendNotification(method, null); return sendNotification(method, null);
} }
/** /**
* Sends a notification to the model server with parameters. * Sends a notification to the model server with parameters.
* *
* <p>Similar to {@link #sendNotification(String)} but allows sending additional * <p>
* parameters with the notification.</p> * 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 * </p>
* @param params a map of parameters to be sent with the notification * @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 * @param params a map of parameters to be sent with the notification
*/ * @return a Mono that completes when the notification has been sent
Mono<Void> sendNotification(String method, Map<String, Object> params); */
Mono<Void> sendNotification(String method, Map<String, Object> params);
/**
* Gracefully closes the session with a specified timeout.
*
* <p>
* This method attempts to close the session cleanly, waiting for any pending
* operations to complete up to the specified timeout duration.
* </p>
* @param timeout the maximum duration to wait for the session to close
* @return a Mono that completes when the session has been closed
*/
Mono<Void> closeGracefully(Duration timeout);
/**
* Gracefully closes the session with a specified timeout.
*
* <p>This method attempts to close the session cleanly, waiting for any pending
* operations to complete up to the specified timeout duration.</p>
*
* @param timeout the maximum duration to wait for the session to close
* @return a Mono that completes when the session has been closed
*/
Mono<Void> closeGracefully(Duration timeout);
} }

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -13,23 +13,22 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.ai.mcp.spec; package org.springframework.ai.mcp.spec;
import java.util.function.Consumer; import java.util.function.Consumer;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage; import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage;
/** /**
* Defines the asynchronous transport layer for the Model Context Protocol * Defines the asynchronous transport layer for the Model Context Protocol (MCP).
* (MCP).
* *
* <p> * <p>
* The McpTransport interface provides the foundation for implementing custom * The McpTransport interface provides the foundation for implementing custom transport
* transport * mechanisms in the Model Context Protocol. It handles the bidirectional communication
* mechanisms in the Model Context Protocol. It handles the bidirectional * between the client and server components, supporting asynchronous message exchange
* communication between
* the client and server components, supporting asynchronous message exchange
* using JSON-RPC format. * using JSON-RPC format.
* </p> * </p>
* *
@ -44,8 +43,7 @@ import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage;
* *
* <p> * <p>
* The transport layer is designed to be protocol-agnostic, allowing for various * The transport layer is designed to be protocol-agnostic, allowing for various
* implementations * implementations such as WebSocket, HTTP, or custom protocols.
* such as WebSocket, HTTP, or custom protocols.
* </p> * </p>
* *
* @author Christian Tzolov * @author Christian Tzolov
@ -53,78 +51,69 @@ import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage;
*/ */
public interface McpTransport { public interface McpTransport {
/** /**
* Initializes and starts the transport connection. * Initializes and starts the transport connection.
* *
* <p> * <p>
* This method should be called before any message exchange can occur. It sets * This method should be called before any message exchange can occur. It sets up the
* up * necessary resources and establishes the connection to the server.
* the necessary resources and establishes the connection to the server. * </p>
* </p> */
*/ void start();
void start();
/** /**
* Closes the transport connection and releases any associated resources. * Closes the transport connection and releases any associated resources.
* *
* <p> * <p>
* This method ensures proper cleanup of resources when the transport is no * This method ensures proper cleanup of resources when the transport is no longer
* longer needed. * needed. It should handle the graceful shutdown of any active connections.
* It should handle the graceful shutdown of any active connections. * </p>
* </p> */
*/ default void close() {
default void close() { this.closeGracefully().subscribe();
this.closeGracefully().subscribe(); }
}
/** /**
* Closes the transport connection and releases any associated resources * Closes the transport connection and releases any associated resources
* asynchronously. * asynchronously.
* * @return a {@link Mono<Void>} that completes when the connection has been closed.
* @return a {@link Mono<Void>} that completes when the connection has been */
* closed. Mono<Void> closeGracefully();
*/
Mono<Void> closeGracefully();
/** /**
* Sets the handler for processing incoming messages from the server. * Sets the handler for processing incoming messages from the server.
* *
* <p> * <p>
* The provided handler will be called whenever a new message is received from * The provided handler will be called whenever a new message is received from the
* the server. * server. Messages are expected to follow the JSON-RPC format as defined in the MCP
* Messages are expected to follow the JSON-RPC format as defined in the MCP * specification.
* specification. * </p>
* </p> * @param inboundMessageHandler a consumer that processes incoming
* * {@link JSONRPCMessage}s
* @param inboundMessageHandler a consumer that processes incoming */
* {@link JSONRPCMessage}s void setInboudMessageHandler(Consumer<JSONRPCMessage> inboundMessageHandler);
*/
void setInboudMessageHandler(Consumer<JSONRPCMessage> inboundMessageHandler);
/** /**
* Sets the handler for processing transport-level errors. * Sets the handler for processing transport-level errors.
* *
* <p> * <p>
* The provided handler will be called when errors occur during transport * The provided handler will be called when errors occur during transport operations,
* operations, * such as connection failures or protocol violations.
* such as connection failures or protocol violations. * </p>
* </p> * @param inboundErrorHandler a consumer that processes error messages
* */
* @param inboundErrorHandler a consumer that processes error messages void setInboundErrorHandler(Consumer<String> inboundErrorHandler);
*/
void setInboundErrorHandler(Consumer<String> inboundErrorHandler); /**
* Sends a message to the server asynchronously.
*
* <p>
* 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.
* </p>
* @param message the {@link JSONRPCMessage} to be sent to the server
* @return a {@link Mono<Void>} that completes when the message has been sent
*/
Mono<Void> sendMessage(JSONRPCMessage message);
/**
* Sends a message to the server asynchronously.
*
* <p>
* 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.
* </p>
*
* @param message the {@link JSONRPCMessage} to be sent to the server
* @return a {@link Mono<Void>} that completes when the message has been sent
*/
Mono<Void> sendMessage(JSONRPCMessage message);
} }

View file

@ -1,23 +1,25 @@
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.ai.mcp.attic; package org.springframework.ai.mcp.attic;
import java.time.Duration; import java.time.Duration;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.mcp.client.McpClient; import org.springframework.ai.mcp.client.McpClient;
import org.springframework.ai.mcp.client.McpSyncClient; import org.springframework.ai.mcp.client.McpSyncClient;
import org.springframework.ai.mcp.client.stdio.ServerParameters; import org.springframework.ai.mcp.client.stdio.ServerParameters;
@ -35,13 +37,11 @@ public class ClientSessionTests2 {
public static void main(String[] args) { public static void main(String[] args) {
var stdioParams = ServerParameters.builder("uv") var stdioParams = ServerParameters.builder("uv")
.args("--directory", "dir", "run", "mcp-server-sqlite", .args("--directory", "dir", "run", "mcp-server-sqlite", "--db-path", "~/test.db")
"--db-path", "~/test.db")
.build(); .build();
try (McpSyncClient clientSession = try (McpSyncClient clientSession = McpClient.sync(new StdioServerTransport(stdioParams), Duration.ofSeconds(10),
McpClient.sync(new StdioServerTransport(stdioParams), new ObjectMapper())) {
Duration.ofSeconds(10), new ObjectMapper())) {
clientSession.initialize(); clientSession.initialize();

View file

@ -1,23 +1,25 @@
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.ai.mcp.attic; package org.springframework.ai.mcp.attic;
import java.time.Duration; import java.time.Duration;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.mcp.client.McpClient; import org.springframework.ai.mcp.client.McpClient;
import org.springframework.ai.mcp.client.McpSyncClient; import org.springframework.ai.mcp.client.McpSyncClient;
import org.springframework.ai.mcp.client.stdio.ServerParameters; import org.springframework.ai.mcp.client.stdio.ServerParameters;
@ -35,16 +37,14 @@ public class ClientSessionTests3 {
public static void main(String[] args) { public static void main(String[] args) {
var stdioParams = ServerParameters.builder("uv") var stdioParams = ServerParameters.builder("uv")
.args("--directory", "dir", "run", "mcp-server-sqlite", .args("--directory", "dir", "run", "mcp-server-sqlite", "--db-path", "~/test.db")
"--db-path", "~/test.db")
.build(); .build();
McpSyncClient clientSession = null; McpSyncClient clientSession = null;
try { try {
clientSession = McpClient.sync(new StdioServerTransport(stdioParams), clientSession = McpClient.sync(new StdioServerTransport(stdioParams), Duration.ofSeconds(10),
Duration.ofSeconds(10), new ObjectMapper()); new ObjectMapper());
clientSession.initialize(); clientSession.initialize();

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * 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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.ai.mcp.client; package org.springframework.ai.mcp.client;
import java.time.Duration; 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.Timeout;
import org.springframework.ai.mcp.client.stdio.ServerParameters; import org.springframework.ai.mcp.client.stdio.ServerParameters;
import org.springframework.ai.mcp.client.stdio.StdioServerTransport; import org.springframework.ai.mcp.client.stdio.StdioServerTransport;
import org.springframework.ai.mcp.spec.McpSchema.CallToolRequest; import org.springframework.ai.mcp.spec.McpSchema.CallToolRequest;
@ -54,8 +56,8 @@ class McpAsyncClientTests {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
stdioParams = ServerParameters.builder("npx") stdioParams = ServerParameters.builder("npx")
.args("-y", "@modelcontextprotocol/server-everything", "dir") .args("-y", "@modelcontextprotocol/server-everything", "dir")
.build(); .build();
assertThatCode(() -> { assertThatCode(() -> {
mcpAsyncClient = McpClient.async(new StdioServerTransport(stdioParams), TIMEOUT, new ObjectMapper()); mcpAsyncClient = McpClient.async(new StdioServerTransport(stdioParams), TIMEOUT, new ObjectMapper());
@ -73,13 +75,13 @@ class McpAsyncClientTests {
@Test @Test
void testConstructorWithInvalidArguments() { void testConstructorWithInvalidArguments() {
assertThatThrownBy(() -> McpClient.sync(null, TIMEOUT, new ObjectMapper())) assertThatThrownBy(() -> McpClient.sync(null, TIMEOUT, new ObjectMapper()))
.isInstanceOf(IllegalArgumentException.class); .isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> McpClient.sync(new StdioServerTransport(stdioParams), null, new ObjectMapper())) assertThatThrownBy(() -> McpClient.sync(new StdioServerTransport(stdioParams), null, new ObjectMapper()))
.isInstanceOf(IllegalArgumentException.class); .isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> McpClient.sync(new StdioServerTransport(stdioParams), TIMEOUT, null)) assertThatThrownBy(() -> McpClient.sync(new StdioServerTransport(stdioParams), TIMEOUT, null))
.isInstanceOf(IllegalArgumentException.class); .isInstanceOf(IllegalArgumentException.class);
} }
@Test @Test
@ -152,16 +154,15 @@ class McpAsyncClientTests {
@Timeout(15) @Timeout(15)
void testListPrompts() { void testListPrompts() {
mcpAsyncClient.listPrompts(null).subscribe(prompts -> { mcpAsyncClient.listPrompts(null).subscribe(prompts -> {
assertThat(prompts).isNotNull() assertThat(prompts).isNotNull().satisfies(result -> {
.satisfies(result -> { assertThat(result.prompts()).isNotNull();
assertThat(result.prompts()).isNotNull();
if (!result.prompts().isEmpty()) { if (!result.prompts().isEmpty()) {
Prompt firstPrompt = result.prompts().get(0); Prompt firstPrompt = result.prompts().get(0);
assertThat(firstPrompt.name()).isNotNull(); assertThat(firstPrompt.name()).isNotNull();
assertThat(firstPrompt.description()).isNotNull(); assertThat(firstPrompt.description()).isNotNull();
} }
}); });
}); });
} }
@ -169,11 +170,10 @@ class McpAsyncClientTests {
@Timeout(15) @Timeout(15)
void testGetPrompt() { void testGetPrompt() {
mcpAsyncClient.getPrompt(new GetPromptRequest("simple_prompt", Map.of())).subscribe(prompt -> { mcpAsyncClient.getPrompt(new GetPromptRequest("simple_prompt", Map.of())).subscribe(prompt -> {
assertThat(prompt).isNotNull() assertThat(prompt).isNotNull().satisfies(result -> {
.satisfies(result -> { assertThat(result.description()).isNotNull();
assertThat(result.description()).isNotNull(); assertThat(result.messages()).isNotEmpty();
assertThat(result.messages()).isNotEmpty(); });
});
}); });
} }

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * 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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.ai.mcp.client; package org.springframework.ai.mcp.client;
import java.time.Duration; 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.Timeout;
import org.springframework.ai.mcp.client.stdio.ServerParameters; import org.springframework.ai.mcp.client.stdio.ServerParameters;
import org.springframework.ai.mcp.client.stdio.StdioServerTransport; import org.springframework.ai.mcp.client.stdio.StdioServerTransport;
import org.springframework.ai.mcp.spec.McpSchema.CallToolRequest; import org.springframework.ai.mcp.spec.McpSchema.CallToolRequest;

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * 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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.ai.mcp.client.stdio; package org.springframework.ai.mcp.client.stdio;
import java.io.BufferedReader; import java.io.BufferedReader;
@ -129,4 +130,5 @@ public class StdioServerTransportTests {
assertThat(exception.getMessage()).contains("Process input or output stream is null"); assertThat(exception.getMessage()).contains("Process input or output stream is null");
verify(mockProcess).destroy(); verify(mockProcess).destroy();
} }
} }

Some files were not shown because too many files have changed in this diff Show more