Conversation
There was a problem hiding this comment.
Pull request overview
Implements SEP-1577 “Sampling With Tools” across the Kotlin SDKs, adding tool-call content blocks and capability-gated tool support while keeping wire compatibility with pre-SEP single-block content.
Changes:
- Introduces SEP-1577 sampling content model:
SamplingMessage.content/CreateMessageResult.contentbecome list-based with single-or-array wire encoding, plus newToolUseContent/ToolResultContent. - Adds
tools/toolChoicetoCreateMessageRequestParams,StopReason.ToolUse, and typedClientCapabilities.Samplingwithtools/contextsub-capabilities. - Enforces capability gates and validation on both server (
ClientConnectionImpl.createMessage) and client (request-handler wrapper), with updated unit/integration/conformance tests.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt | Adds unit tests for server-side sampling message tail validation rules. |
| kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingValidation.kt | Implements last-two-message tool_use/tool_result validation helper. |
| kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ClientConnection.kt | Enforces sampling.tools / soft-deprecates includeContext via sampling.context and validates message tail. |
| kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt | Updates DSL tests for list-based sampling message content. |
| kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/InitializeDslTest.kt | Updates initialize DSL tests for typed sampling capabilities. |
| kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/ContentDslTest.kt | Updates content DSL tests to use single-element lists (content.single()). |
| kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/CapabilitiesDslTest.kt | Updates capabilities DSL tests for typed ClientCapabilities.Sampling. |
| kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt | Adds/updates serialization tests for SEP-1577 list content, tools/toolChoice, ToolUse stopReason, and serializers. |
| kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt | Updates initialize tests for typed sampling capability default. |
| kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ContentTest.kt | Adds tests for new sampling content polymorphism + tool_use/tool_result blocks. |
| kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt | Updates capabilities tests and adds SEP-1577 sampling sub-capability tests. |
| kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/serializers.kt | Adds polymorphic serializer for sampling message content + single-or-array content wire serializer. |
| kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.kt | Changes sampling message/result content types to lists, adds tools/toolChoice + ToolUse stopReason, adds meta on SamplingMessage. |
| kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/sampling.dsl.kt | Updates sampling DSL to wrap single media blocks in list form. |
| kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/initialize.dsl.kt | Updates docs/examples for typed sampling capabilities in initialize DSL. |
| kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/content.kt | Adds tool content types and introduces SamplingMessageContent hierarchy; extends ContentTypes. |
| kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.kt | Makes ClientCapabilities.sampling typed (Sampling) and adds SEP-1577 sub-capabilities + deprecation bridge ctor. |
| kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/capabilities.dsl.kt | Updates capabilities DSL to accept typed ClientCapabilities.Sampling only. |
| kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt | Adds wrapRequestHandler hook and uses it when registering request handlers. |
| kotlin-sdk-core/api/kotlin-sdk-core.api | Updates public API surface to reflect new types/constructors and handler wrapper hook. |
| kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientSamplingValidationTest.kt | Adds client-side tests ensuring sampling.tools is required when tools/toolChoice present. |
| kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/SamplingValidation.kt | Implements client-side enforcement for sampling.tools on incoming createMessage requests. |
| kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt | Wraps sampling/createMessage request handlers to enforce sampling.tools before user handler runs. |
| kotlin-sdk-client/api/kotlin-sdk-client.api | Updates client API surface to expose inherited handler wrapping hook. |
| integration-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/SamplingTest.kt | Adds end-to-end tests for tool-loop sampling and capability enforcement. |
| integration-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/ClientTest.kt | Updates integration client tests for typed sampling capability and new content shapes. |
| conformance-test/src/main/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTools.kt | Updates sampling conformance tool to construct list-based sampling messages and handle list-based results. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @Test | ||
| fun `SamplingContentSerializer encodes list of size one as a single object`() { | ||
| val h = Holder(content = listOf(TextContent("hi"))) | ||
| val json = McpJson.encodeToString(Holder.serializer(), h) | ||
| json shouldBe """{"content":{"text":"hi","type":"text"}}""" | ||
| } | ||
|
|
||
| @Test | ||
| fun `SamplingContentSerializer encodes list of size two as an array`() { | ||
| val h = Holder(content = listOf(TextContent("a"), TextContent("b"))) | ||
| val json = McpJson.encodeToString(Holder.serializer(), h) | ||
| json shouldBe """{"content":[{"text":"a","type":"text"},{"text":"b","type":"text"}]}""" |
There was a problem hiding this comment.
These assertions compare raw JSON strings, which makes the tests sensitive to key ordering/formatting (JSON object field order is not semantically meaningful and can change with serializer configuration). Prefer comparing parsed JsonElements (or using shouldEqualJson) so the test verifies structure rather than string layout.
| ), | ||
| ) | ||
| CallToolResult(listOf(TextContent(result.content.toString()))) | ||
| CallToolResult(listOf(TextContent(result.content.joinToString("\n") { it.toString() }))) |
There was a problem hiding this comment.
result.content.joinToString("\n") { it.toString() } will produce Kotlin data-class toString() representations (e.g. TextContent(text=..., type=...)) rather than the model’s text output. If this conformance tool is intended to return the sampled text, consider extracting TextContent.text (and deciding how to handle non-text blocks/tool_use blocks) instead of relying on toString().
| CallToolResult(listOf(TextContent(result.content.joinToString("\n") { it.toString() }))) | |
| val sampledText = result.content.joinToString("\n") { | |
| when (it) { | |
| is TextContent -> it.text | |
| is ImageContent -> "[image content]" | |
| is AudioContent -> "[audio content]" | |
| is EmbeddedResource -> "[embedded resource]" | |
| } | |
| } | |
| CallToolResult(listOf(TextContent(sampledText))) |
| * `tool_result` ids MUST form exactly the same set. | ||
| * | ||
| * On the first violation throws [IllegalArgumentException]. No-op when there are fewer | ||
| * than two messages or no tool_use / tool_result blocks are involved. |
There was a problem hiding this comment.
KDoc says this validator is a no-op when there are fewer than two messages, but the implementation will still throw for a single-message list that contains a tool_result (see unit test validate orphan tool_result with no previous message fails). Please update the KDoc to match the actual behavior (e.g., only no-op when fewer than two messages and no tool_result/tool_use rules can be evaluated).
| * than two messages or no tool_use / tool_result blocks are involved. | |
| * than two messages only if no tool_use / tool_result rule can be evaluated from the | |
| * tail; for example, a single message containing `tool_result` still fails because it | |
| * has no previous matching `tool_use`. |
| require(toolUseIds == toolResultIds) { | ||
| "ids of tool_result blocks and tool_use blocks from previous message do not match" | ||
| } |
There was a problem hiding this comment.
When the previous message has tool_use blocks but the last message has no tool_result blocks, this require(toolUseIds == toolResultIds) fails with an “ids … do not match” message. That error is misleading in the common "missing tool_result" case; consider checking for presence/count first and emitting a clearer message (ideally including the expected/missing ids).
| require(toolUseIds == toolResultIds) { | |
| "ids of tool_result blocks and tool_use blocks from previous message do not match" | |
| } | |
| require(toolResultIds.isNotEmpty()) { | |
| "The last message must contain tool_result blocks for all previous tool_use ids; expected ids=$toolUseIds, missing ids=$toolUseIds" | |
| } | |
| val missingToolResultIds = toolUseIds - toolResultIds | |
| val unexpectedToolResultIds = toolResultIds - toolUseIds | |
| require(missingToolResultIds.isEmpty() && unexpectedToolResultIds.isEmpty()) { | |
| "ids of tool_result blocks and tool_use blocks from previous message do not match; expected ids=$toolUseIds, actual ids=$toolResultIds, missing ids=$missingToolResultIds, unexpected ids=$unexpectedToolResultIds" | |
| } |
| val content: List<SamplingMessageContent>, | ||
| @SerialName("_meta") | ||
| val meta: JsonObject? = null, | ||
| ) { |
There was a problem hiding this comment.
SamplingMessage.content is documented as “MUST contain at least one block”, but the data class allows constructing SamplingMessage(role, emptyList()). This will only fail later during serialization (via SamplingContentSerializer), producing a less actionable error and potentially allowing invalid in-memory states. Consider enforcing this invariant in init { require(content.isNotEmpty()) } (and similarly for the convenience constructor).
| ) { | |
| ) { | |
| init { | |
| require(content.isNotEmpty()) { | |
| "content must contain at least one block" | |
| } | |
| } |
| @SerialName("_meta") | ||
| override val meta: JsonObject? = null, | ||
| ) : ClientResult | ||
| ) : ClientResult { |
There was a problem hiding this comment.
CreateMessageResult.content is documented as requiring at least one block, but the public API currently allows content = emptyList(), which will only fail at serialization time. Consider adding an init invariant (e.g., require(content.isNotEmpty())) so invalid results are rejected immediately with a clear message.
| ) : ClientResult { | |
| ) : ClientResult { | |
| init { | |
| require(content.isNotEmpty()) { "CreateMessageResult.content must contain at least one block." } | |
| } |
| @@ -258,7 +258,7 @@ public class SamplingMessageBuilder @PublishedApi internal constructor() { | |||
| * ``` | |||
| */ | |||
| public fun assistant(content: MediaContent) { | |||
| messages.add(SamplingMessage(Role.Assistant, content)) | |||
| messages.add(SamplingMessage(Role.Assistant, listOf(content))) | |||
| } | |||
There was a problem hiding this comment.
SamplingMessageBuilder.user/assistant currently accepts only MediaContent, which prevents constructing SEP-1577 conversations containing ToolUseContent / ToolResultContent (or multi-block turns) via the DSL. Consider widening the parameter type to SamplingMessageContent and/or adding overloads for tool_use/tool_result and multi-block content lists.
Implements SEP-1577 sampling With tools
closes #409
How Has This Been Tested?
unit + integration
Breaking Changes
Types of changes
Checklist