ext-apps/src/app.examples.ts
Olivier Chafik 3fb521b71f
spec: Add tool registration for Apps, to be called by Host (WebMCP-style!) (#72)
* feat: Add tool registration and bidirectional tool support

This PR adds comprehensive tool support for MCP Apps, enabling apps
to register their own tools and handle tool calls from the host.

- Add `registerTool()` method for registering tools with input/output schemas
- Add `oncalltool` setter for handling tool call requests from host
- Add `onlisttools` setter for handling tool list requests from host
- Add `sendToolListChanged()` for notifying host of tool updates
- Registered tools support enable/disable/update/remove operations

- Add `sendCallTool()` method for calling tools on the app
- Add `sendListTools()` method for listing available app tools
- Fix: Use correct ListToolsResultSchema (was ListToolsRequestSchema)

- Add comprehensive tests for tool registration lifecycle
- Add tests for input/output schema validation
- Add tests for bidirectional tool call communication
- Add tests for tool list change notifications
- All 27 tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* nits

* Update apps.mdx

* feat: Add automatic request handlers for app tool registration

Implement automatic `oncalltool` and `onlisttools` handlers that are
initialized when apps register tools. This removes the need for manual
handler setup and ensures tools work seamlessly out of the box.

- Add automatic `oncalltool` handler that routes calls to registered tools
- Add automatic `onlisttools` handler that returns full Tool objects with JSON schemas
- Convert Zod schemas to MCP-compliant JSON Schema using `zod-to-json-schema`
- Add 27 comprehensive tests covering automatic handlers and tool lifecycle
- Test coverage includes error handling, schema validation, and multi-app isolation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* type updates

* fix: Ensure tools/list returns valid JSON Schema for all tools

- Always return inputSchema as object (never undefined)
- Keep filter for enabled tools only in list
- Update test to match behavior (only enabled tools in list)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* type updates

* rm zod-to-json-schema

* fix: Update RegisteredTool to use 'handler' instead of 'callback' (SDK API change)

* refactor: Rename sendCallTool/sendListTools to callTool/listTools

Avoid double-verb naming pattern for consistency with existing API.

* feat: Add screenshot and click support to SDK

- Add McpUiScreenshotRequest/Result and McpUiClickRequest/Result types
- Add onscreenshot and onclick handlers to App class
- Add screenshot() and click() methods to AppBridge class
- Generate updated Zod schemas

* feat(map-server): Enable registerTool with navigate-to and get-current-view

- Uncomment and fix the navigate-to tool for animated navigation
- Add get-current-view tool to query camera position and bounding box
- Add flyToBoundingBox function for smooth camera animation
- Add setLabel function for displaying location labels

* feat(pdf-server): Add widget interaction tools

- get-document-info: Get title, current page, total pages, zoom level
- go-to-page: Navigate to a specific page
- get-page-text: Extract text from a page
- search-text: Search for text across the document
- set-zoom: Adjust zoom level

* feat(shadertoy-server): Add widget interaction tools

* feat(examples): Add widget interaction tools to budget-allocator, shadertoy, wiki-explorer, and threejs

budget-allocator:
- get-allocations: Get current budget allocations
- set-allocation: Set allocation for a category
- set-total-budget: Adjust total budget
- set-company-stage: Change stage for benchmarks
- get-benchmark-comparison: Compare against benchmarks

shadertoy:
- set-shader-source: Update shader source code
- get-shader-info: Get shader source and compilation status
- Sends errors via updateModelContext

wiki-explorer:
- search-article: Search for Wikipedia articles
- get-current-article: Get current article info
- highlight-node: Highlight a graph node
- get-visible-nodes: List visible nodes

threejs:
- set-scene-source: Update the Three.js scene source code
- get-scene-info: Get current scene state and any errors
- Sends syntax errors to model via updateModelContext

* feat: Add expand-node tool to wiki-explorer and update tool descriptions

Wiki Explorer:
- Add expand-node tool - the critical missing tool for graph exploration
- Claude can now programmatically expand nodes to discover linked articles

Server descriptions updated to mention widget tools:
- map-server: navigate-to, get-current-view
- pdf-server: go-to-page, get-page-text, search-text, set-zoom, get-document-info
- budget-allocator: get-allocations, set-allocation, set-total-budget, etc.
- shadertoy: set-shader-source, get-shader-info
- wiki-explorer: expand-node, search-article, highlight-node, etc.

All descriptions now mention 'Use list_widget_tools to discover available actions.'

* refactor: Simplify tool descriptions - remove client implementation details

The server tool descriptions now just mention that widgets are interactive
and can be controlled, without teaching the model about list_widget_tools
(which is the client's responsibility to teach).

Before: 'The widget exposes tools: X, Y, Z. Use list_widget_tools to discover...'
After:  'The widget is interactive and exposes tools for X and Y.'

* Remove ui/screenshot and ui/click protocol methods

These will be proposed separately from the app tool registration changes.

* fix(app): auto-register tools capability in registerTool

When registerTool is called before connect() on an App created without
explicit tools capability, setRequestHandler's capability assertion
would throw, breaking app initialization at module load.

Auto-register { tools: { listChanged: true } } on first registerTool
call (pre-connect only), mirroring McpServer.registerTool behavior.

Fixes pdf-annotations e2e failures where the PDF canvas never rendered
because registerTool threw at module scope.

* pdf-server: replace app-tools with 1:1 interact command mapping

Converts the 5 placeholder app-registered tools into 12 tools that map
directly to the server's interact commands (navigate, search, find,
search_navigate, zoom, add_annotations, update_annotations,
remove_annotations, highlight_text, fill_form, get_text, get_screenshot)
plus the existing get-document-info.

Implementation stays DRY by dispatching through the existing
processCommands() handler — each tool callback constructs a PdfCommand
and runs it via a small runCommand() wrapper. For get_text and
get_screenshot, the page-data collection is extracted from
handleGetPages into a shared collectPageData() helper so results can
be returned directly instead of round-tripping through the server.

Tool names and zod schemas mirror the interact command parameter shapes
from server.ts so the model sees the same surface whether it goes
through interact or app-tools. The server-side interact tool is
unchanged and remains available for hosts without app-tool support.

* chore: drop unused test vars and stale Request/Result imports

* docs(spec): address review feedback on app tool registration

- Add outputSchema to Tool struct (matches SDK + core MCP)
- Reword "black box" bullets: drop stale screenshots reference, clarify no DOM access
- Clarify push vs pull contrast with explicit setWidgetState + ui/update-model-context refs
- Add Task-reference future-work bullet for tools outliving render lifecycle

* docs(spec): clarify app DOM is not host-introspectable; frame tools as pull complementing update-model-context push

* fix(app): emit title and omit absent outputSchema in tools/list

* feat(app): accept Standard Schema in registerTool/registerAppTool

App.registerTool and registerAppTool now accept any schema implementing
the Standard Schema spec (Zod ≥3.25, ArkType, Valibot, …) instead of
zod-only. Uses ~standard.validate / ~standard.jsonSchema directly, so
app.ts no longer imports zod at runtime — zod is now an optional peer.

Mirrors the StandardSchemaWithJSON type from
modelcontextprotocol/typescript-sdk#1689 so bumping to SDK v2 is a
drop-in import swap. Adds @standard-schema/spec (types-only) and a
hand-rolled non-zod test.

* docs(spec): reference core MCP Tasks for long-running app tools

App tools/call MAY be task-augmented per the core MCP Tasks utility
(spec 2025-11-25). Adds a Long-Running App Tools subsection: hosts proxy
tasks/* while the app is mounted; apps SHOULD delegate long-lived work
to the server so the task survives iframe teardown. Removes the stale
'future extension' bullet — Tasks is shipped.

* docs(spec): note Schema Validation accepts any Standard Schema library

* chore: clean unused-var and no-op-await diagnostics in app.ts and tests

* refactor(app): fold error prefix into validateStandardSchema; clarify registerAppTool runtime caveat

* chore(typedoc): mark RequestHandlerExtra as intentionally not exported

AppToolCallback (introduced with the Standard Schema migration) references
the local RequestHandlerExtra alias in its parameter type. The alias is a
private Parameters<> derivation and isn't part of the public API surface,
so add it to intentionallyNotExported alongside AppOptions/MethodSchema.

* fix(app): restore zod v3.25 compat via lazy z.toJSONSchema fallback

zod v3.25.x implements ~standard.validate but not ~standard.jsonSchema,
so constraining to StandardSchemaWithJSON broke the ^3.25.0 peer range.
Widen registerTool to StandardSchemaV1 and fall back to a lazy zod/v4
import for serialization when ~standard.jsonSchema is absent and
vendor==='zod'. Non-zod schemas without jsonSchema get a clear error.

* docs(spec): slim Long-Running App Tools to a pointer into core MCP Tasks

* chore: add TODO(sdk-v2) marker in standard-schema.ts

* fix(app): align registerTool runtime with declared types

- Handler reads registeredTool.{input,output}Schema so update() takes effect
- Call cb(extra) when no inputSchema, matching AppToolCallback<undefined>
- Throw on duplicate name; guard list_changed pre-connect; notify on register
- Drop misleading zod optional peer (still required by generated/schema.ts)
- examples: fix pdf search/find stale match count; threejs set-scene-source re-render
- spec: Zod → Standard Schema; clarify tools availability via tools/list after init

* feat(app): type structuredContent from outputSchema in registerTool

AppToolCallback gains an Out param: when outputSchema is provided,
structuredContent is required and typed via StandardSchemaV1.InferOutput,
with an `isError: true` escape hatch. Without outputSchema, return type
is unchanged (CallToolResult). Uses intersection rather than Omit since
CallToolResult's index signature swallows Omit's known keys.

Also drops the `as any` casts on z.object(...) in tests — zod 4 satisfies
StandardSchemaV1 directly.

* fix(app): registerTool edge cases + example tool correctness

App.registerTool:
- Skip outputSchema validation when result.isError (matches AppToolResult type)
- Pin callback arity to original config.inputSchema; only the validation schema is mutable via update()
- Default missing tools/call arguments to {}
- remove() is a no-op if the handle is stale (re-registered or already removed)
- Gate list_changed notifications on the listChanged capability

Examples:
- threejs: propagate executeThreeCode failures to onSceneError/onSceneRendering; catch updateModelContext rejection
- wiki-explorer: search-article preserves graph on no-match
- budget-allocator: fix category/stage values in tool descriptions

* feat(app,app-bridge): guard requests sent before handshake completes (#620)

* feat(app,app-bridge): guard requests sent before handshake completes

Calling host-bound methods before the ui/initialize →
ui/notifications/initialized handshake completes can race the handshake
on strict hosts and leave the iframe permanently hidden.

App (View side):
- All 8 host-bound methods (callServerTool, readServerResource,
  listServerResources, sendMessage, updateModelContext, openLink,
  downloadFile, requestDisplayMode) now check that connect() has sent
  the initialized notification.
- New AppOptions.strict (default false): console.error when false,
  throw when true. AppOptions is now exported.
- useApp() forwards strict to the App constructor.
- Flag resets on reconnect.

AppBridge (Host side):
- replaceRequestHandler is overridden to wrap every host-bound handler
  with a console.warn if the request arrives before
  ui/notifications/initialized. Never throws. ui/initialize and ping
  use setRequestHandler directly and are exempt; notifications are
  unaffected.
- Catches Views that hand-roll postMessage without the SDK.
- Flag resets on reconnect.

Refs anthropics/claude-ai-mcp#61, anthropics/claude-ai-mcp#149.

* fix(docs): drop AppOptions from intentionallyNotExported now that it is exported

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-21 13:46:13 +01:00

635 lines
18 KiB
TypeScript

/**
* Type-checked examples for {@link App `App`} and constants in {@link ./app.ts `app.ts`}.
*
* These examples are included in the API documentation via `@includeCode` tags.
* Each function's region markers define the code snippet that appears in the docs.
*
* @module
*/
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import type {
McpServer,
ToolCallback,
} from "@modelcontextprotocol/sdk/server/mcp.js";
import {
App,
PostMessageTransport,
RESOURCE_URI_META_KEY,
McpUiToolMeta,
} from "./app.js";
import { registerAppTool } from "./server/index.js";
/**
* Example: Modern format for registering tools with UI (recommended).
*/
function RESOURCE_URI_META_KEY_modernFormat(
server: McpServer,
handler: ToolCallback,
) {
//#region RESOURCE_URI_META_KEY_modernFormat
// Preferred: Use registerAppTool with nested ui.resourceUri
registerAppTool(
server,
"weather",
{
description: "Get weather forecast",
_meta: {
ui: { resourceUri: "ui://weather/forecast" },
},
},
handler,
);
//#endregion RESOURCE_URI_META_KEY_modernFormat
}
/**
* Example: Legacy format using RESOURCE_URI_META_KEY (deprecated).
*/
function RESOURCE_URI_META_KEY_legacyFormat(
server: McpServer,
handler: ToolCallback,
) {
//#region RESOURCE_URI_META_KEY_legacyFormat
// Deprecated: Direct use of RESOURCE_URI_META_KEY
server.registerTool(
"weather",
{
description: "Get weather forecast",
_meta: {
[RESOURCE_URI_META_KEY]: "ui://weather/forecast",
},
},
handler,
);
//#endregion RESOURCE_URI_META_KEY_legacyFormat
}
/**
* Example: How hosts check for RESOURCE_URI_META_KEY metadata (must support both formats).
*/
function RESOURCE_URI_META_KEY_hostSide(tool: Tool) {
//#region RESOURCE_URI_META_KEY_hostSide
// Hosts should check both modern and legacy formats
const meta = tool._meta;
const uiMeta = meta?.ui as McpUiToolMeta | undefined;
const legacyUri = meta?.[RESOURCE_URI_META_KEY] as string | undefined;
const uiUri = uiMeta?.resourceUri ?? legacyUri;
if (typeof uiUri === "string" && uiUri.startsWith("ui://")) {
// Fetch the resource and display the UI
}
//#endregion RESOURCE_URI_META_KEY_hostSide
}
/**
* Example: App constructor with appInfo, capabilities, and options.
*/
function App_constructor_basic() {
//#region App_constructor_basic
const app = new App(
{ name: "MyApp", version: "1.0.0" },
{ tools: { listChanged: true } }, // capabilities
{ autoResize: true }, // options
);
//#endregion App_constructor_basic
return app;
}
/**
* Example: Basic usage of the App class with PostMessageTransport.
*/
async function App_basicUsage() {
//#region 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();
//#endregion App_basicUsage
}
/**
* Example: Check host capabilities after connection.
*/
async function App_getHostCapabilities_checkAfterConnection(app: App) {
//#region App_getHostCapabilities_checkAfterConnection
await app.connect();
if (app.getHostCapabilities()?.serverTools) {
console.log("Host supports server tool calls");
}
//#endregion App_getHostCapabilities_checkAfterConnection
}
/**
* Example: Log host information after connection.
*/
async function App_getHostVersion_logAfterConnection(
app: App,
transport: PostMessageTransport,
) {
//#region App_getHostVersion_logAfterConnection
await app.connect(transport);
const { name, version } = app.getHostVersion() ?? {};
console.log(`Connected to ${name} v${version}`);
//#endregion App_getHostVersion_logAfterConnection
}
/**
* Example: Access host context after connection.
*/
async function App_getHostContext_accessAfterConnection(
app: App,
transport: PostMessageTransport,
) {
//#region 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);
}
//#endregion App_getHostContext_accessAfterConnection
}
/**
* Example: Using the ontoolinput setter (simpler approach).
*/
async function App_ontoolinput_setter(app: App) {
//#region 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();
//#endregion App_ontoolinput_setter
}
/**
* Example: Progressive rendering of tool arguments using ontoolinputpartial.
*/
function App_ontoolinputpartial_progressiveRendering(app: App) {
//#region App_ontoolinputpartial_progressiveRendering
const codePreview = document.querySelector<HTMLPreElement>("#code-preview")!;
const canvas = document.querySelector<HTMLCanvasElement>("#canvas")!;
app.ontoolinputpartial = (params) => {
codePreview.textContent = (params.arguments?.code as string) ?? "";
codePreview.style.display = "block";
canvas.style.display = "none";
};
app.ontoolinput = (params) => {
codePreview.style.display = "none";
canvas.style.display = "block";
render(params.arguments?.code as string);
};
//#endregion App_ontoolinputpartial_progressiveRendering
}
// Stub for App_ontoolinputpartial_progressiveRendering example
declare function render(code: string): void;
/**
* Example: Display tool execution results using ontoolresult.
*/
function App_ontoolresult_displayResults(app: App) {
//#region 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);
}
};
//#endregion App_ontoolresult_displayResults
}
/**
* Example: Handle tool cancellation notifications.
*/
function App_ontoolcancelled_handleCancellation(app: App) {
//#region App_ontoolcancelled_handleCancellation
app.ontoolcancelled = (params) => {
console.log("Tool cancelled:", params.reason);
// Update your UI to show cancellation state
};
//#endregion App_ontoolcancelled_handleCancellation
}
/**
* Example: Respond to theme changes using onhostcontextchanged.
*/
function App_onhostcontextchanged_respondToTheme(app: App) {
//#region App_onhostcontextchanged_respondToTheme
app.onhostcontextchanged = (ctx) => {
if (ctx.theme === "dark") {
document.body.classList.add("dark-theme");
} else {
document.body.classList.remove("dark-theme");
}
};
//#endregion App_onhostcontextchanged_respondToTheme
}
/**
* Example: Respond to display mode changes using onhostcontextchanged.
*/
function App_onhostcontextchanged_respondToDisplayMode(app: App) {
//#region App_onhostcontextchanged_respondToDisplayMode
app.onhostcontextchanged = (ctx) => {
// Adjust to current display mode
if (ctx.displayMode) {
const container = document.getElementById("main")!;
const isFullscreen = ctx.displayMode === "fullscreen";
container.classList.toggle("fullscreen", isFullscreen);
}
// Adjust display mode controls
if (ctx.availableDisplayModes) {
const fullscreenBtn = document.getElementById("fullscreen-btn")!;
const canFullscreen = ctx.availableDisplayModes.includes("fullscreen");
fullscreenBtn.style.display = canFullscreen ? "block" : "none";
}
};
//#endregion App_onhostcontextchanged_respondToDisplayMode
}
/**
* Example: Perform cleanup before teardown.
*/
function App_onteardown_performCleanup(app: App) {
//#region App_onteardown_performCleanup
app.onteardown = async () => {
await saveState();
closeConnections();
console.log("App ready for teardown");
return {};
};
//#endregion App_onteardown_performCleanup
}
// Stubs for example
declare function saveState(): Promise<void>;
declare function closeConnections(): void;
/**
* Example: Handle tool calls from the host.
*/
function App_oncalltool_handleFromHost(app: App) {
//#region 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}`);
};
//#endregion App_oncalltool_handleFromHost
}
/**
* Example: Return available tools from the onlisttools handler.
*/
function App_onlisttools_returnTools(app: App) {
//#region App_onlisttools_returnTools
app.onlisttools = async (params, extra) => {
return {
tools: [
{
name: "greet",
description: "Greet the user",
inputSchema: { type: "object" as const },
},
{
name: "calculate",
description: "Perform a calculation",
inputSchema: { type: "object" as const },
},
{
name: "format",
description: "Format text",
inputSchema: { type: "object" as const },
},
],
};
};
//#endregion App_onlisttools_returnTools
}
/**
* Example: Fetch updated weather data using callServerTool.
*/
async function App_callServerTool_fetchWeather(app: App) {
//#region 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);
}
//#endregion App_callServerTool_fetchWeather
}
/**
* Example: Simple LLM completion via host sampling.
*/
async function App_createSamplingMessage_simple(app: App) {
//#region App_createSamplingMessage_simple
const result = await app.createSamplingMessage({
messages: [
{
role: "user",
content: { type: "text", text: "Summarize this in one line." },
},
],
maxTokens: 100,
});
console.log(result.content);
//#endregion App_createSamplingMessage_simple
}
/**
* Example: Agentic loop with tools (requires host sampling.tools capability).
*/
async function App_createSamplingMessage_withTools(
app: App,
messages: import("@modelcontextprotocol/sdk/types.js").SamplingMessage[],
) {
//#region App_createSamplingMessage_withTools
if (!app.getHostCapabilities()?.sampling?.tools) return;
const result = await app.createSamplingMessage({
messages,
maxTokens: 1024,
tools: [
{
name: "get_weather",
description: "Get the current weather",
inputSchema: {
type: "object",
properties: { city: { type: "string" } },
},
},
],
});
if (result.stopReason === "toolUse") {
// result.content may be an array containing tool_use blocks
}
//#endregion App_createSamplingMessage_withTools
}
/**
* Example: Read a video resource and play it.
*/
async function App_readServerResource_playVideo(
app: App,
videoElement: HTMLVideoElement,
) {
//#region App_readServerResource_playVideo
try {
const result = await app.readServerResource({
uri: "videos://bunny-1mb",
});
const content = result.contents[0];
if (content && "blob" in content) {
const binary = Uint8Array.from(atob(content.blob), (c) =>
c.charCodeAt(0),
);
const url = URL.createObjectURL(
new Blob([binary], { type: content.mimeType || "video/mp4" }),
);
videoElement.src = url;
videoElement.play();
}
} catch (error) {
console.error("Failed to read resource:", error);
}
//#endregion App_readServerResource_playVideo
}
/**
* Example: Discover available videos and build a picker UI.
*/
async function App_listServerResources_buildPicker(
app: App,
selectElement: HTMLSelectElement,
) {
//#region App_listServerResources_buildPicker
try {
const result = await app.listServerResources();
const videoResources = result.resources.filter((r) =>
r.mimeType?.startsWith("video/"),
);
videoResources.forEach((resource) => {
const option = document.createElement("option");
option.value = resource.uri;
option.textContent = resource.description || resource.name;
selectElement.appendChild(option);
});
} catch (error) {
console.error("Failed to list resources:", error);
}
//#endregion App_listServerResources_buildPicker
}
/**
* Example: Send a text message from user interaction.
*/
async function App_sendMessage_textFromInteraction(app: App) {
//#region 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
}
//#endregion App_sendMessage_textFromInteraction
}
/**
* Example: Send follow-up message after offloading large data to model context.
*/
async function App_sendMessage_withLargeContext(
app: App,
fullTranscript: string,
speakerNames: string[],
) {
//#region 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" }],
});
//#endregion App_sendMessage_withLargeContext
}
/**
* Example: Log app state for debugging.
*/
function App_sendLog_debugState(app: App) {
//#region App_sendLog_debugState
app.sendLog({
level: "info",
data: "Weather data refreshed",
logger: "WeatherApp",
});
//#endregion App_sendLog_debugState
}
/**
* Example: Update model context with current app state.
*/
async function App_updateModelContext_appState(
app: App,
itemList: string[],
totalCost: string,
currency: string,
) {
//#region 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 }],
});
//#endregion App_updateModelContext_appState
}
/**
* Example: Report runtime error to model.
*/
async function App_updateModelContext_reportError(app: App) {
//#region 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",
},
],
});
}
//#endregion App_updateModelContext_reportError
}
/**
* Example: Open documentation link.
*/
async function App_openLink_documentation(app: App) {
//#region 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");
}
//#endregion App_openLink_documentation
}
/**
* Example: Toggle between inline and fullscreen display modes.
*/
async function App_requestDisplayMode_toggle(app: App) {
//#region App_requestDisplayMode_toggle
const container = document.getElementById("main")!;
const ctx = app.getHostContext();
const newMode = ctx?.displayMode === "inline" ? "fullscreen" : "inline";
if (ctx?.availableDisplayModes?.includes(newMode)) {
const result = await app.requestDisplayMode({ mode: newMode });
container.classList.toggle("fullscreen", result.mode === "fullscreen");
}
//#endregion App_requestDisplayMode_toggle
}
/**
* Example: Manually notify host of size change.
*/
function App_sendSizeChanged_manual(app: App) {
//#region App_sendSizeChanged_manual
app.sendSizeChanged({
width: 400,
height: 600,
});
//#endregion App_sendSizeChanged_manual
}
/**
* Example: Manual setup for custom scenarios (setupSizeChangedNotifications).
*/
async function App_setupAutoResize_manual(transport: PostMessageTransport) {
//#region 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();
//#endregion App_setupAutoResize_manual
}
/**
* Example: Connect with PostMessageTransport.
*/
async function App_connect_withPostMessageTransport() {
//#region 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);
}
//#endregion App_connect_withPostMessageTransport
}