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
0037
0038 from monitor_app.mcp import mcp
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)