cortena-ui 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/README.md +90 -13
- package/dist/agent-chat/mcp-app/bridge.d.ts +194 -0
- package/dist/agent-chat/mcp-app/bridge.js +327 -0
- package/dist/agent-chat/mcp-app/bridge.js.map +1 -0
- package/dist/agent-chat/mcp-app/frame.d.ts +64 -0
- package/dist/agent-chat/mcp-app/frame.js +195 -0
- package/dist/agent-chat/mcp-app/frame.js.map +1 -0
- package/dist/agent-chat/mcp-app/index.d.ts +8 -0
- package/dist/agent-chat/mcp-app/render-tool-call.d.ts +47 -0
- package/dist/agent-chat/mcp-app/render-tool-call.js +77 -0
- package/dist/agent-chat/mcp-app/render-tool-call.js.map +1 -0
- package/dist/agent-chat/mcp-app/resource.d.ts +148 -0
- package/dist/agent-chat/mcp-app/resource.js +172 -0
- package/dist/agent-chat/mcp-app/resource.js.map +1 -0
- package/dist/agent-chat/mcp-app/sandbox.d.ts +90 -0
- package/dist/agent-chat/mcp-app/sandbox.js +177 -0
- package/dist/agent-chat/mcp-app/sandbox.js.map +1 -0
- package/dist/agent-chat/mcp-app/theme.d.ts +33 -0
- package/dist/agent-chat/mcp-app/theme.js +157 -0
- package/dist/agent-chat/mcp-app/theme.js.map +1 -0
- package/dist/agent-chat/session.d.ts +40 -0
- package/dist/agent-chat/session.js +264 -6
- package/dist/agent-chat/session.js.map +1 -1
- package/dist/agent-chat/store.d.ts +46 -0
- package/dist/agent-chat/store.js +176 -13
- package/dist/agent-chat/store.js.map +1 -1
- package/dist/agent-chat.d.ts +10 -3
- package/dist/agent-chat.js +9 -3
- package/dist/components/agent-chat.d.ts +39 -2
- package/dist/components/agent-chat.js +212 -24
- package/dist/components/agent-chat.js.map +1 -1
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/chip.d.ts +1 -1
- package/package.json +1 -1
- package/src/agent-chat/mcp-app/bridge.ts +455 -0
- package/src/agent-chat/mcp-app/frame.tsx +332 -0
- package/src/agent-chat/mcp-app/index.ts +56 -0
- package/src/agent-chat/mcp-app/render-tool-call.tsx +175 -0
- package/src/agent-chat/mcp-app/resource.ts +273 -0
- package/src/agent-chat/mcp-app/sandbox.ts +244 -0
- package/src/agent-chat/mcp-app/theme.ts +184 -0
- package/src/agent-chat/session.ts +341 -5
- package/src/agent-chat/store.ts +279 -9
- package/src/components/agent-chat.tsx +388 -70
- package/src/entries/agent-chat.ts +56 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,90 @@
|
|
|
3
3
|
Notable changes per release. Versions before 1.6.0 are recorded in the git log
|
|
4
4
|
and in `../../CONSUMING.md`; this file starts where the changelog does.
|
|
5
5
|
|
|
6
|
+
## 1.10.0
|
|
7
|
+
|
|
8
|
+
1.9.0 was never released: it was published by mistake and has been deprecated.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Screens inside the pop-up** (DESIGN-78). An extension's tool can answer
|
|
13
|
+
with its own screen — a `ui://` resource — and until now only cortenaweb
|
|
14
|
+
could show it; the pop-up inside the extension had no host. The host is now
|
|
15
|
+
in this package, on the `cortena-ui/agent-chat` entry:
|
|
16
|
+
- `McpAppFrame` — the sandboxed iframe (`allow-scripts allow-forms`, never
|
|
17
|
+
`allow-same-origin`), the `srcdoc` with the CSP written ahead of the app's
|
|
18
|
+
markup (`default-src 'none'`, no `unsafe-eval`, `form-action 'none'`), the
|
|
19
|
+
JSON-RPC bridge (`ui/initialize`, `ui/message`, `tools/call`,
|
|
20
|
+
`ui/update-model-context`, size and theme), the source and origin check
|
|
21
|
+
on every message, the height clamp (120–720 px), the `ui/message` bound
|
|
22
|
+
(`MCP_APP_MAX_MESSAGE_CHARS`, 4,000 chars; longer is refused with
|
|
23
|
+
`-32602`, since that text reaches the model) and the theme push. It holds
|
|
24
|
+
no credential: `callTool({ name, arguments, extensionId, lane })` is
|
|
25
|
+
supplied by the mounting app. A name that visibly belongs to another
|
|
26
|
+
extension (`mcp__notes__x`, `notes.note.delete`) is refused with `-32602`
|
|
27
|
+
before the relay is reached; a bare name (`task_get`, and equally
|
|
28
|
+
`broker_invoke`, `exec`, `read_file`) names no extension, so the frame
|
|
29
|
+
does not claim to confine it — it arrives as `lane: "bare"` beside
|
|
30
|
+
`extensionId`, for a relay that resolves it on that extension and nowhere
|
|
31
|
+
else. A resource another extension owns, a non-HTML type or a document
|
|
32
|
+
over
|
|
33
|
+
`MCP_APP_MAX_DOCUMENT_CHARS` (512,000; exported so a caller can make the
|
|
34
|
+
bound its own later, PLATFORM-82) is drawn as nothing and reported through
|
|
35
|
+
`onRefused`. Design tokens go in as `tokensCss` (inlined) or `tokensUrl`
|
|
36
|
+
(linked; its origin joins `style-src`/`font-src`); nothing is fetched
|
|
37
|
+
implicitly.
|
|
38
|
+
- `renderMcpAppToolCall({ extensionId, callTool, onMessage, … })` — a
|
|
39
|
+
`renderToolCall` for one extension. A screen is drawn only when the
|
|
40
|
+
*producing tool* can be named as this extension's — `mcp__<extensionId>__*`,
|
|
41
|
+
or `broker_invoke` whose `source` argument is this extension — **and** the
|
|
42
|
+
result carries that extension's embedded `ui://` resource (with
|
|
43
|
+
`structuredContent` from the result, or from
|
|
44
|
+
`_meta.cortena.structuredContent` on the block, as a reopened chat carries
|
|
45
|
+
it). A result is not a permission: an `exec` or a `read_file` whose output
|
|
46
|
+
happens to embed a `ui://` block keeps the ordinary card. `isEligible`
|
|
47
|
+
replaces the allowlist for a host whose tools are spelled another way.
|
|
48
|
+
Anything else returns `undefined` and the default card is drawn. Same type
|
|
49
|
+
as the seam in every mode.
|
|
50
|
+
- `AgentChatPopup` and owned-mode `AgentChat` take `renderToolCall` as
|
|
51
|
+
hosted mode does — the prop was on the shared base already; this release
|
|
52
|
+
pins it with a type-level test that the 1.7.0 mount shapes still compile.
|
|
53
|
+
See "Screens inside the pop-up" in `README.md`.
|
|
54
|
+
- **The pop-up keeps every turn, and a screen can answer back** (DESIGN-80).
|
|
55
|
+
The pop-up used to hold the current turn only: `send` emptied the tool
|
|
56
|
+
cards, so an extension's screen was unmounted by the very turn it asked
|
|
57
|
+
for. It now behaves like the main chat.
|
|
58
|
+
- **A turn's tool record is its own.** Each turn keeps its steps, its cards
|
|
59
|
+
and its screens, and they are drawn in the transcript **under the answer
|
|
60
|
+
that turn produced** rather than in a strip at the foot that the next run
|
|
61
|
+
clears. A screen from three turns ago is still on the page and still
|
|
62
|
+
running. `AgentChatState` gains `turns?: AgentChatTurn[]` — optional, so a
|
|
63
|
+
host projecting its own store onto `UseAgentChatResult` compiles and
|
|
64
|
+
behaves exactly as before — with `TURN_LIMIT` (20) turns kept.
|
|
65
|
+
- **A reopened chat gets the record back, not just the prose.**
|
|
66
|
+
`parseHistory(records)` returns the messages *and* the per-turn tool
|
|
67
|
+
record behind them: the calls (`tool_use`, `toolCall`, `serverToolUse` —
|
|
68
|
+
matched with the case and the underscores taken out, as cortenacore
|
|
69
|
+
matches them), their results, and the embedded `ui://` resource with its
|
|
70
|
+
payload read from `_meta.cortena.structuredContent` where the pod
|
|
71
|
+
persisted it. The budget is cortenaweb's: 1,100,000 characters for one
|
|
72
|
+
result (`MAX_HISTORY_TOOL_RESULT_CHARS`) and 2,000,000 for a whole load
|
|
73
|
+
(`MAX_HISTORY_SNAPSHOT_RESULT_CHARS`), spent newest turn first; past it a
|
|
74
|
+
result becomes a note giving its size, with the call id, the tool's name
|
|
75
|
+
and whether it failed all kept. `parseHistoryMessages` is unchanged.
|
|
76
|
+
- **`AgentToolRenderContext` gains `send(text)`**, and
|
|
77
|
+
`AgentChatToolsSlotProps` gains the same field for a host that replaces
|
|
78
|
+
the region and builds the context itself. Optional on both, so a host
|
|
79
|
+
that builds the render context as an object literal of its own — as
|
|
80
|
+
cortenaweb's step accordion does — compiles unchanged. `renderMcpAppToolCall` wires
|
|
81
|
+
`onMessage` to it by default, so `onMessage` is now optional: a
|
|
82
|
+
`ui/message` from a screen goes out as the MCP App action envelope —
|
|
83
|
+
`{ toolCallId, appUri, serverName, name: "message", payload: { text } }` —
|
|
84
|
+
attributed to the screen, with no user bubble, bounded by
|
|
85
|
+
`MCP_APP_MAX_MESSAGE_CHARS` (4,000) and refused while a run is live.
|
|
86
|
+
- A host that supplies `components.Tools` still owns the whole tool region:
|
|
87
|
+
it is handed every turn's entries, and the transcript draws none.
|
|
88
|
+
- See "Screens inside the pop-up" in `README.md`.
|
|
89
|
+
|
|
6
90
|
## 1.8.0
|
|
7
91
|
|
|
8
92
|
### Added
|
package/README.md
CHANGED
|
@@ -300,24 +300,101 @@ cortenacore.http.endpoints.agui.enabled = true
|
|
|
300
300
|
cortenacore.http.endpoints.controlPlane.enabled = true
|
|
301
301
|
```
|
|
302
302
|
|
|
303
|
-
###
|
|
303
|
+
### Screens inside the pop-up
|
|
304
304
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
305
|
+
An extension's tool may answer with the extension's own screen — an embedded
|
|
306
|
+
`ui://` resource whose body is HTML — and the pop-up draws it inside the
|
|
307
|
+
conversation, sandboxed, with a postMessage bridge (DESIGN-78; the protocol as
|
|
308
|
+
implemented is cortena's `docs/mcp-apps.md`). `renderToolCall` is the seam,
|
|
309
|
+
in every mode, and `renderMcpAppToolCall` answers it for one extension:
|
|
310
310
|
|
|
311
311
|
```tsx
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
client={client}
|
|
315
|
-
renderToolCall={({ entry, mcpAppUri }) =>
|
|
316
|
-
mcpAppUri ? <McpAppFrame uri={mcpAppUri} result={entry.output} /> : undefined
|
|
317
|
-
}
|
|
318
|
-
/>
|
|
312
|
+
const renderToolCall = renderMcpAppToolCall({ extensionId: "tasks", callTool });
|
|
313
|
+
<AgentChatPopup extension={extension} client={client} renderToolCall={renderToolCall} />
|
|
319
314
|
```
|
|
320
315
|
|
|
316
|
+
That is the whole round trip. A tool answers with a screen, the screen is
|
|
317
|
+
drawn under the answer it came with, a click inside it starts the next turn,
|
|
318
|
+
and the screen is still there when the answer arrives (DESIGN-80).
|
|
319
|
+
|
|
320
|
+
- **Only a tool that can be named as yours may draw a screen.** The default
|
|
321
|
+
allowlist is `mcp__<extensionId>__*`, and `broker_invoke` whose `source`
|
|
322
|
+
argument is your extension. A result is not a permission: an `exec` that
|
|
323
|
+
cat'd a fixture or a `read_file` over your test data answers with whatever
|
|
324
|
+
`ui://` block was in the file, and that stays an ordinary tool card. Pass
|
|
325
|
+
`isEligible(entry)` to replace the rule if your host's tools are spelled a
|
|
326
|
+
third way — you are replacing an allowlist, so you are deciding which tools
|
|
327
|
+
may put unreviewed HTML on the page.
|
|
328
|
+
- `callTool(call)` is **yours**: the relay that reaches your extension — your
|
|
329
|
+
own MCP relay, or cortenaweb's `/tools/invoke` — and whatever credential it
|
|
330
|
+
takes stays with it. The frame never sees a token. It hands you
|
|
331
|
+
`{ name, arguments, extensionId, lane }`, and the lane is the point:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
type McpAppToolCaller = (call: {
|
|
335
|
+
name: string;
|
|
336
|
+
arguments: Record<string, unknown>;
|
|
337
|
+
extensionId: string;
|
|
338
|
+
lane: "mcp" | "broker" | "bare";
|
|
339
|
+
}) => Promise<unknown>;
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
`lane: "mcp"` (`mcp__tasks__x`) and `lane: "broker"` (`tasks.task.get`)
|
|
343
|
+
carry the extension in the name, and a name that carries a *different* one
|
|
344
|
+
(`mcp__notes__x`, `notes.note.delete`) is refused with `-32602` before your
|
|
345
|
+
relay is reached. `lane: "bare"` (`task_get`) carries nothing: `exec`,
|
|
346
|
+
`read_file` and `broker_invoke` are bare names too. **Resolve a bare name on
|
|
347
|
+
`extensionId` and nowhere else** — never forward it as a tool name of your
|
|
348
|
+
own — and an app cannot reach past its extension whatever it asks for.
|
|
349
|
+
- `onMessage(text)` is the app asking for something to be said, and **it is
|
|
350
|
+
wired for you**: with nothing passed, the text goes out through the seam's
|
|
351
|
+
own `send` as the next turn, attributed to the screen and not to the user —
|
|
352
|
+
nobody typed it, so no user bubble is drawn. What reaches the agent is the
|
|
353
|
+
MCP App action envelope (`{ toolCallId, appUri, serverName, name: "message",
|
|
354
|
+
payload: { text } }`), which is what lets an agent tell an event from your
|
|
355
|
+
board apart from one from another extension's card. It is bounded: the host
|
|
356
|
+
refuses text over 4,000 characters (`MCP_APP_MAX_MESSAGE_CHARS`) with
|
|
357
|
+
`-32602`, because this text reaches the model, and it is a no-op while a run
|
|
358
|
+
is already live — one run at a time, the same rule the composer follows.
|
|
359
|
+
Pass your own `onMessage` to do something else with it instead.
|
|
360
|
+
`onModelContext(ctx)` is its latest context for the next run.
|
|
361
|
+
- A call from a tool outside the allowlist, or a result carrying another
|
|
362
|
+
extension's screen, a bare link, or no screen at all, returns `undefined`
|
|
363
|
+
and the default card is drawn.
|
|
364
|
+
- Design tokens are not fetched implicitly. Pass `tokensCss` (the sheet,
|
|
365
|
+
inlined ahead of the app's markup) or `tokensUrl` (linked; its origin joins
|
|
366
|
+
`style-src` and `font-src`, never `connect-src`).
|
|
367
|
+
|
|
368
|
+
What the frame owns and no caller can widen: `sandbox="allow-scripts
|
|
369
|
+
allow-forms"` (never `allow-same-origin`, so the document runs in an opaque
|
|
370
|
+
origin with no storage and no reach into the page), a CSP written into the
|
|
371
|
+
`srcdoc` ahead of the app's markup (`default-src 'none'`, no `unsafe-eval`,
|
|
372
|
+
`form-action 'none'`; `_meta.ui.csp` on the resource may open named `https`
|
|
373
|
+
origins and nothing else), a message check that accepts only its own frame's
|
|
374
|
+
window at an opaque origin, a height the app asks for clamped to 120–720 px,
|
|
375
|
+
and a document bound of `MCP_APP_MAX_DOCUMENT_CHARS` (512,000) past which the
|
|
376
|
+
resource is refused. A refusal draws nothing and reports through
|
|
377
|
+
`onRefused(reason)`. `McpAppFrame` is exported for a host that finds the
|
|
378
|
+
resource itself.
|
|
379
|
+
|
|
380
|
+
**Where a screen lives.** Each turn keeps its own tool record — its steps, its
|
|
381
|
+
cards and its screens — and the record is drawn in the transcript under the
|
|
382
|
+
answer that turn produced, not in a strip at the foot that the next `send`
|
|
383
|
+
empties. So a screen from three turns ago is still on the page, still running,
|
|
384
|
+
and a turn sent from a screen does not unmount the screen that sent it. The
|
|
385
|
+
record of a turn survives a reopen too: `chat.history` carries the calls and
|
|
386
|
+
their results, so the cards, the worded steps and any screen come back with the
|
|
387
|
+
prose (`parseHistory`). Twenty turns are kept (`TURN_LIMIT`), and a reopen
|
|
388
|
+
spends at most 2 MB on tool results, newest turn first; past that a result
|
|
389
|
+
becomes a note that says how big it was, with its call, its name and its
|
|
390
|
+
outcome intact.
|
|
391
|
+
|
|
392
|
+
`AgentToolRenderContext` therefore carries `send(text)` — the handle
|
|
393
|
+
`renderMcpAppToolCall` wires `onMessage` to. A host that replaces the whole
|
|
394
|
+
tool region with `components.Tools` gets the same handle on its slot props and
|
|
395
|
+
passes it into `renderToolCall` itself; that host owns every card, so the
|
|
396
|
+
transcript draws none, and its `entries` are every turn's, not just the newest.
|
|
397
|
+
|
|
321
398
|
### Hosting the surface
|
|
322
399
|
|
|
323
400
|
`AgentChatPopup` owns its state. A host that already has state of its own —
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
//#region src/agent-chat/mcp-app/bridge.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The host side of the MCP Apps postMessage bridge.
|
|
5
|
+
*
|
|
6
|
+
* Implements the host role of the MCP Apps specification dated 2026-01-26 —
|
|
7
|
+
* the one `@modelcontextprotocol/ext-apps` 1.7.5 publishes — with the method
|
|
8
|
+
* names and handshake verbatim, so an app built with the SDK's `App` class
|
|
9
|
+
* connects with no Cortena-specific code. Ported from cortenaweb's host
|
|
10
|
+
* (DESIGN-36); the spec strings are copied rather than imported so this
|
|
11
|
+
* package does not take `@modelcontextprotocol/sdk` and a zod copy into the
|
|
12
|
+
* browser bundle for one JSON-RPC dialogue, and the tests pin every one.
|
|
13
|
+
*
|
|
14
|
+
* ## Who the host will listen to
|
|
15
|
+
*
|
|
16
|
+
* Three checks, in order, before a frame's message is looked at at all:
|
|
17
|
+
*
|
|
18
|
+
* 1. `event.source === iframe.contentWindow` — the one thing a frame cannot
|
|
19
|
+
* forge. Any other frame on the page fails here.
|
|
20
|
+
* 2. `event.origin === "null"` — an app runs without `allow-same-origin`, so
|
|
21
|
+
* its origin is opaque and serialises as the string `"null"`. A message
|
|
22
|
+
* from a real origin did not come from a sandboxed app, whatever its
|
|
23
|
+
* `source` claims.
|
|
24
|
+
* 3. The body must be a JSON-RPC 2.0 envelope naming a method the host serves.
|
|
25
|
+
*
|
|
26
|
+
* Replies go out with `targetOrigin: "*"`, which is correct rather than lazy:
|
|
27
|
+
* an opaque origin cannot be named, so `"*"` is the only value that reaches
|
|
28
|
+
* it, and nothing sent is a secret — the tool result the app's own extension
|
|
29
|
+
* produced, and the theme.
|
|
30
|
+
*/
|
|
31
|
+
/** `LATEST_PROTOCOL_VERSION` in the MCP Apps spec. */
|
|
32
|
+
export declare const MCP_APP_PROTOCOL_VERSION = "2026-01-26";
|
|
33
|
+
/** Every method name the spec defines, as the host sees them. */
|
|
34
|
+
export declare const MCP_APP_METHOD: {
|
|
35
|
+
readonly INITIALIZE: "ui/initialize";
|
|
36
|
+
readonly OPEN_LINK: "ui/open-link";
|
|
37
|
+
readonly MESSAGE: "ui/message";
|
|
38
|
+
readonly UPDATE_MODEL_CONTEXT: "ui/update-model-context";
|
|
39
|
+
readonly REQUEST_DISPLAY_MODE: "ui/request-display-mode";
|
|
40
|
+
readonly CALL_TOOL: "tools/call";
|
|
41
|
+
readonly PING: "ping";
|
|
42
|
+
readonly INITIALIZED: "ui/notifications/initialized";
|
|
43
|
+
readonly SIZE_CHANGED: "ui/notifications/size-changed";
|
|
44
|
+
readonly TOOL_INPUT: "ui/notifications/tool-input";
|
|
45
|
+
readonly TOOL_RESULT: "ui/notifications/tool-result";
|
|
46
|
+
readonly HOST_CONTEXT_CHANGED: "ui/notifications/host-context-changed";
|
|
47
|
+
};
|
|
48
|
+
/** JSON-RPC error codes the host returns. */
|
|
49
|
+
export declare const MCP_APP_ERROR: {
|
|
50
|
+
readonly METHOD_NOT_FOUND: -32601;
|
|
51
|
+
readonly INVALID_PARAMS: -32602;
|
|
52
|
+
readonly INTERNAL: -32603;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The longest `ui/message` the host will carry.
|
|
56
|
+
*
|
|
57
|
+
* `ui/message` becomes a turn in the conversation, so the text an app sends
|
|
58
|
+
* is text that goes to the model — and an app is HTML nobody reviewed. An
|
|
59
|
+
* unbounded field is a way to spend a context window, and a paste of a
|
|
60
|
+
* hundred kilobytes is not a sentence a screen wanted said. Long enough for
|
|
61
|
+
* any real request; short enough that it cannot be the payload.
|
|
62
|
+
*/
|
|
63
|
+
export declare const MCP_APP_MAX_MESSAGE_CHARS = 4000;
|
|
64
|
+
export interface McpAppHostContext {
|
|
65
|
+
theme?: "light" | "dark";
|
|
66
|
+
styles?: {
|
|
67
|
+
variables?: Record<string, string | undefined>;
|
|
68
|
+
};
|
|
69
|
+
displayMode?: "inline";
|
|
70
|
+
availableDisplayModes?: Array<"inline">;
|
|
71
|
+
containerDimensions?: {
|
|
72
|
+
maxWidth?: number;
|
|
73
|
+
maxHeight?: number;
|
|
74
|
+
};
|
|
75
|
+
locale?: string;
|
|
76
|
+
timeZone?: string;
|
|
77
|
+
userAgent?: string;
|
|
78
|
+
platform?: "web";
|
|
79
|
+
/** Cortena extension: where the app can fetch the design tokens, when the host serves them. */
|
|
80
|
+
cortena?: {
|
|
81
|
+
tokensCssUrl?: string;
|
|
82
|
+
};
|
|
83
|
+
[key: string]: unknown;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* How a scoped `tools/call` reaches the app's own extension. Supplied by the
|
|
87
|
+
* mounting app; the frame never sees a token.
|
|
88
|
+
*
|
|
89
|
+
* The call arrives as one envelope rather than a name and arguments, because
|
|
90
|
+
* a name on its own cannot be confined. Two of the three spellings an app may
|
|
91
|
+
* use carry the extension (`mcp__tasks__x`, `tasks.task.get`) and are refused
|
|
92
|
+
* before they reach here when the extension is not this one; the third is
|
|
93
|
+
* **bare** (`task_get`), and a bare name names nothing — `broker_invoke`,
|
|
94
|
+
* `exec` and `read_file` are bare names too. So `extensionId` and `lane`
|
|
95
|
+
* travel with it: a relay handed `lane: "bare"` must resolve `name` on
|
|
96
|
+
* `extensionId` and nowhere else, and a relay that scopes on `extensionId`
|
|
97
|
+
* cannot be tricked into forwarding an app's bare name as a tool of its own.
|
|
98
|
+
*/
|
|
99
|
+
export type McpAppToolCaller = (call: {
|
|
100
|
+
name: string;
|
|
101
|
+
arguments: Record<string, unknown>;
|
|
102
|
+
extensionId: string;
|
|
103
|
+
lane: "mcp" | "broker" | "bare";
|
|
104
|
+
}) => Promise<unknown>;
|
|
105
|
+
export interface McpAppBridgeOptions {
|
|
106
|
+
/** The frame the app runs in. Its `contentWindow` is the only valid source. */
|
|
107
|
+
iframe: HTMLIFrameElement;
|
|
108
|
+
/** The extension the app belongs to; every `tools/call` is confined to it. */
|
|
109
|
+
extensionId: string;
|
|
110
|
+
/** `_meta.ui` the resource declared, echoed in `hostCapabilities.sandbox`. */
|
|
111
|
+
sandbox?: {
|
|
112
|
+
permissions?: unknown;
|
|
113
|
+
csp?: unknown;
|
|
114
|
+
};
|
|
115
|
+
/** Arguments the agent passed. Sent as `ui/notifications/tool-input`. */
|
|
116
|
+
toolArguments?: Record<string, unknown>;
|
|
117
|
+
/** The whole tool result. Sent as `ui/notifications/tool-result`. */
|
|
118
|
+
toolResult?: unknown;
|
|
119
|
+
hostContext: McpAppHostContext;
|
|
120
|
+
/** Present only when the app's own extension can be reached. */
|
|
121
|
+
callTool?: McpAppToolCaller;
|
|
122
|
+
/** `ui/message`: the text the app wants said in the conversation. */
|
|
123
|
+
onMessage?: (text: string) => void;
|
|
124
|
+
/** The app asked for a height. */
|
|
125
|
+
onSizeChanged?: (size: {
|
|
126
|
+
width?: number;
|
|
127
|
+
height?: number;
|
|
128
|
+
}) => void;
|
|
129
|
+
/** The handshake completed. */
|
|
130
|
+
onInitialized?: () => void;
|
|
131
|
+
/** `ui/open-link`. Defaults to a `noopener` `window.open`. */
|
|
132
|
+
onOpenLink?: (url: string) => boolean;
|
|
133
|
+
/** The app's latest `ui/update-model-context`. */
|
|
134
|
+
onModelContext?: (context: {
|
|
135
|
+
content?: unknown[];
|
|
136
|
+
structuredContent?: unknown;
|
|
137
|
+
}) => void;
|
|
138
|
+
/** For tests. Defaults to `window`. */
|
|
139
|
+
listenTarget?: Pick<Window, "addEventListener" | "removeEventListener">;
|
|
140
|
+
}
|
|
141
|
+
export declare class McpAppHostBridge {
|
|
142
|
+
private readonly options;
|
|
143
|
+
private readonly listenTarget;
|
|
144
|
+
private hostContext;
|
|
145
|
+
private toolResult;
|
|
146
|
+
private initialized;
|
|
147
|
+
private closed;
|
|
148
|
+
private readonly onMessage;
|
|
149
|
+
constructor(options: McpAppBridgeOptions);
|
|
150
|
+
start(): void;
|
|
151
|
+
close(): void;
|
|
152
|
+
/** Did the app complete `ui/initialize` + `ui/notifications/initialized`? */
|
|
153
|
+
get isInitialized(): boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Capabilities the host advertises. `serverTools` only when there is a
|
|
156
|
+
* caller: an app that sees it absent knows not to try, which is the spec's
|
|
157
|
+
* own way of saying "not here" and better than letting it call and fail.
|
|
158
|
+
*/
|
|
159
|
+
private capabilities;
|
|
160
|
+
/**
|
|
161
|
+
* A replaced tool result for a screen already running.
|
|
162
|
+
*
|
|
163
|
+
* The same call re-run answers with the same document, so the frame is not
|
|
164
|
+
* remounted and the app never repeats its handshake — which is where the
|
|
165
|
+
* payload is otherwise sent. Without this the screen would keep drawing the
|
|
166
|
+
* first answer's data for the rest of the conversation.
|
|
167
|
+
*/
|
|
168
|
+
setToolResult(result: unknown): void;
|
|
169
|
+
/** Push a changed theme (or anything else) to a running app. */
|
|
170
|
+
setHostContext(patch: McpAppHostContext): void;
|
|
171
|
+
private post;
|
|
172
|
+
private notify;
|
|
173
|
+
private respond;
|
|
174
|
+
private fail;
|
|
175
|
+
/** The right window, and an origin that proves it is sandboxed. */
|
|
176
|
+
private isFromApp;
|
|
177
|
+
private handleMessage;
|
|
178
|
+
private dispatch;
|
|
179
|
+
/**
|
|
180
|
+
* `tools/call` from the app: scoped, not proxied. The app may call tools on
|
|
181
|
+
* *its own* extension and nothing else — see `scopeMcpAppToolCall` for the
|
|
182
|
+
* rule and why. A name that visibly belongs to another extension is refused
|
|
183
|
+
* here, before the relay is reached; a bare name reaches the relay wrapped
|
|
184
|
+
* in `extensionId` and `lane: "bare"`, so the relay resolves it on this
|
|
185
|
+
* extension rather than taking it for a tool of its own. Whatever the relay
|
|
186
|
+
* answers goes back as the result, and a throw becomes a tool result the
|
|
187
|
+
* app can show.
|
|
188
|
+
*/
|
|
189
|
+
private handleCallTool;
|
|
190
|
+
/** The tool's arguments and result, once the app says it is listening. */
|
|
191
|
+
private sendToolPayload;
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
//# sourceMappingURL=bridge.d.ts.map
|