Skip to content

Commit 5d93967

Browse files
authored
fix(MCP): decode resource content (#7155)
1 parent 738f5f6 commit 5d93967

3 files changed

Lines changed: 113 additions & 58 deletions

File tree

crates/goose/src/conversation/message.rs

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use crate::conversation::tool_result_serde;
2-
use crate::mcp_utils::ToolResult;
2+
use crate::mcp_utils::{extract_text_from_resource, ToolResult};
33
use crate::utils::sanitize_unicode_tags;
44
use chrono::Utc;
55
use rmcp::model::{
66
AnnotateAble, CallToolRequestParams, CallToolResult, Content, ImageContent, JsonObject,
77
PromptMessage, PromptMessageContent, PromptMessageRole, RawContent, RawImageContent,
8-
RawTextContent, ResourceContents, Role, TextContent,
8+
RawTextContent, Role, TextContent,
99
};
1010
use serde::{Deserialize, Deserializer, Serialize};
1111
use std::collections::HashSet;
@@ -546,13 +546,7 @@ impl From<Content> for MessageContent {
546546
}
547547
RawContent::ResourceLink(_link) => MessageContent::text("[Resource link]"),
548548
RawContent::Resource(resource) => {
549-
let text = match &resource.resource {
550-
ResourceContents::TextResourceContents { text, .. } => text.clone(),
551-
ResourceContents::BlobResourceContents { blob, .. } => {
552-
format!("[Binary content: {}]", blob.clone())
553-
}
554-
};
555-
MessageContent::text(text)
549+
MessageContent::text(extract_text_from_resource(&resource.resource))
556550
}
557551
RawContent::Audio(_) => {
558552
MessageContent::text("[Audio content: not supported]".to_string())
@@ -577,15 +571,7 @@ impl From<PromptMessage> for Message {
577571
}
578572
PromptMessageContent::ResourceLink { .. } => MessageContent::text("[Resource link]"),
579573
PromptMessageContent::Resource { resource } => {
580-
// For resources, convert to text content with the resource text
581-
match &resource.resource {
582-
ResourceContents::TextResourceContents { text, .. } => {
583-
MessageContent::text(text.clone())
584-
}
585-
ResourceContents::BlobResourceContents { blob, .. } => {
586-
MessageContent::text(format!("[Binary content: {}]", blob.clone()))
587-
}
588-
}
574+
MessageContent::text(extract_text_from_resource(&resource.resource))
589575
}
590576
};
591577

@@ -1204,37 +1190,6 @@ mod tests {
12041190
}
12051191
}
12061192

