npm-mcp

npm-mcp

Enables conversational management of Nginx Proxy Manager, including reverse-proxy routing, TLS certificates, access lists, and stream forwards, with built-in safety guardrails for production use.

Category
Visit Server

README

<div align="center">

npm-mcp

Model Context Protocol server for Nginx Proxy Manager

Manage reverse-proxy routing, TLS certificates, access lists, and stream forwards conversationally — with guardrails that assume you will eventually point it at production.

Python FastMCP NPM Tests Tools Ruff

</div>


Contents


Why this exists

Nginx Proxy Manager has a full REST API and no MCP server. This is that server — but the interesting part isn't the plumbing, it's the constraints.

A reverse proxy is a single point of failure for everything behind it. An agent with write access to one can take down services it was never asked to touch. So the design starts from that:

Tools are generated from the API's own OpenAPI document, not hand-written. The document is pinned in-tree, and a drift test fails CI if the upstream surface changes — instead of tools silently 404-ing at runtime.

Every result crosses one redaction boundary that fails closed. It raises on anything it can't inspect rather than passing it through.

Guardrails are mutation-tested. Every safety control has a test proven to go red when the control is disabled.


How it works

flowchart LR
    C["MCP Client"] -->|"Bearer (optional)"| S

    subgraph S["npm-mcp"]
        direction TB
        A["Bearer verifier<br/><i>hmac.compare_digest</i>"] --> G["Guardrails<br/><i>S1 · S2 · S6 · S7 · S8</i>"]
        G --> T["66 generated tools"]
        T --> R["serialize_result()<br/><i>redact + cap</i>"]
    end

    S -->|"JWT, auto-refreshed"| N["Nginx Proxy Manager"]
    P["npm-openapi.json<br/><i>pinned, in-package</i>"] -.->|generates| T

Tool signatures are built from the pinned document at import time, so create_proxy_host exposes 18 typed arguments with real enums — not an opaque **kwargs passthrough.


Quick start

uv sync
cp .env.example .env    # then fill in NPM_URL / NPM_IDENTITY / NPM_SECRET
uv run npm-mcp

<details> <summary><b>Claude Desktop / stdio</b></summary>

{
  "mcpServers": {
    "npm": {
      "command": "uv",
      "args": ["run", "npm-mcp"],
      "env": {
        "NPM_URL": "https://nginx-proxy-manager.example.net",
        "NPM_IDENTITY": "npm-mcp@example.net",
        "NPM_SECRET": "…",
        "NPM_MCP_TRANSPORT": "stdio"
      }
    }
  }
}

</details>

<details> <summary><b>Streamable HTTP</b> — note the <code>/mcp</code> path</summary>

{
  "mcpServers": {
    "npm": {
      "type": "http",
      "url": "https://npm-mcp.example.net/mcp",
      "headers": { "Authorization": "Bearer <NPM_MCP_BEARER_TOKEN>" }
    }
  }
}

FastMCP serves at /mcp. A trailing slash 307-redirects, which some clients mishandle — don't let a proxy rewrite the path.

</details>

[!TIP] Call get_guidance first. It reports response shapes, the disable-vs-delete distinction, which latches are currently open, and the active protected-domain list.


Authentication

Two layers, easy to conflate:

Direction Mechanism
Inbound client → npm-mcp Optional Authorization: Bearer … via NPM_MCP_BEARER_TOKEN, compared with hmac.compare_digest. Unset ⇒ no authentication at all.
Outbound npm-mcp → NPM Account credentials → short-lived JWT, refreshed automatically. Callers never see or supply it.

NPM issues no long-lived API keys, which is why the server holds credentials rather than accepting a token.

[!IMPORTANT] POST /tokens has two possible responses: a token, or a 2FA challenge. If the account has 2FA enabled, set NPM_TOTP_SECRET — otherwise the server fails at startup, naming both remedies, rather than coming up healthy and breaking on the first tool call.


Tool catalog

66 tools = 65 API operations + get_guidance.

Family # Representative tools
🔀 Proxy hosts 7 get_proxy_hosts · create_proxy_host · update_proxy_host · delete_proxy_host · enable_proxy_host · disable_proxy_host
↪️ Redirection hosts 7 *_redirection_host
🚫 404 hosts 7 create_404_host · *_dead_host
🔌 Streams 7 *_stream
🔐 Access lists 5 get_access_lists · create_access_list · update_access_list · delete_access_list
📜 Certificates 10 get_certificates · create_certificate · renew_certificate · upload_certificate · validate_certificates · download_certificate · test_http_reach · get_dns_providers
👤 Users 8 get_users · create_user · update_user · update_user_auth · update_user_permissions · login_as_user
🔑 User 2FA 5 setup_user_2fa · enable_user_2fa · disable_user_2fa · get_user_2fa_status · regen_user_2fa_codes
⚙️ Settings 3 get_settings · update_setting
📋 Audit log 2 get_audit_logs · get_audit_log
ℹ️ Meta 4 health · check_version · reports_hosts · schema
🧭 Guidance 1 get_guidance

Names derive from the OpenAPI operationId, so list operations are get_*, not list_*.

[!WARNING] Three operations are deliberately not exposed: requestToken, refreshToken, loginWith2FA. They're the server's own auth plumbing, and requestToken accepts an arbitrary identity and secret — registering it would turn this server into a credential-testing oracle against NPM, with every attempt attributed to the service account.

<details> <summary><b>Two API quirks worth knowing</b></summary>

  • No pagination exists. Not one endpoint accepts limit/offset. Tools accept them and slice client-side; the tool descriptions say so.
  • expand is a per-endpoint enum, not a passthrough — proxy-hosts takes access_list,owner,certificate; certificates take only owner. Out-of-enum values are rejected before the request is sent.

