Ops MCP Server
by twine003
README.md
# Ops MCP Server
A self-hosted [MCP](https://modelcontextprotocol.io) server that gives an AI
assistant (Claude Code, or any MCP-compatible client) live diagnostic and
deployment access to a Windows or Linux machine over HTTP: shell/PowerShell
commands, file exploration, git and Django operations, process/environment
info, and IIS (Windows) or systemd (Linux) service control.
Two parallel implementations, same tool set otherwise:
| | Server | Installer |
|---|---|---|
| Windows | `server_new.py` | `install_service.ps1` (NSSM service, or a scheduled task if NSSM isn't installed) |
| Linux | `server_linux.py` | `install_linux.sh` (systemd service, auto-creates its own venv) |
Both installers are **self-healing** — safe to re-run any time on an
already-installed machine. Re-running one:
- kills a stuck process still holding the port,
- syntax-checks the server script before touching the service (catches a
corrupted/interrupted deploy with a clear error instead of installing
something that crash-loops),
- re-registers the service/task with current settings,
- picks up a changed API key without a reboot,
- and at the end prints the **current API key**, the **reachable LAN IPs**,
and the **live tool list** (queried from the server itself, not a
hardcoded doc) — so re-running with no arguments is also how you find out
what's already configured.
On a Windows PC, `server_new.py --desktop` also serves the **desktop
"computer use" tools** of [`desktop/`](desktop/) (screenshots, UI Automation,
clicks and typing on the interactive desktop) — one server, one port, one key.
See [Desktop computer-use server](#desktop-computer-use-server-windows) below.
## Quick start
### Windows
```powershell
# Copy this folder to a local disk first (e.g. C:\ops-mcp-server) - do not
# run it straight from a mapped network drive, see Troubleshooting below.
powershell -ExecutionPolicy Bypass -File install_service.ps1 -Port 8001 -ApiKey "a-long-random-key"
```
### Linux
```bash
sudo ./install_linux.sh --port 8001 --api-key "a-long-random-key"
```
Omit `-ApiKey`/`--api-key` and the installer will reuse whatever key is
already configured, or generate a new one and print it.
### Connect from Claude Code
Add to `.mcp.json`:
```json
{
"mcpServers": {
"ops-mcp-remote": {
"type": "streamable-http",
"url": "http://<machine-ip>:8001/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
```
## Available tools
**Git** — `git_status`, `git_pull`, `git_log`, `git_branch`
**Django** — `django_collectstatic`, `django_migrate`, `django_check`, `django_manage`
**Web service** — Windows: `iis_status`, `iis_restart`, `iis_stop`, `iis_start`
· Linux: `service_status`, `service_restart`, `service_stop`, `service_start`
(systemd unit name set via `WEB_SERVICE_NAME`)
**Deployment** — `pip_install`, `full_deploy` (git pull + pip install +
collectstatic + migrate + restart web service)
**File system** — `ls`, `cat`, `head`, `tail`, `find`, `grep`, `pwd`, `cd`, `file_info`
**System** — `server_info`, `env`, `ps`, `shell`
Django and git tools are optional conveniences — the file system and system
tools work standalone on any machine, with no Django project required.
## Security
**This server has no authentication unless you configure an API key** — do
that before exposing it beyond an isolated lab machine. `shell` is
unrestricted command execution; treat the API key like a root password.
- Both installers generate/print a key when none is configured. Pass one
explicitly with `-ApiKey`/`--api-key`, or set it later:
- Windows: `[Environment]::SetEnvironmentVariable("MCP_API_KEY", "...", "Machine")`, then re-run the installer so the service picks it up.
- Linux: edit `MCP_API_KEY=` in `/etc/ops-mcp/mcp.env`, then `systemctl restart ops-mcp`.
- The installers open the firewall for the port you choose (`New-NetFirewallRule` / `ufw`/`firewalld`) — nothing else.
- `shell()`/`django_manage()` block an explicit list of destructive command
patterns (`rm -rf /`, `shutdown`, `flush`, ...) as a safety net, **not** a
security boundary — anyone with the API key can still run arbitrary code.
Scope network access accordingly (VPN/LAN-only, not the open internet).
## Configuration
Environment variables:
| Variable | Default | Description |
|---|---|---|
| `MCP_HOST` | `0.0.0.0` | Host to bind to |
| `MCP_PORT` | `8001` | Port to listen on |
| `MCP_API_KEY` | *(empty)* | Bearer token for authentication |
| `IIS_APP_POOL` | *(empty)* | Windows only — required for `iis_*` tools |
| `IIS_SITE_NAME` | *(empty)* | Windows only — required for `iis_*` tools |
| `WEB_SERVICE_NAME` | *(empty)* | Linux only — systemd unit for `service_*` tools |
## Troubleshooting
**"Impossible de charger... l'execution de scripts est desactivee" /
"running scripts is disabled on this system"** (PSSecurityException) when
running `install_service.ps1`
This is Windows' default PowerShell execution policy (`Restricted`), not a
problem with the installer:
```powershell
powershell -ExecutionPolicy Bypass -File install_service.ps1
```
**Service installs but never starts, right after installing from a mapped
drive or `\\server\share` path**
The Windows service (or scheduled task) runs as `SYSTEM`, which cannot see
drives mapped in *your* logged-in session (`N:\`, `W:\`, ...) or shares that
need your user credentials. Copy this folder to a local path first (e.g.
`C:\ops-mcp-server`) and run the installer from there. (Linux has no
equivalent problem — `systemd` services can read whatever's mounted at the
OS level, e.g. NFS/CIFS in `/etc/fstab`.)
**"Python est introuvable..." / pip not recognized, even though `python`
seemingly "runs"**
That's Windows' App Execution Alias stub in `WindowsApps`, not a real
Python — it only opens the Microsoft Store. `install_service.ps1` looks for
a real interpreter on disk (ignoring `PATH`) and silently installs one from
python.org if none is found; just re-run it. This only bites you if you run
`start_server.bat` directly without having run the installer first, or if
the machine has no internet access for the download.
**Server won't start**
- Check if the port is already in use: `netstat -an | findstr 8001` (Windows) / `ss -ltnp | grep 8001` (Linux)
- Check the firewall isn't blocking it
- Windows: `nssm status OpsMCP` or `Get-ScheduledTaskInfo -TaskName OpsMCP`
- Linux: `systemctl status ops-mcp` and `journalctl -u ops-mcp -n 50 --no-pager`
**IIS/systemd commands fail**
- Windows: run the MCP server as Administrator, or grant the account IIS management rights
- Linux: the systemd unit runs as `root` by default (edit `install_linux.sh`'s generated unit if you want a lower-privilege user)
**Git commands fail**
- Ensure `git` is on `PATH`, or set the full path in `Config.GIT_PATH`
## Desktop computer-use server (Windows)
`server_new.py --desktop` mounts the tools of `desktop/` into the same
server. They give an AI assistant control of **this PC's interactive desktop**: multi-monitor
screenshots (with change detection), the UI Automation / Win32 control tree,
reading values, clicking, typing, dragging, scrolling and moving windows,
designed so you can **watch it work without your mouse and its actions
fighting each other**.
`--desktop` only works inside a user's session: a service runs in session
0, which has no access to the interactive desktop (the server refuses to start
there). So on a PC it runs as a **scheduled task at logon, inside your own
session**. Without `--desktop` (the service on a server) nothing changes. With
`--desktop` the server-only deployment tools (`full_deploy`, `git_pull`,
`django_migrate`, `django_collectstatic`, `pip_install`,
`iis_start/stop/restart`) are not offered: on a PC they would touch the local
checkout or database by mistake. `desktop/desktop_server.py` is no longer a
server of its own: run directly (an old task), it starts `server_new.py --desktop`.
**Agents must read [`desktop/AGENT_GUIDE.md`](desktop/AGENT_GUIDE.md)**: a
summary goes out in the server's connection instructions and the `usage_guide()`
tool returns the full text (edit the file, no restart needed).
### Install (normal PowerShell, no admin needed)
```powershell
powershell -ExecutionPolicy Bypass -File install_service.ps1 -Desktop
```
The installer (self-healing, safe to re-run):
- creates/repairs `desktop\.venv` (needs Python >= 3.11 already installed;
nothing is installed system-wide) and installs `requirements-desktop.txt`;
- compiles the server and runs its unit tests before touching anything;
- generates (or keeps) an API key in your **user** environment variable
`DESKTOP_MCP_API_KEY`;
- registers the scheduled task `Desktop-MCP` (`server_new.py --desktop`, at
logon, your user, no stored password), starts it and checks `/health`;
- prints the key, the port, the live tool list and the command to register it
in Claude Code (it does not run it):
```powershell
claude mcp add-json --scope project desktop-mcp '{"type":"http","url":"http://127.0.0.1:8011/mcp","headersHelper":"powershell -NoProfile -ExecutionPolicy Bypass -File C:/path/to/ops-mcp-server/desktop/mcp_headers.ps1"}'
```
`headersHelper` runs `desktop/mcp_headers.ps1` on every connection: it reads
the key from the user registry, so the key is never written into `.mcp.json`
and a Claude session started before the variable existed still gets it
(a `${DESKTOP_MCP_API_KEY}` header would expand to empty, giving HTTP 401).
Options: `-DesktopPort 8011` (default), `-DesktopListenLan` (listen on the
LAN: needs an elevated shell for the firewall rule, and the server refuses to
listen outside 127.0.0.1 without an API key), `-DesktopRemove` (unregister).
By default it only listens on **127.0.0.1**.
### One cursor, three mechanisms
Windows has a single cursor per session, so every action uses the first
mechanism that works and **reports which one** (`mechanism`) and whether the
real cursor moved (`cursor_moved`):
1. **UI Automation** (Invoke, Value, SelectionItem, Toggle, ExpandCollapse,
Text, LegacyIAccessible): no cursor movement, no focus stealing;
2. **Win32 messages** to the window (BM_CLICK, WM_SETTEXT, EM_REPLACESEL,
WM_CHAR, WM_KEYDOWN/UP, CB_SETCURSEL, client-coordinate mouse messages):
no cursor movement;
3. **SendInput** (drawing, dragging, apps that ignore 1 and 2): moves the
real cursor, which is saved and restored immediately.
Your mouse is never blocked by default. Optionally
(`set_option(freeze_user_mouse=true)`, or `input.freeze_user_mouse_during_gestures`
in the config) your physical mouse is frozen only during a SendInput gesture.
### How to stop it
- The on-screen **Stop** button (top centre of the active monitor; only a
physical click counts, and it never appears in screenshots);
- a message to the assistant in the chat;
- the hotkey **Ctrl+Alt+Shift+F12** (`stop_hotkey`);
- the `pause()` / `resume()` / `status()` tools.
Moving your mouse or typing does **not** stop or pause the assistant.
### What you see
An orange "Claude" ghost cursor animates to each target before acting, and a
soft border marks the monitor where it is working (grey while paused). Both
are **mandatory**: every desktop tool call shows them and keeps them up while
it runs, and they fade together a few seconds after the last call; they cannot
be hidden (`hide` is refused, `ghost.enabled` / `border.enabled` are ignored).
`/health` reports `desktop`, `overlay_state` and `ghost_visible` without
lighting them. All
overlay windows are click-through, never take focus and are excluded from
every screen capture (`WDA_EXCLUDEFROMCAPTURE`). Colours and sizes: the
`screen_border` tool or the `border` / `ghost` sections of the config.
**Watching through RustDesk, AnyDesk or TeamViewer?** Those tools stream a
screen capture, so by default they never show the cursor, the border, the Stop
button or the bubbles. Set `"remote_visible": true` in
`%LOCALAPPDATA%\desktop-mcp\config.json` and restart the `Desktop-MCP` task:
everything stays visible remotely (and also appears in Claude's own
screenshots, which then skip drawing the cursor on top).
### Comment bubbles
`callout(text, ...)` shows a bubble next to what the assistant is pointing at
(a rectangle, a point or a UI element, outlined while the bubble is open), for
guided tours, explanations and questions. It has an **OK** button and, with
`ask=True`, a text field for your answer; `wait=True` returns
`accepted` / `answered` (+ `answer`) / `dismissed` / `replaced` / `stopped`, or
`pending` after `timeout` (then `callout_wait(id)` or `callout_close(id)`).
Only your physical click and keys count: the bubble's window is always denied
by the safety policy, so the assistant can neither read nor click it.
### Safety and configuration
- **Password fields are never typed into or read.**
- `deny_processes` (password managers, UAC, lock screen...) and
`deny_window_title_regex` are never controlled, not even for reading;
add your own (e.g. a browser window with a banking session).
- `allow_write_processes`: `"*"` = any app not denied; list exe names to make
everything else read-only.
- Every action is appended to an audit log
(`%LOCALAPPDATA%\desktop-mcp\audit.jsonl`).
- Defaults are in `desktop/config.json`; put your own overrides in
`%LOCALAPPDATA%\desktop-mcp\config.json` (same keys, merged). No secrets in
either file.
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues