MCP Apps
Клиентская половина MCP Apps: объявить, что эта сторона умеет рендерить приложение, а потом прочитать, у каких инструментов оно есть, какие из них может видеть модель и чего просит блок безопасности документа.
Включается фичей apps (входит в client-full).
[dependencies]
neva = { version = "0.7", features = ["client", "apps"] }
Трафик ui/* — рукопожатие, доставка результата инструмента, темизация — идёт
между хостом и его iframe внутри браузера. neva его не моделирует. Что она
даёт — это ту часть, которая нужна хосту от MCP-библиотеки: объявить расширение,
найти инструменты с лицом, забрать HTML и знать, какие инструменты может видеть
модель. Рендеринг — ваш.
Объявление capability
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", "./server/Cargo.toml"])
.with_apps());
client.connect().await?;
client.disconnect().await
}
with_apps()
объявляет io.modelcontextprotocol/ui с единственным типом содержимого, который
определяет спецификация, — text/html;profile=mcp-app. Сервер смотрит на это,
прежде чем предлагать инструмент с UI вместо чисто текстового.
mimeTypes обязателен по спецификации — клиент, не назвавший ни одного, не
объявил поддержку, — поэтому метод его заполняет, а не объявляет пустой объект,
как это делает серверная сторона.
Чтобы назвать другой набор, используйте
with_app_mime_types:
use neva::prelude::*;
fn main() {
let client = Client::new()
.with_options(|opt| opt.with_app_mime_types([APP_MIME_TYPE]));
let _ = client;
}
Первая редакция спецификации определяет только этот тип; остальные зарезервированы.
Объявление расширения — это обещание рендерить. Делайте его, когда процесс встраивает webview, показывающий HTML, или когда он сам является хостом, передающим документ дальше, — но не ради чтения метаданных, которое работает и без объявления.
Куда это отправляется
MCP 2026-07-28
заменил рукопожатие на discovery,
так что вешать объявление на всё соединение стало некуда. Вместо этого оно едет
в каждом запросе, в его _meta под ключом
io.modelcontextprotocol/clientCapabilities:
{
"_meta": {
"io.modelcontextprotocol/clientCapabilities": {
"elicitation": {},
"extensions": {
"io.modelcontextprotocol/ui": {
"mimeTypes": ["text/html;profile=mcp-app"]
}
}
}
}
}
От вас требуется только with_apps() — карту в каждый запрос клиент кладёт
сам. Под legacy-spec то же объявление едет на рукопожатии
initialize внутри capabilities.extensions.
Сервер читает её через
Context::supports_apps()
и может менять свой content в зависимости от того, умеете ли вы рендерить.
Заметьте, что объявление нужно только для того, чтобы сервер мог менять свой
ответ. Чтение метаданных из tools/list и resources/read — всё остальное на
этой странице — не требует никакого согласования.
Поиск инструментов с лицом
Tool::ui()
читает блок _meta.ui обратно:
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", "./server/Cargo.toml"])
.with_apps());
client.connect().await?;
let tools = client.tools().list(None).await?;
for tool in tools.tools.iter() {
// Текстовый ответ есть у каждого инструмента; лицо — не у каждого.
let Some(ui) = tool.ui() else {
println!("{}: no UI", tool.name);
continue;
};
let audience = if tool.is_model_visible() {
"model + app"
} else {
"app only"
};
println!("{}: {} -> {:?}", tool.name, audience, ui.resource_uri);
}
client.disconnect().await
}
| Метод | Отвечает на вопрос |
|---|---|
tool.ui() | Блок UiToolMeta — resource_uri и visibility — либо None для обычного инструмента |
tool.is_model_visible() | Может ли агент видеть и вызывать этот инструмент? |
tool.is_app_visible() | Может ли его вызывать iframe? |
Оба предиката дают true для инструмента вообще без метаданных MCP Apps и для
того, у которого visibility опущен: тогда действует умолчание спецификации
["model", "app"]. Только явный visibility, из которого область исключена,
делает соответствующий предикат false.
ui() намеренно снисходителен в одну сторону и строг в другую. Он принимает и
устаревший плоский ключ _meta["ui/resourceUri"] — этого спецификация ждёт
от читателя (вложенный блок имеет приоритет, если есть оба), — а некорректный
блок читается как отсутствующий, а не роняет весь tools/list. Предикаты
видимости этой снисходительности не разделяют: явный visibility, который не
удалось разобрать, запрещает, так что испорченный блок никогда не протолкнёт
инструмент для приложения в список агента.
Сервер перечисляет инструменты только для приложения в tools/list как любые
другие: метаданные и есть весь механизм. Хост НЕ ДОЛЖЕН класть в список
инструментов агента тот, для которого is_model_visible() вернул false. За вас
этого никто не сделает.
Получение документа
Это тот самый resources/read, который хост делает перед открытием iframe:
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", "./server/Cargo.toml"])
.with_apps());
client.connect().await?;
let tools = client.tools().list(None).await?;
if let Some(uri) = tools
.get("get_time")
.and_then(|tool| tool.ui())
.and_then(|ui| ui.resource_uri)
{
let result = client.resources().read(uri).await?;
for contents in result.contents.iter() {
println!(
"{} [{}] {} bytes",
contents.uri(),
contents.mime().unwrap_or("?"),
contents.text().map(str::len).unwrap_or_default()
);
// Блок, который хост превращает в CSP и атрибут `allow`.
println!(" _meta.ui: {:?}", contents.ui());
}
}
client.disconnect().await
}
Чтение ui:// всегда возвращается как text/html;profile=mcp-app. Блок
_meta.ui несёт csp, permissions, domain и prefersBorder — что означает
каждое поле, см. в
Блоке безопасности.
Отсутствующий _meta.ui или отсутствующий csp внутри него — это
ограничительное умолчание: никакого внешнего доступа. Не читайте его как «не
задано, значит можно» — это переворачивает замысел спецификации и отдаёт
недоверенному документу сеть.
ResourceContents
даёт клиенту методы доступа uri, text, blob, json, mime, title и
annotations; билдеры — серверные.
Что дальше
- MCP Apps на сервере — как отдавать инструмент и документ
- Инструменты — вызов инструментов и структурированные результаты
- Ресурсы — общий механизм
resources/read examples/apps— рабочая пара, клиент и сервер