Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
657 changes: 452 additions & 205 deletions specification/draft/apps.mdx

Large diffs are not rendered by default.

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
53 changes: 53 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import {
CreateMessageResultWithTools,
CreateMessageResultWithToolsSchema,
EmptyResultSchema,
ElicitRequest,
ElicitRequestSchema,
ElicitResult,
Implementation,
ListResourcesRequest,
ListResourcesResult,
Expand Down Expand Up @@ -1013,6 +1016,49 @@ export class App extends ProtocolWithEvents<
);
}

/**
* Handle a form-mode elicitation that the host has bound to this app.
*
* The callback receives the unchanged standard MCP `elicitation/create`
* parameters and returns the unchanged standard MCP `ElicitResult`.
* Register this handler before {@link connect `connect`}; doing so advertises
* the app's `elicitation` capability during `ui/initialize`.
*/
private _onelicitation?: (
params: ElicitRequest["params"],
extra: RequestHandlerExtra,
) => ElicitResult | Promise<ElicitResult>;
get onelicitation() {
return this._onelicitation;
}
set onelicitation(
callback:
| ((
params: ElicitRequest["params"],
extra: RequestHandlerExtra,
) => ElicitResult | Promise<ElicitResult>)
| undefined,
) {
this.warnIfRequestHandlerReplaced(
"onelicitation",
this._onelicitation,
callback,
);
this._onelicitation = callback;
if (callback && !this._capabilities.elicitation && !this.transport) {
this.registerCapabilities({ elicitation: {} });
}
this.replaceRequestHandler(ElicitRequestSchema, (request, extra) => {
if (!this._onelicitation) {
throw new Error("No onelicitation handler set");
}
if (request.params.mode !== undefined && request.params.mode !== "form") {
throw new Error("MCP Apps only support form-mode elicitations");
}
return this._onelicitation(request.params, extra);
});
}

/**
* Convenience handler for tool call requests from the host.
*
Expand Down Expand Up @@ -1165,6 +1211,13 @@ export class App extends ProtocolWithEvents<
);
}
return;
case "elicitation/create":
if (!this._capabilities.elicitation) {
throw new Error(
`App does not support elicitation capability (required for ${method})`,
);
}
return;
case "ping":
case "ui/resource-teardown":
return;
Expand Down
55 changes: 55 additions & 0 deletions src/generated/schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions src/generated/schema.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading