Replace mcp_core::resource::* with rmcp types (#3563)
This commit is contained in:
@@ -14,12 +14,11 @@ use std::os::unix::fs::PermissionsExt;
|
|||||||
use mcp_core::{
|
use mcp_core::{
|
||||||
handler::{PromptError, ResourceError, ToolError},
|
handler::{PromptError, ResourceError, ToolError},
|
||||||
protocol::{JsonRpcMessage, ServerCapabilities},
|
protocol::{JsonRpcMessage, ServerCapabilities},
|
||||||
resource::Resource,
|
|
||||||
tool::{Tool, ToolAnnotations},
|
tool::{Tool, ToolAnnotations},
|
||||||
};
|
};
|
||||||
use mcp_server::router::CapabilitiesBuilder;
|
use mcp_server::router::CapabilitiesBuilder;
|
||||||
use mcp_server::Router;
|
use mcp_server::Router;
|
||||||
use rmcp::model::{Content, Prompt};
|
use rmcp::model::{AnnotateAble, Content, Prompt, RawResource, Resource};
|
||||||
|
|
||||||
mod docx_tool;
|
mod docx_tool;
|
||||||
mod pdf_tool;
|
mod pdf_tool;
|
||||||
@@ -585,14 +584,16 @@ impl ComputerControllerRouter {
|
|||||||
.map_err(|_| ToolError::ExecutionError("Invalid cache path".into()))?
|
.map_err(|_| ToolError::ExecutionError("Invalid cache path".into()))?
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let resource = Resource::new(
|
let mut resource = RawResource::new(uri.clone(), cache_path.to_string_lossy().into_owned());
|
||||||
uri.clone(),
|
resource.mime_type = Some(if mime_type == "blob" {
|
||||||
Some(mime_type.to_string()),
|
"blob".to_string()
|
||||||
Some(cache_path.to_string_lossy().into_owned()),
|
} else {
|
||||||
)
|
"text".to_string()
|
||||||
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
});
|
||||||
|
self.active_resources
|
||||||
self.active_resources.lock().unwrap().insert(uri, resource);
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(uri, resource.no_annotation());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1175,17 +1176,17 @@ impl Router for ComputerControllerRouter {
|
|||||||
.to_file_path()
|
.to_file_path()
|
||||||
.map_err(|_| ResourceError::NotFound("Invalid file path in URI".into()))?;
|
.map_err(|_| ResourceError::NotFound("Invalid file path in URI".into()))?;
|
||||||
|
|
||||||
match resource.mime_type.as_str() {
|
match resource.raw.mime_type.as_deref() {
|
||||||
"text" | "json" => fs::read_to_string(&path).map_err(|e| {
|
Some("text") | Some("json") | None => fs::read_to_string(&path).map_err(|e| {
|
||||||
ResourceError::ExecutionError(format!("Failed to read file: {}", e))
|
ResourceError::ExecutionError(format!("Failed to read file: {}", e))
|
||||||
}),
|
}),
|
||||||
"binary" => {
|
Some("binary") => {
|
||||||
let bytes = fs::read(&path).map_err(|e| {
|
let bytes = fs::read(&path).map_err(|e| {
|
||||||
ResourceError::ExecutionError(format!("Failed to read file: {}", e))
|
ResourceError::ExecutionError(format!("Failed to read file: {}", e))
|
||||||
})?;
|
})?;
|
||||||
Ok(base64::prelude::BASE64_STANDARD.encode(bytes))
|
Ok(base64::prelude::BASE64_STANDARD.encode(bytes))
|
||||||
}
|
}
|
||||||
mime_type => Err(ResourceError::NotFound(format!(
|
Some(mime_type) => Err(ResourceError::NotFound(format!(
|
||||||
"Unsupported mime type: {}",
|
"Unsupported mime type: {}",
|
||||||
mime_type
|
mime_type
|
||||||
))),
|
))),
|
||||||
|
|||||||
@@ -26,12 +26,11 @@ use mcp_core::tool::ToolAnnotations;
|
|||||||
use mcp_core::{
|
use mcp_core::{
|
||||||
handler::{PromptError, ResourceError, ToolError},
|
handler::{PromptError, ResourceError, ToolError},
|
||||||
protocol::{JsonRpcMessage, JsonRpcNotification, ServerCapabilities},
|
protocol::{JsonRpcMessage, JsonRpcNotification, ServerCapabilities},
|
||||||
resource::Resource,
|
|
||||||
tool::Tool,
|
tool::Tool,
|
||||||
};
|
};
|
||||||
use mcp_server::router::CapabilitiesBuilder;
|
use mcp_server::router::CapabilitiesBuilder;
|
||||||
use mcp_server::Router;
|
use mcp_server::Router;
|
||||||
use rmcp::model::{Content, Prompt, PromptArgument, PromptTemplate};
|
use rmcp::model::{Content, Prompt, PromptArgument, PromptTemplate, Resource};
|
||||||
|
|
||||||
use rmcp::model::Role;
|
use rmcp::model::Role;
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use mcp_core::protocol::JsonRpcMessage;
|
|||||||
use mcp_core::tool::ToolAnnotations;
|
use mcp_core::tool::ToolAnnotations;
|
||||||
use oauth_pkce::PkceOAuth2Client;
|
use oauth_pkce::PkceOAuth2Client;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rmcp::model::{Content, Prompt};
|
use rmcp::model::{AnnotateAble, Content, Prompt, RawResource, Resource};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::{env, fs, future::Future, path::Path, pin::Pin, sync::Arc};
|
use std::{env, fs, future::Future, path::Path, pin::Pin, sync::Arc};
|
||||||
@@ -21,7 +21,6 @@ use tokio::sync::mpsc;
|
|||||||
use mcp_core::{
|
use mcp_core::{
|
||||||
handler::{PromptError, ResourceError, ToolError},
|
handler::{PromptError, ResourceError, ToolError},
|
||||||
protocol::ServerCapabilities,
|
protocol::ServerCapabilities,
|
||||||
resource::Resource,
|
|
||||||
tool::Tool,
|
tool::Tool,
|
||||||
};
|
};
|
||||||
use mcp_server::router::CapabilitiesBuilder;
|
use mcp_server::router::CapabilitiesBuilder;
|
||||||
@@ -1888,12 +1887,15 @@ impl GoogleDriveRouter {
|
|||||||
Ok(r) => {
|
Ok(r) => {
|
||||||
r.1.files
|
r.1.files
|
||||||
.map(|files| {
|
.map(|files| {
|
||||||
files.into_iter().map(|f| Resource {
|
files.into_iter().map(|f| {
|
||||||
uri: f.id.unwrap_or_default(),
|
RawResource {
|
||||||
mime_type: f.mime_type.unwrap_or_default(),
|
uri: f.id.unwrap_or_default(),
|
||||||
name: f.name.unwrap_or_default(),
|
mime_type: f.mime_type,
|
||||||
description: None,
|
name: f.name.unwrap_or_default(),
|
||||||
annotations: None,
|
description: None,
|
||||||
|
size: None,
|
||||||
|
}
|
||||||
|
.no_annotation()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -15,12 +15,11 @@ use tokio::sync::mpsc;
|
|||||||
use mcp_core::{
|
use mcp_core::{
|
||||||
handler::{PromptError, ResourceError, ToolError},
|
handler::{PromptError, ResourceError, ToolError},
|
||||||
protocol::{JsonRpcMessage, ServerCapabilities},
|
protocol::{JsonRpcMessage, ServerCapabilities},
|
||||||
resource::Resource,
|
|
||||||
tool::{Tool, ToolAnnotations, ToolCall},
|
tool::{Tool, ToolAnnotations, ToolCall},
|
||||||
};
|
};
|
||||||
use mcp_server::router::CapabilitiesBuilder;
|
use mcp_server::router::CapabilitiesBuilder;
|
||||||
use mcp_server::Router;
|
use mcp_server::Router;
|
||||||
use rmcp::model::{Content, Prompt};
|
use rmcp::model::{Content, Prompt, Resource};
|
||||||
|
|
||||||
// MemoryRouter implementation
|
// MemoryRouter implementation
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use include_dir::{include_dir, Dir};
|
use include_dir::{include_dir, Dir};
|
||||||
use indoc::formatdoc;
|
use indoc::formatdoc;
|
||||||
use rmcp::model::{Content, Prompt, Role};
|
use rmcp::model::{Content, Prompt, Resource, Role};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::{future::Future, pin::Pin};
|
use std::{future::Future, pin::Pin};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
@@ -9,7 +9,6 @@ use tokio::sync::mpsc;
|
|||||||
use mcp_core::{
|
use mcp_core::{
|
||||||
handler::{PromptError, ResourceError, ToolError},
|
handler::{PromptError, ResourceError, ToolError},
|
||||||
protocol::{JsonRpcMessage, ServerCapabilities},
|
protocol::{JsonRpcMessage, ServerCapabilities},
|
||||||
resource::Resource,
|
|
||||||
tool::{Tool, ToolAnnotations},
|
tool::{Tool, ToolAnnotations},
|
||||||
};
|
};
|
||||||
use mcp_server::router::CapabilitiesBuilder;
|
use mcp_server::router::CapabilitiesBuilder;
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ use goose::providers::base::{ConfigKey, ModelInfo, ProviderMetadata};
|
|||||||
use goose::session::info::SessionInfo;
|
use goose::session::info::SessionInfo;
|
||||||
use goose::session::SessionMetadata;
|
use goose::session::SessionMetadata;
|
||||||
use mcp_core::handler::ToolResultSchema;
|
use mcp_core::handler::ToolResultSchema;
|
||||||
use mcp_core::resource::ResourceContents;
|
|
||||||
use mcp_core::tool::{Tool, ToolAnnotations};
|
use mcp_core::tool::{Tool, ToolAnnotations};
|
||||||
|
use rmcp::model::ResourceContents;
|
||||||
use rmcp::model::{Annotations, Content, EmbeddedResource, ImageContent, Role, TextContent};
|
use rmcp::model::{Annotations, Content, EmbeddedResource, ImageContent, Role, TextContent};
|
||||||
use utoipa::{OpenApi, ToSchema};
|
use utoipa::{OpenApi, ToSchema};
|
||||||
|
|
||||||
@@ -286,6 +286,7 @@ derive_utoipa!(EmbeddedResource as EmbeddedResourceSchema);
|
|||||||
derive_utoipa!(ImageContent as ImageContentSchema);
|
derive_utoipa!(ImageContent as ImageContentSchema);
|
||||||
derive_utoipa!(TextContent as TextContentSchema);
|
derive_utoipa!(TextContent as TextContentSchema);
|
||||||
derive_utoipa!(Annotations as AnnotationsSchema);
|
derive_utoipa!(Annotations as AnnotationsSchema);
|
||||||
|
derive_utoipa!(ResourceContents as ResourceContentsSchema);
|
||||||
|
|
||||||
#[allow(dead_code)] // Used by utoipa for OpenAPI generation
|
#[allow(dead_code)] // Used by utoipa for OpenAPI generation
|
||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
@@ -352,7 +353,7 @@ derive_utoipa!(Annotations as AnnotationsSchema);
|
|||||||
ThinkingContent,
|
ThinkingContent,
|
||||||
RedactedThinkingContent,
|
RedactedThinkingContent,
|
||||||
FrontendToolRequest,
|
FrontendToolRequest,
|
||||||
ResourceContents,
|
ResourceContentsSchema,
|
||||||
ContextLengthExceeded,
|
ContextLengthExceeded,
|
||||||
SummarizationRequested,
|
SummarizationRequested,
|
||||||
RoleSchema,
|
RoleSchema,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use crate::prompt_template;
|
|||||||
use mcp_client::client::{ClientCapabilities, ClientInfo, McpClient, McpClientTrait};
|
use mcp_client::client::{ClientCapabilities, ClientInfo, McpClient, McpClientTrait};
|
||||||
use mcp_client::transport::{SseTransport, StdioTransport, StreamableHttpTransport, Transport};
|
use mcp_client::transport::{SseTransport, StdioTransport, StreamableHttpTransport, Transport};
|
||||||
use mcp_core::{Tool, ToolCall, ToolError};
|
use mcp_core::{Tool, ToolCall, ToolError};
|
||||||
use rmcp::model::{Content, Prompt};
|
use rmcp::model::{Content, Prompt, Resource, ResourceContents};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
// By default, we set it to Jan 1, 2020 if the resource does not have a timestamp
|
// 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 {
|
for resource in resources.resources {
|
||||||
// Skip reading the resource if it's not marked active
|
// Skip reading the resource if it's not marked active
|
||||||
// This avoids blowing up the context with inactive resources
|
// This avoids blowing up the context with inactive resources
|
||||||
if !resource.is_active() {
|
if !resource_is_active(&resource) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(contents) = client_guard.read_resource(&resource.uri).await {
|
if let Ok(contents) = client_guard.read_resource(&resource.uri).await {
|
||||||
for content in contents.contents {
|
for content in contents.contents {
|
||||||
let (uri, content_str) = match content {
|
let (uri, content_str) = match content {
|
||||||
mcp_core::resource::ResourceContents::TextResourceContents {
|
ResourceContents::TextResourceContents { uri, text, .. } => (uri, text),
|
||||||
uri,
|
ResourceContents::BlobResourceContents { uri, blob, .. } => (uri, blob),
|
||||||
text,
|
|
||||||
..
|
|
||||||
} => (uri, text),
|
|
||||||
mcp_core::resource::ResourceContents::BlobResourceContents {
|
|
||||||
uri,
|
|
||||||
blob,
|
|
||||||
..
|
|
||||||
} => (uri, blob),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
result.push(ResourceItem::new(
|
result.push(ResourceItem::new(
|
||||||
@@ -550,8 +542,7 @@ impl ExtensionManager {
|
|||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
for content in read_result.contents {
|
for content in read_result.contents {
|
||||||
// Only reading the text resource content; skipping the blob content cause it's too long
|
// 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);
|
let content_str = format!("{}\n\n{}", uri, text);
|
||||||
result.push(Content::text(content_str));
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
pub mod handler;
|
pub mod handler;
|
||||||
pub mod tool;
|
pub mod tool;
|
||||||
pub use tool::{Tool, ToolCall};
|
pub use tool::{Tool, ToolCall};
|
||||||
pub mod resource;
|
|
||||||
pub use resource::{Resource, ResourceContents};
|
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
pub use handler::{ToolError, ToolResult};
|
pub use handler::{ToolError, ToolResult};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/// The protocol messages exchanged between client and server
|
/// The protocol messages exchanged between client and server
|
||||||
use crate::{resource::Resource, resource::ResourceContents, tool::Tool};
|
use crate::tool::Tool;
|
||||||
use rmcp::model::{Content, Prompt, PromptMessage};
|
use rmcp::model::{Content, Prompt, PromptMessage, Resource, ResourceContents};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
|||||||
@@ -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<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(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<String>,
|
|
||||||
text: String,
|
|
||||||
},
|
|
||||||
BlobResourceContents {
|
|
||||||
uri: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
mime_type: Option<String>,
|
|
||||||
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<S: AsRef<str>>(
|
|
||||||
uri: S,
|
|
||||||
mime_type: Option<String>,
|
|
||||||
name: Option<String>,
|
|
||||||
) -> Result<Self> {
|
|
||||||
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<S: Into<String>>(
|
|
||||||
uri: S,
|
|
||||||
name: S,
|
|
||||||
priority: f32,
|
|
||||||
mime_type: Option<String>,
|
|
||||||
) -> Result<Self> {
|
|
||||||
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<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)?;
|
|
||||||
Ok(url.scheme().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the description of the resource
|
|
||||||
pub fn with_description<S: Into<String>>(mut self, description: S) -> Self {
|
|
||||||
self.description = Some(description.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the MIME type of the resource
|
|
||||||
pub fn with_mime_type<S: Into<String>>(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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,10 +2,10 @@ use anyhow::Result;
|
|||||||
use mcp_core::handler::{PromptError, ResourceError};
|
use mcp_core::handler::{PromptError, ResourceError};
|
||||||
use mcp_core::protocol::JsonRpcMessage;
|
use mcp_core::protocol::JsonRpcMessage;
|
||||||
use mcp_core::tool::ToolAnnotations;
|
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::router::{CapabilitiesBuilder, RouterService};
|
||||||
use mcp_server::{ByteTransport, Router, Server};
|
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 serde_json::Value;
|
||||||
use std::{future::Future, pin::Pin, sync::Arc};
|
use std::{future::Future, pin::Pin, sync::Arc};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
@@ -47,7 +47,7 @@ impl CounterRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn _create_resource_text(&self, uri: &str, name: &str) -> Resource {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ use mcp_core::{
|
|||||||
PromptsCapability, ReadResourceResult, ResourcesCapability, ServerCapabilities,
|
PromptsCapability, ReadResourceResult, ResourcesCapability, ServerCapabilities,
|
||||||
ToolsCapability,
|
ToolsCapability,
|
||||||
},
|
},
|
||||||
ResourceContents,
|
|
||||||
};
|
};
|
||||||
use rmcp::model::{Content, Prompt, PromptMessage, PromptMessageRole};
|
use rmcp::model::{Content, Prompt, PromptMessage, PromptMessageRole, Resource, ResourceContents};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tower_service::Service;
|
use tower_service::Service;
|
||||||
@@ -93,7 +92,7 @@ pub trait Router: Send + Sync + 'static {
|
|||||||
arguments: Value,
|
arguments: Value,
|
||||||
notifier: mpsc::Sender<JsonRpcMessage>,
|
notifier: mpsc::Sender<JsonRpcMessage>,
|
||||||
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ToolError>> + Send + 'static>>;
|
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ToolError>> + Send + 'static>>;
|
||||||
fn list_resources(&self) -> Vec<mcp_core::resource::Resource>;
|
fn list_resources(&self) -> Vec<Resource>;
|
||||||
fn read_resource(
|
fn read_resource(
|
||||||
&self,
|
&self,
|
||||||
uri: &str,
|
uri: &str,
|
||||||
|
|||||||
Reference in New Issue
Block a user