Skip to content

feat!: SEP-1577 sampling with tools#718

Open
devcrocod wants to merge 1 commit intomainfrom
devcrocod/sep-1577-sampling-with-tools
Open

feat!: SEP-1577 sampling with tools#718
devcrocod wants to merge 1 commit intomainfrom
devcrocod/sep-1577-sampling-with-tools

Conversation

@devcrocod
Copy link
Copy Markdown
Contributor

Implements SEP-1577 sampling With tools

closes #409

How Has This Been Tested?

unit + integration

Breaking Changes

  • binary changes
  • MediaContent -> SamplingMessageContent

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.content become list-based with single-or-array wire encoding, plus new ToolUseContent / ToolResultContent.
  • Adds tools / toolChoice to CreateMessageRequestParams, StopReason.ToolUse, and typed ClientCapabilities.Sampling with tools / context sub-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.

Comment on lines +431 to +442
@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"}]}"""
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
),
)
CallToolResult(listOf(TextContent(result.content.toString())))
CallToolResult(listOf(TextContent(result.content.joinToString("\n") { it.toString() })))
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Suggested change
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)))

Copilot uses AI. Check for mistakes.
* `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.
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
* 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`.

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +49
require(toolUseIds == toolResultIds) {
"ids of tool_result blocks and tool_use blocks from previous message do not match"
}
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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"
}

Copilot uses AI. Check for mistakes.
val content: List<SamplingMessageContent>,
@SerialName("_meta")
val meta: JsonObject? = null,
) {
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
) {
) {
init {
require(content.isNotEmpty()) {
"content must contain at least one block"
}
}

Copilot uses AI. Check for mistakes.
@SerialName("_meta")
override val meta: JsonObject? = null,
) : ClientResult
) : ClientResult {
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
) : ClientResult {
) : ClientResult {
init {
require(content.isNotEmpty()) { "CreateMessageResult.content must contain at least one block." }
}

Copilot uses AI. Check for mistakes.
Comment on lines 248 to 262
@@ -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)))
}
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement SEP-1577: Sampling With Tools

2 participants