From 6a2905d17d70de4899dff57515abe3e5cbaaca62 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Wed, 28 Jan 2026 11:44:10 -0600 Subject: [PATCH 1/2] Use note callouts for pattern guide example references Convert italic `_See example..._` text to `> [!NOTE]` callouts for better visual prominence, and expand cross-references to include additional relevant examples for each pattern. Co-Authored-By: Claude Opus 4.5 --- docs/patterns.md | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/docs/patterns.md b/docs/patterns.md index 4bc4b0c..d59d972 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -32,7 +32,8 @@ registerAppTool( ); ``` -_See [`examples/system-monitor-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/system-monitor-server) for a full implementation of this pattern._ +> [!NOTE] +> For full examples that implement this pattern, see: [`examples/system-monitor-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/system-monitor-server) and [`examples/pdf-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/pdf-server). ## Reading large amounts of data via chunked tool calls @@ -156,7 +157,8 @@ loadDataInChunks(resourceId, (loaded, total) => { }); ``` -_See [`examples/pdf-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/pdf-server) for a full implementation of this pattern._ +> [!NOTE] +> For a full example that implements this pattern, see: [`examples/pdf-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/pdf-server). ## Giving errors back to the model @@ -279,7 +281,8 @@ function MyApp() { } ``` -_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._ +> [!NOTE] +> For full examples that implement this pattern, 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). ## Entering / exiting fullscreen @@ -329,7 +332,8 @@ In fullscreen mode, remove the container's border radius so content extends to t } ``` -_See [`examples/shadertoy-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server) for a full implementation of this pattern._ +> [!NOTE] +> For full examples that implement this pattern, see: [`examples/shadertoy-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server), [`examples/pdf-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/pdf-server), and [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server). ## Passing contextual information from the App to the model @@ -352,7 +356,8 @@ await app.updateModelContext({ }); ``` -_See [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) for a full implementation of this pattern._ +> [!NOTE] +> For full examples that implement this pattern, see: [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) and [`examples/transcript-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/transcript-server). ## Sending large follow-up messages @@ -377,7 +382,8 @@ await app.sendMessage({ }); ``` -_See [`examples/transcript-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/transcript-server) for a full implementation of this pattern._ +> [!NOTE] +> For a full example that implements this pattern, see: [`examples/transcript-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/transcript-server). ## Persisting view state @@ -443,7 +449,8 @@ app.ontoolresult = (result) => { // e.g., saveState({ currentPage: 5 }); ``` -_See [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) for a full implementation of this pattern._ +> [!NOTE] +> For full examples that implement this pattern, see: [`examples/pdf-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/pdf-server) (persists current page) and [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) (persists camera position). ## Pausing computation-heavy views when offscreen @@ -471,7 +478,8 @@ app.onteardown = async () => { }; ``` -_See [`examples/shadertoy-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server) for a full implementation of this pattern._ +> [!NOTE] +> For full examples that implement this pattern, see: [`examples/shadertoy-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server) and [`examples/threejs-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/threejs-server). ## Lowering perceived latency @@ -498,4 +506,5 @@ app.ontoolinput = (params) => { > [!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. -_See [`examples/threejs-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/threejs-server) for a full implementation of this pattern._ +> [!NOTE] +> For full examples that implement this pattern, see: [`examples/shadertoy-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server) and [`examples/threejs-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/threejs-server). From 818bd71c9ffea66ab04db514d4cf58a15cd82db9 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Wed, 28 Jan 2026 12:41:28 -0600 Subject: [PATCH 2/2] Add polling pattern to patterns guide Documents the recommended approach for real-time dashboards that need to poll server tools at regular intervals. Includes both Vanilla JS and React examples with proper cleanup handling via `app.onteardown` and `useEffect` cleanup functions respectively. Also updates the "Tools that are private to Apps" section to mention polling as a use case for app-only visibility. Co-Authored-By: Claude Opus 4.5 --- docs/patterns.md | 67 +++++++++++++++++++++++++++++++++++++++++++++- docs/patterns.tsx | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/docs/patterns.md b/docs/patterns.md index d59d972..55952fe 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -8,7 +8,7 @@ This document covers common patterns and recipes for building MCP Apps. ## Tools that are private to 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. +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 server-side state, polling, or other interactions that shouldn't appear in the model's tool list. ```ts source="../src/server/index.examples.ts#registerAppTool_appOnlyVisibility" @@ -35,6 +35,71 @@ registerAppTool( > [!NOTE] > For full examples that implement this pattern, see: [`examples/system-monitor-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/system-monitor-server) and [`examples/pdf-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/pdf-server). +## Polling for live data + +For real-time dashboards or monitoring views, use an app-only tool (with `visibility: ["app"]`) that the App polls at regular intervals. + +**Vanilla JS:** + + +```ts source="./patterns.tsx#pollingVanillaJs" +let intervalId: number | null = null; + +async function poll() { + const result = await app.callServerTool({ + name: "poll-data", + arguments: {}, + }); + updateUI(result.structuredContent); +} + +function startPolling() { + if (intervalId !== null) return; + poll(); + intervalId = window.setInterval(poll, 2000); +} + +function stopPolling() { + if (intervalId === null) return; + clearInterval(intervalId); + intervalId = null; +} + +// Clean up when host tears down the view +app.onteardown = async () => { + stopPolling(); + return {}; +}; +``` + +**React:** + + +```tsx source="./patterns.tsx#pollingReact" +useEffect(() => { + if (!app) return; + let cancelled = false; + + async function poll() { + const result = await app!.callServerTool({ + name: "poll-data", + arguments: {}, + }); + if (!cancelled) setData(result.structuredContent); + } + + poll(); + const id = setInterval(poll, 2000); + return () => { + cancelled = true; + clearInterval(id); + }; +}, [app]); +``` + +> [!NOTE] +> For a full example that implements this pattern, see: [`examples/system-monitor-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/system-monitor-server). + ## Reading large amounts of data via chunked tool calls Some host platforms have size limits on tool call responses, so large files (PDFs, images, etc.) cannot be sent in a single response. Use an app-only tool with chunked responses to bypass these limits while keeping the data out of model context. diff --git a/docs/patterns.tsx b/docs/patterns.tsx index cae3529..f575eee 100644 --- a/docs/patterns.tsx +++ b/docs/patterns.tsx @@ -22,6 +22,72 @@ import { registerAppTool } from "../src/server/index.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; +/** + * Example: Polling for live data (Vanilla JS) + */ +function pollingVanillaJs(app: App, updateUI: (data: unknown) => void) { + //#region pollingVanillaJs + let intervalId: number | null = null; + + async function poll() { + const result = await app.callServerTool({ + name: "poll-data", + arguments: {}, + }); + updateUI(result.structuredContent); + } + + function startPolling() { + if (intervalId !== null) return; + poll(); + intervalId = window.setInterval(poll, 2000); + } + + function stopPolling() { + if (intervalId === null) return; + clearInterval(intervalId); + intervalId = null; + } + + // Clean up when host tears down the view + app.onteardown = async () => { + stopPolling(); + return {}; + }; + //#endregion pollingVanillaJs +} + +/** + * Example: Polling for live data (React) + */ +function pollingReact( + app: App | null, // via useApp() +) { + const [data, setData] = useState(); + + //#region pollingReact + useEffect(() => { + if (!app) return; + let cancelled = false; + + async function poll() { + const result = await app!.callServerTool({ + name: "poll-data", + arguments: {}, + }); + if (!cancelled) setData(result.structuredContent); + } + + poll(); + const id = setInterval(poll, 2000); + return () => { + cancelled = true; + clearInterval(id); + }; + }, [app]); + //#endregion pollingReact +} + /** * Example: Server-side chunked data tool (app-only) */ @@ -336,6 +402,8 @@ function visibilityBasedPause( } // Suppress unused variable warnings +void pollingVanillaJs; +void pollingReact; void chunkedDataServer; void chunkedDataClient; void hostContextVanillaJs;