Move resource and more types to mcp-core (#470)

Co-authored-by: Bradley Axen <baxen@squareup.com>
This commit is contained in:
Salman Mohammed
2024-12-14 20:00:28 -05:00
committed by GitHub
parent 043bab3955
commit 51c07dffdb
18 changed files with 142 additions and 130 deletions
+24 -13
View File
@@ -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));
+3 -6
View File
@@ -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<Tool>,
@@ -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)
},
+2 -5
View File
@@ -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()
+1 -4
View File
@@ -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,
-3
View File
@@ -1,7 +1,4 @@
mod system;
pub use system::System;
mod resource;
pub use resource::Resource;
pub mod goose_hints;
+1 -3
View File
@@ -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]
+2 -3
View File
@@ -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 {
+3 -6
View File
@@ -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"
-1
View File
@@ -2,4 +2,3 @@ pub mod session;
pub mod sse_transport;
pub mod stdio_transport;
pub mod transport;
pub mod types;
+3 -3
View File
@@ -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)
);
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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};
+1 -1
View File
@@ -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
+7 -1
View File
@@ -8,4 +8,10 @@ async-trait = "0.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
schemars = "0.8"
schemars = "0.8"
anyhow = "1.0"
chrono = { version = "0.4.38", features = ["serde"] }
url = "2.5"
[dev-dependencies]
tempfile = "3.8"
+21
View File
@@ -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<Vec<Role>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<DateTime<Utc>>,
}
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<Utc>) -> 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
+7
View File
@@ -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::*;
@@ -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<Utc>,
/// 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<String>,
/// 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<Annotations>,
}
#[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<String>,
text: String,
},
BlobResourceContents {
uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
mime_type: Option<String>,
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<S: Into<String>>(
uri: S,
name: S,
priority: i32,
priority: f32,
mime_type: Option<String>,
) -> Result<Self> {
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<f32> {
self.annotations.as_ref().and_then(|a| a.priority)
}
/// Returns the timestamp of the resource, if set
pub fn timestamp(&self) -> Option<DateTime<Utc>> {
self.annotations.as_ref().and_then(|a| a.timestamp)
}
/// Returns the scheme of the URI
pub fn scheme(&self) -> Result<String> {
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");
@@ -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<Value>,
}
#[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<PromptsCapability>,
pub resources: Option<ResourcesCapability>,
@@ -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<bool>,
pub list_changed: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesCapability {
pub subscribe: Option<bool>,
pub listChanged: Option<bool>,
pub list_changed: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ToolsCapability {
pub listChanged: Option<bool>,
pub list_changed: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ListResourcesResult {
pub resources: Vec<Resource>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Resource {
pub uri: String,
pub name: String,
pub description: Option<String>,
pub mimeType: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ReadResourceResult {
pub contents: Vec<ResourceContents>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ResourceContents {
pub uri: String,
pub mimeType: Option<String>,
#[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<Tool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Tool {
pub name: String,
pub description: Option<String>,
pub inputSchema: Value,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallToolResult {
pub content: Vec<Content>,
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)]