Initial curated ITWorx_MCP_Hub source from 368fba691ea8d23403b4d56b53b5ec670d7c5020
This commit is contained in:
commit
cb5be898ea
663 files changed
+82674
No files matched your search
@@ -0,0 +1,143 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from fastapi import FastAPI, Request, status
|
||||
from fastapi import Response as FastAPIResponse
|
||||
from fastapi.responses import FileResponse, Response
|
||||
|
||||
from .common import run_asgi
|
||||
from .oauth_metadata import OAuthMetadataSettings, protected_resource_metadata
|
||||
|
||||
_REQUEST_EXCLUDED = frozenset({"host", "content-length", "connection"})
|
||||
_RESPONSE_ALLOWED = frozenset(
|
||||
{
|
||||
"cache-control",
|
||||
"content-type",
|
||||
"expires",
|
||||
"location",
|
||||
"pragma",
|
||||
"set-cookie",
|
||||
"www-authenticate",
|
||||
"x-trace-id",
|
||||
}
|
||||
)
|
||||
_MCP_RESOURCE_PATH = re.compile(r"^[a-z0-9][a-z0-9-]{0,62}/[a-z0-9][a-z0-9-]{0,62}$")
|
||||
|
||||
|
||||
def build_app() -> FastAPI:
|
||||
control_url = os.environ.get("HUB_CONTROL_URL", "http://control-api:8080").rstrip("/")
|
||||
worker_url = os.environ.get("HUB_WORKER_URL", "http://worker:8080").rstrip("/")
|
||||
static_root = Path(os.environ.get("HUB_WEB_ROOT", "/opt/hub/web-dist")).resolve()
|
||||
if not (static_root / "index.html").is_file():
|
||||
raise ValueError("built Hub web assets are unavailable")
|
||||
oauth_metadata = OAuthMetadataSettings.from_environment()
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(application: FastAPI) -> AsyncIterator[None]:
|
||||
application.state.control = httpx.AsyncClient(base_url=control_url, timeout=30)
|
||||
application.state.worker = httpx.AsyncClient(base_url=worker_url, timeout=30)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
await application.state.control.aclose()
|
||||
await application.state.worker.aclose()
|
||||
|
||||
app = FastAPI(
|
||||
title="ITWorx MCP Hub web",
|
||||
docs_url=None,
|
||||
redoc_url=None,
|
||||
openapi_url=None,
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
@app.get("/healthz")
|
||||
@app.get("/health")
|
||||
def health() -> dict[str, str]:
|
||||
return {"status": "healthy", "component": "web"}
|
||||
|
||||
@app.get("/ready")
|
||||
@app.get("/health/ready")
|
||||
async def ready(response: FastAPIResponse) -> dict[str, str]:
|
||||
try:
|
||||
upstream = await app.state.control.get("/ready")
|
||||
if upstream.status_code != 200:
|
||||
raise RuntimeError("control API not ready")
|
||||
worker = await app.state.worker.get("/health/ready")
|
||||
if worker.status_code != 200:
|
||||
raise RuntimeError("worker not ready")
|
||||
except Exception:
|
||||
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
return {"status": "not-ready", "component": "web"}
|
||||
return {"status": "ready", "component": "web"}
|
||||
|
||||
@app.get("/version")
|
||||
def version() -> dict[str, str]:
|
||||
return {"version": os.environ.get("HUB_RELEASE_VERSION", "1.0.0-rc")}
|
||||
|
||||
def protected_resource(resource_path: str) -> dict[str, object]:
|
||||
if not _MCP_RESOURCE_PATH.fullmatch(resource_path):
|
||||
resource_path = "gitea-all/prod"
|
||||
return protected_resource_metadata(resource_path, oauth_metadata)
|
||||
|
||||
@app.get("/.well-known/oauth-protected-resource")
|
||||
def default_oauth_protected_resource_metadata() -> dict[str, object]:
|
||||
return protected_resource("gitea-all/prod")
|
||||
|
||||
@app.get("/.well-known/oauth-protected-resource/mcp/{project}/{environment}")
|
||||
def oauth_protected_resource_metadata(project: str, environment: str) -> dict[str, object]:
|
||||
return protected_resource(f"{project}/{environment}")
|
||||
|
||||
async def proxy(request: Request, path: str) -> Response:
|
||||
headers = {
|
||||
key: value
|
||||
for key, value in request.headers.items()
|
||||
if key.lower() not in _REQUEST_EXCLUDED
|
||||
}
|
||||
upstream = await app.state.control.request(
|
||||
request.method,
|
||||
f"/{path}",
|
||||
params=request.query_params,
|
||||
content=await request.body(),
|
||||
headers=headers,
|
||||
follow_redirects=False,
|
||||
)
|
||||
response_headers = {
|
||||
key: value
|
||||
for key, value in upstream.headers.items()
|
||||
if key.lower() in _RESPONSE_ALLOWED
|
||||
}
|
||||
return Response(
|
||||
content=upstream.content,
|
||||
status_code=upstream.status_code,
|
||||
headers=response_headers,
|
||||
media_type=None,
|
||||
)
|
||||
|
||||
@app.api_route("/auth/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
|
||||
async def auth_proxy(request: Request, path: str) -> Response:
|
||||
return await proxy(request, f"auth/{path}")
|
||||
|
||||
@app.api_route("/api/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
|
||||
async def api_proxy(request: Request, path: str) -> Response:
|
||||
return await proxy(request, f"api/{path}")
|
||||
|
||||
@app.get("/{path:path}", response_model=None)
|
||||
async def static(path: str) -> FileResponse:
|
||||
candidate = (static_root / path).resolve()
|
||||
if candidate != static_root and static_root not in candidate.parents:
|
||||
candidate = static_root / "index.html"
|
||||
if not candidate.is_file():
|
||||
candidate = static_root / "index.html"
|
||||
return FileResponse(candidate)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def run() -> None:
|
||||
run_asgi(build_app())
|
||||
Reference in new issue
Block a user