DevOps MCP — Secure MCP Server for Linux Server Automation

by mhasnainjafri

Not rated
GitHub

Description

A three-tier access control MCP server that allows AI assistants (Claude Code, Cursor, Windsurf) to safely scan, plan, and operate Linux servers via SSH without full write access. Includes an out-of-band human consent token gate, automated port-conflict scanning, and a…

About

A three-tier access control MCP server that allows AI assistants (Claude Code, Cursor, Windsurf) to safely scan, plan, and operate Linux servers via SSH without full write access. Includes an out-of-band human consent token gate, automated port-conflict scanning, and a completely read-only default safe mode to…

Details

Author
mhasnainjafri
Categories
Cloud Service, Other, Infrastructure, Security

⚡ First-time setup (read this once, do it once)

There are exactly four steps. Don't skip step 2.

git clone <your-fork-url>.git devops-mcp cd devops-mcp npm install npm run build

DevOps MCP — Secure MCP Server for Linux Server Automation

A three-tier access control MCP server that allows AI assistants (Claude Code, Cursor, Windsurf) to safely scan, plan, and operate Linux servers via SSH without full write access. Includes an out-of-band human consent token gate, automated port-conflict scanning, and a completely read-only default safe mode to eliminate accidental destructive commands on production environments.

A mode-based MCP (Model Context Protocol) server that lets AI assistants (Claude Desktop, Cursor, Windsurf, …) actually operate Linux servers without handing them the keys to the kingdom.

The model can connect, scan, plan, and deploy — but every step thatchanges state on a production-like serverpasses through a consent gate the AI cannot self-approve. Discovery is read-only by design.

┌─────────────────┐ MCP / stdio ┌────────────────────┐ │ AI client │ ───────────────────────────► │ devops-mcp │ │ (Claude / │ │ │ │ Cursor / …) │ ◄─────────────────────────── │ ssh2 / docker / │ └─────────────────┘ │ child_process │ └────────┬───────────┘ │ SSH ▼ ┌────────────────┐ │ Your VPS │ └────────────────┘

⚡ First-time setup (read this once, do it once)

There are exactly four steps. Don't skip step 2.

git clone <your-fork-url>.git devops-mcp cd devops-mcp npm install npm run build

2. Generate yourelevation tokenandsave it somewhere you won't lose it

# Linux / macOS openssl rand -hex 24 # Windows PowerShell $bytes = New-Object byte[] 24; (New-Object System.Security.Cryptography.RNGCryptoServiceProvider).GetBytes($bytes); [BitConverter]::ToString($bytes).Replace("-","").ToLower() # Or, via Node node -e "console.log(require('crypto').randomBytes(24).toString('hex'))"

You'll get something like6ba329add30b19a5a347178f7e3705fdea0ac1aa66cb9274.

This token is the only thing standing between the AI and uncontrolled production access.The model never sees it.Whenever the AI wants to elevate to PROVISION/FULL mode, approve a destructive action, change a server's role, or write on a production-like server, you paste it once.

Put it in a password manager. If you lose it:

