mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
fix(MCP): decode resource content (#7155)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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!!!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
_ => {
|
||||
|
||||
Reference in New Issue
Block a user