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 <noreply@anthropic.com>
This commit is contained in:
parent
f1c995fb99
commit
fa2d413f1d
10 changed files with 1075 additions and 0 deletions
49
examples/system-monitor-server/README.md
Normal file
49
examples/system-monitor-server/README.md
Normal file
|
|
@ -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
|
||||||
60
examples/system-monitor-server/mcp-app.html
Normal file
60
examples/system-monitor-server/mcp-app.html
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>System Monitor</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="main">
|
||||||
|
<header class="header">
|
||||||
|
<h1 class="title">System Monitor</h1>
|
||||||
|
<div class="header-controls">
|
||||||
|
<button id="poll-toggle-btn" class="btn">Start</button>
|
||||||
|
<span class="status">
|
||||||
|
<span id="status-indicator" class="status-indicator"></span>
|
||||||
|
<span id="status-text" class="status-text">Ready</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="chart-section">
|
||||||
|
<h2 class="section-title">CPU Usage</h2>
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="cpu-chart"></canvas>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="memory-section">
|
||||||
|
<h2 class="section-title">Memory</h2>
|
||||||
|
<div class="memory-bar-container">
|
||||||
|
<div class="memory-bar">
|
||||||
|
<div id="memory-bar-fill" class="memory-bar-fill"></div>
|
||||||
|
</div>
|
||||||
|
<span id="memory-percent" class="memory-percent">--%</span>
|
||||||
|
</div>
|
||||||
|
<div id="memory-detail" class="memory-detail">-- / --</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="info-section">
|
||||||
|
<h2 class="section-title">System Info</h2>
|
||||||
|
<dl class="info-list">
|
||||||
|
<div class="info-item">
|
||||||
|
<dt>Hostname</dt>
|
||||||
|
<dd id="info-hostname">--</dd>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<dt>Platform</dt>
|
||||||
|
<dd id="info-platform">--</dd>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<dt>Uptime</dt>
|
||||||
|
<dd id="info-uptime">--</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script type="module" src="./src/mcp-app.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
31
examples/system-monitor-server/package.json
Normal file
31
examples/system-monitor-server/package.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
218
examples/system-monitor-server/server.ts
Normal file
218
examples/system-monitor-server/server.ts
Normal file
|
|
@ -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<typeof CpuCoreSchema>;
|
||||||
|
type MemoryStats = z.infer<typeof MemoryStatsSchema>;
|
||||||
|
type SystemStats = z.infer<typeof SystemStatsSchema>;
|
||||||
|
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<MemoryStats> {
|
||||||
|
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<CallToolResult> => {
|
||||||
|
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<ReadResourceResult> => {
|
||||||
|
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);
|
||||||
12
examples/system-monitor-server/src/global.css
Normal file
12
examples/system-monitor-server/src/global.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
233
examples/system-monitor-server/src/mcp-app.css
Normal file
233
examples/system-monitor-server/src/mcp-app.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
360
examples/system-monitor-server/src/mcp-app.ts
Normal file
360
examples/system-monitor-server/src/mcp-app.ts
Normal file
|
|
@ -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<void> {
|
||||||
|
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);
|
||||||
19
examples/system-monitor-server/tsconfig.json
Normal file
19
examples/system-monitor-server/tsconfig.json
Normal file
|
|
@ -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"]
|
||||||
|
}
|
||||||
24
examples/system-monitor-server/vite.config.ts
Normal file
24
examples/system-monitor-server/vite.config.ts
Normal file
|
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
69
package-lock.json
generated
69
package-lock.json
generated
|
|
@ -160,6 +160,27 @@
|
||||||
"vite-plugin-singlefile": "^2.3.0"
|
"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": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|
@ -471,6 +492,12 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@modelcontextprotocol/ext-apps": {
|
||||||
"resolved": "",
|
"resolved": "",
|
||||||
"link": true
|
"link": true
|
||||||
|
|
@ -1344,6 +1371,18 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/check-error": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|
@ -2944,6 +2983,36 @@
|
||||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
"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": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue