Implement SEP-1577: Sampling With Tools (#628)

* feat: implement SEP-1577 sampling with tools support

* feat: add TryFrom<Content> for backward-compatible migration
This commit is contained in:
Dale Seo 2026-02-06 06:51:26 -05:00 • committed by GitHub
commit 8bd3fcb890
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1850 additions and 141 deletions

View file

@ -72,10 +72,7 @@ impl ClientHandler for TestClientHandler {
};
Ok(CreateMessageResult {
message: SamplingMessage {
role: Role::Assistant,
content: Content::text(response.to_string()),
},
message: SamplingMessage::assistant_text(response.to_string()),
model: "test-model".to_string(),
stop_reason: Some(CreateMessageResult::STOP_REASON_END_TURN.to_string()),
})

View file

@ -13,14 +13,8 @@ use tokio_util::sync::CancellationToken;
#[tokio::test]
async fn test_message_roles() {
let messages = vec![
SamplingMessage {
role: Role::User,
content: Content::text("user message"),
},
SamplingMessage {
role: Role::Assistant,
content: Content::text("assistant message"),
},
SamplingMessage::user_text("user message"),
SamplingMessage::assistant_text("assistant message"),
];
// Verify all roles can be serialized/deserialized correctly
@ -50,10 +44,7 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
params: CreateMessageRequestParams {
meta: None,
task: None,
messages: vec![SamplingMessage {
role: Role::User,
content: Content::text("test message"),
}],
messages: vec![SamplingMessage::user_text("test message")],
include_context: Some(ContextInclusion::ThisServer),
model_preferences: None,
system_prompt: None,
@ -61,6 +52,8 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
max_tokens: 100,
stop_sequences: None,
metadata: None,
tools: None,
tool_choice: None,
},
extensions: Default::default(),
});
@ -79,7 +72,15 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
.await?;
if let ClientResult::CreateMessageResult(result) = result {
let text = result.message.content.as_text().unwrap().text.as_str();
let text = result
.message
.content
.first()
.unwrap()
.as_text()
.unwrap()
.text
.as_str();
assert!(
text.contains("test context"),
"Response should include context for ThisServer"
@ -94,10 +95,7 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
params: CreateMessageRequestParams {
meta: None,
task: None,
messages: vec![SamplingMessage {
role: Role::User,
content: Content::text("test message"),
}],
messages: vec![SamplingMessage::user_text("test message")],
include_context: Some(ContextInclusion::AllServers),
model_preferences: None,
system_prompt: None,
@ -105,6 +103,8 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
max_tokens: 100,
stop_sequences: None,
metadata: None,
tools: None,
tool_choice: None,
},
extensions: Default::default(),
});
@ -123,7 +123,15 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
.await?;
if let ClientResult::CreateMessageResult(result) = result {
let text = result.message.content.as_text().unwrap().text.as_str();
let text = result
.message
.content
.first()
.unwrap()
.as_text()
.unwrap()
.text
.as_str();
assert!(
text.contains("test context"),
"Response should include context for AllServers"
@ -138,10 +146,7 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
params: CreateMessageRequestParams {
meta: None,
task: None,
messages: vec![SamplingMessage {
role: Role::User,
content: Content::text("test message"),
}],
messages: vec![SamplingMessage::user_text("test message")],
include_context: Some(ContextInclusion::None),
model_preferences: None,
system_prompt: None,
@ -149,6 +154,8 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
max_tokens: 100,
stop_sequences: None,
metadata: None,
tools: None,
tool_choice: None,
},
extensions: Default::default(),
});
@ -167,7 +174,15 @@ async fn test_context_inclusion_integration() -> anyhow::Result<()> {
.await?;
if let ClientResult::CreateMessageResult(result) = result {
let text = result.message.content.as_text().unwrap().text.as_str();
let text = result
.message
.content
.first()
.unwrap()
.as_text()
.unwrap()
.text
.as_str();
assert!(
!text.contains("test context"),
"Response should not include context for None"
@ -202,10 +217,7 @@ async fn test_context_inclusion_ignored_integration() -> anyhow::Result<()> {
params: CreateMessageRequestParams {
meta: None,
task: None,
messages: vec![SamplingMessage {
role: Role::User,
content: Content::text("test message"),
}],
messages: vec![SamplingMessage::user_text("test message")],
include_context: Some(ContextInclusion::ThisServer),
model_preferences: None,
system_prompt: None,
@ -213,6 +225,8 @@ async fn test_context_inclusion_ignored_integration() -> anyhow::Result<()> {
max_tokens: 100,
stop_sequences: None,
metadata: None,
tools: None,
tool_choice: None,
},
extensions: Default::default(),
});
@ -231,7 +245,15 @@ async fn test_context_inclusion_ignored_integration() -> anyhow::Result<()> {
.await?;
if let ClientResult::CreateMessageResult(result) = result {
let text = result.message.content.as_text().unwrap().text.as_str();
let text = result
.message
.content
.first()
.unwrap()
.as_text()
.unwrap()
.text
.as_str();
assert!(
!text.contains("test context"),
"Context should be ignored when client chooses not to honor requests"
@ -266,14 +288,8 @@ async fn test_message_sequence_integration() -> anyhow::Result<()> {
meta: None,
task: None,
messages: vec![
SamplingMessage {
role: Role::User,
content: Content::text("first message"),
},
SamplingMessage {
role: Role::Assistant,
content: Content::text("second message"),
},
SamplingMessage::user_text("first message"),
SamplingMessage::assistant_text("second message"),
],
include_context: Some(ContextInclusion::ThisServer),
model_preferences: None,
@ -282,6 +298,8 @@ async fn test_message_sequence_integration() -> anyhow::Result<()> {
max_tokens: 100,
stop_sequences: None,
metadata: None,
tools: None,
tool_choice: None,
},
extensions: Default::default(),
});
@ -300,7 +318,15 @@ async fn test_message_sequence_integration() -> anyhow::Result<()> {
.await?;
if let ClientResult::CreateMessageResult(result) = result {
let text = result.message.content.as_text().unwrap().text.as_str();
let text = result
.message
.content
.first()
.unwrap()
.as_text()
.unwrap()
.text
.as_str();
assert!(
text.contains("test context"),
"Response should include context when ThisServer is specified"
@ -339,18 +365,9 @@ async fn test_message_sequence_validation_integration() -> anyhow::Result<()> {
meta: None,
task: None,
messages: vec![
SamplingMessage {
role: Role::User,
content: Content::text("first user message"),
},
SamplingMessage {
role: Role::Assistant,
content: Content::text("first assistant response"),
},
SamplingMessage {
role: Role::User,
content: Content::text("second user message"),
},
SamplingMessage::user_text("first user message"),
SamplingMessage::assistant_text("first assistant response"),
SamplingMessage::user_text("second user message"),
],
include_context: None,
model_preferences: None,
@ -359,6 +376,8 @@ async fn test_message_sequence_validation_integration() -> anyhow::Result<()> {
max_tokens: 100,
stop_sequences: None,
metadata: None,
tools: None,
tool_choice: None,
},
extensions: Default::default(),
});
@ -384,10 +403,7 @@ async fn test_message_sequence_validation_integration() -> anyhow::Result<()> {
params: CreateMessageRequestParams {
meta: None,
task: None,
messages: vec![SamplingMessage {
role: Role::Assistant,
content: Content::text("assistant message"),
}],
messages: vec![SamplingMessage::assistant_text("assistant message")],
include_context: None,
model_preferences: None,
system_prompt: None,
@ -395,6 +411,8 @@ async fn test_message_sequence_validation_integration() -> anyhow::Result<()> {
max_tokens: 100,
stop_sequences: None,
metadata: None,
tools: None,
tool_choice: None,
},
extensions: Default::default(),
});
@ -439,10 +457,7 @@ async fn test_selective_context_handling_integration() -> anyhow::Result<()> {
params: CreateMessageRequestParams {
meta: None,
task: None,
messages: vec![SamplingMessage {
role: Role::User,
content: Content::text("test message"),
}],
messages: vec![SamplingMessage::user_text("test message")],
include_context: Some(ContextInclusion::ThisServer),
model_preferences: None,
system_prompt: None,
@ -450,6 +465,8 @@ async fn test_selective_context_handling_integration() -> anyhow::Result<()> {
max_tokens: 100,
stop_sequences: None,
metadata: None,
tools: None,
tool_choice: None,
},
extensions: Default::default(),
});
@ -468,7 +485,15 @@ async fn test_selective_context_handling_integration() -> anyhow::Result<()> {
.await?;
if let ClientResult::CreateMessageResult(result) = result {
let text = result.message.content.as_text().unwrap().text.as_str();
let text = result
.message
.content
.first()
.unwrap()
.as_text()
.unwrap()
.text
.as_str();
assert!(
text.contains("test context"),
"ThisServer context request should be honored"
@ -481,10 +506,7 @@ async fn test_selective_context_handling_integration() -> anyhow::Result<()> {
params: CreateMessageRequestParams {
meta: None,
task: None,
messages: vec![SamplingMessage {
role: Role::User,
content: Content::text("test message"),
}],
messages: vec![SamplingMessage::user_text("test message")],
include_context: Some(ContextInclusion::AllServers),
model_preferences: None,
system_prompt: None,
@ -492,6 +514,8 @@ async fn test_selective_context_handling_integration() -> anyhow::Result<()> {
max_tokens: 100,
stop_sequences: None,
metadata: None,
tools: None,
tool_choice: None,
},
extensions: Default::default(),
});
@ -510,7 +534,15 @@ async fn test_selective_context_handling_integration() -> anyhow::Result<()> {
.await?;
if let ClientResult::CreateMessageResult(result) = result {
let text = result.message.content.as_text().unwrap().text.as_str();
let text = result
.message
.content
.first()
.unwrap()
.as_text()
.unwrap()
.text
.as_str();
assert!(
!text.contains("test context"),
"AllServers context request should be ignored"
@ -540,10 +572,7 @@ async fn test_context_inclusion() -> anyhow::Result<()> {
params: CreateMessageRequestParams {
meta: None,
task: None,
messages: vec![SamplingMessage {
role: Role::User,
content: Content::text("test"),
}],
messages: vec![SamplingMessage::user_text("test")],
include_context: Some(ContextInclusion::ThisServer),
model_preferences: None,
system_prompt: None,
@ -551,6 +580,8 @@ async fn test_context_inclusion() -> anyhow::Result<()> {
max_tokens: 100,
stop_sequences: None,
metadata: None,
tools: None,
tool_choice: None,
},
extensions: Default::default(),
});
@ -569,7 +600,15 @@ async fn test_context_inclusion() -> anyhow::Result<()> {
.await?;
if let ClientResult::CreateMessageResult(result) = result {
let text = result.message.content.as_text().unwrap().text.as_str();
let text = result
.message
.content
.first()
.unwrap()
.as_text()
.unwrap()
.text
.as_str();
assert!(text.contains("test context"));
}

View file

@ -318,11 +318,15 @@
]
},
"sampling": {
"type": [
"object",
"null"
],
"additionalProperties": true
"description": "Capability for LLM sampling requests (SEP-1577)",
"anyOf": [
{
"$ref": "#/definitions/SamplingCapability"
},
{
"type": "null"
}
]
},
"tasks": {
"anyOf": [
@ -431,14 +435,21 @@
]
},
"CreateMessageResult": {
"description": "The result of a sampling/createMessage request containing the generated response.\n\nThis structure contains the generated message along with metadata about\nhow the generation was performed and why it stopped.",
"description": "Result of sampling/createMessage (SEP-1577).\nThe result of a sampling/createMessage request containing the generated response.\n\nThis structure contains the generated message along with metadata about\nhow the generation was performed and why it stopped.",
"type": "object",
"properties": {
"_meta": {
"type": [
"object",
"null"
],
"additionalProperties": true
},
"content": {
"description": "The actual content of the message (text, image, etc.)",
"description": "The actual content of the message (text, image, audio, tool use, or tool result)",
"allOf": [
{
"$ref": "#/definitions/Annotated"
"$ref": "#/definitions/SamplingContent"
}
]
},
@ -1741,6 +1752,134 @@
"format": "const",
"const": "notifications/roots/list_changed"
},
"SamplingCapability": {
"description": "Sampling capability with optional sub-capabilities (SEP-1577).",
"type": "object",
"properties": {
"context": {
"description": "Support for `includeContext` (soft-deprecated)",
"type": [
"object",
"null"
],
"additionalProperties": true
},
"tools": {
"description": "Support for `tools` and `toolChoice` parameters",
"type": [
"object",
"null"
],
"additionalProperties": true
}
}
},
"SamplingContent": {
"description": "Single or array content wrapper (SEP-1577).",
"anyOf": [
{
"$ref": "#/definitions/SamplingMessageContent"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/SamplingMessageContent"
}
}
]
},
"SamplingMessageContent": {
"description": "Content types for sampling messages (SEP-1577).",
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "text"
}
},
"allOf": [
{
"$ref": "#/definitions/RawTextContent"
}
],
"required": [
"type"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "image"
}
},
"allOf": [
{
"$ref": "#/definitions/RawImageContent"
}
],
"required": [
"type"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "audio"
}
},
"allOf": [
{
"$ref": "#/definitions/RawAudioContent"
}
],
"required": [
"type"
]
},
{
"description": "Assistant only",
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "tool_use"
}
},
"allOf": [
{
"$ref": "#/definitions/ToolUseContent"
}
],
"required": [
"type"
]
},
{
"description": "User only",
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "tool_result"
}
},
"allOf": [
{
"$ref": "#/definitions/ToolResultContent"
}
],
"required": [
"type"
]
}
]
},
"SamplingTaskCapability": {
"type": "object",
"properties": {
@ -1875,6 +2014,81 @@
}
}
},
"ToolResultContent": {
"description": "Tool execution result in user message (SEP-1577).",
"type": "object",
"properties": {
"_meta": {
"description": "Optional metadata",
"type": [
"object",
"null"
],
"additionalProperties": true
},
"content": {
"description": "Content blocks returned by the tool",
"type": "array",
"items": {
"$ref": "#/definitions/Annotated"
}
},
"isError": {
"description": "Whether tool execution failed",
"type": [
"boolean",
"null"
]
},
"structuredContent": {
"description": "Optional structured result",
"type": [
"object",
"null"
],
"additionalProperties": true
},
"toolUseId": {
"description": "ID of the corresponding tool use",
"type": "string"
}
},
"required": [
"toolUseId"
]
},
"ToolUseContent": {
"description": "Tool call request from assistant (SEP-1577).",
"type": "object",
"properties": {
"_meta": {
"description": "Optional metadata (preserved for caching)",
"type": [
"object",
"null"
],
"additionalProperties": true
},
"id": {
"description": "Unique identifier for this tool call",
"type": "string"
},
"input": {
"description": "Input arguments for the tool",
"type": "object",
"additionalProperties": true
},
"name": {
"description": "Name of the tool to call",
"type": "string"
}
},
"required": [
"id",
"name",
"input"
]
},
"ToolsTaskCapability": {
"type": "object",
"properties": {

View file

@ -318,11 +318,15 @@
]
},
"sampling": {
"type": [
"object",
"null"
],
"additionalProperties": true
"description": "Capability for LLM sampling requests (SEP-1577)",
"anyOf": [
{
"$ref": "#/definitions/SamplingCapability"
},
{
"type": "null"
}
]
},
"tasks": {
"anyOf": [
@ -431,14 +435,21 @@
]
},
"CreateMessageResult": {
"description": "The result of a sampling/createMessage request containing the generated response.\n\nThis structure contains the generated message along with metadata about\nhow the generation was performed and why it stopped.",
"description": "Result of sampling/createMessage (SEP-1577).\nThe result of a sampling/createMessage request containing the generated response.\n\nThis structure contains the generated message along with metadata about\nhow the generation was performed and why it stopped.",
"type": "object",
"properties": {
"_meta": {
"type": [
"object",
"null"
],
"additionalProperties": true
},
"content": {
"description": "The actual content of the message (text, image, etc.)",
"description": "The actual content of the message (text, image, audio, tool use, or tool result)",
"allOf": [
{
"$ref": "#/definitions/Annotated"
"$ref": "#/definitions/SamplingContent"
}
]
},
@ -1741,6 +1752,134 @@
"format": "const",
"const": "notifications/roots/list_changed"
},
"SamplingCapability": {
"description": "Sampling capability with optional sub-capabilities (SEP-1577).",
"type": "object",
"properties": {
"context": {
"description": "Support for `includeContext` (soft-deprecated)",
"type": [
"object",
"null"
],
"additionalProperties": true
},
"tools": {
"description": "Support for `tools` and `toolChoice` parameters",
"type": [
"object",
"null"
],
"additionalProperties": true
}
}
},
"SamplingContent": {
"description": "Single or array content wrapper (SEP-1577).",
"anyOf": [
{
"$ref": "#/definitions/SamplingMessageContent"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/SamplingMessageContent"
}
}
]
},
"SamplingMessageContent": {
"description": "Content types for sampling messages (SEP-1577).",
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "text"
}
},
"allOf": [
{
"$ref": "#/definitions/RawTextContent"
}
],
"required": [
"type"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "image"
}
},
"allOf": [
{
"$ref": "#/definitions/RawImageContent"
}
],
"required": [
"type"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "audio"
}
},
"allOf": [
{
"$ref": "#/definitions/RawAudioContent"
}
],
"required": [
"type"
]
},
{
"description": "Assistant only",
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "tool_use"
}
},
"allOf": [
{
"$ref": "#/definitions/ToolUseContent"
}
],
"required": [
"type"
]
},
{
"description": "User only",
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "tool_result"
}
},
"allOf": [
{
"$ref": "#/definitions/ToolResultContent"
}
],
"required": [
"type"
]
}
]
},
"SamplingTaskCapability": {
"type": "object",
"properties": {
@ -1875,6 +2014,81 @@
}
}
},
"ToolResultContent": {
"description": "Tool execution result in user message (SEP-1577).",
"type": "object",
"properties": {
"_meta": {
"description": "Optional metadata",
"type": [
"object",
"null"
],
"additionalProperties": true
},
"content": {
"description": "Content blocks returned by the tool",
"type": "array",
"items": {
"$ref": "#/definitions/Annotated"
}
},
"isError": {
"description": "Whether tool execution failed",
"type": [
"boolean",
"null"
]
},
"structuredContent": {
"description": "Optional structured result",
"type": [
"object",
"null"
],
"additionalProperties": true
},
"toolUseId": {
"description": "ID of the corresponding tool use",
"type": "string"
}
},
"required": [
"toolUseId"
]
},
"ToolUseContent": {
"description": "Tool call request from assistant (SEP-1577).",
"type": "object",
"properties": {
"_meta": {
"description": "Optional metadata (preserved for caching)",
"type": [
"object",
"null"
],
"additionalProperties": true
},
"id": {
"description": "Unique identifier for this tool call",
"type": "string"
},
"input": {
"description": "Input arguments for the tool",
"type": "object",
"additionalProperties": true
},
"name": {
"description": "Name of the tool to call",
"type": "string"
}
},
"required": [
"id",
"name",
"input"
]
},
"ToolsTaskCapability": {
"type": "object",
"properties": {

View file

@ -641,6 +641,27 @@
"null"
],
"format": "float"
},
"toolChoice": {
"description": "Tool selection behavior (SEP-1577)",
"anyOf": [
{
"$ref": "#/definitions/ToolChoice"
},
{
"type": "null"
}
]
},
"tools": {
"description": "Tools available for the model to call (SEP-1577)",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Tool"
}
}
},
"required": [
@ -2309,15 +2330,36 @@
}
]
},
"SamplingContent": {
"description": "Single or array content wrapper (SEP-1577).",
"anyOf": [
{
"$ref": "#/definitions/SamplingMessageContent"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/SamplingMessageContent"
}
}
]
},
"SamplingMessage": {
"description": "A message in a sampling conversation, containing a role and content.\n\nThis represents a single message in a conversation flow, used primarily\nin LLM sampling requests where the conversation history is important\nfor generating appropriate responses.",
"type": "object",
"properties": {
"_meta": {
"type": [
"object",
"null"
],
"additionalProperties": true
},
"content": {
"description": "The actual content of the message (text, image, etc.)",
"description": "The actual content of the message (text, image, audio, tool use, or tool result)",
"allOf": [
{
"$ref": "#/definitions/Annotated"
"$ref": "#/definitions/SamplingContent"
}
]
},
@ -2335,6 +2377,98 @@
"content"
]
},
"SamplingMessageContent": {
"description": "Content types for sampling messages (SEP-1577).",
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "text"
}
},
"allOf": [
{
"$ref": "#/definitions/RawTextContent"
}
],
"required": [
"type"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "image"
}
},
"allOf": [
{
"$ref": "#/definitions/RawImageContent"
}
],
"required": [
"type"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "audio"
}
},
"allOf": [
{
"$ref": "#/definitions/RawAudioContent"
}
],
"required": [
"type"
]
},
{
"description": "Assistant only",
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "tool_use"
}
},
"allOf": [
{
"$ref": "#/definitions/ToolUseContent"
}
],
"required": [
"type"
]
},
{
"description": "User only",
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "tool_result"
}
},
"allOf": [
{
"$ref": "#/definitions/ToolResultContent"
}
],
"required": [
"type"
]
}
]
},
"SamplingTaskCapability": {
"type": "object",
"properties": {
@ -3008,6 +3142,42 @@
}
}
},
"ToolChoice": {
"description": "Tool choice configuration (SEP-1577).",
"type": "object",
"properties": {
"mode": {
"anyOf": [
{
"$ref": "#/definitions/ToolChoiceMode"
},
{
"type": "null"
}
]
}
}
},
"ToolChoiceMode": {
"description": "Tool selection mode (SEP-1577).",
"oneOf": [
{
"description": "Model decides whether to use tools",
"type": "string",
"const": "auto"
},
{
"description": "Model must use at least one tool",
"type": "string",
"const": "required"
},
{
"description": "Model must not use tools",
"type": "string",
"const": "none"
}
]
},
"ToolExecution": {
"description": "Execution-related configuration for a tool.\n\nThis struct contains settings that control how a tool should be executed,\nincluding task support configuration.",
"type": "object",
@ -3030,6 +3200,81 @@
"format": "const",
"const": "notifications/tools/list_changed"
},
"ToolResultContent": {
"description": "Tool execution result in user message (SEP-1577).",
"type": "object",
"properties": {
"_meta": {
"description": "Optional metadata",
"type": [
"object",
"null"
],
"additionalProperties": true
},
"content": {
"description": "Content blocks returned by the tool",
"type": "array",
"items": {
"$ref": "#/definitions/Annotated"
}
},
"isError": {
"description": "Whether tool execution failed",
"type": [
"boolean",
"null"
]
},
"structuredContent": {
"description": "Optional structured result",
"type": [
"object",
"null"
],
"additionalProperties": true
},
"toolUseId": {
"description": "ID of the corresponding tool use",
"type": "string"
}
},
"required": [
"toolUseId"
]
},
"ToolUseContent": {
"description": "Tool call request from assistant (SEP-1577).",
"type": "object",
"properties": {
"_meta": {
"description": "Optional metadata (preserved for caching)",
"type": [
"object",
"null"
],
"additionalProperties": true
},
"id": {
"description": "Unique identifier for this tool call",
"type": "string"
},
"input": {
"description": "Input arguments for the tool",
"type": "object",
"additionalProperties": true
},
"name": {
"description": "Name of the tool to call",
"type": "string"
}
},
"required": [
"id",
"name",
"input"
]
},
"ToolsCapability": {
"type": "object",
"properties": {

View file

@ -641,6 +641,27 @@
"null"
],
"format": "float"
},
"toolChoice": {
"description": "Tool selection behavior (SEP-1577)",
"anyOf": [
{
"$ref": "#/definitions/ToolChoice"
},
{
"type": "null"
}
]
},
"tools": {
"description": "Tools available for the model to call (SEP-1577)",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/Tool"
}
}
},
"required": [
@ -2309,15 +2330,36 @@
}
]
},
"SamplingContent": {
"description": "Single or array content wrapper (SEP-1577).",
"anyOf": [
{
"$ref": "#/definitions/SamplingMessageContent"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/SamplingMessageContent"
}
}
]
},
"SamplingMessage": {
"description": "A message in a sampling conversation, containing a role and content.\n\nThis represents a single message in a conversation flow, used primarily\nin LLM sampling requests where the conversation history is important\nfor generating appropriate responses.",
"type": "object",
"properties": {
"_meta": {
"type": [
"object",
"null"
],
"additionalProperties": true
},
"content": {
"description": "The actual content of the message (text, image, etc.)",
"description": "The actual content of the message (text, image, audio, tool use, or tool result)",
"allOf": [
{
"$ref": "#/definitions/Annotated"
"$ref": "#/definitions/SamplingContent"
}
]
},
@ -2335,6 +2377,98 @@
"content"
]
},
"SamplingMessageContent": {
"description": "Content types for sampling messages (SEP-1577).",
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "text"
}
},
"allOf": [
{
"$ref": "#/definitions/RawTextContent"
}
],
"required": [
"type"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "image"
}
},
"allOf": [
{
"$ref": "#/definitions/RawImageContent"
}
],
"required": [
"type"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "audio"
}
},
"allOf": [
{
"$ref": "#/definitions/RawAudioContent"
}
],
"required": [
"type"
]
},
{
"description": "Assistant only",
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "tool_use"
}
},
"allOf": [
{
"$ref": "#/definitions/ToolUseContent"
}
],
"required": [
"type"
]
},
{
"description": "User only",
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "tool_result"
}
},
"allOf": [
{
"$ref": "#/definitions/ToolResultContent"
}
],
"required": [
"type"
]
}
]
},
"SamplingTaskCapability": {
"type": "object",
"properties": {
@ -3008,6 +3142,42 @@
}
}
},
"ToolChoice": {
"description": "Tool choice configuration (SEP-1577).",
"type": "object",
"properties": {
"mode": {
"anyOf": [
{
"$ref": "#/definitions/ToolChoiceMode"
},
{
"type": "null"
}
]
}
}
},
"ToolChoiceMode": {
"description": "Tool selection mode (SEP-1577).",
"oneOf": [
{
"description": "Model decides whether to use tools",
"type": "string",
"const": "auto"
},
{
"description": "Model must use at least one tool",
"type": "string",
"const": "required"
},
{
"description": "Model must not use tools",
"type": "string",
"const": "none"
}
]
},
"ToolExecution": {
"description": "Execution-related configuration for a tool.\n\nThis struct contains settings that control how a tool should be executed,\nincluding task support configuration.",
"type": "object",
@ -3030,6 +3200,81 @@
"format": "const",
"const": "notifications/tools/list_changed"
},
"ToolResultContent": {
"description": "Tool execution result in user message (SEP-1577).",
"type": "object",
"properties": {
"_meta": {
"description": "Optional metadata",
"type": [
"object",
"null"
],
"additionalProperties": true
},
"content": {
"description": "Content blocks returned by the tool",
"type": "array",
"items": {
"$ref": "#/definitions/Annotated"
}
},
"isError": {
"description": "Whether tool execution failed",
"type": [
"boolean",
"null"
]
},
"structuredContent": {
"description": "Optional structured result",
"type": [
"object",
"null"
],
"additionalProperties": true
},
"toolUseId": {
"description": "ID of the corresponding tool use",
"type": "string"
}
},
"required": [
"toolUseId"
]
},
"ToolUseContent": {
"description": "Tool call request from assistant (SEP-1577).",
"type": "object",
"properties": {
"_meta": {
"description": "Optional metadata (preserved for caching)",
"type": [
"object",
"null"
],
"additionalProperties": true
},
"id": {
"description": "Unique identifier for this tool call",
"type": "string"
},
"input": {
"description": "Input arguments for the tool",
"type": "object",
"additionalProperties": true
},
"name": {
"description": "Name of the tool to call",
"type": "string"
}
},
"required": [
"id",
"name",
"input"
]
},
"ToolsCapability": {
"type": "object",
"properties": {

View file

@ -13,13 +13,8 @@ use tokio_util::sync::CancellationToken;
#[tokio::test]
async fn test_basic_sampling_message_creation() -> Result<()> {
// Test basic sampling message structure
let message = SamplingMessage {
role: Role::User,
content: Content::text("What is the capital of France?"),
};
let message = SamplingMessage::user_text("What is the capital of France?");
// Verify serialization/deserialization
let json = serde_json::to_string(&message)?;
let deserialized: SamplingMessage = serde_json::from_str(&json)?;
assert_eq!(message, deserialized);
@ -30,14 +25,10 @@ async fn test_basic_sampling_message_creation() -> Result<()> {
#[tokio::test]
async fn test_sampling_request_params() -> Result<()> {
// Test sampling request parameters structure
let params = CreateMessageRequestParams {
meta: None,
task: None,
messages: vec![SamplingMessage {
role: Role::User,
content: Content::text("Hello, world!"),
}],
messages: vec![SamplingMessage::user_text("Hello, world!")],
model_preferences: Some(ModelPreferences {
hints: Some(vec![ModelHint {
name: Some("claude".to_string()),
@ -52,14 +43,14 @@ async fn test_sampling_request_params() -> Result<()> {
stop_sequences: Some(vec!["STOP".to_string()]),
include_context: Some(ContextInclusion::None),
metadata: Some(serde_json::json!({"test": "value"})),
tools: None,
tool_choice: None,
};
// Verify serialization/deserialization
let json = serde_json::to_string(&params)?;
let deserialized: CreateMessageRequestParams = serde_json::from_str(&json)?;
assert_eq!(params, deserialized);
// Verify specific fields
assert_eq!(params.messages.len(), 1);
assert_eq!(params.max_tokens, 100);
assert_eq!(params.temperature, Some(0.7));
@ -69,22 +60,16 @@ async fn test_sampling_request_params() -> Result<()> {
#[tokio::test]
async fn test_sampling_result_structure() -> Result<()> {
// Test sampling result structure
let result = CreateMessageResult {
message: SamplingMessage {
role: Role::Assistant,
content: Content::text("The capital of France is Paris."),
},
message: SamplingMessage::assistant_text("The capital of France is Paris."),
model: "test-model".to_string(),
stop_reason: Some(CreateMessageResult::STOP_REASON_END_TURN.to_string()),
};
// Verify serialization/deserialization
let json = serde_json::to_string(&result)?;
let deserialized: CreateMessageResult = serde_json::from_str(&json)?;
assert_eq!(result, deserialized);
// Verify specific fields
assert_eq!(result.message.role, Role::Assistant);
assert_eq!(result.model, "test-model");
assert_eq!(
@ -97,7 +82,6 @@ async fn test_sampling_result_structure() -> Result<()> {
#[tokio::test]
async fn test_sampling_context_inclusion_enum() -> Result<()> {
// Test context inclusion enum values
let test_cases = vec![
(ContextInclusion::None, "none"),
(ContextInclusion::ThisServer, "thisServer"),
@ -139,10 +123,7 @@ async fn test_sampling_integration_with_test_handlers() -> Result<()> {
params: CreateMessageRequestParams {
meta: None,
task: None,
messages: vec![SamplingMessage {
role: Role::User,
content: Content::text("What is the capital of France?"),
}],
messages: vec![SamplingMessage::user_text("What is the capital of France?")],
include_context: Some(ContextInclusion::ThisServer),
model_preferences: Some(ModelPreferences {
hints: Some(vec![ModelHint {
@ -157,6 +138,8 @@ async fn test_sampling_integration_with_test_handlers() -> Result<()> {
max_tokens: 100,
stop_sequences: None,
metadata: None,
tools: None,
tool_choice: None,
},
extensions: Default::default(),
});
@ -183,7 +166,15 @@ async fn test_sampling_integration_with_test_handlers() -> Result<()> {
Some(CreateMessageResult::STOP_REASON_END_TURN.to_string())
);
let response_text = result.message.content.as_text().unwrap().text.as_str();
let response_text = result
.message
.content
.first()
.unwrap()
.as_text()
.unwrap()
.text
.as_str();
assert!(
response_text.contains("test context"),
"Response should include context for ThisServer inclusion"
@ -221,10 +212,7 @@ async fn test_sampling_no_context_inclusion() -> Result<()> {
params: CreateMessageRequestParams {
meta: None,
task: None,
messages: vec![SamplingMessage {
role: Role::User,
content: Content::text("Hello"),
}],
messages: vec![SamplingMessage::user_text("Hello")],
include_context: Some(ContextInclusion::None),
model_preferences: None,
system_prompt: None,
@ -232,6 +220,8 @@ async fn test_sampling_no_context_inclusion() -> Result<()> {
max_tokens: 50,
stop_sequences: None,
metadata: None,
tools: None,
tool_choice: None,
},
extensions: Default::default(),
});
@ -254,7 +244,15 @@ async fn test_sampling_no_context_inclusion() -> Result<()> {
assert_eq!(result.message.role, Role::Assistant);
assert_eq!(result.model, "test-model");
let response_text = result.message.content.as_text().unwrap().text.as_str();
let response_text = result
.message
.content
.first()
.unwrap()
.as_text()
.unwrap()
.text
.as_str();
assert!(
!response_text.contains("test context"),
"Response should not include context for None inclusion"
@ -292,10 +290,9 @@ async fn test_sampling_error_invalid_message_sequence() -> Result<()> {
params: CreateMessageRequestParams {
meta: None,
task: None,
messages: vec![SamplingMessage {
role: Role::Assistant,
content: Content::text("I'm an assistant message without a user message"),
}],
messages: vec![SamplingMessage::assistant_text(
"I'm an assistant message without a user message",
)],
include_context: Some(ContextInclusion::None),
model_preferences: None,
system_prompt: None,
@ -303,6 +300,8 @@ async fn test_sampling_error_invalid_message_sequence() -> Result<()> {
max_tokens: 50,
stop_sequences: None,
metadata: None,
tools: None,
tool_choice: None,
},
extensions: Default::default(),
});
@ -327,3 +326,314 @@ async fn test_sampling_error_invalid_message_sequence() -> Result<()> {
server_handle.await??;
Ok(())
}
#[tokio::test]
async fn test_tool_choice_serialization() -> Result<()> {
let auto = ToolChoice::auto();
let json = serde_json::to_string(&auto)?;
assert!(json.contains("auto"));
let deserialized: ToolChoice = serde_json::from_str(&json)?;
assert_eq!(auto, deserialized);
let required = ToolChoice::required();
let json = serde_json::to_string(&required)?;
assert!(json.contains("required"));
let deserialized: ToolChoice = serde_json::from_str(&json)?;
assert_eq!(required, deserialized);
let none = ToolChoice::none();
let json = serde_json::to_string(&none)?;
assert!(json.contains("none"));
let deserialized: ToolChoice = serde_json::from_str(&json)?;
assert_eq!(none, deserialized);
Ok(())
}
#[tokio::test]
async fn test_sampling_with_tools() -> Result<()> {
use std::sync::Arc;
let tool = Tool::new(
"get_weather",
"Get the current weather for a location",
Arc::new(
serde_json::json!({
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA"
}
},
"required": ["location"]
})
.as_object()
.unwrap()
.clone(),
),
);
let params = CreateMessageRequestParams {
meta: None,
task: None,
messages: vec![SamplingMessage::user_text(
"What's the weather in San Francisco?",
)],
model_preferences: None,
system_prompt: None,
include_context: None,
temperature: None,
max_tokens: 100,
stop_sequences: None,
metadata: None,
tools: Some(vec![tool]),
tool_choice: Some(ToolChoice::auto()),
};
let json = serde_json::to_string(&params)?;
let deserialized: CreateMessageRequestParams = serde_json::from_str(&json)?;
assert!(deserialized.tools.is_some());
assert_eq!(deserialized.tools.as_ref().unwrap().len(), 1);
assert_eq!(deserialized.tools.as_ref().unwrap()[0].name, "get_weather");
assert!(deserialized.tool_choice.is_some());
Ok(())
}
#[tokio::test]
async fn test_tool_use_content_serialization() -> Result<()> {
let tool_use = ToolUseContent::new(
"call_123",
"get_weather",
serde_json::json!({
"location": "San Francisco, CA"
})
.as_object()
.unwrap()
.clone(),
);
let json = serde_json::to_string(&tool_use)?;
let deserialized: ToolUseContent = serde_json::from_str(&json)?;
assert_eq!(tool_use, deserialized);
assert_eq!(deserialized.id, "call_123");
assert_eq!(deserialized.name, "get_weather");
Ok(())
}
#[tokio::test]
async fn test_tool_result_content_serialization() -> Result<()> {
let tool_result = ToolResultContent::new(
"call_123",
vec![Content::text(
"The weather in San Francisco is 72°F and sunny.",
)],
);
let json = serde_json::to_string(&tool_result)?;
let deserialized: ToolResultContent = serde_json::from_str(&json)?;
assert_eq!(tool_result, deserialized);
assert_eq!(deserialized.tool_use_id, "call_123");
assert!(!deserialized.content.is_empty());
Ok(())
}
#[tokio::test]
async fn test_sampling_message_with_tool_use() -> Result<()> {
let message = SamplingMessage::assistant_tool_use(
"call_123",
"get_weather",
serde_json::json!({
"location": "San Francisco, CA"
})
.as_object()
.unwrap()
.clone(),
);
let json = serde_json::to_string(&message)?;
let deserialized: SamplingMessage = serde_json::from_str(&json)?;
assert_eq!(message, deserialized);
assert_eq!(deserialized.role, Role::Assistant);
let tool_use = deserialized.content.first().unwrap().as_tool_use().unwrap();
assert_eq!(tool_use.name, "get_weather");
Ok(())
}
#[tokio::test]
async fn test_sampling_message_with_tool_result() -> Result<()> {
let message =
SamplingMessage::user_tool_result("call_123", vec![Content::text("72°F and sunny")]);
let json = serde_json::to_string(&message)?;
let deserialized: SamplingMessage = serde_json::from_str(&json)?;
assert_eq!(message, deserialized);
assert_eq!(deserialized.role, Role::User);
let tool_result = deserialized
.content
.first()
.unwrap()
.as_tool_result()
.unwrap();
assert_eq!(tool_result.tool_use_id, "call_123");
Ok(())
}
#[tokio::test]
async fn test_create_message_result_tool_use_stop_reason() -> Result<()> {
let result = CreateMessageResult {
message: SamplingMessage::assistant_tool_use(
"call_123",
"get_weather",
serde_json::json!({
"location": "San Francisco"
})
.as_object()
.unwrap()
.clone(),
),
model: "test-model".to_string(),
stop_reason: Some(CreateMessageResult::STOP_REASON_TOOL_USE.to_string()),
};
let json = serde_json::to_string(&result)?;
let deserialized: CreateMessageResult = serde_json::from_str(&json)?;
assert_eq!(result, deserialized);
assert_eq!(deserialized.stop_reason, Some("toolUse".to_string()));
Ok(())
}
#[tokio::test]
async fn test_sampling_capability() -> Result<()> {
let cap = SamplingCapability {
tools: Some(JsonObject::default()),
context: None,
};
let json = serde_json::to_string(&cap)?;
let deserialized: SamplingCapability = serde_json::from_str(&json)?;
assert_eq!(cap, deserialized);
assert!(deserialized.tools.is_some());
assert!(deserialized.context.is_none());
let client_cap = ClientCapabilities::builder()
.enable_sampling()
.enable_sampling_tools()
.build();
assert!(client_cap.sampling.is_some());
assert!(client_cap.sampling.as_ref().unwrap().tools.is_some());
Ok(())
}
#[tokio::test]
async fn test_backward_compat_sampling_message_deserialization() -> Result<()> {
let old_format_json = r#"{
"role": "user",
"content": {
"type": "text",
"text": "Hello, world!"
}
}"#;
let message: SamplingMessage = serde_json::from_str(old_format_json)?;
assert_eq!(message.role, Role::User);
let text = message.content.first().unwrap().as_text().unwrap();
assert_eq!(text.text, "Hello, world!");
Ok(())
}
#[tokio::test]
async fn test_backward_compat_sampling_message_with_image() -> Result<()> {
let old_format_json = r#"{
"role": "user",
"content": {
"type": "image",
"data": "base64data",
"mimeType": "image/png"
}
}"#;
let message: SamplingMessage = serde_json::from_str(old_format_json)?;
assert_eq!(message.role, Role::User);
assert_eq!(message.content.len(), 1);
Ok(())
}
#[tokio::test]
async fn test_backward_compat_sampling_capability_empty_object() -> Result<()> {
let empty_json = "{}";
let cap: SamplingCapability = serde_json::from_str(empty_json)?;
assert!(cap.tools.is_none());
assert!(cap.context.is_none());
let client_cap_json = r#"{"sampling": {}}"#;
let client_cap: ClientCapabilities = serde_json::from_str(client_cap_json)?;
assert!(client_cap.sampling.is_some());
Ok(())
}
#[tokio::test]
async fn test_content_to_sampling_message_content_conversion() -> Result<()> {
use std::convert::TryInto;
let content = Content::text("Hello");
let sampling_content: SamplingMessageContent =
content.try_into().map_err(|e: &str| anyhow::anyhow!(e))?;
assert!(sampling_content.as_text().is_some());
assert_eq!(sampling_content.as_text().unwrap().text, "Hello");
let content = Content::image("base64data", "image/png");
let sampling_content: SamplingMessageContent =
content.try_into().map_err(|e: &str| anyhow::anyhow!(e))?;
assert!(matches!(sampling_content, SamplingMessageContent::Image(_)));
Ok(())
}
#[tokio::test]
async fn test_content_to_sampling_content_conversion() -> Result<()> {
use std::convert::TryInto;
let content = Content::text("Hello");
let sampling_content: SamplingContent<SamplingMessageContent> =
content.try_into().map_err(|e: &str| anyhow::anyhow!(e))?;
assert_eq!(sampling_content.len(), 1);
assert!(sampling_content.first().unwrap().as_text().is_some());
Ok(())
}
#[tokio::test]
async fn test_content_conversion_unsupported_variants() {
use std::convert::TryInto;
use rmcp::model::ResourceContents;
let resource_content = Content::resource(ResourceContents::TextResourceContents {
uri: "file:///test.txt".to_string(),
mime_type: Some("text/plain".to_string()),
text: "test".to_string(),
meta: None,
});
let result: Result<SamplingMessageContent, _> = resource_content.try_into();
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Resource content is not supported in sampling messages"
);
}