Skip to content

Commit fa2d413

Browse files
Add system-monitor-server example
Demo MCP App showcasing real-time OS metrics visualization with: - Per-core CPU usage via stacked area chart (Chart.js) - Memory usage bar gauge with color-coded thresholds - Auto-polling at 2-second intervals - Light/dark theme support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent f1c995f commit fa2d413

File tree

10 files changed

+1075
-0
lines changed

10 files changed

+1075
-0
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Example: System Monitor App
2+
3+
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.
4+
5+
## Features
6+
7+
- **Per-Core CPU Monitoring**: Stacked area chart showing individual CPU core utilization over a 1-minute sliding window
8+
- **Memory Usage**: Horizontal bar gauge with color-coded thresholds (green/yellow/red)
9+
- **System Info**: Hostname, platform, and uptime display
10+
- **Auto-Polling**: Automatically starts monitoring on load with 2-second refresh interval
11+
- **Theme Support**: Adapts to light/dark mode preferences
12+
13+
## Running
14+
15+
1. Install dependencies:
16+
17+
```bash
18+
npm install
19+
```
20+
21+
2. Build and start the server:
22+
23+
```bash
24+
npm start
25+
```
26+
27+
The server will listen on `http://localhost:3001/mcp`.
28+
29+
3. View using the [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example or another MCP Apps-compatible host.
30+
31+
## Architecture
32+
33+
### Server (`server.ts`)
34+
35+
Exposes a single `get-system-stats` tool that returns:
36+
37+
- Raw per-core CPU timing data (idle/total counters)
38+
- Memory usage (used/total/percentage)
39+
- System info (hostname, platform, uptime)
40+
41+
The tool is linked to a UI resource via `_meta[RESOURCE_URI_META_KEY]`.
42+
43+
### App (`src/mcp-app.ts`)
44+
45+
- Uses Chart.js for the stacked area chart visualization
46+
- Polls the server tool every 2 seconds
47+
- Computes CPU usage percentages client-side from timing deltas
48+
- Maintains a 30-point history (1 minute at 2s intervals)
49+
- Updates all UI elements on each poll
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>System Monitor</title>
7+
</head>
8+
<body>
9+
<main class="main">
10+
<header class="header">
11+
<h1 class="title">System Monitor</h1>
12+
<div class="header-controls">
13+
<button id="poll-toggle-btn" class="btn">Start</button>
14+
<span class="status">
15+
<span id="status-indicator" class="status-indicator"></span>
16+
<span id="status-text" class="status-text">Ready</span>
17+
</span>
18+
</div>
19+
</header>
20+
21+
<section class="chart-section">
22+
<h2 class="section-title">CPU Usage</h2>
23+
<div class="chart-container">
24+
<canvas id="cpu-chart"></canvas>
25+
</div>
26+
</section>
27+
28+
<section class="memory-section">
29+
<h2 class="section-title">Memory</h2>
30+
<div class="memory-bar-container">
31+
<div class="memory-bar">
32+
<div id="memory-bar-fill" class="memory-bar-fill"></div>
33+
</div>
34+
<span id="memory-percent" class="memory-percent">--%</span>
35+
</div>
36+
<div id="memory-detail" class="memory-detail">-- / --</div>
37+
</section>
38+
39+
<section class="info-section">
40+
<h2 class="section-title">System Info</h2>
41+
<dl class="info-list">
42+
<div class="info-item">
43+
<dt>Hostname</dt>
44+
<dd id="info-hostname">--</dd>
45+
</div>
46+
<div class="info-item">
47+
<dt>Platform</dt>
48+
<dd id="info-platform">--</dd>
49+
</div>
50+
<div class="info-item">
51+
<dt>Uptime</dt>
52+
<dd id="info-uptime">--</dd>
53+
</div>
54+
</dl>
55+
</section>
56+
</main>
57+
58+
<script type="module" src="./src/mcp-app.ts"></script>
59+
</body>
60+
</html>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "system-monitor-server",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"build": "INPUT=mcp-app.html vite build",
8+
"watch": "INPUT=mcp-app.html vite build --watch",
9+
"serve": "bun server.ts",
10+
"start": "NODE_ENV=development npm run build && npm run serve",
11+
"dev": "NODE_ENV=development concurrently 'npm run watch' 'npm run serve'"
12+
},
13+
"dependencies": {
14+
"@modelcontextprotocol/ext-apps": "../..",
15+
"@modelcontextprotocol/sdk": "^1.22.0",
16+
"chart.js": "^4.4.0",
17+
"systeminformation": "^5.27.11",
18+
"zod": "^3.25.0"
19+
},
20+
"devDependencies": {
21+
"@types/cors": "^2.8.19",
22+
"@types/express": "^5.0.0",
23+
"@types/node": "^22.0.0",
24+
"concurrently": "^9.2.1",
25+
"cors": "^2.8.5",
26+
"express": "^5.1.0",
27+
"typescript": "^5.9.3",
28+
"vite": "^6.0.0",
29+
"vite-plugin-singlefile": "^2.3.0"
30+
}
31+
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3+
import type {
4+
CallToolResult,
5+
ReadResourceResult,
6+
} from "@modelcontextprotocol/sdk/types.js";
7+
import cors from "cors";
8+
import express, { type Request, type Response } from "express";
9+
import fs from "node:fs/promises";
10+
import os from "node:os";
11+
import path from "node:path";
12+
import si from "systeminformation";
13+
import { z } from "zod";
14+
import { RESOURCE_URI_META_KEY } from "../../dist/src/app";
15+
16+
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
17+
18+
// Schemas - types are derived from these using z.infer
19+
const CpuCoreSchema = z.object({
20+
idle: z.number(),
21+
total: z.number(),
22+
});
23+
24+
const CpuStatsSchema = z.object({
25+
cores: z.array(CpuCoreSchema),
26+
model: z.string(),
27+
count: z.number(),
28+
});
29+
30+
const MemoryStatsSchema = z.object({
31+
usedBytes: z.number(),
32+
totalBytes: z.number(),
33+
usedPercent: z.number(),
34+
freeBytes: z.number(),
35+
usedFormatted: z.string(),
36+
totalFormatted: z.string(),
37+
});
38+
39+
const SystemInfoSchema = z.object({
40+
hostname: z.string(),
41+
platform: z.string(),
42+
arch: z.string(),
43+
uptime: z.number(),
44+
uptimeFormatted: z.string(),
45+
});
46+
47+
const SystemStatsSchema = z.object({
48+
cpu: CpuStatsSchema,
49+
memory: MemoryStatsSchema,
50+
system: SystemInfoSchema,
51+
timestamp: z.string(),
52+
});
53+
54+
// Types derived from schemas
55+
type CpuCore = z.infer<typeof CpuCoreSchema>;
56+
type MemoryStats = z.infer<typeof MemoryStatsSchema>;
57+
type SystemStats = z.infer<typeof SystemStatsSchema>;
58+
const DIST_DIR = path.join(import.meta.dirname, "dist");
59+
60+
// Returns raw CPU timing data per core (client calculates usage from deltas)
61+
function getCpuSnapshots(): CpuCore[] {
62+
return os.cpus().map((cpu) => {
63+
const times = cpu.times;
64+
const idle = times.idle;
65+
const total = times.user + times.nice + times.sys + times.idle + times.irq;
66+
return { idle, total };
67+
});
68+
}
69+
70+
function formatUptime(seconds: number): string {
71+
const days = Math.floor(seconds / 86400);
72+
const hours = Math.floor((seconds % 86400) / 3600);
73+
const minutes = Math.floor((seconds % 3600) / 60);
74+
75+
const parts: string[] = [];
76+
if (days > 0) parts.push(`${days}d`);
77+
if (hours > 0) parts.push(`${hours}h`);
78+
if (minutes > 0) parts.push(`${minutes}m`);
79+
80+
return parts.length > 0 ? parts.join(" ") : "< 1m";
81+
}
82+
83+
function formatBytes(bytes: number): string {
84+
const units = ["B", "KB", "MB", "GB", "TB"];
85+
let value = bytes;
86+
let unitIndex = 0;
87+
88+
while (value >= 1024 && unitIndex < units.length - 1) {
89+
value /= 1024;
90+
unitIndex++;
91+
}
92+
93+
return `${value.toFixed(1)} ${units[unitIndex]}`;
94+
}
95+
96+
async function getMemoryStats(): Promise<MemoryStats> {
97+
const mem = await si.mem();
98+
return {
99+
usedBytes: mem.active,
100+
totalBytes: mem.total,
101+
usedPercent: Math.round((mem.active / mem.total) * 100),
102+
freeBytes: mem.available,
103+
usedFormatted: formatBytes(mem.active),
104+
totalFormatted: formatBytes(mem.total),
105+
};
106+
}
107+
108+
const server = new McpServer({
109+
name: "System Monitor Server",
110+
version: "1.0.0",
111+
});
112+
113+
// Register the get-system-stats tool and its associated UI resource
114+
{
115+
const resourceUri = "ui://system-monitor/mcp-app.html";
116+
117+
server.registerTool(
118+
"get-system-stats",
119+
{
120+
title: "Get System Stats",
121+
description:
122+
"Returns current system statistics including per-core CPU usage, memory, and system info.",
123+
inputSchema: {},
124+
outputSchema: SystemStatsSchema.shape,
125+
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
126+
},
127+
async (): Promise<CallToolResult> => {
128+
const cpuSnapshots = getCpuSnapshots();
129+
const cpuInfo = os.cpus()[0];
130+
const memory = await getMemoryStats();
131+
const uptimeSeconds = os.uptime();
132+
133+
const stats: SystemStats = {
134+
cpu: {
135+
cores: cpuSnapshots,
136+
model: cpuInfo?.model ?? "Unknown",
137+
count: os.cpus().length,
138+
},
139+
memory,
140+
system: {
141+
hostname: os.hostname(),
142+
platform: `${os.platform()} ${os.arch()}`,
143+
arch: os.arch(),
144+
uptime: uptimeSeconds,
145+
uptimeFormatted: formatUptime(uptimeSeconds),
146+
},
147+
timestamp: new Date().toISOString(),
148+
};
149+
150+
return {
151+
content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
152+
structuredContent: stats,
153+
};
154+
},
155+
);
156+
157+
server.registerResource(
158+
resourceUri,
159+
resourceUri,
160+
{ description: "System Monitor UI" },
161+
async (): Promise<ReadResourceResult> => {
162+
const html = await fs.readFile(
163+
path.join(DIST_DIR, "mcp-app.html"),
164+
"utf-8",
165+
);
166+
167+
return {
168+
contents: [{ uri: resourceUri, mimeType: "text/html+mcp", text: html }],
169+
};
170+
},
171+
);
172+
}
173+
174+
const app = express();
175+
app.use(cors());
176+
app.use(express.json());
177+
178+
app.post("/mcp", async (req: Request, res: Response) => {
179+
try {
180+
const transport = new StreamableHTTPServerTransport({
181+
sessionIdGenerator: undefined,
182+
enableJsonResponse: true,
183+
});
184+
res.on("close", () => {
185+
transport.close();
186+
});
187+
188+
await server.connect(transport);
189+
190+
await transport.handleRequest(req, res, req.body);
191+
} catch (error) {
192+
console.error("Error handling MCP request:", error);
193+
if (!res.headersSent) {
194+
res.status(500).json({
195+
jsonrpc: "2.0",
196+
error: { code: -32603, message: "Internal server error" },
197+
id: null,
198+
});
199+
}
200+
}
201+
});
202+
203+
const httpServer = app.listen(PORT, () => {
204+
console.log(
205+
`System Monitor Server listening on http://localhost:${PORT}/mcp`,
206+
);
207+
});
208+
209+
function shutdown() {
210+
console.log("\nShutting down...");
211+
httpServer.close(() => {
212+
console.log("Server closed");
213+
process.exit(0);
214+
});
215+
}
216+
217+
process.on("SIGINT", shutdown);
218+
process.on("SIGTERM", shutdown);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
* {
2+
box-sizing: border-box;
3+
}
4+
5+
html, body {
6+
font-family: system-ui, -apple-system, sans-serif;
7+
font-size: 1rem;
8+
}
9+
10+
code {
11+
font-size: 1em;
12+
}

0 commit comments

Comments
 (0)