"""FastMCP server instance + Streamable HTTP ASGI app construction. We instantiate a single :class:`FastMCP` with all tools registered at import time, then expose :func:`build_mcp_app` so ``app.main`` can mount the resulting Starlette app under ``/api/v1/mcp/`` with our PAT auth middleware in front. Why a singleton: FastMCP keeps a registry of tools indexed by name. Reinstantiating it would lose the ``@mcp.tool(...)``-registered handlers in :mod:`app.mcp.tools`. Importing :mod:`app.mcp.tools` here triggers the side-effect registration; the explicit ``noqa: F401`` keeps linters from dropping the seemingly-unused import. """ from __future__ import annotations import logging from mcp.server.fastmcp import FastMCP from mcp.server.transport_security import TransportSecuritySettings from app.mcp.middleware import PatAuthASGIMiddleware logger = logging.getLogger(__name__) #: One global MCP server. ``stateless_http=True`` matches the most #: lenient Streamable HTTP variant — every request opens a fresh #: session — which is the right default for an HTTP-deployed service #: (no sticky sessions required, scales horizontally). Set to False #: only if we ever introduce server-side conversation state. #: #: ``streamable_http_path="/"`` is critical: the default is ``"/mcp"``, #: which combined with our FastAPI ``app.mount("/api/v1/mcp", …)`` #: would make the actual route live at the awkward ``/api/v1/mcp/mcp`` #: while clients reasonably expect ``/api/v1/mcp/`` (the URL we #: advertise in the help center / docs). Setting the inner path to #: ``"/"`` makes the mount-prefix the only path component the client #: sees — Codex's external probe found this 404 on 2026-04-27. mcp_server = FastMCP( name="helix", instructions=( "Helix is the unified IT platform for managing employees, " "hardware assets, software seats, API keys, cloud servers, " "domains, permissions, and workflow approvals. The tools below " "always operate as the personal-access-token's owner; results " "are scoped to what that user can see in the web UI. Write " "operations submit through the same approval chain a UI click " "would, so an action like 'request a Zoom seat' creates a " "Feishu approval request — it does not bypass review." ), stateless_http=True, streamable_http_path="/", # FastMCP's default DNS-rebinding protection allow-lists only # ``localhost`` / ``127.0.0.1``; under any other Host header it # returns 421 Misdirected Request. That breaks every real # deployment, since prod requests carry ``Host: helix.pingcap.net`` # (and the test client uses ``test``). The protection exists for # cookie-based / origin-trusted services where a malicious DNS # could trick a browser into hitting localhost — it is irrelevant # to a Bearer-token authenticated API. Disable it explicitly: PAT # auth gates every request and there is no implicit credential to # exfiltrate. transport_security=TransportSecuritySettings( enable_dns_rebinding_protection=False, ), ) def build_mcp_app(): """Return the ASGI app to mount under ``/api/v1/mcp/``. Order matters: ``PatAuthASGIMiddleware`` MUST sit OUTSIDE the FastMCP app so unauthenticated requests are rejected before they reach the JSON-RPC dispatcher (otherwise FastMCP would happily return tool list / schemas to anonymous callers). """ # Lazy-import so the first import of this module doesn't have to # walk every tool implementation tree (which itself imports # heavy domain services). from app.mcp import ( # noqa: F401 — prompt/resource side-effects prompts, resources, tools, # noqa: F401 — tool side-effects ) from app.mcp.annotations import apply_annotations apply_annotations(mcp_server) inner = mcp_server.streamable_http_app() return PatAuthASGIMiddleware(inner) __all__ = ["build_mcp_app", "mcp_server"]