@systemdox/runbook-mcp
Your runbooks, as agent tools.
SystemDox gives AI coding agents your
architecture context — the checks, rules, decisions and docs that say what
good looks like. This server gives them hands, safely: declare the scripts
your team already runs in a runbooks.json manifest, and every one of them
becomes an MCP tool with a typed, allowlisted input schema that Claude Code,
Cursor and any other MCP client can call.
No free text ever reaches your shell. That is the whole design.
// runbooks.json
{
"version": 1,
"runbooks": [
{
"name": "sync_repos",
"description": "Fetch every repo and fast-forward clean checkouts. Run before researching a repository.",
"script": "ops/sync-repos.ps1",
"interpreter": "powershell",
"params": {
"reportOnly": { "type": "boolean", "flag": "-ReportOnly", "description": "Fetch and report; change nothing." }
}
}
]
}
Result, as the agent sees it:
sync_repos(reportOnly?: boolean)
"Fetch every repo and fast-forward clean checkouts. Run before researching a repository.
Runs sync-repos.ps1 (powershell); timeout 300s."
Why not just give the agent a shell?
A shell is an unbounded surface: any command, any argument, no record of
which ones the team sanctioned. A runbook server is a declared surface:
| Runs anything it can type | Runs only the scripts in the manifest |
| Composes arguments as free text | Fills typed parameters that must match a pattern or an enum |
| No timeouts, unbounded output | Per-runbook timeout, bounded output (the tail, with the drop reported) |
| Nothing tells the client what is safe | readOnly / destructive / idempotent annotations on every tool |
| The operator learns what ran afterwards | Every result starts with the exact command line that ran |
The manifest is the contract. Agents cannot extend it; they can only call
what it offers, with what it allows.
Quick start
npx @systemdox/runbook-mcp init
npx @systemdox/runbook-mcp validate
Then register the server with your MCP client. Claude Code, project scope
(.mcp.json in the repository root):
{
"mcpServers": {
"runbooks": {
"command": "npx",
"args": ["-y", "@systemdox/runbook-mcp", "--manifest", "./runbooks.json"]
}
}
}
Restart the client and ask: "List the runbooks you have and run hello with
name set to the team."
The manifest is found via --manifest <path>, then $RUNBOOK_MCP_MANIFEST,
then ./runbooks.json in the working directory.
Manifest reference
Runbook
name | yes | — | Tool name; snake_case (^[a-z][a-z0-9_]{0,63}$) |
description | yes | — | What it does and when to use it — this is what the agent reads |
script | yes | — | Path to the script, relative to the manifest (absolute allowed) |
interpreter | yes | — | powershell, pwsh, bash, sh, node, python or exe |
cwd | no | manifest folder | Working directory, relative to the manifest |
timeoutSeconds | no | 300 | Hard limit; the whole process tree is killed when it passes |
maxOutputChars | no | 60000 | Per stream. The tail is kept and the drop is reported |
env | no | — | Extra environment for the script (literal values — put secrets in the client's env) |
readOnly / destructive / idempotent | no | false | Published as MCP tool annotations |
params | no | {} | The tool's input schema — see below |
Parameter
Every parameter maps to one of: a fixed flag from the manifest, or a
positional argument. Booleans emit their flag when true and nothing when
false.
type | all | boolean, string, number or array (of strings) |
flag | all | The literal flag to emit, e.g. --name or -ReportOnly (^-{1,2}[A-Za-z][A-Za-z0-9-]*$) |
positional | string, number, array | Emit the value without a flag |
required | all | Refuse the call when missing |
default | all | Applied when the agent omits the argument; must satisfy the constraint |
description | all | Shown to the agent |
pattern | string, array | Anchored regex (^...$) every value must match |
enum | string, array | The only values accepted |
integer, minimum, maximum | number | Bounds |
join | array | Emit one flag with the items joined by this separator instead of repeating the flag |
maxItems | array | Upper bound on the number of items |
A string or array parameter must declare pattern or enum. There is
no "any string" parameter type. Values containing a double quote or a control
character are refused whatever the pattern says.
Interpreters
powershell | powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "& '<script>' <args>; exit $LASTEXITCODE" | Windows PowerShell 5.1 (pwsh elsewhere) |
pwsh | same, via PowerShell 7 | pwsh |
bash / sh | <bash> <script> <args> | Git for Windows bash ($RUNBOOK_MCP_BASH to override) |
node | the running Node binary | — |
python | python <script> <args> | python (python3 elsewhere) |
exe | the script itself is the executable | — |
Override any of them per manifest:
"interpreters": { "bash": "C:\\Program Files\\Git\\bin\\bash.exe" }
PowerShell scripts are invoked through -Command rather than -File because
-File cannot bind array parameters and cannot set the console encoding.
Every value is single-quoted, with embedded quotes doubled — inside single
quotes PowerShell performs no expansion, so a value that passed its pattern
cannot change the shape of the command. Array parameters arrive as real
PowerShell arrays (-Keep 'a','b'). All other interpreters receive an
argument array directly; nothing goes through a shell.
What the agent gets back
clean_worktrees: exit 0 (41.3s)
$ powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "... & 'C:\ops\clean-worktrees.ps1' -ReportOnly -Match 'sdx*'; exit $LASTEXITCODE"
--- stdout ---
Would delete 7 worktrees (3 held back: uncommitted changes)
...
A non-zero exit, a timeout or a process that failed to start comes back as an
MCP error result with the same layout, so the agent can read the log and
decide — it is never a protocol error.
Safety model
- Declared surface. Only manifest entries are callable; the agent cannot
add scripts, flags or arguments.
- Allowlisted values. Strings and arrays must match an anchored pattern or
an enum; numbers are bounded; booleans are flags. Double quotes and control
characters are refused regardless.
- No shell. POSIX-style interpreters get an argv array; PowerShell gets
single-quoted tokens.
- Bounded. Per-runbook timeout with process-tree kill; per-stream output
cap keeping the tail.
- Fail closed. The server refuses to start if the manifest is invalid or
any script is missing —
validate tells you first.
- Secrets stay in the client. Pass them through the MCP client's
env
(for example "${SENTRY_ACCESS_TOKEN}" in Claude Code's .mcp.json); the
manifest holds literals only.
- Dry run.
RUNBOOK_MCP_DRY_RUN=1 makes every tool report the command it
would run instead of running it — useful in CI and when reviewing a new
manifest.
Composing with SystemDox
The manifest says what the agent can run. The when belongs in your
standards, where every agent in every repository reads it at write time:
- a SystemDox project rule — "Run
sync_repos before researching a
repository checkout; research the fresh tree, never a stale one";
- a prompt template for incident investigation that names the Sentry and
CloudWatch tools to call and the runbook to run afterwards.
One .mcp.json then carries the whole toolbelt:
{
"mcpServers": {
"systemdox": {
"type": "http",
"url": "https://<your-workspace>.mcp.systemdox.com/mcp",
"headers": { "Authorization": "Bearer ${SYSTEMDOX_API_KEY}" }
},
"runbooks": {
"command": "npx",
"args": ["-y", "@systemdox/runbook-mcp", "--manifest", "./runbooks.json"]
},
"sentry": {
"command": "npx",
"args": ["-y", "@sentry/mcp-server@latest", "--host=de.sentry.io"],
"env": { "SENTRY_ACCESS_TOKEN": "${SENTRY_ACCESS_TOKEN}" }
},
"cloudwatch": {
"command": "uvx",
"args": ["awslabs.cloudwatch-mcp-server@latest"],
"env": { "AWS_REGION": "eu-west-2", "AWS_PROFILE": "${AWS_PROFILE:-default}" }
}
}
}
Full guide: Runbooks as agent tools.
CLI
systemdox-runbook-mcp [--manifest <path>] Start the MCP server (stdio)
systemdox-runbook-mcp init Write a starter manifest and example script
systemdox-runbook-mcp validate Check the manifest; list tools and the command each would run
systemdox-runbook-mcp --help | --version
RUNBOOK_MCP_MANIFEST | Manifest path (alternative to --manifest) |
RUNBOOK_MCP_DRY_RUN=1 | Report commands instead of running them |
RUNBOOK_MCP_BASH | Path to bash.exe on Windows |
Library use
import { loadManifest, createServer } from "@systemdox/runbook-mcp";
const server = createServer(loadManifest("./runbooks.json"));
License
ISC — PuglieseWeb LTD.