Merge pull request #389 from jonathanhefner/add-polling-to-patterns-guide
Add polling pattern to patterns guide
This commit is contained in:
commit
246e1ab138
2 changed files with 152 additions and 10 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue