diff --git a/README.md b/README.md index f3bdadf..2bceb2a 100644 --- a/README.md +++ b/README.md @@ -134,8 +134,38 @@ Add to your `claude_desktop_config.json`: | `list_access_lists` | List access lists for authentication/IP restrictions | | `create_proxy_host` | Create a new proxy host | | `update_proxy_host` | Update an existing proxy host (v0.0.3+) | +| `delete_proxy_host` | Delete a proxy host permanently | +| `enable_proxy_host` | Enable (bring online) a disabled proxy host | +| `disable_proxy_host` | Disable (take offline) a proxy host without deleting it | | `create_certificate` | Provision a new Let's Encrypt SSL certificate (v0.0.3+) | +## Managing Proxy Host Lifecycle + +Beyond creating and updating hosts, the server can delete a host outright or +toggle a host on and off without losing its configuration. Find the host ID +with `list_proxy_hosts` first. + +```text +# Take a host offline temporarily (config is preserved) +disable_proxy_host(42) + +# Bring it back online +enable_proxy_host(42) + +# Permanently remove a host (cannot be undone) +delete_proxy_host(42) +``` + +Notes: + +- `enable_proxy_host` / `disable_proxy_host` map to NPM's + `POST /nginx/proxy-hosts/{id}/enable` and `/disable` endpoints. If the host + is already in the requested state, NPM returns an HTTP 400 error + (e.g. `Host is already enabled`), which the tool surfaces as an API error. +- `delete_proxy_host` maps to `DELETE /nginx/proxy-hosts/{id}` and is + destructive — the reverse proxy stops serving the host's domains + immediately. Recreate it with `create_proxy_host` if you need it back. + ## Log Access Setup The `get_proxy_host_logs` tool reads nginx log files directly from disk. Since NPM has no API for log retrieval, you need to mount NPM's log directory into the MCP container. diff --git a/src/npm_mcp/client.py b/src/npm_mcp/client.py index 225f37a..a90d13f 100644 --- a/src/npm_mcp/client.py +++ b/src/npm_mcp/client.py @@ -362,6 +362,71 @@ class NpmClient: response = await self._request("PUT", f"/nginx/proxy-hosts/{host_id}", json=payload) return ProxyHost(**response.json()) + async def delete_proxy_host(self, host_id: int) -> bool: + """Delete a proxy host by ID. + + Calls ``DELETE /nginx/proxy-hosts/{id}``. The reverse proxy stops + serving the host's domains immediately and the configuration is + removed. This is destructive and cannot be undone. + + Args: + host_id: The proxy host ID to delete + + Returns: + True if NPM reported the host was deleted + + Raises: + NpmNotFoundError: If the proxy host does not exist + NpmApiError: For other API errors + """ + response = await self._request("DELETE", f"/nginx/proxy-hosts/{host_id}") + # NPM returns the JSON literal `true` on a successful delete. + return bool(response.json()) + + async def enable_proxy_host(self, host_id: int) -> bool: + """Enable (bring online) a proxy host by ID. + + Calls ``POST /nginx/proxy-hosts/{id}/enable``. If the host is already + enabled, NPM responds with HTTP 400 ("Host is already enabled"), which + raises NpmApiError. + + Args: + host_id: The proxy host ID to enable + + Returns: + True if NPM reported the host was enabled + + Raises: + NpmNotFoundError: If the proxy host does not exist + NpmApiError: For other API errors + """ + response = await self._request("POST", f"/nginx/proxy-hosts/{host_id}/enable") + # NPM returns the JSON literal `true` on success. + return bool(response.json()) + + async def disable_proxy_host(self, host_id: int) -> bool: + """Disable (take offline) a proxy host by ID. + + Calls ``POST /nginx/proxy-hosts/{id}/disable``. The host's + configuration is preserved; the reverse proxy simply stops serving + its domains until it is re-enabled. If the host is already disabled, + NPM responds with HTTP 400 ("Host is already disabled"), which raises + NpmApiError. + + Args: + host_id: The proxy host ID to disable + + Returns: + True if NPM reported the host was disabled + + Raises: + NpmNotFoundError: If the proxy host does not exist + NpmApiError: For other API errors + """ + response = await self._request("POST", f"/nginx/proxy-hosts/{host_id}/disable") + # NPM returns the JSON literal `true` on success. + return bool(response.json()) + async def create_certificate( self, domain_names: list[str], diff --git a/src/npm_mcp/server.py b/src/npm_mcp/server.py index 94efe45..edde0f6 100644 --- a/src/npm_mcp/server.py +++ b/src/npm_mcp/server.py @@ -86,7 +86,7 @@ async def list_proxy_hosts() -> str: result = [] for host in hosts: domains = ", ".join(host.domain_names) - ssl_status = "🔒 SSL" if host.ssl_forced else "🔓 HTTP" + ssl_status = "\U0001f512 SSL" if host.ssl_forced else "\U0001f513 HTTP" enabled_status = "✅" if host.enabled else "❌" result.append( @@ -313,8 +313,8 @@ async def create_proxy_host( """Create a new proxy host in Nginx Proxy Manager. Args: - domain_names: List of domain names (e.g., ["app.ext.ben.io"]) - forward_host: Backend host/IP to forward to (e.g., "192.168.1.100" or "container-name") + domain_names: List of domain names (e.g., ["app.example.com"]) + forward_host: Backend host/IP to forward to (e.g., "10.0.0.50" or "container-name") forward_port: Backend port to forward to (e.g., 8080) forward_scheme: Backend protocol - "http" or "https" (default from config) certificate_id: SSL certificate ID. Use list_certificates to find available certs. @@ -335,10 +335,10 @@ async def create_proxy_host( Example: create_proxy_host( - domain_names=["myapp.ext.ben.io"], + domain_names=["myapp.example.com"], forward_host="10.0.0.50", forward_port=3000, - certificate_id=24, # *.ext.ben.io wildcard + certificate_id=24, # *.example.com wildcard ) """ try: @@ -459,6 +459,118 @@ async def update_proxy_host( return _format_error(e) +@mcp.tool() +async def delete_proxy_host(host_id: int) -> str: + """Delete a proxy host from Nginx Proxy Manager. + + Permanently removes the proxy host configuration via + DELETE /nginx/proxy-hosts/{id}. The reverse proxy stops serving the + host's domains immediately. This action cannot be undone — recreate the + host with create_proxy_host if you need it back. + + Args: + host_id: The ID of the proxy host to delete (use list_proxy_hosts to find IDs) + + Returns: + Confirmation that the proxy host was deleted. + + Example: + delete_proxy_host(42) # permanently remove proxy host 42 + """ + try: + client = get_client() + + # Resolve the domains first so the confirmation message is meaningful. + domains: str | None = None + try: + host = await client.get_proxy_host(host_id) + domains = ", ".join(host.domain_names) + except Exception: + domains = None + + await client.delete_proxy_host(host_id) + + if domains: + return f"Successfully deleted proxy host [{host_id}] ({domains})." + return f"Successfully deleted proxy host [{host_id}]." + + except Exception as e: + return _format_error(e) + + +@mcp.tool() +async def enable_proxy_host(host_id: int) -> str: + """Enable a proxy host in Nginx Proxy Manager. + + Brings a previously disabled proxy host back online via + POST /nginx/proxy-hosts/{id}/enable, so the reverse proxy serves its + domains again. If the host is already enabled, NPM returns an HTTP 400 + error ("Host is already enabled"), which is surfaced as an API error. + + Args: + host_id: The ID of the proxy host to enable (use list_proxy_hosts to find IDs) + + Returns: + Confirmation that the proxy host was enabled. + + Example: + enable_proxy_host(42) # bring proxy host 42 back online + """ + try: + client = get_client() + await client.enable_proxy_host(host_id) + + try: + host = await client.get_proxy_host(host_id) + domains = ", ".join(host.domain_names) + return f"Successfully enabled proxy host [{host_id}] ({domains})." + except Exception: + return f"Successfully enabled proxy host [{host_id}]." + + except Exception as e: + return _format_error(e) + + +@mcp.tool() +async def disable_proxy_host(host_id: int) -> str: + """Disable a proxy host in Nginx Proxy Manager. + + Takes a proxy host offline via POST /nginx/proxy-hosts/{id}/disable + without deleting it. The reverse proxy stops serving the host's domains + until it is re-enabled with enable_proxy_host; the configuration is + preserved. If the host is already disabled, NPM returns an HTTP 400 + error ("Host is already disabled"), which is surfaced as an API error. + + Args: + host_id: The ID of the proxy host to disable (use list_proxy_hosts to find IDs) + + Returns: + Confirmation that the proxy host was disabled. + + Example: + disable_proxy_host(42) # take proxy host 42 offline, keep its config + """ + try: + client = get_client() + + # Resolve domains before disabling (host stays readable while disabled). + domains: str | None = None + try: + host = await client.get_proxy_host(host_id) + domains = ", ".join(host.domain_names) + except Exception: + domains = None + + await client.disable_proxy_host(host_id) + + if domains: + return f"Successfully disabled proxy host [{host_id}] ({domains})." + return f"Successfully disabled proxy host [{host_id}]." + + except Exception as e: + return _format_error(e) + + @mcp.tool() async def get_proxy_host_logs( host_id: int,