diff --git a/crates/goose-mcp/src/computercontroller/mod.rs b/crates/goose-mcp/src/computercontroller/mod.rs index c87c5f19..c54ee567 100644 --- a/crates/goose-mcp/src/computercontroller/mod.rs +++ b/crates/goose-mcp/src/computercontroller/mod.rs @@ -14,12 +14,11 @@ use std::os::unix::fs::PermissionsExt; use mcp_core::{ handler::{PromptError, ResourceError, ToolError}, protocol::{JsonRpcMessage, ServerCapabilities}, - resource::Resource, tool::{Tool, ToolAnnotations}, }; use mcp_server::router::CapabilitiesBuilder; use mcp_server::Router; -use rmcp::model::{Content, Prompt}; +use rmcp::model::{AnnotateAble, Content, Prompt, RawResource, Resource}; mod docx_tool; mod pdf_tool; @@ -585,14 +584,16 @@ impl ComputerControllerRouter { .map_err(|_| ToolError::ExecutionError("Invalid cache path".into()))? .to_string(); - let resource = Resource::new( - uri.clone(), - Some(mime_type.to_string()), - Some(cache_path.to_string_lossy().into_owned()), - ) - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - self.active_resources.lock().unwrap().insert(uri, resource); + let mut resource = RawResource::new(uri.clone(), cache_path.to_string_lossy().into_owned()); + resource.mime_type = Some(if mime_type == "blob" { + "blob".to_string() + } else { + "text".to_string() + }); + self.active_resources + .lock() + .unwrap() + .insert(uri, resource.no_annotation()); Ok(()) } @@ -1175,17 +1176,17 @@ impl Router for ComputerControllerRouter { .to_file_path() .map_err(|_| ResourceError::NotFound("Invalid file path in URI".into()))?; - match resource.mime_type.as_str() { - "text" | "json" => fs::read_to_string(&path).map_err(|e| { + match resource.raw.mime_type.as_deref() { + Some("text") | Some("json") | None => fs::read_to_string(&path).map_err(|e| { ResourceError::ExecutionError(format!("Failed to read file: {}", e)) }), - "binary" => { + Some("binary") => { let bytes = fs::read(&path).map_err(|e| { ResourceError::ExecutionError(format!("Failed to read file: {}", e)) })?; Ok(base64::prelude::BASE64_STANDARD.encode(bytes)) } - mime_type => Err(ResourceError::NotFound(format!( + Some(mime_type) => Err(ResourceError::NotFound(format!( "Unsupported mime type: {}", mime_type ))), diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index d47bc2c2..307a4560 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -26,12 +26,11 @@ use mcp_core::tool::ToolAnnotations; use mcp_core::{ handler::{PromptError, ResourceError, ToolError}, protocol::{JsonRpcMessage, JsonRpcNotification, ServerCapabilities}, - resource::Resource, tool::Tool, }; use mcp_server::router::CapabilitiesBuilder; use mcp_server::Router; -use rmcp::model::{Content, Prompt, PromptArgument, PromptTemplate}; +use rmcp::model::{Content, Prompt, PromptArgument, PromptTemplate, Resource}; use rmcp::model::Role; diff --git a/crates/goose-mcp/src/google_drive/mod.rs b/crates/goose-mcp/src/google_drive/mod.rs index e7f275f7..e3627342 100644 --- a/crates/goose-mcp/src/google_drive/mod.rs +++ b/crates/goose-mcp/src/google_drive/mod.rs @@ -11,7 +11,7 @@ use mcp_core::protocol::JsonRpcMessage; use mcp_core::tool::ToolAnnotations; use oauth_pkce::PkceOAuth2Client; use regex::Regex; -use rmcp::model::{Content, Prompt}; +use rmcp::model::{AnnotateAble, Content, Prompt, RawResource, Resource}; use serde_json::{json, Value}; use std::io::Cursor; use std::{env, fs, future::Future, path::Path, pin::Pin, sync::Arc}; @@ -21,7 +21,6 @@ use tokio::sync::mpsc; use mcp_core::{ handler::{PromptError, ResourceError, ToolError}, protocol::ServerCapabilities, - resource::Resource, tool::Tool, }; use mcp_server::router::CapabilitiesBuilder; @@ -1888,12 +1887,15 @@ impl GoogleDriveRouter { Ok(r) => { r.1.files .map(|files| { - files.into_iter().map(|f| Resource { - uri: f.id.unwrap_or_default(), - mime_type: f.mime_type.unwrap_or_default(), - name: f.name.unwrap_or_default(), - description: None, - annotations: None, + files.into_iter().map(|f| { + RawResource { + uri: f.id.unwrap_or_default(), + mime_type: f.mime_type, + name: f.name.unwrap_or_default(), + description: None, + size: None, + } + .no_annotation() }) }) .into_iter() diff --git a/crates/goose-mcp/src/memory/mod.rs b/crates/goose-mcp/src/memory/mod.rs index e5a2be88..16b3122d 100644 --- a/crates/goose-mcp/src/memory/mod.rs +++ b/crates/goose-mcp/src/memory/mod.rs @@ -15,12 +15,11 @@ use tokio::sync::mpsc; use mcp_core::{ handler::{PromptError, ResourceError, ToolError}, protocol::{JsonRpcMessage, ServerCapabilities}, - resource::Resource, tool::{Tool, ToolAnnotations, ToolCall}, }; use mcp_server::router::CapabilitiesBuilder; use mcp_server::Router; -use rmcp::model::{Content, Prompt}; +use rmcp::model::{Content, Prompt, Resource}; // MemoryRouter implementation #[derive(Clone)] diff --git a/crates/goose-mcp/src/tutorial/mod.rs b/crates/goose-mcp/src/tutorial/mod.rs index 83f1a380..0ce94b12 100644 --- a/crates/goose-mcp/src/tutorial/mod.rs +++ b/crates/goose-mcp/src/tutorial/mod.rs @@ -1,7 +1,7 @@ use anyhow::Result; use include_dir::{include_dir, Dir}; use indoc::formatdoc; -use rmcp::model::{Content, Prompt, Role}; +use rmcp::model::{Content, Prompt, Resource, Role}; use serde_json::{json, Value}; use std::{future::Future, pin::Pin}; use tokio::sync::mpsc; @@ -9,7 +9,6 @@ use tokio::sync::mpsc; use mcp_core::{ handler::{PromptError, ResourceError, ToolError}, protocol::{JsonRpcMessage, ServerCapabilities}, - resource::Resource, tool::{Tool, ToolAnnotations}, }; use mcp_server::router::CapabilitiesBuilder; diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index 46170409..d92dfa2b 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -12,8 +12,8 @@ use goose::providers::base::{ConfigKey, ModelInfo, ProviderMetadata}; use goose::session::info::SessionInfo; use goose::session::SessionMetadata; use mcp_core::handler::ToolResultSchema; -use mcp_core::resource::ResourceContents; use mcp_core::tool::{Tool, ToolAnnotations}; +use rmcp::model::ResourceContents; use rmcp::model::{Annotations, Content, EmbeddedResource, ImageContent, Role, TextContent}; use utoipa::{OpenApi, ToSchema}; @@ -286,6 +286,7 @@ derive_utoipa!(EmbeddedResource as EmbeddedResourceSchema); derive_utoipa!(ImageContent as ImageContentSchema); derive_utoipa!(TextContent as TextContentSchema); derive_utoipa!(Annotations as AnnotationsSchema); +derive_utoipa!(ResourceContents as ResourceContentsSchema); #[allow(dead_code)] // Used by utoipa for OpenAPI generation #[derive(OpenApi)] @@ -352,7 +353,7 @@ derive_utoipa!(Annotations as AnnotationsSchema); ThinkingContent, RedactedThinkingContent, FrontendToolRequest, - ResourceContents, + ResourceContentsSchema, ContextLengthExceeded, SummarizationRequested, RoleSchema, diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index 4667a100..6adac400 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -20,7 +20,7 @@ use crate::prompt_template; use mcp_client::client::{ClientCapabilities, ClientInfo, McpClient, McpClientTrait}; use mcp_client::transport::{SseTransport, StdioTransport, StreamableHttpTransport, Transport}; use mcp_core::{Tool, ToolCall, ToolError}; -use rmcp::model::{Content, Prompt}; +use rmcp::model::{Content, Prompt, Resource, ResourceContents}; use serde_json::Value; // By default, we set it to Jan 1, 2020 if the resource does not have a timestamp @@ -427,23 +427,15 @@ impl ExtensionManager { for resource in resources.resources { // Skip reading the resource if it's not marked active // This avoids blowing up the context with inactive resources - if !resource.is_active() { + if !resource_is_active(&resource) { continue; } if let Ok(contents) = client_guard.read_resource(&resource.uri).await { for content in contents.contents { let (uri, content_str) = match content { - mcp_core::resource::ResourceContents::TextResourceContents { - uri, - text, - .. - } => (uri, text), - mcp_core::resource::ResourceContents::BlobResourceContents { - uri, - blob, - .. - } => (uri, blob), + ResourceContents::TextResourceContents { uri, text, .. } => (uri, text), + ResourceContents::BlobResourceContents { uri, blob, .. } => (uri, blob), }; result.push(ResourceItem::new( @@ -550,8 +542,7 @@ impl ExtensionManager { let mut result = Vec::new(); for content in read_result.contents { // Only reading the text resource content; skipping the blob content cause it's too long - if let mcp_core::resource::ResourceContents::TextResourceContents { text, .. } = content - { + if let ResourceContents::TextResourceContents { text, .. } = content { let content_str = format!("{}\n\n{}", uri, text); result.push(Content::text(content_str)); } @@ -825,6 +816,10 @@ impl ExtensionManager { } } +fn resource_is_active(resource: &Resource) -> bool { + resource.priority().is_some_and(|p| (p - 1.0).abs() < 1e-6) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/mcp-core/src/lib.rs b/crates/mcp-core/src/lib.rs index a5da268f..395d2652 100644 --- a/crates/mcp-core/src/lib.rs +++ b/crates/mcp-core/src/lib.rs @@ -1,7 +1,5 @@ pub mod handler; pub mod tool; pub use tool::{Tool, ToolCall}; -pub mod resource; -pub use resource::{Resource, ResourceContents}; pub mod protocol; pub use handler::{ToolError, ToolResult}; diff --git a/crates/mcp-core/src/protocol.rs b/crates/mcp-core/src/protocol.rs index 8a1788b2..bee144cd 100644 --- a/crates/mcp-core/src/protocol.rs +++ b/crates/mcp-core/src/protocol.rs @@ -1,6 +1,6 @@ /// The protocol messages exchanged between client and server -use crate::{resource::Resource, resource::ResourceContents, tool::Tool}; -use rmcp::model::{Content, Prompt, PromptMessage}; +use crate::tool::Tool; +use rmcp::model::{Content, Prompt, PromptMessage, Resource, ResourceContents}; use serde::{Deserialize, Serialize}; use serde_json::Value; diff --git a/crates/mcp-core/src/resource.rs b/crates/mcp-core/src/resource.rs deleted file mode 100644 index 7ad35d71..00000000 --- a/crates/mcp-core/src/resource.rs +++ /dev/null @@ -1,260 +0,0 @@ -/// Resources that servers provide to clients -use anyhow::{anyhow, Result}; -use chrono::{DateTime, Utc}; -use rmcp::model::Annotations; -use serde::{Deserialize, Serialize}; -use url::Url; -use utoipa::ToSchema; - -const EPSILON: f32 = 1e-6; // Tolerance for floating point comparison - -/// Represents a resource in the extension with metadata -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct Resource { - /// URI representing the resource location (e.g., "file:///path/to/file" or "str:///content") - pub uri: String, - /// Name of the resource - pub name: String, - /// Optional description of the resource - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - /// MIME type of the resource content ("text" or "blob") - #[serde(default = "default_mime_type")] - pub mime_type: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub annotations: Option, -} - -#[derive(ToSchema, Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase", untagged)] -pub enum ResourceContents { - TextResourceContents { - uri: String, - #[serde(skip_serializing_if = "Option::is_none")] - mime_type: Option, - text: String, - }, - BlobResourceContents { - uri: String, - #[serde(skip_serializing_if = "Option::is_none")] - mime_type: Option, - blob: String, - }, -} - -fn default_mime_type() -> String { - "text".to_string() -} - -impl Resource { - /// Creates a new Resource from a URI with explicit mime type - pub fn new>( - uri: S, - mime_type: Option, - name: Option, - ) -> Result { - let uri = uri.as_ref(); - let url = Url::parse(uri).map_err(|e| anyhow!("Invalid URI: {}", e))?; - - // Extract name from the path component of the URI - // Use provided name if available, otherwise extract from URI - let name = match name { - Some(n) => n, - None => url - .path_segments() - .and_then(|mut segments| segments.next_back()) - .unwrap_or("unnamed") - .to_string(), - }; - - // Use provided mime_type or default - let mime_type = match mime_type { - Some(t) if t == "text" || t == "blob" => t, - _ => default_mime_type(), - }; - - Ok(Self { - uri: uri.to_string(), - name, - description: None, - mime_type, - annotations: Some(Annotations::for_resource(0.0, Utc::now())), - }) - } - - /// Creates a new Resource with explicit URI, name, and priority - pub fn with_uri>( - uri: S, - name: S, - priority: f32, - mime_type: Option, - ) -> Result { - let uri_string = uri.into(); - Url::parse(&uri_string).map_err(|e| anyhow!("Invalid URI: {}", e))?; - - // Use provided mime_type or default - let mime_type = match mime_type { - Some(t) if t == "text" || t == "blob" => t, - _ => default_mime_type(), - }; - - Ok(Self { - uri: uri_string, - name: name.into(), - description: None, - mime_type, - annotations: Some(Annotations::for_resource(priority, Utc::now())), - }) - } - - /// Updates the resource's timestamp to the current time - pub fn update_timestamp(&mut self) { - self.annotations.as_mut().unwrap().timestamp = Some(Utc::now()); - } - - /// Sets the priority of the resource and returns self for method chaining - pub fn with_priority(mut self, priority: f32) -> Self { - self.annotations.as_mut().unwrap().priority = Some(priority); - self - } - - /// Mark the resource as active, i.e. set its priority to 1.0 - pub fn mark_active(self) -> Self { - self.with_priority(1.0) - } - - // Check if the resource is active - pub fn is_active(&self) -> bool { - if let Some(priority) = self.priority() { - (priority - 1.0).abs() < EPSILON - } else { - false - } - } - - /// Returns the priority of the resource, if set - pub fn priority(&self) -> Option { - self.annotations.as_ref().and_then(|a| a.priority) - } - - /// Returns the timestamp of the resource, if set - pub fn timestamp(&self) -> Option> { - self.annotations.as_ref().and_then(|a| a.timestamp) - } - - /// Returns the scheme of the URI - pub fn scheme(&self) -> Result { - let url = Url::parse(&self.uri)?; - Ok(url.scheme().to_string()) - } - - /// Sets the description of the resource - pub fn with_description>(mut self, description: S) -> Self { - self.description = Some(description.into()); - self - } - - /// Sets the MIME type of the resource - pub fn with_mime_type>(mut self, mime_type: S) -> Self { - let mime_type = mime_type.into(); - match mime_type.as_str() { - "text" | "blob" => self.mime_type = mime_type, - _ => self.mime_type = default_mime_type(), - } - self - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::io::Write; - use tempfile::NamedTempFile; - - #[test] - fn test_new_resource_with_file_uri() -> Result<()> { - let mut temp_file = NamedTempFile::new()?; - writeln!(temp_file, "test content")?; - - let uri = Url::from_file_path(temp_file.path()) - .map_err(|_| anyhow!("Invalid file path"))? - .to_string(); - - let resource = Resource::new(&uri, Some("text".to_string()), None)?; - assert!(resource.uri.starts_with("file:///")); - assert_eq!(resource.priority(), Some(0.0)); - assert_eq!(resource.mime_type, "text"); - assert_eq!(resource.scheme()?, "file"); - - Ok(()) - } - - #[test] - fn test_resource_with_str_uri() -> Result<()> { - let test_content = "Hello, world!"; - let uri = format!("str:///{}", test_content); - let resource = Resource::with_uri( - uri.clone(), - "test.txt".to_string(), - 0.5, - Some("text".to_string()), - )?; - - assert_eq!(resource.uri, uri); - assert_eq!(resource.name, "test.txt"); - assert_eq!(resource.priority(), Some(0.5)); - assert_eq!(resource.mime_type, "text"); - assert_eq!(resource.scheme()?, "str"); - - Ok(()) - } - - #[test] - fn test_mime_type_validation() -> Result<()> { - // Test valid mime types - let resource = Resource::new("file:///test.txt", Some("text".to_string()), None)?; - assert_eq!(resource.mime_type, "text"); - - let resource = Resource::new("file:///test.bin", Some("blob".to_string()), None)?; - assert_eq!(resource.mime_type, "blob"); - - // Test invalid mime type defaults to "text" - let resource = Resource::new("file:///test.txt", Some("invalid".to_string()), None)?; - assert_eq!(resource.mime_type, "text"); - - // Test None defaults to "text" - let resource = Resource::new("file:///test.txt", None, None)?; - assert_eq!(resource.mime_type, "text"); - - Ok(()) - } - - #[test] - fn test_with_description() -> Result<()> { - let resource = Resource::with_uri("file:///test.txt", "test.txt", 0.0, None)? - .with_description("A test resource"); - - assert_eq!(resource.description, Some("A test resource".to_string())); - Ok(()) - } - - #[test] - fn test_with_mime_type() -> Result<()> { - let resource = - Resource::with_uri("file:///test.txt", "test.txt", 0.0, None)?.with_mime_type("blob"); - - assert_eq!(resource.mime_type, "blob"); - - // Test invalid mime type defaults to "text" - let resource = resource.with_mime_type("invalid"); - assert_eq!(resource.mime_type, "text"); - Ok(()) - } - - #[test] - fn test_invalid_uri() { - let result = Resource::new("not-a-uri", None, None); - assert!(result.is_err()); - } -} diff --git a/crates/mcp-server/src/main.rs b/crates/mcp-server/src/main.rs index 7cf12d03..32688cb0 100644 --- a/crates/mcp-server/src/main.rs +++ b/crates/mcp-server/src/main.rs @@ -2,10 +2,10 @@ use anyhow::Result; use mcp_core::handler::{PromptError, ResourceError}; use mcp_core::protocol::JsonRpcMessage; use mcp_core::tool::ToolAnnotations; -use mcp_core::{handler::ToolError, protocol::ServerCapabilities, resource::Resource, tool::Tool}; +use mcp_core::{handler::ToolError, protocol::ServerCapabilities, tool::Tool}; use mcp_server::router::{CapabilitiesBuilder, RouterService}; use mcp_server::{ByteTransport, Router, Server}; -use rmcp::model::{Content, Prompt, PromptArgument}; +use rmcp::model::{Content, Prompt, PromptArgument, RawResource, Resource}; use serde_json::Value; use std::{future::Future, pin::Pin, sync::Arc}; use tokio::sync::mpsc; @@ -47,7 +47,7 @@ impl CounterRouter { } fn _create_resource_text(&self, uri: &str, name: &str) -> Resource { - Resource::new(uri, Some("text/plain".to_string()), Some(name.to_string())).unwrap() + Resource::new(RawResource::new(uri, name), None) } } diff --git a/crates/mcp-server/src/router.rs b/crates/mcp-server/src/router.rs index f0925bee..c2298e21 100644 --- a/crates/mcp-server/src/router.rs +++ b/crates/mcp-server/src/router.rs @@ -14,9 +14,8 @@ use mcp_core::{ PromptsCapability, ReadResourceResult, ResourcesCapability, ServerCapabilities, ToolsCapability, }, - ResourceContents, }; -use rmcp::model::{Content, Prompt, PromptMessage, PromptMessageRole}; +use rmcp::model::{Content, Prompt, PromptMessage, PromptMessageRole, Resource, ResourceContents}; use serde_json::Value; use tokio::sync::mpsc; use tower_service::Service; @@ -93,7 +92,7 @@ pub trait Router: Send + Sync + 'static { arguments: Value, notifier: mpsc::Sender, ) -> Pin, ToolError>> + Send + 'static>>; - fn list_resources(&self) -> Vec; + fn list_resources(&self) -> Vec; fn read_resource( &self, uri: &str,