[MCP] Add support for resources (#5239)

This PR adds support for [MCP
resources](https://modelcontextprotocol.io/specification/2025-06-18/server/resources)
by adding three new tools for the model:
1. `list_resources`
2. `list_resource_templates`
3. `read_resource`

These 3 tools correspond to the [three primary MCP resource protocol
messages](https://modelcontextprotocol.io/specification/2025-06-18/server/resources#protocol-messages).

Example of listing and reading a GitHub resource tempalte
<img width="2984" height="804" alt="CleanShot 2025-10-15 at 17 31 10"
src="https://github.com/user-attachments/assets/89b7f215-2e2a-41c5-90dd-b932ac84a585"
/>

`/mcp` with Figma configured
<img width="2984" height="442" alt="CleanShot 2025-10-15 at 18 29 35"
src="https://github.com/user-attachments/assets/a7578080-2ed2-4c59-b9b4-d8461f90d8ee"
/>

Fixes #4956
This commit is contained in:
Gabriel Peal
2025-10-16 22:05:15 -07:00
committed by GitHub
parent bdda762deb
commit 40fba1bb4c
18 changed files with 1705 additions and 24 deletions

View File

@@ -18,8 +18,17 @@ use rmcp::handler::server::ServerHandler;
use rmcp::model::CallToolRequestParam;
use rmcp::model::CallToolResult;
use rmcp::model::JsonObject;
use rmcp::model::ListResourceTemplatesResult;
use rmcp::model::ListResourcesResult;
use rmcp::model::ListToolsResult;
use rmcp::model::PaginatedRequestParam;
use rmcp::model::RawResource;
use rmcp::model::RawResourceTemplate;
use rmcp::model::ReadResourceRequestParam;
use rmcp::model::ReadResourceResult;
use rmcp::model::Resource;
use rmcp::model::ResourceContents;
use rmcp::model::ResourceTemplate;
use rmcp::model::ServerCapabilities;
use rmcp::model::ServerInfo;
use rmcp::model::Tool;
@@ -33,13 +42,22 @@ use tokio::task;
#[derive(Clone)]
struct TestToolServer {
tools: Arc<Vec<Tool>>,
resources: Arc<Vec<Resource>>,
resource_templates: Arc<Vec<ResourceTemplate>>,
}
const MEMO_URI: &str = "memo://codex/example-note";
const MEMO_CONTENT: &str = "This is a sample MCP resource served by the rmcp test server.";
impl TestToolServer {
fn new() -> Self {
let tools = vec![Self::echo_tool()];
let resources = vec![Self::memo_resource()];
let resource_templates = vec![Self::memo_template()];
Self {
tools: Arc::new(tools),
resources: Arc::new(resources),
resource_templates: Arc::new(resource_templates),
}
}
@@ -62,6 +80,36 @@ impl TestToolServer {
Arc::new(schema),
)
}
fn memo_resource() -> Resource {
let raw = RawResource {
uri: MEMO_URI.to_string(),
name: "example-note".to_string(),
title: Some("Example Note".to_string()),
description: Some("A sample MCP resource exposed for integration tests.".to_string()),
mime_type: Some("text/plain".to_string()),
size: None,
icons: None,
};
Resource::new(raw, None)
}
fn memo_template() -> ResourceTemplate {
let raw = RawResourceTemplate {
uri_template: "memo://codex/{slug}".to_string(),
name: "codex-memo".to_string(),
title: Some("Codex Memo".to_string()),
description: Some(
"Template for memo://codex/{slug} resources used in tests.".to_string(),
),
mime_type: Some("text/plain".to_string()),
};
ResourceTemplate::new(raw, None)
}
fn memo_text() -> &'static str {
MEMO_CONTENT
}
}
#[derive(Deserialize)]
@@ -77,6 +125,7 @@ impl ServerHandler for TestToolServer {
capabilities: ServerCapabilities::builder()
.enable_tools()
.enable_tool_list_changed()
.enable_resources()
.build(),
..ServerInfo::default()
}
@@ -96,6 +145,53 @@ impl ServerHandler for TestToolServer {
}
}
fn list_resources(
&self,
_request: Option<PaginatedRequestParam>,
_context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
) -> impl std::future::Future<Output = Result<ListResourcesResult, McpError>> + Send + '_ {
let resources = self.resources.clone();
async move {
Ok(ListResourcesResult {
resources: (*resources).clone(),
next_cursor: None,
})
}
}
async fn list_resource_templates(
&self,
_request: Option<PaginatedRequestParam>,
_context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
) -> Result<ListResourceTemplatesResult, McpError> {
Ok(ListResourceTemplatesResult {
resource_templates: (*self.resource_templates).clone(),
next_cursor: None,
})
}
async fn read_resource(
&self,
ReadResourceRequestParam { uri }: ReadResourceRequestParam,
_context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
) -> Result<ReadResourceResult, McpError> {
if uri == MEMO_URI {
Ok(ReadResourceResult {
contents: vec![ResourceContents::TextResourceContents {
uri,
mime_type: Some("text/plain".to_string()),
text: Self::memo_text().to_string(),
meta: None,
}],
})
} else {
Err(McpError::resource_not_found(
"resource_not_found",
Some(json!({ "uri": uri })),
))
}
}
async fn call_tool(
&self,
request: CallToolRequestParam,