ext-apps/examples/system-monitor-server/server.ts
Jonathan Hefner 9acebd7f9f
Separate static and dynamic data in system-monitor-server tools
The two-tool architecture previously had both tools returning identical
`SystemStats` data. This refactors them to properly separate concerns:

- `get-system-info` (Model-visible): Returns static system configuration
  (hostname, platform, CPU model/count, total memory) sent once via
  `ontoolresult`

- `poll-system-stats` (App-only): Returns dynamic metrics (per-core CPU
  timing, memory usage, uptime) polled every 2 seconds

Moves `formatBytes()` and `formatUptime()` utilities to the client since
the server now returns raw values for client-side formatting.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 11:04:35 -06:00

196 lines
5.1 KiB
TypeScript

import {
RESOURCE_MIME_TYPE,
registerAppResource,
registerAppTool,
} from "@modelcontextprotocol/ext-apps/server";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type {
CallToolResult,
ReadResourceResult,
} from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import si from "systeminformation";
import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
? path.join(import.meta.dirname, "dist")
: import.meta.dirname;
// =============================================================================
// Types and schemas
// =============================================================================
const SystemInfoSchema = z.object({
hostname: z.string(),
platform: z.string(),
arch: z.string(),
cpu: z.object({
model: z.string(),
count: z.number(),
}),
memory: z.object({
totalBytes: z.number(),
}),
});
type SystemInfo = z.infer<typeof SystemInfoSchema>;
const CpuCoreSchema = z.object({
idle: z.number(),
total: z.number(),
});
type CpuCore = z.infer<typeof CpuCoreSchema>;
const PollStatsSchema = z.object({
cpu: z.object({
cores: z.array(CpuCoreSchema),
}),
memory: z.object({
usedBytes: z.number(),
usedPercent: z.number(),
freeBytes: z.number(),
}),
uptime: z.object({
seconds: z.number(),
}),
timestamp: z.string(),
});
type PollStats = z.infer<typeof PollStatsSchema>;
// =============================================================================
// Static system info (called once by Model-facing tool)
// =============================================================================
function getSystemInfo(): SystemInfo {
const cpuInfo = os.cpus()[0];
return {
hostname: os.hostname(),
platform: `${os.platform()} ${os.arch()}`,
arch: os.arch(),
cpu: {
model: cpuInfo?.model ?? "Unknown",
count: os.cpus().length,
},
memory: {
totalBytes: os.totalmem(),
},
};
}
// =============================================================================
// Dynamic polling stats (called repeatedly by app-only tool)
// =============================================================================
// 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 };
});
}
async function getPollStats(): Promise<PollStats> {
const mem = await si.mem();
const uptimeSeconds = os.uptime();
return {
cpu: {
cores: getCpuSnapshots(),
},
memory: {
usedBytes: mem.active,
usedPercent: Math.round((mem.active / mem.total) * 100),
freeBytes: mem.available,
},
uptime: {
seconds: uptimeSeconds,
},
timestamp: new Date().toISOString(),
};
}
// =============================================================================
// MCP server
// =============================================================================
export function createServer(): McpServer {
const server = new McpServer({
name: "System Monitor Server",
version: "1.0.0",
});
const resourceUri = "ui://system-monitor/mcp-app.html";
// Model-facing tool: returns static system configuration
registerAppTool(
server,
"get-system-info",
{
title: "Get System Info",
description:
"Returns system information, including hostname, platform, CPU info, and memory.",
inputSchema: {},
outputSchema: SystemInfoSchema.shape,
_meta: { ui: { resourceUri } },
},
(): CallToolResult => {
const info = getSystemInfo();
return {
content: [{ type: "text", text: JSON.stringify(info) }],
structuredContent: info,
};
},
);
// App-only tool: returns dynamic metrics for polling
registerAppTool(
server,
"poll-system-stats",
{
title: "Poll System Stats",
description:
"Returns dynamic system metrics for polling: per-core CPU timing, memory usage, and uptime. App-only.",
inputSchema: {},
outputSchema: PollStatsSchema.shape,
_meta: { ui: { visibility: ["app"] } },
},
async (): Promise<CallToolResult> => {
const stats = await getPollStats();
return {
content: [{ type: "text", text: JSON.stringify(stats) }],
structuredContent: stats,
};
},
);
registerAppResource(
server,
resourceUri,
resourceUri,
{ mimeType: RESOURCE_MIME_TYPE, description: "System Monitor UI" },
async (): Promise<ReadResourceResult> => {
const html = await fs.readFile(
path.join(DIST_DIR, "mcp-app.html"),
"utf-8",
);
return {
contents: [
{
uri: resourceUri,
mimeType: RESOURCE_MIME_TYPE,
text: html,
},
],
};
},
);
return server;
}