Skip to main content

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.

A handler need not be async

A 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 removed
  • notifications/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.

Under legacy-spec

The 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.

Additional examples​