Merge pull request #389 from jonathanhefner/add-polling-to-patterns-guide

Add polling pattern to patterns guide
This commit is contained in:
Jonathan Hefner 2026-01-28 14:30:10 -06:00 • committed by GitHub
commit 246e1ab138
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 152 additions and 10 deletions

View file

@ -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.
<!-- prettier-ignore -->
```ts source="../src/server/index.examples.ts#registerAppTool_appOnlyVisibility"
@ -32,7 +32,73 @@ 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).
## 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:**
<!-- prettier-ignore -->
```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:**
<!-- prettier-ignore -->
```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
@ -156,7 +222,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 +346,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 +397,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 +421,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 +447,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 +514,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 +543,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 +571,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).

View file

@ -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<unknown>();
//#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;