Replace {@includeCode} with synced code fences
Migrate JSDoc and markdown documentation from `{@includeCode ./file#region}`
tags to inline code fences with `source="./file#region"` attributes.
Code examples are now visible directly in source files while remaining
type-checked via their `.examples.ts` source files.
Add `scripts/sync-snippets.ts` to extract code from `//#region` blocks
and sync it into labeled fences. Run with `npm run sync:snippets`.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ac65f46026
commit
58019a48ce
15 changed files with 1776 additions and 96 deletions
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
|
@ -56,6 +56,12 @@ jobs:
|
|||
npm run generate:schemas
|
||||
git diff --exit-code src/generated/ || (echo "Generated schemas are out of date. Run 'npm run generate:schemas' and commit." && exit 1)
|
||||
|
||||
- name: Verify synced snippets are up-to-date
|
||||
shell: bash
|
||||
run: |
|
||||
npm run sync:snippets
|
||||
git diff --exit-code src/ docs/ || (echo "Synced snippets are out of date. Run 'npm run sync:snippets' and commit." && exit 1)
|
||||
|
||||
- run: npm test
|
||||
|
||||
- run: npm run prettier
|
||||
|
|
|
|||
|
|
@ -80,9 +80,9 @@ View (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP Serv
|
|||
|
||||
## Documentation
|
||||
|
||||
JSDoc `@example` tags use `{@includeCode ./file.examples.ts#regionName}` to pull in type-checked code from companion `.examples.ts`/`.examples.tsx` files. Regions are marked with `//#region name` and `//#endregion name`, wrapped in functions (whose parameters provide types for external values). Region names follow `exportedName_variant` or `ClassName_methodName_variant` pattern (e.g., `useApp_basicUsage`, `App_hostCapabilities_checkAfterConnection`).
|
||||
JSDoc `@example` tags should pull type-checked code from companion `.examples.ts` files (e.g., `app.ts` → `app.examples.ts`). Use ` ```ts source="./file.examples.ts#regionName" ` fences referencing `//#region regionName` blocks, then run `npm run sync:snippets`. Region names follow `exportedName_variant` or `ClassName_methodName_variant` pattern (e.g., `useApp_basicUsage`, `App_hostCapabilities_checkAfterConnection`).
|
||||
|
||||
Standalone docs in `docs/` (listed in `typedoc.config.mjs` `projectDocuments`) can also have type-checked companion `.ts`/`.tsx` files using the same `@includeCode` pattern.
|
||||
Standalone docs in `docs/` (listed in `typedoc.config.mjs` `projectDocuments`) can also have type-checked companion `.ts`/`.tsx` files using the same pattern.
|
||||
|
||||
## Full Examples
|
||||
|
||||
|
|
|
|||
332
docs/patterns.md
332
docs/patterns.md
|
|
@ -10,7 +10,27 @@ This document covers common patterns and recipes for building MCP Apps.
|
|||
|
||||
Set {@link types!McpUiToolMeta.visibility `Tool._meta.ui.visibility`} to `["app"]` to make tools only callable by Apps (hidden from the model). This is useful for UI-driven actions like updating quantities, toggling settings, or other interactions that shouldn't appear in the model's tool list.
|
||||
|
||||
{@includeCode ../src/server/index.examples.ts#registerAppTool_appOnlyVisibility}
|
||||
<!-- prettier-ignore -->
|
||||
```ts source="../src/server/index.examples.ts#registerAppTool_appOnlyVisibility"
|
||||
registerAppTool(
|
||||
server,
|
||||
"update-quantity",
|
||||
{
|
||||
description: "Update item quantity in cart",
|
||||
inputSchema: { itemId: z.string(), quantity: z.number() },
|
||||
_meta: {
|
||||
ui: {
|
||||
resourceUri: "ui://shop/cart.html",
|
||||
visibility: ["app"],
|
||||
},
|
||||
},
|
||||
},
|
||||
async ({ itemId, quantity }) => {
|
||||
const cart = await updateCartItem(itemId, quantity);
|
||||
return { content: [{ type: "text", text: JSON.stringify(cart) }] };
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
_See [`examples/system-monitor-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/system-monitor-server) for a full implementation of this pattern._
|
||||
|
||||
|
|
@ -20,11 +40,121 @@ Some host platforms have size limits on tool call responses, so large files (PDF
|
|||
|
||||
**Server-side**: Register an app-only tool that returns data in chunks with pagination metadata:
|
||||
|
||||
{@includeCode ./patterns.tsx#chunkedDataServer}
|
||||
<!-- prettier-ignore -->
|
||||
```tsx source="./patterns.tsx#chunkedDataServer"
|
||||
// Define the chunk response schema
|
||||
const DataChunkSchema = z.object({
|
||||
bytes: z.string(), // base64-encoded data
|
||||
offset: z.number(),
|
||||
byteCount: z.number(),
|
||||
totalBytes: z.number(),
|
||||
hasMore: z.boolean(),
|
||||
});
|
||||
|
||||
const MAX_CHUNK_BYTES = 500 * 1024; // 500KB per chunk
|
||||
|
||||
registerAppTool(
|
||||
server,
|
||||
"read_data_bytes",
|
||||
{
|
||||
title: "Read Data Bytes",
|
||||
description: "Load binary data in chunks",
|
||||
inputSchema: {
|
||||
id: z.string().describe("Resource identifier"),
|
||||
offset: z.number().min(0).default(0).describe("Byte offset"),
|
||||
byteCount: z
|
||||
.number()
|
||||
.default(MAX_CHUNK_BYTES)
|
||||
.describe("Bytes to read"),
|
||||
},
|
||||
outputSchema: DataChunkSchema,
|
||||
// Hidden from model - only callable by the App
|
||||
_meta: { ui: { visibility: ["app"] } },
|
||||
},
|
||||
async ({ id, offset, byteCount }): Promise<CallToolResult> => {
|
||||
const data = await loadData(id); // Your data loading logic
|
||||
const chunk = data.slice(offset, offset + byteCount);
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: `${chunk.length} bytes at ${offset}` }],
|
||||
structuredContent: {
|
||||
bytes: Buffer.from(chunk).toString("base64"),
|
||||
offset,
|
||||
byteCount: chunk.length,
|
||||
totalBytes: data.length,
|
||||
hasMore: offset + chunk.length < data.length,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
**Client-side**: Loop calling the tool until all chunks are received:
|
||||
|
||||
{@includeCode ./patterns.tsx#chunkedDataClient}
|
||||
<!-- prettier-ignore -->
|
||||
```tsx source="./patterns.tsx#chunkedDataClient"
|
||||
interface DataChunk {
|
||||
bytes: string; // base64
|
||||
offset: number;
|
||||
byteCount: number;
|
||||
totalBytes: number;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
async function loadDataInChunks(
|
||||
id: string,
|
||||
onProgress?: (loaded: number, total: number) => void,
|
||||
): Promise<Uint8Array> {
|
||||
const CHUNK_SIZE = 500 * 1024; // 500KB chunks
|
||||
const chunks: Uint8Array[] = [];
|
||||
let offset = 0;
|
||||
let totalBytes = 0;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const result = await app.callServerTool({
|
||||
name: "read_data_bytes",
|
||||
arguments: { id, offset, byteCount: CHUNK_SIZE },
|
||||
});
|
||||
|
||||
if (result.isError || !result.structuredContent) {
|
||||
throw new Error("Failed to load data chunk");
|
||||
}
|
||||
|
||||
const chunk = result.structuredContent as unknown as DataChunk;
|
||||
totalBytes = chunk.totalBytes;
|
||||
hasMore = chunk.hasMore;
|
||||
|
||||
// Decode base64 to bytes
|
||||
const binaryString = atob(chunk.bytes);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
chunks.push(bytes);
|
||||
|
||||
offset += chunk.byteCount;
|
||||
onProgress?.(offset, totalBytes);
|
||||
}
|
||||
|
||||
// Combine all chunks into single array
|
||||
const fullData = new Uint8Array(totalBytes);
|
||||
let pos = 0;
|
||||
for (const chunk of chunks) {
|
||||
fullData.set(chunk, pos);
|
||||
pos += chunk.length;
|
||||
}
|
||||
|
||||
return fullData;
|
||||
}
|
||||
|
||||
// Usage: load data with progress updates
|
||||
loadDataInChunks(resourceId, (loaded, total) => {
|
||||
console.log(`Loading: ${Math.round((loaded / total) * 100)}%`);
|
||||
}).then((data) => {
|
||||
console.log(`Loaded ${data.length} bytes`);
|
||||
});
|
||||
```
|
||||
|
||||
_See [`examples/pdf-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/pdf-server) for a full implementation of this pattern._
|
||||
|
||||
|
|
@ -34,7 +164,23 @@ _See [`examples/pdf-server/`](https://github.com/modelcontextprotocol/ext-apps/t
|
|||
|
||||
**Client-side**: If a runtime error occurs (e.g., API failure, permission denied, resource unavailable), use {@link app!App.updateModelContext `updateModelContext`} to inform the model:
|
||||
|
||||
{@includeCode ../src/app.examples.ts#App_updateModelContext_reportError}
|
||||
<!-- prettier-ignore -->
|
||||
```ts source="../src/app.examples.ts#App_updateModelContext_reportError"
|
||||
try {
|
||||
const _stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
// ... use _stream for transcription
|
||||
} catch (err) {
|
||||
// Inform the model that the app is in a degraded state
|
||||
await app.updateModelContext({
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: transcription unavailable",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Matching host styling (CSS variables, theme, and fonts)
|
||||
|
||||
|
|
@ -46,11 +192,58 @@ Use the SDK's style helpers to apply host styling, then reference them in your C
|
|||
|
||||
**Vanilla JS:**
|
||||
|
||||
{@includeCode ./patterns.tsx#hostStylingVanillaJs}
|
||||
<!-- prettier-ignore -->
|
||||
```tsx source="./patterns.tsx#hostStylingVanillaJs"
|
||||
function applyHostContext(ctx: McpUiHostContext) {
|
||||
if (ctx.theme) {
|
||||
applyDocumentTheme(ctx.theme);
|
||||
}
|
||||
if (ctx.styles?.variables) {
|
||||
applyHostStyleVariables(ctx.styles.variables);
|
||||
}
|
||||
if (ctx.styles?.css?.fonts) {
|
||||
applyHostFonts(ctx.styles.css.fonts);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply when host context changes
|
||||
app.onhostcontextchanged = applyHostContext;
|
||||
|
||||
// Apply initial styles after connecting
|
||||
app.connect().then(() => {
|
||||
const ctx = app.getHostContext();
|
||||
if (ctx) {
|
||||
applyHostContext(ctx);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**React:**
|
||||
|
||||
{@includeCode ./patterns.tsx#hostStylingReact}
|
||||
<!-- prettier-ignore -->
|
||||
```tsx source="./patterns.tsx#hostStylingReact"
|
||||
function MyApp() {
|
||||
const { app } = useApp({
|
||||
appInfo: { name: "MyApp", version: "1.0.0" },
|
||||
capabilities: {},
|
||||
});
|
||||
|
||||
// Apply all host styles (variables, theme, fonts)
|
||||
useHostStyles(app, app?.getHostContext());
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: "var(--color-background-primary)",
|
||||
fontFamily: "var(--font-sans)",
|
||||
}}
|
||||
>
|
||||
<p>Styled with host CSS variables and fonts</p>
|
||||
<p className="theme-aware">Uses [data-theme] selectors</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
_See [`examples/basic-server-vanillajs/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) and [`examples/basic-server-react/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) for full implementations of this pattern._
|
||||
|
||||
|
|
@ -58,11 +251,27 @@ _See [`examples/basic-server-vanillajs/`](https://github.com/modelcontextprotoco
|
|||
|
||||
Toggle fullscreen mode by calling {@link app!App.requestDisplayMode `requestDisplayMode`}:
|
||||
|
||||
{@includeCode ../src/app.examples.ts#App_requestDisplayMode_toggle}
|
||||
<!-- prettier-ignore -->
|
||||
```ts source="../src/app.examples.ts#App_requestDisplayMode_toggle"
|
||||
const ctx = app.getHostContext();
|
||||
if (ctx?.availableDisplayModes?.includes("fullscreen")) {
|
||||
const target = ctx.displayMode === "fullscreen" ? "inline" : "fullscreen";
|
||||
const result = await app.requestDisplayMode({ mode: target });
|
||||
console.log("Now in:", result.mode);
|
||||
}
|
||||
```
|
||||
|
||||
Listen for display mode changes via {@link app!App.onhostcontextchanged `onhostcontextchanged`} to update your UI:
|
||||
|
||||
{@includeCode ../src/app.examples.ts#App_onhostcontextchanged_respondToDisplayMode}
|
||||
<!-- prettier-ignore -->
|
||||
```ts source="../src/app.examples.ts#App_onhostcontextchanged_respondToDisplayMode"
|
||||
app.onhostcontextchanged = (params) => {
|
||||
if (params.displayMode) {
|
||||
const isFullscreen = params.displayMode === "fullscreen";
|
||||
document.body.classList.toggle("fullscreen", isFullscreen);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
_See [`examples/shadertoy-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server) for a full implementation of this pattern._
|
||||
|
||||
|
|
@ -70,7 +279,22 @@ _See [`examples/shadertoy-server/`](https://github.com/modelcontextprotocol/ext-
|
|||
|
||||
Use {@link app!App.updateModelContext `updateModelContext`} to keep the model informed about what the user is viewing or interacting with. Structure the content with YAML frontmatter for easy parsing:
|
||||
|
||||
{@includeCode ../src/app.examples.ts#App_updateModelContext_appState}
|
||||
<!-- prettier-ignore -->
|
||||
```ts source="../src/app.examples.ts#App_updateModelContext_appState"
|
||||
const markdown = `---
|
||||
item-count: ${itemList.length}
|
||||
total-cost: ${totalCost}
|
||||
currency: ${currency}
|
||||
---
|
||||
|
||||
User is viewing their shopping cart with ${itemList.length} items selected:
|
||||
|
||||
${itemList.map((item) => `- ${item}`).join("\n")}`;
|
||||
|
||||
await app.updateModelContext({
|
||||
content: [{ type: "text", text: markdown }],
|
||||
});
|
||||
```
|
||||
|
||||
_See [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) for a full implementation of this pattern._
|
||||
|
||||
|
|
@ -78,7 +302,24 @@ _See [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/t
|
|||
|
||||
When you need to send more data than fits in a message, use {@link app!App.updateModelContext `updateModelContext`} to set the context first, then {@link app!App.sendMessage `sendMessage`} with a brief prompt to trigger a response:
|
||||
|
||||
{@includeCode ../src/app.examples.ts#App_sendMessage_withLargeContext}
|
||||
<!-- prettier-ignore -->
|
||||
```ts source="../src/app.examples.ts#App_sendMessage_withLargeContext"
|
||||
const markdown = `---
|
||||
word-count: ${fullTranscript.split(/\s+/).length}
|
||||
speaker-names: ${speakerNames.join(", ")}
|
||||
---
|
||||
|
||||
${fullTranscript}`;
|
||||
|
||||
// Offload long transcript to model context
|
||||
await app.updateModelContext({ content: [{ type: "text", text: markdown }] });
|
||||
|
||||
// Send brief trigger message
|
||||
await app.sendMessage({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Summarize the key points" }],
|
||||
});
|
||||
```
|
||||
|
||||
_See [`examples/transcript-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/transcript-server) for a full implementation of this pattern._
|
||||
|
||||
|
|
@ -88,11 +329,31 @@ To persist view state across conversation reloads (e.g., current page in a PDF v
|
|||
|
||||
**Server-side**: Tool handler generates a unique `viewUUID` and returns it in `CallToolResult._meta.viewUUID`:
|
||||
|
||||
{@includeCode ./patterns.tsx#persistDataServer}
|
||||
<!-- prettier-ignore -->
|
||||
```tsx source="./patterns.tsx#persistDataServer"
|
||||
// In your tool callback, include viewUUID in the result metadata.
|
||||
return {
|
||||
content: [{ type: "text", text: `Displaying PDF viewer for "${title}"` }],
|
||||
structuredContent: { url, title, pageCount, initialPage: 1 },
|
||||
_meta: {
|
||||
viewUUID: randomUUID(),
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Client-side**: Receive the UUID in {@link app!App.ontoolresult `ontoolresult`} and use it as the storage key:
|
||||
|
||||
{@includeCode ./patterns.tsx#persistData}
|
||||
<!-- prettier-ignore -->
|
||||
```tsx source="./patterns.tsx#persistData"
|
||||
// In your tool callback, include viewUUID in the result metadata.
|
||||
return {
|
||||
content: [{ type: "text", text: `Displaying PDF viewer for "${title}"` }],
|
||||
structuredContent: { url, title, pageCount, initialPage: 1 },
|
||||
_meta: {
|
||||
viewUUID: randomUUID(),
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
_See [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) for a full implementation of this pattern._
|
||||
|
||||
|
|
@ -100,7 +361,27 @@ _See [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/t
|
|||
|
||||
Views with animations, WebGL rendering, or polling can consume significant CPU/GPU even when scrolled out of view. Use [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to pause expensive operations when the view isn't visible:
|
||||
|
||||
{@includeCode ./patterns.tsx#visibilityBasedPause}
|
||||
<!-- prettier-ignore -->
|
||||
```tsx source="./patterns.tsx#visibilityBasedPause"
|
||||
// Use IntersectionObserver to pause when view scrolls out of view
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
animation.play();
|
||||
} else {
|
||||
animation.pause();
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(container);
|
||||
|
||||
// Clean up when the host tears down the view
|
||||
app.onteardown = async () => {
|
||||
observer.disconnect();
|
||||
animation.pause();
|
||||
return {};
|
||||
};
|
||||
```
|
||||
|
||||
_See [`examples/shadertoy-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server) for a full implementation of this pattern._
|
||||
|
||||
|
|
@ -108,7 +389,30 @@ _See [`examples/shadertoy-server/`](https://github.com/modelcontextprotocol/ext-
|
|||
|
||||
Use {@link app!App.ontoolinputpartial `ontoolinputpartial`} to receive streaming tool arguments as they arrive, allowing you to show a loading preview before the complete input is available.
|
||||
|
||||
{@includeCode ../src/app.examples.ts#App_ontoolinputpartial_progressiveRendering}
|
||||
<!-- prettier-ignore -->
|
||||
```ts source="../src/app.examples.ts#App_ontoolinputpartial_progressiveRendering"
|
||||
let toolInputs: Record<string, unknown> | null = null;
|
||||
let toolInputsPartial: Record<string, unknown> | null = null;
|
||||
|
||||
app.ontoolinputpartial = (params) => {
|
||||
toolInputsPartial = params.arguments as Record<string, unknown>;
|
||||
render();
|
||||
};
|
||||
|
||||
app.ontoolinput = (params) => {
|
||||
toolInputs = params.arguments as Record<string, unknown>;
|
||||
toolInputsPartial = null;
|
||||
render();
|
||||
};
|
||||
|
||||
function render() {
|
||||
if (toolInputs) {
|
||||
renderFinalUI(toolInputs);
|
||||
} else {
|
||||
renderLoadingUI(toolInputsPartial); // e.g., shimmer with partial preview
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Partial arguments are "healed" JSON — the host closes unclosed brackets/braces to produce valid JSON. This means objects may be incomplete (e.g., the last item in an array may be truncated). Don't rely on partial data for critical operations; use it only for preview UI.
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@
|
|||
"postinstall": "node scripts/setup-bun.mjs || echo 'setup-bun.mjs failed or not available'",
|
||||
"start": "npm run examples:dev",
|
||||
"generate:schemas": "tsx scripts/generate-schemas.ts && prettier --write \"src/generated/**/*\"",
|
||||
"build": "npm run generate:schemas && node scripts/run-bun.mjs build.bun.ts",
|
||||
"sync:snippets": "bun scripts/sync-snippets.ts",
|
||||
"build": "npm run generate:schemas && npm run sync:snippets && node scripts/run-bun.mjs build.bun.ts",
|
||||
"prepack": "npm run build",
|
||||
"build:all": "npm run examples:build",
|
||||
"test": "bun test src",
|
||||
|
|
|
|||
540
scripts/sync-snippets.ts
Normal file
540
scripts/sync-snippets.ts
Normal file
|
|
@ -0,0 +1,540 @@
|
|||
/**
|
||||
* Code Snippet Sync Script
|
||||
*
|
||||
* This script syncs code snippets from `.examples.ts/.examples.tsx` files
|
||||
* into JSDoc comments containing labeled code fences.
|
||||
*
|
||||
* The script replaces the content inside code fences that have a path#region
|
||||
* reference in their info string.
|
||||
*
|
||||
* ## Code Fence Format
|
||||
*
|
||||
* ``````typescript
|
||||
* ```ts source="./path.examples.ts#regionName"
|
||||
* // code is synced here
|
||||
* ```
|
||||
* ``````
|
||||
*
|
||||
* Optionally, a display filename can be shown before the source reference:
|
||||
*
|
||||
* ``````typescript
|
||||
* ```ts my-app.ts source="./path.examples.ts#regionName"
|
||||
* // code is synced here
|
||||
* ```
|
||||
* ``````
|
||||
*
|
||||
* ## Region Format (in .examples.ts files)
|
||||
*
|
||||
* ``````typescript
|
||||
* //#region regionName
|
||||
* // code here
|
||||
* //#endregion regionName
|
||||
* ``````
|
||||
*
|
||||
* Run: npm run sync:snippets
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, readdirSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const PROJECT_ROOT = join(__dirname, "..");
|
||||
const SRC_DIR = join(PROJECT_ROOT, "src");
|
||||
const DOCS_DIR = join(PROJECT_ROOT, "docs");
|
||||
|
||||
/** Processing mode based on file type */
|
||||
type FileMode = "jsdoc" | "markdown";
|
||||
|
||||
/**
|
||||
* Represents a labeled code fence found in a source file.
|
||||
*/
|
||||
interface LabeledCodeFence {
|
||||
/** Optional display filename (e.g., "my-app.ts") */
|
||||
displayName?: string;
|
||||
/** Relative path to the example file (e.g., "./app.examples.ts") */
|
||||
examplePath: string;
|
||||
/** Region name (e.g., "App_basicUsage") */
|
||||
regionName: string;
|
||||
/** Language from the code fence (ts or tsx) */
|
||||
language: "ts" | "tsx";
|
||||
/** Character index of the opening fence line start */
|
||||
openingFenceStart: number;
|
||||
/** Character index after the opening fence line (after newline) */
|
||||
openingFenceEnd: number;
|
||||
/** Character index of the closing fence line start */
|
||||
closingFenceStart: number;
|
||||
/** The JSDoc line prefix extracted from context (e.g., " * ") */
|
||||
linePrefix: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents extracted region content from an example file.
|
||||
*/
|
||||
interface RegionContent {
|
||||
/** The dedented code content */
|
||||
code: string;
|
||||
/** Language for code fence (ts or tsx) */
|
||||
language: "ts" | "tsx";
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache for example file regions to avoid re-reading files.
|
||||
* Key: absolute example file path
|
||||
* Value: Map<regionName, RegionContent>
|
||||
*/
|
||||
type RegionCache = Map<string, Map<string, RegionContent>>;
|
||||
|
||||
/**
|
||||
* Processing result for a source file.
|
||||
*/
|
||||
interface FileProcessingResult {
|
||||
filePath: string;
|
||||
modified: boolean;
|
||||
snippetsProcessed: number;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
// JSDoc patterns - for code fences inside JSDoc comments with " * " prefix
|
||||
// Matches: <prefix>```<lang> [displayName] source="<path>#<region>"
|
||||
// Example: " * ```ts my-app.ts source="./app.examples.ts#App_basicUsage""
|
||||
// Example: " * ```ts source="./app.examples.ts#App_basicUsage""
|
||||
const JSDOC_LABELED_FENCE_PATTERN =
|
||||
/^(\s*\*\s*)```(ts|tsx)(?:\s+(\S+))?\s+source="([^"#]+)#([^"]+)"/;
|
||||
const JSDOC_CLOSING_FENCE_PATTERN = /^(\s*\*\s*)```\s*$/;
|
||||
|
||||
// Markdown patterns - for plain code fences in markdown files (no prefix)
|
||||
// Matches: ```<lang> [displayName] source="<path>#<region>"
|
||||
// Example: ```tsx source="./patterns.tsx#chunkedDataServer"
|
||||
const MARKDOWN_LABELED_FENCE_PATTERN =
|
||||
/^```(ts|tsx)(?:\s+(\S+))?\s+source="([^"#]+)#([^"]+)"/;
|
||||
const MARKDOWN_CLOSING_FENCE_PATTERN = /^```\s*$/;
|
||||
|
||||
/**
|
||||
* Find all labeled code fences in a source file.
|
||||
* @param content The file content
|
||||
* @param filePath The file path (for error messages)
|
||||
* @param mode The processing mode (jsdoc or markdown)
|
||||
* @returns Array of labeled code fence references
|
||||
*/
|
||||
function findLabeledCodeFences(
|
||||
content: string,
|
||||
filePath: string,
|
||||
mode: FileMode,
|
||||
): LabeledCodeFence[] {
|
||||
const results: LabeledCodeFence[] = [];
|
||||
const lines = content.split("\n");
|
||||
let charIndex = 0;
|
||||
|
||||
// Select patterns based on mode
|
||||
const openPattern =
|
||||
mode === "jsdoc"
|
||||
? JSDOC_LABELED_FENCE_PATTERN
|
||||
: MARKDOWN_LABELED_FENCE_PATTERN;
|
||||
const closePattern =
|
||||
mode === "jsdoc"
|
||||
? JSDOC_CLOSING_FENCE_PATTERN
|
||||
: MARKDOWN_CLOSING_FENCE_PATTERN;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const openMatch = line.match(openPattern);
|
||||
|
||||
if (openMatch) {
|
||||
let linePrefix: string;
|
||||
let language: string;
|
||||
let displayName: string | undefined;
|
||||
let examplePath: string;
|
||||
let regionName: string;
|
||||
|
||||
if (mode === "jsdoc") {
|
||||
// JSDoc: group 1=prefix, 2=lang, 3=displayName, 4=path, 5=region
|
||||
[, linePrefix, language, displayName, examplePath, regionName] =
|
||||
openMatch;
|
||||
} else {
|
||||
// Markdown: group 1=lang, 2=displayName, 3=path, 4=region (no prefix)
|
||||
[, language, displayName, examplePath, regionName] = openMatch;
|
||||
linePrefix = "";
|
||||
}
|
||||
|
||||
const openingFenceStart = charIndex;
|
||||
const openingFenceEnd = charIndex + line.length + 1; // +1 for newline
|
||||
|
||||
// Find closing fence
|
||||
let closingFenceStart = -1;
|
||||
let searchIndex = openingFenceEnd;
|
||||
|
||||
for (let j = i + 1; j < lines.length; j++) {
|
||||
const closeLine = lines[j];
|
||||
if (closePattern.test(closeLine)) {
|
||||
closingFenceStart = searchIndex;
|
||||
break;
|
||||
}
|
||||
searchIndex += closeLine.length + 1;
|
||||
}
|
||||
|
||||
if (closingFenceStart === -1) {
|
||||
throw new Error(
|
||||
`${filePath}: No closing fence for ${examplePath}#${regionName}`,
|
||||
);
|
||||
}
|
||||
|
||||
results.push({
|
||||
displayName,
|
||||
examplePath,
|
||||
regionName,
|
||||
language: language as "ts" | "tsx",
|
||||
openingFenceStart,
|
||||
openingFenceEnd,
|
||||
closingFenceStart,
|
||||
linePrefix,
|
||||
});
|
||||
}
|
||||
|
||||
charIndex += line.length + 1;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedent content by removing a base indentation prefix from each line.
|
||||
* @param content The content to dedent
|
||||
* @param baseIndent The indentation to remove
|
||||
* @returns The dedented content
|
||||
*/
|
||||
function dedent(content: string, baseIndent: string): string {
|
||||
if (!baseIndent) return content;
|
||||
|
||||
const lines = content.split("\n");
|
||||
const dedentedLines = lines.map((line) => {
|
||||
// Preserve empty lines as-is
|
||||
if (line.trim() === "") return "";
|
||||
// Remove the base indentation if present
|
||||
if (line.startsWith(baseIndent)) {
|
||||
return line.slice(baseIndent.length);
|
||||
}
|
||||
// Line has less indentation than base - keep as-is
|
||||
return line;
|
||||
});
|
||||
|
||||
// Trim trailing empty lines
|
||||
while (
|
||||
dedentedLines.length > 0 &&
|
||||
dedentedLines[dedentedLines.length - 1] === ""
|
||||
) {
|
||||
dedentedLines.pop();
|
||||
}
|
||||
|
||||
return dedentedLines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a region from an example file.
|
||||
* @param exampleContent The content of the example file
|
||||
* @param regionName The region name to extract
|
||||
* @param examplePath The example file path (for error messages)
|
||||
* @returns The dedented region content
|
||||
*/
|
||||
function extractRegion(
|
||||
exampleContent: string,
|
||||
regionName: string,
|
||||
examplePath: string,
|
||||
): string {
|
||||
const regionStart = `//#region ${regionName}`;
|
||||
const regionEnd = `//#endregion ${regionName}`;
|
||||
|
||||
const startIndex = exampleContent.indexOf(regionStart);
|
||||
if (startIndex === -1) {
|
||||
throw new Error(`Region "${regionName}" not found in ${examplePath}`);
|
||||
}
|
||||
|
||||
const endIndex = exampleContent.indexOf(regionEnd, startIndex);
|
||||
if (endIndex === -1) {
|
||||
throw new Error(
|
||||
`Region end marker for "${regionName}" not found in ${examplePath}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Get content after the region start line
|
||||
const afterStart = exampleContent.indexOf("\n", startIndex);
|
||||
if (afterStart === -1 || afterStart >= endIndex) {
|
||||
return ""; // Empty region
|
||||
}
|
||||
|
||||
// Extract the raw content
|
||||
const rawContent = exampleContent.slice(afterStart + 1, endIndex);
|
||||
|
||||
// Determine base indentation from the //#region line
|
||||
let lineStart = exampleContent.lastIndexOf("\n", startIndex);
|
||||
lineStart = lineStart === -1 ? 0 : lineStart + 1;
|
||||
const regionLine = exampleContent.slice(lineStart, startIndex);
|
||||
|
||||
// The base indent is the whitespace before //#region
|
||||
const baseIndent = regionLine;
|
||||
|
||||
return dedent(rawContent, baseIndent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or load a region from the cache.
|
||||
* @param sourceFilePath The source file requesting the region
|
||||
* @param examplePath The relative path to the example file
|
||||
* @param regionName The region name to extract
|
||||
* @param cache The region cache
|
||||
* @returns The region content
|
||||
*/
|
||||
function getOrLoadRegion(
|
||||
sourceFilePath: string,
|
||||
examplePath: string,
|
||||
regionName: string,
|
||||
cache: RegionCache,
|
||||
): RegionContent {
|
||||
// Resolve the example path relative to the source file
|
||||
const sourceDir = dirname(sourceFilePath);
|
||||
const absoluteExamplePath = resolve(sourceDir, examplePath);
|
||||
|
||||
// Check cache first
|
||||
let fileCache = cache.get(absoluteExamplePath);
|
||||
if (fileCache) {
|
||||
const cached = fileCache.get(regionName);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
// Load the example file
|
||||
let exampleContent: string;
|
||||
try {
|
||||
exampleContent = readFileSync(absoluteExamplePath, "utf-8");
|
||||
} catch {
|
||||
throw new Error(`Example file not found: ${absoluteExamplePath}`);
|
||||
}
|
||||
|
||||
// Initialize file cache if needed
|
||||
if (!fileCache) {
|
||||
fileCache = new Map();
|
||||
cache.set(absoluteExamplePath, fileCache);
|
||||
}
|
||||
|
||||
// Determine language from file extension
|
||||
const language: "ts" | "tsx" = absoluteExamplePath.endsWith(".tsx")
|
||||
? "tsx"
|
||||
: "ts";
|
||||
|
||||
// Extract the region
|
||||
const code = extractRegion(exampleContent, regionName, examplePath);
|
||||
|
||||
const regionContent: RegionContent = { code, language };
|
||||
fileCache.set(regionName, regionContent);
|
||||
|
||||
return regionContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format code lines for insertion into a JSDoc comment.
|
||||
* @param code The code to format
|
||||
* @param linePrefix The JSDoc line prefix (e.g., " * ")
|
||||
* @returns The formatted code with JSDoc prefixes
|
||||
*/
|
||||
function formatCodeLines(code: string, linePrefix: string): string {
|
||||
const lines = code.split("\n");
|
||||
return lines
|
||||
.map((line) =>
|
||||
line === "" ? linePrefix.trimEnd() : `${linePrefix}${line}`,
|
||||
)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single source file to sync snippets.
|
||||
* @param filePath The source file path
|
||||
* @param cache The region cache
|
||||
* @param mode The processing mode (jsdoc or markdown)
|
||||
* @returns The processing result
|
||||
*/
|
||||
function processFile(
|
||||
filePath: string,
|
||||
cache: RegionCache,
|
||||
mode: FileMode,
|
||||
): FileProcessingResult {
|
||||
const result: FileProcessingResult = {
|
||||
filePath,
|
||||
modified: false,
|
||||
snippetsProcessed: 0,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
let content: string;
|
||||
try {
|
||||
content = readFileSync(filePath, "utf-8");
|
||||
} catch (err) {
|
||||
result.errors.push(`Failed to read file: ${err}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
let fences: LabeledCodeFence[];
|
||||
try {
|
||||
fences = findLabeledCodeFences(content, filePath, mode);
|
||||
} catch (err) {
|
||||
result.errors.push(err instanceof Error ? err.message : String(err));
|
||||
return result;
|
||||
}
|
||||
|
||||
if (fences.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const originalContent = content;
|
||||
|
||||
// Process fences in reverse order to preserve positions
|
||||
for (let i = fences.length - 1; i >= 0; i--) {
|
||||
const fence = fences[i];
|
||||
|
||||
try {
|
||||
const regionContent = getOrLoadRegion(
|
||||
filePath,
|
||||
fence.examplePath,
|
||||
fence.regionName,
|
||||
cache,
|
||||
);
|
||||
|
||||
const formattedCode = formatCodeLines(
|
||||
regionContent.code,
|
||||
fence.linePrefix,
|
||||
);
|
||||
|
||||
// Replace content between opening fence end and closing fence start
|
||||
content =
|
||||
content.slice(0, fence.openingFenceEnd) +
|
||||
formattedCode +
|
||||
"\n" +
|
||||
content.slice(fence.closingFenceStart);
|
||||
|
||||
result.snippetsProcessed++;
|
||||
} catch (err) {
|
||||
result.errors.push(
|
||||
`${filePath}: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
result.snippetsProcessed > 0 &&
|
||||
result.errors.length === 0 &&
|
||||
content !== originalContent
|
||||
) {
|
||||
writeFileSync(filePath, content);
|
||||
result.modified = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all TypeScript source files in a directory, excluding examples, tests, and generated files.
|
||||
* @param dir The directory to search
|
||||
* @returns Array of absolute file paths
|
||||
*/
|
||||
function findSourceFiles(dir: string): string[] {
|
||||
const files: string[] = [];
|
||||
const entries = readdirSync(dir, { withFileTypes: true, recursive: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) continue;
|
||||
|
||||
const name = entry.name;
|
||||
|
||||
// Only process .ts and .tsx files
|
||||
if (!name.endsWith(".ts") && !name.endsWith(".tsx")) continue;
|
||||
|
||||
// Exclude example files, test files
|
||||
if (name.endsWith(".examples.ts") || name.endsWith(".examples.tsx"))
|
||||
continue;
|
||||
if (name.endsWith(".test.ts")) continue;
|
||||
|
||||
// Get the relative path from the parent directory
|
||||
const parentPath = entry.parentPath;
|
||||
|
||||
// Exclude generated directory
|
||||
if (parentPath.includes("/generated") || parentPath.includes("\\generated"))
|
||||
continue;
|
||||
|
||||
const fullPath = join(parentPath, name);
|
||||
files.push(fullPath);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all markdown files in a directory.
|
||||
* @param dir The directory to search
|
||||
* @returns Array of absolute file paths
|
||||
*/
|
||||
function findMarkdownFiles(dir: string): string[] {
|
||||
const files: string[] = [];
|
||||
const entries = readdirSync(dir, { withFileTypes: true, recursive: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) continue;
|
||||
|
||||
// Only process .md files
|
||||
if (!entry.name.endsWith(".md")) continue;
|
||||
|
||||
const fullPath = join(entry.parentPath, entry.name);
|
||||
files.push(fullPath);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("🔧 Syncing code snippets from example files...\n");
|
||||
|
||||
const cache: RegionCache = new Map();
|
||||
const results: FileProcessingResult[] = [];
|
||||
|
||||
// Process TypeScript source files (JSDoc mode)
|
||||
const sourceFiles = findSourceFiles(SRC_DIR);
|
||||
for (const filePath of sourceFiles) {
|
||||
const result = processFile(filePath, cache, "jsdoc");
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
// Process markdown documentation files
|
||||
const markdownFiles = findMarkdownFiles(DOCS_DIR);
|
||||
for (const filePath of markdownFiles) {
|
||||
const result = processFile(filePath, cache, "markdown");
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
// Report results
|
||||
const modified = results.filter((r) => r.modified);
|
||||
const errors = results.flatMap((r) => r.errors);
|
||||
|
||||
if (modified.length > 0) {
|
||||
console.log(`✅ Modified ${modified.length} file(s):`);
|
||||
for (const r of modified) {
|
||||
console.log(` ${r.filePath} (${r.snippetsProcessed} snippet(s))`);
|
||||
}
|
||||
} else {
|
||||
console.log("✅ No files needed modification");
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error("\n❌ Errors:");
|
||||
for (const error of errors) {
|
||||
console.error(` ${error}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("\n🎉 Snippet sync complete!");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("❌ Snippet sync failed:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -215,7 +215,36 @@ type RequestHandlerExtra = Parameters<
|
|||
* 5. **Teardown**: Call {@link teardownResource `teardownResource`} before unmounting iframe
|
||||
*
|
||||
* @example Basic usage
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_basicUsage}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_basicUsage"
|
||||
* // Create MCP client for the server
|
||||
* const client = new Client({
|
||||
* name: "MyHost",
|
||||
* version: "1.0.0",
|
||||
* });
|
||||
* await client.connect(serverTransport);
|
||||
*
|
||||
* // Create bridge for the View
|
||||
* const bridge = new AppBridge(
|
||||
* client,
|
||||
* { name: "MyHost", version: "1.0.0" },
|
||||
* { openLinks: {}, serverTools: {}, logging: {} },
|
||||
* );
|
||||
*
|
||||
* // Set up iframe and connect
|
||||
* const iframe = document.getElementById("app") as HTMLIFrameElement;
|
||||
* const transport = new PostMessageTransport(
|
||||
* iframe.contentWindow!,
|
||||
* iframe.contentWindow!,
|
||||
* );
|
||||
*
|
||||
* bridge.oninitialized = () => {
|
||||
* console.log("View initialized");
|
||||
* // Now safe to send tool input
|
||||
* bridge.sendToolInput({ arguments: { location: "NYC" } });
|
||||
* };
|
||||
*
|
||||
* await bridge.connect(transport);
|
||||
* ```
|
||||
*/
|
||||
export class AppBridge extends Protocol<
|
||||
AppRequest,
|
||||
|
|
@ -238,10 +267,26 @@ export class AppBridge extends Protocol<
|
|||
* @param options - Configuration options (inherited from Protocol)
|
||||
*
|
||||
* @example With MCP client (automatic forwarding)
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_constructor_withMcpClient}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_constructor_withMcpClient"
|
||||
* const bridge = new AppBridge(
|
||||
* mcpClient,
|
||||
* { name: "MyHost", version: "1.0.0" },
|
||||
* { openLinks: {}, serverTools: {}, logging: {} },
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @example Without MCP client (manual handlers)
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_constructor_withoutMcpClient}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_constructor_withoutMcpClient"
|
||||
* const bridge = new AppBridge(
|
||||
* null,
|
||||
* { name: "MyHost", version: "1.0.0" },
|
||||
* { openLinks: {}, serverTools: {}, logging: {} },
|
||||
* );
|
||||
* bridge.oncalltool = async (params, extra) => {
|
||||
* // Handle tool calls manually
|
||||
* return { content: [] };
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
constructor(
|
||||
private _client: Client | null,
|
||||
|
|
@ -280,7 +325,14 @@ export class AppBridge extends Protocol<
|
|||
* @returns view capabilities, or `undefined` if not yet initialized
|
||||
*
|
||||
* @example Check view capabilities after initialization
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_getAppCapabilities_checkAfterInit}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_getAppCapabilities_checkAfterInit"
|
||||
* bridge.oninitialized = () => {
|
||||
* const caps = bridge.getAppCapabilities();
|
||||
* if (caps?.tools) {
|
||||
* console.log("View provides tools");
|
||||
* }
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiAppCapabilities `McpUiAppCapabilities`} for the capabilities structure
|
||||
*/
|
||||
|
|
@ -297,7 +349,14 @@ export class AppBridge extends Protocol<
|
|||
* @returns view implementation info, or `undefined` if not yet initialized
|
||||
*
|
||||
* @example Log view information after initialization
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_getAppVersion_logAfterInit}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_getAppVersion_logAfterInit"
|
||||
* bridge.oninitialized = () => {
|
||||
* const appInfo = bridge.getAppVersion();
|
||||
* if (appInfo) {
|
||||
* console.log(`View: ${appInfo.name} v${appInfo.version}`);
|
||||
* }
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
getAppVersion(): Implementation | undefined {
|
||||
return this._appInfo;
|
||||
|
|
@ -317,7 +376,11 @@ export class AppBridge extends Protocol<
|
|||
* @param extra - Request metadata (abort signal, session info)
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_onping_handleRequest}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_onping_handleRequest"
|
||||
* bridge.onping = (params, extra) => {
|
||||
* console.log("Received ping from view");
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
onping?: (params: PingRequest["params"], extra: RequestHandlerExtra) => void;
|
||||
|
||||
|
|
@ -332,7 +395,16 @@ export class AppBridge extends Protocol<
|
|||
* host container dimension changes, use {@link setHostContext `setHostContext`}.
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_onsizechange_handleResize}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_onsizechange_handleResize"
|
||||
* bridge.onsizechange = ({ width, height }) => {
|
||||
* if (width != null) {
|
||||
* iframe.style.width = `${width}px`;
|
||||
* }
|
||||
* if (height != null) {
|
||||
* iframe.style.height = `${height}px`;
|
||||
* }
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiSizeChangedNotification `McpUiSizeChangedNotification`} for the notification type
|
||||
* @see {@link app!App.sendSizeChanged `App.sendSizeChanged`} - the View method that sends these notifications
|
||||
|
|
@ -390,7 +462,12 @@ export class AppBridge extends Protocol<
|
|||
* initialization handshake and is ready to receive tool input and other data.
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_oninitialized_sendToolInput}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_oninitialized_sendToolInput"
|
||||
* bridge.oninitialized = () => {
|
||||
* console.log("View ready");
|
||||
* bridge.sendToolInput({ arguments: toolArgs });
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiInitializedNotification `McpUiInitializedNotification`} for the notification type
|
||||
* @see {@link sendToolInput `sendToolInput`} for sending tool arguments to the View
|
||||
|
|
@ -422,7 +499,17 @@ export class AppBridge extends Protocol<
|
|||
* - Returns: `Promise<McpUiMessageResult>` with optional `isError` flag
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_onmessage_logMessage}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_onmessage_logMessage"
|
||||
* bridge.onmessage = async ({ role, content }, extra) => {
|
||||
* try {
|
||||
* await chatManager.addMessage({ role, content, source: "app" });
|
||||
* return {}; // Success
|
||||
* } catch (error) {
|
||||
* console.error("Failed to add message:", error);
|
||||
* return { isError: true };
|
||||
* }
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiMessageRequest `McpUiMessageRequest`} for the request type
|
||||
* @see {@link McpUiMessageResult `McpUiMessageResult`} for the result type
|
||||
|
|
@ -460,7 +547,26 @@ export class AppBridge extends Protocol<
|
|||
* - Returns: `Promise<McpUiOpenLinkResult>` with optional `isError` flag
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_onopenlink_handleRequest}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_onopenlink_handleRequest"
|
||||
* bridge.onopenlink = async ({ url }, extra) => {
|
||||
* if (!isAllowedDomain(url)) {
|
||||
* console.warn("Blocked external link:", url);
|
||||
* return { isError: true };
|
||||
* }
|
||||
*
|
||||
* const confirmed = await showDialog({
|
||||
* message: `Open external link?\n${url}`,
|
||||
* buttons: ["Open", "Cancel"],
|
||||
* });
|
||||
*
|
||||
* if (confirmed) {
|
||||
* window.open(url, "_blank", "noopener,noreferrer");
|
||||
* return {};
|
||||
* }
|
||||
*
|
||||
* return { isError: true };
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiOpenLinkRequest `McpUiOpenLinkRequest`} for the request type
|
||||
* @see {@link McpUiOpenLinkResult `McpUiOpenLinkResult`} for the result type
|
||||
|
|
@ -499,7 +605,14 @@ export class AppBridge extends Protocol<
|
|||
* - Returns: `Promise<McpUiRequestDisplayModeResult>` with the actual mode set
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_onrequestdisplaymode_handleRequest}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_onrequestdisplaymode_handleRequest"
|
||||
* bridge.onrequestdisplaymode = async ({ mode }, extra) => {
|
||||
* if (availableDisplayModes.includes(mode)) {
|
||||
* currentDisplayMode = mode;
|
||||
* }
|
||||
* return { mode: currentDisplayMode };
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiRequestDisplayModeRequest `McpUiRequestDisplayModeRequest`} for the request type
|
||||
* @see {@link McpUiRequestDisplayModeResult `McpUiRequestDisplayModeResult`} for the result type
|
||||
|
|
@ -535,7 +648,14 @@ export class AppBridge extends Protocol<
|
|||
* - `params.data` - Log message and optional structured data
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_onloggingmessage_handleLog}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_onloggingmessage_handleLog"
|
||||
* bridge.onloggingmessage = ({ level, logger, data }) => {
|
||||
* console[level === "error" ? "error" : "log"](
|
||||
* `[${logger ?? "View"}] ${level.toUpperCase()}:`,
|
||||
* data,
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
set onloggingmessage(
|
||||
callback: (params: LoggingMessageNotification["params"]) => void,
|
||||
|
|
@ -561,7 +681,16 @@ export class AppBridge extends Protocol<
|
|||
* update received.
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_onupdatemodelcontext_storeContext}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_onupdatemodelcontext_storeContext"
|
||||
* bridge.onupdatemodelcontext = async (
|
||||
* { content, structuredContent },
|
||||
* extra,
|
||||
* ) => {
|
||||
* // Store the context snapshot for inclusion in the next model request
|
||||
* modelContextManager.update({ content, structuredContent });
|
||||
* return {};
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiUpdateModelContextRequest `McpUiUpdateModelContextRequest`} for the request type
|
||||
*/
|
||||
|
|
@ -592,7 +721,15 @@ export class AppBridge extends Protocol<
|
|||
* - `extra` - Request metadata (abort signal, session info)
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_oncalltool_forwardToServer}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_oncalltool_forwardToServer"
|
||||
* bridge.oncalltool = async (params, extra) => {
|
||||
* return mcpClient.request(
|
||||
* { method: "tools/call", params },
|
||||
* CallToolResultSchema,
|
||||
* { signal: extra.signal },
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see `CallToolRequest` from @modelcontextprotocol/sdk for the request type
|
||||
* @see `CallToolResult` from @modelcontextprotocol/sdk for the result type
|
||||
|
|
@ -647,7 +784,15 @@ export class AppBridge extends Protocol<
|
|||
* - `extra` - Request metadata (abort signal, session info)
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_onlistresources_returnResources}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_onlistresources_returnResources"
|
||||
* bridge.onlistresources = async (params, extra) => {
|
||||
* return mcpClient.request(
|
||||
* { method: "resources/list", params },
|
||||
* ListResourcesResultSchema,
|
||||
* { signal: extra.signal },
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see `ListResourcesRequest` from @modelcontextprotocol/sdk for the request type
|
||||
* @see `ListResourcesResult` from @modelcontextprotocol/sdk for the result type
|
||||
|
|
@ -719,7 +864,15 @@ export class AppBridge extends Protocol<
|
|||
* - `extra` - Request metadata (abort signal, session info)
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_onreadresource_returnResource}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_onreadresource_returnResource"
|
||||
* bridge.onreadresource = async (params, extra) => {
|
||||
* return mcpClient.request(
|
||||
* { method: "resources/read", params },
|
||||
* ReadResourceResultSchema,
|
||||
* { signal: extra.signal },
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see `ReadResourceRequest` from @modelcontextprotocol/sdk for the request type
|
||||
* @see `ReadResourceResult` from @modelcontextprotocol/sdk for the result type
|
||||
|
|
@ -779,7 +932,15 @@ export class AppBridge extends Protocol<
|
|||
* - `extra` - Request metadata (abort signal, session info)
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_onlistprompts_returnPrompts}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_onlistprompts_returnPrompts"
|
||||
* bridge.onlistprompts = async (params, extra) => {
|
||||
* return mcpClient.request(
|
||||
* { method: "prompts/list", params },
|
||||
* ListPromptsResultSchema,
|
||||
* { signal: extra.signal },
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see `ListPromptsRequest` from @modelcontextprotocol/sdk for the request type
|
||||
* @see `ListPromptsResult` from @modelcontextprotocol/sdk for the result type
|
||||
|
|
@ -915,10 +1076,17 @@ export class AppBridge extends Protocol<
|
|||
* @param hostContext - The complete new host context state
|
||||
*
|
||||
* @example Update theme when user toggles dark mode
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_setHostContext_updateTheme}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_setHostContext_updateTheme"
|
||||
* bridge.setHostContext({ theme: "dark" });
|
||||
* ```
|
||||
*
|
||||
* @example Update multiple context fields
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_setHostContext_updateMultiple}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_setHostContext_updateMultiple"
|
||||
* bridge.setHostContext({
|
||||
* theme: "dark",
|
||||
* containerDimensions: { maxHeight: 600, width: 800 },
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiHostContext `McpUiHostContext`} for the context structure
|
||||
* @see {@link McpUiHostContextChangedNotification `McpUiHostContextChangedNotification`} for the notification type
|
||||
|
|
@ -971,7 +1139,13 @@ export class AppBridge extends Protocol<
|
|||
* @param params - Complete tool call arguments
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_sendToolInput_afterInit}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_sendToolInput_afterInit"
|
||||
* bridge.oninitialized = () => {
|
||||
* bridge.sendToolInput({
|
||||
* arguments: { location: "New York", units: "metric" },
|
||||
* });
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiToolInputNotification `McpUiToolInputNotification`} for the notification type
|
||||
* @see {@link oninitialized `oninitialized`} for the initialization callback
|
||||
|
|
@ -998,7 +1172,17 @@ export class AppBridge extends Protocol<
|
|||
* @param params - Partial tool call arguments (may be incomplete)
|
||||
*
|
||||
* @example Stream partial arguments as they arrive
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_sendToolInputPartial_streaming}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_sendToolInputPartial_streaming"
|
||||
* // As streaming progresses...
|
||||
* bridge.sendToolInputPartial({ arguments: { loc: "N" } });
|
||||
* bridge.sendToolInputPartial({ arguments: { location: "New" } });
|
||||
* bridge.sendToolInputPartial({ arguments: { location: "New York" } });
|
||||
*
|
||||
* // When complete, send final input
|
||||
* bridge.sendToolInput({
|
||||
* arguments: { location: "New York", units: "metric" },
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiToolInputPartialNotification `McpUiToolInputPartialNotification`} for the notification type
|
||||
* @see {@link sendToolInput `sendToolInput`} for sending complete arguments
|
||||
|
|
@ -1021,7 +1205,13 @@ export class AppBridge extends Protocol<
|
|||
* @param params - Standard MCP tool execution result
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_sendToolResult_afterExecution}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_sendToolResult_afterExecution"
|
||||
* const result = await mcpClient.request(
|
||||
* { method: "tools/call", params: { name: "get_weather", arguments: args } },
|
||||
* CallToolResultSchema,
|
||||
* );
|
||||
* bridge.sendToolResult(result);
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiToolResultNotification `McpUiToolResultNotification`} for the notification type
|
||||
* @see {@link sendToolInput `sendToolInput`} for sending tool arguments before results
|
||||
|
|
@ -1045,10 +1235,19 @@ export class AppBridge extends Protocol<
|
|||
* - `reason`: Human-readable explanation for why the tool was cancelled
|
||||
*
|
||||
* @example User-initiated cancellation
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_sendToolCancelled_userInitiated}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_sendToolCancelled_userInitiated"
|
||||
* // User clicked "Cancel" button
|
||||
* bridge.sendToolCancelled({ reason: "User cancelled the operation" });
|
||||
* ```
|
||||
*
|
||||
* @example System-level cancellation
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_sendToolCancelled_systemLevel}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_sendToolCancelled_systemLevel"
|
||||
* // Sampling error or timeout
|
||||
* bridge.sendToolCancelled({ reason: "Request timeout after 30 seconds" });
|
||||
*
|
||||
* // Classifier intervention
|
||||
* bridge.sendToolCancelled({ reason: "Content policy violation detected" });
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiToolCancelledNotification `McpUiToolCancelledNotification`} for the notification type
|
||||
* @see {@link sendToolResult `sendToolResult`} for sending successful results
|
||||
|
|
@ -1099,7 +1298,15 @@ export class AppBridge extends Protocol<
|
|||
* @returns Promise resolving when view confirms readiness for teardown
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_teardownResource_gracefulShutdown}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_teardownResource_gracefulShutdown"
|
||||
* try {
|
||||
* await bridge.teardownResource({});
|
||||
* // View is ready, safe to unmount iframe
|
||||
* iframe.remove();
|
||||
* } catch (error) {
|
||||
* console.error("Teardown failed:", error);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
teardownResource(
|
||||
params: McpUiResourceTeardownRequest["params"],
|
||||
|
|
@ -1144,10 +1351,33 @@ export class AppBridge extends Protocol<
|
|||
* before calling `bridge.connect()`.
|
||||
*
|
||||
* @example With MCP client (automatic forwarding)
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_connect_withMcpClient}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_connect_withMcpClient"
|
||||
* const bridge = new AppBridge(mcpClient, hostInfo, capabilities);
|
||||
* const transport = new PostMessageTransport(
|
||||
* iframe.contentWindow!,
|
||||
* iframe.contentWindow!,
|
||||
* );
|
||||
*
|
||||
* bridge.oninitialized = () => {
|
||||
* console.log("View ready");
|
||||
* bridge.sendToolInput({ arguments: toolArgs });
|
||||
* };
|
||||
*
|
||||
* await bridge.connect(transport);
|
||||
* ```
|
||||
*
|
||||
* @example Without MCP client (manual handlers)
|
||||
* {@includeCode ./app-bridge.examples.ts#AppBridge_connect_withoutMcpClient}
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_connect_withoutMcpClient"
|
||||
* const bridge = new AppBridge(null, hostInfo, capabilities);
|
||||
*
|
||||
* // Register handlers manually
|
||||
* bridge.oncalltool = async (params, extra) => {
|
||||
* // Custom tool call handling
|
||||
* return { content: [] };
|
||||
* };
|
||||
*
|
||||
* await bridge.connect(transport);
|
||||
* ```
|
||||
*/
|
||||
async connect(transport: Transport) {
|
||||
if (this._client) {
|
||||
|
|
|
|||
296
src/app.ts
296
src/app.ts
|
|
@ -72,10 +72,27 @@ export {
|
|||
* with the `_meta.ui.resourceUri` format instead.
|
||||
*
|
||||
* @example How MCP servers use this key (server-side, not in Apps)
|
||||
* {@includeCode ./app.examples.ts#RESOURCE_URI_META_KEY_serverSide}
|
||||
* ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_serverSide"
|
||||
* server.registerTool(
|
||||
* "weather",
|
||||
* {
|
||||
* description: "Get weather forecast",
|
||||
* _meta: {
|
||||
* [RESOURCE_URI_META_KEY]: "ui://weather/forecast",
|
||||
* },
|
||||
* },
|
||||
* handler,
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @example How hosts check for this metadata (host-side)
|
||||
* {@includeCode ./app.examples.ts#RESOURCE_URI_META_KEY_hostSide}
|
||||
* ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_hostSide"
|
||||
* // Check tool definition metadata (from tools/list response):
|
||||
* const uiUri = tool._meta?.[RESOURCE_URI_META_KEY];
|
||||
* if (typeof uiUri === "string" && uiUri.startsWith("ui://")) {
|
||||
* // Fetch the resource and display the UI
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const RESOURCE_URI_META_KEY = "ui/resourceUri";
|
||||
|
||||
|
|
@ -155,7 +172,19 @@ type RequestHandlerExtra = Parameters<
|
|||
* Both patterns work; use whichever fits your coding style better.
|
||||
*
|
||||
* @example Basic usage with PostMessageTransport
|
||||
* {@includeCode ./app.examples.ts#App_basicUsage}
|
||||
* ```ts source="./app.examples.ts#App_basicUsage"
|
||||
* const app = new App(
|
||||
* { name: "WeatherApp", version: "1.0.0" },
|
||||
* {}, // capabilities
|
||||
* );
|
||||
*
|
||||
* // Register handlers before connecting to ensure no notifications are missed
|
||||
* app.ontoolinput = (params) => {
|
||||
* console.log("Tool arguments:", params.arguments);
|
||||
* };
|
||||
*
|
||||
* await app.connect();
|
||||
* ```
|
||||
*/
|
||||
export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
||||
private _hostCapabilities?: McpUiHostCapabilities;
|
||||
|
|
@ -170,7 +199,13 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* @param options - Configuration options including `autoResize` behavior
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./app.examples.ts#App_constructor_basic}
|
||||
* ```ts source="./app.examples.ts#App_constructor_basic"
|
||||
* const app = new App(
|
||||
* { name: "MyApp", version: "1.0.0" },
|
||||
* { tools: { listChanged: true } }, // capabilities
|
||||
* { autoResize: true }, // options
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
constructor(
|
||||
private _appInfo: Implementation,
|
||||
|
|
@ -199,7 +234,12 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* @returns Host capabilities, or `undefined` if not yet connected
|
||||
*
|
||||
* @example Check host capabilities after connection
|
||||
* {@includeCode ./app.examples.ts#App_getHostCapabilities_checkAfterConnection}
|
||||
* ```ts source="./app.examples.ts#App_getHostCapabilities_checkAfterConnection"
|
||||
* await app.connect();
|
||||
* if (app.getHostCapabilities()?.serverTools) {
|
||||
* console.log("Host supports server tool calls");
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see {@link connect `connect`} for the initialization handshake
|
||||
* @see {@link McpUiHostCapabilities `McpUiHostCapabilities`} for the capabilities structure
|
||||
|
|
@ -218,7 +258,11 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* @returns Host implementation info, or `undefined` if not yet connected
|
||||
*
|
||||
* @example Log host information after connection
|
||||
* {@includeCode ./app.examples.ts#App_getHostVersion_logAfterConnection}
|
||||
* ```ts source="./app.examples.ts#App_getHostVersion_logAfterConnection"
|
||||
* await app.connect(transport);
|
||||
* const { name, version } = app.getHostVersion() ?? {};
|
||||
* console.log(`Connected to ${name} v${version}`);
|
||||
* ```
|
||||
*
|
||||
* @see {@link connect `connect`} for the initialization handshake
|
||||
*/
|
||||
|
|
@ -239,7 +283,16 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* @returns Host context, or `undefined` if not yet connected
|
||||
*
|
||||
* @example Access host context after connection
|
||||
* {@includeCode ./app.examples.ts#App_getHostContext_accessAfterConnection}
|
||||
* ```ts source="./app.examples.ts#App_getHostContext_accessAfterConnection"
|
||||
* await app.connect(transport);
|
||||
* const context = app.getHostContext();
|
||||
* if (context?.theme === "dark") {
|
||||
* document.body.classList.add("dark-theme");
|
||||
* }
|
||||
* if (context?.toolInfo) {
|
||||
* console.log("Tool:", context.toolInfo.tool.name);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see {@link connect `connect`} for the initialization handshake
|
||||
* @see {@link onhostcontextchanged `onhostcontextchanged`} for context change notifications
|
||||
|
|
@ -264,7 +317,14 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* @param callback - Function called with the tool input params ({@link McpUiToolInputNotification.params `McpUiToolInputNotification.params`})
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./app.examples.ts#App_ontoolinput_setter}
|
||||
* ```ts source="./app.examples.ts#App_ontoolinput_setter"
|
||||
* // Register before connecting to ensure no notifications are missed
|
||||
* app.ontoolinput = (params) => {
|
||||
* console.log("Tool:", params.arguments);
|
||||
* // Update your UI with the tool arguments
|
||||
* };
|
||||
* await app.connect();
|
||||
* ```
|
||||
*
|
||||
* @see {@link setNotificationHandler `setNotificationHandler`} for the underlying method
|
||||
* @see {@link McpUiToolInputNotification `McpUiToolInputNotification`} for the notification structure
|
||||
|
|
@ -297,7 +357,29 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* @param callback - Function called with each partial tool input update ({@link McpUiToolInputPartialNotification.params `McpUiToolInputPartialNotification.params`})
|
||||
*
|
||||
* @example Progressive rendering of tool arguments
|
||||
* {@includeCode ./app.examples.ts#App_ontoolinputpartial_progressiveRendering}
|
||||
* ```ts source="./app.examples.ts#App_ontoolinputpartial_progressiveRendering"
|
||||
* let toolInputs: Record<string, unknown> | null = null;
|
||||
* let toolInputsPartial: Record<string, unknown> | null = null;
|
||||
*
|
||||
* app.ontoolinputpartial = (params) => {
|
||||
* toolInputsPartial = params.arguments as Record<string, unknown>;
|
||||
* render();
|
||||
* };
|
||||
*
|
||||
* app.ontoolinput = (params) => {
|
||||
* toolInputs = params.arguments as Record<string, unknown>;
|
||||
* toolInputsPartial = null;
|
||||
* render();
|
||||
* };
|
||||
*
|
||||
* function render() {
|
||||
* if (toolInputs) {
|
||||
* renderFinalUI(toolInputs);
|
||||
* } else {
|
||||
* renderLoadingUI(toolInputsPartial); // e.g., shimmer with partial preview
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see {@link setNotificationHandler `setNotificationHandler`} for the underlying method
|
||||
* @see {@link McpUiToolInputPartialNotification `McpUiToolInputPartialNotification`} for the notification structure
|
||||
|
|
@ -326,7 +408,15 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* @param callback - Function called with the tool result ({@link McpUiToolResultNotification.params `McpUiToolResultNotification.params`})
|
||||
*
|
||||
* @example Display tool execution results
|
||||
* {@includeCode ./app.examples.ts#App_ontoolresult_displayResults}
|
||||
* ```ts source="./app.examples.ts#App_ontoolresult_displayResults"
|
||||
* app.ontoolresult = (params) => {
|
||||
* if (params.isError) {
|
||||
* console.error("Tool execution failed:", params.content);
|
||||
* } else if (params.content) {
|
||||
* console.log("Tool output:", params.content);
|
||||
* }
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see {@link setNotificationHandler `setNotificationHandler`} for the underlying method
|
||||
* @see {@link McpUiToolResultNotification `McpUiToolResultNotification`} for the notification structure
|
||||
|
|
@ -356,7 +446,12 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* @param callback - Function called when tool execution is cancelled. Receives optional cancellation reason — see {@link McpUiToolCancelledNotification.params `McpUiToolCancelledNotification.params`}.
|
||||
*
|
||||
* @example Handle tool cancellation
|
||||
* {@includeCode ./app.examples.ts#App_ontoolcancelled_handleCancellation}
|
||||
* ```ts source="./app.examples.ts#App_ontoolcancelled_handleCancellation"
|
||||
* app.ontoolcancelled = (params) => {
|
||||
* console.log("Tool cancelled:", params.reason);
|
||||
* // Update your UI to show cancellation state
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see {@link setNotificationHandler `setNotificationHandler`} for the underlying method
|
||||
* @see {@link McpUiToolCancelledNotification `McpUiToolCancelledNotification`} for the notification structure
|
||||
|
|
@ -390,7 +485,15 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* @param callback - Function called with the updated host context
|
||||
*
|
||||
* @example Respond to theme changes
|
||||
* {@includeCode ./app.examples.ts#App_onhostcontextchanged_respondToTheme}
|
||||
* ```ts source="./app.examples.ts#App_onhostcontextchanged_respondToTheme"
|
||||
* app.onhostcontextchanged = (params) => {
|
||||
* if (params.theme === "dark") {
|
||||
* document.body.classList.add("dark-theme");
|
||||
* } else {
|
||||
* document.body.classList.remove("dark-theme");
|
||||
* }
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see {@link setNotificationHandler `setNotificationHandler`} for the underlying method
|
||||
* @see {@link McpUiHostContextChangedNotification `McpUiHostContextChangedNotification`} for the notification structure
|
||||
|
|
@ -428,7 +531,14 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* Must return `McpUiResourceTeardownResult` (can be an empty object `{}`) or a Promise resolving to it.
|
||||
*
|
||||
* @example Perform cleanup before teardown
|
||||
* {@includeCode ./app.examples.ts#App_onteardown_performCleanup}
|
||||
* ```ts source="./app.examples.ts#App_onteardown_performCleanup"
|
||||
* app.onteardown = async () => {
|
||||
* await saveState();
|
||||
* closeConnections();
|
||||
* console.log("App ready for teardown");
|
||||
* return {};
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see {@link setRequestHandler `setRequestHandler`} for the underlying method
|
||||
* @see {@link McpUiResourceTeardownRequest `McpUiResourceTeardownRequest`} for the request structure
|
||||
|
|
@ -464,7 +574,15 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* in the constructor.
|
||||
*
|
||||
* @example Handle tool calls from the host
|
||||
* {@includeCode ./app.examples.ts#App_oncalltool_handleFromHost}
|
||||
* ```ts source="./app.examples.ts#App_oncalltool_handleFromHost"
|
||||
* app.oncalltool = async (params, extra) => {
|
||||
* if (params.name === "greet") {
|
||||
* const name = params.arguments?.name ?? "World";
|
||||
* return { content: [{ type: "text", text: `Hello, ${name}!` }] };
|
||||
* }
|
||||
* throw new Error(`Unknown tool: ${params.name}`);
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see {@link setRequestHandler `setRequestHandler`} for the underlying method
|
||||
*/
|
||||
|
|
@ -498,7 +616,13 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* allowed; capability validation occurs when handlers are invoked.
|
||||
*
|
||||
* @example Return available tools
|
||||
* {@includeCode ./app.examples.ts#App_onlisttools_returnTools}
|
||||
* ```ts source="./app.examples.ts#App_onlisttools_returnTools"
|
||||
* app.onlisttools = async (params, extra) => {
|
||||
* return {
|
||||
* tools: ["greet", "calculate", "format"],
|
||||
* };
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see {@link setRequestHandler `setRequestHandler`} for the underlying method
|
||||
* @see {@link oncalltool `oncalltool`} for handling tool execution
|
||||
|
|
@ -587,7 +711,21 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* between transport failures (thrown) and tool execution failures (returned).
|
||||
*
|
||||
* @example Fetch updated weather data
|
||||
* {@includeCode ./app.examples.ts#App_callServerTool_fetchWeather}
|
||||
* ```ts source="./app.examples.ts#App_callServerTool_fetchWeather"
|
||||
* try {
|
||||
* const result = await app.callServerTool({
|
||||
* name: "get_weather",
|
||||
* arguments: { location: "Tokyo" },
|
||||
* });
|
||||
* if (result.isError) {
|
||||
* console.error("Tool returned error:", result.content);
|
||||
* } else {
|
||||
* console.log(result.content);
|
||||
* }
|
||||
* } catch (error) {
|
||||
* console.error("Tool call failed:", error);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async callServerTool(
|
||||
params: CallToolRequest["params"],
|
||||
|
|
@ -613,10 +751,40 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* @throws {Error} If the request times out or the connection is lost
|
||||
*
|
||||
* @example Send a text message from user interaction
|
||||
* {@includeCode ./app.examples.ts#App_sendMessage_textFromInteraction}
|
||||
* ```ts source="./app.examples.ts#App_sendMessage_textFromInteraction"
|
||||
* try {
|
||||
* const result = await app.sendMessage({
|
||||
* role: "user",
|
||||
* content: [{ type: "text", text: "Show me details for item #42" }],
|
||||
* });
|
||||
* if (result.isError) {
|
||||
* console.error("Host rejected the message");
|
||||
* // Handle rejection appropriately for your app
|
||||
* }
|
||||
* } catch (error) {
|
||||
* console.error("Failed to send message:", error);
|
||||
* // Handle transport/protocol error
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example Send follow-up message after offloading large data to model context
|
||||
* {@includeCode ./app.examples.ts#App_sendMessage_withLargeContext}
|
||||
* ```ts source="./app.examples.ts#App_sendMessage_withLargeContext"
|
||||
* const markdown = `---
|
||||
* word-count: ${fullTranscript.split(/\s+/).length}
|
||||
* speaker-names: ${speakerNames.join(", ")}
|
||||
* ---
|
||||
*
|
||||
* ${fullTranscript}`;
|
||||
*
|
||||
* // Offload long transcript to model context
|
||||
* await app.updateModelContext({ content: [{ type: "text", text: markdown }] });
|
||||
*
|
||||
* // Send brief trigger message
|
||||
* await app.sendMessage({
|
||||
* role: "user",
|
||||
* content: [{ type: "text", text: "Summarize the key points" }],
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiMessageRequest `McpUiMessageRequest`} for request structure
|
||||
*/
|
||||
|
|
@ -640,7 +808,13 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* @param params - Log level and message
|
||||
*
|
||||
* @example Log app state for debugging
|
||||
* {@includeCode ./app.examples.ts#App_sendLog_debugState}
|
||||
* ```ts source="./app.examples.ts#App_sendLog_debugState"
|
||||
* app.sendLog({
|
||||
* level: "info",
|
||||
* data: "Weather data refreshed",
|
||||
* logger: "WeatherApp",
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @returns Promise that resolves when the log notification is sent
|
||||
*/
|
||||
|
|
@ -668,10 +842,39 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* @throws {Error} If the request times out or the connection is lost
|
||||
*
|
||||
* @example Update model context with current app state
|
||||
* {@includeCode ./app.examples.ts#App_updateModelContext_appState}
|
||||
* ```ts source="./app.examples.ts#App_updateModelContext_appState"
|
||||
* const markdown = `---
|
||||
* item-count: ${itemList.length}
|
||||
* total-cost: ${totalCost}
|
||||
* currency: ${currency}
|
||||
* ---
|
||||
*
|
||||
* User is viewing their shopping cart with ${itemList.length} items selected:
|
||||
*
|
||||
* ${itemList.map((item) => `- ${item}`).join("\n")}`;
|
||||
*
|
||||
* await app.updateModelContext({
|
||||
* content: [{ type: "text", text: markdown }],
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example Report runtime error to model
|
||||
* {@includeCode ./app.examples.ts#App_updateModelContext_reportError}
|
||||
* ```ts source="./app.examples.ts#App_updateModelContext_reportError"
|
||||
* try {
|
||||
* const _stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
* // ... use _stream for transcription
|
||||
* } catch (err) {
|
||||
* // Inform the model that the app is in a degraded state
|
||||
* await app.updateModelContext({
|
||||
* content: [
|
||||
* {
|
||||
* type: "text",
|
||||
* text: "Error: transcription unavailable",
|
||||
* },
|
||||
* ],
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @returns Promise that resolves when the context update is acknowledged
|
||||
*/
|
||||
|
|
@ -702,7 +905,14 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* @throws {Error} If the request times out or the connection is lost
|
||||
*
|
||||
* @example Open documentation link
|
||||
* {@includeCode ./app.examples.ts#App_openLink_documentation}
|
||||
* ```ts source="./app.examples.ts#App_openLink_documentation"
|
||||
* const { isError } = await app.openLink({ url: "https://docs.example.com" });
|
||||
* if (isError) {
|
||||
* // Host denied the request (e.g., blocked domain, user cancelled)
|
||||
* // Optionally show fallback: display URL for manual copy
|
||||
* console.warn("Link request denied");
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiOpenLinkRequest `McpUiOpenLinkRequest`} for request structure
|
||||
* @see {@link McpUiOpenLinkResult `McpUiOpenLinkResult`} for result structure
|
||||
|
|
@ -734,7 +944,14 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* @returns Result containing the actual display mode that was set
|
||||
*
|
||||
* @example Toggle display mode
|
||||
* {@includeCode ./app.examples.ts#App_requestDisplayMode_toggle}
|
||||
* ```ts source="./app.examples.ts#App_requestDisplayMode_toggle"
|
||||
* const ctx = app.getHostContext();
|
||||
* if (ctx?.availableDisplayModes?.includes("fullscreen")) {
|
||||
* const target = ctx.displayMode === "fullscreen" ? "inline" : "fullscreen";
|
||||
* const result = await app.requestDisplayMode({ mode: target });
|
||||
* console.log("Now in:", result.mode);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiRequestDisplayModeRequest `McpUiRequestDisplayModeRequest`} for request structure
|
||||
* @see {@link McpUiHostContext `McpUiHostContext`} for checking availableDisplayModes
|
||||
|
|
@ -762,7 +979,12 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* @param params - New width and height in pixels
|
||||
*
|
||||
* @example Manually notify host of size change
|
||||
* {@includeCode ./app.examples.ts#App_sendSizeChanged_manual}
|
||||
* ```ts source="./app.examples.ts#App_sendSizeChanged_manual"
|
||||
* app.sendSizeChanged({
|
||||
* width: 400,
|
||||
* height: 600,
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @returns Promise that resolves when the notification is sent
|
||||
*
|
||||
|
|
@ -789,7 +1011,20 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* @returns Cleanup function to disconnect the observer
|
||||
*
|
||||
* @example Manual setup for custom scenarios
|
||||
* {@includeCode ./app.examples.ts#App_setupAutoResize_manual}
|
||||
* ```ts source="./app.examples.ts#App_setupAutoResize_manual"
|
||||
* const app = new App(
|
||||
* { name: "MyApp", version: "1.0.0" },
|
||||
* {},
|
||||
* { autoResize: false },
|
||||
* );
|
||||
* await app.connect(transport);
|
||||
*
|
||||
* // Later, enable auto-resize manually
|
||||
* const cleanup = app.setupSizeChangedNotifications();
|
||||
*
|
||||
* // Clean up when done
|
||||
* cleanup();
|
||||
* ```
|
||||
*/
|
||||
setupSizeChangedNotifications() {
|
||||
let scheduled = false;
|
||||
|
|
@ -861,7 +1096,16 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
|||
* @throws {Error} If initialization fails or connection is lost
|
||||
*
|
||||
* @example Connect with PostMessageTransport
|
||||
* {@includeCode ./app.examples.ts#App_connect_withPostMessageTransport}
|
||||
* ```ts source="./app.examples.ts#App_connect_withPostMessageTransport"
|
||||
* const app = new App({ name: "MyApp", version: "1.0.0" }, {});
|
||||
*
|
||||
* try {
|
||||
* await app.connect(new PostMessageTransport(window.parent, window.parent));
|
||||
* console.log("Connected successfully!");
|
||||
* } catch (error) {
|
||||
* console.error("Failed to connect:", error);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiInitializeRequest `McpUiInitializeRequest`} for the initialization request structure
|
||||
* @see {@link McpUiInitializedNotification `McpUiInitializedNotification`} for the initialized notification
|
||||
|
|
|
|||
|
|
@ -24,10 +24,20 @@ import {
|
|||
* ## Usage
|
||||
*
|
||||
* **View**:
|
||||
* {@includeCode ./message-transport.examples.ts#PostMessageTransport_view}
|
||||
* ```ts source="./message-transport.examples.ts#PostMessageTransport_view"
|
||||
* const transport = new PostMessageTransport(window.parent, window.parent);
|
||||
* await app.connect(transport);
|
||||
* ```
|
||||
*
|
||||
* **Host**:
|
||||
* {@includeCode ./message-transport.examples.ts#PostMessageTransport_host}
|
||||
* ```ts source="./message-transport.examples.ts#PostMessageTransport_host"
|
||||
* const iframe = document.getElementById("app-iframe") as HTMLIFrameElement;
|
||||
* const transport = new PostMessageTransport(
|
||||
* iframe.contentWindow!,
|
||||
* iframe.contentWindow!,
|
||||
* );
|
||||
* await bridge.connect(transport);
|
||||
* ```
|
||||
*
|
||||
* @see {@link app!App.connect `App.connect`} for View usage
|
||||
* @see {@link app-bridge!AppBridge.connect `AppBridge.connect`} for Host usage
|
||||
|
|
@ -46,10 +56,18 @@ export class PostMessageTransport implements Transport {
|
|||
* `window.parent`. For hosts, pass `iframe.contentWindow`.
|
||||
*
|
||||
* @example View connecting to parent
|
||||
* {@includeCode ./message-transport.examples.ts#PostMessageTransport_constructor_view}
|
||||
* ```ts source="./message-transport.examples.ts#PostMessageTransport_constructor_view"
|
||||
* const transport = new PostMessageTransport(window.parent, window.parent);
|
||||
* ```
|
||||
*
|
||||
* @example Host connecting to iframe
|
||||
* {@includeCode ./message-transport.examples.ts#PostMessageTransport_constructor_host}
|
||||
* ```ts source="./message-transport.examples.ts#PostMessageTransport_constructor_host"
|
||||
* const iframe = document.getElementById("app-iframe") as HTMLIFrameElement;
|
||||
* const transport = new PostMessageTransport(
|
||||
* iframe.contentWindow!,
|
||||
* iframe.contentWindow!,
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
constructor(
|
||||
private eventTarget: Window = window.parent,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,19 @@
|
|||
* @module @modelcontextprotocol/ext-apps/react
|
||||
*
|
||||
* @example Basic React App
|
||||
* {@includeCode ./index.examples.tsx#index_basicReactApp}
|
||||
* ```tsx source="./index.examples.tsx#index_basicReactApp"
|
||||
* function MyApp() {
|
||||
* const { app, isConnected, error } = useApp({
|
||||
* appInfo: { name: "MyApp", version: "1.0.0" },
|
||||
* capabilities: {},
|
||||
* });
|
||||
*
|
||||
* if (error) return <div>Error: {error.message}</div>;
|
||||
* if (!isConnected) return <div>Connecting...</div>;
|
||||
*
|
||||
* return <div>Connected!</div>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export * from "./useApp";
|
||||
export * from "./useAutoResize";
|
||||
|
|
|
|||
|
|
@ -30,7 +30,17 @@ export interface UseAppOptions {
|
|||
* @param app - The newly created `App` instance
|
||||
*
|
||||
* @example Register an event handler
|
||||
* {@includeCode ./useApp.examples.tsx#useApp_registerHandler}
|
||||
* ```tsx source="./useApp.examples.tsx#useApp_registerHandler"
|
||||
* useApp({
|
||||
* appInfo: { name: "MyApp", version: "1.0.0" },
|
||||
* capabilities: {},
|
||||
* onAppCreated: (app) => {
|
||||
* app.ontoolresult = (result) => {
|
||||
* console.log("Tool result:", result);
|
||||
* };
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
onAppCreated?: (app: App) => void;
|
||||
}
|
||||
|
|
@ -70,7 +80,39 @@ export interface AppState {
|
|||
* timeouts, initialization handshake failures, or transport errors).
|
||||
*
|
||||
* @example Basic usage of useApp hook with common event handlers
|
||||
* {@includeCode ./useApp.examples.tsx#useApp_basicUsage}
|
||||
* ```tsx source="./useApp.examples.tsx#useApp_basicUsage"
|
||||
* function MyApp() {
|
||||
* const [hostContext, setHostContext] = useState<
|
||||
* McpUiHostContext | undefined
|
||||
* >(undefined);
|
||||
*
|
||||
* const { app, isConnected, error } = useApp({
|
||||
* appInfo: { name: "MyApp", version: "1.0.0" },
|
||||
* capabilities: {},
|
||||
* onAppCreated: (app) => {
|
||||
* app.ontoolinput = (input) => {
|
||||
* console.log("Tool input:", input);
|
||||
* };
|
||||
* app.ontoolresult = (result) => {
|
||||
* console.log("Tool result:", result);
|
||||
* };
|
||||
* app.ontoolcancelled = (params) => {
|
||||
* console.log("Tool cancelled:", params.reason);
|
||||
* };
|
||||
* app.onerror = (error) => {
|
||||
* console.log("Error:", error);
|
||||
* };
|
||||
* app.onhostcontextchanged = (params) => {
|
||||
* setHostContext((prev) => ({ ...prev, ...params }));
|
||||
* };
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* if (error) return <div>Error: {error.message}</div>;
|
||||
* if (!isConnected) return <div>Connecting...</div>;
|
||||
* return <div>Theme: {hostContext?.theme}</div>;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see {@link App.connect `App.connect`} for the underlying connection method
|
||||
* @see {@link useAutoResize `useAutoResize`} for manual auto-resize control when using custom App options
|
||||
|
|
|
|||
|
|
@ -20,7 +20,33 @@ import { App } from "../app";
|
|||
* cause unnecessary effect re-runs; omit this parameter.
|
||||
*
|
||||
* @example Manual App creation with custom auto-resize control
|
||||
* {@includeCode ./useAutoResize.examples.tsx#useAutoResize_manualApp}
|
||||
* ```tsx source="./useAutoResize.examples.tsx#useAutoResize_manualApp"
|
||||
* function MyComponent() {
|
||||
* // For custom App options, create App manually instead of using useApp
|
||||
* const [app, setApp] = useState<App | null>(null);
|
||||
* const [error, setError] = useState<Error | null>(null);
|
||||
*
|
||||
* useEffect(() => {
|
||||
* const myApp = new App(
|
||||
* { name: "MyApp", version: "1.0.0" },
|
||||
* {}, // capabilities
|
||||
* { autoResize: false }, // Disable default auto-resize
|
||||
* );
|
||||
*
|
||||
* const transport = new PostMessageTransport(window.parent, window.parent);
|
||||
* myApp
|
||||
* .connect(transport)
|
||||
* .then(() => setApp(myApp))
|
||||
* .catch((err) => setError(err));
|
||||
* }, []);
|
||||
*
|
||||
* // Add manual auto-resize control
|
||||
* useAutoResize(app);
|
||||
*
|
||||
* if (error) return <div>Connection failed: {error.message}</div>;
|
||||
* return <div>My content</div>;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see {@link App.setupSizeChangedNotifications `App.setupSizeChangedNotifications`} for the underlying implementation
|
||||
* @see {@link useApp `useApp`} which enables auto-resize by default
|
||||
|
|
|
|||
|
|
@ -15,10 +15,31 @@ import { McpUiTheme } from "../types";
|
|||
* @returns The current theme ("light" or "dark")
|
||||
*
|
||||
* @example Conditionally render based on theme
|
||||
* {@includeCode ./useDocumentTheme.examples.tsx#useDocumentTheme_conditionalRender}
|
||||
* ```tsx source="./useDocumentTheme.examples.tsx#useDocumentTheme_conditionalRender"
|
||||
* function MyApp() {
|
||||
* const theme = useDocumentTheme();
|
||||
*
|
||||
* return <div>{theme === "dark" ? <DarkIcon /> : <LightIcon />}</div>;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example Use with theme-aware styling
|
||||
* {@includeCode ./useDocumentTheme.examples.tsx#useDocumentTheme_themedButton}
|
||||
* ```tsx source="./useDocumentTheme.examples.tsx#useDocumentTheme_themedButton"
|
||||
* function ThemedButton() {
|
||||
* const theme = useDocumentTheme();
|
||||
*
|
||||
* return (
|
||||
* <button
|
||||
* style={{
|
||||
* background: theme === "dark" ? "#333" : "#fff",
|
||||
* color: theme === "dark" ? "#fff" : "#333",
|
||||
* }}
|
||||
* >
|
||||
* Click me
|
||||
* </button>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see {@link getDocumentTheme `getDocumentTheme`} for the underlying function
|
||||
* @see {@link applyDocumentTheme `applyDocumentTheme`} to set the theme
|
||||
|
|
|
|||
|
|
@ -26,7 +26,23 @@ import { McpUiHostContext } from "../types";
|
|||
* If provided, styles and theme will be applied immediately on mount.
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./useHostStyles.examples.tsx#useHostStyleVariables_basicUsage}
|
||||
* ```tsx source="./useHostStyles.examples.tsx#useHostStyleVariables_basicUsage"
|
||||
* function MyApp() {
|
||||
* const { app } = useApp({
|
||||
* appInfo: { name: "MyApp", version: "1.0.0" },
|
||||
* capabilities: {},
|
||||
* });
|
||||
*
|
||||
* // Apply host styles - pass initial context to apply styles from connect() immediately
|
||||
* useHostStyleVariables(app, app?.getHostContext());
|
||||
*
|
||||
* return (
|
||||
* <div style={{ background: "var(--color-background-primary)" }}>
|
||||
* Hello!
|
||||
* </div>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see {@link applyHostStyleVariables `applyHostStyleVariables`} for the underlying styles function
|
||||
* @see {@link applyDocumentTheme `applyDocumentTheme`} for the underlying theme function
|
||||
|
|
@ -89,7 +105,19 @@ export function useHostStyleVariables(
|
|||
* If provided, fonts will be applied immediately on mount.
|
||||
*
|
||||
* @example Basic usage with useApp
|
||||
* {@includeCode ./useHostStyles.examples.tsx#useHostFonts_basicUsage}
|
||||
* ```tsx source="./useHostStyles.examples.tsx#useHostFonts_basicUsage"
|
||||
* function MyApp() {
|
||||
* const { app } = useApp({
|
||||
* appInfo: { name: "MyApp", version: "1.0.0" },
|
||||
* capabilities: {},
|
||||
* });
|
||||
*
|
||||
* // Apply host fonts - pass initial context to apply fonts from connect() immediately
|
||||
* useHostFonts(app, app?.getHostContext());
|
||||
*
|
||||
* return <div style={{ fontFamily: "var(--font-sans)" }}>Hello!</div>;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see {@link applyHostFonts `applyHostFonts`} for the underlying fonts function
|
||||
* @see {@link useHostStyleVariables `useHostStyleVariables`} for applying style variables and theme
|
||||
|
|
@ -136,7 +164,23 @@ export function useHostFonts(
|
|||
* Pass `app?.getHostContext()` to apply styles immediately on mount.
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./useHostStyles.examples.tsx#useHostStyles_basicUsage}
|
||||
* ```tsx source="./useHostStyles.examples.tsx#useHostStyles_basicUsage"
|
||||
* function MyApp() {
|
||||
* const { app } = useApp({
|
||||
* appInfo: { name: "MyApp", version: "1.0.0" },
|
||||
* capabilities: {},
|
||||
* });
|
||||
*
|
||||
* // Apply all host styles - pass initial context to apply styles from connect() immediately
|
||||
* useHostStyles(app, app?.getHostContext());
|
||||
*
|
||||
* return (
|
||||
* <div style={{ background: "var(--color-background-primary)" }}>
|
||||
* Hello!
|
||||
* </div>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see {@link useHostStyleVariables `useHostStyleVariables`} for style variables and theme only
|
||||
* @see {@link useHostFonts `useHostFonts`} for fonts only
|
||||
|
|
|
|||
|
|
@ -8,7 +8,27 @@
|
|||
* @module server-helpers
|
||||
*
|
||||
* @example
|
||||
* {@includeCode ./index.examples.ts#index_overview}
|
||||
* ```ts source="./index.examples.ts#index_overview"
|
||||
* // Register a tool that displays a view
|
||||
* registerAppTool(
|
||||
* server,
|
||||
* "weather",
|
||||
* {
|
||||
* description: "Get weather forecast",
|
||||
* _meta: { ui: { resourceUri: "ui://weather/view.html" } },
|
||||
* },
|
||||
* toolCallback,
|
||||
* );
|
||||
*
|
||||
* // Register the HTML resource the tool references
|
||||
* registerAppResource(
|
||||
* server,
|
||||
* "Weather View",
|
||||
* "ui://weather/view.html",
|
||||
* {},
|
||||
* readCallback,
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
|
||||
import {
|
||||
|
|
@ -115,13 +135,67 @@ export interface McpUiAppResourceConfig extends ResourceMetadata {
|
|||
* @param cb - Tool handler function
|
||||
*
|
||||
* @example Basic usage
|
||||
* {@includeCode ./index.examples.ts#registerAppTool_basicUsage}
|
||||
* ```ts source="./index.examples.ts#registerAppTool_basicUsage"
|
||||
* registerAppTool(
|
||||
* server,
|
||||
* "get-weather",
|
||||
* {
|
||||
* title: "Get Weather",
|
||||
* description: "Get current weather for a location",
|
||||
* inputSchema: { location: z.string() },
|
||||
* _meta: {
|
||||
* ui: { resourceUri: "ui://weather/view.html" },
|
||||
* },
|
||||
* },
|
||||
* async (args) => {
|
||||
* const weather = await fetchWeather(args.location);
|
||||
* return { content: [{ type: "text", text: JSON.stringify(weather) }] };
|
||||
* },
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @example Tool visible to model but not callable by UI
|
||||
* {@includeCode ./index.examples.ts#registerAppTool_modelOnlyVisibility}
|
||||
* ```ts source="./index.examples.ts#registerAppTool_modelOnlyVisibility"
|
||||
* registerAppTool(
|
||||
* server,
|
||||
* "show-cart",
|
||||
* {
|
||||
* description: "Display the user's shopping cart",
|
||||
* _meta: {
|
||||
* ui: {
|
||||
* resourceUri: "ui://shop/cart.html",
|
||||
* visibility: ["model"],
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* async () => {
|
||||
* const cart = await getCart();
|
||||
* return { content: [{ type: "text", text: JSON.stringify(cart) }] };
|
||||
* },
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @example Tool hidden from model, only callable by UI
|
||||
* {@includeCode ./index.examples.ts#registerAppTool_appOnlyVisibility}
|
||||
* ```ts source="./index.examples.ts#registerAppTool_appOnlyVisibility"
|
||||
* registerAppTool(
|
||||
* server,
|
||||
* "update-quantity",
|
||||
* {
|
||||
* description: "Update item quantity in cart",
|
||||
* inputSchema: { itemId: z.string(), quantity: z.number() },
|
||||
* _meta: {
|
||||
* ui: {
|
||||
* resourceUri: "ui://shop/cart.html",
|
||||
* visibility: ["app"],
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* async ({ itemId, quantity }) => {
|
||||
* const cart = await updateCartItem(itemId, quantity);
|
||||
* return { content: [{ type: "text", text: JSON.stringify(cart) }] };
|
||||
* },
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @see {@link registerAppResource `registerAppResource`} to register the HTML resource referenced by the tool
|
||||
*/
|
||||
|
|
@ -170,10 +244,54 @@ export function registerAppTool<
|
|||
* @param readCallback - Callback that returns the resource contents
|
||||
*
|
||||
* @example Basic usage
|
||||
* {@includeCode ./index.examples.ts#registerAppResource_basicUsage}
|
||||
* ```ts source="./index.examples.ts#registerAppResource_basicUsage"
|
||||
* registerAppResource(
|
||||
* server,
|
||||
* "Weather View",
|
||||
* "ui://weather/view.html",
|
||||
* {
|
||||
* description: "Interactive weather display",
|
||||
* },
|
||||
* async () => ({
|
||||
* contents: [
|
||||
* {
|
||||
* uri: "ui://weather/view.html",
|
||||
* mimeType: RESOURCE_MIME_TYPE,
|
||||
* text: await fs.readFile("dist/view.html", "utf-8"),
|
||||
* },
|
||||
* ],
|
||||
* }),
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @example With CSP configuration for external domains
|
||||
* {@includeCode ./index.examples.ts#registerAppResource_withCsp}
|
||||
* ```ts source="./index.examples.ts#registerAppResource_withCsp"
|
||||
* registerAppResource(
|
||||
* server,
|
||||
* "Music Player",
|
||||
* "ui://music/player.html",
|
||||
* {
|
||||
* description: "Audio player with external soundfonts",
|
||||
* },
|
||||
* async () => ({
|
||||
* contents: [
|
||||
* {
|
||||
* uri: "ui://music/player.html",
|
||||
* mimeType: RESOURCE_MIME_TYPE,
|
||||
* text: musicPlayerHtml,
|
||||
* _meta: {
|
||||
* ui: {
|
||||
* csp: {
|
||||
* resourceDomains: ["https://cdn.example.com"], // For scripts/styles/images
|
||||
* connectDomains: ["https://api.example.com"], // For fetch/WebSocket
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* ],
|
||||
* }),
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @see {@link registerAppTool `registerAppTool`} to register tools that reference this resource
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ import { McpUiStyles, McpUiTheme } from "./types";
|
|||
* @returns The current theme ("light" or "dark")
|
||||
*
|
||||
* @example Check current theme
|
||||
* {@includeCode ./styles.examples.ts#getDocumentTheme_checkCurrent}
|
||||
* ```ts source="./styles.examples.ts#getDocumentTheme_checkCurrent"
|
||||
* const theme = getDocumentTheme();
|
||||
* document.body.classList.toggle("dark", theme === "dark");
|
||||
* ```
|
||||
*
|
||||
* @see {@link applyDocumentTheme `applyDocumentTheme`} to set the theme
|
||||
* @see {@link McpUiTheme `McpUiTheme`} for the theme type
|
||||
|
|
@ -39,7 +42,22 @@ export function getDocumentTheme(): McpUiTheme {
|
|||
* @param theme - The theme to apply ("light" or "dark")
|
||||
*
|
||||
* @example Apply theme from host context
|
||||
* {@includeCode ./styles.examples.ts#applyDocumentTheme_fromHostContext}
|
||||
* ```ts source="./styles.examples.ts#applyDocumentTheme_fromHostContext"
|
||||
* // Apply when host context changes
|
||||
* app.onhostcontextchanged = (params) => {
|
||||
* if (params.theme) {
|
||||
* applyDocumentTheme(params.theme);
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* // Apply initial theme after connecting
|
||||
* app.connect().then(() => {
|
||||
* const ctx = app.getHostContext();
|
||||
* if (ctx?.theme) {
|
||||
* applyDocumentTheme(ctx.theme);
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example Use with CSS selectors
|
||||
* ```css
|
||||
|
|
@ -72,10 +90,35 @@ export function applyDocumentTheme(theme: McpUiTheme): void {
|
|||
* @param root - The element to apply styles to (defaults to `document.documentElement`)
|
||||
*
|
||||
* @example Apply style variables from host context
|
||||
* {@includeCode ./styles.examples.ts#applyHostStyleVariables_fromHostContext}
|
||||
* ```ts source="./styles.examples.ts#applyHostStyleVariables_fromHostContext"
|
||||
* // Use CSS variables in your styles
|
||||
* document.body.style.background = "var(--color-background-primary)";
|
||||
*
|
||||
* // Apply when host context changes
|
||||
* app.onhostcontextchanged = (params) => {
|
||||
* if (params.styles?.variables) {
|
||||
* applyHostStyleVariables(params.styles.variables);
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* // Apply initial styles after connecting
|
||||
* app.connect().then(() => {
|
||||
* const ctx = app.getHostContext();
|
||||
* if (ctx?.styles?.variables) {
|
||||
* applyHostStyleVariables(ctx.styles.variables);
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example Apply to a specific element
|
||||
* {@includeCode ./styles.examples.ts#applyHostStyleVariables_toElement}
|
||||
* ```ts source="./styles.examples.ts#applyHostStyleVariables_toElement"
|
||||
* app.onhostcontextchanged = (params) => {
|
||||
* const container = document.getElementById("app-root");
|
||||
* if (container && params.styles?.variables) {
|
||||
* applyHostStyleVariables(params.styles.variables, container);
|
||||
* }
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @example Use host style variables in CSS
|
||||
* ```css
|
||||
|
|
@ -118,13 +161,44 @@ export function applyHostStyleVariables(
|
|||
* @param fontCss - CSS string containing `@font-face` rules and/or `@import` statements
|
||||
*
|
||||
* @example Apply fonts from host context
|
||||
* {@includeCode ./styles.examples.ts#applyHostFonts_fromHostContext}
|
||||
* ```ts source="./styles.examples.ts#applyHostFonts_fromHostContext"
|
||||
* // Apply when host context changes
|
||||
* app.onhostcontextchanged = (params) => {
|
||||
* if (params.styles?.css?.fonts) {
|
||||
* applyHostFonts(params.styles.css.fonts);
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* // Apply initial fonts after connecting
|
||||
* app.connect().then(() => {
|
||||
* const ctx = app.getHostContext();
|
||||
* if (ctx?.styles?.css?.fonts) {
|
||||
* applyHostFonts(ctx.styles.css.fonts);
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example Host providing self-hosted fonts
|
||||
* {@includeCode ./styles.examples.ts#applyHostFonts_selfHosted}
|
||||
* ```ts source="./styles.examples.ts#applyHostFonts_selfHosted"
|
||||
* // Example of what a host might provide:
|
||||
* const fontCss = `
|
||||
* @font-face {
|
||||
* font-family: "Anthropic Sans";
|
||||
* src: url("https://assets.anthropic.com/.../Regular.otf") format("opentype");
|
||||
* font-weight: 400;
|
||||
* }
|
||||
* `;
|
||||
* applyHostFonts(fontCss);
|
||||
* ```
|
||||
*
|
||||
* @example Host providing Google Fonts
|
||||
* {@includeCode ./styles.examples.ts#applyHostFonts_googleFonts}
|
||||
* ```ts source="./styles.examples.ts#applyHostFonts_googleFonts"
|
||||
* // Example of what a host might provide:
|
||||
* const fontCss = `
|
||||
* @import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
|
||||
* `;
|
||||
* applyHostFonts(fontCss);
|
||||
* ```
|
||||
*
|
||||
* @example Use host fonts in CSS
|
||||
* ```css
|
||||
|
|
|
|||
Loading…
Reference in a new issue