mcp-systemctl
README.md
# mcp-systemctl
An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that reports on and manages **systemd services** via `systemctl` and `journalctl`. Works with any MCP client — Claude Desktop, AI coding agents, or custom tooling.
## Quick Start
```bash
npm install && npm run build
node dist/index.js
```
The server speaks JSON-RPC over **stdio** (the standard MCP transport). Connect it to your MCP client — see [`MCP_SYSTEMCTL_USAGE.md`](./MCP_SYSTEMCTL_USAGE.md) for client configuration and all available tools.
## Tools
| Tool | Description |
|---|---|
| `list_services` | List systemd units with state/pattern/type filters |
| `get_service_status` | Detailed status of a specific service (PID, memory, CPU, uptime, etc.) |
| `list_failed_services` | All services in a failed/error state |
| `get_service_logs` | Recent journald log entries for a service |
| `service_control` | Start, stop, restart, reload, enable, or disable a service |
Full parameter details in [`MCP_SYSTEMCTL_USAGE.md`](./MCP_SYSTEMCTL_USAGE.md).
---
## Architecture
The project follows a clean three-layer separation:
```
src/index.ts — Server setup, tool registration
src/tools/*.ts — Zod input schemas + handler functions (thin glue)
src/systemctl.ts — Core logic: systemctl/journalctl exec, parsing, formatting
```
```
┌──────────────┐ stdio (JSON-RPC) ┌──────────────────────┐
│ MCP Client │ ◄──────────────────────► │ mcp-systemctl │
│ │ │ (Node.js process) │
│ │ │ │
│ │ │ tools/*.ts │
│ │ │ → systemctl.ts │
│ │ │ → systemd │
└──────────────┘ └──────────────────────┘
```
### Layer Responsibilities
**`src/index.ts`** — Entry point. Creates the `McpServer` instance, calls `server.registerTool()` for each tool, then connects to the `StdioServerTransport`. Adding a new tool means importing its schema and handler and adding one more `registerTool` call here.
**`src/tools/`** — One file per tool. Each exports:
- A **schema** object (Zod-defined input parameters with `describe()` for documentation)
- A **handler** function conforming to `ToolCallback<T>` — receives validated args, calls into `systemctl.ts`, formats the response as MCP `content` blocks
**`src/systemctl.ts`** — The engine. All `systemctl` and `journalctl` interactions live here:
- Shell execution via `execFile` (no shell injection risk)
- Structured result type `SystemctlResult<T>` — every function returns `{ ok: true, data }` or `{ ok: false, error, code, stderr }`
- Parsers for the systemctl table and key=value output formats
- Human-readable formatters for bytes, nanoseconds, etc.
---
## Extending the Server
Adding a new tool takes four steps. This example adds a `get_service_unit_file` tool that returns a service's unit file contents.
### 1. Add the core logic in `src/systemctl.ts`
```typescript
export async function getUnitFile(name: string): Promise<SystemctlResult<string>> {
const unitName = name.includes(".") ? name : `${name}.service`;
// Run: systemctl cat <unit>
const args = ["cat", unitName, "--no-pager", "-l"];
return await runSystemctl(args);
}
```
Use the existing helpers — `runSystemctl()` and `runJournalctl()` handle timeouts, permission errors, and missing commands consistently. If you need to parse new output formats, add a parser alongside the existing ones.
### 2. Create the tool file `src/tools/getUnitFile.ts`
```typescript
import * as z from "zod";
import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
import { getUnitFile as getUnitFileCore } from "../systemctl.js";
export const getUnitFileSchema = {
name: z
.string()
.min(1)
.describe("Service name (e.g. 'sshd', 'cron.service')"),
};
export const getUnitFileHandler: ToolCallback<typeof getUnitFileSchema> = async (args) => {
try {
const result = await getUnitFileCore(args.name);
if (!result.ok) {
return {
content: [{ type: "text", text: `Error: ${result.error}` }],
isError: true,
};
}
return {
content: [{ type: "text", text: result.data }],
};
} catch (err) {
return {
content: [{ type: "text", text: `Internal error: ${err instanceof Error ? err.message : String(err)}` }],
isError: true,
};
}
};
```
The pattern is always: import the core function → call it → check `result.ok` → format the response. Every handler returns `{ content: [...], isError?: boolean }`.
### 3. Register the tool in `src/index.ts`
Add the imports:
```typescript
import { getUnitFileSchema, getUnitFileHandler } from "./tools/getUnitFile.js";
```
Add the registration (alongside the existing ones):
```typescript
server.registerTool(
"get_unit_file",
{
description: "Print the unit file contents of a systemd service.",
inputSchema: getUnitFileSchema,
},
getUnitFileHandler,
);
```
### 4. Build and test
```bash
npm run build
node dist/index.js
```
Your MCP client will now discover the new `get_unit_file` tool automatically — no client-side config changes needed.
### Design conventions to follow
| Convention | Why |
|---|---|
| Every core function returns `SystemctlResult<T>` | Caller always handles errors explicitly; no thrown exceptions |
| Use `execFile`, never `exec` or shell commands | Prevents shell injection through service names or patterns |
| Schema parameters use `.describe()` | MCP clients use this description to document the parameter to the LLM |
| Handlers wrap core calls in try/catch | Last-resort safety net for unexpected errors (not for expected failures) |
| Permission errors handled in `systemctl.ts` | Consistent messaging across all tools |
| Source files use `.js` extensions in imports | Required by NodeNext module resolution with ESM |
### What not to do
- **Don't** put shell execution logic in tool handlers — keep it in `systemctl.ts`
- **Don't** throw exceptions for expected failures (missing service, permission denied) — return error responses
- **Don't** skip error handling on `result.ok` — every `SystemctlResult` must be checked
- **Don't** use `any` types — the Zod schema provides full type inference
- **Don't** add new dependencies without evaluating whether the existing patterns suffice
---
## Project Structure
```
mcp-systemctl/
├── src/
│ ├── index.ts # Server entry point, tool registration
│ ├── systemctl.ts # Core systemctl/journalctl logic
│ └── tools/
│ ├── listServices.ts
│ ├── getServiceStatus.ts
│ ├── listFailedServices.ts
│ ├── getServiceLogs.ts
│ └── serviceControl.ts
├── dist/ # Build output (gitignored)
├── node_modules/ # Dependencies (gitignored)
├── MCP_SYSTEMCTL_USAGE.md
├── package.json
├── tsconfig.json
└── .gitignore
```
---
## License
MIT
TDQS
A4/5.0
Scored across 5 tools
Disambiguation5/5
Each tool targets a distinct aspect of systemd service management: listing all services, listing failed ones, getting detailed status, retrieving logs, and performing control actions. No overlap in purpose.
Naming Consistency5/5
All tool names follow a consistent verb_noun pattern using snake_case (e.g., get_service_logs, list_services, service_control). The naming is predictable and clear.
Tool Count5/5
5 tools is an appropriate number for managing systemd services. It covers the essential operations without being overly granular or sparse.
Completeness5/5
The set covers all common tasks: listing with filters, checking status, retrieving logs, controlling services (start/stop/enable/disable), and listing failed services. No obvious gaps for typical systemctl usage.
Maintenance
ActivityInactive
ResponsivenessNo issues