ghpsolutions/quadro
Quadro: team boards with roles, history and an MCP endpoint for Claude and ChatGPT
A remix template · 0 remixes · Remix ghpsolutions/quadro
app/boards/main.py
"""Wires the layers together: one FastAPI app with the web page, the JSON API, /mcp, and the
sign-in metadata Claude.ai and ChatGPT use to find where a person signs in (RFC 9728)."""
from __future__ import annotations
import json
import logging
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from mcp.server.transport_security import TransportSecuritySettings
from starlette.routing import Route
from boards.application.use_cases import Boards
from boards.doors.identity import person_from
from boards.doors.mcp import build_mcp
from boards.doors.web import build_router, install_errors, static_files
APPID = os.environ.get("APPID_PUBLIC_URL", "https://appid.dodil.io").rstrip("/")
POOL = os.environ.get("DODIL_USER_POOL", "") # "<org>/<pool>", set by .dodil/deploy.yaml
METADATA = "/.well-known/oauth-protected-resource"
log = logging.getLogger("quadro")
def create_app(boards: Boards, on_start=None) -> FastAPI:
gate = {"mcp": None}
@asynccontextmanager
async def lifespan(_app):
if on_start:
try:
on_start()
except Exception: # keep serving; the error is in the log
log.exception("startup step failed")
mcp_app = build_mcp(boards).streamable_http_app(
streamable_http_path="/mcp", stateless_http=True, json_response=True,
# Every call carries identity only the gateway can mint, so host checks add nothing.
transport_security=TransportSecuritySettings(enable_dns_rebinding_protection=False))
gate["mcp"] = mcp_app
async with mcp_app.router.lifespan_context(mcp_app):
yield
app = FastAPI(title="Quadro", lifespan=lifespan, docs_url=None, redoc_url=None)
install_errors(app)
def base(request: Request) -> str:
h = request.headers
return f"{h.get('x-forwarded-proto', 'https')}://{h.get('x-forwarded-host', h.get('host'))}"
@app.get("/healthz", include_in_schema=False)
def healthz():
return {"ok": True}
@app.get(METADATA, include_in_schema=False)
@app.get(METADATA + "/mcp", include_in_schema=False)
def metadata(request: Request):
return {"resource": base(request) + "/mcp", "authorization_servers": [f"{APPID}/{POOL}"],
"bearer_methods_supported": ["header"], "resource_name": "Quadro"}
class McpDoor:
"""No person → 401 with the metadata link, which is what starts Claude's sign-in.
(A class, so Starlette hands it the raw ASGI call instead of a Request.)"""
async def __call__(self, scope, receive, send):
headers = {k.decode(): v.decode() for k, v in scope.get("headers", [])}
if person_from(headers) is None:
body = json.dumps({"detail": "sign in first"}).encode()
link = f'Bearer resource_metadata="{base(Request(scope))}{METADATA}/mcp"'
await send({"type": "http.response.start", "status": 401, "headers": [
(b"content-type", b"application/json"), (b"www-authenticate", link.encode())]})
await send({"type": "http.response.body", "body": body})
return
await gate["mcp"]({**scope, "path": "/mcp"}, receive, send)
mcp_door = McpDoor()
for path in ("/mcp", "/mcp/"):
app.router.routes.append(Route(path, endpoint=mcp_door, methods=["GET", "POST", "DELETE"]))
app.include_router(build_router(boards))
app.mount("/static", static_files(), name="static")
return app
def _production():
"""DataK3 + Dodil Models, or STORE=memory for a laptop run with no Dodil account."""
from boards.infrastructure.dodil import ModelsAssistant
if os.environ.get("STORE", "").lower() == "memory":
from boards.infrastructure.demo import seeded
repo = seeded(os.environ.get("DEV_USER", "you@example.com"))
assistant = ModelsAssistant() if os.environ.get("DODIL_SERVICE_ACCOUNT_ID") else None
return create_app(Boards(repo, assistant))
from boards.infrastructure.repository import SqlBoards
repo = SqlBoards()
return create_app(Boards(repo, ModelsAssistant()), on_start=repo.ensure_schema)
app = _production()