Skip to content

Commit 640dcdb

Browse files
committed
feat: Add RequestIdGenerator for custom request ID generation
Adds support for configuring custom request ID generators, enabling compatibility with MCP servers that require specific ID formats (e.g., numeric-only IDs). Changes: - Add RequestIdGenerator functional interface with factory methods for default (UUID-prefixed) and incremental (numeric) generators - Update McpClientSession to accept a custom RequestIdGenerator - Add requestIdGenerator() builder methods to McpClient.SyncSpec and McpClient.AsyncSpec - Add comprehensive tests for custom ID generators
1 parent fa9dac8 commit 640dcdb

File tree

5 files changed

+271
-14
lines changed

5 files changed

+271
-14
lines changed

mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import io.modelcontextprotocol.spec.McpClientSession.RequestHandler;
2424
import io.modelcontextprotocol.spec.McpClientTransport;
2525
import io.modelcontextprotocol.spec.McpSchema;
26+
import io.modelcontextprotocol.spec.RequestIdGenerator;
2627
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
2728
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
2829
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
@@ -182,6 +183,24 @@ public class McpAsyncClient {
182183
*/
183184
McpAsyncClient(McpClientTransport transport, Duration requestTimeout, Duration initializationTimeout,
184185
JsonSchemaValidator jsonSchemaValidator, McpClientFeatures.Async features) {
186+
this(transport, requestTimeout, initializationTimeout, jsonSchemaValidator, features, null);
187+
}
188+
189+
/**
190+
* Create a new McpAsyncClient with the given transport and session request-response
191+
* timeout.
192+
* @param transport the transport to use.
193+
* @param requestTimeout the session request-response timeout.
194+
* @param initializationTimeout the max timeout to await for the client-server
195+
* @param jsonSchemaValidator the JSON schema validator to use for validating tool
196+
* @param features the MCP Client supported features. responses against output
197+
* schemas.
198+
* @param requestIdGenerator the generator for creating unique request IDs. If null, a
199+
* default generator will be used.
200+
*/
201+
McpAsyncClient(McpClientTransport transport, Duration requestTimeout, Duration initializationTimeout,
202+
JsonSchemaValidator jsonSchemaValidator, McpClientFeatures.Async features,
203+
RequestIdGenerator requestIdGenerator) {
185204

186205
Assert.notNull(transport, "Transport must not be null");
187206
Assert.notNull(requestTimeout, "Request timeout must not be null");
@@ -315,9 +334,11 @@ public class McpAsyncClient {
315334
}).then();
316335
};
317336

337+
RequestIdGenerator effectiveIdGenerator = requestIdGenerator != null ? requestIdGenerator
338+
: RequestIdGenerator.ofDefault();
318339
this.initializer = new LifecycleInitializer(clientCapabilities, clientInfo, transport.protocolVersions(),
319340
initializationTimeout, ctx -> new McpClientSession(requestTimeout, transport, requestHandlers,
320-
notificationHandlers, con -> con.contextWrite(ctx)),
341+
notificationHandlers, con -> con.contextWrite(ctx), effectiveIdGenerator),
321342
postInitializationHook);
322343

323344
this.transport.setExceptionHandler(this.initializer::handleException);

mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import io.modelcontextprotocol.common.McpTransportContext;
1818
import io.modelcontextprotocol.spec.McpClientTransport;
1919
import io.modelcontextprotocol.spec.McpSchema;
20+
import io.modelcontextprotocol.spec.RequestIdGenerator;
2021
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
2122
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
2223
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
@@ -193,6 +194,8 @@ class SyncSpec {
193194

194195
private boolean enableCallToolSchemaCaching = false; // Default to false
195196

197+
private RequestIdGenerator requestIdGenerator;
198+
196199
private SyncSpec(McpClientTransport transport) {
197200
Assert.notNull(transport, "Transport must not be null");
198201
this.transport = transport;
@@ -461,6 +464,31 @@ public SyncSpec enableCallToolSchemaCaching(boolean enableCallToolSchemaCaching)
461464
return this;
462465
}
463466

467+
/**
468+
* Sets a custom request ID generator for creating unique request IDs. This is
469+
* useful for MCP servers that require specific ID formats, such as numeric-only
470+
* IDs.
471+
*
472+
* <p>
473+
* Example usage with a numeric ID generator:
474+
*
475+
* <pre>{@code
476+
* AtomicLong counter = new AtomicLong(0);
477+
* McpClient.sync(transport)
478+
* .requestIdGenerator(() -> String.valueOf(counter.incrementAndGet()))
479+
* .build();
480+
* }</pre>
481+
* @param requestIdGenerator The generator for creating unique request IDs. If
482+
* null, a default UUID-prefixed generator will be used.
483+
* @return This builder instance for method chaining
484+
* @see RequestIdGenerator#ofIncremental()
485+
* @see RequestIdGenerator#ofDefault()
486+
*/
487+
public SyncSpec requestIdGenerator(RequestIdGenerator requestIdGenerator) {
488+
this.requestIdGenerator = requestIdGenerator;
489+
return this;
490+
}
491+
464492
/**
465493
* Create an instance of {@link McpSyncClient} with the provided configurations or
466494
* sensible defaults.
@@ -475,8 +503,8 @@ public McpSyncClient build() {
475503
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
476504

477505
return new McpSyncClient(new McpAsyncClient(transport, this.requestTimeout, this.initializationTimeout,
478-
jsonSchemaValidator != null ? jsonSchemaValidator : JsonSchemaValidator.getDefault(),
479-
asyncFeatures), this.contextProvider);
506+
jsonSchemaValidator != null ? jsonSchemaValidator : JsonSchemaValidator.getDefault(), asyncFeatures,
507+
this.requestIdGenerator), this.contextProvider);
480508
}
481509

482510
}
@@ -531,6 +559,8 @@ class AsyncSpec {
531559

532560
private boolean enableCallToolSchemaCaching = false; // Default to false
533561

562+
private RequestIdGenerator requestIdGenerator;
563+
534564
private AsyncSpec(McpClientTransport transport) {
535565
Assert.notNull(transport, "Transport must not be null");
536566
this.transport = transport;
@@ -802,6 +832,31 @@ public AsyncSpec enableCallToolSchemaCaching(boolean enableCallToolSchemaCaching
802832
return this;
803833
}
804834

835+
/**
836+
* Sets a custom request ID generator for creating unique request IDs. This is
837+
* useful for MCP servers that require specific ID formats, such as numeric-only
838+
* IDs.
839+
*
840+
* <p>
841+
* Example usage with a numeric ID generator:
842+
*
843+
* <pre>{@code
844+
* AtomicLong counter = new AtomicLong(0);
845+
* McpClient.async(transport)
846+
* .requestIdGenerator(() -> String.valueOf(counter.incrementAndGet()))
847+
* .build();
848+
* }</pre>
849+
* @param requestIdGenerator The generator for creating unique request IDs. If
850+
* null, a default UUID-prefixed generator will be used.
851+
* @return This builder instance for method chaining
852+
* @see RequestIdGenerator#ofIncremental()
853+
* @see RequestIdGenerator#ofDefault()
854+
*/
855+
public AsyncSpec requestIdGenerator(RequestIdGenerator requestIdGenerator) {
856+
this.requestIdGenerator = requestIdGenerator;
857+
return this;
858+
}
859+
805860
/**
806861
* Create an instance of {@link McpAsyncClient} with the provided configurations
807862
* or sensible defaults.
@@ -815,7 +870,8 @@ public McpAsyncClient build() {
815870
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
816871
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
817872
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
818-
this.samplingHandler, this.elicitationHandler, this.enableCallToolSchemaCaching));
873+
this.samplingHandler, this.elicitationHandler, this.enableCallToolSchemaCaching),
874+
this.requestIdGenerator);
819875
}
820876

821877
}

mcp-core/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414

1515
import java.time.Duration;
1616
import java.util.Map;
17-
import java.util.UUID;
1817
import java.util.concurrent.ConcurrentHashMap;
19-
import java.util.concurrent.atomic.AtomicLong;
2018
import java.util.function.Function;
2119

2220
/**
@@ -56,11 +54,8 @@ public class McpClientSession implements McpSession {
5654
/** Map of notification handlers keyed by method name */
5755
private final ConcurrentHashMap<String, NotificationHandler> notificationHandlers = new ConcurrentHashMap<>();
5856

59-
/** Session-specific prefix for request IDs */
60-
private final String sessionPrefix = UUID.randomUUID().toString().substring(0, 8);
61-
62-
/** Atomic counter for generating unique request IDs */
63-
private final AtomicLong requestCounter = new AtomicLong(0);
57+
/** Generator for creating unique request IDs */
58+
private final RequestIdGenerator requestIdGenerator;
6459

6560
/**
6661
* Functional interface for handling incoming JSON-RPC requests. Implementations
@@ -123,6 +118,26 @@ public McpClientSession(Duration requestTimeout, McpClientTransport transport,
123118
public McpClientSession(Duration requestTimeout, McpClientTransport transport,
124119
Map<String, RequestHandler<?>> requestHandlers, Map<String, NotificationHandler> notificationHandlers,
125120
Function<? super Mono<Void>, ? extends Publisher<Void>> connectHook) {
121+
this(requestTimeout, transport, requestHandlers, notificationHandlers, connectHook,
122+
RequestIdGenerator.ofDefault());
123+
}
124+
125+
/**
126+
* Creates a new McpClientSession with the specified configuration, handlers, and
127+
* custom request ID generator.
128+
* @param requestTimeout Duration to wait for responses
129+
* @param transport Transport implementation for message exchange
130+
* @param requestHandlers Map of method names to request handlers
131+
* @param notificationHandlers Map of method names to notification handlers
132+
* @param connectHook Hook that allows transforming the connection Publisher prior to
133+
* subscribing
134+
* @param requestIdGenerator Generator for creating unique request IDs. If null, a
135+
* default generator will be used.
136+
*/
137+
public McpClientSession(Duration requestTimeout, McpClientTransport transport,
138+
Map<String, RequestHandler<?>> requestHandlers, Map<String, NotificationHandler> notificationHandlers,
139+
Function<? super Mono<Void>, ? extends Publisher<Void>> connectHook,
140+
RequestIdGenerator requestIdGenerator) {
126141

127142
Assert.notNull(requestTimeout, "The requestTimeout can not be null");
128143
Assert.notNull(transport, "The transport can not be null");
@@ -133,6 +148,7 @@ public McpClientSession(Duration requestTimeout, McpClientTransport transport,
133148
this.transport = transport;
134149
this.requestHandlers.putAll(requestHandlers);
135150
this.notificationHandlers.putAll(notificationHandlers);
151+
this.requestIdGenerator = requestIdGenerator != null ? requestIdGenerator : RequestIdGenerator.ofDefault();
136152

137153
this.transport.connect(mono -> mono.doOnNext(this::handle)).transform(connectHook).subscribe();
138154
}
@@ -243,12 +259,11 @@ private Mono<Void> handleIncomingNotification(McpSchema.JSONRPCNotification noti
243259
}
244260

245261
/**
246-
* Generates a unique request ID in a non-blocking way. Combines a session-specific
247-
* prefix with an atomic counter to ensure uniqueness.
262+
* Generates a unique request ID using the configured request ID generator.
248263
* @return A unique request ID string
249264
*/
250265
private String generateRequestId() {
251-
return this.sessionPrefix + "-" + this.requestCounter.getAndIncrement();
266+
return this.requestIdGenerator.generate();
252267
}
253268

254269
/**
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2024-2025 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.spec;
6+
7+
import java.util.UUID;
8+
import java.util.concurrent.atomic.AtomicLong;
9+
10+
/**
11+
* A generator for creating unique request IDs for JSON-RPC messages.
12+
*
13+
* <p>
14+
* Implementations of this interface are responsible for generating unique IDs that are
15+
* used to correlate requests with their corresponding responses in JSON-RPC
16+
* communication.
17+
*
18+
* <p>
19+
* The MCP specification requires that:
20+
* <ul>
21+
* <li>Request IDs MUST be a string or integer</li>
22+
* <li>Request IDs MUST NOT be null</li>
23+
* <li>Request IDs MUST NOT have been previously used within the same session</li>
24+
* </ul>
25+
*
26+
* <p>
27+
* Example usage with a simple numeric ID generator:
28+
*
29+
* <pre>{@code
30+
* AtomicLong counter = new AtomicLong(0);
31+
* RequestIdGenerator generator = () -> String.valueOf(counter.incrementAndGet());
32+
* }</pre>
33+
*
34+
* @author Christian Tzolov
35+
* @see McpClientSession
36+
*/
37+
@FunctionalInterface
38+
public interface RequestIdGenerator {
39+
40+
/**
41+
* Generates a unique request ID.
42+
*
43+
* <p>
44+
* The generated ID must be unique within the session and must not be null.
45+
* Implementations should ensure thread-safety if the generator may be called from
46+
* multiple threads.
47+
* @return a unique request ID as a String
48+
*/
49+
String generate();
50+
51+
/**
52+
* Creates a default request ID generator that produces UUID-prefixed incrementing
53+
* IDs.
54+
*
55+
* <p>
56+
* The generated IDs follow the format: {@code <8-char-uuid>-<counter>}, for example:
57+
* {@code "a1b2c3d4-0"}, {@code "a1b2c3d4-1"}, etc.
58+
* @return a new default request ID generator
59+
*/
60+
static RequestIdGenerator ofDefault() {
61+
String sessionPrefix = UUID.randomUUID().toString().substring(0, 8);
62+
AtomicLong counter = new AtomicLong(0);
63+
return () -> sessionPrefix + "-" + counter.getAndIncrement();
64+
}
65+
66+
/**
67+
* Creates a request ID generator that produces simple incrementing numeric IDs.
68+
*
69+
* <p>
70+
* This generator is useful for MCP servers that require strictly numeric request IDs
71+
* (such as the Snowflake MCP server).
72+
*
73+
* <p>
74+
* The generated IDs are: {@code "1"}, {@code "2"}, {@code "3"}, etc.
75+
* @return a new numeric request ID generator
76+
*/
77+
static RequestIdGenerator ofIncremental() {
78+
AtomicLong counter = new AtomicLong(0);
79+
return () -> String.valueOf(counter.incrementAndGet());
80+
}
81+
82+
}

0 commit comments

Comments
 (0)