Skip to content

Implement SEP-1577: Sampling With Tools#628

Merged
alexhancock merged 2 commits intomodelcontextprotocol:mainfrom
DaleSeo:SEP-1577
Feb 6, 2026
Merged

Implement SEP-1577: Sampling With Tools#628
alexhancock merged 2 commits intomodelcontextprotocol:mainfrom
DaleSeo:SEP-1577

Conversation

@DaleSeo
Copy link
Contributor

@DaleSeo DaleSeo commented Jan 24, 2026

Closes #552

Motivation and Context

This PR implements SEP-1577: Sampling With Tools, enabling MCP servers to run agentic loops using the client's LLM while maintaining user supervision.

Key additions:

  • ToolChoice / ToolChoiceMode - Control tool selection behavior
  • ToolUseContent / ToolResultContent - Tool calling content types
  • SamplingContent<T> - Single or array content wrapper
  • SamplingMessageContent - Unified content enum with ToolUse and ToolResult variants
  • SamplingCapability - Structured capability with tools and context sub-capabilities

Reference implementations:

How Has This Been Tested?

  • New unit tests covering serialization, deserialization, and API usage for all new types
  • All existing tests updated and passing
  • Backward compatibility tests verifying old JSON formats still deserialize correctly

Breaking Changes

The type signature of SamplingMessage.content changed from Content to SamplingContent<SamplingMessageContent>.

Migration Made Easy

Convenience constructors (recommended):

// Before
let msg = SamplingMessage { role: Role::User, content: Content::text("hi") };

// After - use the new helper methods
let msg = SamplingMessage::user_text("hi");
let msg = SamplingMessage::assistant_text("Hello");

Converting existing Content values:

use std::convert::TryInto;

let content: Content = Content::text("hello");

// Convert to SamplingMessageContent
let sampling_content: SamplingMessageContent = content.try_into()?;

// Or convert directly to SamplingContent<SamplingMessageContent>
let content: Content = Content::text("hello");
let wrapped: SamplingContent<SamplingMessageContent> = content.try_into()?;

Note: TryFrom is used because Content::Resource and Content::ResourceLink variants are not supported in sampling messages.

Wire Format Compatibility

  • Deserialization is backward compatible - Old JSON format (single content object) still deserializes correctly
  • ClientCapabilities.sampling - Empty {} JSON still deserializes to SamplingCapability

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

Additional context

// Advertise capability
let capabilities = ClientCapabilities::builder()
    .enable_sampling()
    .enable_sampling_tools()
    .build();

// Create request with tools
let params = CreateMessageRequestParams {
    messages: vec![SamplingMessage::user_text("What's the weather?")],
    tools: Some(vec![weather_tool]),
    tool_choice: Some(ToolChoice::auto()),
    max_tokens: 1000,
    ..Default::default()
};

// Handle tool use response  
if result.stop_reason.as_deref() == Some(CreateMessageResult::STOP_REASON_TOOL_USE) {
    for content in result.message.content.iter() {
        if let Some(tool_use) = content.as_tool_use() {
            // Execute tool and continue conversation
        }
    }
}

@github-actions github-actions bot added T-test Testing related changes T-config Configuration file changes T-core Core library changes T-examples Example code changes T-model Model/data structure changes labels Jan 24, 2026
@github-actions github-actions bot added the T-documentation Documentation improvements label Jan 24, 2026
@github-actions github-actions bot removed the T-documentation Documentation improvements label Jan 24, 2026
@DaleSeo DaleSeo marked this pull request as ready for review January 25, 2026 02:37
@DaleSeo DaleSeo force-pushed the SEP-1577 branch 6 times, most recently from c2da172 to f037e01 Compare January 25, 2026 03:08
@DaleSeo
Copy link
Contributor Author

DaleSeo commented Jan 28, 2026

Hi @alexhancock, could you please take a look at this PR? Thanks!

@alexhancock alexhancock self-requested a review January 30, 2026 20:20
@alexhancock
Copy link
Collaborator

@DaleSeo Sorry it took me a bit. I'm glad to have support for this, but a little worried about the breaking change. Can you think of any alternatives that would keep compat, or make it a but lighter for updaters?

Interested to discuss!

@DaleSeo DaleSeo force-pushed the SEP-1577 branch 2 times, most recently from 95e640e to 73e60d7 Compare February 4, 2026 21:56
@DaleSeo
Copy link
Contributor Author

DaleSeo commented Feb 4, 2026

Thanks for the feedback, @alexhancock! I've made a few updates to simplify the migration:

  1. I added TryFrom conversions for existing Content values:
use std::convert::TryInto;

let content: Content = Content::text("hello");
let sampling_content: SamplingMessageContent = content.try_into()?;
  1. The wire format is backward compatible. The old JSON (single content object) still deserializes correctly, so existing serialized data will remain intact.

  2. I created convenience constructors to make common cases easier:

// Before
SamplingMessage { role: Role::User, content: Content::text("hi") }

// After
SamplingMessage::user_text("hi")

The type change is unavoidable to support tool use and result content types from SEP-1577, but I think the migration path is now reasonable. Most users can either use the new constructors or add .try_into() to their existing code.

Let me know if you have any other suggestions!

Copy link
Collaborator

@alexhancock alexhancock left a comment

Choose a reason for hiding this comment

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

Thanks @DaleSeo, approving but also leaving this list of recommendations.

Generated by Opus 4.5 comparing the diff to https://modelcontextprotocol.io/community/seps/1577--sampling-with-tools.md

## SEP-1577 Implementation Review - Recommended Changes

1. **Missing capability validation (MUST requirement)**: Add runtime validation to throw an error when `tools` or `toolChoice` are provided but `clientCapabilities.sampling.tools` is missing.

2. **Tool use/result balancing not enforced (MUST requirement)**: Add validation that every assistant message with `ToolUseContent` (id: X) is followed by a user message with `ToolResultContent` (toolUseId: X).

3. **Tool result content mixing not enforced**: Spec states "SamplingMessage with tool result content blocks MUST NOT contain other content types" - add validation for this.

4. **Role-content type mismatch possible**: `ToolUseContent` should only appear in assistant messages, `ToolResultContent` only in user messages. Currently not enforced - consider runtime validation.

5. **CreateMessageResult.role not constrained**: Spec says `role: "assistant"` (literal), but implementation allows any `Role`. Minor, but could be tightened.

6. **Dead code**: `AssistantMessageContent` and `UserMessageContent` enums in `content.rs` are defined but never used. Remove or use them.

The validations seem good to do as followups?

@alexhancock alexhancock merged commit 8bd3fcb into modelcontextprotocol:main Feb 6, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-config Configuration file changes T-core Core library changes T-examples Example code changes T-model Model/data structure changes T-test Testing related changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement SEP-1577: Sampling With Tools

2 participants