1207-
#[test]
1208-
fn test_from_prompt_message_blob_resource() {
1209-
let resource = ResourceContents::BlobResourceContents {
1210-
uri: "file:///test.bin".to_string(),
1211-
mime_type: Some("application/octet-stream".to_string()),
1212-
blob: "binary_data".to_string(),
1213-
meta: None,
1214-
};
1215-
1216-
let prompt_content = PromptMessageContent::Resource {
1217-
resource: RawEmbeddedResource {
1218-
resource,
1219-
meta: None,
1220-
}
1221-
.no_annotation(),
1222-
};
1223-
1224-
let prompt_message = PromptMessage {
1225-
role: PromptMessageRole::User,
1226-
content: prompt_content,
1227-
};
1228-
1229-
let message = Message::from(prompt_message);
1230-
1231-
if let MessageContent::Text(text_content) = &message.content[0] {
1232-
assert_eq!(text_content.text, "[Binary content: binary_data]");
1233-
} else {
1234-
panic!("Expected MessageContent::Text");
1235-
}
1236-
}
1237-
12381193
#[test]
12391194
fn test_from_prompt_message() {
12401195
// Test user message conversion

crates/goose/src/mcp_utils.rs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,108 @@
1+
use base64::Engine;
12
pub use rmcp::model::ErrorData;
3+
use rmcp::model::ResourceContents;
24

3-
/// Type alias for tool results
45
pub type ToolResult<T> = Result<T, ErrorData>;
6+
7+
pub fn extract_text_from_resource(resource: &ResourceContents) -> String {
8+
match resource {
9+
ResourceContents::TextResourceContents { text, .. } => text.clone(),
10+
ResourceContents::BlobResourceContents {
11+
blob, mime_type, ..
12+
} => match base64::engine::general_purpose::STANDARD.decode(blob) {
13+
Ok(bytes) => {
14+
let byte_len = bytes.len();
15+
match String::from_utf8(bytes) {
16+
Ok(text) => text,
17+
Err(_) => {
18+
let mime = mime_type
19+
.as_ref()
20+
.map(|m| m.as_str())
21+
.unwrap_or("application/octet-stream");
22+
format!("[Binary content ({}) - {} bytes]", mime, byte_len)
23+
}
24+
}
25+
}
26+
Err(_) => blob.clone(),
27+
},
28+
}
29+
}
30+
31+
#[cfg(test)]
32+
mod tests {
33+
use super::*;
34+
use test_case::test_case;
35+
36+
#[test_case("Hello, World!", "Hello, World!" ; "simple text")]
37+
#[test_case("Hello from GitHub!", "Hello from GitHub!" ; "github content")]
38+
#[test_case("", "" ; "empty text")]
39+
fn test_extract_text_from_text_resource(input: &str, expected: &str) {
40+
let resource = ResourceContents::TextResourceContents {
41+
uri: "file:///test.txt".to_string(),
42+
mime_type: Some("text/plain".to_string()),
43+
text: input.to_string(),
44+
meta: None,
45+
};
46+
assert_eq!(extract_text_from_resource(&resource), expected);
47+
}
48+
49+
#[test_case("Hello from GitHub!", "Hello from GitHub!" ; "utf8 markdown")]
50+
#[test_case("Simple text", "Simple text" ; "utf8 plain")]
51+
fn test_extract_text_from_blob_utf8(input: &str, expected: &str) {
52+
let blob = base64::engine::general_purpose::STANDARD.encode(input.as_bytes());
53+
let resource = ResourceContents::BlobResourceContents {
54+
uri: "github://repo/file.md".to_string(),
55+
mime_type: Some("text/markdown".to_string()),
56+
blob,
57+
meta: None,
58+
};
59+
assert_eq!(extract_text_from_resource(&resource), expected);
60+
}
61+
62+
#[test]
63+
fn test_extract_text_from_blob_binary() {
64+
let binary_data: Vec<u8> = vec![0xFF, 0xFE, 0x00, 0x01, 0x89, 0x50, 0x4E, 0x47];
65+
let blob = base64::engine::general_purpose::STANDARD.encode(&binary_data);
66+
67+
let resource = ResourceContents::BlobResourceContents {
68+
uri: "file:///image.png".to_string(),
69+
mime_type: Some("image/png".to_string()),
70+
blob,
71+
meta: None,
72+
};
73+
74+
assert_eq!(
75+
extract_text_from_resource(&resource),
76+
"[Binary content (image/png) - 8 bytes]"
77+
);
78+
}
79+
80+
#[test]
81+
fn test_extract_text_from_blob_binary_no_mime_type() {
82+
let binary_data: Vec<u8> = vec![0xFF, 0xFE];
83+
let blob = base64::engine::general_purpose::STANDARD.encode(&binary_data);
84+
85+
let resource = ResourceContents::BlobResourceContents {
86+
uri: "file:///unknown".to_string(),
87+
mime_type: None,
88+
blob,
89+
meta: None,
90+
};
91+
92+
assert_eq!(
93+
extract_text_from_resource(&resource),
94+
"[Binary content (application/octet-stream) - 2 bytes]"
95+
);
96+
}
97+
98+
#[test]
99+
fn test_extract_text_from_blob_invalid_base64() {
100+
let resource = ResourceContents::BlobResourceContents {
101+
uri: "file:///test.txt".to_string(),
102+
mime_type: Some("text/plain".to_string()),
103+
blob: "not valid base64!!!".to_string(),
104+
meta: None,
105+
};
106+
assert_eq!(extract_text_from_resource(&resource), "not valid base64!!!");
107+
}
108+
}

crates/goose/src/providers/formats/openai.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::conversation::message::{Message, MessageContent, ProviderMetadata};
2+
use crate::mcp_utils::extract_text_from_resource;
23
use crate::model::ModelConfig;
34
use crate::providers::base::{ProviderUsage, Usage};
45
use crate::providers::utils::{
@@ -10,8 +11,8 @@ use async_stream::try_stream;
1011
use chrono;
1112
use futures::Stream;
1213
use rmcp::model::{
13-
object, AnnotateAble, CallToolRequestParams, Content, ErrorCode, ErrorData, RawContent,
14-
ResourceContents, Role, Tool,
14+
object, AnnotateAble, CallToolRequestParams, Content, ErrorCode, ErrorData, RawContent, Role,
15+
Tool,
1516
};
1617
use serde::{Deserialize, Serialize};
1718
use serde_json::{json, Value};
@@ -178,12 +179,7 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
178179
}));
179180
}
180181
RawContent::Resource(resource) => {
181-
let text = match &resource.resource {
182-
ResourceContents::TextResourceContents {
183-
text, ..
184-
} => text.clone(),
185-
_ => String::new(),
186-
};
182+
let text = extract_text_from_resource(&resource.resource);
187183
tool_content.push(Content::text(text));
188184
}
189185
_ => {

0 commit comments

Comments
 (0)