Skip to content

Repository files navigation

MCP Ubuntu Server

An MCP (Model Context Protocol) server that lets Claude (or any MCP client) control a real Ubuntu machine — shell, filesystem, services, processes, packages, network, cron, and users — through a configurable safety layer instead of raw, unchecked command execution.

Every tool call is evaluated by a YAML-driven intercept engine before anything runs: it classifies risk, checks the caller's permission level, and for dangerous actions creates a pending approval that a human reviews in a web UI before it's allowed to execute.

Why

Giving an AI agent direct shell access to a server is powerful but risky — a single bad command (rm -rf, a careless sudo, an overwritten config) can take down a system with no chance to intervene. This project puts a human-in-the-loop approval gate between the AI and anything destructive, while still allowing safe, read-only operations to run instantly.

How it works

MCP Client → FastMCP → Tool Handler → Intercept Engine
                                           │
                          Rule match (YAML patterns) → Risk level (SAFE/CAUTION/DANGEROUS/BLOCKED)
                                           │
                          Permission check (read_only < operator < admin)
                                           │
                ┌──────────┬───────────────┴───────────────┐
              allow      block                          confirm
                │          │                                │
            Execute    Audit + Alert            PendingAction created (DB)
                                                 → AI polls check_pending_action
                                                 → Human approves/denies in Web UI
                                                 → Action re-executes and returns the result
  • Rules live in config/rules/default_rules.yaml and config/rules/custom_rules.yaml, hot-reloaded every 30 seconds — no restart needed to change policy.
  • Every call is audited: command, arguments, risk level, outcome, stdout/stderr, and who triggered it.
  • Alerting (Slack/PagerDuty-compatible webhook) can fire on blocked actions and on anything awaiting approval.

Tool modules

Module Example tools
shell shell_execute, shell_script
filesystem file_read, file_write, file_delete, file_move, file_copy, directory_list, file_search
process process_list, process_info, process_kill, process_tree
service service_status, service_start/stop/restart/reload, service_enable/disable, service_logs
package package_install, package_remove, package_update, package_search, package_list
network network_interfaces, network_routes, network_dns, firewall_status, firewall_add_rule, connection_list
cron cron_list, cron_add, cron_remove, cron_validate
users user_list, user_add, user_modify, user_delete, group_add/delete, sudo_rules_list
pending check_pending_action — poll for a human's approve/deny decision

Installation

1. Prerequisites (on the Ubuntu server)

# Python 3.12+ is required
python3 --version

# Install pip/venv if missing
sudo apt install python3-pip python3-venv -y

# (Optional) create a dedicated, less-privileged system user for isolated execution
sudo useradd -r -s /bin/bash mcp-runner

Passwordless sudo — the server runs commands without a TTY, so sudo can't prompt for a password. Grant passwordless sudo to whichever user will run the server (skip this if you don't need sudo commands to work):

echo 'youruser ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/youruser
sudo chmod 440 /etc/sudoers.d/youruser

Replace youruser with the actual account. Without this, any command that calls sudo fails with pam_unix(sudo:auth): auth could not identify password.

2. Clone and install

git clone <your-repo-url> /opt/mcp-server
cd /opt/mcp-server

python3 -m venv .venv
source .venv/bin/activate

pip install -e ".[dev]"

3. Configure environment

cp .env.example .env
nano .env

Minimum required changes:

# Generate a strong secret key: python3 -c "import secrets; print(secrets.token_hex(32))"
MCP_SECRET_KEY=paste-your-generated-key-here

# Optional: lock execution to a less-privileged user
MCP_EXECUTION_USER=mcp-runner

# Optional: Slack/PagerDuty webhook for blocked action alerts
MCP_ALERTING__WEBHOOK_URL=https://hooks.slack.com/services/...

Leave everything else at defaults for a first run.

4. Create the database and admin user

python scripts/create_user.py --username admin --password "your-strong-password" --role admin

5. Generate an API key for Claude

python scripts/generate_api_key.py --name "claude-code" --permission admin

The script prints the raw key once — copy it now.

6. Run the server

Foreground (test it first):

source .venv/bin/activate
python main.py
# Server is now at http://0.0.0.0:8000
# Web UI: http://YOUR_SERVER_IP:8000/ui/login

As a systemd service (recommended for production):

sudo nano /etc/systemd/system/mcp-server.service
[Unit]
Description=MCP Ubuntu Server
After=network.target

[Service]
Type=simple
User=your-user
WorkingDirectory=/opt/mcp-server
EnvironmentFile=/opt/mcp-server/.env
ExecStart=/opt/mcp-server/.venv/bin/python main.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-server
sudo systemctl status mcp-server

7. Open the firewall

# Allow the MCP port (only expose to trusted IPs in production)
sudo ufw allow 8000/tcp
sudo ufw reload

Connecting Claude

There are two ways to connect, depending on where Claude runs relative to the server.

Option A — Claude Code CLI, same machine (stdio)

Use this when Claude Code runs on the same host as the server (or over SSH). No API key needed — stdio mode grants admin automatically.

// ~/.claude/claude_code_config.json
{
  "mcpServers": {
    "ubuntu-server": {
      "command": "python",
      "args": ["/opt/mcp-server/main.py", "--stdio"]
    }
  }
}

Restart Claude Code and run /mcp to confirm tools like shell_execute and service_start show up under ubuntu-server.

Option B — Claude Desktop / remote Claude Code (SSE)

