Skip to content

Implement negotiated app-rendered form elicitations #1854

Description

@krubenok

Goal

Implement negotiated, app-rendered form elicitations in Inspector v2 so that a server can ask an MCP App to render and resolve a standard elicitation/create request. Keep the native Inspector elicitation UI as the fallback.

This should consume the MCP Apps draft/API proposed in:

Related design discussion:

This issue is intentionally server-agnostic. The fixture and examples must not depend on a private or product-specific server.

Protocol contract

App rendering is selected only when all four conditions hold:

  1. The Inspector client advertises core form elicitation support:

    {
      "elicitation": {
        "form": {}
      }
    }
  2. The Inspector client advertises app-rendered elicitation support in the existing MCP Apps extension:

    {
      "extensions": {
        "io.modelcontextprotocol/ui": {
          "mimeTypes": ["text/html;profile=mcp-app"],
          "elicitation": {}
        }
      }
    }
  3. The server advertises the corresponding MCP Apps capability:

    {
      "extensions": {
        "io.modelcontextprotocol/ui": {
          "elicitation": {}
        }
      }
    }
  4. The elicitation/create request contains a valid absolute ui:// URI:

    {
      "_meta": {
        "ui": {
          "resourceUri": "ui://example/choose-option"
        }
      }
    }

Do not introduce a second extension, a custom elicitation method, a custom result shape, or a protocol-level retry mechanism. The app returns the ordinary MCP ElicitResult, including accept, decline, and cancel.

Only form mode is app-renderable. An omitted mode is treated as form; url mode continues through its existing path.

Inspector capability advertisement

The nested MCP Apps elicitation capability must be advertised only by an Inspector client that can actually host MCP Apps and resolve requests through an app bridge. In particular, the web client may opt in when its sandbox renderer is available; CLI/TUI clients must not claim the capability merely because the shared InspectorClient knows the MCP Apps MIME type.

Preserve the existing core elicitation toggle. If form elicitation is disabled, omit both the core form capability and the nested MCP Apps elicitation capability.

Request routing and ownership

Extend the existing standard elicitation/create request handler in core/mcp/inspectorClient.ts. For every incoming request:

  1. Check the negotiated client and server capabilities.
  2. Read and validate _meta.ui.resourceUri.
  3. If eligible, create or select the renderer instance owned by that exact request.
  4. Load the requested MCP App resource and complete ui/initialize.
  5. Confirm that the app advertised appCapabilities.elicitation.
  6. Forward the original elicitation/create request unchanged through that renderer's AppBridge.
  7. Validate the returned standard ElicitResult and form content against the requested schema.
  8. Resolve the original server request with that result.

The association must be request-scoped—for example, a map keyed by request ID that owns the resource URI, renderer/bridge instance, promise, and cancellation state. Do not use a global or "currently active app" bridge. Concurrent elicitations must not cross wires.

The host-side bridge must advertise:

{
  "hostCapabilities": {
    "elicitation": {}
  }
}

Use the ext-apps helpers introduced by #733 (or their released equivalents) to test negotiated support, extract the resource URI, and issue the request through the exact bridge instance.

Renderer integration

The current AppRenderer is tool-result-oriented and creates its bridge from a Tool. Either generalize it or add a focused elicitation renderer so it can:

  • resolve a resource from an explicit absolute ui:// URI;
  • expose initialization and app-capability state;
  • send an elicitation/create request through its exact AppBridge;
  • resolve or reject a request-owned promise;
  • tear down on completion, abort, disconnect, or navigation;
  • render multiple concurrent requests without sharing mutable bridge state.

The app itself must be able to register App.onelicitation and return:

{ "action": "accept", "content": { "choice": "option-a" } }

or:

{ "action": "decline" }

or:

{ "action": "cancel" }

Native fallback

Fall back to the existing Inspector elicitation queue/UI when any of the following happens before a valid app result is obtained:

  • either peer did not negotiate app-rendered elicitation;
  • metadata is absent, malformed, non-absolute, non-ui://, or otherwise unusable;
  • resource read or rendering fails;
  • sandbox or bridge initialization fails;
  • the app does not advertise elicitation support;
  • the bridge request fails or times out;
  • the app returns an invalid result or accepted content that fails schema validation.

An explicit, valid decline or cancel is a completed elicitation and must be returned to the server without opening the native fallback UI.

Public test fixture

Add a small, generic test server/app fixture that:

  • advertises the server-side MCP Apps elicitation capability;
  • serves a self-contained MCP App resource;
  • requests a simple form choice using _meta.ui.resourceUri;
  • can return or exercise modern MRTR input_required behavior where applicable;
  • demonstrates accept, decline, and cancel;
  • has no dependency on a private application or service.

The UI can be as simple as "Choose option A or B." Its purpose is to verify the protocol and renderer lifecycle, not to be a product example.

Acceptance criteria

  • Capability serialization tests cover web opt-in, form-disabled behavior, and non-rendering clients.
  • Handler tests cover all four negotiation gates.
  • Renderer/bridge tests prove the request is delivered to the exact app instance and its standard result is returned unchanged.
  • Two simultaneous requests for different resource URIs cannot resolve through each other's bridges.
  • Native fallback is covered for absent capability, invalid metadata, resource/init failure, missing app capability, timeout, invalid result, and schema mismatch.
  • Explicit decline and cancel do not trigger fallback.
  • A browser integration test exercises the public fixture end to end.
  • Existing native elicitation and task/MRTR behavior remains passing.

Suggested implementation areas

  • core/mcp/inspectorClient.ts
  • core/mcp/apps.ts
  • clients/web/src/components/elements/AppRenderer/
  • the existing elicitation state/panel hooks
  • Inspector v2 test-server fixtures and browser tests

The implementation should consume an early or released version of @modelcontextprotocol/ext-apps containing #733 rather than creating an Inspector-local protocol variant.

Metadata

Metadata

Assignees

No one assigned

    Labels

    v2Issues and PRs for v2

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions