Skip to content

Commit f15f1ac

Browse files
committed
add SEP-1577 sampling with tools support
Introduces parallel V2 sampling types (SamplingMessageV2, CreateMessageWithToolsRequest/Result, ToolUseContent, ToolResultContent, ToolChoice) alongside the existing V1 types, leaving the legacy wire format byte-identical. Servers call createMessageWithTools on the exchange; a version gate refuses multi-content or tools payloads when the negotiated protocol version is older than 2025-11-25. Clients opt in via samplingWithTools(handler) on the builder, which advertises sampling.tools in the capability handshake. StopReason gains TOOL_USE. Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
1 parent df75857 commit f15f1ac

12 files changed

Lines changed: 1234 additions & 81 deletions

File tree

MIGRATION-2.0.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,13 @@ The 1.x canonical 4-arg constructors continue to compile.
173173
When a `JsonSchemaValidator` is available (including the default from `McpJsonDefaults.getSchemaValidator()` when you do not configure one explicitly) and `validateToolInputs` is left at its default of `true`, the server validates incoming tool arguments against `tool.inputSchema()` before invoking the tool. Failed validation produces a `CallToolResult` with `isError` set and a textual error in the content.
174174

175175
**Action:** Ensure `inputSchema` maps are valid for your validator, tighten client arguments, or disable validation with `validateToolInputs(false)` on the server builder if you must preserve pre-2.0 behaviour.
176+
177+
---
178+
179+
## Sampling with tools (SEP-1577)
180+
181+
### New `StopReason.TOOL_USE` enum constant
182+
183+
`McpSchema.CreateMessageResult.StopReason` gains a new value, `TOOL_USE("toolUse")`. Exhaustive `switch` *expressions* over the enum (which the compiler enforces at compile time) that covered all pre-existing constants without a `default` branch will no longer compile.
184+
185+
**Action:** Add a `case TOOL_USE` branch or a `default` branch to any exhaustive `switch` expression on `StopReason`.

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

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ public class McpAsyncClient {
103103
public static final TypeRef<CreateMessageRequest> CREATE_MESSAGE_REQUEST_TYPE_REF = new TypeRef<>() {
104104
};
105105

106+
public static final TypeRef<McpSchema.CreateMessageWithToolsRequest> CREATE_MESSAGE_WITH_TOOLS_REQUEST_TYPE_REF = new TypeRef<>() {
107+
};
108+
106109
public static final TypeRef<LoggingMessageNotification> LOGGING_MESSAGE_NOTIFICATION_TYPE_REF = new TypeRef<>() {
107110
};
108111

@@ -142,6 +145,8 @@ public class McpAsyncClient {
142145
*/
143146
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
144147

148+
private Function<McpSchema.CreateMessageWithToolsRequest, Mono<McpSchema.CreateMessageWithToolsResult>> samplingWithToolsHandler;
149+
145150
/**
146151
* MCP provides a standardized way for servers to request additional information from
147152
* users through the client during interactions. This flow allows clients to maintain
@@ -227,11 +232,21 @@ public class McpAsyncClient {
227232

228233
// Sampling Handler
229234
if (this.clientCapabilities.sampling() != null) {
230-
if (features.samplingHandler() == null) {
231-
throw new IllegalArgumentException(
232-
"Sampling handler must not be null when client capabilities include sampling");
235+
boolean withTools = this.clientCapabilities.sampling().tools() != null;
236+
if (withTools) {
237+
if (features.samplingWithToolsHandler() == null) {
238+
throw new IllegalArgumentException(
239+
"Sampling-with-tools handler must not be null when client capabilities include sampling.tools");
240+
}
241+
this.samplingWithToolsHandler = features.samplingWithToolsHandler();
242+
}
243+
else {
244+
if (features.samplingHandler() == null) {
245+
throw new IllegalArgumentException(
246+
"Sampling handler must not be null when client capabilities include sampling");
247+
}
248+
this.samplingHandler = features.samplingHandler();
233249
}
234-
this.samplingHandler = features.samplingHandler();
235250
requestHandlers.put(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE, samplingCreateMessageHandler());
236251
}
237252

@@ -575,11 +590,29 @@ private RequestHandler<McpSchema.ListRootsResult> rootsListRequestHandler() {
575590
// --------------------------
576591
// Sampling
577592
// --------------------------
578-
private RequestHandler<CreateMessageResult> samplingCreateMessageHandler() {
593+
private RequestHandler<Object> samplingCreateMessageHandler() {
579594
return params -> {
580-
McpSchema.CreateMessageRequest request = transport.unmarshalFrom(params, CREATE_MESSAGE_REQUEST_TYPE_REF);
581-
582-
return this.samplingHandler.apply(request);
595+
// Dispatch to V2 handler if the request carries tools/toolChoice, otherwise
596+
// V1
597+
McpSchema.CreateMessageWithToolsRequest v2Request = transport.unmarshalFrom(params,
598+
CREATE_MESSAGE_WITH_TOOLS_REQUEST_TYPE_REF);
599+
boolean isV2Request = (v2Request.tools() != null && !v2Request.tools().isEmpty())
600+
|| v2Request.toolChoice() != null;
601+
if (isV2Request) {
602+
if (this.samplingWithToolsHandler == null) {
603+
return Mono.error(new IllegalStateException(
604+
"Received sampling request with tools, but no samplingWithTools handler is registered. "
605+
+ "Use McpClient.async(transport).samplingWithTools(handler) to register one."));
606+
}
607+
return this.samplingWithToolsHandler.apply(v2Request).cast(Object.class);
608+
}
609+
// V1 path: unmarshal as the legacy type for handler type-safety
610+
McpSchema.CreateMessageRequest v1Request = transport.unmarshalFrom(params, CREATE_MESSAGE_REQUEST_TYPE_REF);
611+
if (this.samplingHandler == null) {
612+
return Mono.error(
613+
new IllegalStateException("Received sampling request, but no sampling handler is registered."));
614+
}
615+
return this.samplingHandler.apply(v1Request).cast(Object.class);
583616
};
584617
}
585618

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

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ class SyncSpec {
190190

191191
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
192192

193+
private Function<McpSchema.CreateMessageWithToolsRequest, McpSchema.CreateMessageWithToolsResult> samplingWithToolsHandler;
194+
193195
private Function<ElicitFormRequest, ElicitResult> formElicitationHandler;
194196

195197
private Function<ElicitUrlRequest, ElicitResult> urlElicitationHandler;
@@ -310,6 +312,23 @@ public SyncSpec sampling(Function<CreateMessageRequest, CreateMessageResult> sam
310312
return this;
311313
}
312314

315+
/**
316+
* Sets a sampling handler that supports tool use (SEP-1577) and registers the
317+
* {@code sampling.tools} client capability. Use this instead of
318+
* {@link #sampling(Function)} when the server may include {@code tools} and
319+
* {@code toolChoice} in its {@code sampling/createMessage} requests.
320+
* @param samplingWithToolsHandler A function that processes sampling-with-tools
321+
* requests and returns results. Must not be null.
322+
* @return This builder instance for method chaining
323+
* @throws IllegalArgumentException if samplingWithToolsHandler is null
324+
*/
325+
public SyncSpec samplingWithTools(
326+
Function<McpSchema.CreateMessageWithToolsRequest, McpSchema.CreateMessageWithToolsResult> samplingWithToolsHandler) {
327+
Assert.notNull(samplingWithToolsHandler, "Sampling-with-tools handler must not be null");
328+
this.samplingWithToolsHandler = samplingWithToolsHandler;
329+
return this;
330+
}
331+
313332
/**
314333
* Sets a custom elicitation handler for processing elicitation message requests.
315334
* The elicitation handler can modify or validate messages before they are sent to
@@ -554,7 +573,8 @@ public McpSyncClient build() {
554573
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
555574
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
556575
this.elicitationCompleteConsumers, this.samplingHandler, this.formElicitationHandler,
557-
this.urlElicitationHandler, this.enableCallToolSchemaCaching, this.applyElicitationDefaults);
576+
this.urlElicitationHandler, this.enableCallToolSchemaCaching, this.applyElicitationDefaults,
577+
this.samplingWithToolsHandler);
558578

559579
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
560580

@@ -611,6 +631,8 @@ class AsyncSpec {
611631

612632
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
613633

634+
private Function<McpSchema.CreateMessageWithToolsRequest, Mono<McpSchema.CreateMessageWithToolsResult>> samplingWithToolsHandler;
635+
614636
private Function<ElicitFormRequest, Mono<ElicitResult>> formElicitationHandler;
615637

616638
private Function<ElicitUrlRequest, Mono<ElicitResult>> urlElicitationHandler;
@@ -729,6 +751,23 @@ public AsyncSpec sampling(Function<CreateMessageRequest, Mono<CreateMessageResul
729751
return this;
730752
}
731753

754+
/**
755+
* Sets a sampling handler that supports tool use (SEP-1577) and registers the
756+
* {@code sampling.tools} client capability. Use this instead of
757+
* {@link #sampling(Function)} when the server may include {@code tools} and
758+
* {@code toolChoice} in its {@code sampling/createMessage} requests.
759+
* @param samplingWithToolsHandler A function that processes sampling-with-tools
760+
* requests and returns results. Must not be null.
761+
* @return This builder instance for method chaining
762+
* @throws IllegalArgumentException if samplingWithToolsHandler is null
763+
*/
764+
public AsyncSpec samplingWithTools(
765+
Function<McpSchema.CreateMessageWithToolsRequest, Mono<McpSchema.CreateMessageWithToolsResult>> samplingWithToolsHandler) {
766+
Assert.notNull(samplingWithToolsHandler, "Sampling-with-tools handler must not be null");
767+
this.samplingWithToolsHandler = samplingWithToolsHandler;
768+
return this;
769+
}
770+
732771
/**
733772
* Sets a custom elicitation handler for processing elicitation message requests.
734773
* The elicitation handler can modify or validate messages before they are sent to
@@ -964,8 +1003,8 @@ public McpAsyncClient build() {
9641003
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
9651004
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
9661005
this.elicitationCompleteConsumers, this.samplingHandler, this.formElicitationHandler,
967-
this.urlElicitationHandler, this.enableCallToolSchemaCaching,
968-
this.applyElicitationDefaults));
1006+
this.urlElicitationHandler, this.enableCallToolSchemaCaching, this.applyElicitationDefaults,
1007+
this.samplingWithToolsHandler));
9691008
}
9701009

9711010
}

0 commit comments

Comments
 (0)