diff --git a/src/npm_mcp/client.py b/src/npm_mcp/client.py index 167a06f..34deb4f 100644 --- a/src/npm_mcp/client.py +++ b/src/npm_mcp/client.py @@ -13,6 +13,7 @@ from .exceptions import ( NpmNotFoundError, ) from .models import ( + AccessList, AuditLogEntry, Certificate, HealthStatus, @@ -92,9 +93,7 @@ class NpmClient: raise NpmAuthenticationError("Invalid credentials") if response.status_code != 200: - raise NpmApiError( - f"Login failed: {response.text}", status_code=response.status_code - ) + raise NpmApiError(f"Login failed: {response.text}", status_code=response.status_code) data = response.json() token_response = TokenResponse(**data) @@ -172,9 +171,7 @@ class NpmClient: raise NpmNotFoundError(f"Resource not found: {endpoint}") if response.status_code >= 400: - raise NpmApiError( - f"API error: {response.text}", status_code=response.status_code - ) + raise NpmApiError(f"API error: {response.text}", status_code=response.status_code) return response @@ -259,3 +256,70 @@ class NpmClient: ) data = response.json() return [AuditLogEntry(**entry) for entry in data] + + async def get_access_lists(self) -> list[AccessList]: + """Get all access lists.""" + response = await self._request("GET", "/nginx/access-lists") + data = response.json() + return [AccessList(**item) for item in data] + + async def create_proxy_host( + self, + domain_names: list[str], + forward_host: str, + forward_port: int, + forward_scheme: str = "http", + certificate_id: int | None = None, + ssl_forced: bool = True, + hsts_enabled: bool = True, + hsts_subdomains: bool = False, + http2_support: bool = True, + block_exploits: bool = True, + caching_enabled: bool = False, + allow_websocket_upgrade: bool = True, + access_list_id: int = 0, + advanced_config: str = "", + meta: dict | None = None, + ) -> ProxyHost: + """Create a new proxy host. + + Args: + domain_names: List of domain names for this host + forward_host: Backend host to forward to + forward_port: Backend port to forward to + forward_scheme: http or https + certificate_id: SSL certificate ID (0 for none, use list_certificates to find) + ssl_forced: Force SSL/HTTPS + hsts_enabled: Enable HSTS + hsts_subdomains: Include subdomains in HSTS + http2_support: Enable HTTP/2 + block_exploits: Enable exploit blocking + caching_enabled: Enable caching + allow_websocket_upgrade: Allow WebSocket upgrades + access_list_id: Access list ID (0 for none, use list_access_lists to find) + advanced_config: Custom nginx configuration + meta: Additional metadata + + Returns: + Created ProxyHost object + """ + payload = { + "domain_names": domain_names, + "forward_host": forward_host, + "forward_port": forward_port, + "forward_scheme": forward_scheme, + "certificate_id": certificate_id or 0, + "ssl_forced": ssl_forced, + "hsts_enabled": hsts_enabled, + "hsts_subdomains": hsts_subdomains, + "http2_support": http2_support, + "block_exploits": block_exploits, + "caching_enabled": caching_enabled, + "allow_websocket_upgrade": allow_websocket_upgrade, + "access_list_id": access_list_id, + "advanced_config": advanced_config, + "meta": meta or {}, + } + + response = await self._request("POST", "/nginx/proxy-hosts", json=payload) + return ProxyHost(**response.json()) diff --git a/src/npm_mcp/models.py b/src/npm_mcp/models.py index f107152..e1be9f4 100644 --- a/src/npm_mcp/models.py +++ b/src/npm_mcp/models.py @@ -34,6 +34,18 @@ class Owner(BaseModel): roles: list[str] +class AccessList(BaseModel): + """Access list for authentication/IP restrictions.""" + + id: int + created_on: datetime + modified_on: datetime + owner_user_id: int = 0 + name: str + satisfy_any: bool = False + pass_auth: bool = False + + class Certificate(BaseModel): """SSL Certificate information.""" diff --git a/src/npm_mcp/server.py b/src/npm_mcp/server.py index 7a2d756..d2821c9 100644 --- a/src/npm_mcp/server.py +++ b/src/npm_mcp/server.py @@ -179,7 +179,7 @@ async def get_system_health() -> str: try: await client._ensure_authenticated() result.append("Authenticated: ✅") - + # Try to get settings (admin only) try: settings_list = await client.get_settings() @@ -253,11 +253,103 @@ async def list_certificates() -> str: expiry = f" (expires: {cert.expires_on.strftime('%Y-%m-%d')})" result.append( - f"[{cert.id}] {cert.nice_name} ({cert.provider})\n" - f" Domains: {domains}{expiry}" + f"[{cert.id}] {cert.nice_name} ({cert.provider})\n Domains: {domains}{expiry}" ) return f"Found {len(certs)} certificate(s):\n\n" + "\n\n".join(result) except Exception as e: return _format_error(e) + + +@mcp.tool() +async def list_access_lists() -> str: + """List all access lists configured in Nginx Proxy Manager. + + Returns a summary of all access lists including their IDs and names. + Use these IDs when creating proxy hosts that require access control. + """ + try: + client = get_client() + access_lists = await client.get_access_lists() + + if not access_lists: + return "No access lists configured." + + result = [] + for al in access_lists: + result.append(f"[{al.id}] {al.name}") + + return f"Found {len(access_lists)} access list(s):\n\n" + "\n".join(result) + + except Exception as e: + return _format_error(e) + + +@mcp.tool() +async def create_proxy_host( + domain_names: list[str], + forward_host: str, + forward_port: int, + forward_scheme: str = "http", + certificate_id: int = 0, + ssl_forced: bool = True, + block_exploits: bool = True, + allow_websocket_upgrade: bool = True, + access_list_id: int = 0, + advanced_config: str = "", +) -> str: + """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") + forward_port: Backend port to forward to (e.g., 8080) + forward_scheme: Backend protocol - "http" or "https" (default: "http") + certificate_id: SSL certificate ID. Use list_certificates to find available certs. + Use 0 for no SSL, or the ID of a matching wildcard cert. + ssl_forced: Force HTTPS redirect (default: True) + block_exploits: Enable common exploit blocking (default: True) + allow_websocket_upgrade: Allow WebSocket connections (default: True) + access_list_id: Access list ID for authentication. Use list_access_lists to find. + Use 0 for no access restrictions (default: 0) + advanced_config: Custom nginx configuration block (default: "") + + Returns: + JSON with created proxy host details including the new host ID. + + Example: + create_proxy_host( + domain_names=["myapp.ext.ben.io"], + forward_host="10.0.0.50", + forward_port=3000, + certificate_id=24, # *.ext.ben.io wildcard + ssl_forced=True + ) + """ + try: + client = get_client() + host = await client.create_proxy_host( + domain_names=domain_names, + forward_host=forward_host, + forward_port=forward_port, + forward_scheme=forward_scheme, + certificate_id=certificate_id, + ssl_forced=ssl_forced, + block_exploits=block_exploits, + allow_websocket_upgrade=allow_websocket_upgrade, + access_list_id=access_list_id, + advanced_config=advanced_config, + ) + + domains = ", ".join(host.domain_names) + return ( + f"Successfully created proxy host!\n\n" + f"ID: {host.id}\n" + f"Domains: {domains}\n" + f"Forward: {host.forward_scheme}://{host.forward_host}:{host.forward_port}\n" + f"SSL: {'Enabled' if host.ssl_forced else 'Disabled'}" + ) + + except Exception as e: + return _format_error(e)