Attempting to structure the client API with sync and async abstractions
This commit is contained in:
parent
59066253f6
commit
4b6201a8ef
9 changed files with 521 additions and 230 deletions
|
|
@ -5,6 +5,7 @@ import java.util.List;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import spring.ai.mcp.client.McpClient;
|
import spring.ai.mcp.client.McpClient;
|
||||||
|
import spring.ai.mcp.client.McpSyncClient;
|
||||||
import spring.ai.mcp.client.stdio.StdioServerParameters;
|
import spring.ai.mcp.client.stdio.StdioServerParameters;
|
||||||
import spring.ai.mcp.client.stdio.StdioServerTransport;
|
import spring.ai.mcp.client.stdio.StdioServerTransport;
|
||||||
|
|
||||||
|
|
@ -41,7 +42,7 @@ public class McpApplication {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public List<McpFunctionCallback> functionCallbacks(McpClient mcpClient) {
|
public List<McpFunctionCallback> functionCallbacks(McpSyncClient mcpClient) {
|
||||||
|
|
||||||
return mcpClient.listTools(null)
|
return mcpClient.listTools(null)
|
||||||
.tools()
|
.tools()
|
||||||
|
|
@ -51,16 +52,17 @@ public class McpApplication {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(destroyMethod = "close")
|
@Bean(destroyMethod = "close")
|
||||||
public McpClient clientSession() {
|
public McpSyncClient clientSession() {
|
||||||
|
|
||||||
var stdioParams = StdioServerParameters.builder("npx")
|
var stdioParams = StdioServerParameters.builder("npx")
|
||||||
.args("-y", "@modelcontextprotocol/server-filesystem",
|
.args("-y", "@modelcontextprotocol/server-filesystem",
|
||||||
"/Users/christiantzolov/Dev/projects/demo/mcp-server/dir")
|
"dir")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
McpClient mcpClient = null;
|
McpSyncClient mcpClient = null;
|
||||||
try {
|
try {
|
||||||
mcpClient = new McpClient(new StdioServerTransport(stdioParams, Duration.ofMillis(100)),
|
mcpClient = McpClient.sync(new StdioServerTransport(stdioParams,
|
||||||
|
Duration.ofMillis(100)),
|
||||||
Duration.ofSeconds(10), new ObjectMapper());
|
Duration.ofSeconds(10), new ObjectMapper());
|
||||||
|
|
||||||
var init = mcpClient.initialize();
|
var init = mcpClient.initialize();
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package spring.ai.mcp;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import spring.ai.mcp.client.McpClient;
|
import spring.ai.mcp.client.McpClient;
|
||||||
|
import spring.ai.mcp.client.McpSyncClient;
|
||||||
import spring.ai.mcp.spec.McpSchema.CallToolRequest;
|
import spring.ai.mcp.spec.McpSchema.CallToolRequest;
|
||||||
import spring.ai.mcp.spec.McpSchema.CallToolResult;
|
import spring.ai.mcp.spec.McpSchema.CallToolResult;
|
||||||
import spring.ai.mcp.spec.McpSchema.Tool;
|
import spring.ai.mcp.spec.McpSchema.Tool;
|
||||||
|
|
@ -32,11 +33,12 @@ import org.springframework.ai.model.function.FunctionCallback;
|
||||||
|
|
||||||
public class McpFunctionCallback implements FunctionCallback {
|
public class McpFunctionCallback implements FunctionCallback {
|
||||||
|
|
||||||
private final McpClient mcpClient;
|
// TODO: revisit function calling as well to handle the async case
|
||||||
|
private final McpSyncClient mcpClient;
|
||||||
|
|
||||||
private final Tool tool;
|
private final Tool tool;
|
||||||
|
|
||||||
public McpFunctionCallback(McpClient clientSession, Tool tool) {
|
public McpFunctionCallback(McpSyncClient clientSession, Tool tool) {
|
||||||
this.mcpClient = clientSession;
|
this.mcpClient = clientSession;
|
||||||
this.tool = tool;
|
this.tool = tool;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
187
src/main/java/spring/ai/mcp/client/McpAsyncClient.java
Normal file
187
src/main/java/spring/ai/mcp/client/McpAsyncClient.java
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
package spring.ai.mcp.client;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import spring.ai.mcp.spec.McpSchema;
|
||||||
|
import spring.ai.mcp.spec.McpSession;
|
||||||
|
import spring.ai.mcp.spec.McpTransport;
|
||||||
|
|
||||||
|
public class McpAsyncClient extends McpAsyncSession {
|
||||||
|
|
||||||
|
public McpAsyncClient(McpTransport transport) {
|
||||||
|
this(transport, Duration.ofSeconds(10), new ObjectMapper());
|
||||||
|
}
|
||||||
|
|
||||||
|
public McpAsyncClient(McpTransport transport, Duration requestTimeout, ObjectMapper objectMapper) {
|
||||||
|
super(requestTimeout, objectMapper, transport);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
* <li>Exchange and negotiate capabilities</li>
|
||||||
|
* <li>Share implementation details</li>
|
||||||
|
* </ul>
|
||||||
|
* <br/>
|
||||||
|
* 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>
|
||||||
|
* <li>Client implementation information</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* The server MUST respond with its own capabilities and information:
|
||||||
|
* {@link McpSchema.ServerCapabilities}. <br/>
|
||||||
|
* After successful initialization, the client MUST send an initialized notification
|
||||||
|
* to indicate it is ready to begin normal operations.
|
||||||
|
*
|
||||||
|
* <br/>
|
||||||
|
*
|
||||||
|
* <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() {
|
||||||
|
McpSchema.InitializeRequest initializeRequest = new McpSchema.InitializeRequest(// @formatter:off
|
||||||
|
McpSchema.LATEST_PROTOCOL_VERSION,
|
||||||
|
new McpSchema.ClientCapabilities(null, new McpSchema.ClientCapabilities.RootCapabilities(true), null),
|
||||||
|
new McpSchema.Implementation("mcp-java-client", "0.0.1")); // @formatter:on
|
||||||
|
|
||||||
|
Mono<McpSchema.InitializeResult> result =
|
||||||
|
this.sendRequest("initialize", initializeRequest, new TypeReference<McpSchema.InitializeResult>() {});
|
||||||
|
|
||||||
|
return result.flatMap(initializeResult -> {
|
||||||
|
if (initializeResult.protocolVersion() != McpSchema.LATEST_PROTOCOL_VERSION) {
|
||||||
|
return Mono.error(
|
||||||
|
new McpError("Unsupported protocol version from the server: "
|
||||||
|
+ initializeResult.protocolVersion()));
|
||||||
|
} else {
|
||||||
|
return this.sendNotification("notifications/initialized", null)
|
||||||
|
.thenReturn(initializeResult);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a roots/list_changed notification.
|
||||||
|
*/
|
||||||
|
public Mono<Void> sendRootsListChanged() {
|
||||||
|
return this.sendNotification("notifications/roots/list_changed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a synchronous ping request.
|
||||||
|
*/
|
||||||
|
public Mono<Void> ping() {
|
||||||
|
return this.sendRequest("ping", null,
|
||||||
|
new TypeReference<Void>() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------
|
||||||
|
// Tools
|
||||||
|
// --------------------------
|
||||||
|
/**
|
||||||
|
* Send a tools/call request.
|
||||||
|
* @param callToolRequest the call tool request.
|
||||||
|
* @return the call tool result.
|
||||||
|
*/
|
||||||
|
public Mono<McpSchema.CallToolResult> callTool(McpSchema.CallToolRequest callToolRequest) {
|
||||||
|
return this.sendRequest("tools/call", callToolRequest,
|
||||||
|
new TypeReference<McpSchema.CallToolResult>() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a tools/list request.
|
||||||
|
* @return the list of tools result.
|
||||||
|
*/
|
||||||
|
public Mono<McpSchema.ListToolsResult> listTools(String cursor) {
|
||||||
|
return this.sendRequest("tools/list", new McpSchema.PaginatedRequest(cursor),
|
||||||
|
new TypeReference<McpSchema.ListToolsResult>() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------
|
||||||
|
// Resources
|
||||||
|
// --------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a resources/list request.
|
||||||
|
* @param cursor the cursor
|
||||||
|
* @return the list of resources result.
|
||||||
|
*/
|
||||||
|
public Mono<McpSchema.ListResourcesResult> listResources(String cursor) {
|
||||||
|
return this.sendRequest("resources/list", new McpSchema.PaginatedRequest(cursor),
|
||||||
|
new TypeReference<McpSchema.ListResourcesResult>() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a resources/read request.
|
||||||
|
* @param resource the resource to read
|
||||||
|
* @return the resource content.
|
||||||
|
*/
|
||||||
|
public Mono<McpSchema.ReadResourceResult> readResource(McpSchema.Resource resource) {
|
||||||
|
return this.readResource(new McpSchema.ReadResourceRequest(resource.uri()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a resources/read request.
|
||||||
|
* @param readResourceRequest the read resource request.
|
||||||
|
* @return the resource content.
|
||||||
|
*/
|
||||||
|
public Mono<McpSchema.ReadResourceResult> readResource(McpSchema.ReadResourceRequest readResourceRequest) {
|
||||||
|
return this.sendRequest("resources/read", readResourceRequest,
|
||||||
|
new TypeReference<McpSchema.ReadResourceResult>() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource templates allow servers to expose parameterized resources using URI
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public Mono<McpSchema.ListResourceTemplatesResult> listResourceTemplates(String cursor) {
|
||||||
|
return this.sendRequest("resources/templates/list", new McpSchema.PaginatedRequest(cursor),
|
||||||
|
new TypeReference<McpSchema.ListResourceTemplatesResult>() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* Send a resources/subscribe request.
|
||||||
|
* @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,
|
||||||
|
new TypeReference<Void>() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a resources/unsubscribe request.
|
||||||
|
* @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,
|
||||||
|
new TypeReference<Void>() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/main/java/spring/ai/mcp/client/McpAsyncSession.java
Normal file
112
src/main/java/spring/ai/mcp/client/McpAsyncSession.java
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
package spring.ai.mcp.client;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.publisher.MonoSink;
|
||||||
|
import reactor.core.publisher.Sinks;
|
||||||
|
import spring.ai.mcp.spec.McpSchema;
|
||||||
|
import spring.ai.mcp.spec.McpSession;
|
||||||
|
import spring.ai.mcp.spec.McpTransport;
|
||||||
|
|
||||||
|
public class McpAsyncSession {
|
||||||
|
|
||||||
|
private final ConcurrentHashMap<Object, MonoSink<McpSchema.JSONRPCResponse>> pendingResponses
|
||||||
|
= new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final Duration requestTimeout;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
private final McpTransport transport;
|
||||||
|
|
||||||
|
public McpAsyncSession(Duration requestTimeout,
|
||||||
|
ObjectMapper objectMapper,
|
||||||
|
McpTransport transport) {
|
||||||
|
this.requestTimeout = requestTimeout;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.transport = transport;
|
||||||
|
|
||||||
|
this.transport.setMessageHandler(message -> {
|
||||||
|
switch (message) {
|
||||||
|
case McpSchema.JSONRPCResponse response -> {
|
||||||
|
var sink = pendingResponses.remove(response.id());
|
||||||
|
if (sink == null) {
|
||||||
|
System.out.println("Unexpected response for unkown id " + response.id());
|
||||||
|
} else {
|
||||||
|
sink.success(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case McpSchema.JSONRPCRequest request -> {
|
||||||
|
System.out.println("Client does not yet support server requests");
|
||||||
|
}
|
||||||
|
case McpSchema.JSONRPCNotification notification -> {
|
||||||
|
System.out.println("Notifications not yet supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.transport.setErrorHandler(error -> System.out.println("Error received " + error));
|
||||||
|
|
||||||
|
this.transport.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> Mono<T> sendRequest(String method, Object requestParams, TypeReference<T> typeRef) {
|
||||||
|
String requestId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
return Mono.<McpSchema.JSONRPCResponse>create(sink -> {
|
||||||
|
this.pendingResponses.put(requestId, sink);
|
||||||
|
McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, method, requestId, requestParams);
|
||||||
|
try {
|
||||||
|
// TODO convert the transport to non-blocking as well
|
||||||
|
this.transport.sendMessage(jsonrpcRequest);
|
||||||
|
} catch (Exception e) {
|
||||||
|
sink.error(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.timeout(this.requestTimeout)
|
||||||
|
.handle((jsonRpcResponse, s) -> {
|
||||||
|
if (jsonRpcResponse.error() != null) {
|
||||||
|
s.error(new McpError(jsonRpcResponse.error()));
|
||||||
|
} else {
|
||||||
|
s.next(this.objectMapper.convertValue(jsonRpcResponse.result(), typeRef));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class McpError extends RuntimeException {
|
||||||
|
|
||||||
|
public McpError(Object error) {
|
||||||
|
super(error.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> sendNotification(String method) {
|
||||||
|
return sendNotification(method, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> sendNotification(String method, Map<String, Object> params) {
|
||||||
|
McpSchema.JSONRPCNotification
|
||||||
|
jsonrpcNotification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION, method, params);
|
||||||
|
try {
|
||||||
|
// TODO: make it non-blocking
|
||||||
|
this.transport.sendMessage(jsonrpcNotification);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
return Mono.error(new McpError(e));
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> closeGracefully(Duration timeout) {
|
||||||
|
// TODO handle the timeout in transport
|
||||||
|
return Mono.fromRunnable(this.transport::close);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -47,232 +47,35 @@ import spring.ai.mcp.spec.McpSchema.ClientCapabilities.RootCapabilities;
|
||||||
* @author Christian Tzolov
|
* @author Christian Tzolov
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
public class McpClient extends McpSession {
|
public class McpClient {
|
||||||
|
|
||||||
public McpClient(McpTransport transport) {
|
private McpClient() {
|
||||||
this(transport, Duration.ofSeconds(10), new ObjectMapper());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public McpClient(McpTransport transport, Duration readTimeout, ObjectMapper objectMapper) {
|
// TODO: introduce a builder like:
|
||||||
super(transport, readTimeout, objectMapper);
|
// McpClient.using(transport)
|
||||||
|
// .withRequestTimeout(Duration.ofSeconds(5))
|
||||||
|
// .withObjectMapper(objectMapper); <- or even a more sophisticated
|
||||||
|
// JSONtoPOJOCodec type
|
||||||
|
// .sync();
|
||||||
|
|
||||||
|
public static McpAsyncClient async(McpTransport transport) {
|
||||||
|
return new McpAsyncClient(transport);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static McpAsyncClient async(McpTransport transport, Duration requestTimeout,
|
||||||
* The initialization phase MUST be the first interaction between client and server.
|
ObjectMapper objectMapper) {
|
||||||
* During this phase, the client and server:
|
return new McpAsyncClient(transport, requestTimeout, objectMapper);
|
||||||
* <ul>
|
|
||||||
* <li>Establish protocol version compatibility</li>
|
|
||||||
* <li>Exchange and negotiate capabilities</li>
|
|
||||||
* <li>Share implementation details</li>
|
|
||||||
* </ul>
|
|
||||||
* <br/>
|
|
||||||
* 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>
|
|
||||||
* <li>Client implementation information</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* The server MUST respond with its own capabilities and information:
|
|
||||||
* {@link McpSchema.ServerCapabilities}. <br/>
|
|
||||||
* After successful initialization, the client MUST send an initialized notification
|
|
||||||
* to indicate it is ready to begin normal operations.
|
|
||||||
*
|
|
||||||
* <br/>
|
|
||||||
*
|
|
||||||
* <a href=
|
|
||||||
* "https://github.com/modelcontextprotocol/specification/blob/main/docs/specification/basic/lifecycle.md#initialization">Initialization
|
|
||||||
* Spec</a>
|
|
||||||
* @return the initialize result.
|
|
||||||
*/
|
|
||||||
public InitializeResult initialize() {
|
|
||||||
|
|
||||||
InitializeRequest initializeRequest = new InitializeRequest(// @formatter:off
|
|
||||||
McpSchema.LATEST_PROTOCOL_VERSION,
|
|
||||||
new ClientCapabilities(null, new RootCapabilities(true), null),
|
|
||||||
new Implementation("mcp-java-client", "0.0.1")); // @formatter:on
|
|
||||||
|
|
||||||
CompletableFuture<InitializeResult> initializeResultFuture = this.sendRequest("initialize", initializeRequest,
|
|
||||||
new TypeReference<InitializeResult>() {
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
InitializeResult initializeResult = initializeResultFuture.get();
|
|
||||||
|
|
||||||
if (initializeRequest.protocolVersion() != McpSchema.LATEST_PROTOCOL_VERSION) {
|
|
||||||
throw new McpError(
|
|
||||||
"Unsupported protocol version from the server: " + initializeRequest.protocolVersion());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sendNotification("notifications/initialized", null);
|
|
||||||
|
|
||||||
return initializeResult;
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
throw new McpError(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static McpSyncClient sync(McpTransport transport) {
|
||||||
* Send a roots/list_changed notification.
|
return new McpSyncClient(async(transport));
|
||||||
*/
|
|
||||||
public void sendRootsListChanged() {
|
|
||||||
this.sendNotification("notifications/roots/list_changed");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static McpSyncClient sync(McpTransport transport, Duration requestTimeout,
|
||||||
* Send a synchronous ping request.
|
ObjectMapper objectMapper) {
|
||||||
*/
|
return new McpSyncClient(async(transport, requestTimeout, objectMapper));
|
||||||
public void ping() {
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.sendRequest("ping", null, new TypeReference<Void>() {
|
|
||||||
}).get();
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
throw new McpError(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------
|
|
||||||
// Tools
|
|
||||||
// --------------------------
|
|
||||||
/**
|
|
||||||
* Send a tools/call request.
|
|
||||||
* @param callToolRequest the call tool request.
|
|
||||||
* @return the call tool result.
|
|
||||||
*/
|
|
||||||
public CallToolResult callTool(CallToolRequest callToolRequest) {
|
|
||||||
try {
|
|
||||||
return this.sendRequest("tools/call", callToolRequest, new TypeReference<CallToolResult>() {
|
|
||||||
}).get();
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
throw new McpError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a tools/list request.
|
|
||||||
* @return the list of tools result.
|
|
||||||
*/
|
|
||||||
public ListToolsResult listTools(String cursor) {
|
|
||||||
try {
|
|
||||||
return this.sendRequest("tools/list", new PaginatedRequest(cursor), new TypeReference<ListToolsResult>() {
|
|
||||||
}).get();
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
throw new McpError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------
|
|
||||||
// Resources
|
|
||||||
// --------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a resources/list request.
|
|
||||||
* @param cursor the cursor
|
|
||||||
* @return the list of resources result.
|
|
||||||
*/
|
|
||||||
public ListResourcesResult listResources(String cursor) {
|
|
||||||
try {
|
|
||||||
return this
|
|
||||||
.sendRequest("resources/list", new PaginatedRequest(cursor), new TypeReference<ListResourcesResult>() {
|
|
||||||
})
|
|
||||||
.get();
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
throw new McpError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a resources/read request.
|
|
||||||
* @param resource the resource to read
|
|
||||||
* @return the resource content.
|
|
||||||
*/
|
|
||||||
public McpSchema.ReadResourceResult readResource(Resource resource) {
|
|
||||||
return this.readResource(new ReadResourceRequest(resource.uri()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a resources/read request.
|
|
||||||
* @param readResourceRequest the read resource request.
|
|
||||||
* @return the resource content.
|
|
||||||
*/
|
|
||||||
public McpSchema.ReadResourceResult readResource(ReadResourceRequest readResourceRequest) {
|
|
||||||
try {
|
|
||||||
return this
|
|
||||||
.sendRequest("resources/read", readResourceRequest, new TypeReference<McpSchema.ReadResourceResult>() {
|
|
||||||
})
|
|
||||||
.get();
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
throw new McpError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resource templates allow servers to expose parameterized resources using URI
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
public ListResourceTemplatesResult listResourceTemplates(String cursor) {
|
|
||||||
try {
|
|
||||||
return this.sendRequest("resources/templates/list", new PaginatedRequest(cursor),
|
|
||||||
new TypeReference<ListResourceTemplatesResult>() {
|
|
||||||
})
|
|
||||||
.get();
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
throw new McpError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List Changed Notification. When the list of available resources changes, servers
|
|
||||||
* that declared the listChanged capability SHOULD send a notification:
|
|
||||||
*/
|
|
||||||
public void sendResourcesListChanged() {
|
|
||||||
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.
|
|
||||||
*
|
|
||||||
* Send a resources/subscribe request.
|
|
||||||
* @param subscribeRequest the subscribe request contains the uri of the resource to
|
|
||||||
* subscribe to.
|
|
||||||
*/
|
|
||||||
public void subscribeResource(SubscribeRequest subscribeRequest) {
|
|
||||||
try {
|
|
||||||
this.sendRequest("resources/subscribe", subscribeRequest, new TypeReference<Void>() {
|
|
||||||
}).get();
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
throw new McpError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a resources/unsubscribe request.
|
|
||||||
* @param unsubscribeRequest the unsubscribe request contains the uri of the resource
|
|
||||||
* to unsubscribe from.
|
|
||||||
*/
|
|
||||||
public void unsubscribeResource(UnsubscribeRequest unsubscribeRequest) {
|
|
||||||
try {
|
|
||||||
this.sendRequest("resources/unsubscribe", unsubscribeRequest, new TypeReference<Void>() {
|
|
||||||
}).get();
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
throw new McpError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
177
src/main/java/spring/ai/mcp/client/McpSyncClient.java
Normal file
177
src/main/java/spring/ai/mcp/client/McpSyncClient.java
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
package spring.ai.mcp.client;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
import spring.ai.mcp.spec.McpSchema;
|
||||||
|
|
||||||
|
public class McpSyncClient implements AutoCloseable {
|
||||||
|
|
||||||
|
// TODO: Consider providing a client config to set this properly
|
||||||
|
// this is currently a concern only because AutoCloseable is used - perhaps it is
|
||||||
|
// not a requirement?
|
||||||
|
private static final long DEFAULT_CLOSE_TIMEOUT_MS = 10_000L;
|
||||||
|
|
||||||
|
McpAsyncClient delegate;
|
||||||
|
|
||||||
|
public McpSyncClient(McpAsyncClient delegate) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
delegate.closeGracefully(Duration.ofMillis(DEFAULT_CLOSE_TIMEOUT_MS)).block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
* <li>Exchange and negotiate capabilities</li>
|
||||||
|
* <li>Share implementation details</li>
|
||||||
|
* </ul>
|
||||||
|
* <br/>
|
||||||
|
* 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>
|
||||||
|
* <li>Client implementation information</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* The server MUST respond with its own capabilities and information:
|
||||||
|
* {@link McpSchema.ServerCapabilities}. <br/>
|
||||||
|
* After successful initialization, the client MUST send an initialized notification
|
||||||
|
* to indicate it is ready to begin normal operations.
|
||||||
|
*
|
||||||
|
* <br/>
|
||||||
|
*
|
||||||
|
* <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() {
|
||||||
|
// TODO: block takes no argument here as we assume the async client is
|
||||||
|
// configured with a requestTimeout at all times
|
||||||
|
return delegate.initialize()
|
||||||
|
.block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a roots/list_changed notification.
|
||||||
|
*/
|
||||||
|
public void sendRootsListChanged() {
|
||||||
|
delegate.sendRootsListChanged()
|
||||||
|
.block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a synchronous ping request.
|
||||||
|
*/
|
||||||
|
public void ping() {
|
||||||
|
delegate.ping()
|
||||||
|
.block();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------
|
||||||
|
// Tools
|
||||||
|
// --------------------------
|
||||||
|
/**
|
||||||
|
* Send a tools/call request.
|
||||||
|
* @param callToolRequest the call tool request.
|
||||||
|
* @return the call tool result.
|
||||||
|
*/
|
||||||
|
public McpSchema.CallToolResult callTool(McpSchema.CallToolRequest callToolRequest) {
|
||||||
|
return delegate.callTool(callToolRequest)
|
||||||
|
.block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a tools/list request.
|
||||||
|
* @return the list of tools result.
|
||||||
|
*/
|
||||||
|
public McpSchema.ListToolsResult listTools(String cursor) {
|
||||||
|
return delegate.listTools(cursor)
|
||||||
|
.block();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------
|
||||||
|
// Resources
|
||||||
|
// --------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a resources/list request.
|
||||||
|
* @param cursor the cursor
|
||||||
|
* @return the list of resources result.
|
||||||
|
*/
|
||||||
|
public McpSchema.ListResourcesResult listResources(String cursor) {
|
||||||
|
return delegate.listResources(cursor)
|
||||||
|
.block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a resources/read request.
|
||||||
|
* @param resource the resource to read
|
||||||
|
* @return the resource content.
|
||||||
|
*/
|
||||||
|
public McpSchema.ReadResourceResult readResource(McpSchema.Resource resource) {
|
||||||
|
return delegate.readResource(resource)
|
||||||
|
.block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a resources/read request.
|
||||||
|
* @param readResourceRequest the read resource request.
|
||||||
|
* @return the resource content.
|
||||||
|
*/
|
||||||
|
public McpSchema.ReadResourceResult readResource(McpSchema.ReadResourceRequest readResourceRequest) {
|
||||||
|
return delegate.readResource(readResourceRequest)
|
||||||
|
.block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource templates allow servers to expose parameterized resources using URI
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public McpSchema.ListResourceTemplatesResult listResourceTemplates(String cursor) {
|
||||||
|
return delegate.listResourceTemplates(cursor)
|
||||||
|
.block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List Changed Notification. When the list of available resources changes, servers
|
||||||
|
* that declared the listChanged capability SHOULD send a notification:
|
||||||
|
*/
|
||||||
|
public void sendResourcesListChanged() {
|
||||||
|
delegate.sendResourcesListChanged()
|
||||||
|
.block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public void subscribeResource(McpSchema.SubscribeRequest subscribeRequest) {
|
||||||
|
delegate.subscribeResource(subscribeRequest)
|
||||||
|
.block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a resources/unsubscribe request.
|
||||||
|
* @param unsubscribeRequest the unsubscribe request contains the uri of the resource
|
||||||
|
* to unsubscribe from.
|
||||||
|
*/
|
||||||
|
public void unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) {
|
||||||
|
delegate.unsubscribeResource(unsubscribeRequest)
|
||||||
|
.block();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ import java.time.Duration;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import spring.ai.mcp.client.McpClient;
|
import spring.ai.mcp.client.McpClient;
|
||||||
|
import spring.ai.mcp.client.McpSyncClient;
|
||||||
import spring.ai.mcp.client.stdio.StdioServerParameters;
|
import spring.ai.mcp.client.stdio.StdioServerParameters;
|
||||||
import spring.ai.mcp.client.stdio.StdioServerTransport;
|
import spring.ai.mcp.client.stdio.StdioServerTransport;
|
||||||
import spring.ai.mcp.spec.McpSchema.ListResourcesResult;
|
import spring.ai.mcp.spec.McpSchema.ListResourcesResult;
|
||||||
|
|
@ -33,10 +34,12 @@ public class ClientSessionTests {
|
||||||
|
|
||||||
var stdioParams = StdioServerParameters.builder("npx")
|
var stdioParams = StdioServerParameters.builder("npx")
|
||||||
.args("-y", "@modelcontextprotocol/server-everything",
|
.args("-y", "@modelcontextprotocol/server-everything",
|
||||||
"/Users/christiantzolov/Dev/projects/demo/mcp-server/dir")
|
"dir")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
try (McpClient clientSession = new McpClient(new StdioServerTransport(stdioParams), Duration.ofSeconds(10),
|
try (McpSyncClient clientSession =
|
||||||
|
McpClient.sync(new StdioServerTransport(stdioParams),
|
||||||
|
Duration.ofSeconds(10),
|
||||||
new ObjectMapper())) {
|
new ObjectMapper())) {
|
||||||
// clientSession.handleIncomingMessages();
|
// clientSession.handleIncomingMessages();
|
||||||
clientSession.initialize();
|
clientSession.initialize();
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import java.time.Duration;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import spring.ai.mcp.client.McpClient;
|
import spring.ai.mcp.client.McpClient;
|
||||||
|
import spring.ai.mcp.client.McpSyncClient;
|
||||||
import spring.ai.mcp.client.stdio.StdioServerParameters;
|
import spring.ai.mcp.client.stdio.StdioServerParameters;
|
||||||
import spring.ai.mcp.client.stdio.StdioServerTransport;
|
import spring.ai.mcp.client.stdio.StdioServerTransport;
|
||||||
import spring.ai.mcp.spec.McpSchema.ListResourcesResult;
|
import spring.ai.mcp.spec.McpSchema.ListResourcesResult;
|
||||||
|
|
@ -34,11 +35,13 @@ public class ClientSessionTests2 {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|
||||||
var stdioParams = StdioServerParameters.builder("uv")
|
var stdioParams = StdioServerParameters.builder("uv")
|
||||||
.args("--directory", "/Users/christiantzolov/Dev/projects/demo/mcp-server/dir", "run", "mcp-server-sqlite",
|
.args("--directory", "dir", "run", "mcp-server-sqlite",
|
||||||
"--db-path", "~/test.db")
|
"--db-path", "~/test.db")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
try (McpClient clientSession = new McpClient(new StdioServerTransport(stdioParams), Duration.ofSeconds(10), new ObjectMapper())) {
|
try (McpSyncClient clientSession =
|
||||||
|
McpClient.sync(new StdioServerTransport(stdioParams),
|
||||||
|
Duration.ofSeconds(10), new ObjectMapper())) {
|
||||||
|
|
||||||
clientSession.initialize();
|
clientSession.initialize();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import java.time.Duration;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import spring.ai.mcp.client.McpClient;
|
import spring.ai.mcp.client.McpClient;
|
||||||
|
import spring.ai.mcp.client.McpSyncClient;
|
||||||
import spring.ai.mcp.client.stdio.StdioServerParameters;
|
import spring.ai.mcp.client.stdio.StdioServerParameters;
|
||||||
import spring.ai.mcp.client.stdio.StdioServerTransport;
|
import spring.ai.mcp.client.stdio.StdioServerTransport;
|
||||||
import spring.ai.mcp.spec.McpSchema.ListResourcesResult;
|
import spring.ai.mcp.spec.McpSchema.ListResourcesResult;
|
||||||
|
|
@ -34,15 +35,16 @@ public class ClientSessionTests3 {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|
||||||
var stdioParams = StdioServerParameters.builder("uv")
|
var stdioParams = StdioServerParameters.builder("uv")
|
||||||
.args("--directory", "/Users/christiantzolov/Dev/projects/demo/mcp-server/dir", "run", "mcp-server-sqlite",
|
.args("--directory", "dir", "run", "mcp-server-sqlite",
|
||||||
"--db-path", "~/test.db")
|
"--db-path", "~/test.db")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
||||||
McpClient clientSession = null;
|
McpSyncClient clientSession = null;
|
||||||
try {
|
try {
|
||||||
|
|
||||||
clientSession = new McpClient(new StdioServerTransport(stdioParams), Duration.ofSeconds(10), new ObjectMapper());
|
clientSession = McpClient.sync(new StdioServerTransport(stdioParams),
|
||||||
|
Duration.ofSeconds(10), new ObjectMapper());
|
||||||
|
|
||||||
clientSession.initialize();
|
clientSession.initialize();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue