Перейти к основному содержимому

Ресурсы

Model Context Protocol (MCP) предоставляет стандартизированный способ для серверов предоставлять клиентам ресурсы. Ресурсы позволяют серверам передавать данные, обеспечивающие контекст для языковых моделей: файлы, схемы баз данных или специфичная для приложения информация. Каждый ресурс уникально идентифицируется по URI.

В главе Основы мы научились объявлять простой динамический ресурс:

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"; // Читаем ресурс из источника

ResourceContents::new(uri)
.with_title(name)
.with_blob(data)
}

Того же результата можно добиться без использования процедурного макроса:

use neva::prelude::*;

async fn get_res(uri: Uri, name: String) -> ResourceContents {
let data = "some file contents"; // Читаем ресурс из источника

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;
}

В примере выше имя шаблона ресурса должно быть задано явно. При использовании атрибутного макроса #[resource] имя шаблона ресурса автоматически выводится из имени функции.

Обработчик не обязан быть async

Обработчик чтения ресурса или списка ресурсов тоже может быть обычной fn. Чтение с диска блокируется, так что это хрестоматийный случай для #[resource(blocking)] или neva::blocking: тело уедет на blocking-пул Tokio вместо того, чтобы удерживать воркер рантайма. См. Формы обработчиков.

Все остальные параметры шаблона ресурса, доступные в атрибутном макросе, можно настроить с помощью методов with_* (например, with_description()).

Метод map_resource() регистрирует обработчик шаблона ресурса под указанным именем и возвращает изменяемую ссылку на зарегистрированный шаблон ресурса.

Содержимое​

Существует несколько способов вернуть результат ресурса. Наиболее удобный — использование перечисления ResourceContents:

let resource = ResourceContents::new("res://text")
.with_title("Text resource")
.with_text("Some text content");

Конкретный тип содержимого задаётся вспомогательными методами:

Каждый из этих методов задаёт как содержимое, так и соответствующий MIME-тип. При необходимости MIME-тип можно переопределить с помощью with_mime().

Также можно использовать специализированные структуры напрямую:

Можно также вернуть массив или Vec из ResourceContents. Все они автоматически преобразуются в ReadResourceResult.

Статические ресурсы​

Выше рассматривались динамические ресурсы, однако можно также определить обработчик статического ресурса, например:

#[resource(uri = "res://static_resource")]
async fn get_res(uri: Uri) -> ResourceContents {
TextResourceContents::new(uri, "some file contents")
}

или с помощью метода add_resource():

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://​

Ресурс, URI которого начинается с ui://, — это MCP App: HTML-документ, который хост рендерит в песочнице iframe для указывающего на него инструмента. Схема зарезервирована, и именно она помечает ресурс: neva отдаёт такое чтение как text/html;profile=mcp-app и прикладывает блок безопасности _meta.ui:

app.add_ui_resource("ui://clock/app.html", "clock", "<!doctype html>…")
.with_title("Clock")
.with_prefers_border(true);

По умолчанию ресурсы ui:// не попадают в resources/list — хост находит их через метаданные инструмента, а не просмотром списка. См. MCP Apps.

Обработка list_resources​

С помощью атрибутного макроса #[resources] можно переопределить функцию, предоставляющую список ресурсов, и при необходимости реализовать пагинацию:

use neva::prelude::*;

#[resources]
async fn list_resources(_: ListResourcesRequestParams) -> Vec<Resource> {
// Читаем список ресурсов из источника
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
}

Также можно использовать метод map_resources():

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 {
// Читаем список ресурсов из источника
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;

Обновление ресурсов​

Помимо чтения ресурсов, инструменты MCP-сервера также могут добавлять, обновлять или удалять их через ctx.resources():

use neva::prelude::*;

/// Добавление нового ресурса
#[tool]
async fn add_resource(ctx: Context, uri: Uri) -> Result<(), Error> {
let resource = Resource::from(uri); // Создаём новый ресурс
ctx.resources().add(resource).await
}

/// Удаление ресурса
#[tool]
async fn remove_resource(ctx: Context, uri: Uri) -> Result<(), Error> {
ctx.resources().remove(uri).await?;
Ok(())
}

/// Обновление существующего ресурса
#[tool]
async fn update_resource(ctx: Context, uri: Uri) -> Result<(), Error> {
// Читаем и обновляем ресурс с указанным URI
// ...

ctx.resources().notify_updated(uri).await
}

Каждая из этих операций автоматически уведомляет всех клиентов, подписанных на соответствующее событие:

  • notifications/resources/list_changed — при добавлении или удалении ресурса
  • notifications/resources/updated — при обновлении ресурса

Доставка идёт в живые потоки subscriptions/listen, чей фильтр допускает уведомление, поэтому сервер должен объявить listChanged и subscribe, чтобы клиент вообще мог о них попросить:

App::new()
.with_options(|opt| opt
.with_resources(|res| res.with_list_changed().with_subscribe()));

Чтобы не делать дорогую локальную работу, которую никто не слушает, используйте ctx.resources().is_subscribed(&uri) — но не для того, чтобы решить, слать ли уведомление: он знает только про свой узел, и при шине уведомлений подписчик на другом экземпляре может ждать ровно то, что этот пропустит. Поэтому notify_updated предпроверку не делает и публикует безусловно, оставляя маршрутизацию фильтрам подписок.

Под флагом legacy-spec

Возвращается пара RPC-методов resources/subscribe / resources/unsubscribe, а вместе с ней ctx.resources().subscribe(uri) / unsubscribe(&uri). В сборке по умолчанию этих методов нет — подпиской теперь владеет клиент. См. Подписки и Легаси-спецификация.

Обучение на примерах​

Полный пример доступен здесь.

Дополнительные примеры​