</details>


Safety model

[!CAUTION] Writes are enabled by default. This server can rewrite the routing table for every service behind the proxy. Set NPM_READ_ONLY=1 to disable all mutations.

Control Override
NPM_READ_ONLY Rejects every mutating tool, checked before any guardrail read
S1 Refuses delete / disable / update on protected hosts, and on the certificates and access lists those hosts depend on NPM_ALLOW_SELF_MUTATION
S2 Every DELETE requires confirm: true; without it the tool returns what would be affected and writes nothing per-call
S5 Every mutation emits one audit line; NPM's own audit log is queryable
S6 Every mutating operation under /users or /settings is latched NPM_ALLOW_ACCOUNT_MUTATION
S7 Refuses to modify, disable, delete, or login_as its own account none
S8 download_certificate returns TLS private keys, so it's latched NPM_ALLOW_CERT_EXPORT

<details> <summary><b>Why these specific shapes</b> — each closes a bypass found in review</summary>

  • S1 matches ANY protected domain, not ALL. ALL would let the guardrail be disarmed through the tools it guards: add one unrelated domain to a host and protection evaporates.
  • S1 covers update, not just delete/disable. Otherwise you strip the protected name out of domain_names, then delete cleanly — same outage.
  • S1 matches on current upstream state, never the submitted body. Checking the request would let the strip-then-update path walk straight through.
  • S1 wildcards match in both directions. NPM_PROTECTED_DOMAINS=*.example.net must protect app.example.net. It once matched nothing and suppressed the "unprotected" warning, because the value was explicitly set.
  • S2 scopes by HTTP method, not name prefix. A delete_* rule misses disable_user_2fa — a DELETE that strips someone's second factor.
  • S6 is a rule, not a list. An enumerated version silently omitted update_user, so the latch stayed shut while is_disabled: true locked out an admin.
  • S7 has no override. A server that can delete its own credentials locks itself out permanently.

</details>


Configuration

<details open> <summary><b>Required</b></summary>

Variable Meaning
NPM_URL Base URL of the NPM instance
NPM_IDENTITY Account email
NPM_SECRET Account password

</details>

<details> <summary><b>Transport & inbound auth</b></summary>

Variable Default Meaning
NPM_MCP_BEARER_TOKEN unset Inbound token. Unset ⇒ no inbound auth
NPM_MCP_TRANSPORT streamable-http stdio | streamable-http
NPM_MCP_HTTP_HOST 0.0.0.0 Bind address
NPM_MCP_HTTP_PORT 8000 Bind port

</details>

<details> <summary><b>Safety latches</b></summary>

Variable Default Lifts
NPM_READ_ONLY 0 — (1 blocks all writes)
NPM_PROTECTED_DOMAINS derived from NPM_URL S1 denylist, comma-separated
NPM_ALLOW_SELF_MUTATION 0 S1
NPM_ALLOW_ACCOUNT_MUTATION 0 S6
NPM_ALLOW_CERT_EXPORT 0 S8

</details>

<details> <summary><b>Upstream behaviour</b></summary>

Variable Default Meaning
NPM_TOTP_SECRET unset Base32 seed; only if the account has 2FA
NPM_TLS_VERIFY 1 Verify NPM's certificate
NPM_TIMEOUT 30 Upstream timeout, seconds
NPM_MAX_RESPONSE_CHARS 50000 Response cap before truncation
NPM_GUIDANCE_GATE 1 Hint toward get_guidance on early mutations
LOG_LEVEL INFO

</details>


Deployment

docker build -t npm-mcp:latest .
docker compose up -d

The container joins an existing Docker network alongside NPM and publishes no ports. NPM reaches it by container DNS and terminates TLS, so the Bearer token never crosses the wire in plaintext.

<details> <summary><b>Three details that bite</b></summary>

  • No build: key in the compose file. A compose-string deploy (Portainer, for one) ships no build context, so the image is built first and referenced by tag.
  • The healthcheck resolves the bind host instead of hardcoding 127.0.0.1. With a custom NPM_MCP_HTTP_HOST the naive version marks a perfectly healthy container as unhealthy forever. It also short-circuits under stdio, where nothing is listening at all.
  • The authenticating warm-up runs in the server lifespan, so a misconfiguration fails the healthcheck rather than coming up green and breaking on first use.

</details>


Testing

uv run pytest              # 420 tests
uv run ruff check
uv run ruff format --check

Roughly 4,700 lines of tests against 3,300 lines of source, but the count matters less than the shape:

  • 🧬 Mutation-verified guardrails — every safety control has a test proven to fail when the control is disabled. Written after discovering an asyncio.Lock whose removal kept the suite green.
  • 🌐 Zero network access — every upstream call is respx-mocked. A test that needs the network is a broken test.
  • 🔍 A7 sweep — all 65 tools are called against an upstream returning secrets at four nesting depths, with a negative control asserting the fixture really contains them, so the sweep can't pass vacuously.
  • 📐 Schema drift guard — operation counts, payload shapes, and the packaged data file are all asserted, so an upstream upgrade fails here rather than in production.

Design notes

Document Contents
spec.md Product contract — decisions D1–D13, controls S1–S8, acceptance criteria A1–A10
docs/api-surface.md All 68 operations with body fields and required-ness
docs/module-contract.md Internal module interfaces
docs/findings.md Two things the OpenAPI document gets wrong, measured against a live instance
npm_mcp/data/npm-openapi.json Verbatim copy of the instance's /api/schemainside the package, because it's a runtime dependency, not documentation

<div align="center"> <sub>Built against Nginx Proxy Manager 2.15.1 · 44 paths · 68 operations</sub> </div>

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured