Resources
The Model Context Protocol (MCP) provides a standardized way for servers to expose resources to clients. Resources allow servers to share data that provides context to language models, such as files, database schemas, or application-specific information. Each resource is uniquely identified by a URI.
In the Basics chapter, we learned how to declare a simple dynamic resource:
use neva::prelude::*;
#[resource(
uri = "res://{name}",
title = "Read resource",
descr = "Some details about resource",
mime = "application/octet-stream",
annotations = r#"{
"audience": ["user"],
"priority": 1.0
}"#
)]
async fn get_res(uri: Uri, name: String) -> ResourceContents {
let data = "some file contents"; // Read a resource from some source
ResourceContents::new(uri)
.with_title(name)
.with_blob(data)
}
You can achieve the same result without using the procedural macro:
use neva::prelude::*;
async fn get_res(uri: Uri, name: String) -> ResourceContents {
let data = "some file contents"; // Read a resource from some source
ResourceContents::new(uri)
.with_title(name)
.with_blob(data)
}
#[tokio::main]
async fn main() {
let mut mcp_server = App::new()
.with_options(|opt| opt
.with_stdio()
.with_name("Sample MCP Server")
.with_version("1.0.0"));
mcp_server
.map_resource("res://{name}", "get_res", hello)
.with_description("Some details about resource")
.with_title("Read resource")
.with_mime("application/octet-stream")
.with_annotations(|anotations| anotations
.with_audience("user")
.with_priority(1.0));
mcp_server.run().await;
}
In the example above, the resource template name must be set explicitly.
When using the #[resource] attribute macro, the resource template name is automatically inferred from the function name.
asyncA resource read or listing handler may also be a plain fn. Reading from disk
blocks, so that is the textbook case for #[resource(blocking)] or
neva::blocking, which moves the body onto Tokio's blocking pool instead of
holding a runtime worker. See Handler shapes.
All other resource template parameters that can be specified in the attribute macro can also be configured using with_* methods (for example, with_description()).
The map_resource() method registers a resource template handler under a specified name and returns a mutable reference to the registered resource template.
Contents
There are several ways to provide a resource result.
The most convenient one is to use the ResourceContents enum:
let resource = ResourceContents::new("res://text")
.with_title("Text resource")
.with_text("Some text content");
You can specify a particular content type using helper methods such as:
Each of these methods sets both the content and an appropriate MIME type.
If needed, you can override the MIME type using with_mime().
Alternatively, you can use one of the specialized structs directly:
You can also return an array or Vec of ResourceContents.
They will all be automatically converted into a ReadResourceResult.
Static resources
Above we considered dynamic resources, however, you may also want to define a static resource handler, for example:
#[resource(uri = "res://static_resource")]
async fn get_res(uri: Uri) -> ResourceContents {
TextResourceContents::new(uri, "some file contents")
}
or by using the add_resource() method:
let mut mcp_server = App::new()
.with_options(|opt| opt
.with_stdio()
.with_name("Sample MCP Server")
.with_version("1.0.0"));
mcp_server
.add_resource("res://static_resource", "Some static resource");
mcp_server.run().await;
ui:// resources
A resource whose URI starts with ui:// is an MCP App: an HTML
document a host renders in a sandboxed iframe for a tool that points at it. The
scheme is reserved, and it is what marks the resource — neva serves such a read
as text/html;profile=mcp-app and attaches the _meta.ui security block:
app.add_ui_resource("ui://clock/app.html", "clock", "<!doctype html>…")
.with_title("Clock")
.with_prefers_border(true);
ui:// resources stay out of resources/list by default — a host finds
them through the tool's metadata, not by browsing. See MCP Apps.
Handling list_resources
You can override the function that provides a list of resources and optionally handle pagination using the #[resources] attribute macro:
use neva::prelude::*;
#[resources]
async fn list_resources(_: ListResourcesRequestParams) -> Vec<Resource> {
// Read a list of resources from some source
let resources = vec![
Resource::new("res://res1", "resource 1")
.with_descr("A test resource 1")
.with_mime("text/plain"),
Resource::new("res://res2", "resource 2")
.with_descr("A test resource 2")
.with_mime("text/plain"),
];
resources
}
Alternatively, you can use the map_resources() method:
let mut mcp_server = App::new()
.with_options(|opt| opt
.with_stdio()
.with_name("Sample MCP Server")
.with_version("1.0.0"));
mcp_server.map_resources(|_: ListResourcesRequestParams| async {
// Read a list of resources from some source
let resources = vec![
Resource::new("res://res1", "resource 1")
.with_descr("A test resource 1")
.with_mime("text/plain"),
Resource::new("res://res2", "resource 2")
.with_descr("A test resource 2")
.with_mime("text/plain"),
];
resources
});
mcp_server.run().await;
Resource updates
In addition to reading resources, MCP Server Tools can also add, update, or remove them through
ctx.resources():
use neva::prelude::*;
/// Adding a new resource
#[tool]
async fn add_resource(ctx: Context, uri: Uri) -> Result<(), Error> {
let resource = Resource::from(uri); // Create a new resource
ctx.resources().add(resource).await
}
/// Removing a resource
#[tool]
async fn remove_resource(ctx: Context, uri: Uri) -> Result<(), Error> {
ctx.resources().remove(uri).await?;
Ok(())
}
/// Updating an existing resource
#[tool]
async fn update_resource(ctx: Context, uri: Uri) -> Result<(), Error> {
// Read and update the resource with the given URI
// ...
ctx.resources().notify_updated(uri).await
}
Each of these operations automatically notifies every client subscribed to the corresponding event:
notifications/resources/list_changed— when a resource has been added or removednotifications/resources/updated— when a resource has been updated
Delivery goes to the live subscriptions/listen streams
whose filter admits the notification, so the server needs listChanged and
subscribe advertised for a client to be able to ask for them:
App::new()
.with_options(|opt| opt
.with_resources(|res| res.with_list_changed().with_subscribe()));
Use
ctx.resources().is_subscribed(&uri)
to skip expensive local work nobody is listening for — but not to decide
whether to notify: it is node-local, and under a
notification bus a subscriber
on another instance may be waiting for exactly what this one would skip.
notify_updated therefore does not pre-check it: it publishes
unconditionally and lets the subscription filters route the result.
legacy-specThe resources/subscribe / resources/unsubscribe RPC pair comes back, and
with it ctx.resources().subscribe(uri) / unsubscribe(&uri). Those
methods do not exist in the default build — the client owns the subscription
now. See Subscriptions and Legacy spec.
Learn By Example
Here you may find the full example.