From 7da72a38887936598873ef048f979bd84c046362 Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Thu, 21 May 2026 09:04:14 +0200 Subject: [PATCH] feat(api): log inbound User-Agent on management API and webhook receiver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Astrolabe (and any other PHP-side client) sends a stable User-Agent on every outbound call to the MCP server. Capture it at the middleware layer so backend access logs can attribute each request to a specific client build — e.g. ``Nextcloud-Astrolabe/0.14.1``. The middleware fires only for /api/v1/* and /webhooks/nextcloud, which is the surface PHP-side clients hit; /mcp and /health stay silent. The structured ``extra`` ({user_agent, http_method, http_path}) flows into OTel spans so the field is queryable in Grafana / Loki. Co-Authored-By: Claude Opus 4.7 (1M context) --- nextcloud_mcp_server/app.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/nextcloud_mcp_server/app.py b/nextcloud_mcp_server/app.py index f5d0d8c2..5a01f745 100644 --- a/nextcloud_mcp_server/app.py +++ b/nextcloud_mcp_server/app.py @@ -2475,6 +2475,29 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None = response = await call_next(request) return response + # Log the inbound User-Agent on management API and webhook receiver routes + # so we can tell which Astrolabe (or other PHP-side client) build is + # talking to the backend. Astrolabe sends ``Nextcloud-Astrolabe/``. + _UA_LOGGED_PATH_PREFIXES = ("/api/v1/", "/webhooks/nextcloud") + + @app.middleware("http") + async def log_client_user_agent(request, call_next): + path = request.url.path + if path.startswith(_UA_LOGGED_PATH_PREFIXES): + ua = request.headers.get("user-agent") or "(none)" + logger.info( + "%s %s from %s", + request.method, + path, + ua, + extra={ + "user_agent": ua, + "http_method": request.method, + "http_path": path, + }, + ) + return await call_next(request) + # Add CORS middleware to allow browser-based clients like MCP Inspector app.add_middleware( CORSMiddleware, # type: ignore[invalid-argument-type]