Skip to main content
Glama
shahinnr

server-admin-mcp

by shahinnr

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

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 -tautomatic 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=true removes 33 write tools from the interface entirely. Run a second, writable instance only when you need it.

  • Leave SRVMCP_PG_WRITE_USER unset until a database write is genuinely required — that makes pg_write, pg_restore_db and pg_maintenance structurally 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_PATHS tight. 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_backup and auto_setup_backup stay 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.

-
license - not tested
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/shahinnr/server_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server