Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 199 additions & 2 deletions specification/draft/apps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2179,6 +2179,8 @@ See [ext-apps#35](https://github.com/modelcontextprotocol/ext-apps/issues/35) fo
### Client\<\>Server Capability Negotiation

Clients and servers negotiate MCP Apps support through the standard MCP extensions capability mechanism (defined in SEP-1724).
Features of MCP Apps are settings of the existing `io.modelcontextprotocol/ui`
extension. They do not introduce additional extension identifiers.

#### Client (Host) Capabilities

Expand All @@ -2188,11 +2190,15 @@ Clients advertise MCP Apps support in the initialize request using the extension
{
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"protocolVersion": "2025-11-25",
"capabilities": {
"elicitation": {
"form": {}
},
"extensions": {
"io.modelcontextprotocol/ui": {
"mimeTypes": ["text/html;profile=mcp-app"]
"mimeTypes": ["text/html;profile=mcp-app"],
"elicitation": {}
}
}
},
Expand All @@ -2207,12 +2213,51 @@ Clients advertise MCP Apps support in the initialize request using the extension
**Extension Settings:**

- `mimeTypes`: Array of supported content types (REQUIRED, e.g., `["text/html;profile=mcp-app"]`)
- `elicitation`: Client can use an MCP App to render and resolve form-mode
`elicitation/create` requests that identify a UI resource

Future versions may add additional settings:

- `features`: Specific feature support (e.g., `["streaming", "persistence"]`)
- `sandboxPolicies`: Supported sandbox attribute configurations

#### Server Capabilities

A server that may attach an MCP App to a form-mode elicitation advertises the
same setting in its initialize result:

```json
{
"result": {
"protocolVersion": "2025-11-25",
"capabilities": {
"extensions": {
"io.modelcontextprotocol/ui": {
"elicitation": {}
}
}
},
"serverInfo": {
"name": "example-server",
"version": "1.0.0"
}
}
}
```

App-rendered elicitation is negotiated only when all of the following are
true:

1. The client advertises the core `elicitation.form` capability.
2. The client advertises `text/html;profile=mcp-app` in
`io.modelcontextprotocol/ui.mimeTypes`.
3. The client advertises `io.modelcontextprotocol/ui.elicitation`.
4. The server advertises `io.modelcontextprotocol/ui.elicitation`.

A MIME type match alone does not negotiate app-rendered elicitation. This
explicit two-sided setting lets either peer use MCP Apps without also opting
in to app-rendered elicitation.

#### Server Behavior

Servers SHOULD check client capabilities before registering UI-enabled tools. The SDK provides the `getUiCapability` helper for this:
Expand Down Expand Up @@ -2246,6 +2291,158 @@ if (uiCap?.mimeTypes?.includes(RESOURCE_MIME_TYPE)) {
- Tools MUST return meaningful content array even when UI is available
- Servers MAY register different tool variants based on host capabilities

### App-Rendered Elicitation

When app-rendered elicitation is negotiated, a server can associate an MCP App
resource with a standard form-mode `elicitation/create` request. This feature
uses the existing MCP Apps extension, the standard MCP elicitation request and
result, and the existing View↔Host JSON-RPC channel. It defines no new method,
result, or extension identifier.

#### Associating an App with an Elicitation

The server attaches the app resource URI at `_meta.ui.resourceUri`:

```json
{
"jsonrpc": "2.0",
"id": 12,
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Choose a delivery window",
"requestedSchema": {
"type": "object",
"properties": {
"window": {
"type": "string",
"oneOf": [
{ "const": "morning", "title": "Morning" },
{ "const": "afternoon", "title": "Afternoon" }
]
}
},
"required": ["window"]
},
"_meta": {
"ui": {
"resourceUri": "ui://delivery/choose-window.html"
}
}
}
}
```

`resourceUri` MUST be an absolute `ui://` URI. It is resolved against the same
MCP server connection that sent the elicitation. A server MUST NOT attach this
metadata unless app-rendered elicitation was negotiated.

App rendering is defined only for form mode. A client MUST NOT route URL-mode
elicitations to an MCP App under this setting.

#### Host↔App Capability Negotiation

The host and the selected app independently confirm that their View↔Host
channel supports elicitation:

```json
{
"method": "ui/initialize",
"params": {
"protocolVersion": "2025-11-25",
"appInfo": {
"name": "delivery-window",
"version": "1.0.0"
},
"appCapabilities": {
"elicitation": {}
}
}
}
```

```json
{
"result": {
"protocolVersion": "2025-11-25",
"hostInfo": {
"name": "example-host",
"version": "1.0.0"
},
"hostCapabilities": {
"elicitation": {}
},
"hostContext": {}
}
}
```

An app MUST advertise `appCapabilities.elicitation` before it receives an
elicitation. A host MUST advertise `hostCapabilities.elicitation` before it
forwards one. If either setting is absent, the host MUST use its native
form-elicitation UI instead.

#### Resolution Flow

For each app-rendered elicitation, the client:

1. Receives the standard form-mode `elicitation/create` request.
2. Confirms that app-rendered elicitation was negotiated and validates
`_meta.ui.resourceUri`.
3. Reads and validates the MCP App resource using the same server connection.
4. Creates or selects an app instance that is bound to this exact request and
resource URI, then completes the `ui/initialize` handshake.
5. If the app and host advertised their View↔Host `elicitation` settings,
forwards the unchanged `elicitation/create` request to that app instance.
6. Validates the app's response as a standard MCP `ElicitResult` and returns it
unchanged to the server.

For example, an app resolves the request by returning the standard result:

```json
{
"jsonrpc": "2.0",
"id": 12,
"result": {
"action": "accept",
"content": {
"window": "morning"
}
}
}
```

The host remains the MCP client and policy enforcement point. The app does not
connect to the server directly and does not introduce an app-specific result
or callback method.

The host MUST bind forwarding to the originating request, resource URI, and app
instance. It MUST NOT forward to a global or "currently active" app bridge.
This requirement also applies when multiple elicitations are in flight.
Cancellation and timeout signals SHOULD be propagated across the bound
View↔Host request.

#### Validation, Fallback, and Retry

The host MUST validate an accepted app result against the standard
`ElicitResult` shape and the request's `requestedSchema` before returning it to
the server.

If the app resource is missing or invalid, cannot be loaded, fails to
initialize, does not advertise `elicitation`, cannot complete the bridge
request, or returns an invalid result, the host SHOULD fall back to its native
form-elicitation UI. The original message and requested schema MUST be
preserved.

An app result with `action: "decline"` or `action: "cancel"` is a successful
resolution and MUST be returned to the server. It MUST NOT trigger native
fallback.

Subsequent validation and retry use the core MCP elicitation flow. MCP Apps add
no retry method. When the core protocol associates a retry with the same
request, the host SHOULD preserve the app instance and its local state when
policy and lifecycle permit.

#### App (Guest UI) Capabilities

Apps advertise their capabilities in the `ui/initialize` request to the host. When an app supports tool registration, it includes the `tools` capability:
Expand Down
55 changes: 55 additions & 0 deletions src/app-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,61 @@ describe("App <-> AppBridge integration", () => {
});
});

describe("app-rendered elicitation", () => {
it("forwards the standard request to the bound app and returns its standard result", async () => {
const elicitationHostCapabilities: McpUiHostCapabilities = {
...testHostCapabilities,
elicitation: {},
};
bridge = new AppBridge(
createMockClient() as Client,
testHostInfo,
elicitationHostCapabilities,
);
app.onelicitation = async (params) => ({
action: "accept",
content: { choice: params.message },
});

await bridge.connect(bridgeTransport);
await app.connect(appTransport);

expect(bridge.getAppCapabilities()?.elicitation).toEqual({});
await expect(
bridge.requestElicitation({
message: "Choose an option",
requestedSchema: {
type: "object",
properties: {
choice: { type: "string" },
},
required: ["choice"],
},
}),
).resolves.toEqual({
action: "accept",
content: { choice: "Choose an option" },
});
});

it("fails closed when the app did not advertise elicitation", async () => {
bridge = new AppBridge(createMockClient() as Client, testHostInfo, {
...testHostCapabilities,
elicitation: {},
});

await bridge.connect(bridgeTransport);
await app.connect(appTransport);

expect(() =>
bridge.requestElicitation({
message: "Choose an option",
requestedSchema: { type: "object", properties: {} },
}),
).toThrow("App does not support elicitation");
});
});

describe("Host -> App notifications", () => {
beforeEach(async () => {
await bridge.connect(bridgeTransport);
Expand Down
42 changes: 41 additions & 1 deletion src/app-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import {
CreateMessageResult,
CreateMessageResultWithTools,
EmptyResult,
ElicitRequest,
ElicitResult,
ElicitResultSchema,
Implementation,
ListPromptsRequest,
ListPromptsRequestSchema,
Expand Down Expand Up @@ -443,6 +446,36 @@ export class AppBridge extends ProtocolWithEvents<
return this._appCapabilities;
}

/**
* Forward a form-mode MCP elicitation to this exact app instance.
*
* Hosts should call this only from the core MCP client's handler for the
* originating `elicitation/create` request. The returned value is the
* standard MCP `ElicitResult` and can be returned directly to the server.
*/
requestElicitation(
params: ElicitRequest["params"],
options?: RequestOptions,
): Promise<ElicitResult> {
if (!this._initializedReceived) {
throw new Error("App has not completed ui/initialize");
}
if (!this._capabilities.elicitation) {
throw new Error("Host does not support app-rendered elicitation");
}
if (!this._appCapabilities?.elicitation) {
throw new Error("App does not support elicitation");
}
if (params.mode !== undefined && params.mode !== "form") {
throw new Error("MCP Apps only support form-mode elicitations");
}
return this.request(
{ method: "elicitation/create", params },
ElicitResultSchema,
options,
);
}

/**
* Get the view's implementation info discovered during initialization.
*
Expand Down Expand Up @@ -1408,7 +1441,14 @@ export class AppBridge extends ProtocolWithEvents<
* @internal
*/
assertCapabilityForMethod(method: AppRequest["method"]): void {
// TODO
if (
method === "elicitation/create" &&
!this._appCapabilities?.elicitation
) {
throw new Error(
`App does not support elicitation capability (required for ${method})`,
);
}
}

/**
Expand Down
Loading