MCP Resources
Implementing MCP resources and templates for exposing data and UI artifacts to clients.
On this page
MCP resources allow servers to expose data that clients can read. Unlike tools which perform actions, resources provide read-only access to content. This is useful for:
- Exposing stored artifacts for display
- Providing UI renderings of tool results
- Sharing configuration or reference data
- Listing available content
Resource Types
Static Resources
Resources with fixed URIs that always exist:
my-server://config
my-server://status
Dynamic Resources
Resources created at runtime (e.g., artifacts):
my-server://artifacts/abc123
my-server://content/blog-post-slug
Resource Templates
URI patterns with variables that clients can fill in:
ui://my-server/{artifact_id}
content://my-server/posts/{slug}
Enabling Resources
Enable resources in your server capabilities:
impl ServerHandler for MyServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
protocol_version: ProtocolVersion::V_2024_11_05,
capabilities: ServerCapabilities::builder()
.enable_tools()
.enable_resources() // Enable resources
.build(),
// ...
}
}
}
Implementing Resource Methods
list_resources
Returns currently available resources:
use rmcp::model::{
ListResourcesResult, PaginatedRequestParams, RawResource, Resource,
};
async fn list_resources(
&self,
_request: Option<PaginatedRequestParams>,
_ctx: RequestContext<RoleServer>,
) -> Result<ListResourcesResult, McpError> {
// Example: List all published blog posts as resources
let posts = self.load_published_posts().await?;
let resources: Vec<Resource> = posts
.into_iter()
.map(|post| Resource {
raw: RawResource {
uri: format!("content://my-server/posts/{}", post.slug),
name: post.title,
description: Some(post.description),
mime_type: Some("text/markdown".to_string()),
size: Some(post.content.len() as u64),
icons: None,
},
annotations: None,
})
.collect();
Ok(ListResourcesResult {
resources,
next_cursor: None,
meta: None,
})
}
list_resource_templates
Returns URI templates for dynamic resources:
use rmcp::model::{
ListResourceTemplatesResult, RawResourceTemplate, ResourceTemplate,
};
use systemprompt::mcp::services::ui_renderer::MCP_APP_MIME_TYPE;
const SERVER_NAME: &str = "my-server";
async fn list_resource_templates(
&self,
_request: Option<PaginatedRequestParams>,
_ctx: RequestContext<RoleServer>,
) -> Result<ListResourceTemplatesResult, McpError> {
let templates = vec![
ResourceTemplate {
raw: RawResourceTemplate {
uri_template: format!("ui://{SERVER_NAME}/{{artifact_id}}"),
name: "artifact-ui".to_string(),
title: Some("Artifact UI".to_string()),
description: Some(
"Interactive UI for artifacts. Provide artifact_id.".to_string()
),
mime_type: Some(MCP_APP_MIME_TYPE.to_string()),
icons: None,
},
annotations: None,
},
ResourceTemplate {
raw: RawResourceTemplate {
uri_template: format!("content://{SERVER_NAME}/posts/{{slug}}"),
name: "blog-post".to_string(),
title: Some("Blog Post".to_string()),
description: Some("Read a blog post by slug".to_string()),
mime_type: Some("text/markdown".to_string()),
icons: None,
},
annotations: None,
},
];
Ok(ListResourceTemplatesResult {
resource_templates: templates,
next_cursor: None,
meta: None,
})
}
read_resource
Reads the content of a resource:
use rmcp::model::{
ReadResourceRequestParams, ReadResourceResult, ResourceContents,
};
async fn read_resource(
&self,
request: ReadResourceRequestParams,
_ctx: RequestContext<RoleServer>,
) -> Result<ReadResourceResult, McpError> {
let uri = &request.uri;
// Route to appropriate handler based on URI prefix
if let Some(artifact_id) = parse_ui_uri(uri) {
self.read_artifact_ui(&artifact_id).await
} else if let Some(slug) = parse_content_uri(uri) {
self.read_blog_post(&slug).await
} else {
Err(McpError::invalid_params(
format!("Unknown resource URI: {uri}"),
None,
))
}
}
URI Parsing
Create helpers to parse resource URIs:
const SERVER_NAME: &str = "my-server";
/// Parse ui://my-server/{artifact_id}
pub fn parse_ui_uri(uri: &str) -> Option<String> {
let prefix = format!("ui://{SERVER_NAME}/");
if uri.starts_with(&prefix) {
Some(uri[prefix.len()..].to_string())
} else {
None
}
}
/// Parse content://my-server/posts/{slug}
pub fn parse_content_uri(uri: &str) -> Option<String> {
let prefix = format!("content://{SERVER_NAME}/posts/");
if uri.starts_with(&prefix) {
Some(uri[prefix.len()..].to_string())
} else {
None
}
}
Returning Resource Content
Text Content
async fn read_blog_post(&self, slug: &str) -> Result<ReadResourceResult, McpError> {
let post = self.load_post_by_slug(slug).await.map_err(|e| {
McpError::internal_error(format!("Failed to load post: {e}"), None)
})?;
let contents = ResourceContents::TextResourceContents {
uri: format!("content://{SERVER_NAME}/posts/{slug}"),
mime_type: Some("text/markdown".to_string()),
text: post.content,
meta: None,
};
Ok(ReadResourceResult {
contents: vec![contents],
})
}
Binary Content
async fn read_image(&self, id: &str) -> Result<ReadResourceResult, McpError> {
let image = self.load_image(id).await?;
let contents = ResourceContents::BlobResourceContents {
uri: format!("images://{SERVER_NAME}/{id}"),
mime_type: Some(image.mime_type),
blob: base64::encode(&image.data),
meta: None,
};
Ok(ReadResourceResult {
contents: vec![contents],
})
}
UI Resources
UI resources render artifacts as HTML for display in clients that support it.
Setup UI Registry
use systemprompt::mcp::services::ui_renderer::{
registry::create_default_registry,
UiRendererRegistry,
MCP_APP_MIME_TYPE,
};
#[derive(Clone)]
pub struct MyServer {
db_pool: DbPool,
service_id: McpServerId,
ui_registry: Arc<UiRendererRegistry>,
}
impl MyServer {
pub fn new(db_pool: DbPool, service_id: McpServerId) -> Self {
Self {
db_pool,
service_id,
ui_registry: Arc::new(create_default_registry()),
}
}
}
Read Artifact UI
async fn read_artifact_ui(&self, artifact_id: &str) -> Result<ReadResourceResult, McpError> {
// Load artifact from database
let artifact = self.load_artifact(artifact_id).await.map_err(|e| {
McpError::internal_error(format!("Failed to load artifact: {e}"), None)
})?;
// Render to HTML using registry
let html = self.ui_registry
.render(&artifact)
.map_err(|e| {
McpError::internal_error(format!("Failed to render: {e}"), None)
})?;
let contents = ResourceContents::TextResourceContents {
uri: format!("ui://{SERVER_NAME}/{artifact_id}"),
mime_type: Some(MCP_APP_MIME_TYPE.to_string()),
text: html,
meta: None,
};
Ok(ReadResourceResult {
contents: vec![contents],
})
}
Custom Renderers
Extend the registry with custom renderers:
use systemprompt::mcp::services::ui_renderer::{UiRenderer, UiRendererRegistry};
struct MyCustomRenderer;
impl UiRenderer for MyCustomRenderer {
fn render(&self, artifact: &Artifact) -> Result<String> {
// Custom HTML rendering logic
let data = extract_data(artifact)?;
Ok(format!(
r#"<div class="my-custom-artifact">
<h2>{}</h2>
<p>{}</p>
</div>"#,
data.title, data.content
))
}
}
// Register custom renderer
let mut registry = create_default_registry();
registry.register("my_custom_type", Box::new(MyCustomRenderer));
Resource MIME Types
| MIME Type | Use For |
|---|---|
text/plain |
Plain text |
text/markdown |
Markdown content |
text/html |
HTML content |
application/json |
JSON data |
application/x-systemprompt-ui |
UI artifacts (MCP_APP_MIME_TYPE) |
image/png, image/jpeg |
Images (as blob) |
Pagination
For large resource lists, implement pagination:
async fn list_resources(
&self,
request: Option<PaginatedRequestParams>,
_ctx: RequestContext<RoleServer>,
) -> Result<ListResourcesResult, McpError> {
let cursor = request
.as_ref()
.and_then(|r| r.cursor.as_ref())
.and_then(|c| c.parse::<usize>().ok())
.unwrap_or(0);
let page_size = 50;
let all_resources = self.load_all_resources().await?;
let page: Vec<_> = all_resources
.into_iter()
.skip(cursor)
.take(page_size)
.collect();
let next_cursor = if page.len() == page_size {
Some((cursor + page_size).to_string())
} else {
None
};
Ok(ListResourcesResult {
resources: page,
next_cursor,
meta: None,
})
}
Complete Example
impl ServerHandler for MyServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
capabilities: ServerCapabilities::builder()
.enable_tools()
.enable_resources()
.build(),
// ...
}
}
async fn list_resources(
&self,
_request: Option<PaginatedRequestParams>,
_ctx: RequestContext<RoleServer>,
) -> Result<ListResourcesResult, McpError> {
Ok(ListResourcesResult {
resources: vec![], // No static resources
next_cursor: None,
meta: None,
})
}
async fn list_resource_templates(
&self,
_request: Option<PaginatedRequestParams>,
_ctx: RequestContext<RoleServer>,
) -> Result<ListResourceTemplatesResult, McpError> {
let template = ResourceTemplate {
raw: RawResourceTemplate {
uri_template: format!("ui://{}/{{artifact_id}}", SERVER_NAME),
name: "artifact-ui".to_string(),
title: Some("Artifact UI".to_string()),
description: Some("Render artifact as HTML".to_string()),
mime_type: Some(MCP_APP_MIME_TYPE.to_string()),
icons: None,
},
annotations: None,
};
Ok(ListResourceTemplatesResult {
resource_templates: vec![template],
next_cursor: None,
meta: None,
})
}
async fn read_resource(
&self,
request: ReadResourceRequestParams,
_ctx: RequestContext<RoleServer>,
) -> Result<ReadResourceResult, McpError> {
let uri = &request.uri;
let artifact_id = parse_ui_uri(uri).ok_or_else(|| {
McpError::invalid_params(format!("Invalid URI: {uri}"), None)
})?;
let html = render_artifact(&self.db_pool, &self.ui_registry, &artifact_id)
.await
.map_err(|e| McpError::internal_error(e.to_string(), None))?;
Ok(ReadResourceResult {
contents: vec![ResourceContents::TextResourceContents {
uri: uri.clone(),
mime_type: Some(MCP_APP_MIME_TYPE.to_string()),
text: html,
meta: None,
}],
})
}
}