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
|
## 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 -->
|
<!-- prettier-ignore -->
|
||||||
```ts source="../src/server/index.examples.ts#registerAppTool_appOnlyVisibility"
|
```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
|
## 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
|
## 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
|
## 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
|
## 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
|
## 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
|
## Persisting view state
|
||||||
|
|
||||||
|
|
@ -443,7 +514,8 @@ app.ontoolresult = (result) => {
|
||||||
// e.g., saveState({ currentPage: 5 });
|
// 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
|
## 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
|
## Lowering perceived latency
|
||||||
|
|
||||||
|
|
@ -498,4 +571,5 @@ app.ontoolinput = (params) => {
|
||||||
> [!IMPORTANT]
|
> [!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.
|
> 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 { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from "zod";
|
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)
|
* Example: Server-side chunked data tool (app-only)
|
||||||
*/
|
*/
|
||||||
|
|
@ -336,6 +402,8 @@ function visibilityBasedPause(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suppress unused variable warnings
|
// Suppress unused variable warnings
|
||||||
|
void pollingVanillaJs;
|
||||||
|
void pollingReact;
|
||||||
void chunkedDataServer;
|
void chunkedDataServer;
|
||||||
void chunkedDataClient;
|
void chunkedDataClient;
|
||||||
void hostContextVanillaJs;
|
void hostContextVanillaJs;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue