web package

This commit is contained in:
John Lancaster
2026-06-18 21:15:54 -05:00
parent 4b1a261b2d
commit b6010775ad
11 changed files with 277 additions and 28 deletions
+1
View File
@@ -0,0 +1 @@
"""FastAPI web runtime for personal MCP."""
+36
View File
@@ -0,0 +1,36 @@
from fastapi import FastAPI
from personal_mcp.mcp import mcp
from personal_mcp.web.config import Settings, get_settings
from personal_mcp.web.docs_mount import mount_docs_static
from personal_mcp.web.health import router as health_router
def create_app(settings: Settings | None = None) -> FastAPI:
runtime_settings = settings or get_settings()
mcp_app = mcp.http_app(
path=runtime_settings.mcp_route,
json_response=True,
stateless_http=True,
transport="http",
)
app = FastAPI(
debug=runtime_settings.debug,
docs_url=None,
redoc_url=None,
openapi_url=None,
lifespan=mcp_app.lifespan,
)
app.state.settings = runtime_settings
app.include_router(health_router)
mount_docs_static(
app,
docs_route=runtime_settings.docs_route,
site_dir=runtime_settings.site_dir,
)
app.mount("/", mcp_app, name="mcp")
return app
app = create_app()
+28
View File
@@ -0,0 +1,28 @@
from pathlib import Path
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
_REPO_ROOT = Path(__file__).resolve().parents[3]
class Settings(BaseSettings):
"""Runtime settings for the HTTP MCP and docs server."""
model_config = SettingsConfigDict(
env_file=".env",
env_prefix="PERSONAL_MCP_",
extra="ignore",
)
host: str = "127.0.0.1"
port: int = 8000
debug: bool = False
log_level: str = "info"
docs_route: str = "/docs"
mcp_route: str = "/mcp"
site_dir: Path = Field(default=_REPO_ROOT / "site")
def get_settings() -> Settings:
return Settings()
+40
View File
@@ -0,0 +1,40 @@
from pathlib import Path
from fastapi import FastAPI, Response, status
from fastapi.staticfiles import StaticFiles
def mount_docs_static(app: FastAPI, *, docs_route: str, site_dir: Path) -> None:
"""Mount the pre-built static docs site, or expose a clear missing-build response."""
normalized_route = docs_route.rstrip("/") or "/docs"
if site_dir.is_dir():
app.mount(
normalized_route,
StaticFiles(directory=site_dir, html=True),
name="docs",
)
return
async def docs_not_built() -> Response:
return Response(
content=(
"Static docs have not been built yet. "
"Run `uv run zensical build` before using this route."
),
media_type="text/plain",
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
)
app.add_api_route(
normalized_route,
docs_not_built,
methods=["GET"],
include_in_schema=False,
)
app.add_api_route(
f"{normalized_route}/{{path:path}}",
docs_not_built,
methods=["GET"],
include_in_schema=False,
)
+8
View File
@@ -0,0 +1,8 @@
from fastapi import APIRouter
router = APIRouter()
@router.get("/healthz", include_in_schema=False)
def healthz() -> dict[str, str]:
return {"status": "ok"}