diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index e6a09cd08..cdcb99c0b 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -23,6 +23,7 @@ import io.modelcontextprotocol.spec.McpClientSession.RequestHandler; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.RequestIdGenerator; import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; @@ -182,6 +183,24 @@ public class McpAsyncClient { */ McpAsyncClient(McpClientTransport transport, Duration requestTimeout, Duration initializationTimeout, JsonSchemaValidator jsonSchemaValidator, McpClientFeatures.Async features) { + this(transport, requestTimeout, initializationTimeout, jsonSchemaValidator, features, null); + } + + /** + * Create a new McpAsyncClient with the given transport and session request-response + * timeout. + * @param transport the transport to use. + * @param requestTimeout the session request-response timeout. + * @param initializationTimeout the max timeout to await for the client-server + * @param jsonSchemaValidator the JSON schema validator to use for validating tool + * @param features the MCP Client supported features. responses against output + * schemas. + * @param requestIdGenerator the generator for creating unique request IDs. If null, a + * default generator will be used. + */ + McpAsyncClient(McpClientTransport transport, Duration requestTimeout, Duration initializationTimeout, + JsonSchemaValidator jsonSchemaValidator, McpClientFeatures.Async features, + RequestIdGenerator requestIdGenerator) { Assert.notNull(transport, "Transport must not be null"); Assert.notNull(requestTimeout, "Request timeout must not be null"); @@ -315,9 +334,11 @@ public class McpAsyncClient { }).then(); }; + RequestIdGenerator effectiveIdGenerator = requestIdGenerator != null ? requestIdGenerator + : RequestIdGenerator.ofDefault(); this.initializer = new LifecycleInitializer(clientCapabilities, clientInfo, transport.protocolVersions(), initializationTimeout, ctx -> new McpClientSession(requestTimeout, transport, requestHandlers, - notificationHandlers, con -> con.contextWrite(ctx)), + notificationHandlers, con -> con.contextWrite(ctx), effectiveIdGenerator), postInitializationHook); this.transport.setExceptionHandler(this.initializer::handleException); diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java index 421f2fc7f..07432b692 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java @@ -17,6 +17,7 @@ import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.RequestIdGenerator; import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; @@ -193,6 +194,8 @@ class SyncSpec { private boolean enableCallToolSchemaCaching = false; // Default to false + private RequestIdGenerator requestIdGenerator; + private SyncSpec(McpClientTransport transport) { Assert.notNull(transport, "Transport must not be null"); this.transport = transport; @@ -461,6 +464,31 @@ public SyncSpec enableCallToolSchemaCaching(boolean enableCallToolSchemaCaching) return this; } + /** + * Sets a custom request ID generator for creating unique request IDs. This is + * useful for MCP servers that require specific ID formats, such as numeric-only + * IDs. + * + *
+ * Example usage with a numeric ID generator: + * + *
{@code
+ * AtomicLong counter = new AtomicLong(0);
+ * McpClient.sync(transport)
+ * .requestIdGenerator(() -> String.valueOf(counter.incrementAndGet()))
+ * .build();
+ * }
+ * @param requestIdGenerator The generator for creating unique request IDs. If
+ * null, a default UUID-prefixed generator will be used.
+ * @return This builder instance for method chaining
+ * @see RequestIdGenerator#ofIncremental()
+ * @see RequestIdGenerator#ofDefault()
+ */
+ public SyncSpec requestIdGenerator(RequestIdGenerator requestIdGenerator) {
+ this.requestIdGenerator = requestIdGenerator;
+ return this;
+ }
+
/**
* Create an instance of {@link McpSyncClient} with the provided configurations or
* sensible defaults.
@@ -475,8 +503,8 @@ public McpSyncClient build() {
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
return new McpSyncClient(new McpAsyncClient(transport, this.requestTimeout, this.initializationTimeout,
- jsonSchemaValidator != null ? jsonSchemaValidator : JsonSchemaValidator.getDefault(),
- asyncFeatures), this.contextProvider);
+ jsonSchemaValidator != null ? jsonSchemaValidator : JsonSchemaValidator.getDefault(), asyncFeatures,
+ this.requestIdGenerator), this.contextProvider);
}
}
@@ -531,6 +559,8 @@ class AsyncSpec {
private boolean enableCallToolSchemaCaching = false; // Default to false
+ private RequestIdGenerator requestIdGenerator;
+
private AsyncSpec(McpClientTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
@@ -802,6 +832,31 @@ public AsyncSpec enableCallToolSchemaCaching(boolean enableCallToolSchemaCaching
return this;
}
+ /**
+ * Sets a custom request ID generator for creating unique request IDs. This is
+ * useful for MCP servers that require specific ID formats, such as numeric-only
+ * IDs.
+ *
+ * + * Example usage with a numeric ID generator: + * + *
{@code
+ * AtomicLong counter = new AtomicLong(0);
+ * McpClient.async(transport)
+ * .requestIdGenerator(() -> String.valueOf(counter.incrementAndGet()))
+ * .build();
+ * }
+ * @param requestIdGenerator The generator for creating unique request IDs. If
+ * null, a default UUID-prefixed generator will be used.
+ * @return This builder instance for method chaining
+ * @see RequestIdGenerator#ofIncremental()
+ * @see RequestIdGenerator#ofDefault()
+ */
+ public AsyncSpec requestIdGenerator(RequestIdGenerator requestIdGenerator) {
+ this.requestIdGenerator = requestIdGenerator;
+ return this;
+ }
+
/**
* Create an instance of {@link McpAsyncClient} with the provided configurations
* or sensible defaults.
@@ -815,7 +870,8 @@ public McpAsyncClient build() {
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
- this.samplingHandler, this.elicitationHandler, this.enableCallToolSchemaCaching));
+ this.samplingHandler, this.elicitationHandler, this.enableCallToolSchemaCaching),
+ this.requestIdGenerator);
}
}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
index 0ba7ab3b8..cd97f60bf 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
@@ -14,9 +14,7 @@
import java.time.Duration;
import java.util.Map;
-import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
/**
@@ -56,11 +54,8 @@ public class McpClientSession implements McpSession {
/** Map of notification handlers keyed by method name */
private final ConcurrentHashMap+ * Implementations of this interface are responsible for generating unique IDs that are + * used to correlate requests with their corresponding responses in JSON-RPC + * communication. + * + *
+ * The MCP specification requires that: + *
+ * Example usage with a simple numeric ID generator: + * + *
{@code
+ * AtomicLong counter = new AtomicLong(0);
+ * RequestIdGenerator generator = () -> String.valueOf(counter.incrementAndGet());
+ * }
+ *
+ * @author Christian Tzolov
+ * @see McpClientSession
+ */
+@FunctionalInterface
+public interface RequestIdGenerator {
+
+ /**
+ * Generates a unique request ID.
+ *
+ * + * The generated ID must be unique within the session and must not be null. + * Implementations should ensure thread-safety if the generator may be called from + * multiple threads. + * @return a unique request ID as a String + */ + String generate(); + + /** + * Creates a default request ID generator that produces UUID-prefixed incrementing + * IDs. + * + *
+ * The generated IDs follow the format: {@code <8-char-uuid>-
+ * This generator is useful for MCP servers that require strictly numeric request IDs
+ * (such as the Snowflake MCP server).
+ *
+ *
+ * The generated IDs are: {@code "1"}, {@code "2"}, {@code "3"}, etc.
+ * @return a new numeric request ID generator
+ */
+ static RequestIdGenerator ofIncremental() {
+ AtomicLong counter = new AtomicLong(0);
+ return () -> String.valueOf(counter.incrementAndGet());
+ }
+
+}
diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java
index 3de06f503..d263b0d1d 100644
--- a/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java
+++ b/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java
@@ -6,6 +6,7 @@
import java.time.Duration;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import io.modelcontextprotocol.MockMcpClientTransport;
@@ -310,4 +311,86 @@ void testGracefulShutdown() {
StepVerifier.create(session.closeGracefully()).verifyComplete();
}
+ @Test
+ void testCustomRequestIdGeneratorWithNumericIds() {
+ AtomicLong counter = new AtomicLong(0);
+ RequestIdGenerator numericIdGenerator = () -> String.valueOf(counter.incrementAndGet());
+
+ var transport = new MockMcpClientTransport();
+ var session = new McpClientSession(TIMEOUT, transport, Map.of(), Map.of(), Function.identity(),
+ numericIdGenerator);
+
+ // Send first request
+ Mono