Skip to main content
Glama
rileylsmith1997

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