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 spring.ai.mcp.client.McpClient;
|
||||
import spring.ai.mcp.client.McpSyncClient;
|
||||
import spring.ai.mcp.client.stdio.StdioServerParameters;
|
||||
import spring.ai.mcp.client.stdio.StdioServerTransport;
|
||||
|
||||
|
|
@ -41,7 +42,7 @@ public class McpApplication {
|
|||
}
|
||||
|
||||
@Bean
|
||||
public List<McpFunctionCallback> functionCallbacks(McpClient mcpClient) {
|
||||
public List<McpFunctionCallback> functionCallbacks(McpSyncClient mcpClient) {
|
||||
|
||||
return mcpClient.listTools(null)
|
||||
.tools()
|
||||
|
|
@ -51,16 +52,17 @@ public class McpApplication {
|
|||
}
|
||||
|
||||
@Bean(destroyMethod = "close")
|
||||
public McpClient clientSession() {
|
||||
public McpSyncClient clientSession() {
|
||||
|
||||
var stdioParams = StdioServerParameters.builder("npx")
|
||||
.args("-y", "@modelcontextprotocol/server-filesystem",
|
||||
"/Users/christiantzolov/Dev/projects/demo/mcp-server/dir")
|
||||
"dir")
|
||||
.build();
|
||||
|
||||
McpClient mcpClient = null;
|
||||
McpSyncClient mcpClient = null;
|
||||
try {
|
||||
mcpClient = new McpClient(new StdioServerTransport(stdioParams, Duration.ofMillis(100)),
|
||||
mcpClient = McpClient.sync(new StdioServerTransport(stdioParams,
|
||||
Duration.ofMillis(100)),
|
||||
Duration.ofSeconds(10), new ObjectMapper());
|
||||
|
||||
var init = mcpClient.initialize();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package spring.ai.mcp;
|
|||
import java.util.Map;
|
||||
|
||||
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.CallToolResult;
|
||||
import spring.ai.mcp.spec.McpSchema.Tool;
|
||||
|
|
@ -32,11 +33,12 @@ import org.springframework.ai.model.function.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;
|
||||
|
||||
public McpFunctionCallback(McpClient clientSession, Tool tool) {
|
||||
public McpFunctionCallback(McpSyncClient clientSession, Tool tool) {
|
||||
this.mcpClient = clientSession;
|
||||
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
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class McpClient extends McpSession {
|
||||
public class McpClient {
|
||||
|
||||
public McpClient(McpTransport transport) {
|
||||
this(transport, Duration.ofSeconds(10), new ObjectMapper());
|
||||
private McpClient() {
|
||||
}
|
||||
|
||||
public McpClient(McpTransport transport, Duration readTimeout, ObjectMapper objectMapper) {
|
||||
super(transport, readTimeout, objectMapper);
|
||||
// TODO: introduce a builder like:
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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 McpAsyncClient async(McpTransport transport, Duration requestTimeout,
|
||||
ObjectMapper objectMapper) {
|
||||
return new McpAsyncClient(transport, requestTimeout, objectMapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a roots/list_changed notification.
|
||||
*/
|
||||
public void sendRootsListChanged() {
|
||||
this.sendNotification("notifications/roots/list_changed");
|
||||
public static McpSyncClient sync(McpTransport transport) {
|
||||
return new McpSyncClient(async(transport));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a synchronous ping request.
|
||||
*/
|
||||
public void ping() {
|
||||
|
||||
try {
|
||||
this.sendRequest("ping", null, new TypeReference<Void>() {
|
||||
}).get();
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new McpError(e);
|
||||
}
|
||||
public static McpSyncClient sync(McpTransport transport, Duration requestTimeout,
|
||||
ObjectMapper objectMapper) {
|
||||
return new McpSyncClient(async(transport, requestTimeout, objectMapper));
|
||||
}
|
||||
|
||||
// --------------------------
|
||||
// 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 spring.ai.mcp.client.McpClient;
|
||||
import spring.ai.mcp.client.McpSyncClient;
|
||||
import spring.ai.mcp.client.stdio.StdioServerParameters;
|
||||
import spring.ai.mcp.client.stdio.StdioServerTransport;
|
||||
import spring.ai.mcp.spec.McpSchema.ListResourcesResult;
|
||||
|
|
@ -33,10 +34,12 @@ public class ClientSessionTests {
|
|||
|
||||
var stdioParams = StdioServerParameters.builder("npx")
|
||||
.args("-y", "@modelcontextprotocol/server-everything",
|
||||
"/Users/christiantzolov/Dev/projects/demo/mcp-server/dir")
|
||||
"dir")
|
||||
.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())) {
|
||||
// clientSession.handleIncomingMessages();
|
||||
clientSession.initialize();
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import java.time.Duration;
|
|||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.StdioServerTransport;
|
||||
import spring.ai.mcp.spec.McpSchema.ListResourcesResult;
|
||||
|
|
@ -34,11 +35,13 @@ public class ClientSessionTests2 {
|
|||
public static void main(String[] args) {
|
||||
|
||||
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")
|
||||
.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();
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import java.time.Duration;
|
|||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.StdioServerTransport;
|
||||
import spring.ai.mcp.spec.McpSchema.ListResourcesResult;
|
||||
|
|
@ -34,15 +35,16 @@ public class ClientSessionTests3 {
|
|||
public static void main(String[] args) {
|
||||
|
||||
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")
|
||||
.build();
|
||||
|
||||
|
||||
McpClient clientSession = null;
|
||||
|
||||
McpSyncClient clientSession = null;
|
||||
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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue