Merge pull request #388 from jonathanhefner/system-monitor-static-vs-dynamic
Separate static and dynamic data in `system-monitor-server` tools
This commit is contained in:
commit
55f0ef0198
3 changed files with 206 additions and 149 deletions
|
|
@ -79,18 +79,24 @@ To test local modifications, use this configuration (replace `~/code/ext-apps` w
|
||||||
|
|
||||||
### Server (`server.ts`)
|
### Server (`server.ts`)
|
||||||
|
|
||||||
Exposes a single `get-system-stats` tool that returns:
|
Exposes two tools demonstrating a polling pattern:
|
||||||
|
|
||||||
- Raw per-core CPU timing data (idle/total counters)
|
1. **`get-system-info`** (Model-visible) — Returns static system configuration:
|
||||||
- Memory usage (used/total/percentage)
|
- Hostname, platform, architecture
|
||||||
- System info (hostname, platform, uptime)
|
- CPU model and core count
|
||||||
|
- Total memory
|
||||||
|
|
||||||
The tool is linked to a UI resource via `_meta.ui.resourceUri`.
|
2. **`poll-system-stats`** (App-only, `visibility: ["app"]`) — Returns dynamic metrics:
|
||||||
|
- Per-core CPU timing data (idle/total counters)
|
||||||
|
- Memory usage (used/free/percentage)
|
||||||
|
- Uptime
|
||||||
|
|
||||||
|
The Model-visible tool is linked to a UI resource via `_meta.ui.resourceUri`.
|
||||||
|
|
||||||
### App (`src/mcp-app.ts`)
|
### App (`src/mcp-app.ts`)
|
||||||
|
|
||||||
|
- Receives static system info via `ontoolresult` when the host sends the `get-system-info` result
|
||||||
|
- Polls `poll-system-stats` every 2 seconds for dynamic metrics
|
||||||
- Uses Chart.js for the stacked area chart visualization
|
- 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
|
- Computes CPU usage percentages client-side from timing deltas
|
||||||
- Maintains a 30-point history (1 minute at 2s intervals)
|
- Maintains a 30-point history (1 minute at 2s intervals)
|
||||||
- Updates all UI elements on each poll
|
|
||||||
|
|
|
||||||
|
|
@ -13,50 +13,78 @@ import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import si from "systeminformation";
|
import si from "systeminformation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
// Schemas - types are derived from these using z.infer
|
|
||||||
const CpuCoreSchema = z.object({
|
|
||||||
idle: z.number(),
|
|
||||||
total: z.number(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const CpuStatsSchema = z.object({
|
// Works both from source (server.ts) and compiled (dist/server.js)
|
||||||
cores: z.array(CpuCoreSchema),
|
const DIST_DIR = import.meta.filename.endsWith(".ts")
|
||||||
model: z.string(),
|
? path.join(import.meta.dirname, "dist")
|
||||||
count: z.number(),
|
: import.meta.dirname;
|
||||||
});
|
|
||||||
|
|
||||||
const MemoryStatsSchema = z.object({
|
// =============================================================================
|
||||||
usedBytes: z.number(),
|
// Types and schemas
|
||||||
totalBytes: z.number(),
|
// =============================================================================
|
||||||
usedPercent: z.number(),
|
|
||||||
freeBytes: z.number(),
|
|
||||||
usedFormatted: z.string(),
|
|
||||||
totalFormatted: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const SystemInfoSchema = z.object({
|
const SystemInfoSchema = z.object({
|
||||||
hostname: z.string(),
|
hostname: z.string(),
|
||||||
platform: z.string(),
|
platform: z.string(),
|
||||||
arch: z.string(),
|
arch: z.string(),
|
||||||
uptime: z.number(),
|
cpu: z.object({
|
||||||
uptimeFormatted: z.string(),
|
model: z.string(),
|
||||||
|
count: z.number(),
|
||||||
|
}),
|
||||||
|
memory: z.object({
|
||||||
|
totalBytes: z.number(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const SystemStatsSchema = z.object({
|
type SystemInfo = z.infer<typeof SystemInfoSchema>;
|
||||||
cpu: CpuStatsSchema,
|
|
||||||
memory: MemoryStatsSchema,
|
const CpuCoreSchema = z.object({
|
||||||
system: SystemInfoSchema,
|
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(),
|
timestamp: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Types derived from schemas
|
type PollStats = z.infer<typeof PollStatsSchema>;
|
||||||
type CpuCore = z.infer<typeof CpuCoreSchema>;
|
|
||||||
type MemoryStats = z.infer<typeof MemoryStatsSchema>;
|
// =============================================================================
|
||||||
type SystemStats = z.infer<typeof SystemStatsSchema>;
|
// Static system info (called once by Model-facing tool)
|
||||||
// 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")
|
function getSystemInfo(): SystemInfo {
|
||||||
: import.meta.dirname;
|
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)
|
// Returns raw CPU timing data per core (client calculates usage from deltas)
|
||||||
function getCpuSnapshots(): CpuCore[] {
|
function getCpuSnapshots(): CpuCore[] {
|
||||||
|
|
@ -67,111 +95,73 @@ function getCpuSnapshots(): CpuCore[] {
|
||||||
return { idle, total };
|
return { idle, total };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
async function getPollStats(): Promise<PollStats> {
|
||||||
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<MemoryStats> {
|
|
||||||
const mem = await si.mem();
|
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),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getStats(): Promise<SystemStats> {
|
|
||||||
const cpuSnapshots = getCpuSnapshots();
|
|
||||||
const cpuInfo = os.cpus()[0];
|
|
||||||
const memory = await getMemoryStats();
|
|
||||||
const uptimeSeconds = os.uptime();
|
const uptimeSeconds = os.uptime();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cpu: {
|
cpu: {
|
||||||
cores: cpuSnapshots,
|
cores: getCpuSnapshots(),
|
||||||
model: cpuInfo?.model ?? "Unknown",
|
|
||||||
count: os.cpus().length,
|
|
||||||
},
|
},
|
||||||
memory,
|
memory: {
|
||||||
system: {
|
usedBytes: mem.active,
|
||||||
hostname: os.hostname(),
|
usedPercent: Math.round((mem.active / mem.total) * 100),
|
||||||
platform: `${os.platform()} ${os.arch()}`,
|
freeBytes: mem.available,
|
||||||
arch: os.arch(),
|
},
|
||||||
uptime: uptimeSeconds,
|
uptime: {
|
||||||
uptimeFormatted: formatUptime(uptimeSeconds),
|
seconds: uptimeSeconds,
|
||||||
},
|
},
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// MCP server
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
export function createServer(): McpServer {
|
export function createServer(): McpServer {
|
||||||
const server = new McpServer({
|
const server = new McpServer({
|
||||||
name: "System Monitor Server",
|
name: "System Monitor Server",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register the get-system-stats tool and its associated UI resource
|
|
||||||
const resourceUri = "ui://system-monitor/mcp-app.html";
|
const resourceUri = "ui://system-monitor/mcp-app.html";
|
||||||
|
|
||||||
|
// Model-facing tool: returns static system configuration
|
||||||
registerAppTool(
|
registerAppTool(
|
||||||
server,
|
server,
|
||||||
"get-system-stats",
|
"get-system-info",
|
||||||
{
|
{
|
||||||
title: "Get System Stats",
|
title: "Get System Info",
|
||||||
description:
|
description:
|
||||||
"Returns current system statistics including per-core CPU usage, memory, and system info.",
|
"Returns system information, including hostname, platform, CPU info, and memory.",
|
||||||
inputSchema: {},
|
inputSchema: {},
|
||||||
outputSchema: SystemStatsSchema.shape,
|
outputSchema: SystemInfoSchema.shape,
|
||||||
_meta: { ui: { resourceUri } },
|
_meta: { ui: { resourceUri } },
|
||||||
},
|
},
|
||||||
async (): Promise<CallToolResult> => {
|
(): CallToolResult => {
|
||||||
const stats = await getStats();
|
const info = getSystemInfo();
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: JSON.stringify(stats) }],
|
content: [{ type: "text", text: JSON.stringify(info) }],
|
||||||
structuredContent: stats,
|
structuredContent: info,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// App-only tool for polling - used by the UI for periodic refresh
|
// App-only tool: returns dynamic metrics for polling
|
||||||
registerAppTool(
|
registerAppTool(
|
||||||
server,
|
server,
|
||||||
"refresh-stats",
|
"poll-system-stats",
|
||||||
{
|
{
|
||||||
title: "Refresh Stats",
|
title: "Poll System Stats",
|
||||||
description: "Refresh system statistics (app-only, for polling)",
|
description:
|
||||||
|
"Returns dynamic system metrics for polling: per-core CPU timing, memory usage, and uptime. App-only.",
|
||||||
inputSchema: {},
|
inputSchema: {},
|
||||||
outputSchema: SystemStatsSchema.shape,
|
outputSchema: PollStatsSchema.shape,
|
||||||
_meta: { ui: { visibility: ["app"] } },
|
_meta: { ui: { visibility: ["app"] } },
|
||||||
},
|
},
|
||||||
async (): Promise<CallToolResult> => {
|
async (): Promise<CallToolResult> => {
|
||||||
const stats = await getStats();
|
const stats = await getPollStats();
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: JSON.stringify(stats) }],
|
content: [{ type: "text", text: JSON.stringify(stats) }],
|
||||||
structuredContent: stats,
|
structuredContent: stats,
|
||||||
|
|
|
||||||
|
|
@ -13,36 +13,44 @@ Chart.register(...registerables);
|
||||||
// Types
|
// Types
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
interface SystemStats {
|
// Static system info (received once via ontoolresult from LLM-facing tool)
|
||||||
|
interface SystemInfo {
|
||||||
|
hostname: string;
|
||||||
|
platform: string;
|
||||||
|
arch: string;
|
||||||
cpu: {
|
cpu: {
|
||||||
cores: Array<{ idle: number; total: number }>;
|
|
||||||
model: string;
|
model: string;
|
||||||
count: number;
|
count: number;
|
||||||
};
|
};
|
||||||
memory: {
|
memory: {
|
||||||
usedBytes: number;
|
|
||||||
totalBytes: number;
|
totalBytes: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic polling data (received repeatedly via poll-system-stats)
|
||||||
|
interface PollStats {
|
||||||
|
cpu: {
|
||||||
|
cores: Array<{ idle: number; total: number }>;
|
||||||
|
};
|
||||||
|
memory: {
|
||||||
|
usedBytes: number;
|
||||||
usedPercent: number;
|
usedPercent: number;
|
||||||
freeBytes: number;
|
freeBytes: number;
|
||||||
usedFormatted: string;
|
|
||||||
totalFormatted: string;
|
|
||||||
};
|
};
|
||||||
system: {
|
uptime: {
|
||||||
hostname: string;
|
seconds: number;
|
||||||
platform: string;
|
|
||||||
arch: string;
|
|
||||||
uptime: number;
|
|
||||||
uptimeFormatted: string;
|
|
||||||
};
|
};
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PollingState {
|
interface AppState {
|
||||||
|
// Static info (set once via ontoolresult)
|
||||||
|
systemInfo: SystemInfo | null;
|
||||||
|
// Polling state
|
||||||
isPolling: boolean;
|
isPolling: boolean;
|
||||||
intervalId: number | null;
|
intervalId: number | null;
|
||||||
cpuHistory: number[][]; // [timestamp][coreIndex] = usage%
|
cpuHistory: number[][]; // [timestamp][coreIndex] = usage%
|
||||||
labels: string[];
|
labels: string[];
|
||||||
coreCount: number;
|
|
||||||
chart: Chart | null;
|
chart: Chart | null;
|
||||||
previousCpuSnapshots: Array<{ idle: number; total: number }> | null;
|
previousCpuSnapshots: Array<{ idle: number; total: number }> | null;
|
||||||
}
|
}
|
||||||
|
|
@ -92,16 +100,42 @@ const CORE_COLORS = [
|
||||||
"rgba(244, 114, 182, 0.7)", // pink-light
|
"rgba(244, 114, 182, 0.7)", // pink-light
|
||||||
];
|
];
|
||||||
|
|
||||||
const state: PollingState = {
|
const state: AppState = {
|
||||||
|
systemInfo: null,
|
||||||
isPolling: false,
|
isPolling: false,
|
||||||
intervalId: null,
|
intervalId: null,
|
||||||
cpuHistory: [],
|
cpuHistory: [],
|
||||||
labels: [],
|
labels: [],
|
||||||
coreCount: 0,
|
|
||||||
chart: null,
|
chart: null,
|
||||||
previousCpuSnapshots: null,
|
previousCpuSnapshots: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Formatting Utilities
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
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]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Chart.js Setup
|
// Chart.js Setup
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -179,13 +213,17 @@ function initChart(coreCount: number): Chart {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateChart(cpuHistory: number[][], labels: string[]): void {
|
function updateChart(
|
||||||
|
cpuHistory: number[][],
|
||||||
|
labels: string[],
|
||||||
|
coreCount: number,
|
||||||
|
): void {
|
||||||
if (!state.chart) return;
|
if (!state.chart) return;
|
||||||
|
|
||||||
state.chart.data.labels = labels;
|
state.chart.data.labels = labels;
|
||||||
|
|
||||||
// Transpose: cpuHistory[time][core] -> datasets[core].data[time]
|
// Transpose: cpuHistory[time][core] -> datasets[core].data[time]
|
||||||
for (let coreIdx = 0; coreIdx < state.coreCount; coreIdx++) {
|
for (let coreIdx = 0; coreIdx < coreCount; coreIdx++) {
|
||||||
state.chart.data.datasets[coreIdx].data = cpuHistory.map(
|
state.chart.data.datasets[coreIdx].data = cpuHistory.map(
|
||||||
(snapshot) => snapshot[coreIdx] ?? 0,
|
(snapshot) => snapshot[coreIdx] ?? 0,
|
||||||
);
|
);
|
||||||
|
|
@ -200,8 +238,8 @@ function updateChart(cpuHistory: number[][], labels: string[]): void {
|
||||||
|
|
||||||
// Add 20% headroom, clamp to reasonable bounds
|
// Add 20% headroom, clamp to reasonable bounds
|
||||||
const headroom = 1.2;
|
const headroom = 1.2;
|
||||||
const minVisible = state.coreCount * 15; // At least 15% per core visible
|
const minVisible = coreCount * 15; // At least 15% per core visible
|
||||||
const absoluteMax = state.coreCount * 100;
|
const absoluteMax = coreCount * 100;
|
||||||
|
|
||||||
const dynamicMax = Math.min(
|
const dynamicMax = Math.min(
|
||||||
Math.max(currentMax * headroom, minVisible),
|
Math.max(currentMax * headroom, minVisible),
|
||||||
|
|
@ -217,7 +255,10 @@ function updateChart(cpuHistory: number[][], labels: string[]): void {
|
||||||
// UI Updates
|
// UI Updates
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
function updateMemoryBar(memory: SystemStats["memory"]): void {
|
function updateMemoryBar(
|
||||||
|
memory: PollStats["memory"],
|
||||||
|
totalBytes: number,
|
||||||
|
): void {
|
||||||
const percent = memory.usedPercent;
|
const percent = memory.usedPercent;
|
||||||
|
|
||||||
memoryBarFill.style.width = `${percent}%`;
|
memoryBarFill.style.width = `${percent}%`;
|
||||||
|
|
@ -230,13 +271,18 @@ function updateMemoryBar(memory: SystemStats["memory"]): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
memoryPercent.textContent = `${percent}%`;
|
memoryPercent.textContent = `${percent}%`;
|
||||||
memoryDetail.textContent = `${memory.usedFormatted} / ${memory.totalFormatted}`;
|
memoryDetail.textContent = `${formatBytes(memory.usedBytes)} / ${formatBytes(totalBytes)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSystemInfo(system: SystemStats["system"]): void {
|
// Called once when static system info is received
|
||||||
infoHostname.textContent = system.hostname;
|
function updateStaticSystemInfo(info: SystemInfo): void {
|
||||||
infoPlatform.textContent = system.platform;
|
infoHostname.textContent = info.hostname;
|
||||||
infoUptime.textContent = system.uptimeFormatted;
|
infoPlatform.textContent = info.platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called on each poll for dynamic uptime
|
||||||
|
function updateDynamicUptime(uptime: PollStats["uptime"]): void {
|
||||||
|
infoUptime.textContent = formatUptime(uptime.seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStatus(text: string, isPolling = false, isError = false): void {
|
function updateStatus(text: string, isPolling = false, isError = false): void {
|
||||||
|
|
@ -273,19 +319,18 @@ function calculateCpuUsage(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchStats(): Promise<void> {
|
async function fetchStats(): Promise<void> {
|
||||||
|
// systemInfo is guaranteed to exist (polling starts after ontoolresult)
|
||||||
|
const { systemInfo } = state;
|
||||||
|
if (!systemInfo) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await app.callServerTool({
|
const result = await app.callServerTool({
|
||||||
name: "refresh-stats", // Use app-only tool for polling
|
name: "poll-system-stats", // App-only tool for polling dynamic data
|
||||||
arguments: {},
|
arguments: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const stats = result.structuredContent as unknown as SystemStats;
|
const stats = result.structuredContent as unknown as PollStats;
|
||||||
|
const coreCount = systemInfo.cpu.count;
|
||||||
// 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)
|
// Calculate CPU usage from raw timing data (client-side)
|
||||||
const coreUsages = calculateCpuUsage(
|
const coreUsages = calculateCpuUsage(
|
||||||
|
|
@ -302,10 +347,10 @@ async function fetchStats(): Promise<void> {
|
||||||
state.labels.shift();
|
state.labels.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update UI
|
// Update UI with dynamic data
|
||||||
updateChart(state.cpuHistory, state.labels);
|
updateChart(state.cpuHistory, state.labels, coreCount);
|
||||||
updateMemoryBar(stats.memory);
|
updateMemoryBar(stats.memory, systemInfo.memory.totalBytes);
|
||||||
updateSystemInfo(stats.system);
|
updateDynamicUptime(stats.uptime);
|
||||||
|
|
||||||
const time = new Date().toLocaleTimeString("en-US", { hour12: false });
|
const time = new Date().toLocaleTimeString("en-US", { hour12: false });
|
||||||
updateStatus(time, true);
|
updateStatus(time, true);
|
||||||
|
|
@ -366,15 +411,34 @@ pollToggleBtn.addEventListener("click", togglePolling);
|
||||||
window
|
window
|
||||||
.matchMedia("(prefers-color-scheme: dark)")
|
.matchMedia("(prefers-color-scheme: dark)")
|
||||||
.addEventListener("change", () => {
|
.addEventListener("change", () => {
|
||||||
if (state.chart) {
|
if (state.chart && state.systemInfo) {
|
||||||
|
const coreCount = state.systemInfo.cpu.count;
|
||||||
state.chart.destroy();
|
state.chart.destroy();
|
||||||
state.chart = initChart(state.coreCount);
|
state.chart = initChart(coreCount);
|
||||||
updateChart(state.cpuHistory, state.labels);
|
updateChart(state.cpuHistory, state.labels, coreCount);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.onerror = console.error;
|
app.onerror = console.error;
|
||||||
|
|
||||||
|
// Receive static system info when host sends the initial tool result
|
||||||
|
app.ontoolresult = (result) => {
|
||||||
|
const info = result.structuredContent as unknown as SystemInfo;
|
||||||
|
|
||||||
|
if (info) {
|
||||||
|
state.systemInfo = info;
|
||||||
|
|
||||||
|
// Initialize chart with CPU count from static info
|
||||||
|
state.chart = initChart(info.cpu.count);
|
||||||
|
|
||||||
|
// Update static UI elements (hostname, platform)
|
||||||
|
updateStaticSystemInfo(info);
|
||||||
|
|
||||||
|
// Start polling after receiving static info
|
||||||
|
startPolling();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function handleHostContextChanged(ctx: McpUiHostContext) {
|
function handleHostContextChanged(ctx: McpUiHostContext) {
|
||||||
if (ctx.safeAreaInsets) {
|
if (ctx.safeAreaInsets) {
|
||||||
mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
|
mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
|
||||||
|
|
@ -392,6 +456,3 @@ app.connect().then(() => {
|
||||||
handleHostContextChanged(ctx);
|
handleHostContextChanged(ctx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-start polling after a short delay
|
|
||||||
setTimeout(startPolling, 500);
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue