Update to rmcp 1.1.0 (#7619)

Co-authored-by: Alex Hancock <alexhancock@block.xyz>
This commit is contained in:
Jack Amadeo
2026-03-05 21:40:52 -05:00
committed by GitHub
parent abadb87892
commit 325bf396af
61 changed files with 729 additions and 1757 deletions
+17 -36
View File
@@ -295,12 +295,11 @@ impl MessageContent {
// Preserve ToolResponse even when content is empty - some providers
// (like Google) need to handle empty tool responses specially
let mut tool_result = result.clone();
tool_result.content = filtered_content;
Some(MessageContent::ToolResponse(ToolResponse {
id: res.id.clone(),
tool_result: Ok(CallToolResult {
content: filtered_content,
..result.clone()
}),
tool_result: Ok(tool_result),
metadata: res.metadata.clone(),
}))
}
@@ -999,12 +998,8 @@ mod tests {
.with_text("Hello, I'll help you with that.")
.with_tool_request(
"tool123",
Ok(CallToolRequestParams {
meta: None,
task: None,
name: "test_tool".into(),
arguments: Some(object!({"param": "value"})),
}),
Ok(CallToolRequestParams::new("test_tool")
.with_arguments(object!({"param": "value"}))),
);
let json_str = serde_json::to_string_pretty(&message).unwrap();
@@ -1120,10 +1115,7 @@ mod tests {
text: "Hello, world!".to_string(),
};
let prompt_message = PromptMessage {
role: PromptMessageRole::User,
content: prompt_content,
};
let prompt_message = PromptMessage::new(PromptMessageRole::User, prompt_content);
let message = Message::from(prompt_message);
@@ -1145,10 +1137,7 @@ mod tests {
.no_annotation(),
};
let prompt_message = PromptMessage {
role: PromptMessageRole::User,
content: prompt_content,
};
let prompt_message = PromptMessage::new(PromptMessageRole::User, prompt_content);
let message = Message::from(prompt_message);
@@ -1177,10 +1166,7 @@ mod tests {
.no_annotation(),
};
let prompt_message = PromptMessage {
role: PromptMessageRole::User,
content: prompt_content,
};
let prompt_message = PromptMessage::new(PromptMessageRole::User, prompt_content);
let message = Message::from(prompt_message);
@@ -1194,12 +1180,12 @@ mod tests {
#[test]
fn test_from_prompt_message() {
// Test user message conversion
let prompt_message = PromptMessage {
role: PromptMessageRole::User,
content: PromptMessageContent::Text {
let prompt_message = PromptMessage::new(
PromptMessageRole::User,
PromptMessageContent::Text {
text: "Hello, world!".to_string(),
},
};
);
let message = Message::from(prompt_message);
assert_eq!(message.role, Role::User);
@@ -1207,12 +1193,12 @@ mod tests {
assert_eq!(message.as_concat_text(), "Hello, world!");
// Test assistant message conversion
let prompt_message = PromptMessage {
role: PromptMessageRole::Assistant,
content: PromptMessageContent::Text {
let prompt_message = PromptMessage::new(
PromptMessageRole::Assistant,
PromptMessageContent::Text {
text: "I can help with that.".to_string(),
},
};
);
let message = Message::from(prompt_message);
assert_eq!(message.role, Role::Assistant);
@@ -1228,12 +1214,7 @@ mod tests {
#[test]
fn test_message_with_tool_request() {
let tool_call = Ok(CallToolRequestParams {
meta: None,
task: None,
name: "test_tool".into(),
arguments: Some(object!({})),
});
let tool_call = Ok(CallToolRequestParams::new("test_tool").with_arguments(object!({})));
let message = Message::assistant().with_tool_request("req1", tool_call);
assert!(message.is_tool_call());
+17 -75
View File
@@ -6,7 +6,7 @@ use thiserror::Error;
use utoipa::ToSchema;
pub mod message;
pub mod tool_result_serde;
mod tool_result_serde;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq)]
pub struct Conversation(Vec<Message>);
@@ -550,22 +550,11 @@ mod tests {
.with_text("I'll help you search.")
.with_tool_request(
"search_1",
Ok(CallToolRequestParams {
meta: None,
task: None,
name: "web_search".into(),
arguments: Some(object!({"query": "rust programming"})),
}),
Ok(CallToolRequestParams::new("web_search")
.with_arguments(object!({"query": "rust programming"}))),
),
Message::user().with_tool_response(
"search_1",
Ok(rmcp::model::CallToolResult {
content: vec![],
structured_content: None,
is_error: Some(false),
meta: None,
}),
),
Message::user()
.with_tool_response("search_1", Ok(rmcp::model::CallToolResult::success(vec![]))),
Message::assistant().with_text("Based on the search results, here's what I found..."),
];
@@ -602,25 +591,12 @@ mod tests {
Message::user().with_text("Another user message"),
Message::assistant()
.with_text("Response")
.with_tool_response(
"orphan_1",
Ok(rmcp::model::CallToolResult {
content: vec![],
structured_content: None,
is_error: Some(false),
meta: None,
}),
), // Wrong role
.with_tool_response("orphan_1", Ok(rmcp::model::CallToolResult::success(vec![]))), // Wrong role
Message::assistant().with_thinking("Let me think", "sig"),
Message::user()
.with_tool_request(
"bad_req",
Ok(CallToolRequestParams {
meta: None,
task: None,
name: "search".into(),
arguments: Some(object!({})),
}),
Ok(CallToolRequestParams::new("search").with_arguments(object!({}))),
)
.with_text("User with bad tool request"),
];
@@ -656,31 +632,14 @@ mod tests {
.with_text("I'll search for you")
.with_tool_request(
"search_1",
Ok(CallToolRequestParams {
meta: None,
task: None,
name: "search".into(),
arguments: Some(object!({})),
}),
Ok(CallToolRequestParams::new("search").with_arguments(object!({}))),
),
Message::user(),
Message::user().with_tool_response(
"wrong_id",
Ok(rmcp::model::CallToolResult {
content: vec![],
structured_content: None,
is_error: Some(false),
meta: None,
}),
),
Message::user()
.with_tool_response("wrong_id", Ok(rmcp::model::CallToolResult::success(vec![]))),
Message::assistant().with_tool_request(
"search_2",
Ok(CallToolRequestParams {
meta: None,
task: None,
name: "search".into(),
arguments: Some(object!({})),
}),
Ok(CallToolRequestParams::new("search").with_arguments(object!({}))),
),
];
@@ -712,19 +671,14 @@ mod tests {
Message::assistant()
.with_text("I'll help you run `ls` in the current directory and then perform a word count on the smallest file. Let me start by listing the directory contents.")
.with_tool_request("toolu_bdrk_018adWbP4X26CfoJU5hkhu3i", Ok(CallToolRequestParams { meta: None, task: None, name: "developer__shell".into(), arguments: Some(object!({"command": "ls -la"})) })),
.with_tool_request("toolu_bdrk_018adWbP4X26CfoJU5hkhu3i", Ok(CallToolRequestParams::new("developer__shell").with_arguments(object!({"command": "ls -la"})))),
Message::assistant()
.with_text("Now I'll identify the smallest file by size. Looking at the output, I can see that both `slack.yaml` and `subrecipes.yaml` have a size of 0 bytes, making them the smallest files. I'll run a word count on one of them:")
.with_tool_request("toolu_bdrk_01KgDYHs4fAodi22NqxRzmwx", Ok(CallToolRequestParams { meta: None, task: None, name: "developer__shell".into(), arguments: Some(object!({"command": "wc slack.yaml"})) })),
.with_tool_request("toolu_bdrk_01KgDYHs4fAodi22NqxRzmwx", Ok(CallToolRequestParams::new("developer__shell").with_arguments(object!({"command": "wc slack.yaml"})))),
Message::user()
.with_tool_response("toolu_bdrk_01KgDYHs4fAodi22NqxRzmwx", Ok(rmcp::model::CallToolResult {
content: vec![],
structured_content: None,
is_error: Some(false),
meta: None,
})),
.with_tool_response("toolu_bdrk_01KgDYHs4fAodi22NqxRzmwx", Ok(rmcp::model::CallToolResult::success(vec![]))),
Message::assistant()
.with_text("I ran `ls -la` in the current directory and found several files. Looking at the file sizes, I can see that both `slack.yaml` and `subrecipes.yaml` are 0 bytes (the smallest files). I ran a word count on `slack.yaml` which shows: **0 lines**, **0 words**, **0 characters**"),
@@ -750,22 +704,10 @@ mod tests {
.with_text("I'll search for you")
.with_tool_request(
"search_1",
Ok(CallToolRequestParams {
meta: None,
task: None,
name: "search".into(),
arguments: Some(object!({})),
}),
Ok(CallToolRequestParams::new("search").with_arguments(object!({}))),
),
Message::user().with_tool_response(
"search_1",
Ok(rmcp::model::CallToolResult {
content: vec![],
structured_content: None,
is_error: Some(false),
meta: None,
}),
),
Message::user()
.with_tool_response("search_1", Ok(rmcp::model::CallToolResult::success(vec![]))),
Message::user().with_text("Thanks!"),
];
@@ -42,11 +42,12 @@ impl ToolCallWithValueArguments {
Some(map)
}
};
CallToolRequestParams {
meta: None,
task: None,
name: Cow::Owned(self.name),
arguments,
{
let mut params = CallToolRequestParams::new(self.name);
if let Some(args) = arguments {
params = params.with_arguments(args);
}
params
}
}
}
@@ -147,17 +148,7 @@ pub mod call_tool_result {
},
}
let original_value = serde_json::Value::deserialize(deserializer)?;
let format = ResultFormat::deserialize(&original_value).map_err(|e| {
tracing::debug!(
"Failed to deserialize call_tool_result: {}. Original data: {}",
e,
serde_json::to_string(&original_value)
.unwrap_or_else(|_| "<invalid json>".to_string())
);
serde::de::Error::custom(e)
})?;
let format = ResultFormat::deserialize(deserializer)?;
match format {
ResultFormat::SuccessWithCallToolResult { status, value } => {
@@ -196,87 +187,4 @@ pub mod call_tool_result {
}
}
}
pub fn validate(result: ToolResult<CallToolResult>) -> ToolResult<CallToolResult> {
match &result {
Ok(call_tool_result) => match serde_json::to_string(call_tool_result) {
Ok(json_str) => match serde_json::from_str::<CallToolResult>(&json_str) {
Ok(_) => result,
Err(e) => {
tracing::error!("CallToolResult failed validation by deserialization: {}. Original data: {}", e, json_str);
Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Tool result validation failed: {}", e)),
data: None,
})
}
},
Err(e) => {
tracing::error!("CallToolResult failed serialization: {}", e);
Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Tool result serialization failed: {}", e)),
data: None,
})
}
},
Err(_) => result,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rmcp::model::{CallToolResult, Content, ErrorCode, ErrorData};
use std::borrow::Cow;
#[test]
fn test_validate_accepts_valid_call_tool_result() {
let valid_result = CallToolResult {
content: vec![Content::text("test")],
is_error: Some(false),
structured_content: None,
meta: None,
};
let tool_result: ToolResult<CallToolResult> = Ok(valid_result);
let validated = call_tool_result::validate(tool_result);
assert!(
validated.is_ok(),
"Expected validation to pass for valid CallToolResult"
);
}
#[test]
fn test_validate_returns_error_for_invalid_calltoolresult() {
let valid_result = CallToolResult {
content: vec![],
is_error: Some(false),
structured_content: None,
meta: None,
};
let tool_result: ToolResult<CallToolResult> = Ok(valid_result);
let validated = call_tool_result::validate(tool_result);
assert!(validated.is_err());
assert!(validated
.unwrap_err()
.message
.contains("Tool result validation failed"))
}
#[test]
fn test_validate_passes_through_errors() {
let error_result: ToolResult<CallToolResult> = Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from("test error"),
data: None,
});
let validated = call_tool_result::validate(error_result.clone());
assert!(validated.is_err());
assert_eq!(validated.unwrap_err().message, "test error");
}
}