<!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>
<style>
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--text-muted: #6e7681;
--border-color: #30363d;
--accent-blue: #58a6ff;
--accent-green: #3fb950;
--accent-purple: #a371f7;
--accent-orange: #d29922;
--accent-red: #f85149;
--accent-cyan: #79c0ff;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
}
h1 {
font-size: 24px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 10px;
}
.subtitle {
color: var(--text-secondary);
font-size: 14px;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 8px;
}
.live-indicator {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(63, 185, 80, 0.1);
border: 1px solid var(--accent-green);
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
color: var(--accent-green);
}
.live-dot {
width: 8px;
height: 8px;
background: var(--accent-green);
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.metric-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px;
}
.metric-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.metric-label {
font-size: 12px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.metric-icon {
font-size: 18px;
}
.metric-value {
font-size: 28px;
font-weight: 700;
}
.metric-unit {
font-size: 14px;
font-weight: 400;
color: var(--text-secondary);
}
.metric-bar {
height: 4px;
background: var(--bg-tertiary);
border-radius: 2px;
margin-top: 12px;
overflow: hidden;
}
.metric-bar-fill {
height: 100%;
border-radius: 2px;
transition: width 0.3s ease;
}
.chart-container {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.chart-title {
font-size: 16px;
font-weight: 600;
}
.chart-legend {
display: flex;
gap: 16px;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text-secondary);
}
.legend-color {
width: 12px;
height: 3px;
border-radius: 1px;
}
.chart-area {
height: 200px;
position: relative;
}
svg {
width: 100%;
height: 100%;
}
.grid-line {
stroke: var(--border-color);
stroke-width: 1;
stroke-dasharray: 4, 4;
}
.axis-label {
fill: var(--text-muted);
font-size: 10px;
}
.area-path {
transition: d 0.3s ease;
}
.line-path {
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
transition: d 0.3s ease;
}
.processes-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.process-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px;
}
.process-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.process-title {
font-size: 14px;
font-weight: 600;
}
.process-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.process-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
background: var(--bg-tertiary);
border-radius: 6px;
}
.process-rank {
width: 20px;
height: 20px;
background: var(--bg-secondary);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
}
.process-name {
flex: 1;
font-size: 13px;
font-family: 'SF Mono', Monaco, monospace;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.process-value {
font-size: 13px;
font-weight: 600;
min-width: 50px;
text-align: right;
}
.process-bar {
width: 60px;
height: 4px;
background: var(--bg-secondary);
border-radius: 2px;
overflow: hidden;
}
.process-bar-fill {
height: 100%;
border-radius: 2px;
}
.actions {
display: flex;
gap: 12px;
margin-top: 20px;
}
.btn {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.btn:hover {
border-color: var(--accent-blue);
background: rgba(88, 166, 255, 0.1);
}
.btn.active {
background: rgba(63, 185, 80, 0.1);
border-color: var(--accent-green);
color: var(--accent-green);
}
.btn-primary {
background: var(--accent-blue);
border-color: var(--accent-blue);
color: #fff;
}
.btn-primary:hover {
background: #4c9aed;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: var(--text-secondary);
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 24px;
height: 24px;
border: 2px solid var(--border-color);
border-top-color: var(--accent-blue);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-right: 12px;
}
@media (max-width: 700px) {
.metrics-grid {
grid-template-columns: repeat(2, 1fr);
}
.processes-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🖥️ System Monitor</h1>
<p class="subtitle">
Real-time system metrics
<span class="live-indicator">
<span class="live-dot"></span>
Live
</span>
</p>
<div class="metrics-grid" id="metrics-grid">
<div class="metric-card">
<div class="metric-header">
<span class="metric-label">CPU Usage</span>
<span class="metric-icon">⚡</span>
</div>
<div class="metric-value"><span id="cpu-value">0</span><span class="metric-unit">%</span></div>
<div class="metric-bar">
<div class="metric-bar-fill" id="cpu-bar" style="width: 0%; background: var(--accent-blue);"></div>
</div>
</div>
<div class="metric-card">
<div class="metric-header">
<span class="metric-label">Memory</span>
<span class="metric-icon">💾</span>
</div>
<div class="metric-value"><span id="mem-value">0</span><span class="metric-unit">%</span></div>
<div class="metric-bar">
<div class="metric-bar-fill" id="mem-bar" style="width: 0%; background: var(--accent-purple);"></div>
</div>
</div>
<div class="metric-card">
<div class="metric-header">
<span class="metric-label">Disk</span>
<span class="metric-icon">💿</span>
</div>
<div class="metric-value"><span id="disk-value">0</span><span class="metric-unit">%</span></div>
<div class="metric-bar">
<div class="metric-bar-fill" id="disk-bar" style="width: 0%; background: var(--accent-orange);"></div>
</div>
</div>
<div class="metric-card">
<div class="metric-header">
<span class="metric-label">Network</span>
<span class="metric-icon">🌐</span>
</div>
<div class="metric-value"><span id="net-value">0</span><span class="metric-unit">MB/s</span></div>
<div class="metric-bar">
<div class="metric-bar-fill" id="net-bar" style="width: 0%; background: var(--accent-green);"></div>
</div>
</div>
</div>
<div class="chart-container">
<div class="chart-header">
<span class="chart-title">CPU & Memory History (60s)</span>
<div class="chart-legend">
<div class="legend-item">
<div class="legend-color" style="background: var(--accent-blue);"></div>
<span>CPU</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: var(--accent-purple);"></div>
<span>Memory</span>
</div>
</div>
</div>
<div class="chart-area" id="history-chart">
<div class="loading"><div class="spinner"></div>Collecting data...</div>
</div>
</div>
<div class="processes-grid">
<div class="process-card">
<div class="process-header">
<span class="process-title">Top CPU Processes</span>
</div>
<div class="process-list" id="cpu-processes">
<!-- Generated dynamically -->
</div>
</div>
<div class="process-card">
<div class="process-header">
<span class="process-title">Top Memory Processes</span>
</div>
<div class="process-list" id="mem-processes">
<!-- Generated dynamically -->
</div>
</div>
</div>
<div class="actions">
<button class="btn active" id="live-btn">⏸️ Pause</button>
<button class="btn" id="refresh-btn">🔄 Refresh Now</button>
<button class="btn btn-primary" id="report-btn">📊 Generate Report</button>
</div>
</div>
<script type="module">
import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps";
// State
let systemData = null;
let historyData = { cpu: [], mem: [] };
let isLive = true;
let updateInterval = null;
const MAX_HISTORY = 60;
// Generate simulated metrics
function generateSimulatedMetrics() {
const processes = ["chrome", "node", "code", "docker", "slack", "spotify", "zoom", "firefox"];
return {
cpu: 25 + Math.random() * 40,
memory: 45 + Math.random() * 25,
disk: 52 + Math.random() * 10,
network: Math.random() * 5,
processes: processes.map(name => ({
name,
cpu: Math.random() * 20,
memory: Math.random() * 12
}))
};
}
// Initialize MCP App
const app = new App({
appInfo: { name: "System Monitor", version: "1.0.0" },
appCapabilities: {}
});
app.ontoolinput = (input) => {
console.log("[Monitor] Tool input received:", input);
};
app.ontoolresult = (result) => {
console.log("[Monitor] Tool result received:", result);
try {
const content = result.content?.[0];
if (content?.type === 'text') {
systemData = JSON.parse(content.text);
updateMetrics();
updateHistory();
renderHistoryChart();
renderProcesses();
}
} catch (e) {
console.error("[Monitor] Failed to parse result:", e);
}
};
// Connect to host
await app.connect(new PostMessageTransport(window.parent));
console.log("[Monitor] Connected to host");
// Start live updates with simulated data
startLiveUpdates();
function startLiveUpdates() {
if (updateInterval) clearInterval(updateInterval);
// Generate initial data immediately
systemData = generateSimulatedMetrics();
updateMetrics();
updateHistory();
renderHistoryChart();
renderProcesses();
updateInterval = setInterval(async () => {
if (isLive) {
try {
// Try to get real data from server
await app.callServerTool('get-system-metrics', {});
} catch (e) {
// Fall back to simulated data if server call fails
systemData = generateSimulatedMetrics();
updateMetrics();
updateHistory();
renderHistoryChart();
renderProcesses();
}
}
}, 1000);
}
function updateMetrics() {
if (!systemData) return;
// CPU
const cpu = systemData.cpu || 0;
document.getElementById('cpu-value').textContent = Math.round(cpu);
document.getElementById('cpu-bar').style.width = `${cpu}%`;
updateBarColor('cpu-bar', cpu);
// Memory
const mem = systemData.memory || 0;
document.getElementById('mem-value').textContent = Math.round(mem);
document.getElementById('mem-bar').style.width = `${mem}%`;
updateBarColor('mem-bar', mem, 'var(--accent-purple)');
// Disk
const disk = systemData.disk || 0;
document.getElementById('disk-value').textContent = Math.round(disk);
document.getElementById('disk-bar').style.width = `${disk}%`;
updateBarColor('disk-bar', disk, 'var(--accent-orange)');
// Network
const net = systemData.network || 0;
document.getElementById('net-value').textContent = net.toFixed(1);
document.getElementById('net-bar').style.width = `${Math.min(net * 10, 100)}%`;
}
function updateBarColor(id, value, defaultColor = 'var(--accent-blue)') {
const el = document.getElementById(id);
if (value > 90) {
el.style.background = 'var(--accent-red)';
} else if (value > 70) {
el.style.background = 'var(--accent-orange)';
} else {
el.style.background = defaultColor;
}
}
function updateHistory() {
if (!systemData) return;
historyData.cpu.push(systemData.cpu || 0);
historyData.mem.push(systemData.memory || 0);
if (historyData.cpu.length > MAX_HISTORY) {
historyData.cpu.shift();
historyData.mem.shift();
}
}
function renderHistoryChart() {
const container = document.getElementById('history-chart');
if (historyData.cpu.length < 2) {
container.innerHTML = '<div class="loading"><div class="spinner"></div>Collecting data...</div>';
return;
}
const padding = { top: 10, right: 10, bottom: 25, left: 35 };
const width = container.clientWidth;
const height = container.clientHeight;
const chartWidth = width - padding.left - padding.right;
const chartHeight = height - padding.top - padding.bottom;
const xScale = (i) => padding.left + (i / (MAX_HISTORY - 1)) * chartWidth;
const yScale = (v) => padding.top + chartHeight - (v / 100) * chartHeight;
let svg = `<svg viewBox="0 0 ${width} ${height}">`;
// Grid lines
[0, 25, 50, 75, 100].forEach(v => {
const y = yScale(v);
svg += `<line class="grid-line" x1="${padding.left}" y1="${y}" x2="${width - padding.right}" y2="${y}" />`;
svg += `<text class="axis-label" x="${padding.left - 8}" y="${y + 3}" text-anchor="end">${v}%</text>`;
});
// Time labels
const timeLabels = ['60s', '45s', '30s', '15s', 'now'];
timeLabels.forEach((label, i) => {
const x = padding.left + (i / (timeLabels.length - 1)) * chartWidth;
svg += `<text class="axis-label" x="${x}" y="${height - 5}" text-anchor="middle">${label}</text>`;
});
// CPU area and line
const cpuPoints = historyData.cpu.map((v, i) => {
const x = xScale(i + (MAX_HISTORY - historyData.cpu.length));
return `${x},${yScale(v)}`;
}).join(' ');
const cpuAreaPoints = `${xScale(MAX_HISTORY - historyData.cpu.length)},${yScale(0)} ${cpuPoints} ${xScale(MAX_HISTORY - 1)},${yScale(0)}`;
svg += `<polygon class="area-path" points="${cpuAreaPoints}" fill="rgba(88, 166, 255, 0.15)" />`;
svg += `<polyline class="line-path" points="${cpuPoints}" stroke="var(--accent-blue)" />`;
// Memory area and line
const memPoints = historyData.mem.map((v, i) => {
const x = xScale(i + (MAX_HISTORY - historyData.mem.length));
return `${x},${yScale(v)}`;
}).join(' ');
const memAreaPoints = `${xScale(MAX_HISTORY - historyData.mem.length)},${yScale(0)} ${memPoints} ${xScale(MAX_HISTORY - 1)},${yScale(0)}`;
svg += `<polygon class="area-path" points="${memAreaPoints}" fill="rgba(163, 113, 247, 0.15)" />`;
svg += `<polyline class="line-path" points="${memPoints}" stroke="var(--accent-purple)" />`;
svg += '</svg>';
container.innerHTML = svg;
}
function renderProcesses() {
if (!systemData?.processes) return;
const cpuProcesses = [...(systemData.processes || [])]
.sort((a, b) => b.cpu - a.cpu)
.slice(0, 5);
const memProcesses = [...(systemData.processes || [])]
.sort((a, b) => b.memory - a.memory)
.slice(0, 5);
document.getElementById('cpu-processes').innerHTML = cpuProcesses.map((p, i) => `
<div class="process-item">
<span class="process-rank">${i + 1}</span>
<span class="process-name">${p.name}</span>
<span class="process-value" style="color: var(--accent-blue);">${p.cpu.toFixed(1)}%</span>
<div class="process-bar">
<div class="process-bar-fill" style="width: ${Math.min(p.cpu, 100)}%; background: var(--accent-blue);"></div>
</div>
</div>
`).join('');
document.getElementById('mem-processes').innerHTML = memProcesses.map((p, i) => `
<div class="process-item">
<span class="process-rank">${i + 1}</span>
<span class="process-name">${p.name}</span>
<span class="process-value" style="color: var(--accent-purple);">${p.memory.toFixed(1)}%</span>
<div class="process-bar">
<div class="process-bar-fill" style="width: ${Math.min(p.memory, 100)}%; background: var(--accent-purple);"></div>
</div>
</div>
`).join('');
}
// Event handlers
document.getElementById('live-btn').addEventListener('click', () => {
isLive = !isLive;
const btn = document.getElementById('live-btn');
if (isLive) {
btn.textContent = '⏸️ Pause';
btn.classList.add('active');
} else {
btn.textContent = '▶️ Resume';
btn.classList.remove('active');
}
});
document.getElementById('refresh-btn').addEventListener('click', async () => {
await app.callServerTool('get-system-metrics', {});
});
document.getElementById('report-btn').addEventListener('click', () => {
generateReport();
});
function generateReport() {
if (!systemData) {
app.sendMessage('user', [{ type: 'text', text: '⚠️ No system data available yet.' }]);
return;
}
const avgCpu = historyData.cpu.length > 0
? historyData.cpu.reduce((a, b) => a + b, 0) / historyData.cpu.length
: systemData.cpu;
const avgMem = historyData.mem.length > 0
? historyData.mem.reduce((a, b) => a + b, 0) / historyData.mem.length
: systemData.memory;
const maxCpu = historyData.cpu.length > 0 ? Math.max(...historyData.cpu) : systemData.cpu;
const maxMem = historyData.mem.length > 0 ? Math.max(...historyData.mem) : systemData.memory;
let report = `🖥️ **System Performance Report**\n\n`;
report += `**Current Metrics:**\n`;
report += `- CPU: ${Math.round(systemData.cpu)}%\n`;
report += `- Memory: ${Math.round(systemData.memory)}%\n`;
report += `- Disk: ${Math.round(systemData.disk)}%\n`;
report += `- Network: ${systemData.network.toFixed(1)} MB/s\n\n`;
report += `**60-Second Averages:**\n`;
report += `- CPU: ${avgCpu.toFixed(1)}% (peak: ${maxCpu.toFixed(1)}%)\n`;
report += `- Memory: ${avgMem.toFixed(1)}% (peak: ${maxMem.toFixed(1)}%)\n\n`;
if (systemData.processes?.length > 0) {
report += `**Top CPU Processes:**\n`;
[...systemData.processes].sort((a, b) => b.cpu - a.cpu).slice(0, 3).forEach((p, i) => {
report += `${i + 1}. ${p.name}: ${p.cpu.toFixed(1)}%\n`;
});
}
// Health assessment
report += `\n**Health Status:** `;
if (systemData.cpu > 90 || systemData.memory > 90) {
report += `🔴 Critical - High resource usage detected`;
} else if (systemData.cpu > 70 || systemData.memory > 70) {
report += `🟡 Warning - Elevated resource usage`;
} else {
report += `🟢 Healthy - System operating normally`;
}
app.sendMessage('user', [{ type: 'text', text: report }]);
}
// Handle resize
window.addEventListener('resize', () => {
renderHistoryChart();
});
// Notify host of size
const observer = new ResizeObserver(() => {
const height = document.body.scrollHeight;
app.sendSizeChanged(undefined, height);
});
observer.observe(document.body);
</script>
</body>
</html>