docs: standardize UI terminology to use "view" consistently (#325)

* docs: standardize UI terminology to use "view" consistently

Replaces "widget" and "Guest UI" terminology with "view" across
the codebase as decided in the community survey.

This change updates:
- Specification document
- SDK source code and JSDoc comments
- Example servers and documentation
- Plugin documentation and skills

Closes #71

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Liad Yosef 2026-01-23 04:36:52 +02:00 • committed by GitHub
commit 4fc71656a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 381 additions and 393 deletions

View file

@ -6,7 +6,7 @@ MCP Apps SDK (`@modelcontextprotocol/ext-apps`) enables MCP servers to display i
Key abstractions: Key abstractions:
- **Guest** - UI running in an iframe, uses `App` class with `PostMessageTransport` to communicate with host - **View** - UI running in an iframe, uses `App` class with `PostMessageTransport` to communicate with host
- **Host** - Chat client embedding the iframe, uses `AppBridge` class to proxy MCP requests - **Host** - Chat client embedding the iframe, uses `AppBridge` class to proxy MCP requests
- **Server** - MCP server that registers tools/resources with UI metadata - **Server** - MCP server that registers tools/resources with UI metadata
@ -67,14 +67,14 @@ rm -fR package-lock.json node_modules && \
### Protocol Flow ### Protocol Flow
``` ```
Guest UI (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP Server View (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP Server
``` ```
1. Host creates iframe with Guest UI HTML 1. Host creates iframe with view HTML
2. Guest UI creates `App` instance and calls `connect()` with `PostMessageTransport` 2. View creates `App` instance and calls `connect()` with `PostMessageTransport`
3. App sends `ui/initialize` request, receives host capabilities and context 3. View sends `ui/initialize` request, receives host capabilities and context
4. Host sends `sendToolInput()` with tool arguments after initialization 4. Host sends `sendToolInput()` with tool arguments after initialization
5. Guest UI can call server tools via `app.callServerTool()` or send messages via `app.sendMessage()` 5. View can call server tools via `app.callServerTool()` or send messages via `app.sendMessage()`
6. Host sends `sendToolResult()` when tool execution completes 6. Host sends `sendToolResult()` when tool execution completes
7. Host calls `teardownResource()` before unmounting iframe 7. Host calls `teardownResource()` before unmounting iframe

View file

@ -72,7 +72,7 @@ function createServer() {
inputSchema: { userId: z.string() }, inputSchema: { userId: z.string() },
annotations: { readOnlyHint: true }, annotations: { readOnlyHint: true },
_meta: { _meta: {
"openai/outputTemplate": "ui://widget/cart.html", "openai/outputTemplate": "ui://view/cart.html",
"openai/toolInvocation/invoking": "Loading cart...", "openai/toolInvocation/invoking": "Loading cart...",
"openai/toolInvocation/invoked": "Cart ready", "openai/toolInvocation/invoked": "Cart ready",
"openai/widgetAccessible": true, "openai/widgetAccessible": true,
@ -89,13 +89,13 @@ function createServer() {
// Register UI resource // Register UI resource
server.registerResource( server.registerResource(
"Cart Widget", "Cart View",
"ui://widget/cart.html", "ui://view/cart.html",
{ mimeType: "text/html+skybridge" }, { mimeType: "text/html+skybridge" },
async () => ({ async () => ({
contents: [ contents: [
{ {
uri: "ui://widget/cart.html", uri: "ui://view/cart.html",
mimeType: "text/html+skybridge", mimeType: "text/html+skybridge",
text: getCartHtml(), text: getCartHtml(),
_meta: { _meta: {
@ -137,7 +137,7 @@ function createServer() {
description: "Display the user's shopping cart", description: "Display the user's shopping cart",
inputSchema: { userId: z.string() }, inputSchema: { userId: z.string() },
annotations: { readOnlyHint: true }, annotations: { readOnlyHint: true },
_meta: { ui: { resourceUri: "ui://widget/cart.html" } }, _meta: { ui: { resourceUri: "ui://view/cart.html" } },
}, },
async (args) => { async (args) => {
const cart = await getCart(args.userId); const cart = await getCart(args.userId);
@ -151,13 +151,13 @@ function createServer() {
// Register UI resource // Register UI resource
registerAppResource( registerAppResource(
server, server,
"Cart Widget", "Cart View",
"ui://widget/cart.html", "ui://view/cart.html",
{ description: "Shopping cart UI" }, { description: "Shopping cart UI" },
async () => ({ async () => ({
contents: [ contents: [
{ {
uri: "ui://widget/cart.html", uri: "ui://view/cart.html",
mimeType: RESOURCE_MIME_TYPE, mimeType: RESOURCE_MIME_TYPE,
text: getCartHtml(), text: getCartHtml(),
_meta: { _meta: {

View file

@ -82,11 +82,11 @@ When you need to send more data than fits in a message, use {@link app!App.updat
_See [`examples/transcript-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/transcript-server) for a full implementation of this pattern._ _See [`examples/transcript-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/transcript-server) for a full implementation of this pattern._
## Persisting widget state ## Persisting view state
To persist widget state across conversation reloads (e.g., current page in a PDF viewer, camera position in a map), use [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) with a stable identifier provided by the server. To persist view state across conversation reloads (e.g., current page in a PDF viewer, camera position in a map), use [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) with a stable identifier provided by the server.
**Server-side**: Tool handler generates a unique `widgetUUID` and returns it in `CallToolResult._meta.widgetUUID`: **Server-side**: Tool handler generates a unique `viewUUID` and returns it in `CallToolResult._meta.viewUUID`:
{@includeCode ./patterns.tsx#persistDataServer} {@includeCode ./patterns.tsx#persistDataServer}
@ -96,9 +96,9 @@ To persist widget state across conversation reloads (e.g., current page in a PDF
_See [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) for a full implementation of this pattern._ _See [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) for a full implementation of this pattern._
## Pausing computation-heavy widgets when out of view ## Pausing computation-heavy views when out of view
Widgets 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 widget isn't visible: 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} {@includeCode ./patterns.tsx#visibilityBasedPause}

View file

@ -206,21 +206,17 @@ function hostStylingReact() {
} }
/** /**
* Example: Persisting widget state (server-side) * Example: Persisting view state (server-side)
*/ */
function persistWidgetStateServer( function persistViewStateServer(url: string, title: string, pageCount: number) {
url: string,
title: string,
pageCount: number,
) {
function toolCallback(): CallToolResult { function toolCallback(): CallToolResult {
//#region persistDataServer //#region persistDataServer
// In your tool callback, include widgetUUID in the result metadata. // In your tool callback, include viewUUID in the result metadata.
return { return {
content: [{ type: "text", text: `Displaying PDF viewer for "${title}"` }], content: [{ type: "text", text: `Displaying PDF viewer for "${title}"` }],
structuredContent: { url, title, pageCount, initialPage: 1 }, structuredContent: { url, title, pageCount, initialPage: 1 },
_meta: { _meta: {
widgetUUID: randomUUID(), viewUUID: randomUUID(),
}, },
}; };
//#endregion persistDataServer //#endregion persistDataServer
@ -228,39 +224,39 @@ function persistWidgetStateServer(
} }
/** /**
* Example: Persisting widget state (client-side) * Example: Persisting view state (client-side)
*/ */
function persistWidgetState(app: App) { function persistViewState(app: App) {
//#region persistData //#region persistData
// Store the widgetUUID received from the server // Store the viewUUID received from the server
let widgetUUID: string | undefined; let viewUUID: string | undefined;
// Helper to save state to localStorage // Helper to save state to localStorage
function saveState<T>(state: T): void { function saveState<T>(state: T): void {
if (!widgetUUID) return; if (!viewUUID) return;
try { try {
localStorage.setItem(widgetUUID, JSON.stringify(state)); localStorage.setItem(viewUUID, JSON.stringify(state));
} catch (err) { } catch (err) {
console.error("Failed to save widget state:", err); console.error("Failed to save view state:", err);
} }
} }
// Helper to load state from localStorage // Helper to load state from localStorage
function loadState<T>(): T | null { function loadState<T>(): T | null {
if (!widgetUUID) return null; if (!viewUUID) return null;
try { try {
const saved = localStorage.getItem(widgetUUID); const saved = localStorage.getItem(viewUUID);
return saved ? (JSON.parse(saved) as T) : null; return saved ? (JSON.parse(saved) as T) : null;
} catch (err) { } catch (err) {
console.error("Failed to load widget state:", err); console.error("Failed to load view state:", err);
return null; return null;
} }
} }
// Receive widgetUUID from the tool result // Receive viewUUID from the tool result
app.ontoolresult = (result) => { app.ontoolresult = (result) => {
widgetUUID = result._meta?.widgetUUID viewUUID = result._meta?.viewUUID
? String(result._meta.widgetUUID) ? String(result._meta.viewUUID)
: undefined; : undefined;
// Restore any previously saved state // Restore any previously saved state
@ -270,13 +266,13 @@ function persistWidgetState(app: App) {
} }
}; };
// Call saveState() whenever your widget state changes // Call saveState() whenever your view state changes
// e.g., saveState({ currentPage: 5 }); // e.g., saveState({ currentPage: 5 });
//#endregion persistData //#endregion persistData
} }
/** /**
* Example: Pausing computation-heavy widgets when out of view * Example: Pausing computation-heavy views when out of view
*/ */
function visibilityBasedPause( function visibilityBasedPause(
app: App, app: App,
@ -284,7 +280,7 @@ function visibilityBasedPause(
animation: { play: () => void; pause: () => void }, animation: { play: () => void; pause: () => void },
) { ) {
//#region visibilityBasedPause //#region visibilityBasedPause
// Use IntersectionObserver to pause when widget scrolls out of view // Use IntersectionObserver to pause when view scrolls out of view
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
@ -296,7 +292,7 @@ function visibilityBasedPause(
}); });
observer.observe(container); observer.observe(container);
// Clean up when the host tears down the widget // Clean up when the host tears down the view
app.onteardown = async () => { app.onteardown = async () => {
observer.disconnect(); observer.disconnect();
animation.pause(); animation.pause();
@ -310,6 +306,6 @@ void chunkedDataServer;
void chunkedDataClient; void chunkedDataClient;
void hostStylingVanillaJs; void hostStylingVanillaJs;
void hostStylingReact; void hostStylingReact;
void persistWidgetStateServer; void persistViewStateServer;
void persistWidgetState; void persistViewState;
void visibilityBasedPause; void visibilityBasedPause;

View file

@ -277,7 +277,7 @@ export function newAppBridge(
}, },
}); });
// Register all handlers before calling connect(). The Guest UI can start // Register all handlers before calling connect(). The view can start
// sending requests immediately after the initialization handshake, so any // sending requests immediately after the initialization handshake, so any
// handlers registered after connect() might miss early requests. // handlers registered after connect() might miss early requests.

View file

@ -56,7 +56,7 @@ const PROXY_READY_NOTIFICATION: McpUiSandboxProxyReadyNotification["method"] =
// Message relay: This Sandbox (outer iframe) acts as a bidirectional bridge, // Message relay: This Sandbox (outer iframe) acts as a bidirectional bridge,
// forwarding messages between: // forwarding messages between:
// //
// Host (parent window) ↔ Sandbox (outer frame) ↔ Guest UI (inner iframe) // Host (parent window) ↔ Sandbox (outer frame) ↔ View (inner iframe)
// //
// Reason: the parent window and inner iframe have different origins and can't // Reason: the parent window and inner iframe have different origins and can't
// communicate directly, so the outer iframe forwards messages in both // communicate directly, so the outer iframe forwards messages in both
@ -64,7 +64,7 @@ const PROXY_READY_NOTIFICATION: McpUiSandboxProxyReadyNotification["method"] =
// //
// Special case: The "ui/notifications/sandbox-proxy-ready" message is // Special case: The "ui/notifications/sandbox-proxy-ready" message is
// intercepted here (not relayed) because the Sandbox uses it to configure and // intercepted here (not relayed) because the Sandbox uses it to configure and
// load the inner iframe with the Guest UI HTML content. // load the inner iframe with the view HTML content.
// //
// Security: CSP is enforced via HTTP headers on sandbox.html (set by serve.ts // Security: CSP is enforced via HTTP headers on sandbox.html (set by serve.ts
// based on ?csp= query param). This is tamper-proof unlike meta tags. // based on ?csp= query param). This is tamper-proof unlike meta tags.
@ -128,7 +128,7 @@ window.addEventListener("message", async (event) => {
} }
}); });
// Notify the Host that the Sandbox is ready to receive Guest UI HTML. // Notify the Host that the Sandbox is ready to receive view HTML.
// Use specific origin instead of "*" to ensure only the expected host receives this. // Use specific origin instead of "*" to ensure only the expected host receives this.
window.parent.postMessage({ window.parent.postMessage({
jsonrpc: "2.0", jsonrpc: "2.0",

View file

@ -185,7 +185,7 @@ export function createServer(): McpServer {
}, },
], ],
_meta: { _meta: {
widgetUUID: randomUUID(), viewUUID: randomUUID(),
}, },
}), }),
); );

View file

@ -71,7 +71,7 @@ let persistViewTimer: ReturnType<typeof setTimeout> | null = null;
// Track whether tool input has been received (to know if we should restore persisted state) // Track whether tool input has been received (to know if we should restore persisted state)
let hasReceivedToolInput = false; let hasReceivedToolInput = false;
let widgetUUID: string | undefined = undefined; let viewUUID: string | undefined = undefined;
/** /**
* Persisted camera state for localStorage * Persisted camera state for localStorage
@ -122,7 +122,7 @@ function schedulePersistViewState(cesiumViewer: any): void {
* Persist current view state to localStorage * Persist current view state to localStorage
*/ */
function persistViewState(cesiumViewer: any): void { function persistViewState(cesiumViewer: any): void {
if (!widgetUUID) { if (!viewUUID) {
log.info("No storage key available, skipping view persistence"); log.info("No storage key available, skipping view persistence");
return; return;
} }
@ -132,8 +132,8 @@ function persistViewState(cesiumViewer: any): void {
try { try {
const value = JSON.stringify(state); const value = JSON.stringify(state);
localStorage.setItem(widgetUUID, value); localStorage.setItem(viewUUID, value);
log.info("Persisted view state:", widgetUUID, value); log.info("Persisted view state:", viewUUID, value);
} catch (e) { } catch (e) {
log.warn("Failed to persist view state:", e); log.warn("Failed to persist view state:", e);
} }
@ -143,10 +143,10 @@ function persistViewState(cesiumViewer: any): void {
* Load persisted view state from localStorage * Load persisted view state from localStorage
*/ */
function loadPersistedViewState(): PersistedCameraState | null { function loadPersistedViewState(): PersistedCameraState | null {
if (!widgetUUID) return null; if (!viewUUID) return null;
try { try {
const stored = localStorage.getItem(widgetUUID); const stored = localStorage.getItem(viewUUID);
if (!stored) { if (!stored) {
console.info("No persisted view state found"); console.info("No persisted view state found");
return null; return null;
@ -938,16 +938,14 @@ app.ontoolinput = async (params) => {
// }, // },
// ); // );
// Handle tool result - extract widgetUUID and restore persisted view if available // Handle tool result - extract viewUUID and restore persisted view if available
app.ontoolresult = async (result) => { app.ontoolresult = async (result) => {
widgetUUID = result._meta?.widgetUUID viewUUID = result._meta?.viewUUID ? String(result._meta.viewUUID) : undefined;
? String(result._meta.widgetUUID) log.info("Tool result received, viewUUID:", viewUUID);
: undefined;
log.info("Tool result received, widgetUUID:", widgetUUID);
// Now that we have widgetUUID, try to restore persisted view // Now that we have viewUUID, try to restore persisted view
// This overrides the tool input position if a saved state exists // This overrides the tool input position if a saved state exists
if (viewer && widgetUUID) { if (viewer && viewUUID) {
const restored = restorePersistedView(viewer); const restored = restorePersistedView(viewer);
if (restored) { if (restored) {
log.info("Restored persisted view from tool result handler"); log.info("Restored persisted view from tool result handler");

View file

@ -189,7 +189,7 @@ The viewer supports zoom, navigation, text selection, and fullscreen mode.`,
], ],
structuredContent: result, structuredContent: result,
_meta: { _meta: {
widgetUUID: randomUUID(), viewUUID: randomUUID(),
}, },
}; };
}, },

View file

@ -35,7 +35,7 @@ let totalPages = 0;
let scale = 1.0; let scale = 1.0;
let pdfUrl = ""; let pdfUrl = "";
let pdfTitle: string | undefined; let pdfTitle: string | undefined;
let widgetUUID: string | undefined; let viewUUID: string | undefined;
let currentRenderTask: { cancel: () => void } | null = null; let currentRenderTask: { cancel: () => void } | null = null;
// DOM Elements // DOM Elements
@ -404,10 +404,10 @@ async function renderPage() {
} }
function saveCurrentPage() { function saveCurrentPage() {
log.info("saveCurrentPage: key=", widgetUUID, "page=", currentPage); log.info("saveCurrentPage: key=", viewUUID, "page=", currentPage);
if (widgetUUID) { if (viewUUID) {
try { try {
localStorage.setItem(widgetUUID, String(currentPage)); localStorage.setItem(viewUUID, String(currentPage));
log.info("saveCurrentPage: saved successfully"); log.info("saveCurrentPage: saved successfully");
} catch (err) { } catch (err) {
log.error("saveCurrentPage: error", err); log.error("saveCurrentPage: error", err);
@ -416,10 +416,10 @@ function saveCurrentPage() {
} }
function loadSavedPage(): number | null { function loadSavedPage(): number | null {
log.info("loadSavedPage: key=", widgetUUID); log.info("loadSavedPage: key=", viewUUID);
if (!widgetUUID) return null; if (!viewUUID) return null;
try { try {
const saved = localStorage.getItem(widgetUUID); const saved = localStorage.getItem(viewUUID);
log.info("loadSavedPage: saved value=", saved); log.info("loadSavedPage: saved value=", saved);
if (saved) { if (saved) {
const page = parseInt(saved, 10); const page = parseInt(saved, 10);
@ -706,9 +706,7 @@ app.ontoolresult = async (result) => {
pdfUrl = parsed.url; pdfUrl = parsed.url;
pdfTitle = parsed.title; pdfTitle = parsed.title;
totalPages = parsed.pageCount; totalPages = parsed.pageCount;
widgetUUID = result._meta?.widgetUUID viewUUID = result._meta?.viewUUID ? String(result._meta.viewUUID) : undefined;
? String(result._meta.widgetUUID)
: undefined;
// Restore saved page or use initial page // Restore saved page or use initial page
const savedPage = loadSavedPage(); const savedPage = loadSavedPage();

View file

@ -1,6 +1,6 @@
# QR Code MCP Server # QR Code MCP Server
A minimal Python MCP server that generates customizable QR codes with an interactive widget UI. A minimal Python MCP server that generates customizable QR codes with an interactive view UI.
![Screenshot](https://modelcontextprotocol.github.io/ext-apps/screenshots/qr-server/screenshot.png) ![Screenshot](https://modelcontextprotocol.github.io/ext-apps/screenshots/qr-server/screenshot.png)
@ -33,7 +33,7 @@ Then add to your MCP client configuration (stdio transport), replacing the path
- Generate QR codes from any text or URL - Generate QR codes from any text or URL
- Customizable colors, size, and error correction - Customizable colors, size, and error correction
- Interactive widget that displays in MCP-UI enabled clients - Interactive view that displays in MCP-UI enabled clients
- Supports both HTTP (for web clients) and stdio (for MCP clients) - Supports both HTTP (for web clients) and stdio (for MCP clients)
## Prerequisites ## Prerequisites
@ -151,19 +151,19 @@ Generate a QR code with optional customization.
``` ```
qr-server/ qr-server/
├── server.py # MCP server (FastMCP + uvicorn, deps inline via PEP 723) ├── server.py # MCP server (FastMCP + uvicorn, deps inline via PEP 723)
├── widget.html # Interactive UI widget ├── view.html # Interactive UI view
└── README.md └── README.md
``` ```
### Protocol ### Protocol
The widget uses MCP Apps SDK protocol: The view uses MCP Apps SDK protocol:
1. Widget sends `ui/initialize` request 1. The view sends `ui/initialize` request
2. Host responds with capabilities 2. Host responds with capabilities
3. Widget sends `ui/notifications/initialized` 3. The view sends `ui/notifications/initialized`
4. Host sends `ui/notifications/tool-result` with QR image 4. Host sends `ui/notifications/tool-result` with QR image
5. Widget renders image and sends `ui/notifications/size-changed` 5. The view renders image and sends `ui/notifications/size-changed`
## Dependencies ## Dependencies

View file

@ -22,14 +22,14 @@ from mcp.server.fastmcp import FastMCP
from mcp import types from mcp import types
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
WIDGET_URI = "ui://qr-server/widget.html" VIEW_URI = "ui://qr-server/view.html"
HOST = os.environ.get("HOST", "0.0.0.0") # 0.0.0.0 for Docker compatibility HOST = os.environ.get("HOST", "0.0.0.0") # 0.0.0.0 for Docker compatibility
PORT = int(os.environ.get("PORT", "3108")) PORT = int(os.environ.get("PORT", "3108"))
mcp = FastMCP("QR Code Server") mcp = FastMCP("QR Code Server")
# Embedded widget HTML for self-contained usage (uv run <url> or unbundled) # Embedded View HTML for self-contained usage (uv run <url> or unbundled)
EMBEDDED_WIDGET_HTML = """<!DOCTYPE html> EMBEDDED_VIEW_HTML = """<!DOCTYPE html>
<html> <html>
<head> <head>
<meta name="color-scheme" content="light dark"> <meta name="color-scheme" content="light dark">
@ -60,7 +60,7 @@ EMBEDDED_WIDGET_HTML = """<!DOCTYPE html>
<script type="module"> <script type="module">
import { App } from "https://unpkg.com/@modelcontextprotocol/ext-apps@0.4.0/app-with-deps"; import { App } from "https://unpkg.com/@modelcontextprotocol/ext-apps@0.4.0/app-with-deps";
const app = new App({ name: "QR Widget", version: "1.0.0" }); const app = new App({ name: "QR View", version: "1.0.0" });
app.ontoolresult = ({ content }) => { app.ontoolresult = ({ content }) => {
const img = content?.find(c => c.type === 'image'); const img = content?.find(c => c.type === 'image');
@ -100,8 +100,8 @@ EMBEDDED_WIDGET_HTML = """<!DOCTYPE html>
@mcp.tool(meta={ @mcp.tool(meta={
"ui":{"resourceUri": WIDGET_URI}, "ui":{"resourceUri": VIEW_URI},
"ui/resourceUri": WIDGET_URI, # legacy support "ui/resourceUri": VIEW_URI, # legacy support
}) })
def generate_qr( def generate_qr(
text: str = "https://modelcontextprotocol.io", text: str = "https://modelcontextprotocol.io",
@ -147,13 +147,13 @@ def generate_qr(
# IMPORTANT: all the external domains used by app must be listed # IMPORTANT: all the external domains used by app must be listed
# in the meta.ui.csp.resourceDomains - otherwise they will be blocked by CSP policy # in the meta.ui.csp.resourceDomains - otherwise they will be blocked by CSP policy
@mcp.resource( @mcp.resource(
WIDGET_URI, VIEW_URI,
mime_type="text/html;profile=mcp-app", mime_type="text/html;profile=mcp-app",
meta={"ui": {"csp": {"resourceDomains": ["https://unpkg.com"]}}}, meta={"ui": {"csp": {"resourceDomains": ["https://unpkg.com"]}}},
) )
def widget() -> str: def view() -> str:
"""Widget HTML resource with CSP metadata for external dependencies.""" """View HTML resource with CSP metadata for external dependencies."""
return EMBEDDED_WIDGET_HTML return EMBEDDED_VIEW_HTML
if __name__ == "__main__": if __name__ == "__main__":
if "--stdio" in sys.argv: if "--stdio" in sys.argv:

View file

@ -30,13 +30,13 @@ Add to your MCP client configuration (stdio transport):
This example showcases several MCP App capabilities: This example showcases several MCP App capabilities:
- **Single-file executable**: Python server with embedded React UI - no build step required - **Single-file executable**: Python server with embedded React UI - no build step required
- **Partial tool inputs** (`ontoolinputpartial`): Widget receives streaming text as it's being generated - **Partial tool inputs** (`ontoolinputpartial`): The view receives streaming text as it's being generated
- **Queue-based streaming**: Demonstrates how to stream text out and audio in via a polling tool (adds text to an input queue, retrieves audio chunks from an output queue) - **Queue-based streaming**: Demonstrates how to stream text out and audio in via a polling tool (adds text to an input queue, retrieves audio chunks from an output queue)
- **Model context updates**: Widget updates the LLM with playback progress ("Playing: ...snippet...") - **Model context updates**: The view updates the LLM with playback progress ("Playing: ...snippet...")
- **Native theming**: Uses CSS variables for automatic dark/light mode adaptation - **Native theming**: Uses CSS variables for automatic dark/light mode adaptation
- **Fullscreen mode**: Toggle fullscreen via `requestDisplayMode()` API, press Escape to exit - **Fullscreen mode**: Toggle fullscreen via `requestDisplayMode()` API, press Escape to exit
- **Multi-widget speak lock**: Coordinates multiple TTS widgets via localStorage so only one plays at a time - **Multi-view speak lock**: Coordinates multiple TTS views via localStorage so only one plays at a time
- **Hidden tools** (`visibility: ["app"]`): Private tools only accessible to the widget, not the model - **Hidden tools** (`visibility: ["app"]`): Private tools only accessible to the view, not the model
- **External links** (`openLink`): Attribution popup uses `app.openLink()` to open external URLs - **External links** (`openLink`): Attribution popup uses `app.openLink()` to open external URLs
- **CSP metadata**: Resource declares required domains (`esm.sh`) for in-browser transpilation - **CSP metadata**: Resource declares required domains (`esm.sh`) for in-browser transpilation
@ -132,29 +132,29 @@ See the [kyutai/tts-voices](https://huggingface.co/kyutai/tts-voices) repository
The entire server is contained in a single `server.py` file: The entire server is contained in a single `server.py` file:
1. **`say` tool**: Public tool that triggers the widget with text to speak 1. **`say` tool**: Public tool that triggers the view with text to speak
2. **Private tools** (`create_tts_queue`, `add_tts_text`, `poll_tts_audio`, etc.): Hidden from the model, only callable by the widget 2. **Private tools** (`create_tts_queue`, `add_tts_text`, `poll_tts_audio`, etc.): Hidden from the model, only callable by the view
3. **Embedded React widget**: Uses [Babel standalone](https://babeljs.io/docs/babel-standalone) for in-browser JSX transpilation - no build step needed 3. **Embedded React view**: Uses [Babel standalone](https://babeljs.io/docs/babel-standalone) for in-browser JSX transpilation - no build step needed
4. **TTS backend**: Manages per-request audio queues using Pocket TTS 4. **TTS backend**: Manages per-request audio queues using Pocket TTS
The widget communicates with the server via MCP tool calls: The view communicates with the server via MCP tool calls:
- Receives streaming text via `ontoolinputpartial` callback - Receives streaming text via `ontoolinputpartial` callback
- Incrementally sends new text to the server as it arrives (via `add_tts_text`) - Incrementally sends new text to the server as it arrives (via `add_tts_text`)
- Polls for generated audio chunks while TTS runs in parallel - Polls for generated audio chunks while TTS runs in parallel
- Plays audio via Web Audio API with synchronized text highlighting - Plays audio via Web Audio API with synchronized text highlighting
## Multi-Widget Speak Lock ## Multi-view Speak Lock
When multiple TTS widgets exist in the same browser (e.g., multiple chat messages each with their own say widget), they coordinate via localStorage to ensure only one plays at a time: When multiple TTS views exist in the same browser (e.g., multiple chat messages each with their own say view), they coordinate via localStorage to ensure only one plays at a time:
1. **Unique Widget IDs**: Each widget receives a UUID via `toolResult._meta.widgetUUID` 1. **Unique view IDs**: Each view receives a UUID via `toolResult._meta.viewUUID`
2. **Announce on Play**: When starting, a widget writes `{uuid, timestamp}` to `localStorage["mcp-tts-playing"]` 2. **Announce on Play**: When starting, a view writes `{uuid, timestamp}` to `localStorage["mcp-tts-playing"]`
3. **Poll for Conflicts**: Every 200ms, playing widgets check if another widget took the lock 3. **Poll for Conflicts**: Every 200ms, playing views check if another view took the lock
4. **Yield Gracefully**: If another widget started playing, pause and yield 4. **Yield Gracefully**: If another view started playing, pause and yield
5. **Clean Up**: On pause/finish, clear the lock (only if owned) 5. **Clean Up**: On pause/finish, clear the lock (only if owned)
This "last writer wins" protocol ensures a seamless experience: clicking play on any widget immediately pauses others, without requiring cross-iframe postMessage coordination. This "last writer wins" protocol ensures a seamless experience: clicking play on any view immediately pauses others, without requiring cross-iframe postMessage coordination.
## TODO ## TODO

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark"> <meta name="color-scheme" content="light dark">
<title>Say Widget</title> <title>Say View</title>
<link rel="stylesheet" href="/src/global.css"> <link rel="stylesheet" href="/src/global.css">
</head> </head>
<body> <body>

View file

@ -12,17 +12,17 @@
Say Demo - MCP App for streaming text-to-speech. Say Demo - MCP App for streaming text-to-speech.
This MCP server provides a "say" tool that speaks text using TTS. This MCP server provides a "say" tool that speaks text using TTS.
The widget receives streaming partial input and starts speaking immediately. The view receives streaming partial input and starts speaking immediately.
Architecture: Architecture:
- The `say` tool itself is a no-op - it just triggers the widget - The `say` tool itself is a no-op - it just triggers the view
- The widget uses `ontoolinputpartial` to receive text as it streams - The view uses `ontoolinputpartial` to receive text as it streams
- Widget calls private tools to create TTS queue, add text, and poll audio - view calls private tools to create TTS queue, add text, and poll audio
- Audio plays in the widget using Web Audio API - Audio plays in the view using Web Audio API
- Model context updates show playback progress to the LLM - Model context updates show playback progress to the LLM
- Native theming adapts to dark/light mode automatically - Native theming adapts to dark/light mode automatically
- Fullscreen mode with Escape key to exit - Fullscreen mode with Escape key to exit
- Multi-widget speak lock coordinates playback across instances - Multi-View speak lock coordinates playback across instances
Usage: Usage:
# Start the MCP server # Start the MCP server
@ -56,7 +56,7 @@ from pocket_tts.default_parameters import DEFAULT_AUDIO_PROMPT
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
WIDGET_URI = "ui://say-demo/widget.html" VIEW_URI = "ui://say-demo/view.html"
HOST = os.environ.get("HOST", "0.0.0.0") HOST = os.environ.get("HOST", "0.0.0.0")
PORT = int(os.environ.get("PORT", "3109")) PORT = int(os.environ.get("PORT", "3109"))
@ -167,8 +167,8 @@ def list_voices() -> list[types.TextContent]:
@mcp.tool(meta={ @mcp.tool(meta={
"ui":{"resourceUri": WIDGET_URI}, "ui":{"resourceUri": VIEW_URI},
"ui/resourceUri": WIDGET_URI, # legacy support "ui/resourceUri": VIEW_URI, # legacy support
}) })
def say( def say(
text: Annotated[str, Field(description="The English text to speak aloud")] = DEFAULT_TEXT, text: Annotated[str, Field(description="The English text to speak aloud")] = DEFAULT_TEXT,
@ -192,19 +192,19 @@ def say(
Note: English only. Non-English text may produce poor or garbled results. Note: English only. Non-English text may produce poor or garbled results.
""" """
# Generate a unique ID for this widget instance (used for speak lock coordination) # Generate a unique ID for this view instance (used for speak lock coordination)
widget_uuid = uuid.uuid4().hex[:12] view_uuid = uuid.uuid4().hex[:12]
# This is a no-op - the widget handles everything via ontoolinputpartial # This is a no-op - the view handles everything via ontoolinputpartial
# The tool exists to: # The tool exists to:
# 1. Trigger the widget to load # 1. Trigger the view to load
# 2. Provide the resourceUri metadata # 2. Provide the resourceUri metadata
# 3. Show the final text in the tool result # 3. Show the final text in the tool result
# 4. Provide widget UUID for multi-player coordination # 4. Provide view UUID for multi-player coordination
return [types.TextContent( return [types.TextContent(
type="text", type="text",
text=f"Displayed a TTS widget with voice '{voice}'. Click to play/pause, use toolbar to restart or fullscreen.", text=f"Displayed a TTS view with voice '{voice}'. Click to play/pause, use toolbar to restart or fullscreen.",
_meta={"widgetUUID": widget_uuid}, _meta={"viewUUID": view_uuid},
)] )]
@ -348,7 +348,7 @@ def poll_tts_audio(queue_id: str) -> list[types.TextContent]:
new_chunks = state.audio_chunks[state.chunks_delivered:] new_chunks = state.audio_chunks[state.chunks_delivered:]
state.chunks_delivered = len(state.audio_chunks) state.chunks_delivered = len(state.audio_chunks)
# Consider queues with errors as "done" so widget stops polling # Consider queues with errors as "done" so view stops polling
done = (state.status == "complete" or state.status == "error") and state.chunks_delivered >= len(state.audio_chunks) done = (state.status == "complete" or state.status == "error") and state.chunks_delivered >= len(state.audio_chunks)
response = { response = {
@ -661,18 +661,18 @@ async def _process_tts_chunk(
# ------------------------------------------------------ # ------------------------------------------------------
# Widget Resource # View Resource
# ------------------------------------------------------ # ------------------------------------------------------
# Embedded widget HTML for standalone execution via `uv run <url>` # Embedded View HTML for standalone execution via `uv run <url>`
# Uses Babel standalone for in-browser JSX transpilation # Uses Babel standalone for in-browser JSX transpilation
# This is a copy of widget.html - keep them in sync! # This is a copy of view.html - keep them in sync!
EMBEDDED_WIDGET_HTML = """<!DOCTYPE html> EMBEDDED_VIEW_HTML = """<!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Say Widget</title> <title>Say View</title>
<script src="https://unpkg.com/@babel/standalone@7.26.10/babel.min.js"></script> <script src="https://unpkg.com/@babel/standalone@7.26.10/babel.min.js"></script>
<script type="importmap"> <script type="importmap">
{ {
@ -784,7 +784,7 @@ EMBEDDED_WIDGET_HTML = """<!DOCTYPE html>
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { useApp } from '@modelcontextprotocol/ext-apps/react'; import { useApp } from '@modelcontextprotocol/ext-apps/react';
function SayWidget() { function SayView() {
const [hostContext, setHostContext] = useState(undefined); const [hostContext, setHostContext] = useState(undefined);
const [displayText, setDisplayText] = useState(""); const [displayText, setDisplayText] = useState("");
const [charPosition, setCharPosition] = useState(0); const [charPosition, setCharPosition] = useState(0);
@ -796,7 +796,7 @@ EMBEDDED_WIDGET_HTML = """<!DOCTYPE html>
const [showInfo, setShowInfo] = useState(false); const [showInfo, setShowInfo] = useState(false);
const voiceRef = useRef("cosette"); // Current voice, updated from tool input const voiceRef = useRef("cosette"); // Current voice, updated from tool input
const widgetUuidRef = useRef(null); // Widget UUID for speak lock coordination const viewUuidRef = useRef(null); // View UUID for speak lock coordination
const speakLockIntervalRef = useRef(null); // Polling interval for speak lock const speakLockIntervalRef = useRef(null); // Polling interval for speak lock
const queueIdRef = useRef(null); const queueIdRef = useRef(null);
const audioContextRef = useRef(null); const audioContextRef = useRef(null);
@ -816,22 +816,22 @@ EMBEDDED_WIDGET_HTML = """<!DOCTYPE html>
const initQueuePromiseRef = useRef(null); const initQueuePromiseRef = useRef(null);
const pendingModelContextUpdateRef = useRef(null); const pendingModelContextUpdateRef = useRef(null);
// Speak lock: coordinates multiple TTS widgets so only one plays at a time // Speak lock: coordinates multiple TTS views so only one plays at a time
const SPEAK_LOCK_KEY = "mcp-tts-playing"; const SPEAK_LOCK_KEY = "mcp-tts-playing";
const announcePlayback = useCallback(() => { const announcePlayback = useCallback(() => {
if (!widgetUuidRef.current) return; if (!viewUuidRef.current) return;
localStorage.setItem(SPEAK_LOCK_KEY, JSON.stringify({ localStorage.setItem(SPEAK_LOCK_KEY, JSON.stringify({
uuid: widgetUuidRef.current, uuid: viewUuidRef.current,
timestamp: Date.now() timestamp: Date.now()
})); }));
}, []); }, []);
const clearSpeakLock = useCallback(() => { const clearSpeakLock = useCallback(() => {
if (!widgetUuidRef.current) return; if (!viewUuidRef.current) return;
try { try {
const current = JSON.parse(localStorage.getItem(SPEAK_LOCK_KEY) || "null"); const current = JSON.parse(localStorage.getItem(SPEAK_LOCK_KEY) || "null");
if (current?.uuid === widgetUuidRef.current) { if (current?.uuid === viewUuidRef.current) {
localStorage.removeItem(SPEAK_LOCK_KEY); localStorage.removeItem(SPEAK_LOCK_KEY);
} }
} catch {} } catch {}
@ -842,9 +842,9 @@ EMBEDDED_WIDGET_HTML = """<!DOCTYPE html>
speakLockIntervalRef.current = setInterval(() => { speakLockIntervalRef.current = setInterval(() => {
try { try {
const current = JSON.parse(localStorage.getItem(SPEAK_LOCK_KEY) || "null"); const current = JSON.parse(localStorage.getItem(SPEAK_LOCK_KEY) || "null");
if (current && current.uuid !== widgetUuidRef.current) { if (current && current.uuid !== viewUuidRef.current) {
// Someone else started playing - yield // Someone else started playing - yield
console.log('[TTS] Another widget started playing, pausing'); console.log('[TTS] Another view started playing, pausing');
onStolenCallback(); onStolenCallback();
} }
} catch {} } catch {}
@ -1161,7 +1161,7 @@ EMBEDDED_WIDGET_HTML = """<!DOCTYPE html>
}, [displayMode]); }, [displayMode]);
const { app, error } = useApp({ const { app, error } = useApp({
appInfo: { name: "Say Widget", version: "1.0.0" }, appInfo: { name: "Say View", version: "1.0.0" },
capabilities: {}, capabilities: {},
onHostContextChanged: (ctx) => { onHostContextChanged: (ctx) => {
if (ctx.availableDisplayModes?.includes("fullscreen")) { if (ctx.availableDisplayModes?.includes("fullscreen")) {
@ -1219,11 +1219,11 @@ EMBEDDED_WIDGET_HTML = """<!DOCTYPE html>
app.ontoolresult = async (params) => { app.ontoolresult = async (params) => {
console.log('[TTS] ontoolresult called, queueId:', queueIdRef.current); console.log('[TTS] ontoolresult called, queueId:', queueIdRef.current);
fullTextRef.current = lastTextRef.current; fullTextRef.current = lastTextRef.current;
// Read widget UUID from tool result _meta for speak lock coordination // Read View UUID from tool result _meta for speak lock coordination
const resultUuid = params.content?.[0]?._meta?.widgetUUID; const resultUuid = params.content?.[0]?._meta?.viewUUID;
if (resultUuid) { if (resultUuid) {
widgetUuidRef.current = resultUuid; viewUuidRef.current = resultUuid;
console.log('[TTS] Widget UUID:', resultUuid); console.log('[TTS] View UUID:', resultUuid);
} }
if (queueIdRef.current) { if (queueIdRef.current) {
console.log('[TTS] Calling end_tts_queue for:', queueIdRef.current); console.log('[TTS] Calling end_tts_queue for:', queueIdRef.current);
@ -1267,14 +1267,14 @@ EMBEDDED_WIDGET_HTML = """<!DOCTYPE html>
} }
}, [app]); }, [app]);
// Speak lock: coordinate with other TTS widgets // Speak lock: coordinate with other TTS Views
useEffect(() => { useEffect(() => {
if (status === "playing") { if (status === "playing") {
// Announce that we're playing // Announce that we're playing
announcePlayback(); announcePlayback();
// Poll to detect if another widget starts // Poll to detect if another View starts
startSpeakLockPolling(() => { startSpeakLockPolling(() => {
// Another widget took over - pause // Another View took over - pause
const ctx = audioContextRef.current; const ctx = audioContextRef.current;
if (ctx && ctx.state === "running") { if (ctx && ctx.state === "running") {
ctx.suspend(); ctx.suspend();
@ -1386,32 +1386,32 @@ EMBEDDED_WIDGET_HTML = """<!DOCTYPE html>
); );
} }
createRoot(document.getElementById('root')).render(<StrictMode><SayWidget /></StrictMode>); createRoot(document.getElementById('root')).render(<StrictMode><SayView /></StrictMode>);
</script> </script>
</body> </body>
</html>""" </html>"""
def get_widget_html() -> str: def get_view_html() -> str:
"""Get the widget HTML, preferring built version from dist/.""" """Get the View HTML, preferring built version from dist/."""
# Prefer built version from dist/ (local development with npm run build) # Prefer built version from dist/ (local development with npm run build)
dist_path = Path(__file__).parent / "dist" / "mcp-app.html" dist_path = Path(__file__).parent / "dist" / "mcp-app.html"
if dist_path.exists(): if dist_path.exists():
return dist_path.read_text() return dist_path.read_text()
# Fallback to embedded widget (for `uv run <url>` or unbundled usage) # Fallback to embedded View (for `uv run <url>` or unbundled usage)
return EMBEDDED_WIDGET_HTML return EMBEDDED_VIEW_HTML
# IMPORTANT: all the external domains used by app must be listed # IMPORTANT: all the external domains used by app must be listed
# in the meta.ui.csp.resourceDomains - otherwise they will be blocked by CSP policy # in the meta.ui.csp.resourceDomains - otherwise they will be blocked by CSP policy
@mcp.resource( @mcp.resource(
WIDGET_URI, VIEW_URI,
mime_type="text/html;profile=mcp-app", mime_type="text/html;profile=mcp-app",
meta={"ui": {"csp": {"resourceDomains": ["https://esm.sh", "https://unpkg.com"]}}}, meta={"ui": {"csp": {"resourceDomains": ["https://esm.sh", "https://unpkg.com"]}}},
) )
def widget() -> str: def view() -> str:
"""Widget HTML resource with CSP metadata for external dependencies.""" """View HTML resource with CSP metadata for external dependencies."""
return get_widget_html() return get_view_html()
# ------------------------------------------------------ # ------------------------------------------------------

View file

@ -69,7 +69,7 @@ html, body {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: background 0.2s, opacity 0.2s; transition: background 0.2s, opacity 0.2s;
opacity: 0; /* Initially invisible, shown on widget hover */ opacity: 0; /* Initially invisible, shown on View hover */
z-index: 100; z-index: 100;
} }
@ -91,7 +91,7 @@ html, body {
display: flex; display: flex;
} }
/* Show button on widget hover */ /* Show button on View hover */
.main:hover .fullscreen-btn.available { .main:hover .fullscreen-btn.available {
opacity: 0.7; opacity: 0.7;
} }

View file

@ -134,7 +134,7 @@ React component that:
### Visibility-Based Pause ### Visibility-Based Pause
Animation automatically pauses when the widget scrolls out of view: Animation automatically pauses when the view scrolls out of view:
- Uses `IntersectionObserver` to track visibility (browser-native, no polling) - Uses `IntersectionObserver` to track visibility (browser-native, no polling)
- Wraps `requestAnimationFrame` to skip frames when not visible - Wraps `requestAnimationFrame` to skip frames when not visible

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark"> <meta name="color-scheme" content="light dark">
<title>Three.js Widget</title> <title>Three.js View</title>
<link rel="stylesheet" href="/src/global.css"> <link rel="stylesheet" href="/src/global.css">
</head> </head>
<body> <body>

View file

@ -19,7 +19,7 @@ const DIST_DIR = import.meta.filename.endsWith(".ts")
? path.join(import.meta.dirname, "dist") ? path.join(import.meta.dirname, "dist")
: import.meta.dirname; : import.meta.dirname;
// Default code example for the Three.js widget // Default code example for the Three.js view
const DEFAULT_THREEJS_CODE = `const scene = new THREE.Scene(); const DEFAULT_THREEJS_CODE = `const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
@ -45,7 +45,7 @@ function animate() {
} }
animate();`; animate();`;
const THREEJS_DOCUMENTATION = `# Three.js Widget Documentation const THREEJS_DOCUMENTATION = `# Three.js view Documentation
## Available Globals ## Available Globals
- \`THREE\` - Three.js library (r181) - \`THREE\` - Three.js library (r181)
@ -191,8 +191,7 @@ export function createServer(): McpServer {
"learn_threejs", "learn_threejs",
{ {
title: "Learn Three.js", title: "Learn Three.js",
description: description: "Get documentation and examples for using the Three.js View",
"Get documentation and examples for using the Three.js widget",
inputSchema: {}, inputSchema: {},
}, },
async () => { async () => {
@ -207,7 +206,7 @@ export function createServer(): McpServer {
server, server,
resourceUri, resourceUri,
resourceUri, resourceUri,
{ mimeType: RESOURCE_MIME_TYPE, description: "Three.js Widget UI" }, { mimeType: RESOURCE_MIME_TYPE, description: "Three.js View UI" },
async (): Promise<ReadResourceResult> => { async (): Promise<ReadResourceResult> => {
const html = await fs.readFile( const html = await fs.readFile(
path.join(DIST_DIR, "mcp-app.html"), path.join(DIST_DIR, "mcp-app.html"),

View file

@ -1,8 +1,8 @@
/** /**
* Three.js Widget - MCP App Wrapper * Three.js view - MCP App Wrapper
* *
* Generic wrapper that handles MCP App connection and passes all relevant * Generic wrapper that handles MCP App connection and passes all relevant
* props to the actual widget component. * props to the actual view component.
*/ */
import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import { useApp, useHostStyles } from "@modelcontextprotocol/ext-apps/react"; import { useApp, useHostStyles } from "@modelcontextprotocol/ext-apps/react";
@ -17,10 +17,10 @@ import "./global.css";
// ============================================================================= // =============================================================================
/** /**
* Props passed to the widget component. * Props passed to the view component.
* This interface can be reused for other widgets. * This interface can be reused for other views.
*/ */
export interface WidgetProps<TToolInput = Record<string, unknown>> { export interface ViewProps<TToolInput = Record<string, unknown>> {
/** Complete tool input (after streaming finishes) */ /** Complete tool input (after streaming finishes) */
toolInputs: TToolInput | null; toolInputs: TToolInput | null;
/** Partial tool input (during streaming) */ /** Partial tool input (during streaming) */
@ -55,7 +55,7 @@ function McpAppWrapper() {
const [hostContext, setHostContext] = useState<McpUiHostContext | null>(null); const [hostContext, setHostContext] = useState<McpUiHostContext | null>(null);
const { app, error } = useApp({ const { app, error } = useApp({
appInfo: { name: "Three.js Widget", version: "1.0.0" }, appInfo: { name: "Three.js View", version: "1.0.0" },
capabilities: {}, capabilities: {},
onAppCreated: (app) => { onAppCreated: (app) => {
// Complete tool input (streaming finished) // Complete tool input (streaming finished)

Some files were not shown because too many files have changed in this diff Show more