You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Inspector integration: local branch u/kyrubeno/inspector-app-elicitations,
commit c39355e5dc8f735963f29b2f29fefa1f6135991a, based on
modelcontextprotocol/inspector.
Outlook MCP integration: the private Azure DevOps server-outlook-mcp
implementation consumes the two experimental packages and adapts the HITL
widget into an MCP App elicitation.
What this demonstrates
The server sends a complete native form elicitation and, when supported, an
absolute ui:// MCP App resource hint.
The host renders the App for Accept/Decline/Cancel and returns standard
ElicitResult values.
MRTR retries preserve the original arguments, opaque requestState, and
request-scoped capabilities.
Unsupported, malformed, or failed App flows fall back to the unchanged native
form.
Interoperability is based on canonical wire capabilities, not package identity.
The current gist files contain the runnable C# example, TypeScript host/app
guidance, and Inspector test recipe. Configure GitHub Packages with
read:packages before restoring or installing dependencies.
The published previews are a dual-shape compatibility snapshot. Hardened
releases should use the later C# and TypeScript preview versions described in
the session validation notes.
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
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
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
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
The experimental client currently advertises both the nested SEP-3118 candidate
and the temporary pre-approval gate. Keep those details inside the package
helpers so the example can move to standardized APIs without changing its host
or tool logic.
Start the generic local fixture
From the Inspector repository:
cd clients/web
npm run test-servers:build
node ../../test-servers/build/server-composable.js \
--config ../../test-servers/configs/modern-app-elicitation-http.json
The MCP endpoint is:
http://localhost:3102/mcp
In another terminal, from the repository root:
npm run build
MCP_INSPECTOR_API_TOKEN=local-token \
MCP_AUTO_OPEN_ENABLED=false \
npm run web
Open the printed Inspector URL.
Connect and invoke
Add http://localhost:3102/mcp as a Streamable HTTP server.
Open Server Settings → Options.
Set Protocol Era to Modern (2026-07-28, sessionless).
Disconnect and reconnect. The header should show MCP 2026-07-28; a modern
connection begins with server/discover, not initialize.
Open Tools, select mrtr_confirm, and execute:
{ "action": "publish demo" }
Expected behavior
The first tools/call returns input_required containing:
the complete native form schema (a required boolean confirm field)
an opaque requestState
_meta.ui.resourceUri pointing to
ui://demo/elicitation-confirm.mcp-app.html
Inspector reads that resource over the same MCP connection and opens the
App Elicitation Request modal. The App renders Accept, Decline, and
Cancel.
The selected action is returned as a standard elicitation result:
The separate extension is a temporary opt-in gate, not the proposed final SEP
shape. Remove it only through an explicit package migration after SEP approval.
Fail-closed checks
Switch the server to Legacy, reconnect, and invoke the tool: the modern
request-scoped flow must not silently activate.
Remove or corrupt _meta.ui.resourceUri: Inspector must render the unchanged
native form instead of an unrelated App.
Make the resource or App initialization fail: Inspector must surface a
diagnostic and fall back to the complete native form.
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
@krubenok/mcp-apps-elicitation-client provides experimental TypeScript client, host, and app
conventions for draft SEP-3118.
It targets @modelcontextprotocol/client@2.0.0 and supports both direct legacy elicitation requests
and Modern 2026-07-28 multi-round-trip requests (MRTR).
This is an unofficial public prototype, not an adopted MCP extension or an official SDK package.
GitHub Packages requires a token with read:packages, including for public npm packages.
@modelcontextprotocol/ext-apps@1.7.5 still peers on the v1 MCP SDK. This package uses structural
adapters so it does not add that SDK as a runtime dependency. In a v2 host, construct AppBridge
with null instead of passing the v2 Client; use the originating v2 client explicitly for
resource loading.
Capability shape
The canonical SEP-3118 candidate is the nested MCP Apps capability:
By default, withAppElicitationClientCapabilities() also emits the temporary gate required by
Krubenok.ModelContextProtocol.Extensions.Apps.Elicitation@0.2.0-preview.1:
The separate identifier is an early prototype, not part of SEP-3118. Its requires array is a
compatibility hint, not normative extension dependency semantics. For canonical-only testing:
The default remains true for preview.5 compatibility. The helper preserves unrelated
capabilities and extension settings, merges MIME and requires arrays without duplicates, and is
idempotent.
Interoperability is based only on the wire shape, never on which package constructed it. No
receive-side helper reads or requires io.modelcontextprotocol/ui-elicitation.
Client capability input
Server behavior
Nested-only canonical shape from a future main MCP Apps client
App-rendered elicitation enabled
Dual canonical + prototype shape from this preview.5 package
App-rendered elicitation enabled
Canonical shape plus a malformed or unknown prototype gate
App-rendered elicitation enabled; gate ignored
Prototype gate only
Native form fallback
Base MCP Apps MIME type without nested elicitation
Native form fallback
Missing core elicitation.form or MCP App MIME type
Native form fallback
Malformed MCP Apps settings or malformed nested elicitation
Native form fallback
This means the current separate client package works with a future server or main Apps extension
that ignores the prototype gate, and a future nested-only main Apps client works with the separate
server package. App/host bridge negotiation follows the same rule: only first-class
appCapabilities.elicitation and hostCapabilities.elicitation count; package markers do not.
Register the elicitation handler before connect(). Modern clients automatically place the same
capabilities in the request-scoped
_meta['io.modelcontextprotocol/clientCapabilities'] envelope on every request.
import{AppBridge}from'@modelcontextprotocol/ext-apps/app-bridge';import{Client,StreamableHTTPClientTransport,typeElicitRequest,typeElicitResult}from'@modelcontextprotocol/client';import{MCP_APP_HTML_MIME_TYPE,adaptExtAppsBridge,preserveAppElicitationCapability,registerAppElicitationClient,waitForAppInitialized,withAppElicitationBridgeCapabilities,withAppElicitationClientCapabilities,typeAppElicitationBridgeSession}from'@krubenok/mcp-apps-elicitation-client';declarefunctionmountMcpApp(options: {html: string;resourceUri: string;request: ElicitRequest;signal: AbortSignal}): Promise<{transport: Parameters<AppBridge['connect']>[0];dispose(): void;}>;declarefunctionrenderNativeForm(params: ElicitRequest['params'],signal: AbortSignal): Promise<ElicitResult>;constendpoint=newURL('https://example.com/mcp');constclient=newClient({name: 'example-app-host',version: '1.0.0'},{versionNegotiation: {mode: {pin: '2026-07-28'}},capabilities: withAppElicitationClientCapabilities()});registerAppElicitationClient(client,{appHost: {asyncopen({client: originatingClient, request, resourceUri, signal }){constresource=awaitoriginatingClient.readResource({uri: resourceUri},{ signal });constappHtml=resource.contents.find(content=>'text'incontent&&content.mimeType?.toLowerCase()===MCP_APP_HTML_MIME_TYPE);if(appHtml===undefined||!('text'inappHtml)){thrownewTypeError(`Resource ${resourceUri} did not contain MCP App HTML`);}// This seam is host-specific: create the sandboxed iframe, load appHtml.text,// and return the ext-apps transport plus a disposer for that exact instance.constmounted=awaitmountMcpApp({html: appHtml.text, resourceUri, request, signal });// Pass null: ext-apps 1.7.5 peers on the v1 SDK. The v2 client above remains// authoritative for this server connection and resource binding.constbridge=newAppBridge(null,{name: 'example-app-host',version: '1.0.0'},withAppElicitationBridgeCapabilities({logging: {}}));// ext-apps 1.7.5 strips unknown appCapabilities members during ui/initialize.preserveAppElicitationCapability(bridge);try{constconnecting=bridge.connect(mounted.transport);constready=waitForAppInitialized(bridge,signal);awaitconnecting;awaitready;}catch(error){try{awaitbridge.close();}finally{mounted.dispose();}throwerror;}constadapted=adaptExtAppsBridge(bridge);return{
...adapted,asyncclose(){try{awaitadapted.close();}finally{mounted.dispose();}}}satisfiesAppElicitationBridgeSession;}},// The unchanged complete requestedSchema is always available here.renderNative: (request,signal)=>renderNativeForm(request.params,signal),// App failures are diagnostic; the package then invokes renderNative.onAppError: (error,request)=>{console.warn('App-rendered elicitation failed; using native form',{
error,method: request.method});}});awaitclient.connect(newStreamableHTTPClientTransport(endpoint));constresult=awaitclient.callTool({name: 'operation-requiring-input',arguments: {}});
mountMcpApp() and renderNativeForm() are the only host UI seams. The returned bridge session
must be bound to the originating Client, elicitation request, resourceUri, and app instance.
Never return whichever bridge happens to be active.
The handler uses the app only for form mode with a valid absolute ui:// hint. It falls back to the
native renderer when resource loading, sandbox creation, ui/initialize, readiness, capability
negotiation, bridge dispatch, or app result validation fails. URL mode always stays on the native
path.
Copy-ready app setup
The app declares its bridge capability, preserves the host's matching capability through the
ext-apps 1.7.5 result schema, and registers the standard request before connecting:
import{App}from'@modelcontextprotocol/ext-apps';importtype{ElicitRequestFormParams,ElicitResult}from'@modelcontextprotocol/client';import{preserveHostAppElicitationCapability,registerAppElicitationApp,supportsAppElicitationBridgeCapability,withAppElicitationBridgeCapabilities}from'@krubenok/mcp-apps-elicitation-client';declarefunctionrenderAppForm(params: ElicitRequestFormParams,signal: AbortSignal): Promise<ElicitResult>;constapp=newApp({name: 'example-elicitation-app',version: '1.0.0'},withAppElicitationBridgeCapabilities());preserveHostAppElicitationCapability(app);registerAppElicitationApp(app,async(request,signal)=>{if(request.params.mode!=='form')return{action: 'decline'};returnrenderAppForm(request.params,signal);});awaitapp.connect();if(!supportsAppElicitationBridgeCapability(app.getHostCapabilities())){thrownewError('The host did not negotiate app-rendered elicitation');}
registerAppElicitationApp() temporarily bypasses only the ext-apps 1.7.5 registration guard for
elicitation/create, delegates all other checks, and immediately restores the original guard.
Accepted content is validated against the original requestedSchema before it crosses the bridge.
Readiness and ui/initialize
Call preserveAppElicitationCapability(bridge) immediately after constructing AppBridge and
before bridge.connect(). It replaces only the constructor-registered ui/initialize handler
through ext-apps 1.7.5's private _baseReplaceRequestHandler and _oninitialize seams. The
replacement preserves appCapabilities.elicitation, future nested fields, and the original
JSON-RPC envelope stripping behavior.
Call preserveHostAppElicitationCapability(app) after constructing App and before
app.connect(). It replaces the initialize result schema only for ui/initialize, preserving
hostCapabilities.elicitation and future nested fields for getHostCapabilities().
After bridge.connect() starts, await waitForAppInitialized(bridge, signal). It attaches the
listener before checking ext-apps' actual _initializedReceived flag, handles already-ready and
slow apps, detaches on completion, and rejects on cancellation. onAppInitialized() provides the
same race-safe behavior as a callback API. Do not use getAppCapabilities() as a readiness signal:
ext-apps records capabilities during ui/initialize, before ui/notifications/initialized.
These helpers intentionally target ext-apps 1.7.5 structural seams and throw TypeError when a
required seam is unavailable. Remove them when the minimum ext-apps release natively preserves and
types SEP-3118 capabilities and accepts elicitation/create.
MRTR retry behavior
On Modern 2026-07-28, the server returns the standard app-enhanced elicitation/create entry in
an input_required result. The v2 client:
dispatches it to the same handler registered by registerAppElicitationClient();
collects the standard ElicitResult;
retries the original request with the bare result under inputResponses;
echoes opaque requestState byte-for-byte; and
repeats until the server returns complete or inputRequired.maxRounds is reached.
No app-specific retry API is needed. The app must return its ElicitResult to the host and must not
retry the server operation itself.
Public API
API
Purpose
withAppElicitationClientCapabilities()
Adds core form, canonical MCP Apps elicitation, MIME type, and the default-on prototype gate.
withAppElicitationBridgeCapabilities()
Adds elicitation: {} to app or host bridge capabilities without replacing other settings.
supportsAppElicitationBridgeCapability()
Checks the negotiated app or host bridge member.
registerAppElicitationClient()
Registers legacy and MRTR client handling with native fallback.
registerAppElicitationApp()
Registers the standard app-side elicitation/create handler.
preserveAppElicitationCapability()
Preserves app capabilities through ext-apps 1.7.5 ui/initialize.
preserveHostAppElicitationCapability()
Preserves host capabilities through ext-apps 1.7.5 ui/initialize.
waitForAppInitialized() / onAppInitialized()
Race-safe app readiness APIs.
adaptExtAppsBridge()
Adapts the ext-apps bridge to a request-bound session.
validateAppElicitationResult()
Validates the core result and accepted content schema.
getAppElicitationResourceUri()
Reads and validates an absolute ui:// form hint.
Migration after SEP-3118 standardization
Stop emitting the temporary gate by setting includePrototypeGate: false; a later package
revision can flip the single default centrally. No server, host, app, or receive-side logic
changes are required.
Upgrade to an ext-apps release whose app and host capability types include elicitation and
whose schemas preserve it.
Remove the two preserve*Capability() calls and the app registration guard workaround when
ext-apps handles the standardized request natively.
Keep the core client handler, explicit resource binding, native fallback, accepted-content
validation, and standard MRTR retry behavior.
Inspector testing
In the Inspector fork, inject withAppElicitationClientCapabilities() into the v2 client before
connection and register the handler before the first tool call. Test one app-capable tool returning
input_required, then verify the retry contains standard inputResponses and the unchanged
requestState. Also test a missing app capability or failed resource load and confirm the same
complete schema renders through Inspector's native form path.
The cross-SDK example at
examples/apps-elicitation-interop runs this
TypeScript client against the C# reference server and covers capability discovery, MRTR
auto-fulfillment, resource loading, schema validation, opaque state echo, and the final retry.