Source code for speaker_helper.mcp

"""
The MCP surface: the same routes, republished as agent tools.

Module summary
--------------
There is no second implementation here, and that is the design. The HTTP API
in :mod:`speaker_helper.api` already describes every operation — its
parameters, its types, its ``operation_id`` — and ``fastapi-mcp`` turns that
description into a tool catalogue. An agent calling ``narrate`` and a shell
calling ``POST /narrate`` reach the same function, so the two surfaces cannot
drift into disagreeing about what this package does.

What this module adds is a way to *start* it: a console script that serves the
app with the MCP endpoint mounted, and whose defaults name this package's own
environment variables rather than a neighbour's.

Why it came back
----------------
This surface was removed in 0.7.5. ``fastapi-mcp`` 0.4.0 called the ``mcp``
SDK with an argument count that release no longer accepted, and the mount was
guarded by ``except ImportError`` alone — so a ``TypeError`` escaped
``create_app()`` and broke every API test, not only the MCP ones. Both halves
are fixed: the pairing is pinned in the ``mcp`` extra, and the mount in
:func:`speaker_helper.api._mount_mcp` now catches more than ``ImportError``,
because an optional surface must never be able to take the HTTP API down.

Author
------
Warith HARCHAOUI — https://linkedin.com/in/warith-harchaoui
"""

from __future__ import annotations

import argparse
import os
import sys


[docs] def main(argv: list[str] | None = None) -> int: """Serve the API with its MCP endpoint mounted. Parameters ---------- argv : list of str or None Argument vector without ``argv[0]``; reads ``sys.argv`` when ``None``. Returns ------- int A process exit code. Non-zero when the extra is missing, with the install line rather than a traceback about a module the reader has never imported. """ parser = argparse.ArgumentParser( prog="speaker-helper-mcp", description=( "Serve speaker-helper's MCP tools over HTTP. Every tool is a route " "on the same FastAPI app, with the MCP endpoint mounted beside " "them at /mcp." ), formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "Defaults come from SPEAKER_HELPER_MCP_HOST and " "SPEAKER_HELPER_MCP_PORT when those are set.\n" "The host defaults to 127.0.0.1: pass --host 0.0.0.0 to accept " "connections from outside this machine." ), ) parser.add_argument( "--host", default=os.environ.get("SPEAKER_HELPER_MCP_HOST", "127.0.0.1"), help="Interface to bind (default: %(default)s).", ) parser.add_argument( "--port", type=int, default=int(os.environ.get("SPEAKER_HELPER_MCP_PORT", "8080")), help="Port to bind (default: %(default)s).", ) parser.add_argument( "--config", default=None, help="Settings file to load (default: ./settings.yaml)." ) args = parser.parse_args(argv) try: import uvicorn except ImportError: sys.stderr.write( "The MCP surface needs a web server. Install it with:\n" " pip install 'speaker-helper[mcp]'\n" ) return 2 from speaker_helper.api import create_app from speaker_helper.config import Settings app = create_app(Settings.load(args.config)) # One worker: the backend loads its model once per process and keeps it. # A second worker does not share that model, it pays for its own copy — # which for an ONNX graph is several hundred megabytes of silence. uvicorn.run(app, host=args.host, port=args.port, workers=1) return 0
if __name__ == "__main__": # pragma: no cover sys.exit(main())