system-health-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # System Health MCP Server
2
+
3
+ A Model Context Protocol (MCP) server that provides real-time system health monitoring capabilities including CPU load, memory usage, and battery status.
4
+
5
+ ## Features
6
+
7
+ - **Real-time CPU monitoring**: Get current CPU load percentage
8
+ - **Memory statistics**: Track active and total memory usage
9
+ - **Battery information**: Monitor battery level and charging status
10
+ - **Hardware details**: GPU, network interfaces, disk usage, and Docker containers
11
+
12
+ ## Prerequisites
13
+
14
+ - Node.js (v18 or higher recommended)
15
+ - TypeScript
16
+ - npm or yarn
17
+
18
+ ## Installation
19
+
20
+ 1. Clone this repository:
21
+
22
+ ```bash
23
+ git clone <repository-url>
24
+ cd system-health-mcp
25
+ ```
26
+
27
+ 2. Install dependencies:
28
+
29
+ 3. Build the TypeScript code:
30
+
31
+ ## Configuration
32
+
33
+ To use this MCP server with an MCP-compatible client (like Claude Desktop), add the following configuration to your client's MCP settings file:
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "system-health": {
39
+ "command": "node",
40
+ "args": ["/path/to/your/project/index.js"]
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ **Important**: Replace `/path/to/your/project/index.js` with the absolute path to the built `index.js` file in your cloned repository.
47
+
48
+ ### Example Configuration Locations
49
+
50
+ - **Claude Desktop (macOS)**: `~/Library/Application Support/Claude/claude_desktop_config.json`
51
+ - **Claude Desktop (Windows)**: `%APPDATA%\Claude\claude_desktop_config.json`
52
+ - **Claude Desktop (Linux)**: `~/.config/Claude/claude_desktop_config.json`
53
+
54
+ After updating the configuration, restart your MCP client for the changes to take effect.
55
+
56
+ ## Usage
57
+
58
+ This MCP server exposes two tools for comprehensive system monitoring.
59
+
60
+ ### Running the Server
61
+
62
+ ```bash
63
+ node index.js
64
+ ```
65
+
66
+ The server communicates via stdio and is designed to be used with MCP-compatible clients.
67
+
68
+ ### Tool: get_system_stats
69
+
70
+ Returns real-time system health information.
71
+
72
+ **Response:**
73
+
74
+ - `cpuLoadPercentage`: Current CPU load as a percentage
75
+ - `memoryUsedGB`: Active memory in gigabytes
76
+ - `memoryTotalGB`: Total system memory in gigabytes
77
+ - `batteryLevel`: Battery percentage (or "No battery" if not applicable)
78
+ - `isCharging`: Boolean indicating if the battery is currently charging
79
+
80
+ **Example Response:**
81
+
82
+ ```json
83
+ {
84
+ "cpuLoadPercentage": "23.45",
85
+ "memoryUsedGB": "8.42",
86
+ "memoryTotalGB": "16.00",
87
+ "batteryLevel": "85%",
88
+ "isCharging": false
89
+ }
90
+ ```
91
+
92
+ ### Tool: get_hardware_details
93
+
94
+ Returns detailed hardware information based on the specified category.
95
+
96
+ **Parameters:**
97
+
98
+ - `category` (required): One of `"graphics"`, `"network"`, `"disks"`, or `"docker"`
99
+
100
+ **Categories:**
101
+
102
+ - `graphics`: GPU and graphics card information
103
+ - `network`: Network interface configurations
104
+ - `disks`: Disk usage and filesystem information
105
+ - `docker`: Docker container details
106
+
107
+ **Example Request:**
108
+
109
+ ```json
110
+ {
111
+ "category": "graphics"
112
+ }
113
+ ```
114
+
115
+ ## Development
116
+
117
+ ### Project Structure
118
+
119
+ - `index.ts`: Main server implementation
120
+ - `package.json`: Project dependencies and configuration
121
+ - `tsconfig.json`: TypeScript compiler configuration
122
+
123
+ ### Building
124
+
125
+ ```bash
126
+ npx tsc
127
+ ```
128
+
129
+ ### Type Checking
130
+
131
+ ```bash
132
+ npx tsc --noEmit
133
+ ```
134
+
135
+ ## Dependencies
136
+
137
+ - `@modelcontextprotocol/sdk`: MCP SDK for building servers
138
+ - `systeminformation`: Cross-platform system information library
139
+
140
+ ## License
141
+
142
+ ISC
@@ -0,0 +1,6 @@
1
+ export default {
2
+ presets: [
3
+ ["@babel/preset-env", { targets: { node: "current" } }],
4
+ "@babel/preset-typescript",
5
+ ],
6
+ };
package/index.ts ADDED
@@ -0,0 +1,213 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
+ import si from 'systeminformation';
5
+ import psList from 'ps-list';
6
+ import pidusage from 'pidusage';
7
+ import getFolderSize from 'get-folder-size';
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+
11
+ const server = new Server(
12
+ { name: "system-health-monitor", version: "1.0.0" },
13
+ { capabilities: { tools: {} } }
14
+ );
15
+
16
+
17
+ // Standalone handler for ListToolsRequest
18
+ async function handleListToolsRequest() {
19
+ return {
20
+ tools: [
21
+ {
22
+ name: "get_system_stats",
23
+ description: "Get real-time CPU load, memory usage, and battery status",
24
+ inputSchema: { type: "object", properties: {} }
25
+ },
26
+ {
27
+ name: "get_hardware_details",
28
+ description: "Get detailed hardware info like GPU, Disk health, and Network config",
29
+ inputSchema: {
30
+ type: "object",
31
+ properties: {
32
+ category: {
33
+ type: "string",
34
+ enum: ["graphics", "network", "disks", "docker"]
35
+ }
36
+ }
37
+ }
38
+ },
39
+ {
40
+ name: "get_resource_hogs",
41
+ description: "Get top N resource hog processes by CPU usage",
42
+ inputSchema: {
43
+ type: "object",
44
+ properties: {
45
+ topN: { type: "number", default: 5 }
46
+ }
47
+ }
48
+ },
49
+ {
50
+ name: "get_largest_files_folders",
51
+ description: "Get top N largest files/folders in a directory",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: {
55
+ scanPath: { type: "string", default: "/" },
56
+ topN: { type: "number", default: 5 }
57
+ }
58
+ }
59
+ }
60
+ ]
61
+ };
62
+ }
63
+
64
+ server.setRequestHandler(ListToolsRequestSchema, handleListToolsRequest);
65
+
66
+ // 2. Implement the logic
67
+ // Standalone handler for CallToolRequest
68
+ async function handleCallToolRequest(request: any) {
69
+ // Get top N resource hog processes
70
+ if (request.params.name === "get_resource_hogs") {
71
+ const { topN = 5 } = request.params.arguments || {};
72
+ const processes = await psList();
73
+ const usages = await Promise.all(
74
+ processes.map(async (proc) => {
75
+ try {
76
+ const stats: { cpu: number; memory: number } = await pidusage(proc.pid);
77
+ return {
78
+ ...proc,
79
+ cpu: stats.cpu,
80
+ memory: stats.memory,
81
+ };
82
+ } catch {
83
+ return null;
84
+ }
85
+ })
86
+ );
87
+ const validUsages = usages.filter((u): u is NonNullable<typeof u> => !!u);
88
+ validUsages.sort((a, b) => ((b && b.cpu) || 0) - ((a && a.cpu) || 0));
89
+ return {
90
+ content: [{ type: "text", text: JSON.stringify(validUsages.slice(0, Number(topN)), null, 2) }]
91
+ };
92
+ }
93
+
94
+ // Get top N largest files/folders in a directory
95
+ if (request.params.name === "get_largest_files_folders") {
96
+ const { scanPath = "/", topN = 5 } = request.params.arguments || {};
97
+ const items: { name: string; size: number; isDir: boolean }[] = [];
98
+ let dirents: fs.Dirent[];
99
+ try {
100
+ dirents = fs.readdirSync(scanPath as string, { withFileTypes: true });
101
+ } catch (err) {
102
+ return {
103
+ content: [{ type: "text", text: `Error reading directory: ${err}` }]
104
+ };
105
+ }
106
+ for (const dirent of dirents) {
107
+ const fullPath = path.join(scanPath as string, dirent.name);
108
+ try {
109
+ if (dirent.isDirectory()) {
110
+ const size = await getFolderSize(fullPath).then((result: { size: number }) => result.size);
111
+ items.push({ name: fullPath, size, isDir: true });
112
+ } else if (dirent.isFile()) {
113
+ const stats = fs.statSync(fullPath);
114
+ items.push({ name: fullPath, size: stats.size, isDir: false });
115
+ }
116
+ } catch {
117
+ // Ignore errors
118
+ }
119
+ }
120
+ items.sort((a, b) => b.size - a.size);
121
+ return {
122
+ content: [{ type: "text", text: JSON.stringify(items.slice(0, Number(topN)), null, 2) }]
123
+ };
124
+ }
125
+ if (request.params.name === "get_system_stats") {
126
+ const [cpu, mem, battery] = await Promise.all([
127
+ si.currentLoad(),
128
+ si.mem(),
129
+ si.battery()
130
+ ]);
131
+
132
+ const stats = {
133
+ cpuLoadPercentage: cpu.currentLoad.toFixed(2),
134
+ memoryUsedGB: (mem.active / 1024 / 1024 / 1024).toFixed(2),
135
+ memoryTotalGB: (mem.total / 1024 / 1024 / 1024).toFixed(2),
136
+ batteryLevel: battery.hasBattery ? `${battery.percent}%` : "No battery",
137
+ isCharging: battery.isCharging
138
+ };
139
+
140
+ return {
141
+ content: [{ type: "text", text: JSON.stringify(stats, null, 2) }]
142
+ };
143
+ }
144
+
145
+ if (request.params.name === "get_hardware_details") {
146
+ const { category } = request.params.arguments as { category: string };
147
+ let data;
148
+
149
+ switch (category) {
150
+ case "graphics":
151
+ data = { graphics: await si.graphics() };
152
+ break;
153
+ case "network":
154
+ data = await si.networkInterfaces();
155
+ break;
156
+ case "disks":
157
+ data = await si.fsSize();
158
+ break;
159
+ case "docker":
160
+ data = await si.dockerContainers();
161
+ break;
162
+ default:
163
+ data = { error: "Invalid category" };
164
+ }
165
+
166
+ return {
167
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
168
+ };
169
+ }
170
+ throw new Error("Tool not found");
171
+ }
172
+ server.setRequestHandler(CallToolRequestSchema, handleCallToolRequest);
173
+
174
+
175
+ // 3. Connect the transport
176
+
177
+
178
+ // Single-request mode for testability
179
+ if (process.env.TEST_MODE === '1') {
180
+ process.stdin.setEncoding('utf8');
181
+ let input = '';
182
+ process.stdin.on('data', (chunk) => {
183
+ input += chunk;
184
+ if (input.includes('\n')) {
185
+ process.stdin.pause();
186
+ handleInput(input.trim());
187
+ }
188
+ });
189
+
190
+ async function handleInput(line: string) {
191
+ try {
192
+ const req = JSON.parse(line);
193
+ let result;
194
+ if (req.schema === 'ListToolsRequest') {
195
+ result = await handleListToolsRequest();
196
+ } else if (req.schema === 'CallToolRequest') {
197
+ result = await handleCallToolRequest(req);
198
+ } else {
199
+ throw new Error('Unknown schema');
200
+ }
201
+ process.stdout.write(JSON.stringify(result) + '\n');
202
+ process.exit(0);
203
+ } catch (err: any) {
204
+ process.stderr.write((err && err.message ? err.message : String(err)) + '\n');
205
+ process.exit(1);
206
+ }
207
+ }
208
+ } else {
209
+ (async () => {
210
+ const transport = new StdioServerTransport();
211
+ await server.connect(transport);
212
+ })();
213
+ }
@@ -0,0 +1,8 @@
1
+ export default {
2
+ testEnvironment: "node",
3
+ moduleFileExtensions: ["ts", "js", "json", "node"],
4
+ transform: {
5
+ "^.+\\.(ts|js)$": "babel-jest"
6
+ },
7
+ extensionsToTreatAsEsm: [".ts"],
8
+ };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "system-health-mcp",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "scripts": {
6
+ "test": "echo \"Error: no test specified\" && exit 1"
7
+ },
8
+ "keywords": [
9
+ "mcp",
10
+ "system",
11
+ "debugging"
12
+ ],
13
+ "author": "John Van Wagenen",
14
+ "license": "ISC",
15
+ "type": "module",
16
+ "description": "A local MCP server to give your LLM access to hardware information.",
17
+ "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.25.3",
19
+ "get-folder-size": "^5.0.0",
20
+ "pidusage": "^4.0.1",
21
+ "ps-list": "^9.0.0",
22
+ "systeminformation": "^5.30.5"
23
+ },
24
+ "devDependencies": {
25
+ "repository": "https://github.com/jvdub/system-health-mcp",
26
+ "@types/node": "^25.0.10",
27
+ "@types/pidusage": "^2.0.5",
28
+ "@types/ps-list": "^6.0.0",
29
+ "typescript": "^5.9.3"
30
+ }
31
+ }
@@ -0,0 +1,62 @@
1
+ import { spawn } from 'child_process';
2
+ import path from 'path';
3
+ import { describe, expect, test } from '@jest/globals';
4
+
5
+ describe('System Health MCP Server Edge Cases', () => {
6
+ const serverPath = path.resolve(__dirname, '../out/index.js');
7
+
8
+ function runServerWithInput(input: string): Promise<string> {
9
+ return new Promise((resolve, reject) => {
10
+ const proc = spawn('node', [serverPath], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, TEST_MODE: '1' } });
11
+ let output = '';
12
+ let error = '';
13
+ proc.stdout.on('data', (data: { toString: () => string; }) => {
14
+ output += data.toString();
15
+ });
16
+ proc.stderr.on('data', (data: { toString: () => string; }) => {
17
+ error += data.toString();
18
+ });
19
+ proc.on('close', () => {
20
+ if (error) reject(error);
21
+ else resolve(output);
22
+ });
23
+ proc.stdin.write(input);
24
+ proc.stdin.end();
25
+ });
26
+ }
27
+ it('should handle invalid tool name', async () => {
28
+ const request = JSON.stringify({
29
+ schema: 'CallToolRequest',
30
+ params: { name: 'nonexistent_tool', arguments: {} }
31
+ }) + '\n';
32
+ await expect(runServerWithInput(request)).rejects.toMatch(/Tool not found/);
33
+ });
34
+
35
+ it('should handle invalid hardware category', async () => {
36
+ const request = JSON.stringify({
37
+ schema: 'CallToolRequest',
38
+ params: { name: 'get_hardware_details', arguments: { category: 'invalid' } }
39
+ }) + '\n';
40
+ const output = await runServerWithInput(request);
41
+ expect(output).toMatch(/Invalid category/);
42
+ });
43
+
44
+ it('should handle unreadable directory in get_largest_files_folders', async () => {
45
+ const request = JSON.stringify({
46
+ schema: 'CallToolRequest',
47
+ params: { name: 'get_largest_files_folders', arguments: { scanPath: '/root/forbidden', topN: 1 } }
48
+ }) + '\n';
49
+ const output = await runServerWithInput(request);
50
+ expect(output).toMatch(/Error reading directory/);
51
+ });
52
+
53
+ it('should default to topN=5 if not provided', async () => {
54
+ const request = JSON.stringify({
55
+ schema: 'CallToolRequest',
56
+ params: { name: 'get_resource_hogs', arguments: {} }
57
+ }) + '\n';
58
+ const output = await runServerWithInput(request);
59
+ // Should return at least one process
60
+ expect(output).toMatch(/cpu/);
61
+ });
62
+ });
@@ -0,0 +1,80 @@
1
+ import { spawn } from 'child_process';
2
+ import path from 'path';
3
+ import { describe, expect, test } from '@jest/globals';
4
+
5
+ describe('System Health MCP Server', () => {
6
+ const serverPath = path.resolve(__dirname, '../out/index.js');
7
+
8
+ function runServerWithInput(input: string): Promise<string> {
9
+ return new Promise((resolve, reject) => {
10
+ const proc = spawn('node', [serverPath], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, TEST_MODE: '1' } });
11
+ let output = '';
12
+ let error = '';
13
+ proc.stdout.on('data', (data: { toString: () => string; }) => {
14
+ output += data.toString();
15
+ });
16
+ proc.stderr.on('data', (data: { toString: () => string; }) => {
17
+ error += data.toString();
18
+ });
19
+ proc.on('close', () => {
20
+ if (error) reject(error);
21
+ else resolve(output);
22
+ });
23
+ proc.stdin.write(input);
24
+ proc.stdin.end();
25
+ });
26
+ }
27
+ it('should list all tools', async () => {
28
+ const request = JSON.stringify({
29
+ schema: 'ListToolsRequest',
30
+ params: {}
31
+ }) + '\n';
32
+ const output = await runServerWithInput(request);
33
+ expect(output).toContain('get_system_stats');
34
+ expect(output).toContain('get_hardware_details');
35
+ expect(output).toContain('get_resource_hogs');
36
+ expect(output).toContain('get_largest_files_folders');
37
+ });
38
+
39
+ it('should return system stats', async () => {
40
+ const request = JSON.stringify({
41
+ schema: 'CallToolRequest',
42
+ params: { name: 'get_system_stats', arguments: {} }
43
+ }) + '\n';
44
+ const output = await runServerWithInput(request);
45
+ expect(output).toMatch(/cpuLoadPercentage/);
46
+ expect(output).toMatch(/memoryUsedGB/);
47
+ expect(output).toMatch(/memoryTotalGB/);
48
+ expect(output).toMatch(/batteryLevel/);
49
+ expect(output).toMatch(/isCharging/);
50
+ });
51
+
52
+ it('should return hardware details for graphics', async () => {
53
+ const request = JSON.stringify({
54
+ schema: 'CallToolRequest',
55
+ params: { name: 'get_hardware_details', arguments: { category: 'graphics' } }
56
+ }) + '\n';
57
+ const output = await runServerWithInput(request);
58
+ expect(output).toMatch(/graphics/);
59
+ });
60
+
61
+ it('should return resource hogs', async () => {
62
+ const request = JSON.stringify({
63
+ schema: 'CallToolRequest',
64
+ params: { name: 'get_resource_hogs', arguments: { topN: 2 } }
65
+ }) + '\n';
66
+ const output = await runServerWithInput(request);
67
+ expect(output).toMatch(/cpu/);
68
+ expect(output).toMatch(/memory/);
69
+ });
70
+
71
+ it('should return largest files/folders', async () => {
72
+ const request = JSON.stringify({
73
+ schema: 'CallToolRequest',
74
+ params: { name: 'get_largest_files_folders', arguments: { scanPath: '.', topN: 2 } }
75
+ }) + '\n';
76
+ const output = await runServerWithInput(request);
77
+ expect(output).toMatch(/name/);
78
+ expect(output).toMatch(/size/);
79
+ });
80
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ // Visit https://aka.ms/tsconfig to read more about this file
3
+ "compilerOptions": {
4
+ // File Layout
5
+ // "rootDir": "./src",
6
+ "outDir": "./out",
7
+
8
+ // Environment Settings
9
+ // See also https://aka.ms/tsconfig/module
10
+ "module": "nodenext",
11
+ "target": "esnext",
12
+ "types": ["node", "jest"],
13
+ // For nodejs:
14
+ // "lib": ["esnext"],
15
+ // "types": ["node"],
16
+ // and npm install -D @types/node
17
+
18
+ // Other Outputs
19
+ "sourceMap": true,
20
+ "declaration": true,
21
+ "declarationMap": true,
22
+
23
+ // Stricter Typechecking Options
24
+ "noUncheckedIndexedAccess": true,
25
+ "exactOptionalPropertyTypes": true,
26
+
27
+ // Style Options
28
+ // "noImplicitReturns": true,
29
+ // "noImplicitOverride": true,
30
+ // "noUnusedLocals": true,
31
+ // "noUnusedParameters": true,
32
+ // "noFallthroughCasesInSwitch": true,
33
+ // "noPropertyAccessFromIndexSignature": true,
34
+
35
+ // Recommended Options
36
+ "strict": true,
37
+ "jsx": "react-jsx",
38
+ "verbatimModuleSyntax": true,
39
+ "isolatedModules": true,
40
+ "noUncheckedSideEffectImports": true,
41
+ "moduleDetection": "force",
42
+ "skipLibCheck": true,
43
+ }
44
+ ,
45
+ "include": ["index.ts"]
46
+ }