MCP Resources
Expose read-only data to MCP clients: implement list_resources, list_resource_templates, and read_resource, returning text, blobs, or rendered UI artifacts.
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_2025_06_18,
capabilities: ServerCapabilities::builder()
.enable_tools()
.enable_resources() // Enable resources
.build(),
// ...
}
}
}
Implementing Resource Methods
list_resources
Returns currently available resources:
use rmcp::model::{ListResourcesResult, PaginatedRequestParams, 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::new(
format!("content://my-server/posts/{}", post.slug),
post.title,
)
.with_description(post.description)
.with_mime_type("text/markdown")
.with_size(post.content.len() as u64)
})
.collect();
Ok(ListResourcesResult {
resources,
next_cursor: None,
meta: None,
})
}
list_resource_templates
Returns URI templates for dynamic resources:
use rmcp::model::{ListResourceTemplatesResult, 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::new(
format!("ui://{SERVER_NAME}/{{artifact_id}}"),
"artifact-ui",
)
.with_title("Artifact UI")
.with_description("Interactive UI for artifacts. Provide artifact_id.")
.with_mime_type(MCP_APP_MIME_TYPE),
ResourceTemplate::new(
format!("content://{SERVER_NAME}/posts/{{slug}}"),
"blog-post",
)
.with_title("Blog Post")
.with_description("Read a blog post by slug")
.with_mime_type("text/markdown"),
];
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
use base64::Engine;
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::engine::general_purpose::STANDARD.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 a UiResource (HTML + CSP policy) using the registry
let ui = self.ui_registry
.render(&artifact)
.await
.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: ui.html,
meta: None,
};
Ok(ReadResourceResult {
contents: vec![contents],
})
}
Custom Renderers
Extend the registry with custom renderers:
The UiRenderer trait is async and keyed by ArtifactType. A renderer declares which artifact type it handles and returns a UiResource (HTML plus a CSP policy):
use async_trait::async_trait;
use systemprompt::mcp::services::ui_renderer::{
registry::create_default_registry, UiRenderer, UiResource,
};
use systemprompt::models::a2a::Artifact;
use systemprompt::models::artifacts::ArtifactType;
struct MyCustomRenderer;
#[async_trait]
impl UiRenderer for MyCustomRenderer {
fn artifact_type(&self) -> ArtifactType {
ArtifactType::Custom("my_custom_type".to_string())
}
async fn render(&self, artifact: &Artifact) -> McpDomainResult<UiResource> {
// Custom HTML rendering logic
let data = extract_data(artifact)?;
Ok(UiResource::new(format!(
r#"<div class="my-custom-artifact">
<h2>{}</h2>
<p>{}</p>
</div>"#,
data.title, data.content
)))
}
}
// Register the custom renderer (keyed by its artifact_type)
let mut registry = create_default_registry();
registry.register(MyCustomRenderer);
Resource MIME Types
| MIME Type | Use For |
|---|---|
text/plain |
Plain text |
text/markdown |
Markdown content |
text/html |
HTML content |
application/json |
JSON data |
text/html;profile=mcp-app |
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::new(
format!("ui://{SERVER_NAME}/{{artifact_id}}"),
"artifact-ui",
)
.with_title("Artifact UI")
.with_description("Render artifact as HTML")
.with_mime_type(MCP_APP_MIME_TYPE);
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,
}],
})
}
}