Source code for elbow_helper.mcp_server

"""MCP server — the agent-tool door (optional ``[mcp]`` extra), via fastapi-mcp.

The fourth of elbow-helper's surfaces lets an AI agent call the pipeline as
Model-Context-Protocol tools by mounting ``fastapi-mcp`` on a copy of
:mod:`elbow_helper.api`'s FastAPI app: the four REST operations already
tagged with an explicit ``operation_id`` there (``knee``/``elbow``/
``diagnostics``/``locator``) become the four MCP tools here, automatically,
with the same request/response schema. Rename or extend a route in
``api.py`` and this door follows without a second edit — MCP must never be
a standalone, hand-written tool server that could drift from the REST API.

MCP is served over **Streamable HTTP** (mounted at ``/mcp`` on a running
app), not stdio — an HTTP-based MCP client pointed at the running server's
``/mcp`` endpoint is what ``fastapi-mcp`` offers. The same four tools are
also just REST endpoints an agent (or a human with curl) can hit directly.

Run it with::

    pip install 'elbow-helper[mcp]'
    uvicorn elbow_helper.mcp_server:app --port 8021   # or: python -m elbow_helper.mcp_server

MCP endpoint: ``http://127.0.0.1:8021/mcp``. Runs on a different port from
:mod:`elbow_helper.api`'s 8020 by default so both doors can run side by side.

Consumes: ``fastapi-mcp`` (optional, pulls in ``fastapi``/``mcp``),
:mod:`elbow_helper.api`.
Produces: :data:`app` (the ASGI application), :func:`build_server`.

Author
------
Warith Harchaoui, <warith.harchaoui@deraison.ai>
"""

from __future__ import annotations

from typing import TYPE_CHECKING

try:
    from fastapi_mcp import FastApiMCP
except ImportError as exc:  # pragma: no cover - exercised only without the extra
    raise ImportError(
        "the MCP server needs the [mcp] extra. Run: pip install 'elbow-helper[mcp]'"
    ) from exc

from .api import create_app

if TYPE_CHECKING:  # pragma: no cover - typing only
    from fastapi import FastAPI

# The only REST operations exposed as MCP tools — matches the operation_id
# set on each route in elbow_helper.api.create_app(). An explicit allowlist
# (rather than excluding FastAPI's auto /docs, /openapi.json) so a future
# route added to the API door doesn't silently become an MCP tool without a
# deliberate opt-in.
_EXPOSED_OPERATIONS = ["knee", "elbow", "diagnostics", "locator"]


[docs] def build_server(app: FastAPI) -> FastApiMCP: """Mount the MCP server (knee/elbow/diagnostics/locator tools) onto ``app``. Parameters ---------- app : fastapi.FastAPI The app to mount onto — typically a fresh :func:`elbow_helper.api.create_app` instance, so this module's :data:`app` stays independent of :mod:`elbow_helper.api`'s own module-level one. Returns ------- fastapi_mcp.FastApiMCP The mounted server descriptor (``.name`` defaults to ``app.title``). Examples -------- >>> from elbow_helper.api import create_app >>> mcp = build_server(create_app()) # doctest: +SKIP >>> mcp.name # doctest: +SKIP 'elbow-helper' """ mcp = FastApiMCP(app, include_operations=_EXPOSED_OPERATIONS) mcp.mount_http() return mcp
# A fresh app (not elbow_helper.api's module-level one) with the MCP server # mounted at /mcp, so `uvicorn elbow_helper.mcp_server:app` works out of the box. app = create_app() build_server(app)
[docs] def main() -> None: """Run the MCP-mounted app over HTTP (the ``python -m elbow_helper.mcp_server`` path).""" import uvicorn uvicorn.run(app, host="127.0.0.1", port=8021)
if __name__ == "__main__": # pragma: no cover main()