fix(MCP): decode resource content (#7155)

This commit is contained in:
Alex Hancock
2026-02-12 10:57:03 -05:00
committed by GitHub
parent 738f5f647c
commit 5d939674be
3 changed files with 113 additions and 58 deletions
+4 -49
View File
@@ -1,11 +1,11 @@
use crate::conversation::tool_result_serde;
use crate::mcp_utils::ToolResult;
use crate::mcp_utils::{extract_text_from_resource, ToolResult};
use crate::utils::sanitize_unicode_tags;
use chrono::Utc;
use rmcp::model::{
AnnotateAble, CallToolRequestParams, CallToolResult, Content, ImageContent, JsonObject,
PromptMessage, PromptMessageContent, PromptMessageRole, RawContent, RawImageContent,
RawTextContent, ResourceContents, Role, TextContent,
RawTextContent, Role, TextContent,
};
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::HashSet;
@@ -546,13 +546,7 @@ impl From<Content> for MessageContent {
}
RawContent::ResourceLink(_link) => MessageContent::text("[Resource link]"),
RawContent::Resource(resource) => {
let text = match &resource.resource {
ResourceContents::TextResourceContents { text, .. } => text.clone(),
ResourceContents::BlobResourceContents { blob, .. } => {
format!("[Binary content: {}]", blob.clone())
}
};
MessageContent::text(text)
MessageContent::text(extract_text_from_resource(&resource.resource))
}
RawContent::Audio(_) => {
MessageContent::text("[Audio content: not supported]".to_string())
@@ -577,15 +571,7 @@ impl From<PromptMessage> for Message {
}
PromptMessageContent::ResourceLink { .. } => MessageContent::text("[Resource link]"),
PromptMessageContent::Resource { resource } => {
// For resources, convert to text content with the resource text
match &resource.resource {
ResourceContents::TextResourceContents { text, .. } => {
MessageContent::text(text.clone())
}
ResourceContents::BlobResourceContents { blob, .. } => {
MessageContent::text(format!("[Binary content: {}]", blob.clone()))
}
}
MessageContent::text(extract_text_from_resource(&resource.resource))
}
};
@@ -1204,37 +1190,6 @@ mod tests {
}
}
#[test]
fn test_from_prompt_message_blob_resource() {
let resource = ResourceContents::BlobResourceContents {
uri: "file:///test.bin".to_string(),
mime_type: Some("application/octet-stream".to_string()),
blob: "binary_data".to_string(),
meta: None,
};
let prompt_content = PromptMessageContent::Resource {
resource: RawEmbeddedResource {
resource,
meta: None,
}
.no_annotation(),
};
let prompt_message = PromptMessage {
role: PromptMessageRole::User,
content: prompt_content,
};
let message = Message::from(prompt_message);
if let MessageContent::Text(text_content) = &message.content[0] {
assert_eq!(text_content.text, "[Binary content: binary_data]");
} else {
panic!("Expected MessageContent::Text");
}
}
#[test]
fn test_from_prompt_message() {
// Test user message conversion
+105 -1
View File
@@ -1,4 +1,108 @@
use base64::Engine;
pub use rmcp::model::ErrorData;
use rmcp::model::ResourceContents;
/// Type alias for tool results
pub type ToolResult<T> = Result<T, ErrorData>;
pub fn extract_text_from_resource(resource: &ResourceContents) -> String {
match resource {
ResourceContents::TextResourceContents { text, .. } => text.clone(),
ResourceContents::BlobResourceContents {
blob, mime_type, ..
} => match base64::engine::general_purpose::STANDARD.decode(blob) {
Ok(bytes) => {
let byte_len = bytes.len();
match String::from_utf8(bytes) {
Ok(text) => text,
Err(_) => {
let mime = mime_type
.as_ref()
.map(|m| m.as_str())
.unwrap_or("application/octet-stream");
format!("[Binary content ({}) - {} bytes]", mime, byte_len)
}
}
}
Err(_) => blob.clone(),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_case::test_case;
#[test_case("Hello, World!", "Hello, World!" ; "simple text")]
#[test_case("Hello from GitHub!", "Hello from GitHub!" ; "github content")]
#[test_case("", "" ; "empty text")]
fn test_extract_text_from_text_resource(input: &str, expected: &str) {
let resource = ResourceContents::TextResourceContents {
uri: "file:///test.txt".to_string(),
mime_type: Some("text/plain".to_string()),
text: input.to_string(),
meta: None,
};
assert_eq!(extract_text_from_resource(&resource), expected);
}
#[test_case("Hello from GitHub!", "Hello from GitHub!" ; "utf8 markdown")]
#[test_case("Simple text", "Simple text" ; "utf8 plain")]
fn test_extract_text_from_blob_utf8(input: &str, expected: &str) {
let blob = base64::engine::general_purpose::STANDARD.encode(input.as_bytes());
let resource = ResourceContents::BlobResourceContents {
uri: "github://repo/file.md".to_string(),
mime_type: Some("text/markdown".to_string()),
blob,
meta: None,
};
assert_eq!(extract_text_from_resource(&resource), expected);
}
#[test]
fn test_extract_text_from_blob_binary() {
let binary_data: Vec<u8> = vec![0xFF, 0xFE, 0x00, 0x01, 0x89, 0x50, 0x4E, 0x47];
let blob = base64::engine::general_purpose::STANDARD.encode(&binary_data);
let resource = ResourceContents::BlobResourceContents {
uri: "file:///image.png".to_string(),
mime_type: Some("image/png".to_string()),
blob,
meta: None,
};
assert_eq!(
extract_text_from_resource(&resource),
"[Binary content (image/png) - 8 bytes]"
);
}
#[test]
fn test_extract_text_from_blob_binary_no_mime_type() {
let binary_data: Vec<u8> = vec![0xFF, 0xFE];
let blob = base64::engine::general_purpose::STANDARD.encode(&binary_data);
let resource = ResourceContents::BlobResourceContents {
uri: "file:///unknown".to_string(),
mime_type: None,
blob,
meta: None,
};
assert_eq!(
extract_text_from_resource(&resource),
"[Binary content (application/octet-stream) - 2 bytes]"
);
}
#[test]
fn test_extract_text_from_blob_invalid_base64() {
let resource = ResourceContents::BlobResourceContents {
uri: "file:///test.txt".to_string(),
mime_type: Some("text/plain".to_string()),
blob: "not valid base64!!!".to_string(),
meta: None,
};
assert_eq!(extract_text_from_resource(&resource), "not valid base64!!!");
}
}
+4 -8
View File
@@ -1,4 +1,5 @@
use crate::conversation::message::{Message, MessageContent, ProviderMetadata};
use crate::mcp_utils::extract_text_from_resource;
use crate::model::ModelConfig;
use crate::providers::base::{ProviderUsage, Usage};
use crate::providers::utils::{
@@ -10,8 +11,8 @@ use async_stream::try_stream;
use chrono;
use futures::Stream;
use rmcp::model::{
object, AnnotateAble, CallToolRequestParams, Content, ErrorCode, ErrorData, RawContent,
ResourceContents, Role, Tool,
object, AnnotateAble, CallToolRequestParams, Content, ErrorCode, ErrorData, RawContent, Role,
Tool,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
@@ -178,12 +179,7 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
}));
}
RawContent::Resource(resource) => {
let text = match &resource.resource {
ResourceContents::TextResourceContents {
text, ..
} => text.clone(),
_ => String::new(),
};
let text = extract_text_from_resource(&resource.resource);
tool_content.push(Content::text(text));
}
_ => {