import { McpUiHostContext, McpUiHostCapabilities } from '@modelcontextprotocol/ext-apps'; import { ReadResourceRequest, ReadResourceResult, Task } from '@modelcontextprotocol/sdk/types.js'; /** * Shape of the `tasks` capability advertised in `appCapabilities` on the * iframe side, and mirrored back by the host in `hostCapabilities.experimental` * under the MCP Tasks extension identifier (see `readHostTasksCapability`). * * Matches the MCP 2025-11-25 tasks utility: empty objects (`{}`) are used * as presence flags — NOT booleans — so future sub-fields can be added * without wire-format breaks. * * Shape sourced from the MCP SDK's `ServerTasksCapabilitySchema` / * `ClientCapabilities.tasks` contract. Defined locally as a plain * interface because the SDK publishes the shape only as a Zod schema, * not an exported TypeScript type — but the field names below are * identical to the spec and will fail compilation against any SDK-typed * consumer (e.g. `McpUiInitializeResult["hostCapabilities"]`) if they * drift. */ interface TasksCapability { /** Present (as `{}`) if listing tasks is supported. Deferred for MVP. */ list?: Record; /** Present (as `{}`) if cancelling tasks is supported. */ cancel?: Record; /** Which request types may be task-augmented. */ requests?: { tools?: { /** Present (as `{}`) if `tools/call` can be task-augmented. */ call?: Record; }; }; } /** * Options for task-augmenting a `tools/call` request per MCP 2025-11-25. * * The `task` object on `tools/call` params carries caller hints for task * creation. The receiver MAY override (e.g. a server may enforce a lower * TTL); clients read back the authoritative values from `CreateTaskResult.task`. */ interface CallToolAsTaskOptions { /** * Hint for how long (in milliseconds) the receiver should retain task * results after a terminal status. Omit to let the receiver decide. * Per spec, `null` means unlimited lifetime — represented here as the * absence of the field (omit) since requestors rarely need to pin * "unlimited" explicitly. */ ttl?: number; } /** * Handle returned by `synapse.callToolAsTask`. Lifecycle mirrors the MCP * 2025-11-25 tasks utility: the `tools/call` response is a * `CreateTaskResult` (accessible via `task`), and the caller separately * blocks for the terminal `CallToolResult` via `result()`. * * All operations route via the transport's message plumbing; no polling * is performed here — `result()` is a blocking `tasks/result` RPC. If * consumers want interstitial updates they can call `refresh()` or * subscribe to `onStatus` (which is OPTIONAL per spec — hosts MAY or * MAY NOT emit `notifications/tasks/status`). */ interface TaskHandle { /** * Initial task state from the `CreateTaskResult` returned by * `tools/call`. Always populated before the handle is returned. */ readonly task: Task; /** * Send `tasks/result { taskId }` and resolve once the receiver returns * the terminal payload. Per spec, the result shape is exactly what a * non-task `tools/call` would return — parsed here via the shared * `parseToolResult` so `_meta` (including * `io.modelcontextprotocol/related-task`) propagates through. */ result(): Promise>; /** * Send `tasks/get { taskId }` and resolve with the current `Task`. * Non-blocking — returns whatever status the receiver holds right now. */ refresh(): Promise; /** * Send `tasks/cancel { taskId }` and resolve with the final `Task` * (expected `status: "cancelled"`). Cancelling an already-terminal * task surfaces the receiver's `-32602` error. */ cancel(): Promise; /** * Subscribe to `notifications/tasks/status` events scoped to this * handle's `taskId`. Returns an unsubscribe. Spec: status * notifications are OPTIONAL; consumers MUST NOT depend on them for * correctness. */ onStatus(cb: (task: Task) => void): () => void; } interface Theme { mode: "light" | "dark"; tokens: Record; } interface ToolCallResult { data: T; isError: boolean; /** Raw MCP content blocks from the tool response. */ content?: unknown[]; /** * `_meta` field from the underlying `CallToolResult`, passed through * unchanged. Notably carries `io.modelcontextprotocol/related-task` * (`{ taskId }`) on task-augmented results per MCP 2025-11-25. * * Key-preserving: any `_meta` entry the host/server attaches propagates * without explicit support here. Consumers reading known keys should * reference the canonical key names (e.g. `RELATED_TASK_META_KEY` from * `@modelcontextprotocol/sdk/types.js`). */ _meta?: { [key: string]: unknown; }; } /** * Result from a file picker request. Returned after the host has * persisted the picked file to its workspace store; the bytes never * cross the iframe boundary. * * Tools that need the bytes look the file up by `id` (e.g. by passing * it to a tool that calls the host's file APIs server-side). Bytes * inline in tool-call arguments was the prior shape and capped uploads * at the JSON body limit; this `id`-shaped result removes that ceiling. */ interface FileResult { /** Workspace file ID (`fl_` + 24 hex). Stable identifier for this * file in the originating workspace. */ id: string; filename: string; mimeType: string; size: number; } /** Options for requesting a file from the user */ interface RequestFileOptions { /** File type filter (e.g., ".csv,.json", "image/*") */ accept?: string; /** Max file size in bytes. Default: 25 MB */ maxSize?: number; /** Allow multiple file selection. Default: false */ multiple?: boolean; } /** What `useModelContext`'s declarative factory returns. */ interface ModelContext { /** Structured state the agent's tools can read ids and values out of. */ state: Record; /** The one line the model actually reads. */ summary?: string; } interface KeyForwardConfig { key: string; ctrl?: boolean; meta?: boolean; shift?: boolean; alt?: boolean; } interface ToolDefinition { name: string; description?: string; inputSchema: Record; outputSchema?: Record; } interface ConnectOptions { /** App name — must match the bundle name registered with the host. */ name: string; /** Semver version string. */ version: string; /** Track the document height and re-send `size-changed` as it moves. */ autoResize?: boolean; /** * Forward keyboard shortcuts from this iframe up to the host, so the host's * own shortcuts still fire while focus is inside the app. * * `true` forwards the default set (Escape plus every Ctrl/Cmd combo except * the clipboard keys the browser must handle itself); an array forwards * exactly the listed combos. Absent means no forwarding. * * Forwarding is on only where the host declares `ai.nimblebrain/keydown`, and * stays off everywhere else — a `preventDefault` on a host that does nothing with * the key would swallow it for no one's benefit. */ forwardKeys?: boolean | KeyForwardConfig[]; /** Pre-register event handlers before the handshake completes. * These are wired before `initialized` is sent, so no messages are lost. */ on?: Record void>; } interface Dimensions { width?: number; height?: number; maxWidth?: number; maxHeight?: number; } interface ToolResultData { content: unknown; structuredContent: unknown; raw: Record; } /** Known short event names for {@link App.on}. */ type AppEventName = "tool-result" | "tool-input" | "tool-input-partial" | "tool-cancelled" | "theme-changed" | "host-context-changed" | "teardown"; /** * A connected app — what `connect()` resolves to, and the only runtime object * this SDK hands out. * * Deliberately small: it carries the ext-apps spec surface plus the state the * handshake established. NimbleBrain's own extensions (the file picker, * `action`), `downloadFile` and the MCP tasks utility are composable functions * over this object rather than more methods on it. */ interface App { readonly theme: Theme; readonly hostInfo: { name: string; version: string; }; readonly toolInfo: { tool: Record; } | null; readonly containerDimensions: Dimensions | null; /** * The current host context: the handshake's, with every * `host-context-changed` delta merged into it. Spec fields (`theme`, * `styles`, `displayMode`, `toolInfo`) are typed; the open index signature * carries host extensions — NimbleBrain publishes `workspace` here. Read * host-specific fields as optional; another host will not send them. */ readonly hostContext: McpUiHostContext; /** * What the host declared it supports, from the `ui/initialize` result. Every * method here and every helper beside it checks this before it sends, and * degrades in a documented way when the capability is absent. Read it to * decide what to offer at all, rather than to find out from a no-op or an * exception. NimbleBrain extensions are declared under `experimental`; see * `hostSupports`. */ readonly hostCapabilities: McpUiHostCapabilities; /** * True when the host identified itself as NimbleBrain in the handshake. * Identity, not capability: nothing is gated on it except the NimbleBrain * chat context on `sendMessage` (`_meta["ai.nimblebrain/context"]`), which other hosts ignore. */ readonly isNimbleBrainHost: boolean; /** True after `destroy()` has been called. */ readonly destroyed: boolean; /** * Whether the host negotiated the MCP tasks utility for `tools/call`. * * `callToolAsTask` throws when this is false — per MCP 2025-11-25 a * requestor MUST NOT task-augment a call the receiver did not advertise. Read * it to decide whether to offer a long-running action at all, rather than to * discover the answer from an exception. */ readonly supportsTasks: boolean; on(event: "tool-input", handler: (args: Record) => void): () => void; on(event: "tool-result", handler: (data: ToolResultData) => void): () => void; on(event: "theme-changed", handler: (theme: Theme) => void): () => void; /** Fires with the merged snapshot — the same value as `hostContext`. For the * notification exactly as sent, subscribe to the wire method instead: * `on("ui/notifications/host-context-changed", …)`. */ on(event: "host-context-changed", handler: (ctx: McpUiHostContext) => void): () => void; on(event: "teardown", handler: () => void): () => void; on(event: string, handler: (params: any) => void): () => void; /** Report the frame's size (`ui/notifications/size-changed`). Every host accepts it. */ resize(width?: number, height?: number): void; /** * Open a URL through the host (`ui/open-link`). Without `openLinks`, or when * the host refuses, opens it with `window.open` instead. */ openLink(url: string): void; /** * Push the app's visible state to the agent (ext-apps * `ui/update-model-context`). `summary` is what the model reads as text; * `state` rides along as `structuredContent` for tools that need the ids. * * Sends immediately. Callers that push on every keystroke or selection * change want `useModelContext`, which debounces. A no-op when the host did * not declare `updateModelContext`. */ updateModelContext(state: Record, summary?: string): void; /** * Call a tool on this app's own MCP server. * * An app reaches its own server and nothing else. A host scopes every call * to the server that mounted the app, so there is no target to name and no * option to pass — cross-source work belongs to the agent, which can call * two servers and hand one's result to the other. * * Rejects with `HostCapabilityError`, without sending, when the host did not * declare `serverTools`. */ callTool(name: string, args?: Record): Promise>; /** * Read an MCP resource from the originating server via the host bridge * (ext-apps `resources/read`). Named to mirror the ext-apps spec's * `App.readServerResource`. * * Rejects with `HostCapabilityError`, without sending, when the host did not * declare `serverResources`. */ readServerResource(params: ReadResourceRequest["params"]): Promise; /** * Send a user message into the agent conversation (ext-apps `ui/message`). * A no-op when the host did not declare `message`. */ sendMessage(text: string, context?: { action?: string; entity?: string; }): void; destroy(): void; } export type { App as A, ConnectOptions as C, Dimensions as D, FileResult as F, KeyForwardConfig as K, ModelContext as M, RequestFileOptions as R, TaskHandle as T, CallToolAsTaskOptions as a, ToolCallResult as b, Theme as c, ToolResultData as d, ToolDefinition as e, AppEventName as f, TasksCapability as g };