server-admin-mcp
Enables remote administration of a Debian stage server over SSH, including monitoring, Docker Compose, PostgreSQL, logs, Nginx, firewall, and automation with 82 tools.
README
server-admin-mcp
An MCP server that administers a Debian 13 stage host over SSH: monitoring, Docker Compose, deployment, PostgreSQL, logs, Nginx/TLS, firewall and scheduled automation — 82 tools and 4 workflow prompts.
Stage environments only. This server gives an assistant broad, real access to a machine. Everything destructive sits behind an explicit confirmation step, and a denylist rejects unrecoverable commands outright, but neither is a substitute for pointing it at a host you can afford to rebuild. See Security warnings.
Contents
- Installation
- Configuration
- Preparing the server
- Connecting a client
- Safety model
- Tool reference
- Prompts
- Testing
- Security warnings
- Troubleshooting
- Design decisions
Installation
Requires Node.js 20 or newer on the machine running the MCP client (not on the server).
npm install
npm run build
This produces dist/index.js, which is the entry point your MCP client launches. The server speaks MCP over stdio; it never opens a port of its own.
Available scripts:
| Script | Purpose |
|---|---|
npm run build |
Compile TypeScript to dist/ |
npm run typecheck |
tsc --noEmit |
npm test |
Unit tests for the safety layer (no server needed) |
npm run smoke |
Call every read-only tool against the real server and report |
npm run lint |
ESLint |
npm run format |
Prettier |
npm run inspect |
Launch the MCP Inspector against the built server |
Configuration
Every setting is an environment variable prefixed SRVMCP_. Copy .env.example and fill it in, then pass the values through your client's env block.
Optionally, a JSON file with the same keys in camelCase can be pointed at with SRVMCP_CONFIG_FILE (default ./config.json when it exists). Environment variables always win over the file.
SSH connection
| Variable | Required | Default | Description |
|---|---|---|---|
SRVMCP_HOST |
yes | — | Hostname or IP of the stage server |
SRVMCP_PORT |
no | 22 |
SSH port |
SRVMCP_USER |
yes | — | SSH login user (e.g. deploy) |
SRVMCP_PRIVATE_KEY_PATH |
yes | — | Private key used to authenticate. Must exist; ~ is expanded |
SRVMCP_PASSPHRASE |
no | — | Passphrase, if the key is encrypted |
SRVMCP_KNOWN_HOSTS_PATH |
no | — | known_hosts file used to verify the server's key. Strongly recommended |
SRVMCP_INSECURE_SKIP_HOST_KEY_CHECK |
no | false |
Disables host-key verification. Throwaway lab hosts only |
Project layout on the server
| Variable | Required | Default | Description |
|---|---|---|---|
SRVMCP_PROJECT_ROOT |
no | /opt/stage |
Directory holding the compose file. Always writable; writes here need no extra confirmation |
SRVMCP_COMPOSE_FILE |
no | docker-compose.yml |
Relative to PROJECT_ROOT, or absolute |
SRVMCP_ALLOWED_PATHS |
no | /opt/stage,/etc/nginx,/var/log |
Comma-separated roots for file operations. PROJECT_ROOT is always included |
PostgreSQL
Reached by running psql on the server, so SRVMCP_PG_HOST is resolved from the server's point of view (localhost or 127.0.0.1 for a container publishing 5432).
| Variable | Required | Default | Description |
|---|---|---|---|
SRVMCP_PG_HOST |
no | localhost |
Database host, as seen from the server |
SRVMCP_PG_PORT |
no | 5432 |
Database port |
SRVMCP_PG_USER |
for pg_* |
— | Read-only role used by every read tool |
SRVMCP_PG_PASSWORD |
no | — | Password for that role |
SRVMCP_PG_DATABASE |
for pg_* |
— | Default database |
SRVMCP_PG_WRITE_USER |
no | — | Write role. Leave unset to make every database write impossible |
SRVMCP_PG_WRITE_PASSWORD |
no | — | Password for the write role |
Limits and behaviour
| Variable | Required | Default | Description |
|---|---|---|---|
SRVMCP_MAX_OUTPUT_LINES |
no | 200 |
Line cap per command; excess is removed from the middle |
SRVMCP_COMMAND_TIMEOUT_MS |
no | 60000 |
Default command timeout. The process is killed on the server when it expires |
SRVMCP_AUDIT_LOG_PATH |
no | ./audit.log |
JSONL audit trail (local to the client machine) |
SRVMCP_READ_ONLY |
no | false |
When true, tools with readOnlyHint: false are not registered at all |
SRVMCP_CONFIG_FILE |
no | ./config.json |
Optional JSON config file |
Configuration is validated at startup with zod. A missing or wrong value produces a message naming the variable and how to fix it, and the process exits before the transport opens — so the client shows the error instead of hanging.
Preparing the server
The server should connect as a dedicated, restricted user — not root.
1. Create the deploy user
sudo adduser --disabled-password --gecos "" deploy
sudo mkdir -p /home/deploy/.ssh
sudo chmod 700 /home/deploy/.ssh
Add the MCP server's public key:
echo 'ssh-ed25519 AAAA... server-admin-mcp' | sudo tee -a /home/deploy/.ssh/authorized_keys
sudo chmod 600 /home/deploy/.ssh/authorized_keys
sudo chown -R deploy:deploy /home/deploy/.ssh
2. Grant Docker access
sudo usermod -aG docker deploy
Group membership only applies to new sessions — restart the MCP server afterwards so it reconnects.
3. Give the project directory to deploy
sudo chown -R deploy:deploy /opt/stage
4. A narrow sudoers file
Only the binaries the tools actually need, with no password prompt (an interactive prompt would hang a non-interactive session):
sudo visudo -f /etc/sudoers.d/deploy-mcp
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl, /usr/sbin/nginx, /usr/bin/journalctl, \
/usr/bin/apt-get, /usr/sbin/ufw, /usr/bin/certbot, /usr/bin/install, \
/usr/bin/crontab, /usr/sbin/logrotate, /usr/bin/systemd-analyze, \
/usr/bin/fail2ban-client, /bin/ln, /bin/rm, /bin/cat, /usr/bin/openssl
sudo chmod 440 /etc/sudoers.d/deploy-mcp
sudo visudo -c # verify before logging out
Grant only what you need. Dropping /usr/bin/apt-get disables package upgrades; dropping /usr/sbin/ufw disables firewall changes. Each affected tool then returns an actionable "this needs a sudoers entry" error rather than failing obscurely.
To read logs without sudo, add the user to adm instead:
sudo usermod -aG adm deploy # /var/log/nginx, journal
5. Create a read-only PostgreSQL role
CREATE ROLE mcp_ro LOGIN PASSWORD 'choose-a-strong-password';
GRANT CONNECT ON DATABASE stage_db TO mcp_ro;
GRANT USAGE ON SCHEMA public TO mcp_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mcp_ro;
The ALTER DEFAULT PRIVILEGES line matters: without it, tables created by future migrations are invisible to mcp_ro. It applies to objects created by the role that runs it, so run it as the role owning your migrations. pg_fix_schema_permissions applies this whole set for you.
Optionally, a separate write role for pg_write, pg_maintenance, pg_restore_db and pg_fix_schema_permissions:
CREATE ROLE mcp_rw LOGIN PASSWORD 'another-strong-password';
GRANT CONNECT ON DATABASE stage_db TO mcp_rw;
GRANT USAGE, CREATE ON SCHEMA public TO mcp_rw;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO mcp_rw;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO mcp_rw;
Leaving SRVMCP_PG_WRITE_USER unset makes every database write structurally impossible, which is a good default until you need one.
psql must be installed on the server: sudo apt-get install -y postgresql-client.
6. Record the host key
ssh-keyscan -p 22 stage.example.com >> ~/.ssh/known_hosts
Verify the fingerprint out of band before trusting it, then point SRVMCP_KNOWN_HOSTS_PATH at that file.
Connecting a client
Claude Desktop / Claude Code
claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/, Windows: %APPDATA%\Claude\):
{
"mcpServers": {
"server-admin": {
"command": "node",
"args": ["/absolute/path/to/server-admin-mcp/dist/index.js"],
"env": {
"SRVMCP_HOST": "stage.example.com",
"SRVMCP_USER": "deploy",
"SRVMCP_PRIVATE_KEY_PATH": "/home/you/.ssh/id_ed25519",
"SRVMCP_KNOWN_HOSTS_PATH": "/home/you/.ssh/known_hosts",
"SRVMCP_PROJECT_ROOT": "/opt/stage",
"SRVMCP_ALLOWED_PATHS": "/opt/stage,/etc/nginx,/var/log",
"SRVMCP_PG_HOST": "localhost",
"SRVMCP_PG_USER": "mcp_ro",
"SRVMCP_PG_PASSWORD": "...",
"SRVMCP_PG_DATABASE": "stage_db",
"SRVMCP_AUDIT_LOG_PATH": "/var/log/server-admin-mcp/audit.log"
}
}
}
}
Use absolute paths — the client's working directory is not yours.
A read-only instance, useful for giving an assistant visibility without any ability to change anything:
{
"mcpServers": {
"stage-readonly": {
"command": "node",
"args": ["/absolute/path/to/server-admin-mcp/dist/index.js"],
"env": { "SRVMCP_READ_ONLY": "true", "SRVMCP_HOST": "...", "SRVMCP_USER": "deploy" }
}
}
}
Safety model
Four independent layers, applied in this order:
1. Denylist — absolute, cannot be overridden
Checked on the fully assembled command string inside the SSH layer, so no tool can route around it, and confirm: true does not help. It targets commands that are unrecoverable on a remote box with no console:
| Rule | Blocks |
|---|---|
rm-rf-root / rm-rf-root-alt-flags |
rm -rf / and recursive deletes of system directories, in any flag order |
rm-home-wildcard |
Wildcard deletion of a home directory |
filesystem-format |
mkfs, fdisk, parted, sgdisk, mkswap |
dd-to-device |
dd of=/dev/sda and similar |
redirect-to-block-device |
> /dev/sda, > /dev/nvme0n1 |
power-state-change |
shutdown, reboot, halt, poweroff, init 0/6, systemctl reboot |
chmod-777-system, chmod-777-root-shorthand |
chmod -R 777 on / or a system path |
chown-recursive-system |
chown -R on / or a system path |
fork-bomb |
:(){ :|:& };: |
system-user-delete |
userdel/groupdel on system accounts |
firewall-flush, firewall-default-drop |
iptables -F, nft flush ruleset, ufw reset, default-DROP on INPUT |
history-wipe |
history -c, truncating shell history |
remote-code-execution |
curl … | bash, wget … | sh, and the python/perl/ruby/node variants |
overwrite-shadow |
Writing over /etc/shadow, /etc/passwd, /etc/sudoers |
ssh-lockout |
Deleting or truncating authorized_keys |
docker-kill-all |
docker rm $(docker ps -q) and friends |
The error names the rule that fired, so it is obvious what triggered and why.
Beyond the denylist there is one more hard guard: sec_firewall_rule refuses to deny or delete the SSH port, because applying that rule would disconnect the server permanently.
2. Confirmation — preview, then execute
Destructive tools take confirm: boolean. Without confirm: true the tool executes nothing and instead returns a preview: the exact command, the resulting state change, what is irreversible, and what to check first.
PREVIEW — nothing has been executed.
Command that would run:
$ docker volume prune -f
Effect: Deletes every Docker volume not currently attached to a container.
Irreversible: YES — volume data cannot be recovered. A stopped database
container makes its data volume look unused.
Before approving:
- Run compose_ps first: every stack service must exist, otherwise its
volume counts as unused.
- Take a database backup with pg_backup and copy it off the server first.
To execute, call docker_prune again with the same arguments plus confirm: true.
Tools requiring confirmation: sys_service_control (stop only), sys_run_command (when the command mutates state), docker_exec (same), compose_action (down/stop/rm), docker_prune, docker_volume_remove, git_reset, deploy_run, migration_run, pg_write, pg_kill_query, pg_maintenance, pg_restore_db, pg_fix_schema_permissions, nginx_write_config, nginx_enable_site, nginx_disable_site, ssl_renew, sec_firewall_rule, sec_package_upgrade, env_set, all five auto_* tools, and file_write/file_patch outside PROJECT_ROOT.
For free-form commands (sys_run_command, docker_exec) confirmation is demanded when the command contains rm, mv, truncate, kill/pkill, chmod/chown, apt, an overwriting redirect (>, but not 2>&1), a Docker removal, or destructive SQL.
3. Path allowlist — checked twice
Every file operation normalises the path (resolving ..), then verifies it is inside one of SRVMCP_ALLOWED_PATHS. A second check runs on the server after readlink -f, so a symlink planted inside an allowed root cannot be used to reach outside it. A sibling with a shared prefix (/opt/stagemore against /opt/stage) is correctly excluded.
4. Audit log — every call, JSONL
Every invocation is appended to SRVMCP_AUDIT_LOG_PATH, successful or not:
{"ts":"2026-07-25T10:00:00.000Z","tool":"compose_action","args":{"action":"restart","services":["api"],"confirm":true},"status":"ok","command":"docker compose -f /opt/stage/docker-compose.yml restart api","exitCode":0,"durationMs":1234,"confirmed":true}
Writes are queued so concurrent calls cannot interleave, and a failure to write warns on stderr without aborting the tool. Credentials are masked before anything is written.
Read-only mode
With SRVMCP_READ_ONLY=true, tools whose readOnlyHint is false are never registered — 49 tools are exposed instead of 82, and a write tool is not merely refused but absent from tools/list entirely. There is no runtime flag an assistant could flip.
Secret masking
Everything leaving the process — tool text, structured content and audit records — passes through a masker covering password/secret/token/api_key/auth/credential-style keys, Bearer/Basic headers, credentials embedded in URLs, JWTs, PEM private-key blocks and PGPASSWORD=. docker_inspect masks environment values by key name, and env_read masks every value unless a single revealKey is named explicitly.
Database credentials are never placed on a command line: a ~/.srvmcp.pgpass file (mode 0600) is written once and referenced through PGPASSFILE, keeping the password out of ps output entirely.
Tool reference
82 tools. RO marks read-only (available in read-only mode); ! marks tools requiring confirm.
System — sys_* (10)
| Tool | Description | |
|---|---|---|
sys_metrics |
RO | CPU, load, RAM, swap, every filesystem, uptime — one structured snapshot |
sys_top_processes |
RO | Heaviest processes by CPU or memory |
sys_disk_usage |
RO | Directory sizes under a path, largest first |
sys_ports |
RO | Listening TCP/UDP sockets with the owning process |
sys_services |
RO | systemd units and their state; onlyFailed to scan for breakage |
sys_service_control |
! | start / stop / restart / reload / status a unit (stop needs confirm) |
sys_uptime_reboots |
RO | Uptime, recent reboots, OOM kills from the kernel log |
sys_run_command |
! | Arbitrary shell command, subject to the denylist |
sys_run_background |
Start a long command detached; returns PID and log path | |
sys_check_background |
RO | Status and log tail of a background job |
Docker — docker_*, compose_* (13)
| Tool | Description | |
|---|---|---|
docker_ps |
RO | Containers with state, health, ports, uptime |
docker_stats |
RO | Per-container CPU / memory / network / block I/O sample |
docker_logs |
RO | Container logs with tail, since and grep filters |
docker_inspect |
RO | Restart count, exit code, OOM flag, health, mounts, networks, masked env |
docker_exec |
! | Run a command inside a container |
compose_ps |
RO | Compose service states |
compose_action |
! | up/down/restart/build/pull/stop/rm |
compose_config |
RO | Fully resolved compose configuration; validates the file |
docker_images |
RO | Images with size and age |
docker_prune |
! | Reclaim space: images / volumes / builder / containers / system |
docker_networks |
RO | Networks and the containers attached to each |
docker_volumes |
RO | Volumes with size and whether anything uses them |
docker_volume_remove |
! | Delete one named volume |
Deployment and git — git_*, deploy_*, migration_* (9)
| Tool | Description | |
|---|---|---|
git_status |
RO | Branch, HEAD, ahead/behind (fetches first), working-tree changes |
git_pull |
Fast-forward-only pull | |
git_checkout |
Switch to a branch, tag or commit | |
git_log |
RO | Recent commits |
git_reset |
! | Rollback to a ref; returns the previous SHA so it can be undone |
deploy_health_check |
RO | Request URLs from the server; status code and response time |
deploy_run |
! | pull → build → up -d → settle → container check → health check, stopping at the first failure |
migration_run |
! | Apply EF Core / Django / Alembic / custom migrations in a container |
migration_status |
RO | Applied vs shipped migrations, reporting pending and drift |
PostgreSQL — pg_* (13)
| Tool | Description | |
|---|---|---|
pg_query |
RO | Read-only SQL as the read-only role; auto-LIMIT; writes rejected |
pg_write |
! | INSERT / UPDATE / DELETE / DDL as the write role |
pg_databases |
RO | Databases with owner, encoding and size |
pg_tables |
RO | Tables with estimated rows, table and index size |
pg_table_schema |
RO | Columns, indexes and foreign keys of one table |
pg_activity |
RO | Active backends, query age, lock waits |
pg_kill_query |
! | Cancel (default) or terminate a backend |
pg_slow_queries |
RO | Slowest statements via pg_stat_statements, with setup guidance if absent |
pg_maintenance |
! | VACUUM / VACUUM FULL / ANALYZE / REINDEX |
pg_backup |
pg_dump -Fc to a directory; returns path and size |
|
pg_restore_db |
! | Restore a custom-format dump |
pg_permissions |
RO | Roles, schema ACL and table grants |
pg_fix_schema_permissions |
! | Apply the standard grant set incl. ALTER DEFAULT PRIVILEGES (the PG15+ trap) |
Logs — log_* (6)
| Tool | Description | |
|---|---|---|
log_journal |
RO | journalctl with unit/time/priority/grep filters, repeated messages grouped |
log_tail |
RO | Tail a log file with optional (invertible) grep |
log_nginx_errors |
RO | Nginx errors grouped by message, upstream failures isolated |
log_nginx_access_summary |
RO | Requests by status, busiest paths, noisiest IPs, slowest requests, error rate |
log_search |
RO | grep across several files with context |
log_elasticsearch_query |
RO | POST a search body to Elasticsearch from the server |
Nginx and TLS — nginx_*, ssl_* (9)
| Tool | Description | |
|---|---|---|
nginx_list_sites |
RO | vhosts with enabled state, server names, listens, upstreams |
nginx_read_config |
RO | Read a config file from /etc/nginx |
nginx_write_config |
! | Backup → write → nginx -t → automatic rollback on failure |
nginx_test |
RO | nginx -t |
nginx_reload |
Test, then reload only if the test passes | |
nginx_enable_site |
! | Symlink into sites-enabled, validated, auto-reverted on failure |
nginx_disable_site |
! | Remove the symlink; the vhost file is untouched |
ssl_cert_info |
RO | Certificate issuer, domains, expiry, days remaining |
ssl_renew |
! | certbot renew, dry run by default |
Security — sec_* (9)
| Tool | Description | |
|---|---|---|
sec_firewall_status |
RO | ufw / nftables / iptables rules, plus an SSH-reachability sanity check |
sec_firewall_rule |
! | allow / deny / delete a ufw rule (refuses to touch the SSH port) |
sec_fail2ban_status |
RO | Jails, failure counts, banned addresses |
sec_ssh_attempts |
RO | Failed logins grouped by source, successful logins listed separately |
sec_users |
RO | Accounts, shells, sudo membership, last login, locked state |
sec_ssh_keys |
RO | authorized_keys per account — fingerprints and comments only |
sec_package_updates |
RO | Upgradable packages, security ones flagged, reboot-required state |
sec_package_upgrade |
! | Non-interactive apt upgrade, keeping existing config files |
sec_cron_jobs |
RO | User crontabs, /etc/cron.*, and systemd timers |
Files — file_*, env_* (8)
| Tool | Description | |
|---|---|---|
file_read |
RO | Read a file over SFTP (1 MB cap), optionally a line range |
file_write |
! | Atomic write with automatic timestamped backup |
file_patch |
! | Replace one unique string; ambiguous matches are refused |
file_list |
RO | Directory listing with size, owner, permissions, mtime |
file_search |
RO | Find files by name glob and/or content regex |
file_stat |
RO | Metadata for one path, including symlink target |
env_read |
RO | dotenv keys with masked values unless one revealKey is named |
env_set |
! | Add or update one key, preserving comments and ordering |
Automation — auto_* (5)
| Tool | Description | |
|---|---|---|
auto_create_cron |
! | Add a cron job (schedule validated first), tagged # SRVMCP |
auto_remove_cron |
! | Remove matching cron lines and their marker comment |
auto_create_systemd_unit |
! | Write a unit, daemon-reload, verify, optionally enable and start |
auto_setup_backup |
! | pg_dump script + retention + cron, test-run once before scheduling |
auto_setup_logrotate |
! | logrotate config, validated with a dry run and removed if invalid |
Prompts
| Prompt | Arguments | What it does |
|---|---|---|
diagnose_service |
service |
State → history → logs → resources → connectivity → external health, ending in a stated root cause with evidence |
daily_health_report |
— | Full read-only sweep, reported by urgency rather than by category |
pre_deploy_check |
services? |
Disk, git cleanliness, incoming commits, current health, compose validity, database, backups → GO / NO-GO plus the rollback SHA |
investigate_5xx |
since? |
Correlates Nginx 5xx with backend logs, resources and database over one window |
Testing
Unit tests — no server required
npm test
134 assertions covering the denylist (each dangerous command individually, plus a set of safe ones that must not be blocked), the path allowlist including traversal and shared-prefix cases, middle-out truncation, control-character sanitising, secret masking, shell quoting, read-only SQL classification (including SELECT 1; DROP TABLE users), and CSV parsing.
Schema inspection
npm run inspect
Confirms all 82 tools list with valid input and output schemas and complete annotations.
Smoke test — read-only, against the real server
npm run smoke # every read-only tool
npm run smoke -- sys_ pg_ # only these prefixes
Calls each read-only tool in turn and prints a pass/fail table. It never invokes a tool whose readOnlyHint is false, so it is safe against a live stage box.
Security warnings
This server grants an assistant broad, real access to a machine. Treat it accordingly.
- Stage only. Do not point it at production. Nothing here distinguishes the two for you.
- Never connect as
root. Use a dedicated user with the narrow sudoers file above. The safety layers are defence in depth, not a replacement for least privilege. - Prefer read-only.
SRVMCP_READ_ONLY=trueremoves 33 write tools from the interface entirely. Run a second, writable instance only when you need it. - Leave
SRVMCP_PG_WRITE_USERunset until a database write is genuinely required — that makespg_write,pg_restore_dbandpg_maintenancestructurally unavailable. - Set
SRVMCP_KNOWN_HOSTS_PATH. Without it the first connection is trusted blindly, which makes a man-in-the-middle attack possible on the first connect. - Review the audit log. It records every call with arguments and exit codes. It is the only record of what was actually done:
tail -f audit.log | jq 'select(.confirmed == true)' # everything executed with confirmation jq 'select(.status == "error")' audit.log # everything that failed jq -r '[.ts, .tool, .command] | @tsv' audit.log # a readable timeline - Keep
SRVMCP_ALLOWED_PATHStight. Every added root widens what can be read and written. Adding/defeats the mechanism. - The private key is the whole boundary. Anyone who can read it, and the environment holding the passphrase, has the same access this server does.
- Backups made by
pg_backupandauto_setup_backupstay on the same host as the database. They protect against a bad migration, not against losing the server. Arrange an off-box copy separately.
Troubleshooting
SSH authentication to deploy@host:22 failed (permission denied)
The key is wrong or not authorised. Reproduce outside the MCP server:
ssh -i ~/.ssh/id_ed25519 -p 22 deploy@stage.example.com
Check the public half is in /home/deploy/.ssh/authorized_keys, and that modes are 700 on ~/.ssh and 600 on authorized_keys — sshd silently refuses looser permissions.
Host key verification failed
Either the host is not in known_hosts yet, or its key changed. Verify the fingerprint out of band, then:
ssh-keyscan -p 22 stage.example.com >> ~/.ssh/known_hosts
A changed key on a host you have connected to before deserves investigation before you clear the old entry.
Cannot talk to the Docker daemon: permission denied on /var/run/docker.sock
sudo usermod -aG docker deploy
Then restart the MCP server — group membership only applies to new sessions.
The command needs sudo but the account cannot use it without a password
The binary is missing from /etc/sudoers.d/deploy-mcp. Add it, then sudo visudo -c to verify. A prompting sudo hangs a non-interactive session, which is why sudo -n is used throughout.
The command timed out after 60s
Either raise timeoutMs for that call, or start the work in the background:
sys_run_background(command: "...") -> sys_check_background(pid: 1234)
Builds, migrations, large upgrades and pg_dump on a big database are the usual candidates. deploy_run already allows 30 minutes for the build step.
Cannot reach PostgreSQL at localhost:5432
SRVMCP_PG_HOST is resolved from the server's point of view. If the database runs in Docker, check docker_ps for the published port — usually 127.0.0.1:5432. If it is not published at all, run queries inside the container with docker_exec instead.
Refused: path "/etc/shadow" is outside the configured allowlist
Working as intended. Add the root to SRVMCP_ALLOWED_PATHS only if you genuinely want it reachable.
permission denied for schema public after upgrading to PostgreSQL 15
PostgreSQL 15 removed the implicit CREATE grant on public from PUBLIC. Fix it in one call:
pg_fix_schema_permissions(user: "app_user", schema: "public", confirm: true)
A tool is missing from tools/list
Check whether SRVMCP_READ_ONLY=true — write tools are not registered in that mode. The server logs N tools registered, M withheld (read-only mode) to stderr at startup.
Nginx logs are unreadable
They are root-owned by default:
sudo usermod -aG adm deploy
The audit log is not being written
The server warns once on stderr and keeps working. Check that the directory exists and is writable by whoever runs the MCP client.
Design decisions
Choices made where the specification left room, recorded here as required.
Project at the repository root, not in a server-admin-mcp/ sub-directory. The working directory was already the project root; nesting would have added a level for no benefit. The package is still named server-admin-mcp.
Two extra source files beyond the specified layout. src/utils/shell.ts holds shell quoting, and src/safety/paths.ts holds the path allowlist — both are separate concerns that would otherwise have been duplicated across nine tool modules. src/tools/registry.ts, src/tools/types.ts and src/tools/context.ts hold the shared registration, envelope and dependency plumbing that keeps each tool module free of boilerplate. src/ssh/knownHosts.ts implements host-key verification, which ssh2 leaves to the caller.
Manual POSIX quoting instead of shell-quote. Single-quoting is five lines; for a server with this much reach, one fewer dependency to audit was worth more than the package.
PGPASSFILE instead of PGPASSWORD. The specification asked for the password to be passed by environment rather than visibly in the command. Because the environment is exported inside the remote sh -c string, PGPASSWORD would still appear in ps output. A ~/.srvmcp.pgpass file at mode 0600, referenced through PGPASSFILE, keeps it out of process listings entirely.
Uniform output envelope. Every tool's outputSchema carries status and message alongside its own fields, with tool-specific fields optional. That is what lets a confirm preview validate against the same schema as a successful run, so previews are ordinary results rather than errors.
Statement batching rejected in pg_query. Checking only the first keyword would let SELECT 1; DROP TABLE users through. String literals and comments are stripped before keyword matching so a value like 'delete from users' is not mistaken for a write.
Remote timeout(1) plus a local timer. Killing the SSH channel alone leaves the remote process running. The command is wrapped in timeout -k 5s, so it dies on the server; the local timer is only a backstop for a wedged channel.
Atomic writes and double path checking. Files are written to a temporary sibling and renamed, so an interrupted transfer cannot leave a half-written config. Paths are checked textually and again on the server after readlink -f, so a symlink inside an allowed root cannot escape it.
ssl_renew defaults to dryRun: true. Let's Encrypt rate-limits real issuance; a dry run costs nothing and catches a broken challenge setup.
sec_firewall_rule will not touch the SSH port. Not a confirmation — a refusal. Applying such a rule would disconnect the server with no way back in, so it must be done from a console session.
auto_setup_backup test-runs the script before scheduling it. A backup that only fails at 3 a.m. is worse than no backup, so nothing is added to cron until one real dump has succeeded.
Extra tools beyond the specification. file_stat (metadata for a single path, useful when diagnosing permissions) and migration_status support for Alembic alongside EF Core and Django.
docker_exec requires confirmation for mutating commands, matching the rule the specification set for sys_run_command; it is the same class of escape hatch.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。