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 Monitor

+
+ + + + Ready + +
+
+ +
+

CPU Usage

+
+ +
+
+ +
+

Memory

+
+
+
+
+ --% +
+
-- / --
+
+ +
+

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,