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;