From 51c07dffdbdde89dcfaa13f7f1a6dfeb0bbe7c50 Mon Sep 17 00:00:00 2001 From: Salman Mohammed Date: Sat, 14 Dec 2024 20:00:28 -0500 Subject: [PATCH] Move resource and more types to mcp-core (#470) Co-authored-by: Bradley Axen --- crates/goose/src/agent.rs | 37 ++++++--- crates/goose/src/developer.rs | 9 +- crates/goose/src/memory.rs | 7 +- crates/goose/src/systems/goose_hints.rs | 5 +- crates/goose/src/systems/mod.rs | 3 - crates/goose/src/systems/system.rs | 4 +- crates/goose/tests/systems.rs | 5 +- crates/mcp-client/Cargo.toml | 9 +- crates/mcp-client/src/lib.rs | 1 - crates/mcp-client/src/session.rs | 6 +- crates/mcp-client/src/sse_transport.rs | 2 +- crates/mcp-client/src/stdio_transport.rs | 2 +- crates/mcp-client/src/transport.rs | 2 +- crates/mcp-core/Cargo.toml | 8 +- crates/mcp-core/src/content.rs | 21 +++++ crates/mcp-core/src/lib.rs | 7 ++ .../src/systems => mcp-core/src}/resource.rs | 62 ++++++++++---- crates/{mcp-client => mcp-core}/src/types.rs | 82 +++++-------------- 18 files changed, 142 insertions(+), 130 deletions(-) rename crates/{goose/src/systems => mcp-core/src}/resource.rs (79%) rename crates/{mcp-client => mcp-core}/src/types.rs (62%) diff --git a/crates/goose/src/agent.rs b/crates/goose/src/agent.rs index e0d21a5675..c544a1cf9c 100644 --- a/crates/goose/src/agent.rs +++ b/crates/goose/src/agent.rs @@ -8,16 +8,18 @@ use crate::errors::{AgentError, AgentResult}; use crate::message::{Message, ToolRequest}; use crate::prompt_template::load_prompt_file; use crate::providers::base::Provider; -use crate::systems::{Resource, System}; +use crate::systems::System; use crate::token_counter::TokenCounter; -use mcp_core::content::Content; -use mcp_core::tool::{Tool, ToolCall}; +use mcp_core::{Content, Resource, Tool, ToolCall}; use serde::Serialize; const CONTEXT_LIMIT: usize = 200_000; // TODO: model's context limit should be in provider config const ESTIMATE_FACTOR: f32 = 0.8; const ESTIMATED_TOKEN_LIMIT: usize = (CONTEXT_LIMIT as f32 * ESTIMATE_FACTOR) as usize; +// used to sort resources by priority within error margin +const PRIORITY_EPSILON: f32 = 0.001; + #[derive(Clone, Debug, Serialize)] struct SystemInfo { name: String, @@ -239,12 +241,20 @@ impl Agent { } // Sort by priority (high to low) and timestamp (newest to oldest) + // since priority is float, we need to sort by priority within error margin - PRIORITY_EPSILON all_resources.sort_by(|a, b| { - let priority_cmp = b.2.priority.cmp(&a.2.priority); - if priority_cmp == std::cmp::Ordering::Equal { - b.2.timestamp.cmp(&a.2.timestamp) + // Compare priorities with epsilon + // Compare priorities with Option handling - default to 0.0 if None + let a_priority = a.2.priority().unwrap_or(0.0); + let b_priority = b.2.priority().unwrap_or(0.0); + if (b_priority - a_priority).abs() < PRIORITY_EPSILON { + // Priorities are "equal" within epsilon, use timestamp as tiebreaker + b.2.timestamp().cmp(&a.2.timestamp()) } else { - priority_cmp + // Priorities are different enough, use priority ordering + b.2.priority() + .partial_cmp(&a.2.priority()) + .unwrap_or(std::cmp::Ordering::Equal) } }); @@ -392,9 +402,11 @@ mod tests { use super::*; use crate::message::MessageContent; use crate::providers::mock::MockProvider; - use crate::systems::Resource; use async_trait::async_trait; + use chrono::Utc; use futures::TryStreamExt; + use mcp_core::resource::Resource; + use mcp_core::Annotations; use serde_json::json; // Mock system for testing @@ -419,13 +431,12 @@ mod tests { } } - fn add_resource(&mut self, name: &str, content: &str, priority: i32) { + fn add_resource(&mut self, name: &str, content: &str, priority: f32) { let uri = format!("file://{}", name); let resource = Resource { name: name.to_string(), uri: uri.clone(), - priority, - timestamp: chrono::Utc::now(), + annotations: Some(Annotations::for_resource(priority, Utc::now())), description: Some("A mock resource".to_string()), mime_type: "text/plain".to_string(), }; @@ -602,8 +613,8 @@ mod tests { // Add two resources with different priorities let string_10toks = "hello ".repeat(10); - system.add_resource("high_priority", &string_10toks, 4); - system.add_resource("low_priority", &string_10toks, 1); + system.add_resource("high_priority", &string_10toks, 0.8); + system.add_resource("low_priority", &string_10toks, 0.1); agent.add_system(Box::new(system)); diff --git a/crates/goose/src/developer.rs b/crates/goose/src/developer.rs index d98bfd4434..3e0cdca56d 100644 --- a/crates/goose/src/developer.rs +++ b/crates/goose/src/developer.rs @@ -1,6 +1,5 @@ mod lang; -use crate::systems::Resource; use anyhow::Result as AnyhowResult; use async_trait::async_trait; use base64::Engine; @@ -17,9 +16,7 @@ use xcap::{Monitor, Window}; use crate::errors::{AgentError, AgentResult}; use crate::systems::System; -use mcp_core::content::Content; -use mcp_core::role::Role; -use mcp_core::tool::{Tool, ToolCall}; +use mcp_core::{Content, Resource, Role, Tool, ToolCall}; pub struct DeveloperSystem { tools: Vec, @@ -172,7 +169,7 @@ impl DeveloperSystem { You can capture either: 1. A full display (monitor) using the display parameter 2. A specific window by its title using the window_title parameter - + Only one of display or window_title should be specified. "#}, json!({ @@ -276,7 +273,7 @@ impl DeveloperSystem { uri.clone(), Resource::new(uri, Some("text".to_string()), Some("cwd".to_string())) .unwrap() - .with_priority(1000), // Set highest priority + .with_priority(1.0), // Set highest priority ); Mutex::new(resources) }, diff --git a/crates/goose/src/memory.rs b/crates/goose/src/memory.rs index b9c9ebdaee..e40179ddf4 100644 --- a/crates/goose/src/memory.rs +++ b/crates/goose/src/memory.rs @@ -3,16 +3,13 @@ use crate::systems::System; use anyhow::Result as AnyhowResult; use async_trait::async_trait; use indoc::formatdoc; -use mcp_core::content::Content; -use mcp_core::tool::{Tool, ToolCall}; +use mcp_core::{Content, Resource, Tool, ToolCall}; use serde_json::json; use std::collections::HashMap; use std::fs; use std::io::{self, Read, Write}; use std::path::PathBuf; -use crate::systems::Resource; - #[derive(Debug, Default)] pub struct MemoryManager { global_memory_dir: PathBuf, @@ -406,7 +403,7 @@ impl System for MemorySystem { Resource::with_uri( format!("str:///{}.txt", memories.join(" ")), format!("{}.txt", category), - 0, + 0.0, Some("text".to_string()), ) .ok() diff --git a/crates/goose/src/systems/goose_hints.rs b/crates/goose/src/systems/goose_hints.rs index ad4feadf28..b5056f8d9f 100644 --- a/crates/goose/src/systems/goose_hints.rs +++ b/crates/goose/src/systems/goose_hints.rs @@ -2,12 +2,9 @@ use anyhow::Result as AnyhowResult; use async_trait::async_trait; use std::fs; -use super::Resource; use crate::errors::{AgentError, AgentResult}; use crate::systems::System; -use mcp_core::content::Content; -use mcp_core::tool::Tool; -use mcp_core::tool::ToolCall; +use mcp_core::{Content, Resource, Tool, ToolCall}; pub struct GooseHintsSystem { instructions: String, diff --git a/crates/goose/src/systems/mod.rs b/crates/goose/src/systems/mod.rs index 3a8e34a2a5..142a9c3665 100644 --- a/crates/goose/src/systems/mod.rs +++ b/crates/goose/src/systems/mod.rs @@ -1,7 +1,4 @@ mod system; pub use system::System; -mod resource; -pub use resource::Resource; - pub mod goose_hints; diff --git a/crates/goose/src/systems/system.rs b/crates/goose/src/systems/system.rs index 726091a3d6..0151224a36 100644 --- a/crates/goose/src/systems/system.rs +++ b/crates/goose/src/systems/system.rs @@ -1,10 +1,8 @@ use anyhow::Result as AnyhowResult; use async_trait::async_trait; -use super::Resource; use crate::errors::AgentResult; -use mcp_core::content::Content; -use mcp_core::tool::{Tool, ToolCall}; +use mcp_core::{Content, Resource, Tool, ToolCall}; /// Core trait that defines a system that can be operated by an AI agent #[async_trait] diff --git a/crates/goose/tests/systems.rs b/crates/goose/tests/systems.rs index 390f4f2414..70c0af33e3 100644 --- a/crates/goose/tests/systems.rs +++ b/crates/goose/tests/systems.rs @@ -3,9 +3,8 @@ use async_trait::async_trait; use serde_json::{json, Value}; use goose::errors::{AgentError, AgentResult}; -use goose::systems::{Resource, System}; -use mcp_core::content::Content; -use mcp_core::tool::{Tool, ToolCall}; +use goose::systems::System; +use mcp_core::{Content, Resource, Tool, ToolCall}; /// A simple system that echoes input back to the caller pub struct EchoSystem { diff --git a/crates/mcp-client/Cargo.toml b/crates/mcp-client/Cargo.toml index 71e81ba6b0..6de3d39033 100644 --- a/crates/mcp-client/Cargo.toml +++ b/crates/mcp-client/Cargo.toml @@ -1,13 +1,10 @@ [package] name = "mcp-client" -edition.workspace = true -version.workspace = true -authors.workspace = true -license.workspace = true -repository.workspace = true -description.workspace = true +version = "0.1.0" +edition = "2021" [dependencies] +mcp-core = { path = "../mcp-core" } tokio = { version = "1", features = ["full"] } reqwest = { version = "0.11", default-features = false, features = ["json", "stream", "rustls-tls"] } reqwest-eventsource = "0.5.0" diff --git a/crates/mcp-client/src/lib.rs b/crates/mcp-client/src/lib.rs index c95ef319af..3172f2944f 100644 --- a/crates/mcp-client/src/lib.rs +++ b/crates/mcp-client/src/lib.rs @@ -2,4 +2,3 @@ pub mod session; pub mod sse_transport; pub mod stdio_transport; pub mod transport; -pub mod types; diff --git a/crates/mcp-client/src/session.rs b/crates/mcp-client/src/session.rs index 13dff95e9b..7d8a32cb78 100644 --- a/crates/mcp-client/src/session.rs +++ b/crates/mcp-client/src/session.rs @@ -1,6 +1,6 @@ use crate::transport::{ReadStream, WriteStream}; -use crate::types::*; use anyhow::{anyhow, Context, Result}; +use mcp_core::types::*; use serde::de::DeserializeOwned; use serde_json::{json, Value}; use std::sync::atomic::{AtomicU64, Ordering}; @@ -373,9 +373,9 @@ mod tests { // Initialize the session let init_result = session.initialize().await?; - assert_eq!(init_result.protocolVersion, "2024-11-05"); + assert_eq!(init_result.protocol_version, "2024-11-05"); assert_eq!( - init_result.capabilities.resources.unwrap().listChanged, + init_result.capabilities.resources.unwrap().list_changed, Some(false) ); diff --git a/crates/mcp-client/src/sse_transport.rs b/crates/mcp-client/src/sse_transport.rs index 43651caf4b..dd275486d3 100644 --- a/crates/mcp-client/src/sse_transport.rs +++ b/crates/mcp-client/src/sse_transport.rs @@ -1,8 +1,8 @@ use crate::transport::{ReadStream, Transport, WriteStream}; -use crate::types::JsonRpcMessage; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use futures_util::StreamExt; +use mcp_core::types::JsonRpcMessage; use reqwest::{Client, Url}; use reqwest_eventsource::{Event, EventSource}; use std::sync::Arc; diff --git a/crates/mcp-client/src/stdio_transport.rs b/crates/mcp-client/src/stdio_transport.rs index 298ae41d6c..ea95e0e870 100644 --- a/crates/mcp-client/src/stdio_transport.rs +++ b/crates/mcp-client/src/stdio_transport.rs @@ -1,7 +1,7 @@ use crate::transport::{ReadStream, Transport, WriteStream}; -use crate::types::*; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; +use mcp_core::types::*; use std::process::Stdio; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::process::{Child, Command}; diff --git a/crates/mcp-client/src/transport.rs b/crates/mcp-client/src/transport.rs index f56086ae4c..77fc2d279c 100644 --- a/crates/mcp-client/src/transport.rs +++ b/crates/mcp-client/src/transport.rs @@ -1,6 +1,6 @@ -use crate::types::JsonRpcMessage; use anyhow::Result; use async_trait::async_trait; +use mcp_core::types::JsonRpcMessage; use tokio::sync::mpsc::{Receiver, Sender}; // Stream types for consistent interface diff --git a/crates/mcp-core/Cargo.toml b/crates/mcp-core/Cargo.toml index 380553cb08..e60aeabdeb 100644 --- a/crates/mcp-core/Cargo.toml +++ b/crates/mcp-core/Cargo.toml @@ -8,4 +8,10 @@ async-trait = "0.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" -schemars = "0.8" \ No newline at end of file +schemars = "0.8" +anyhow = "1.0" +chrono = { version = "0.4.38", features = ["serde"] } +url = "2.5" + +[dev-dependencies] +tempfile = "3.8" diff --git a/crates/mcp-core/src/content.rs b/crates/mcp-core/src/content.rs index 18f279295f..e84696b916 100644 --- a/crates/mcp-core/src/content.rs +++ b/crates/mcp-core/src/content.rs @@ -1,4 +1,5 @@ use super::role::Role; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -8,6 +9,24 @@ pub struct Annotations { pub audience: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub priority: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub timestamp: Option>, +} + +impl Annotations { + /// Creates a new Annotations instance specifically for resources + /// optional priority, and a timestamp (defaults to now if None) + pub fn for_resource(priority: f32, timestamp: DateTime) -> Self { + assert!( + (0.0..=1.0).contains(&priority), + "Priority {priority} must be between 0.0 and 1.0" + ); + Annotations { + priority: Some(priority), + timestamp: Some(timestamp), + audience: None, + } + } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -81,6 +100,7 @@ impl Content { None => Annotations { audience: Some(audience), priority: None, + timestamp: None, }, }); self @@ -105,6 +125,7 @@ impl Content { None => Annotations { audience: None, priority: Some(priority), + timestamp: None, }, }); self diff --git a/crates/mcp-core/src/lib.rs b/crates/mcp-core/src/lib.rs index d62ee80f7d..b070c0c5fe 100644 --- a/crates/mcp-core/src/lib.rs +++ b/crates/mcp-core/src/lib.rs @@ -1,4 +1,11 @@ pub mod content; +pub use content::{Annotations, Content, ImageContent, TextContent}; pub mod handler; pub mod role; +pub use role::Role; pub mod tool; +pub use tool::{Tool, ToolCall}; +pub mod resource; +pub use resource::{Resource, ResourceContents}; +pub mod types; +pub use types::*; diff --git a/crates/goose/src/systems/resource.rs b/crates/mcp-core/src/resource.rs similarity index 79% rename from crates/goose/src/systems/resource.rs rename to crates/mcp-core/src/resource.rs index e424d481d8..9972fab0b7 100644 --- a/crates/goose/src/systems/resource.rs +++ b/crates/mcp-core/src/resource.rs @@ -3,23 +3,41 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use url::Url; +use crate::content::Annotations; + /// Represents a resource in the system with metadata -#[derive(Debug, Clone, Serialize, Deserialize)] +#[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, - /// Last modified timestamp - pub timestamp: DateTime, - /// Priority of the resource (higher number means higher priority) - pub priority: i32, /// 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(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 { @@ -56,10 +74,9 @@ impl Resource { Ok(Self { uri: uri.to_string(), name, - timestamp: Utc::now(), - priority: 0, description: None, mime_type, + annotations: Some(Annotations::for_resource(0.0, Utc::now())), }) } @@ -67,7 +84,7 @@ impl Resource { pub fn with_uri>( uri: S, name: S, - priority: i32, + priority: f32, mime_type: Option, ) -> Result { let uri_string = uri.into(); @@ -82,24 +99,33 @@ impl Resource { Ok(Self { uri: uri_string, name: name.into(), - timestamp: Utc::now(), - priority, 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.timestamp = Utc::now(); + 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: i32) -> Self { - self.priority = priority; + pub fn with_priority(mut self, priority: f32) -> Self { + self.annotations.as_mut().unwrap().priority = Some(priority); self } + /// 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)?; @@ -140,7 +166,7 @@ mod tests { let resource = Resource::new(&uri, Some("text".to_string()), None)?; assert!(resource.uri.starts_with("file:///")); - assert_eq!(resource.priority, 0); + assert_eq!(resource.priority(), Some(0.0)); assert_eq!(resource.mime_type, "text"); assert_eq!(resource.scheme()?, "file"); @@ -154,13 +180,13 @@ mod tests { let resource = Resource::with_uri( uri.clone(), "test.txt".to_string(), - 5, + 0.5, Some("text".to_string()), )?; assert_eq!(resource.uri, uri); assert_eq!(resource.name, "test.txt"); - assert_eq!(resource.priority, 5); + assert_eq!(resource.priority(), Some(0.5)); assert_eq!(resource.mime_type, "text"); assert_eq!(resource.scheme()?, "str"); @@ -189,7 +215,7 @@ mod tests { #[test] fn test_with_description() -> Result<()> { - let resource = Resource::with_uri("file:///test.txt", "test.txt", 0, None)? + 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())); @@ -199,7 +225,7 @@ mod tests { #[test] fn test_with_mime_type() -> Result<()> { let resource = - Resource::with_uri("file:///test.txt", "test.txt", 0, None)?.with_mime_type("blob"); + Resource::with_uri("file:///test.txt", "test.txt", 0.0, None)?.with_mime_type("blob"); assert_eq!(resource.mime_type, "blob"); diff --git a/crates/mcp-client/src/types.rs b/crates/mcp-core/src/types.rs similarity index 62% rename from crates/mcp-client/src/types.rs rename to crates/mcp-core/src/types.rs index 8647c155a9..dc606ac51d 100644 --- a/crates/mcp-client/src/types.rs +++ b/crates/mcp-core/src/types.rs @@ -1,5 +1,4 @@ -#![allow(non_snake_case)] - +use crate::{content::Content, resource::Resource, resource::ResourceContents, tool::Tool}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -64,20 +63,21 @@ pub struct ErrorData { pub data: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct InitializeResult { - pub protocolVersion: String, + pub protocol_version: String, pub capabilities: ServerCapabilities, - pub serverInfo: Implementation, + pub server_info: Implementation, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Implementation { pub name: String, pub version: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ServerCapabilities { pub prompts: Option, pub resources: Option, @@ -85,85 +85,45 @@ pub struct ServerCapabilities { // Add other capabilities as needed } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct PromptsCapability { - pub listChanged: Option, + pub list_changed: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct ResourcesCapability { pub subscribe: Option, - pub listChanged: Option, + pub list_changed: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct ToolsCapability { - pub listChanged: Option, + pub list_changed: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ListResourcesResult { pub resources: Vec, } -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct Resource { - pub uri: String, - pub name: String, - pub description: Option, - pub mimeType: Option, -} - -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ReadResourceResult { pub contents: Vec, } -#[derive(Debug, Serialize, Deserialize)] -pub struct ResourceContents { - pub uri: String, - pub mimeType: Option, - #[serde(flatten)] - pub content: ResourceContent, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum ResourceContent { - Text { text: String }, - Blob { blob: String }, // Base64-encoded -} - -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ListToolsResult { pub tools: Vec, } #[derive(Debug, Serialize, Deserialize)] -pub struct Tool { - pub name: String, - pub description: Option, - pub inputSchema: Value, -} - -#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct CallToolResult { pub content: Vec, - pub isError: bool, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "type")] -pub enum Content { - #[serde(rename = "text")] - Text { text: String }, - #[serde(rename = "image")] - Image { - data: String, // Base64-encoded image data - mimeType: String, - }, - #[serde(rename = "resource")] - EmbeddedResource { resource: ResourceContents }, + pub is_error: bool, } #[derive(Debug, Serialize, Deserialize)]