Back to home page

EIC code displayed by LXR

 
 

    


Warning, file /swf-monitor/src/swf_monitor_project/mcp_asgi.py was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 """Standalone ASGI entrypoint for the swf-monitor MCP server.
0002 
0003 Replaces the Django ASGI app for /swf-monitor/mcp/ traffic with a
0004 lifespan-managed FastMCP service. The Starlette wrapper owns
0005 mcp.session_manager.run() for the lifetime of the uvicorn process — the
0006 fix for the per-request StreamableHTTPSessionManager lifecycle that
0007 django-mcp-server's adapter has.
0008 
0009 MCPRequestGuard wraps the Starlette app and enforces:
0010 - /health returns {"status": "ok"} with no auth, for the watchdog
0011 - Only POST is accepted (405 otherwise) — no server-pushed SSE, no GET
0012 - Authorization: Bearer <settings.MCP_BEARER_TOKEN> on every non-health
0013   request (401 missing, 403 wrong, 503 not configured)
0014 - Path normalization so /swf-monitor/mcp[/...], /mcp[/...], and / all
0015   reach the FastMCP app cleanly, regardless of what Apache strips
0016 
0017 See docs/MCP_FASTMCP_MIGRATION_PLAN.md.
0018 """
0019 
0020 from __future__ import annotations
0021 
0022 import contextlib
0023 import hmac
0024 import json
0025 import os
0026 from typing import Any
0027 
0028 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "swf_monitor_project.settings")
0029 
0030 import django
0031 from starlette.applications import Starlette
0032 from starlette.routing import Mount
0033 
0034 django.setup()
0035 
0036 from django.conf import settings  # noqa: E402
0037 
0038 from monitor_app.mcp import mcp  # noqa: E402
0039 
0040 
0041 def _json_body(value: dict[str, Any]) -> bytes:
0042     return json.dumps(value).encode("utf-8")
0043 
0044 
0045 async def _send_json(send, status: int, value: dict[str, Any], headers=None) -> None:
0046     body = _json_body(value)
0047     response_headers = [
0048         (b"content-type", b"application/json"),
0049         (b"content-length", str(len(body)).encode("ascii")),
0050     ]
0051     if headers:
0052         response_headers.extend(headers)
0053     await send({
0054         "type": "http.response.start",
0055         "status": status,
0056         "headers": response_headers,
0057     })
0058     await send({"type": "http.response.body", "body": body})
0059 
0060 
0061 class MCPRequestGuard:
0062     """Enforce auth and finite POST JSON-RPC before FastMCP sees a request."""
0063 
0064     def __init__(self, app):
0065         self.app = app
0066 
0067     async def __call__(self, scope, receive, send):
0068         if scope["type"] != "http":
0069             await self.app(scope, receive, send)
0070             return
0071 
0072         path = scope.get("path", "")
0073         if path == "/health":
0074             await _send_json(send, 200, {"status": "ok"})
0075             return
0076 
0077         scope = self._normalize_mcp_path(scope)
0078         method = scope.get("method", "").upper()
0079         if method != "POST":
0080             await _send_json(
0081                 send,
0082                 405,
0083                 {
0084                     "error": "MCP endpoint accepts POST JSON-RPC only",
0085                     "allowed_methods": ["POST"],
0086                 },
0087                 headers=[(b"allow", b"POST")],
0088             )
0089             return
0090 
0091         headers = self._headers(scope)
0092         auth_header = headers.get("authorization", "")
0093         if not auth_header.startswith("Bearer "):
0094             await _send_json(send, 401, {"error": "Authorization required"})
0095             return
0096 
0097         expected = getattr(settings, "MCP_BEARER_TOKEN", "") or ""
0098         if not expected:
0099             await _send_json(send, 503, {"error": "MCP token not configured"})
0100             return
0101 
0102         if not hmac.compare_digest(auth_header[7:], expected):
0103             await _send_json(send, 403, {"error": "Invalid token"})
0104             return
0105 
0106         await self.app(scope, receive, send)
0107 
0108     def _normalize_mcp_path(self, scope):
0109         """Accept common proxy forms: /, /mcp[/...], or /swf-monitor/mcp[/...]."""
0110         path = scope.get("path", "")
0111         root_path = scope.get("root_path", "")
0112         for prefix in ("/swf-monitor/mcp", "/mcp"):
0113             if path == prefix or path.startswith(prefix + "/"):
0114                 scope = dict(scope)
0115                 scope["root_path"] = root_path + prefix
0116                 scope["path"] = path[len(prefix):] or "/"
0117                 return scope
0118         return scope
0119 
0120     def _headers(self, scope) -> dict[str, str]:
0121         headers = {}
0122         for key, value in scope.get("headers", []):
0123             headers[key.decode("latin1").lower()] = value.decode("latin1")
0124         return headers
0125 
0126 
0127 @contextlib.asynccontextmanager
0128 async def lifespan(app: Starlette):
0129     async with mcp.session_manager.run():
0130         yield
0131 
0132 
0133 _mcp_application = Starlette(
0134     routes=[Mount("/", app=mcp.streamable_http_app())],
0135     lifespan=lifespan,
0136 )
0137 
0138 application = MCPRequestGuard(_mcp_application)