diff --git a/examples/system-monitor-server/README.md b/examples/system-monitor-server/README.md
new file mode 100644
index 0000000..3ce91df
--- /dev/null
+++ b/examples/system-monitor-server/README.md
@@ -0,0 +1,49 @@
+# Example: System Monitor App
+
+A demo MCP App that displays real-time OS metrics with a stacked area chart for per-core CPU usage and a bar gauge for memory.
+
+## Features
+
+- **Per-Core CPU Monitoring**: Stacked area chart showing individual CPU core utilization over a 1-minute sliding window
+- **Memory Usage**: Horizontal bar gauge with color-coded thresholds (green/yellow/red)
+- **System Info**: Hostname, platform, and uptime display
+- **Auto-Polling**: Automatically starts monitoring on load with 2-second refresh interval
+- **Theme Support**: Adapts to light/dark mode preferences
+
+## Running
+
+1. Install dependencies:
+
+ ```bash
+ npm install
+ ```
+
+2. Build and start the server:
+
+ ```bash
+ npm start
+ ```
+
+ The server will listen on `http://localhost:3001/mcp`.
+
+3. View using the [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example or another MCP Apps-compatible host.
+
+## Architecture
+
+### Server (`server.ts`)
+
+Exposes a single `get-system-stats` tool that returns:
+
+- Raw per-core CPU timing data (idle/total counters)
+- Memory usage (used/total/percentage)
+- System info (hostname, platform, uptime)
+
+The tool is linked to a UI resource via `_meta[RESOURCE_URI_META_KEY]`.
+
+### App (`src/mcp-app.ts`)
+
+- Uses Chart.js for the stacked area chart visualization
+- Polls the server tool every 2 seconds
+- Computes CPU usage percentages client-side from timing deltas
+- Maintains a 30-point history (1 minute at 2s intervals)
+- Updates all UI elements on each poll
diff --git a/examples/system-monitor-server/mcp-app.html b/examples/system-monitor-server/mcp-app.html
new file mode 100644
index 0000000..c86a89f
--- /dev/null
+++ b/examples/system-monitor-server/mcp-app.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+ System Monitor
+
+
+
+
+
+
+
+
+
+
+ System Info
+
+
+
- Hostname
+ - --
+
+
+
- Platform
+ - --
+
+
+
- Uptime
+ - --
+
+
+
+
+
+
+
+
diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json
new file mode 100644
index 0000000..2632307
--- /dev/null
+++ b/examples/system-monitor-server/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "system-monitor-server",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "INPUT=mcp-app.html vite build",
+ "watch": "INPUT=mcp-app.html vite build --watch",
+ "serve": "bun server.ts",
+ "start": "NODE_ENV=development npm run build && npm run serve",
+ "dev": "NODE_ENV=development concurrently 'npm run watch' 'npm run serve'"
+ },
+ "dependencies": {
+ "@modelcontextprotocol/ext-apps": "../..",
+ "@modelcontextprotocol/sdk": "^1.22.0",
+ "chart.js": "^4.4.0",
+ "systeminformation": "^5.27.11",
+ "zod": "^3.25.0"
+ },
+ "devDependencies": {
+ "@types/cors": "^2.8.19",
+ "@types/express": "^5.0.0",
+ "@types/node": "^22.0.0",
+ "concurrently": "^9.2.1",
+ "cors": "^2.8.5",
+ "express": "^5.1.0",
+ "typescript": "^5.9.3",
+ "vite": "^6.0.0",
+ "vite-plugin-singlefile": "^2.3.0"
+ }
+}
diff --git a/examples/system-monitor-server/server.ts b/examples/system-monitor-server/server.ts
new file mode 100644
index 0000000..926252d
--- /dev/null
+++ b/examples/system-monitor-server/server.ts
@@ -0,0 +1,218 @@
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import type {
+ CallToolResult,
+ ReadResourceResult,
+} from "@modelcontextprotocol/sdk/types.js";
+import cors from "cors";
+import express, { type Request, type Response } from "express";
+import fs from "node:fs/promises";
+import os from "node:os";
+import path from "node:path";
+import si from "systeminformation";
+import { z } from "zod";
+import { RESOURCE_URI_META_KEY } from "../../dist/src/app";
+
+const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
+
+// Schemas - types are derived from these using z.infer
+const CpuCoreSchema = z.object({
+ idle: z.number(),
+ total: z.number(),
+});
+
+const CpuStatsSchema = z.object({
+ cores: z.array(CpuCoreSchema),
+ model: z.string(),
+ count: z.number(),
+});
+
+const MemoryStatsSchema = z.object({
+ usedBytes: z.number(),
+ totalBytes: z.number(),
+ usedPercent: z.number(),
+ freeBytes: z.number(),
+ usedFormatted: z.string(),
+ totalFormatted: z.string(),
+});
+
+const SystemInfoSchema = z.object({
+ hostname: z.string(),
+ platform: z.string(),
+ arch: z.string(),
+ uptime: z.number(),
+ uptimeFormatted: z.string(),
+});
+
+const SystemStatsSchema = z.object({
+ cpu: CpuStatsSchema,
+ memory: MemoryStatsSchema,
+ system: SystemInfoSchema,
+ timestamp: z.string(),
+});
+
+// Types derived from schemas
+type CpuCore = z.infer;
+type MemoryStats = z.infer;
+type SystemStats = z.infer;
+const DIST_DIR = path.join(import.meta.dirname, "dist");
+
+// Returns raw CPU timing data per core (client calculates usage from deltas)
+function getCpuSnapshots(): CpuCore[] {
+ return os.cpus().map((cpu) => {
+ const times = cpu.times;
+ const idle = times.idle;
+ const total = times.user + times.nice + times.sys + times.idle + times.irq;
+ return { idle, total };
+ });
+}
+
+function formatUptime(seconds: number): string {
+ const days = Math.floor(seconds / 86400);
+ const hours = Math.floor((seconds % 86400) / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+
+ const parts: string[] = [];
+ if (days > 0) parts.push(`${days}d`);
+ if (hours > 0) parts.push(`${hours}h`);
+ if (minutes > 0) parts.push(`${minutes}m`);
+
+ return parts.length > 0 ? parts.join(" ") : "< 1m";
+}
+
+function formatBytes(bytes: number): string {
+ const units = ["B", "KB", "MB", "GB", "TB"];
+ let value = bytes;
+ let unitIndex = 0;
+
+ while (value >= 1024 && unitIndex < units.length - 1) {
+ value /= 1024;
+ unitIndex++;
+ }
+
+ return `${value.toFixed(1)} ${units[unitIndex]}`;
+}
+
+async function getMemoryStats(): Promise {
+ const mem = await si.mem();
+ return {
+ usedBytes: mem.active,
+ totalBytes: mem.total,
+ usedPercent: Math.round((mem.active / mem.total) * 100),
+ freeBytes: mem.available,
+ usedFormatted: formatBytes(mem.active),
+ totalFormatted: formatBytes(mem.total),
+ };
+}
+
+const server = new McpServer({
+ name: "System Monitor Server",
+ version: "1.0.0",
+});
+
+// Register the get-system-stats tool and its associated UI resource
+{
+ const resourceUri = "ui://system-monitor/mcp-app.html";
+
+ server.registerTool(
+ "get-system-stats",
+ {
+ title: "Get System Stats",
+ description:
+ "Returns current system statistics including per-core CPU usage, memory, and system info.",
+ inputSchema: {},
+ outputSchema: SystemStatsSchema.shape,
+ _meta: { [RESOURCE_URI_META_KEY]: resourceUri },
+ },
+ async (): Promise => {
+ const cpuSnapshots = getCpuSnapshots();
+ const cpuInfo = os.cpus()[0];
+ const memory = await getMemoryStats();
+ const uptimeSeconds = os.uptime();
+
+ const stats: SystemStats = {
+ cpu: {
+ cores: cpuSnapshots,
+ model: cpuInfo?.model ?? "Unknown",
+ count: os.cpus().length,
+ },
+ memory,
+ system: {
+ hostname: os.hostname(),
+ platform: `${os.platform()} ${os.arch()}`,
+ arch: os.arch(),
+ uptime: uptimeSeconds,
+ uptimeFormatted: formatUptime(uptimeSeconds),
+ },
+ timestamp: new Date().toISOString(),
+ };
+
+ return {
+ content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
+ structuredContent: stats,
+ };
+ },
+ );
+
+ server.registerResource(
+ resourceUri,
+ resourceUri,
+ { description: "System Monitor UI" },
+ async (): Promise => {
+ const html = await fs.readFile(
+ path.join(DIST_DIR, "mcp-app.html"),
+ "utf-8",
+ );
+
+ return {
+ contents: [{ uri: resourceUri, mimeType: "text/html+mcp", text: html }],
+ };
+ },
+ );
+}
+
+const app = express();
+app.use(cors());
+app.use(express.json());
+
+app.post("/mcp", async (req: Request, res: Response) => {
+ try {
+ const transport = new StreamableHTTPServerTransport({
+ sessionIdGenerator: undefined,
+ enableJsonResponse: true,
+ });
+ res.on("close", () => {
+ transport.close();
+ });
+
+ await server.connect(transport);
+
+ await transport.handleRequest(req, res, req.body);
+ } catch (error) {
+ console.error("Error handling MCP request:", error);
+ if (!res.headersSent) {
+ res.status(500).json({
+ jsonrpc: "2.0",
+ error: { code: -32603, message: "Internal server error" },
+ id: null,
+ });
+ }
+ }
+});
+
+const httpServer = app.listen(PORT, () => {
+ console.log(
+ `System Monitor Server listening on http://localhost:${PORT}/mcp`,
+ );
+});
+
+function shutdown() {
+ console.log("\nShutting down...");
+ httpServer.close(() => {
+ console.log("Server closed");
+ process.exit(0);
+ });
+}
+
+process.on("SIGINT", shutdown);
+process.on("SIGTERM", shutdown);
diff --git a/examples/system-monitor-server/src/global.css b/examples/system-monitor-server/src/global.css
new file mode 100644
index 0000000..97cda44
--- /dev/null
+++ b/examples/system-monitor-server/src/global.css
@@ -0,0 +1,12 @@
+* {
+ box-sizing: border-box;
+}
+
+html, body {
+ font-family: system-ui, -apple-system, sans-serif;
+ font-size: 1rem;
+}
+
+code {
+ font-size: 1em;
+}
diff --git a/examples/system-monitor-server/src/mcp-app.css b/examples/system-monitor-server/src/mcp-app.css
new file mode 100644
index 0000000..ac95af4
--- /dev/null
+++ b/examples/system-monitor-server/src/mcp-app.css
@@ -0,0 +1,233 @@
+:root {
+ --color-bg: #ffffff;
+ --color-text: #1f2937;
+ --color-text-muted: #6b7280;
+ --color-primary: #2563eb;
+ --color-primary-hover: #1d4ed8;
+ --color-success: #10b981;
+ --color-warning: #f59e0b;
+ --color-danger: #ef4444;
+ --color-bar-bg: #e5e7eb;
+ --color-card-bg: #f9fafb;
+ --color-border: #e5e7eb;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --color-bg: #111827;
+ --color-text: #f9fafb;
+ --color-text-muted: #9ca3af;
+ --color-primary: #3b82f6;
+ --color-primary-hover: #60a5fa;
+ --color-success: #34d399;
+ --color-warning: #fbbf24;
+ --color-danger: #f87171;
+ --color-bar-bg: #374151;
+ --color-card-bg: #1f2937;
+ --color-border: #374151;
+ }
+}
+
+html, body {
+ margin: 0;
+ padding: 0;
+ background: var(--color-bg);
+ color: var(--color-text);
+}
+
+.main {
+ width: 600px;
+ margin: 0 auto;
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+/* Header */
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 12px;
+}
+
+.title {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ flex-shrink: 0;
+}
+
+.header-controls {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-shrink: 0;
+}
+
+.btn {
+ padding: 6px 16px;
+ min-width: 60px;
+ font-size: 0.875rem;
+ font-weight: 500;
+ border: none;
+ border-radius: 6px;
+ background: var(--color-primary);
+ color: white;
+ cursor: pointer;
+ transition: background 0.15s ease;
+}
+
+.btn:hover {
+ background: var(--color-primary-hover);
+}
+
+.btn.active {
+ background: var(--color-danger);
+}
+
+.btn.active:hover {
+ background: #dc2626;
+}
+
+.status {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.75rem;
+ color: var(--color-text-muted);
+}
+
+.status-text {
+ min-width: 8ch;
+ font-family: ui-monospace, monospace;
+ text-align: right;
+}
+
+.status-indicator {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--color-text-muted);
+}
+
+.status-indicator.polling {
+ background: var(--color-success);
+ animation: pulse 1.5s infinite;
+}
+
+.status-indicator.error {
+ background: var(--color-danger);
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
+
+/* Section titles */
+.section-title {
+ margin: 0 0 8px 0;
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--color-text-muted);
+}
+
+/* Chart section */
+.chart-section {
+ background: var(--color-card-bg);
+ border-radius: 8px;
+ padding: 12px;
+ border: 1px solid var(--color-border);
+}
+
+.chart-container {
+ position: relative;
+ height: 200px;
+}
+
+/* Memory section */
+.memory-section {
+ background: var(--color-card-bg);
+ border-radius: 8px;
+ padding: 12px;
+ border: 1px solid var(--color-border);
+}
+
+.memory-bar-container {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.memory-bar {
+ flex: 1;
+ height: 20px;
+ background: var(--color-bar-bg);
+ border-radius: 10px;
+ overflow: hidden;
+}
+
+.memory-bar-fill {
+ height: 100%;
+ width: 0%;
+ background: var(--color-success);
+ border-radius: 10px;
+ transition: width 0.3s ease, background 0.3s ease;
+}
+
+.memory-bar-fill.warning {
+ background: var(--color-warning);
+}
+
+.memory-bar-fill.danger {
+ background: var(--color-danger);
+}
+
+.memory-percent {
+ font-size: 1.25rem;
+ font-weight: 600;
+ min-width: 50px;
+ text-align: right;
+}
+
+.memory-detail {
+ margin-top: 4px;
+ font-size: 0.875rem;
+ color: var(--color-text-muted);
+}
+
+/* Info section */
+.info-section {
+ background: var(--color-card-bg);
+ border-radius: 8px;
+ padding: 12px;
+ border: 1px solid var(--color-border);
+}
+
+.info-list {
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.info-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.info-item dt {
+ font-size: 0.875rem;
+ color: var(--color-text-muted);
+}
+
+.info-item dd {
+ margin: 0;
+ font-size: 0.875rem;
+ font-weight: 500;
+}
diff --git a/examples/system-monitor-server/src/mcp-app.ts b/examples/system-monitor-server/src/mcp-app.ts
new file mode 100644
index 0000000..7c645e0
--- /dev/null
+++ b/examples/system-monitor-server/src/mcp-app.ts
@@ -0,0 +1,360 @@
+/**
+ * @file System Monitor App - displays real-time OS metrics with Chart.js
+ */
+import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps";
+import { Chart, registerables } from "chart.js";
+import "./global.css";
+import "./mcp-app.css";
+
+// Register Chart.js components
+Chart.register(...registerables);
+
+const log = {
+ info: console.log.bind(console, "[APP]"),
+ error: console.error.bind(console, "[APP]"),
+};
+
+// Types for system stats response
+interface SystemStats {
+ cpu: {
+ cores: Array<{ idle: number; total: number }>;
+ model: string;
+ count: number;
+ };
+ memory: {
+ usedBytes: number;
+ totalBytes: number;
+ usedPercent: number;
+ freeBytes: number;
+ usedFormatted: string;
+ totalFormatted: string;
+ };
+ system: {
+ hostname: string;
+ platform: string;
+ arch: string;
+ uptime: number;
+ uptimeFormatted: string;
+ };
+ timestamp: string;
+}
+
+// DOM element references
+const pollToggleBtn = document.getElementById("poll-toggle-btn")!;
+const statusIndicator = document.getElementById("status-indicator")!;
+const statusText = document.getElementById("status-text")!;
+const cpuChartCanvas = document.getElementById(
+ "cpu-chart",
+) as HTMLCanvasElement;
+const memoryBarFill = document.getElementById("memory-bar-fill")!;
+const memoryPercent = document.getElementById("memory-percent")!;
+const memoryDetail = document.getElementById("memory-detail")!;
+const infoHostname = document.getElementById("info-hostname")!;
+const infoPlatform = document.getElementById("info-platform")!;
+const infoUptime = document.getElementById("info-uptime")!;
+
+// Polling state
+const HISTORY_LENGTH = 30;
+const POLL_INTERVAL = 2000;
+
+interface PollingState {
+ isPolling: boolean;
+ intervalId: number | null;
+ cpuHistory: number[][]; // [timestamp][coreIndex] = usage%
+ labels: string[];
+ coreCount: number;
+ chart: Chart | null;
+ previousCpuSnapshots: Array<{ idle: number; total: number }> | null;
+}
+
+const state: PollingState = {
+ isPolling: false,
+ intervalId: null,
+ cpuHistory: [],
+ labels: [],
+ coreCount: 0,
+ chart: null,
+ previousCpuSnapshots: null,
+};
+
+// Color palette for CPU cores (distinct colors)
+const CORE_COLORS = [
+ "rgba(59, 130, 246, 0.7)", // blue
+ "rgba(16, 185, 129, 0.7)", // green
+ "rgba(245, 158, 11, 0.7)", // amber
+ "rgba(239, 68, 68, 0.7)", // red
+ "rgba(139, 92, 246, 0.7)", // purple
+ "rgba(236, 72, 153, 0.7)", // pink
+ "rgba(20, 184, 166, 0.7)", // teal
+ "rgba(249, 115, 22, 0.7)", // orange
+ "rgba(34, 197, 94, 0.7)", // emerald
+ "rgba(168, 85, 247, 0.7)", // violet
+ "rgba(251, 146, 60, 0.7)", // orange-light
+ "rgba(74, 222, 128, 0.7)", // green-light
+ "rgba(96, 165, 250, 0.7)", // blue-light
+ "rgba(248, 113, 113, 0.7)", // red-light
+ "rgba(167, 139, 250, 0.7)", // purple-light
+ "rgba(244, 114, 182, 0.7)", // pink-light
+];
+
+// Calculate CPU usage percentages from raw timing data
+function calculateCpuUsage(
+ current: Array<{ idle: number; total: number }>,
+ previous: Array<{ idle: number; total: number }> | null,
+): number[] {
+ if (!previous || previous.length !== current.length) {
+ return current.map(() => 0);
+ }
+ return current.map((cur, i) => {
+ const prev = previous[i];
+ const idleDiff = cur.idle - prev.idle;
+ const totalDiff = cur.total - prev.total;
+ if (totalDiff === 0) return 0;
+ return Math.round((1 - idleDiff / totalDiff) * 100);
+ });
+}
+
+function initChart(coreCount: number): Chart {
+ const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
+ const textColor = isDarkMode ? "#9ca3af" : "#6b7280";
+ const gridColor = isDarkMode ? "#374151" : "#e5e7eb";
+
+ const datasets = Array.from({ length: coreCount }, (_, i) => ({
+ label: `P${i}`,
+ data: [] as number[],
+ fill: true,
+ backgroundColor: CORE_COLORS[i % CORE_COLORS.length],
+ borderColor: CORE_COLORS[i % CORE_COLORS.length].replace("0.7", "1"),
+ borderWidth: 1,
+ pointRadius: 0,
+ tension: 0.3,
+ }));
+
+ return new Chart(cpuChartCanvas, {
+ type: "line",
+ data: {
+ labels: [],
+ datasets,
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: {
+ duration: 300,
+ },
+ interaction: {
+ intersect: false,
+ mode: "index",
+ },
+ plugins: {
+ legend: {
+ display: true,
+ position: "bottom",
+ labels: {
+ boxWidth: 12,
+ padding: 8,
+ font: { size: 10 },
+ color: textColor,
+ },
+ },
+ tooltip: {
+ enabled: true,
+ callbacks: {
+ label: (context) =>
+ `${context.dataset.label}: ${context.parsed.y}%`,
+ },
+ },
+ },
+ scales: {
+ x: {
+ display: false,
+ },
+ y: {
+ stacked: true,
+ min: 0,
+ max: coreCount * 100,
+ ticks: {
+ callback: (value) => `${value}%`,
+ color: textColor,
+ font: { size: 10 },
+ },
+ grid: {
+ color: gridColor,
+ },
+ },
+ },
+ },
+ });
+}
+
+function updateChart(cpuHistory: number[][], labels: string[]): void {
+ if (!state.chart) return;
+
+ state.chart.data.labels = labels;
+
+ // Transpose: cpuHistory[time][core] -> datasets[core].data[time]
+ for (let coreIdx = 0; coreIdx < state.coreCount; coreIdx++) {
+ state.chart.data.datasets[coreIdx].data = cpuHistory.map(
+ (snapshot) => snapshot[coreIdx] ?? 0,
+ );
+ }
+
+ // Dynamic y-axis scaling
+ // Calculate max stacked value (sum of all cores at each time point)
+ const stackedTotals = cpuHistory.map((snapshot) =>
+ snapshot.reduce((sum, val) => sum + val, 0),
+ );
+ const currentMax = Math.max(...stackedTotals, 0);
+
+ // Add 20% headroom, clamp to reasonable bounds
+ const headroom = 1.2;
+ const minVisible = state.coreCount * 15; // At least 15% per core visible
+ const absoluteMax = state.coreCount * 100;
+
+ const dynamicMax = Math.min(
+ Math.max(currentMax * headroom, minVisible),
+ absoluteMax,
+ );
+
+ state.chart.options.scales!.y!.max = dynamicMax;
+
+ state.chart.update("none");
+}
+
+function updateMemoryBar(memory: SystemStats["memory"]): void {
+ const percent = memory.usedPercent;
+
+ memoryBarFill.style.width = `${percent}%`;
+ memoryBarFill.classList.remove("warning", "danger");
+
+ if (percent >= 80) {
+ memoryBarFill.classList.add("danger");
+ } else if (percent >= 60) {
+ memoryBarFill.classList.add("warning");
+ }
+
+ memoryPercent.textContent = `${percent}%`;
+ memoryDetail.textContent = `${memory.usedFormatted} / ${memory.totalFormatted}`;
+}
+
+function updateSystemInfo(system: SystemStats["system"]): void {
+ infoHostname.textContent = system.hostname;
+ infoPlatform.textContent = system.platform;
+ infoUptime.textContent = system.uptimeFormatted;
+}
+
+function updateStatus(text: string, isPolling = false, isError = false): void {
+ statusText.textContent = text;
+ statusIndicator.classList.remove("polling", "error");
+
+ if (isError) {
+ statusIndicator.classList.add("error");
+ } else if (isPolling) {
+ statusIndicator.classList.add("polling");
+ }
+}
+
+// Create app instance
+const app = new App({ name: "System Monitor", version: "1.0.0" });
+
+async function fetchStats(): Promise {
+ try {
+ const result = await app.callServerTool({
+ name: "get-system-stats",
+ arguments: {},
+ });
+
+ const stats = result.structuredContent as unknown as SystemStats;
+
+ // Initialize chart on first data if needed
+ if (!state.chart && stats.cpu.count > 0) {
+ state.coreCount = stats.cpu.count;
+ state.chart = initChart(state.coreCount);
+ }
+
+ // Calculate CPU usage from raw timing data (client-side)
+ const coreUsages = calculateCpuUsage(
+ stats.cpu.cores,
+ state.previousCpuSnapshots,
+ );
+ state.previousCpuSnapshots = stats.cpu.cores;
+ state.cpuHistory.push(coreUsages);
+ state.labels.push(new Date().toLocaleTimeString());
+
+ // Trim to window size
+ if (state.cpuHistory.length > HISTORY_LENGTH) {
+ state.cpuHistory.shift();
+ state.labels.shift();
+ }
+
+ // Update UI
+ updateChart(state.cpuHistory, state.labels);
+ updateMemoryBar(stats.memory);
+ updateSystemInfo(stats.system);
+
+ const time = new Date().toLocaleTimeString("en-US", { hour12: false });
+ updateStatus(time, true);
+ } catch (error) {
+ log.error("Failed to fetch stats:", error);
+ updateStatus("Error", false, true);
+ }
+}
+
+function startPolling(): void {
+ if (state.isPolling) return;
+
+ state.isPolling = true;
+ pollToggleBtn.textContent = "Stop";
+ pollToggleBtn.classList.add("active");
+ updateStatus("Starting...", true);
+
+ // Immediate first fetch
+ fetchStats();
+
+ // Start interval
+ state.intervalId = window.setInterval(fetchStats, POLL_INTERVAL);
+}
+
+function stopPolling(): void {
+ if (!state.isPolling) return;
+
+ state.isPolling = false;
+ if (state.intervalId) {
+ clearInterval(state.intervalId);
+ state.intervalId = null;
+ }
+
+ pollToggleBtn.textContent = "Start";
+ pollToggleBtn.classList.remove("active");
+ updateStatus("Stopped");
+}
+
+function togglePolling(): void {
+ if (state.isPolling) {
+ stopPolling();
+ } else {
+ startPolling();
+ }
+}
+
+// Event listeners
+pollToggleBtn.addEventListener("click", togglePolling);
+
+// Handle theme changes
+window
+ .matchMedia("(prefers-color-scheme: dark)")
+ .addEventListener("change", () => {
+ if (state.chart) {
+ state.chart.destroy();
+ state.chart = initChart(state.coreCount);
+ updateChart(state.cpuHistory, state.labels);
+ }
+ });
+
+// Register handlers and connect
+app.onerror = log.error;
+
+app.connect(new PostMessageTransport(window.parent));
+
+// Auto-start polling after a short delay
+setTimeout(startPolling, 500);
diff --git a/examples/system-monitor-server/tsconfig.json b/examples/system-monitor-server/tsconfig.json
new file mode 100644
index 0000000..535267b
--- /dev/null
+++ b/examples/system-monitor-server/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src", "server.ts"]
+}
diff --git a/examples/system-monitor-server/vite.config.ts b/examples/system-monitor-server/vite.config.ts
new file mode 100644
index 0000000..6ff6d99
--- /dev/null
+++ b/examples/system-monitor-server/vite.config.ts
@@ -0,0 +1,24 @@
+import { defineConfig } from "vite";
+import { viteSingleFile } from "vite-plugin-singlefile";
+
+const INPUT = process.env.INPUT;
+if (!INPUT) {
+ throw new Error("INPUT environment variable is not set");
+}
+
+const isDevelopment = process.env.NODE_ENV === "development";
+
+export default defineConfig({
+ plugins: [viteSingleFile()],
+ build: {
+ sourcemap: isDevelopment ? "inline" : undefined,
+ cssMinify: !isDevelopment,
+ minify: !isDevelopment,
+
+ rollupOptions: {
+ input: INPUT,
+ },
+ outDir: "dist",
+ emptyOutDir: false,
+ },
+});
diff --git a/package-lock.json b/package-lock.json
index b33cef7..a724462 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -160,6 +160,27 @@
"vite-plugin-singlefile": "^2.3.0"
}
},
+ "examples/system-monitor-server": {
+ "version": "1.0.0",
+ "dependencies": {
+ "@modelcontextprotocol/ext-apps": "../..",
+ "@modelcontextprotocol/sdk": "^1.22.0",
+ "chart.js": "^4.4.0",
+ "systeminformation": "^5.27.11",
+ "zod": "^3.25.0"
+ },
+ "devDependencies": {
+ "@types/cors": "^2.8.19",
+ "@types/express": "^5.0.0",
+ "@types/node": "^22.0.0",
+ "concurrently": "^9.2.1",
+ "cors": "^2.8.5",
+ "express": "^5.1.0",
+ "typescript": "^5.9.3",
+ "vite": "^6.0.0",
+ "vite-plugin-singlefile": "^2.3.0"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"dev": true,
@@ -471,6 +492,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
"node_modules/@modelcontextprotocol/ext-apps": {
"resolved": "",
"link": true
@@ -1344,6 +1371,18 @@
"node": ">=8"
}
},
+ "node_modules/chart.js": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
+ "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
"node_modules/check-error": {
"version": "2.1.1",
"dev": true,
@@ -2944,6 +2983,36 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
+ "node_modules/system-monitor-server": {
+ "resolved": "examples/system-monitor-server",
+ "link": true
+ },
+ "node_modules/systeminformation": {
+ "version": "5.27.11",
+ "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.27.11.tgz",
+ "integrity": "sha512-K3Lto/2m3K2twmKHdgx5B+0in9qhXK4YnoT9rIlgwN/4v7OV5c8IjbeAUkuky/6VzCQC7iKCAqi8rZathCdjHg==",
+ "license": "MIT",
+ "os": [
+ "darwin",
+ "linux",
+ "win32",
+ "freebsd",
+ "openbsd",
+ "netbsd",
+ "sunos",
+ "android"
+ ],
+ "bin": {
+ "systeminformation": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "funding": {
+ "type": "Buy me a coffee",
+ "url": "https://www.buymeacoffee.com/systeminfo"
+ }
+ },
"node_modules/tinybench": {
"version": "2.9.0",
"dev": true,