Skip to main content
Glama
linuskang

nginx-proxy-manager-mcp

by linuskang
README.md
# nginx-proxy-manager-mcp

[![License: CC BY-NC 4.0](https://img.shields.io/badge/License-CC%20BY--NC%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc/4.0/)
[![Node](https://img.shields.io/badge/node-%3E%3D18-green.svg)](https://nodejs.org)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue.svg)](https://www.typescriptlang.org/)
[![MCP](https://img.shields.io/badge/MCP-compatible-purple.svg)](https://modelcontextprotocol.io)

A [Model Context Protocol](https://modelcontextprotocol.io) server that exposes
the [Nginx Proxy Manager](https://nginxproxymanager.com) (NPM) REST API to AI
agents. Let assistants like **Claude Desktop**, **Hermes**, **Cursor**, or any
MCP-compatible client create reverse proxies, TCP/UDP streams, redirects, 404
hosts, and request/renew Let's Encrypt certificates for you — in plain English.

> "Hey Hermes, point `grafana.example.com` at `192.168.1.50:3000` and get a Let's Encrypt cert for it."
> → `npm_create_proxy_host` + `npm_create_certificate` + `npm_update_proxy_host`

## Features

- **Proxy hosts** — create / list / update / delete / enable / disable reverse proxies
- **Streams** — TCP & UDP port forwarding
- **Redirection hosts** — domain → domain redirects (301/302)
- **Dead (404) hosts** — return 404 for parked/unused domains
- **Certificates** — request Let's Encrypt (HTTP-01 or DNS-01), renew, delete, download
- **Users & audit log** — inspect users and recent actions
- **Raw passthrough** — `npm_raw_request` for anything not covered
- **30 tools** total, all typed with JSON schemas for reliable agent use
- **Two transports** — `stdio` (default, for local agents) and Streamable `http`
- **No runtime deps besides the official MCP SDK + zod**

## Quick start

### 1. Install

```bash
git clone https://github.com/linuskang/nginx-proxy-manager-mcp.git
cd nginx-proxy-manager-mcp
npm install
npm run build
```

Or run directly without cloning (npx once published):

```bash
npx nginx-proxy-manager-mcp
```

### 2. Configure

Copy `.env.example` → `.env` and fill in your NPM admin credentials:

```bash
cp .env.example .env
```

| Variable         | Required | Description                                                     |
| ---------------- | -------- | --------------------------------------------------------------- |
| `NPM_BASE_URL`   | ✅       | Base URL of NPM, e.g. `http://npm.local:81` (no trailing slash) |
| `NPM_EMAIL`      | ✅\*     | Admin email (`NPM_TOKEN` alternative)                           |
| `NPM_PASSWORD`   | ✅\*     | Admin password (`NPM_TOKEN` alternative)                        |
| `NPM_TOKEN`      | ✅\*     | Pre-existing JWT token (takes precedence over email/password)   |
| `NPM_TIMEOUT_MS` | ❌       | Request timeout, default `30000`                                |
| `NPM_DEBUG`      | ❌       | Verbose stderr logging, default `false`                         |
| `MCP_TRANSPORT`  | ❌       | `stdio` (default) or `http`                                     |
| `MCP_PORT`       | ❌       | HTTP port, default `3000`                                       |
| `MCP_HOST`       | ❌       | HTTP bind address, default `0.0.0.0`                            |
| `MCP_ENDPOINT`   | ❌       | HTTP endpoint path, default `/mcp`                              |

\* Provide **either** `NPM_TOKEN` **or both** `NPM_EMAIL` + `NPM_PASSWORD`.

### 3. Wire it up to your agent

#### Claude Desktop (or Claude Code)

Edit `claude_desktop_config.json` (macOS:
`~/Library/Application Support/Claude/claude_desktop_config.json`):

```jsonc
{
  "mcpServers": {
    "nginx-proxy-manager": {
      "command": "node",
      "args": ["D:/Code/Git/nginx-proxy-manager-mcp/dist/index.js"],
      "env": {
        "NPM_BASE_URL": "http://npm.local:81",
        "NPM_EMAIL": "[email protected]",
        "NPM_PASSWORD": "your-password",
      },
    },
  },
}
```

#### Hermes

In your Hermes agent config (or MCP registry), add a server entry pointing at the
same command/env above. Hermes speaks standard MCP over stdio, so no special
setup is needed — just point it at `node dist/index.js` with your `NPM_*` env.

#### Remote / HTTP transport (browser agents, remote runners)

```bash
MCP_TRANSPORT=http MCP_PORT=3000 MCP_HOST=0.0.0.0 node dist/index.js
# POST your JSON-RPC messages to http://localhost:3000/mcp
```

## Available tools (30)

### Proxy hosts

| Tool                     | Description                                |
| ------------------------ | ------------------------------------------ |
| `npm_list_proxy_hosts`   | List all reverse proxies                   |
| `npm_get_proxy_host`     | Get one by id                              |
| `npm_create_proxy_host`  | Create a reverse proxy (the headline tool) |
| `npm_update_proxy_host`  | Update fields on an existing proxy         |
| `npm_delete_proxy_host`  | Delete by id                               |
| `npm_enable_proxy_host`  | Enable a disabled proxy                    |
| `npm_disable_proxy_host` | Disable a proxy (without deleting)         |

### Streams (TCP/UDP port forwarding)

| Tool                | Description                   |
| ------------------- | ----------------------------- |
| `npm_list_streams`  | List all streams              |
| `npm_get_stream`    | Get one by id                 |
| `npm_create_stream` | Create a TCP/UDP port forward |
| `npm_update_stream` | Update a stream               |
| `npm_delete_stream` | Delete a stream               |

### Redirection hosts

| Tool                          | Description                 |
| ----------------------------- | --------------------------- |
| `npm_list_redirection_hosts`  | List redirections           |
| `npm_create_redirection_host` | Create a redirect (301/302) |
| `npm_update_redirection_host` | Update a redirect           |
| `npm_delete_redirection_host` | Delete a redirect           |

### Dead (404) hosts

| Tool                   | Description       |
| ---------------------- | ----------------- |
| `npm_list_dead_hosts`  | List 404 hosts    |
| `npm_create_dead_host` | Create a 404 host |
| `npm_update_dead_host` | Update a 404 host |
| `npm_delete_dead_host` | Delete a 404 host |

### Certificates (Let's Encrypt & custom)

| Tool                       | Description                                          |
| -------------------------- | ---------------------------------------------------- |
| `npm_list_certificates`    | List all certificates                                |
| `npm_create_certificate`   | Request a new Let's Encrypt cert (HTTP-01 or DNS-01) |
| `npm_renew_certificate`    | Renew an existing cert                               |
| `npm_delete_certificate`   | Delete a certificate                                 |
| `npm_download_certificate` | Download cert / private key / chain (PEM)            |

### Users, audit & power tools

| Tool                 | Description                                           |
| -------------------- | ----------------------------------------------------- |
| `npm_ping`           | Health check the NPM instance                         |
| `npm_list_users`     | List NPM users                                        |
| `npm_get_user`       | Get a user by id                                      |
| `npm_list_audit_log` | Browse recent audit log entries                       |
| `npm_raw_request`    | Authenticated escape hatch to any `/api/...` endpoint |

## Example: create a proxy host

```jsonc
// npm_create_proxy_host
{
  "domain_names": ["grafana.example.com"],
  "forward_scheme": "http",
  "forward_host": "192.168.1.50",
  "forward_port": 3000,
  "block_exploits": true,
  "allow_websocket_upgrade": true,
  "certificate_id": 0,
  "ssl_forced": false,
  "enabled": true,
}
```

A natural-language request like _"Proxy `grafana.example.com` → `192.168.1.50:3000`
with a Let's Encrypt cert"_ will typically run `npm_create_proxy_host`, then
`npm_create_certificate` (provider `letsencrypt`), then
`npm_update_proxy_host` with the returned `certificate_id` and `ssl_forced: true`.

## Deploying with Docker

A prebuilt multi-arch image (`linux/amd64` + `linux/arm64`) is published to the
GitHub Container Registry on every push to `main`/`master` and on version tags:

```bash
docker pull ghcr.io/linuskang/nginx-proxy-manager-mcp:latest
```

Image tags: `latest`, `:vX.Y.Z`, `:X.Y`, `:X`, `:sha-<short>`, `:master`.

> **Which transport?** Use **stdio** when the agent runs on the same machine
> (Claude Desktop, Hermes, Cursor) and you launch the container as a child
> process. Use **http** when a remote agent or browser client needs to POST
> JSON-RPC over the network.

### Option A — stdio (local agent on the same machine)

Your agent config launches the container and speaks MCP over its stdin/stdout.
Point the agent's `command` at `docker` and pass `run -i --rm` plus the env vars:

```jsonc
// Claude Desktop / Hermes MCP server config
{
  "mcpServers": {
    "nginx-proxy-manager": {
      "command": "docker",
      "args": [
        "run",
        "-i",
        "--rm",
        "-e",
        "NPM_BASE_URL",
        "-e",
        "NPM_EMAIL",
        "-e",
        "NPM_PASSWORD",
        "ghcr.io/linuskang/nginx-proxy-manager-mcp:latest",
      ],
      "env": {
        "NPM_BASE_URL": "http://your-npm-host:81",
        "NPM_EMAIL": "[email protected]",
        "NPM_PASSWORD": "your-password",
      },
    },
  },
}
```

`-i` keeps stdin open so the agent can talk to the server; `--rm` cleans up the
container when the agent exits. No port is published because stdio doesn't need
one. Transport defaults to `stdio`, so you don't have to set `MCP_TRANSPORT`.

### Option B — HTTP (remote / networked agents)

Expose a Streamable HTTP endpoint that any MCP client can POST to:

```bash
docker run -d --name npm-mcp \
  -p 3000:3000 \
  -e MCP_TRANSPORT=http \
  -e MCP_PORT=3000 \
  -e MCP_HOST=0.0.0.0 \
  -e NPM_BASE_URL=http://your-npm-host:81 \
  -e [email protected] \
  -e NPM_PASSWORD=your-password \
  ghcr.io/linuskang/nginx-proxy-manager-mcp:latest
```

MCP endpoint: `http://localhost:3000/mcp`

Verify it's up:

```bash
curl -s http://localhost:3000/mcp -X POST \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
```

You should get a JSON-RPC response with `serverInfo.name == "nginx-proxy-manager-mcp"`.

Point your agent at the URL and it will `initialize`, call `tools/list`, and
start driving your NPM instance.

### Option C — docker compose

For a persistent HTTP deployment, use the bundled `docker-compose.yml`:

```bash
cp .env.example .env
# edit .env: set NPM_BASE_URL, NPM_EMAIL, NPM_PASSWORD,
#           MCP_TRANSPORT=http, MCP_PORT=3000, MCP_HOST=0.0.0.0
docker compose up -d
```

Logs:

```bash
docker compose logs -f
```

Stop / remove:

```bash
docker compose down
```

### Putting NPM and the MCP server on the same Docker network (recommended)

If Nginx Proxy Manager itself runs in Docker, put the MCP container on the same
network so you can address NPM by service name — no published ports needed:

```bash
docker network create npm-net
docker network connect npm-mcp npm-net        # if MCP created its own net
docker network connect <npm-container> npm-net  # for the NPM container
```

Then set `NPM_BASE_URL=http://<npm-service-name>:81` (e.g. `http://npm:81`) and
the MCP server talks to NPM over the private bridge.

### Build locally (optional, same Dockerfile used by CI)

```bash
docker build -t nginx-proxy-manager-mcp .
# stdio:
docker run -i --rm \
  -e NPM_BASE_URL=http://npm:81 -e NPM_EMAIL=a@b.c -e NPM_PASSWORD=secret \
  nginx-proxy-manager-mcp
# http:
docker run -d -p 3000:3000 \
  -e MCP_TRANSPORT=http -e MCP_PORT=3000 -e MCP_HOST=0.0.0.0 \
  -e NPM_BASE_URL=http://npm:81 -e NPM_EMAIL=a@b.c -e NPM_PASSWORD=secret \
  nginx-proxy-manager-mcp
```

### Troubleshooting

- **`npm:54` / connection refused on port 81** — `NPM_BASE_URL` must point at the
  **admin API** port (81), not 80/443. If NPM is in another container, use the
  service name and a shared Docker network.
- **`NPM_TOKEN or both NPM_EMAIL and NPM_PASSWORD must be set`** — provide
  either a JWT (`NPM_TOKEN`) **or** an email + password pair.
- **Tools call returns HTTP 401** — your token expired or the credentials are
  wrong. Remove `NPM_TOKEN` to fall back to email/password auto-refresh, or
  regenerate the token from the NPM UI.
- **Want to debug requests?** Set `-e NPM_DEBUG=true` to log every request URL
  to stderr (`docker logs -f npm-mcp`).

## Development

```bash
npm install         # install deps
npm run dev         # run from source (tsx)
npm run build       # compile to dist/
npm run typecheck   # typecheck only
npm run lint        # eslint
npm run format      # prettier write
npm run check       # lint + typecheck + format:check
```

Project layout:

```
src/
  config.ts        # env-based configuration
  types.ts         # NPM API TypeScript types
  client.ts        # NPM REST client (auth, retry, JSON)
  index.ts         # MCP server + stdio/http transports
  tools/
    _shared.ts     # tool helpers (ok/fail/guard)
    proxy-hosts.ts
    streams.ts
    redirection-hosts.ts
    dead-hosts.ts
    certificates.ts
    misc.ts        # ping, users, audit, raw request
    index.ts       # aggregator
```

## Compatibility

- Nginx Proxy Manager **2.x** (REST API on port 81)
- Node.js **≥ 18** (uses global `fetch`, `AbortSignal.timeout`)
- MCP protocol version **2024-11-05** and later

## Security notes

- Credentials are passed via environment variables only — never logged unless
  `NPM_DEBUG` is on (and even then, only request URLs, never the password).
- The exposed tools use your NPM admin token, so they can do anything an admin
  can. Run this server in a trusted environment and restrict network exposure.
- Prefer scoped NPM users if your deployment supports it.

## Contributing

PRs welcome! Please open an issue first for larger changes. Run `npm run check`
before submitting. See [`CONTRIBUTING.md`](CONTRIBUTING.md).

## License

Licensed under [Creative Commons Attribution-NonCommercial 4.0 International
(CC BY-NC 4.0)](https://creativecommons.org/licenses/by-nc/4.0/) — see
[`LICENSE`](LICENSE). You are free to share and adapt for **non-commercial**
purposes with attribution. Commercial use requires a separate license from the
author.

TDQS

B3.2/5.0

Scored across 30 tools

Disambiguation5/5

Every tool targets a specific resource and action (e.g., proxy hosts, streams, certificates), and even similar host types (redirection vs dead) have distinct purposes. The descriptions clearly separate list/get/create/update/delete/control operations, so an agent can reliably select the intended tool.

Naming Consistency4/5

The vast majority of tools follow a consistent verb_noun pattern (e.g., npm_list_proxy_hosts, npm_create_certificate). However, npm_ping and npm_raw_request deviate from this pattern, and the mix of plural list names with singular get names is a minor inconsistency.

Tool Count2/5

With 30 tools, the server clearly exceeds the 25+ threshold for 'too many'. While each tool covers a distinct operation, the sheer number makes the surface area heavy and potentially overwhelming for agents, even though the underlying domain (NPM) is broad.

Completeness4/5

The set provides comprehensive CRUD coverage for proxy hosts, streams, redirection hosts, and dead hosts, plus certificate management. Minor gaps exist: no get-by-id for redirection/dead hosts, no user write operations, and no certificate get-by-id; the npm_raw_request escape hatch helps work around these gaps.

Maintenance

ActivityStale
ResponsivenessNo issues