diff --git a/crates/mcp-core/src/handler.rs b/crates/mcp-core/src/handler.rs index 5dfd51681e89..407d91e64cb1 100644 --- a/crates/mcp-core/src/handler.rs +++ b/crates/mcp-core/src/handler.rs @@ -17,6 +17,14 @@ pub enum ToolError { NotFound(String), } +#[derive(Error, Debug)] +pub enum ResourceError { + #[error("Execution failed: {0}")] + ExecutionError(String), + #[error("Resource not found: {0}")] + NotFound(String), +} + pub type Result = std::result::Result; /// Trait for implementing MCP tools diff --git a/crates/mcp-server/README.md b/crates/mcp-server/README.md new file mode 100644 index 000000000000..1e4f06176553 --- /dev/null +++ b/crates/mcp-server/README.md @@ -0,0 +1,7 @@ +### Test with MCP Inspector + +```bash +npx @modelcontextprotocol/inspector cargo run -p mcp-server +``` + +Then visit the Inspector in the browser window and test the different endpoints. \ No newline at end of file diff --git a/crates/mcp-server/src/errors.rs b/crates/mcp-server/src/errors.rs index 1f19069cdb99..a87cd7aff1f6 100644 --- a/crates/mcp-server/src/errors.rs +++ b/crates/mcp-server/src/errors.rs @@ -49,6 +49,8 @@ pub enum RouterError { #[error("Tool not found: {0}")] ToolNotFound(String), + #[error("Resource not found: {0}")] + ResourceNotFound(String), } impl From for mcp_core::protocol::ErrorData { @@ -75,6 +77,20 @@ impl From for mcp_core::protocol::ErrorData { message: msg, data: None, }, + RouterError::ResourceNotFound(msg) => ErrorData { + code: INVALID_REQUEST, + message: msg, + data: None, + }, + } + } +} + +impl From for RouterError { + fn from(err: mcp_core::handler::ResourceError) -> Self { + match err { + mcp_core::handler::ResourceError::NotFound(msg) => RouterError::ResourceNotFound(msg), + _ => RouterError::Internal("Unknown resource error".to_string()), } } } diff --git a/crates/mcp-server/src/main.rs b/crates/mcp-server/src/main.rs index 9542962e5253..07a89e0dd6d8 100644 --- a/crates/mcp-server/src/main.rs +++ b/crates/mcp-server/src/main.rs @@ -1,8 +1,10 @@ use anyhow::Result; +use mcp_core::handler::ResourceError; use mcp_core::{ handler::ToolError, tool::Tool, protocol::ServerCapabilities, + resource::Resource, }; use mcp_server::{Router, Server, ByteTransport}; use mcp_server::router::{RouterService, CapabilitiesBuilder}; @@ -104,6 +106,36 @@ impl Router for CounterRouter { } }) } + + fn list_resources(&self) -> Vec { + vec![ + Resource::new( + "memo://insights".to_string(), + Some("text/plain".to_string()), + Some("memo-resource".to_string()) + ).unwrap() + ] + } + + fn read_resource( + &self, + uri: &str, + ) -> Pin> + Send + 'static>> { + let uri = uri.to_string(); + Box::pin(async move { + match uri.as_str() { + "memo://insights" => { + let memo = + "Business Intelligence Memo\n\nAnalysis has revealed 5 key insights ..."; + Ok(memo.to_string()) + } + _ => Err(ResourceError::NotFound(format!( + "Resource {} not found", + uri + ))), + } + }) + } } #[tokio::main] diff --git a/crates/mcp-server/src/router.rs b/crates/mcp-server/src/router.rs index 925f21148713..4c2448843334 100644 --- a/crates/mcp-server/src/router.rs +++ b/crates/mcp-server/src/router.rs @@ -7,12 +7,9 @@ use std::{ use tokio::time::sleep; use mcp_core::{ - handler::ToolError, - protocol::{ - JsonRpcRequest, JsonRpcResponse, ServerCapabilities, InitializeResult, Implementation, - PromptsCapability, ResourcesCapability, ToolsCapability, ListToolsResult, CallToolResult, - }, - content::Content, + content::Content, handler::{ResourceError, ToolError}, protocol::{ + CallToolResult, Implementation, InitializeResult, JsonRpcRequest, JsonRpcResponse, ListResourcesResult, ListToolsResult, PromptsCapability, ReadResourceResult, ResourcesCapability, ServerCapabilities, ToolsCapability + }, ResourceContents }; use tower_service::Service; use serde_json::Value; @@ -76,6 +73,8 @@ pub trait Router: Send + Sync + 'static { fn capabilities(&self) -> ServerCapabilities; fn list_tools(&self) -> Vec; fn call_tool(&self, tool_name: &str, arguments: Value) -> Pin> + Send + 'static>>; + fn list_resources(&self) -> Vec; + fn read_resource(&self, uri: &str) -> Pin> + Send + 'static>>; // Helper method to create base response fn create_response(&self, id: Option) -> JsonRpcResponse { @@ -149,6 +148,53 @@ pub trait Router: Send + Sync + 'static { Ok(response) } } + + fn handle_resources_list(&self, req: JsonRpcRequest) -> impl Future> + Send { + async move { + let resources = self.list_resources(); + + let result = ListResourcesResult { resources }; + let mut response = self.create_response(req.id); + response.result = Some(serde_json::to_value(result) + .map_err(|e| RouterError::Internal(format!("JSON serialization error: {}", e)))?); + + Ok(response) + } + } + + fn handle_resources_read( + &self, + req: JsonRpcRequest, + ) -> impl Future> + Send { + async move { + let params = req + .params + .ok_or_else(|| RouterError::InvalidParams("Missing parameters".into()))?; + + let uri = params + .get("uri") + .and_then(Value::as_str) + .ok_or_else(|| RouterError::InvalidParams("Missing resource URI".into()))?; + + let contents = self.read_resource(uri).await.map_err(RouterError::from)?; + + let result = ReadResourceResult { + contents: vec![ResourceContents::TextResourceContents { + uri: uri.to_string(), + mime_type: Some("text/plain".to_string()), + text: contents, + }], + }; + + let mut response = self.create_response(req.id); + response.result = + Some(serde_json::to_value(result).map_err(|e| { + RouterError::Internal(format!("JSON serialization error: {}", e)) + })?); + + Ok(response) + } + } } // A wrapper type to implement the Service trait locally @@ -180,6 +226,8 @@ where "initialize" => this.handle_initialize(req).await, "tools/list" => this.handle_tools_list(req).await, "tools/call" => this.handle_tools_call(req).await, + "resources/list" => this.handle_resources_list(req).await, + "resources/read" => this.handle_resources_read(req).await, _ => { let mut response = this.create_response(req.id); response.error = Some(RouterError::MethodNotFound(req.method).into());