Use this when Claude runs on a different machine than the server (e.g. your laptop → a server on the same LAN).

  1. Generate an API key (see step 5 above) if you haven't already.
  2. On the machine running Claude, install the local SSE bridge: npm install -g mcp-remote (requires Node.js). This is needed because Claude Desktop's config schema doesn't support custom HTTP headers for SSE connections directly.
  3. Configure Claude Desktop (%APPDATA%\Claude\claude_desktop_config.json on Windows, ~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{
  "mcpServers": {
    "ubuntu-server": {
      "command": "mcp-remote",
      "args": [
        "http://YOUR_SERVER_IP:8000/sse-transport/sse?token=YOUR_API_KEY",
        "--allow-http"
      ]
    }
  }
}

Or, if you'd rather send the key as a header than a URL query param:

{
  "mcpServers": {
    "ubuntu-server": {
      "command": "mcp-remote",
      "args": [
        "http://YOUR_SERVER_IP:8000/sse-transport/sse",
        "--header",
        "Authorization: Bearer YOUR_API_KEY",
        "--allow-http"
      ]
    }
  }
}

Fully quit and relaunch Claude Desktop after saving the config.

For Claude Code CLI connecting to a remote server, the headers field works directly and mcp-remote isn't needed:

{
  "mcpServers": {
    "ubuntu-server": {
      "type": "sse",
      "url": "http://YOUR_SERVER_IP:8000/sse-transport/sse",
      "headers": { "Authorization": "Bearer YOUR_API_KEY" }
    }
  }
}

--allow-http is fine on a trusted LAN; put HTTPS via a reverse proxy (nginx/caddy) in front if the server is reachable from the internet.

Note: if you're driving this from a Claude Code session running on a different machine than the target server (e.g. a Windows/Mac dev box controlling a remote Linux box), Claude Code's own local tools (like its Bash tool) run on your machine — they can't see the remote server. Only the shell_execute MCP tool actually runs commands on the target server, via the intercept engine there.

Using the server

Talking to Claude

Once connected, just talk to Claude naturally:

List running services
Show disk usage
Restart nginx
Install htop
Read /etc/nginx/nginx.conf
Show the last 50 lines of /var/log/syslog

How the intercept engine responds

Every command is evaluated against the rule set before execution:

Outcome What Claude sees What you do
Allowed Result returned immediately Nothing — it ran
Pending "Action is pending approval (ID: abc123)" Approve in the Web UI
Blocked "Blocked: [reason]" Cannot be approved — rule must be changed

Claude polls check_pending_action automatically while waiting for your approval.

Approving pending actions (Web UI)

  1. Open http://YOUR_SERVER_IP:8000/ui/login
  2. Log in with the admin credentials created during installation
  3. Go to Pending Actions
  4. Review the command and click Approve or Deny
  5. Claude receives the result within seconds

Viewing audit history

Go to History in the web UI to see every tool call — what ran, who called it, the risk level assigned, the outcome, and the full stdout/stderr.

Permission model

Role Can do
read_only View audit logs and pending actions only
operator Run SAFE/CAUTION tools directly; DANGEROUS actions require UI approval
admin Full access, including rule management

In --stdio mode the server assumes it's embedded in a trusted local Claude Code session and grants admin with no auth.

Customizing the rules

Rules live in config/rules/custom_rules.yaml. This file is hot-reloaded every 30 seconds — no server restart needed. Custom rules with the same id as a default rule override it; lower priority numbers run first.

Example — always block a specific path from deletion:

rules:
  - id: block_delete_var_www
    name: "Protect /var/www from deletion"
    enabled: true
    tool: "file_delete"
    conditions:
      pattern: "/var/www"
    risk: blocked
    action: block
    reason: "Web root is protected."
    alert: true
    priority: 10

Example — allow a normally-dangerous command without approval:

rules:
  - id: allow_restart_nginx
    name: "Allow nginx restart without approval"
    enabled: true
    tool: "service_restart"
    conditions:
      pattern: "nginx"
    risk: caution
    action: allow
    priority: 5

Development

pytest

Set MCP_INTERCEPT__DRY_RUN_MODE=true to log every tool call through the intercept engine without actually executing anything — useful for testing rule coverage.

Quick reference

Task Command / URL
Start server python main.py
Run as stdio python main.py --stdio
Create web user python scripts/create_user.py --username X --password Y --role admin
Generate API key python scripts/generate_api_key.py --name X --permission admin
Web UI http://SERVER:8000/ui/login
Health check http://SERVER:8000/api/health
View logs (systemd) journalctl -u mcp-server -f
Reload rules Edit config/rules/custom_rules.yaml (auto-reloads in 30s)
Dry-run mode Set MCP_INTERCEPT__DRY_RUN_MODE=true in .env
Install mcp-remote npm install -g mcp-remote (needed for Claude Desktop)

Architecture reference

Open architecture.html in a browser — it renders four Mermaid diagrams via the Mermaid CDN (requires internet):

  1. Overall Architecture — the two connection modes side by side: stdio (local Claude Code CLI, direct admin access, no auth) vs. HTTP/SSE (remote Claude, Bearer token → auth middleware → MCP tools).
  2. Intercept Engine Decision Flow — the exact branching logic inside engine.process(): rule match → risk classification → permission check → allow/block/confirm.
  3. Human-in-the-Loop Sequence — the full numbered sequence for a DANGEROUS action, from pending_id through UI approval to re-execution.
  4. Permission × Risk Matrix — which role can execute each risk level.

Status

This is a personal project, not a hardened security product. Review the default rules, run it in dry-run mode first, and don't expose it beyond a trusted network without HTTPS in front of it.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages