Skip to content

Commit 55f0ef0

Browse files
Merge pull request modelcontextprotocol#388 from jonathanhefner/system-monitor-static-vs-dynamic
Separate static and dynamic data in `system-monitor-server` tools
2 parents 6028cf2 + 9acebd7 commit 55f0ef0

File tree

3 files changed

+206
-149
lines changed

3 files changed

+206
-149
lines changed

examples/system-monitor-server/README.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,18 +79,24 @@ To test local modifications, use this configuration (replace `~/code/ext-apps` w
7979

8080
### Server (`server.ts`)
8181

82-
Exposes a single `get-system-stats` tool that returns:
82+
Exposes two tools demonstrating a polling pattern:
8383

84-
- Raw per-core CPU timing data (idle/total counters)
85-
- Memory usage (used/total/percentage)
86-
- System info (hostname, platform, uptime)
84+
1. **`get-system-info`** (Model-visible) — Returns static system configuration:
85+
- Hostname, platform, architecture
86+
- CPU model and core count
87+
- Total memory
8788

88-
The tool is linked to a UI resource via `_meta.ui.resourceUri`.
89+
2. **`poll-system-stats`** (App-only, `visibility: ["app"]`) — Returns dynamic metrics:
90+
- Per-core CPU timing data (idle/total counters)
91+
- Memory usage (used/free/percentage)
92+
- Uptime
93+
94+
The Model-visible tool is linked to a UI resource via `_meta.ui.resourceUri`.
8995

9096
### App (`src/mcp-app.ts`)
9197

98+
- Receives static system info via `ontoolresult` when the host sends the `get-system-info` result
99+
- Polls `poll-system-stats` every 2 seconds for dynamic metrics
92100
- Uses Chart.js for the stacked area chart visualization
93-
- Polls the server tool every 2 seconds
94101
- Computes CPU usage percentages client-side from timing deltas
95102
- Maintains a 30-point history (1 minute at 2s intervals)
96-
- Updates all UI elements on each poll

examples/system-monitor-server/server.ts

Lines changed: 89 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -13,50 +13,78 @@ import os from "node:os";
1313
import path from "node:path";
1414
import si from "systeminformation";
1515
import { z } from "zod";
16-
// Schemas - types are derived from these using z.infer
17-
const CpuCoreSchema = z.object({
18-
idle: z.number(),
19-
total: z.number(),
20-
});
2116

22-
const CpuStatsSchema = z.object({
23-
cores: z.array(CpuCoreSchema),
24-
model: z.string(),
25-
count: z.number(),
26-
});
17+
// Works both from source (server.ts) and compiled (dist/server.js)
18+
const DIST_DIR = import.meta.filename.endsWith(".ts")
19+
? path.join(import.meta.dirname, "dist")
20+
: import.meta.dirname;
2721

28-
const MemoryStatsSchema = z.object({
29-
usedBytes: z.number(),
30-
totalBytes: z.number(),
31-
usedPercent: z.number(),
32-
freeBytes: z.number(),
33-
usedFormatted: z.string(),
34-
totalFormatted: z.string(),
35-
});
22+
// =============================================================================
23+
// Types and schemas
24+
// =============================================================================
3625

3726
const SystemInfoSchema = z.object({
3827
hostname: z.string(),
3928
platform: z.string(),
4029
arch: z.string(),
41-
uptime: z.number(),
42-
uptimeFormatted: z.string(),
30+
cpu: z.object({
31+
model: z.string(),
32+
count: z.number(),
33+
}),
34+
memory: z.object({
35+
totalBytes: z.number(),
36+
}),
4337
});
4438

45-
const SystemStatsSchema = z.object({
46-
cpu: CpuStatsSchema,
47-
memory: MemoryStatsSchema,
48-
system: SystemInfoSchema,
49-
timestamp: z.string(),
39+
type SystemInfo = z.infer<typeof SystemInfoSchema>;
40+
41+
const CpuCoreSchema = z.object({
42+
idle: z.number(),
43+
total: z.number(),
5044
});
5145

52-
// Types derived from schemas
5346
type CpuCore = z.infer<typeof CpuCoreSchema>;
54-
type MemoryStats = z.infer<typeof MemoryStatsSchema>;
55-
type SystemStats = z.infer<typeof SystemStatsSchema>;
56-
// Works both from source (server.ts) and compiled (dist/server.js)
57-
const DIST_DIR = import.meta.filename.endsWith(".ts")
58-
? path.join(import.meta.dirname, "dist")
59-
: import.meta.dirname;
47+
48+
const PollStatsSchema = z.object({
49+
cpu: z.object({
50+
cores: z.array(CpuCoreSchema),
51+
}),
52+
memory: z.object({
53+
usedBytes: z.number(),
54+
usedPercent: z.number(),
55+
freeBytes: z.number(),
56+
}),
57+
uptime: z.object({
58+
seconds: z.number(),
59+
}),
60+
timestamp: z.string(),
61+
});
62+
63+
type PollStats = z.infer<typeof PollStatsSchema>;
64+
65+
// =============================================================================
66+
// Static system info (called once by Model-facing tool)
67+
// =============================================================================
68+
69+
function getSystemInfo(): SystemInfo {
70+
const cpuInfo = os.cpus()[0];
71+
return {
72+
hostname: os.hostname(),
73+
platform: `${os.platform()} ${os.arch()}`,
74+
arch: os.arch(),
75+
cpu: {
76+
model: cpuInfo?.model ?? "Unknown",
77+
count: os.cpus().length,
78+
},
79+
memory: {
80+
totalBytes: os.totalmem(),
81+
},
82+
};
83+
}
84+
85+
// =============================================================================
86+
// Dynamic polling stats (called repeatedly by app-only tool)
87+
// =============================================================================
6088

6189
// Returns raw CPU timing data per core (client calculates usage from deltas)
6290
function getCpuSnapshots(): CpuCore[] {
@@ -67,111 +95,73 @@ function getCpuSnapshots(): CpuCore[] {
6795
return { idle, total };
6896
});
6997
}
70-
71-
function formatUptime(seconds: number): string {
72-
const days = Math.floor(seconds / 86400);
73-
const hours = Math.floor((seconds % 86400) / 3600);
74-
const minutes = Math.floor((seconds % 3600) / 60);
75-
76-
const parts: string[] = [];
77-
if (days > 0) parts.push(`${days}d`);
78-
if (hours > 0) parts.push(`${hours}h`);
79-
if (minutes > 0) parts.push(`${minutes}m`);
80-
81-
return parts.length > 0 ? parts.join(" ") : "< 1m";
82-
}
83-
84-
function formatBytes(bytes: number): string {
85-
const units = ["B", "KB", "MB", "GB", "TB"];
86-
let value = bytes;
87-
let unitIndex = 0;
88-
89-
while (value >= 1024 && unitIndex < units.length - 1) {
90-
value /= 1024;
91-
unitIndex++;
92-
}
93-
94-
return `${value.toFixed(1)} ${units[unitIndex]}`;
95-
}
96-
97-
async function getMemoryStats(): Promise<MemoryStats> {
98+
async function getPollStats(): Promise<PollStats> {
9899
const mem = await si.mem();
99-
return {
100-
usedBytes: mem.active,
101-
totalBytes: mem.total,
102-
usedPercent: Math.round((mem.active / mem.total) * 100),
103-
freeBytes: mem.available,
104-
usedFormatted: formatBytes(mem.active),
105-
totalFormatted: formatBytes(mem.total),
106-
};
107-
}
108-
109-
async function getStats(): Promise<SystemStats> {
110-
const cpuSnapshots = getCpuSnapshots();
111-
const cpuInfo = os.cpus()[0];
112-
const memory = await getMemoryStats();
113100
const uptimeSeconds = os.uptime();
114101

115102
return {
116103
cpu: {
117-
cores: cpuSnapshots,
118-
model: cpuInfo?.model ?? "Unknown",
119-
count: os.cpus().length,
104+
cores: getCpuSnapshots(),
105+
},
106+
memory: {
107+
usedBytes: mem.active,
108+
usedPercent: Math.round((mem.active / mem.total) * 100),
109+
freeBytes: mem.available,
120110
},
121-
memory,
122-
system: {
123-
hostname: os.hostname(),
124-
platform: `${os.platform()} ${os.arch()}`,
125-
arch: os.arch(),
126-
uptime: uptimeSeconds,
127-
uptimeFormatted: formatUptime(uptimeSeconds),
111+
uptime: {
112+
seconds: uptimeSeconds,
128113
},
129114
timestamp: new Date().toISOString(),
130115
};
131116
}
132117

118+
// =============================================================================
119+
// MCP server
120+
// =============================================================================
121+
133122
export function createServer(): McpServer {
134123
const server = new McpServer({
135124
name: "System Monitor Server",
136125
version: "1.0.0",
137126
});
138127

139-
// Register the get-system-stats tool and its associated UI resource
140128
const resourceUri = "ui://system-monitor/mcp-app.html";
141129

130+
// Model-facing tool: returns static system configuration
142131
registerAppTool(
143132
server,
144-
"get-system-stats",
133+
"get-system-info",
145134
{
146-
title: "Get System Stats",
135+
title: "Get System Info",
147136
description:
148-
"Returns current system statistics including per-core CPU usage, memory, and system info.",
137+
"Returns system information, including hostname, platform, CPU info, and memory.",
149138
inputSchema: {},
150-
outputSchema: SystemStatsSchema.shape,
139+
outputSchema: SystemInfoSchema.shape,
151140
_meta: { ui: { resourceUri } },
152141
},
153-
async (): Promise<CallToolResult> => {
154-
const stats = await getStats();
142+
(): CallToolResult => {
143+
const info = getSystemInfo();
155144
return {
156-
content: [{ type: "text", text: JSON.stringify(stats) }],
157-
structuredContent: stats,
145+
content: [{ type: "text", text: JSON.stringify(info) }],
146+
structuredContent: info,
158147
};
159148
},
160149
);
161150

162-
// App-only tool for polling - used by the UI for periodic refresh
151+
// App-only tool: returns dynamic metrics for polling
163152
registerAppTool(
164153
server,
165-
"refresh-stats",
154+
"poll-system-stats",
166155
{
167-
title: "Refresh Stats",
168-
description: "Refresh system statistics (app-only, for polling)",
156+
title: "Poll System Stats",
157+
description:
158+
"Returns dynamic system metrics for polling: per-core CPU timing, memory usage, and uptime. App-only.",
169159
inputSchema: {},
170-
outputSchema: SystemStatsSchema.shape,
160+
outputSchema: PollStatsSchema.shape,
171161
_meta: { ui: { visibility: ["app"] } },
172162
},
173163
async (): Promise<CallToolResult> => {
174-
const stats = await getStats();
164+
const stats = await getPollStats();
175165
return {
176166
content: [{ type: "text", text: JSON.stringify(stats) }],
177167
structuredContent: stats,

0 commit comments

Comments
 (0)