fix(apps): gate the capability observer on the bridge's own initialize schema
The hand-rolled accept check was shallower than the bridge's, so frames it
rejects could still set `elicitation` here — `appInfo: {}` passed, though
`McpUiInitializeRequestSchema` requires an implementation name and version.
Any approximation drifts; the schema itself does not, and ext-apps exports it.
Capabilities are still read from the ORIGINAL frame rather than the parse
output, since that schema is precisely what strips `elicitation` — validating
with it and reading through it would defeat the module. The one check the
schema cannot make is that the frame is a request at all (a notification
carries the same method and params), so the id check stays.
Also stops treating a truthy non-object as an advertisement: the draft declares
`elicitation` as an object, so `elicitation: true` is a value it does not
define and the bridge's schema will reject it the moment it carries the key.
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
b86db5c7a2
commit
98d1fe752d
1 changed files with 83 additions and 36 deletions
|
|
@ -57,6 +57,10 @@ describe("appCapabilities (#1854)", () => {
|
|||
expect(appAdvertisesElicitation(bridge)).toBe(true);
|
||||
});
|
||||
|
||||
it("is false when the bridge advertises nothing at all", () => {
|
||||
expect(appAdvertisesElicitation(makeBridge(undefined))).toBe(false);
|
||||
});
|
||||
|
||||
it("is false for an app that advertised no elicitation", () => {
|
||||
const bridge = makeBridge({ availableDisplayModes: ["inline"] });
|
||||
const { transport } = makeTransport();
|
||||
|
|
@ -176,6 +180,40 @@ describe("appCapabilities (#1854)", () => {
|
|||
expect(appAdvertisesElicitation(bridge)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects a frame the bridge's own schema rejects", () => {
|
||||
// `appInfo: {}` is missing the implementation name/version that
|
||||
// `McpUiInitializeRequestSchema` requires, so the bridge answers an error
|
||||
// and stores nothing — this gate must not be the laxer of the two.
|
||||
const bridge = makeBridge({});
|
||||
const { transport } = makeTransport();
|
||||
observeAppCapabilities(bridge, transport);
|
||||
transport.onmessage?.({
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "ui/initialize",
|
||||
params: {
|
||||
protocolVersion: "2026-01-26",
|
||||
appInfo: {},
|
||||
appCapabilities: { elicitation: {} },
|
||||
},
|
||||
} as unknown as JSONRPCMessage);
|
||||
expect(appAdvertisesElicitation(bridge)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not treat a non-object elicitation value as an advertisement", () => {
|
||||
// The draft declares `elicitation` as an object; `true` is a value it does
|
||||
// not define, and truthiness would accept it.
|
||||
const bridge = makeBridge({});
|
||||
const { transport } = makeTransport();
|
||||
observeAppCapabilities(bridge, transport);
|
||||
transport.onmessage?.(initializeFrame({ elicitation: true }));
|
||||
expect(appAdvertisesElicitation(bridge)).toBe(false);
|
||||
transport.onmessage?.(initializeFrame({ elicitation: [] }));
|
||||
expect(appAdvertisesElicitation(bridge)).toBe(false);
|
||||
transport.onmessage?.(initializeFrame({ elicitation: {} }));
|
||||
expect(appAdvertisesElicitation(bridge)).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps bridges independent", () => {
|
||||
const a = makeBridge({});
|
||||
const b = makeBridge({});
|
||||
|
|
|
|||
Loading…
Reference in a new issue