Skip to content

MCP Apps extension (#87) - #124

Open
RomanEmreis wants to merge 8 commits into
mainfrom
feature/mcp-apps
Open

MCP Apps extension (#87)#124
RomanEmreis wants to merge 8 commits into
mainfrom
feature/mcp-apps

Conversation

@RomanEmreis

@RomanEmreis RomanEmreis commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #87. New apps feature; additive, no new dependencies, nothing changes in a build that leaves it off.

SEP-1865 has been stable since 2026-01-26. Its two halves are not equally an SDK's problem: the data plane (_meta.ui on tools and on ui:// resources) is ordinary tools/list and resources/read traffic, while everything named ui/* is JSON-RPC over postMessage between a host and an iframe. Only the first is implemented. The second is also the half still moving -- the draft adds ui/download-file, sampling/createMessage from a View and bidirectional tools/list, none of which exist in the stable text -- and nothing here would exercise it, so the types would drift against prose rather than a peer. Types live in types/apps.rs (not a flat module) so types/apps/ui.rs remains a non-move if that changes.

Server: McpOptions::with_apps(), App::add_ui_resource / map_ui_resource, Tool::with_ui / with_visibility, AppsExtension. Client: McpOptions::with_apps() / with_app_mime_types(..), plus Tool::ui(), Tool::is_model_visible(), ResourceContents::ui(). Macros: #[tool(ui = ..., visibility = [...])] and #[resource(ui_meta = ...)].

Judgment calls

ui:// resources are materialized at into_runtime(), not at registration. add_ui_resource returns &mut UiResource so the security block can be chained on; a handler captured eagerly would snapshot the state before that chain ran and silently serve a resource without its CSP. It also makes the listing switch order-independent, since with_extension(..) and add_ui_resource(..) may appear either way round in a builder chain. Both pinned by tests.

It registers a route, not a resource template. A ui:// URI is a literal, and listing it in resources/templates/list would surface in the catalogue exactly what the listing switch was asked to keep out of it. The route key is therefore the URI rather than the resource name -- keyed by name, read_resource would read a same-named ordinary template's roles and permissions off it by accident (a_ui_route_never_inherits_a_same_named_template).

ui:// resources stay out of resources/list by default, with AppsExtension::with_listed_resources() to opt in. The spec permits omitting them, discovery runs through the tool's _meta.ui.resourceUri, and a UI template is not something a user browses.

The server advertises {}. The spec defines settings for the client direction only, and io.modelcontextprotocol/ui is a reserved identifier, so a guessed server-side mimeTypes could be contradicted later. Forward compatibility is in AppsExtension's private state instead.

Server-side apps is gated on not(legacy-spec), matching the existing Extension machinery: a legacy initialize has nowhere to put a server's capability map. The _meta builders on the types are not gated -- metadata is metadata.

#[resource(ui_meta = "...")] is one JSON blob, not four flat params. #[resource] already has a permissions param and it means OAuth permissions. The blob also matches how annotations and input_schema already work in this macro.

The macros check keys at compile time. _meta is an open map, so a prefers_border in snake case serializes happily and is ignored by every host -- a security block that silently does nothing. Along with a bad scheme, an unknown visibility scope, a wrong MIME type and ui_meta on a non-app resource, that is a compile_error!. An apps attribute without the feature names the feature rather than failing as a missing method.

Known limits

Both tracked, neither blocking: MCP 2026-07-28 has no handshake, so a client advertises on initialize only (#122 -- the per-request channel needs a Copy-preserving wrapper that server-side reads need too, so both directions are one change), and add_ui_resource carries no role or permission requirement (#123).

Type

  • Bug fix
  • Feature
  • Enhancement
  • Performance
  • Documentation
  • Refactor
  • Security
  • Breaking change

Checklist

  • I added/updated tests where it makes sense
  • I updated docs/examples if needed
  • This change is backwards-compatible (or clearly marked as breaking)
  • I ran formatting/lints locally (if applicable)

Both profiles: 1382 tests on the default one, 996 under legacy-spec; neva_macros 18 with apps and 10 without, including the feature gate itself. Clippy -D warnings, cargo fmt --check and cargo doc clean on both, on both macro configurations and on the example.

Notes for reviewers

Two places worth the most attention:

  • McpOptions::register_ui_resources (app/options.rs) -- the deferred materialization above. It runs before the collections freeze, and correctness of both the security block and the listing switch hinges on it.
  • ClientCapabilities::extensions is no longer gated on the protocol generation (types/capabilities.rs), the only wire-surface change here. Its counterpart on ServerCapabilities stays 2026-07-28-only; a legacy initialize cannot carry a server's capability map but carries the client's fine, which is the channel every host shipping MCP Apps today negotiates over. Additive -- omitted when unset.

examples/apps is a runnable server and client; the client prints each tool's audience and resource URI and fetches the document behind one of them.

@RomanEmreis RomanEmreis self-assigned this Aug 30, 2026
@RomanEmreis RomanEmreis changed the title Feature/mcp apps MCP Apps extension (#87) Aug 30, 2026
@RomanEmreis

Copy link
Copy Markdown
Owner Author

@codex review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP Apps extension

1 participant