MCP Apps extension (#87) - #124
Open
RomanEmreis wants to merge 8 commits into
Open
Conversation
Owner
Author
|
@codex review |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #87. New
appsfeature; 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.uion tools and onui://resources) is ordinarytools/listandresources/readtraffic, while everything namedui/*is JSON-RPC overpostMessagebetween a host and an iframe. Only the first is implemented. The second is also the half still moving -- the draft addsui/download-file,sampling/createMessagefrom a View and bidirectionaltools/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 intypes/apps.rs(not a flat module) sotypes/apps/ui.rsremains 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(..), plusTool::ui(),Tool::is_model_visible(),ResourceContents::ui(). Macros:#[tool(ui = ..., visibility = [...])]and#[resource(ui_meta = ...)].Judgment calls
ui://resources are materialized atinto_runtime(), not at registration.add_ui_resourcereturns&mut UiResourceso 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, sincewith_extension(..)andadd_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 inresources/templates/listwould 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_resourcewould 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 ofresources/listby default, withAppsExtension::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, andio.modelcontextprotocol/uiis a reserved identifier, so a guessed server-sidemimeTypescould be contradicted later. Forward compatibility is inAppsExtension's private state instead.Server-side
appsis gated onnot(legacy-spec), matching the existingExtensionmachinery: a legacyinitializehas nowhere to put a server's capability map. The_metabuilders on the types are not gated -- metadata is metadata.#[resource(ui_meta = "...")]is one JSON blob, not four flat params.#[resource]already has apermissionsparam and it means OAuth permissions. The blob also matches howannotationsandinput_schemaalready work in this macro.The macros check keys at compile time.
_metais an open map, so aprefers_borderin 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 andui_metaon a non-app resource, that is acompile_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
initializeonly (#122 -- the per-request channel needs aCopy-preserving wrapper that server-side reads need too, so both directions are one change), andadd_ui_resourcecarries no role or permission requirement (#123).Type
Checklist
Both profiles: 1382 tests on the default one, 996 under
legacy-spec;neva_macros18 withappsand 10 without, including the feature gate itself. Clippy-D warnings,cargo fmt --checkandcargo docclean 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::extensionsis no longer gated on the protocol generation (types/capabilities.rs), the only wire-surface change here. Its counterpart onServerCapabilitiesstays 2026-07-28-only; a legacyinitializecannot 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/appsis a runnable server and client; the client prints each tool's audience and resource URI and fetches the document behind one of them.