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>
<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>
<groupId>org.apache.maven.plugins</groupId>
<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>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-ai-mcp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>spring-ai-mcp-core</artifactId>
<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;
import java.time.Duration;
@ -5,14 +20,15 @@ import java.time.Duration;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Mono;
import org.springframework.ai.mcp.spec.DefaultMcpSession;
import org.springframework.ai.mcp.spec.McpTransport;
import org.springframework.ai.mcp.spec.McpError;
import org.springframework.ai.mcp.spec.McpSchema;
import org.springframework.ai.mcp.spec.McpSchema.GetPromptRequest;
import org.springframework.ai.mcp.spec.McpSchema.GetPromptResult;
import org.springframework.ai.mcp.spec.McpSchema.ListPromptsResult;
import org.springframework.ai.mcp.spec.McpSchema.PaginatedRequest;
import org.springframework.ai.mcp.spec.McpTransport;
/**
* @author Dariusz Jędrzejczyk
@ -32,8 +48,7 @@ public class McpAsyncClient extends DefaultMcpSession {
}
/**
* The initialization phase MUST be the first interaction between client and
* server.
* The initialization phase MUST be the first interaction between client and server.
* During this phase, the client and server:
* <ul>
* <li>Establish protocol version compatibility</li>
@ -41,8 +56,7 @@ public class McpAsyncClient extends DefaultMcpSession {
* <li>Share implementation details</li>
* </ul>
* <br/>
* The client MUST initiate this phase by sending an initialize request
* containing:
* The client MUST initiate this phase by sending an initialize request containing:
* <ul>
* <li>The protocol version the client supports</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:
* {@link McpSchema.ServerCapabilities}. <br/>
* After successful initialization, the client MUST send an initialized
* notification
* After successful initialization, the client MUST send an initialized notification
* to indicate it is ready to begin normal operations.
*
* <br/>
@ -60,7 +73,6 @@ public class McpAsyncClient extends DefaultMcpSession {
* <a href=
* "https://github.com/modelcontextprotocol/specification/blob/main/docs/specification/basic/lifecycle.md#initialization">Initialization
* Spec</a>
*
* @return the initialize result.
*/
public Mono<McpSchema.InitializeResult> initialize() {
@ -75,12 +87,11 @@ public class McpAsyncClient extends DefaultMcpSession {
return result.flatMap(initializeResult -> {
if (!McpSchema.LATEST_PROTOCOL_VERSION.equals(initializeResult.protocolVersion())) {
return Mono.error(
new McpError("Unsupported protocol version from the server: "
+ initializeResult.protocolVersion()));
} else {
return this.sendNotification("notifications/initialized", null)
.thenReturn(initializeResult);
return Mono.error(new McpError(
"Unsupported protocol version from the server: " + initializeResult.protocolVersion()));
}
else {
return this.sendNotification("notifications/initialized", null).thenReturn(initializeResult);
}
});
}
@ -104,12 +115,12 @@ public class McpAsyncClient extends DefaultMcpSession {
// --------------------------
private static TypeReference<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>() {
};
/**
* Send a tools/call request.
*
* @param callToolRequest the call tool request.
* @return the call tool result.
*/
@ -119,7 +130,6 @@ public class McpAsyncClient extends DefaultMcpSession {
/**
* Send a tools/list request.
*
* @return the list of tools result.
*/
public Mono<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.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>() {
};
/**
* Send a resources/list request.
*
* @param cursor the cursor
* @return the list of resources result.
*/
@ -150,7 +161,6 @@ public class McpAsyncClient extends DefaultMcpSession {
/**
* Send a resources/read request.
*
* @param resource the resource to read
* @return the resource content.
*/
@ -160,7 +170,6 @@ public class McpAsyncClient extends DefaultMcpSession {
/**
* Send a resources/read request.
*
* @param readResourceRequest the read resource request.
* @return the resource content.
*/
@ -173,7 +182,6 @@ public class McpAsyncClient extends DefaultMcpSession {
* templates. Arguments may be auto-completed through the completion API.
*
* Request a list of resource templates the server has.
*
* @param cursor the cursor
* @return the list of resource templates result.
*/
@ -183,23 +191,21 @@ public class McpAsyncClient extends DefaultMcpSession {
}
/**
* List Changed Notification. When the list of available resources changes,
* servers that declared the listChanged capability SHOULD send a notification:
* List Changed Notification. When the list of available resources changes, servers
* that declared the listChanged capability SHOULD send a notification:
*/
public Mono<Void> sendResourcesListChanged() {
return this.sendNotification("notifications/resources/list_changed");
}
/**
* Subscriptions. The protocol supports optional subscriptions to resource
* changes.
* Clients can subscribe to specific resources and receive notifications when
* they change.
* Subscriptions. The protocol supports optional subscriptions to resource changes.
* Clients can subscribe to specific resources and receive notifications when they
* change.
*
* Send a resources/subscribe request.
*
* @param subscribeRequest the subscribe request contains the uri of the
* resource to subscribe to.
* @param subscribeRequest the subscribe request contains the uri of the resource to
* subscribe to.
*/
public Mono<Void> subscribeResource(McpSchema.SubscribeRequest subscribeRequest) {
return this.sendRequest("resources/subscribe", subscribeRequest, VOID_TYPE_REFERENCE);
@ -207,9 +213,8 @@ public class McpAsyncClient extends DefaultMcpSession {
/**
* Send a resources/unsubscribe request.
*
* @param unsubscribeRequest the unsubscribe request contains the uri of the
* resource to unsubscribe from.
* @param unsubscribeRequest the unsubscribe request contains the uri of the resource
* to unsubscribe from.
*/
public Mono<Void> unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) {
return this.sendRequest("resources/unsubscribe", unsubscribeRequest, VOID_TYPE_REFERENCE);
@ -220,12 +225,12 @@ public class McpAsyncClient extends DefaultMcpSession {
// --------------------------
private static TypeReference<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>() {
};
public Mono<ListPromptsResult> listPrompts(String cursor) {
return this
.sendRequest("prompts/list", new PaginatedRequest(cursor), LIST_PROMPTS_RESULT_TYPE_REF);
return this.sendRequest("prompts/list", new PaginatedRequest(cursor), LIST_PROMPTS_RESULT_TYPE_REF);
}
public Mono<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
* that
* the list of prompts it offers has changed. This may be issued by servers
* without
* (Server) An optional notification from the server to the client, informing it that
* the list of prompts it offers has changed. This may be issued by servers without
* any previous subscription from the client.
*/
public Mono<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");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@ -13,93 +13,96 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.mcp.client;
import java.time.Duration;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.mcp.spec.McpTransport;
/**
* Factory class providing static methods for creating Model Context Protocol (MCP) clients.
* This class serves as the main entry point for establishing connections with MCP servers,
* offering both synchronous and asynchronous client implementations.
*
* <p>The class provides factory methods to create either:
* Factory class providing static methods for creating Model Context Protocol (MCP)
* clients. This class serves as the main entry point for establishing connections with
* MCP servers, offering both synchronous and asynchronous client implementations.
*
* <p>
* The class provides factory methods to create either:
* <ul>
* <li>{@link McpAsyncClient} for non-blocking operations
* <li>{@link McpSyncClient} for blocking operations
* <li>{@link McpAsyncClient} for non-blocking operations
* <li>{@link McpSyncClient} for blocking operations
* </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.
*
* <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 Dariusz Jędrzejczyk
*/
public class McpClient {
/**
* Private constructor to prevent instantiation as this is a utility class
* containing only static factory methods.
*/
private McpClient() {
}
/**
* Private constructor to prevent instantiation as this is a utility class containing
* only static factory methods.
*/
private McpClient() {
}
// TODO: introduce a builder like:
// McpClient.using(transport)
// .withRequestTimeout(Duration.ofSeconds(5))
// .withObjectMapper(objectMapper); <- or even a more sophisticated
// JSONtoPOJOCodec type
// .sync();
// TODO: introduce a builder like:
// McpClient.using(transport)
// .withRequestTimeout(Duration.ofSeconds(5))
// .withObjectMapper(objectMapper); <- or even a more sophisticated
// JSONtoPOJOCodec type
// .sync();
/**
* Creates an asynchronous MCP client with default configuration.
*
* @param transport The transport layer implementation for MCP communication
* @return A new instance of {@link McpAsyncClient}
*/
public static McpAsyncClient async(McpTransport transport) {
return new McpAsyncClient(transport);
}
/**
* Creates an asynchronous MCP client with default configuration.
* @param transport The transport layer implementation for MCP communication
* @return A new instance of {@link McpAsyncClient}
*/
public static McpAsyncClient async(McpTransport transport) {
return new McpAsyncClient(transport);
}
/**
* Creates an asynchronous MCP client with custom configuration.
*
* @param transport The transport layer implementation for MCP communication
* @param requestTimeout The duration to wait before timing out requests
* @param objectMapper The Jackson object mapper for JSON serialization/deserialization
* @return A new instance of {@link McpAsyncClient}
*/
public static McpAsyncClient async(McpTransport transport, Duration requestTimeout,
ObjectMapper objectMapper) {
return new McpAsyncClient(transport, requestTimeout, objectMapper);
}
/**
* Creates an asynchronous MCP client with custom configuration.
* @param transport The transport layer implementation for MCP communication
* @param requestTimeout The duration to wait before timing out requests
* @param objectMapper The Jackson object mapper for JSON
* serialization/deserialization
* @return A new instance of {@link McpAsyncClient}
*/
public static McpAsyncClient async(McpTransport transport, Duration requestTimeout, ObjectMapper objectMapper) {
return new McpAsyncClient(transport, requestTimeout, objectMapper);
}
/**
* Creates a synchronous MCP client with default configuration.
* This method wraps an asynchronous client to provide synchronous operations.
*
* @param transport The transport layer implementation for MCP communication
* @return A new instance of {@link McpSyncClient}
*/
public static McpSyncClient sync(McpTransport transport) {
return new McpSyncClient(async(transport));
}
/**
* Creates a synchronous MCP client with default configuration. This method wraps an
* asynchronous client to provide synchronous operations.
* @param transport The transport layer implementation for MCP communication
* @return A new instance of {@link McpSyncClient}
*/
public static McpSyncClient sync(McpTransport transport) {
return new McpSyncClient(async(transport));
}
/**
* Creates a synchronous MCP client with custom configuration. This method wraps an
* asynchronous client to provide synchronous operations.
* @param transport The transport layer implementation for MCP communication
* @param requestTimeout The duration to wait before timing out requests
* @param objectMapper The Jackson object mapper for JSON
* serialization/deserialization
* @return A new instance of {@link McpSyncClient}
*/
public static McpSyncClient sync(McpTransport transport, Duration requestTimeout, ObjectMapper objectMapper) {
return new McpSyncClient(async(transport, requestTimeout, objectMapper));
}
/**
* Creates a synchronous MCP client with custom configuration.
* This method wraps an asynchronous client to provide synchronous operations.
*
* @param transport The transport layer implementation for MCP communication
* @param requestTimeout The duration to wait before timing out requests
* @param objectMapper The Jackson object mapper for JSON serialization/deserialization
* @return A new instance of {@link McpSyncClient}
*/
public static McpSyncClient sync(McpTransport transport, Duration requestTimeout,
ObjectMapper objectMapper) {
return new McpSyncClient(async(transport, requestTimeout, objectMapper));
}
}

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

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

View file

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

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

View file

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

View file

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

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

View file

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

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");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.mcp.spec;
import java.time.Duration;
@ -23,65 +24,72 @@ import reactor.core.publisher.Mono;
/**
* Represents a Model Control Protocol (MCP) session that handles communication between
* clients and the server. This interface provides methods for sending requests
* and notifications, as well as managing the session lifecycle.
*
* <p>The session operates asynchronously using Project Reactor's {@link Mono} type
* for non-blocking operations. It supports both request-response patterns and
* one-way notifications.</p>
* clients and the server. This interface provides methods for sending requests and
* notifications, as well as managing the session lifecycle.
*
* <p>
* The session operates asynchronously using Project Reactor's {@link Mono} type for
* non-blocking operations. It supports both request-response patterns and one-way
* notifications.
* </p>
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
*/
public interface McpSession {
/**
* 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
* from the server. The response type is determined by the provided TypeReference.</p>
*
* @param <T> the type of the expected response
* @param method the name of the method to be called on the server
* @param requestParams the parameters to be sent with the request
* @param typeRef the TypeReference describing the expected response type
* @return a Mono that will emit the response when received
*/
<T> Mono<T> sendRequest(String method, Object requestParams, TypeReference<T> typeRef);
/**
* 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 from
* the server. The response type is determined by the provided TypeReference.
* </p>
* @param <T> the type of the expected response
* @param method the name of the method to be called on the server
* @param requestParams the parameters to be sent with the request
* @param typeRef the TypeReference describing the expected response type
* @return a Mono that will emit the response when received
*/
<T> Mono<T> sendRequest(String method, Object requestParams, TypeReference<T> typeRef);
/**
* Sends a notification to the model server without parameters.
*
* <p>This method implements the notification pattern where no response is expected
* from the server. It's useful for fire-and-forget scenarios.</p>
*
* @param method the name of the notification method to be called on the server
* @return a Mono that completes when the notification has been sent
*/
/**
* Sends a notification to the model server without parameters.
*
* <p>
* This method implements the notification pattern where no response is expected from
* the server. It's useful for fire-and-forget scenarios.
* </p>
* @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) {
return sendNotification(method, null);
}
/**
* Sends a notification to the model server with parameters.
*
* <p>Similar to {@link #sendNotification(String)} but allows sending additional
* parameters with the notification.</p>
*
* @param method the name of the notification method to be called on the server
* @param params a map of parameters to be sent with the notification
* @return a Mono that completes when the notification has been sent
*/
Mono<Void> sendNotification(String method, Map<String, Object> params);
/**
* Sends a notification to the model server with parameters.
*
* <p>
* Similar to {@link #sendNotification(String)} but allows sending additional
* parameters with the notification.
* </p>
* @param method the name of the notification method to be called on the server
* @param params a map of parameters to be sent with the notification
* @return a Mono that completes when the notification has been sent
*/
Mono<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,38 +1,37 @@
/*
* Copyright 2024 - 2024 the original author or authors.
*
/*
* Copyright 2024-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.mcp.spec;
import java.util.function.Consumer;
import reactor.core.publisher.Mono;
import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage;
/**
* Defines the asynchronous transport layer for the Model Context Protocol
* (MCP).
*
* Defines the asynchronous transport layer for the Model Context Protocol (MCP).
*
* <p>
* The McpTransport interface provides the foundation for implementing custom
* transport
* mechanisms in the Model Context Protocol. It handles the bidirectional
* communication between
* the client and server components, supporting asynchronous message exchange
* The McpTransport interface provides the foundation for implementing custom transport
* mechanisms in the Model Context Protocol. It handles the bidirectional communication
* between the client and server components, supporting asynchronous message exchange
* using JSON-RPC format.
* </p>
*
*
* <p>
* Implementations of this interface are responsible for:
* </p>
@ -41,11 +40,10 @@ import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage;
* <li>Handling incoming messages and errors from the server</li>
* <li>Sending outbound messages to the server</li>
* </ul>
*
*
* <p>
* The transport layer is designed to be protocol-agnostic, allowing for various
* implementations
* such as WebSocket, HTTP, or custom protocols.
* implementations such as WebSocket, HTTP, or custom protocols.
* </p>
*
* @author Christian Tzolov
@ -53,78 +51,69 @@ import org.springframework.ai.mcp.spec.McpSchema.JSONRPCMessage;
*/
public interface McpTransport {
/**
* Initializes and starts the transport connection.
*
* <p>
* This method should be called before any message exchange can occur. It sets
* up
* the necessary resources and establishes the connection to the server.
* </p>
*/
void start();
/**
* Initializes and starts the transport connection.
*
* <p>
* This method should be called before any message exchange can occur. It sets up the
* necessary resources and establishes the connection to the server.
* </p>
*/
void start();
/**
* Closes the transport connection and releases any associated resources.
*
* <p>
* This method ensures proper cleanup of resources when the transport is no
* longer needed.
* It should handle the graceful shutdown of any active connections.
* </p>
*/
default void close() {
this.closeGracefully().subscribe();
}
/**
* Closes the transport connection and releases any associated resources.
*
* <p>
* This method ensures proper cleanup of resources when the transport is no longer
* needed. It should handle the graceful shutdown of any active connections.
* </p>
*/
default void close() {
this.closeGracefully().subscribe();
}
/**
* Closes the transport connection and releases any associated resources
* asynchronously.
*
* @return a {@link Mono<Void>} that completes when the connection has been
* closed.
*/
Mono<Void> closeGracefully();
/**
* Closes the transport connection and releases any associated resources
* asynchronously.
* @return a {@link Mono<Void>} that completes when the connection has been closed.
*/
Mono<Void> closeGracefully();
/**
* Sets the handler for processing incoming messages from the server.
*
* <p>
* The provided handler will be called whenever a new message is received from
* the server.
* Messages are expected to follow the JSON-RPC format as defined in the MCP
* specification.
* </p>
*
* @param inboundMessageHandler a consumer that processes incoming
* {@link JSONRPCMessage}s
*/
void setInboudMessageHandler(Consumer<JSONRPCMessage> inboundMessageHandler);
/**
* Sets the handler for processing incoming messages from the server.
*
* <p>
* The provided handler will be called whenever a new message is received from the
* server. Messages are expected to follow the JSON-RPC format as defined in the MCP
* specification.
* </p>
* @param inboundMessageHandler a consumer that processes incoming
* {@link JSONRPCMessage}s
*/
void setInboudMessageHandler(Consumer<JSONRPCMessage> inboundMessageHandler);
/**
* Sets the handler for processing transport-level errors.
*
* <p>
* The provided handler will be called when errors occur during transport
* operations,
* such as connection failures or protocol violations.
* </p>
*
* @param inboundErrorHandler a consumer that processes error messages
*/
void setInboundErrorHandler(Consumer<String> inboundErrorHandler);
/**
* Sets the handler for processing transport-level errors.
*
* <p>
* The provided handler will be called when errors occur during transport operations,
* such as connection failures or protocol violations.
* </p>
* @param inboundErrorHandler a consumer that processes error messages
*/
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.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* Copyright 2024-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.mcp.attic;
import java.time.Duration;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.mcp.client.McpClient;
import org.springframework.ai.mcp.client.McpSyncClient;
import org.springframework.ai.mcp.client.stdio.ServerParameters;
@ -35,13 +37,11 @@ public class ClientSessionTests2 {
public static void main(String[] args) {
var stdioParams = ServerParameters.builder("uv")
.args("--directory", "dir", "run", "mcp-server-sqlite",
"--db-path", "~/test.db")
.args("--directory", "dir", "run", "mcp-server-sqlite", "--db-path", "~/test.db")
.build();
try (McpSyncClient clientSession =
McpClient.sync(new StdioServerTransport(stdioParams),
Duration.ofSeconds(10), new ObjectMapper())) {
try (McpSyncClient clientSession = McpClient.sync(new StdioServerTransport(stdioParams), Duration.ofSeconds(10),
new ObjectMapper())) {
clientSession.initialize();

View file

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

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

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");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.mcp.client;
import java.time.Duration;
@ -23,6 +24,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.springframework.ai.mcp.client.stdio.ServerParameters;
import org.springframework.ai.mcp.client.stdio.StdioServerTransport;
import org.springframework.ai.mcp.spec.McpSchema.CallToolRequest;
@ -104,7 +106,7 @@ class McpSyncClientTests {
CallToolResult toolResult = mcpSyncClient.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4)));
assertThat(toolResult).isNotNull().satisfies(result -> {
assertThat(result.content()).hasSize(1);
TextContent content = (TextContent) result.content().get(0);

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

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