- You can hand-edit your MCP client's config to set a new one,or
- You can ask the AI to callrotate_consent_tokenif you still have the old one (which is circular if you've lost both).

There is no recovery flow. This is the gate; we don't ship a back door.

3. Add devops-mcp to your MCP client's config

ForClaude Desktop, editclaude_desktop_config.json:

- Windows:%APPDATA%\Claude\claude_desktop_config.json
- macOS:~/Library/Application Support/Claude/claude_desktop_config.json
- Linux:~/.config/Claude/claude_desktop_config.json

Add (or merge into existingmcpServers):

{ "mcpServers": { "devops-mcp": { "command": "node", "args": ["/absolute/path/to/devops-mcp/dist/index.js"], "env": { "DEVOPS_MCP_ELEVATION_TOKEN": "<paste your token from step 2 here>", "LOG_LEVEL": "info" } } } }

The same structure works for Cursor, Windsurf, and any other MCP client — the env block is the standard MCP way of passing secrets.

4. Fully quit and reopen your MCP client

Not "close the window." On Windows, that meanssystem tray → Quit. The elevation token is read at startup; the client has to restart for it to take effect.

You're done. Next time you talk to the AI, say "add my server at …" and it will walk you through.

Generic "run-any-command" MCP servers are dangerous on production boxes. A model with full shell on a live server can — and will — restart the wrong service, deploy onto an in-use port,docker prunea database volume, or escalate itself to root because nothing told it not to.

devops-mcp draws a hard line betweenreadingandchanging:

- Readingis always allowed (within a read-only SAFE allowlist).
- Changingon a production-like server requires the human's token — passed out-of-band, invisible to the model.
- Deploying a new projectgoes through a port-conflict check and a reviewable script, not 40 ad-hoc commands.

- Three-tier mode:SAFE(default, read-only allowlist),PROVISION(system installs, 1 h default expiry),FULL(root, 30 min default expiry).
- Out-of-band consent token— elevation and approvals require a string only the user has. The model literally cannot read it.
- Production write-gate— onrole: productionservers (or any server the scanner flags asproductionLikely), any non-SAFE command requiresconsentToken+acknowledgeProductionWrite: true.Catastrophiccommands — ones that are irrecoverable without a backup (rmof a non-temp path,rm -rf /…,dd of=/dev/…,mkfs, SQLDROP TABLE/DATABASE,docker rmi,docker volume rm,docker rm -v,docker system prune) — additionally requirebackupVerified: true. Ordinary writes and recoverable ops (editing a file,rm /tmp/scratch, baredocker rmof a container that can be recreated from its image) donotneedbackupVerified. Refusals echo theexact resolved command.
- Per-server policyallowedModes,blockedCommands,allowedPaths,requireApprovallive inconfig/<server-id>/server.jsonand are enforced on every SSH command.
- Requiredroleadd_serverwill not let the AI silently default the role; it must ask the user, and the response includes aroleConsequencesblock the AI reads back to you.
- Token rotationrotate_consent_tokengenerates a fresh token (defaults to dry-run;apply: trueatomically updates your MCP client config).
- Credential rotationupdate_server_credentialsrotates the password, swaps the SSH key (incl. encrypted keys viakeyPassphrase), or migrates host/user/port without re-adding the server. The role, restrictions, and scan profile stay intact. Closes any active session to that server first, validates the new creds with a test connection, and is consent-gated on production.
- AWS / EC2.pemready— onboard with the.pemfile + username + IP. Reference it in place (externalKeyPath) or copy it into the config bundle (keyFilePath); addkeyPassphraseonly if the key is encrypted.
- Multiple simultaneous connections, keyed by serverId— the MCP holds one SSH connectionper server, not one global slot. Claude Desktop runs a single shared MCP process across all your conversations; with a global connection, two chats working on two servers would clobber each other ("session 1 on server A, session 2 connects to B, now A's commands hit B"). Keyed connections let both coexist.run_commandtakes aserverId: optional when exactly one server is connected,required when two or more are— an ambiguous call is refused rather than guessed. Everyrun_commandresponse echoestarget.serverIdandactiveConnections.
- Anti-target-driftrun_command,set_mode, andget_current_moderesponses all carry the connected server identity, so a conversation can never silently end up operating the wrong box.disconnect_servertakes an optionalserverId(or"all").
- Live-session aware onboardingadd_serversurfaces the currently-connected server(s) in its response and tells the AI not to auto-switch to the newly-added one without asking.

- Server discovery scan— read-only probe of OS, hardware, listening ports, installed stack (docker / nginx / apache / node / pm2), running containers, parsed nginx sites, systemd services. Output persisted as aServerProfile.
- Profile diff on reconnectdiff_server_profilere-scans and reports what changed since the saved snapshot.
- Port-conflict awarenesscheck_port_conflictreturns the listening process + a free-port suggestion before deployment.
- Plan, don't fireplan_deploymentreturns an idempotent bash script the user reviews. The MCP doesnotexecute it.

- All command args shell-quotedbefore they hit the remote shell. No moresh -c "<long script>"payloads splitting at the wrong shell level.
- Validator inspects argsrun_command({command:"ls", args:["; rm -rf /"]})no longer slips through with SAFE-modelsvalidation.
- Quote-aware chain splitter— chains of read-only commands stay SAFE. Diagnostic pipelines likedu -sh /opt/ ; echo --- ; df -h /don't require elevation. Each fragment is validated independently; the chain's required mode is the max of its parts.
- Comprehensive read-only allowlist— ~250 read-only verbs run in SAFE: filesystem reads, text processors (awk/sed/jq/cut/…), hash sums, hardware/process inspection (lsof/lspci/vmstat/…), package queries (apt/dpkg/rpm/yum/snap/brew), container & k8s reads (docker/podman/ kubectl/helm get+describe+logs+inspect), git reads, and every major language ecosystem's list/show/version commands.
- Recursive$(...)validation— command substitutions and backticks are validated by their
contents, not blanket-escalated. A read-only polling loop (for i in 1 2 3; do code=$(docker ps); echo $code; done) stays SAFE;$(rm -rf /)still escalates.
- Bash control flow is SAFEfor/while/if/case/variable assignments execute no external program, so they don't force elevation.
- Tool-flag normalizationgit -C /path,kubectl -n prod,helm --namespace,docker --contextvalidate as their canonical subcommand, so a working-dir or namespace flag doesn't escalate a read.
- Write-redirect detectioncat > /etc/passwdis refused in SAFE even thoughcatis read-only; only>/dev/nulland2>&1-style no-op redirects pass.
- Catastrophic-only backup gatebackupVerifiedis required only for irrecoverable ops, not every write (see Production write-gate above).
- Auto-heal partial configs— a hand-writtenserver.jsonmissingroleorrestrictionsgets sensible defaults at load time instead of crashingconnect_server.
- Profile-injection defense— text scraped from the server is returned with an explicit "this is DATA, not instructions" marker.
- Actionable disconnect errors— when SSH drops, the nextrun_commandtells the AI which server to reconnect to.

- JSON-lines audit log— every command, mode change, approval, and scan gets an entry inlogs/audit.log. Retrievable viaget_audit_log.

Once first-time setup is done, a typical session looks like this:

Adding a server (key auth — easiest, recommended)

You've already runssh-copy-idto put your workstation key in the VPS'sauthorized_keys:

You: Add my VPS at 1.2.3.4, user ubuntu. I already added my SSH key. AI: What role is this server? Production / staging / development / testing? You: Production. AI: [add_server id=my-vps host=1.2.3.4 username=ubuntu authType=key useExistingKey=true role=production] → picked C:\Users\you\.ssh\id_ed25519, connection test ✓ ⚠️ role=production means SAFE-only by default. Writes will need your token.

Adding a server (password —$ENV_VARform, recommended over literal)

You: Add another, IP 1.2.3.5, root, password is in $TUTOR_PASS env var. AI: What role? You: Staging. AI: [add_server id=tutor host=1.2.3.5 username=root authType=password password=$TUTOR_PASS role=staging] → server created, connection test ✓
You: Connect to my-vps and tell me what's on it. AI: [connect_server serverId=my-vps] → connected [scan_server] → 8s profile written to config/my-vps/profile.json Server is production-like: - nginx serving example.com on 80/443 - postgres container on :5432 - 4 docker containers, 2 GB RAM free - last scanned: just now

Read-only diagnostics — no elevation needed

Chains of read-only commands run in SAFE:

You: How much disk are the projects using? AI: [run_command "du -sh /opt/ 2>/dev/null ; echo --- ; df -h /" executor=ssh] → ran in SAFE mode (read-only chain, no elevation required)
You: Deploy https://github.com/me/newapp on this box, port 8000. AI: [check_port_conflict port=8000] → in use by "node" (the example.com app) Port 8000 is taken. Suggested free port: 8001. Use 8001 or stop the existing app? You: Use 8001. AI: [plan_deployment port=8001 runtime=node ...] → returns a 26-line bash script Here's the script. Please review. You: Looks good. Run it. AI: [run_command ...] → refused: production write-gate. To run this I need your elevation token and confirmation that a backup exists. You: Token is <paste>. Yes, snapshot taken this morning. AI: [run_command consentToken=<…> acknowledgeProductionWrite=true backupVerified=true] → ✓ deployed

Rotating credentials (the VPS password changed, or you swapped your SSH key)

You: I rotated my-vps's root password. New one is in $MY_VPS_PASS_NEW. AI: my-vps is role=production — for the rotation I need your elevation token. You: Token is <paste>. AI: [update_server_credentials serverId=my-vps authType=password password=$MY_VPS_PASS_NEW consentToken=<…>] → closed active SSH session (was connected), new creds tested ✓ Reconnect with connect_server when ready. You: Connect. AI: [connect_server serverId=my-vps] → ✓

The role, restrictions, blocked-commands list, and scan profile are preserved. Only the auth fields change.

You: Actually my-vps is staging now, not production. AI: This is a production-touching change, please confirm with the token. You: Token is <paste>. AI: [update_server serverId=my-vps role=staging applyRoleDefaults=true consentToken=<…>] → role changed; allowedModes now [SAFE, PROVISION].

Rotating the token (when the old one has leaked, e.g. into chat)

You: Generate a new elevation token and update Claude Desktop's config. AI: For verification, paste the current token. You: <paste current> AI: [rotate_consent_token consentToken=<current> apply=true] → 🔑 NEW TOKEN: <new> ← SAVE THIS NOW, in a password manager. Claude Desktop must be fully restarted for the new token to take effect. Until then, the OLD token still works on this running session.

- acknowledgeRisk: true(the AI sets this)
- consentToken: "<your token>"(only you have it)

Downgrade is always allowed and instant. Sessions auto-expire back to SAFE.

Five ways to authenticate, picked byauthType+ which key/password field you set:

The handler validatesexactly onekey source per call. Combining e.g.useExistingKeyandkeyFilePathis refused with a clear error.

Any key path (keyFilePath/externalKeyPath) accepts an optionalkeyPassphrase(literal or$ENV_VAR) for encrypted private keys.

You get a.pemfile, a username (ubuntu,ec2-user,admin, …), and a public IP/DNS. Two ways:

// Reference the .pem where it sits (recommended — nothing copied) { "id": "my-ec2", "host": "ec2-1-2-3-4.compute.amazonaws.com", "username": "ec2-user", "authType": "key", "externalKeyPath": "C:\\Users\\you\\Downloads\\my-key.pem", "role": "production" } // Or copy the .pem into the server's config folder (portable bundle) { "id": "my-ec2", "host": "1.2.3.4", "username": "ubuntu", "authType": "key", "keyFilePath": "C:\\Users\\you\\Downloads\\my-key.pem", "role": "staging" }

Most AWS keys have no passphrase — omitkeyPassphrase. If yours is encrypted, add"keyPassphrase": "$MY_PEM_PASS"and set that env var.

Modern sshd + password auth: ssh2 needstryKeyboard: truefor sshd setups that use PAM (Ubuntu 22.04+, Debian 12, Amazon Linux 2023, RHEL 9). devops-mcp sets this automatically — passwords work even when the server hasPasswordAuthentication noand only allowskeyboard-interactive.

config/ ├── my-vps/ │ ├── server.json # config (host, user, auth, role, restrictions) │ ├── key.pem # optional SSH private key (only if you used keyFilePath / privateKey) │ └── profile.json # written by scan_server └── _example/ └── server.json # template
{ "name": "Production Web", "host": "1.2.3.4", "port": 22, "username": "ubuntu", "authType": "key", "keyFile": "production.pem", "role": "production", "restrictions": { "allowedModes": ["SAFE"], "blockedCommands": ["rm -rf", "shutdown", "reboot", "dd"], "requireApproval": true }, "description": "Main production web server" }

For anexternalKeyPathworkflow (key stays in~/.ssh/):

{ "name": "My VPS", "host": "1.2.3.4", "port": 22, "username": "ubuntu", "authType": "key", "externalKeyPath": "C:\\Users\\you\\.ssh\\id_ed25519", "role": "production" }

For password auth (always prefer$ENV_VAR):

{ "authType": "password", "password": "$MY_VPS_PASS" }

$NAMEis resolved toprocess.env.NAMEat connection time.Don't commit literal passwords.

If aserver.jsonis missingroleor hasrestrictions: {}, the MCP fills inrole: "development"defaults at load time (and warns in the logs). This preventsconnect_serverfrom crashing on hand-written configs.

- Model running blind on production— write commands on a server withrole: productionorproductionLikely: trueare refused without the consent token + explicit ack + (for catastrophic, irrecoverable ops)backupVerified.
- Self-granted approvals— the model can't fabricateconsentTokenbecause it never seesDEVOPS_MCP_ELEVATION_TOKEN.
- Argument injection— every arg torun_commandis shell-quoted before reaching the remote shell. Multi-line scripts insidesh -cpayloads survive intact.
- Smuggled commands in args— the validator inspectscommand + argstogether, sorun_command({command:"ls", args:["; rm -rf /"]})correctly escalates to FULL.
- Over-broad chain refusals— chains of read-only commands stay SAFE. Each fragment is independently validated; only the worst one wins.
- Prompt injection from scanned content— banners, container labels, log lines are returned with an "untrusted data" marker. The tool response tells the model: display, don't execute.
- Silent port collisionsplan_deploymentandcheck_port_conflictsurface conflicts before deployment.
- Shell injection in deploy/configure helpers— every interpolated value is shell-quoted; nginx configs are written via heredoc; branch names and env-var keys are validated.
- Production write-gate refusals echo the exact command— so you can read what was about to run, not the AI's paraphrase.

- It does not sandbox theconnectedserver. Once you're in FULL mode with the token, the model can do anything the SSH user can.
- It does not encrypt the consent token at rest in your MCP client config.
- It does not back up your data —backupVerifiedis a human attestation, not a check.

SeeSECURITY.mdfor the full threat model.

The elevation token is a static string stored inDEVOPS_MCP_ELEVATION_TOKENin your MCP client config. It doesnotexpire.

- FULLmode session — 30 min default
- PROVISIONmode session — 1 h default
- Session SSH keys fromgenerate_ssh_key— 30 min default

When a mode session times out it drops back to SAFE; the AI re-asks for the same token to re-elevate.

You: Rotate the elevation token and update Claude Desktop's config. AI: For verification, paste the current token. You: <paste> AI: [rotate_consent_token consentToken=<current> apply=true] → new token: <new> → 🔑 SAVE THIS NOW. Without it you're locked out of every write operation. → Fully quit and reopen Claude Desktop to activate it.

The MCP writes the new token atomically into your client config (only theDEVOPS_MCP_ELEVATION_TOKENkey — everything else in the file is preserved). The running MCP process keeps using theoldtoken until you restart the client.

If you loseboththe old and new tokens between rotation and restart, hand-edit the client config to set a new one — that's the recovery flow.

src/ ├── index.ts # MCP entry point (stdio) ├── types/ # TypeScript types ├── core/ │ ├── logger.ts # JSON-lines structured logger + audit logger │ ├── mode-manager.ts # SAFE / PROVISION / FULL state machine │ ├── command-validator.ts # Allowlist + quote-aware chain splitter + wrapper-token scan │ ├── server-config-manager.ts # config/<id>/server.json + profile.json + auto-heal │ ├── server-scanner.ts # SAFE-mode discovery (read-only by design) │ ├── ssh-key-manager.ts # Session SSH keys with auto-expiry │ └── approval-manager.ts # Approval queue ├── executors/ # Local / SSH / Docker — all shell-quote args ├── playbooks/ # Provisioning playbooks (Docker, Nginx, …) └── tools/ ├── tool-schemas.ts # Zod schemas + MCP tool definitions └── tool-handlers.ts # The actual handlers
npm run dev # watch mode (tsx) npm run build # tsc → dist/ npm test # vitest npm run test:run # vitest run (CI mode) npm run lint # eslint src//.ts

When adding a new tool thatwriteson the connected server, make sure it runs throughBaseExecutor.execute()so the mode validator and the production write-gate apply.Do not shell out directly from a handler*, and if you must interpolate a value into a shell command, use theshellQuotehelper in the executor — the historical bugs in this codebase have all been quoting bugs.

The official Impreza Host connector for offshore server management

MCP (Model Context Protocol) server for managing VPS servers via the mikr.us API and remote Linux servers over SSH. Built in Python, runs anywhere — locally, in Docker, or as a Claude Desktop integration.

No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.