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

Основы

Давайте воспользуемся MCP-клиентом Neva для подключения к вашим MCP-серверам (и другим тоже).

Создание приложения​

Создайте новое бинарное приложение:

cargo new neva-mcp-client
cd neva-mcp-client

Добавьте следующие зависимости в ваш Cargo.toml:

[dependencies]
neva = { version = "...", features = "client-full" }
tokio = { version = "1", features = ["full"] }

Вызов инструмента​

Для начала вызовем инструмент, созданный в разделе основы сервера.

use neva::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Error> {
let mut client = Client::new()
.with_options(|opt| opt
.with_stdio(
"cargo",
["run", "--manifest-path", "./neva-mcp-server/Cargo.toml"]));

client.connect().await?;

let args = ("name", "John");
let result = client.tools().call("hello", args).await?;

println!("{:?}", result.content);

client.disconnect().await
}

Здесь мы настраиваем MCP-клиент для подключения к серверу через stdio. После подключения можно вызывать инструменты, получать запросы или читать ресурсы вплоть до отключения (или удаления клиента).

Примитивы сервера сгруппированы так же, как MCP называет свои методы, — по одному пространству имён на префикс:

Пространство имёнМетоды
client.tools()tools/list, tools/call
client.resources()resources/list, resources/templates/list, resources/read
client.prompts()prompts/list, prompts/get
client.tasks()tasks/get, tasks/update, tasks/cancel — см. Задачи

Каждое пространство имён — Copy-представление, заимствованное у клиента, поэтому их можно держать сколько угодно одновременно: let (tools, prompts) = (client.tools(), client.prompts());.

disconnect() — локальная операция

Он останавливает транспорт и ничего не отправляет по сети. В протоколе нет прощального сообщения: notifications/cancelled без параметров, который neva отправляла раньше, не проходит проверку по схеме самой спецификации, а о том, что клиент ушёл, сервер и так узнаёт по закрытому соединению.

Когда connect не удался​

connect() запускает транспорт, и транспорт, который не запустился, сообщает об этом сразу: команда stdio, которую не удалось запустить, отвергнутая конфигурация OAuth или TLS, неудачная привязка к порту. Это больше не всплывает позже как истёкший таймаут запроса, а клиенту, которому вовсе не задали транспорт, об этом говорят на connect, а не на первом вызове.

Неудача на этапе подъёма оставляет клиент целым: настроенный транспорт никуда не делся, поэтому тем же клиентом можно попробовать ещё раз — или направить его в другое место.

use neva::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Error> {
let mut client = Client::new()
.with_options(|opt| opt.with_stdio("weather-mcp", ["--stdio"]));

if let Err(err) = client.connect().await {
eprintln!("установленный сервер не запустился: {err}");

// Тот же клиент, но теперь нацеленный на локальную сборку.
client = client.with_options(|opt| opt
.with_stdio("cargo", ["run", "-p", "weather-mcp"]));
client.connect().await?;
}

let tools = client.tools().list(None).await?;
println!("{} инструментов", tools.tools.len());

client.disconnect().await
}
Что можно повторить, а что нет

Всё, что транспорт отверг целиком: ничего не израсходовано, поэтому следующая попытка — настоящая попытка. Когда транспорт уже запущен, более поздний сбой (например, сервер отвечает на discovery ошибкой) повтором не отменяется: нужен новый Client, потому что stdio-серверу в любом случае нужен новый дочерний процесс.

Получение промпта​

Далее получим промпт, чтобы увидеть, как они работают на стороне клиента.

let args = ("lang", "Rust");
let prompt = client.prompts().get("hello_world_code", args).await?;

Чтение ресурса​

Затем прочитаем ресурс, объявленный здесь.

let resource = client.resources().read("res://resource-1").await?;

Список инструментов, промптов и ресурсов​

Наконец, вот как динамически изучить все доступные инструменты, промпты и ресурсы.

// Все инструменты, со всех страниц
let tools = client.tools().list_all().await?;

// Все ресурсы
let resources = client.resources().list_all().await?;

// Первая страница шаблонов ресурсов
let templates = client.resources().templates(None).await?;

// Все промпты
let prompts = client.prompts().list_all().await?;

list_all() обходит список страница за страницей и возвращает элементы вместе. Если сервер всё ещё отдаёт страницы после 64-й, это ошибка, а не частичный список: обрезанный список выглядел бы полным.

Пагинация​

Большие списки по умолчанию возвращаются постранично по 10 элементов. Чтобы обходить их самостоятельно, list(cursor) запрашивает одну страницу: None — первую, а next_cursor предыдущей страницы — следующую:

// Первые 10
let resources = client.resources().list(None).await?;

// Следующие 10
let resources = client.resources().list(resources.next_cursor).await?;

// Ещё 10
let resources = client.resources().list(resources.next_cursor).await?;

Списки на стороне сервера упорядочены детерминированно по имени, поэтому постраничный обход больше не может пропустить или продублировать запись. См. Порядок в списке.

Совместное использование клиента​

Все методы запросов принимают &self, поэтому подключённый клиент можно разделить между задачами — положите его в Arc и клонируйте дескриптор:

use std::sync::Arc;
use neva::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Error> {
let mut client = Client::new()
.with_options(|opt| opt.with_default_http());
client.connect().await?;

let client = Arc::new(client);
let calls = ["London", "Paris", "Tokyo"].map(|city| {
let client = client.clone();
tokio::spawn(async move {
client.tools().call("get_weather", ("city", city)).await
})
});

for call in calls {
println!("{:?}", call.await);
}
Ok(())
}

Настройка по-прежнему требует &mut self: connect, обработчики map_* и on_* и корневые каталоги настраиваются до того, как клиент станет общим.

Таймауты и отмена​

Запрос, которого клиент перестал ждать, отменяется — когда он превышает таймаут (with_timeout, по умолчанию 10 секунд) и когда его future удаляется, например tokio::time::timeout или проигравшей веткой select!:

use std::time::Duration;

// Удалён через секунду: сервер получит указание прекратить работу над ним.
let result = tokio::time::timeout(
Duration::from_secs(1),
client.tools().call("slow_report", ()),
).await;

Как об этом узнаёт сервер, зависит от транспорта. Через Streamable HTTP в MCP 2026-07-28 клиент закрывает поток ответа запроса — там это и есть отмена, и notifications/cancelled не отправляется. Через stdio и легаси-узлу клиент отправляет notifications/cancelled. В любом случае слот ожидания запроса освобождается сразу, а запоздавший ответ на него отбрасывается. Запросы брошенного пакета тоже отменяются; легаси-initialize, который спецификация отменять не разрешает, — никогда.

Кэширование​

Каждый результат-список — а также server/discover и resources/read — несёт ttlMs и cacheScope. В MCP 2026-07-28 это обязательные члены, а не опциональные подсказки, поэтому клиент всегда знает, как долго результат остаётся актуальным и кому его можно показывать:

cacheScopeСмысл
privateКэшируется только для этого клиента (значение по умолчанию)
publicМожет использоваться разными клиентами

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

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