"""Proxy ``FastMCP`` servers for user-declared external MCPs (``stdio``/``http``). Both flavours end up as a ``FastMCPProxy`` instance, built via ``fastmcp.server.create_proxy``. The proxy lazily opens the underlying client transport when the first MCP request arrives, so we don't pay for connections that nothing routes to. From the aggregator app's point of view a proxy is indistinguishable from a regular ``FastMCP`` namespace — same ``http_app`` surface, same mount semantics. """ from __future__ import annotations from typing import TYPE_CHECKING from fastmcp import Client from fastmcp.client.transports import StdioTransport, StreamableHttpTransport from fastmcp.server import create_proxy from beaver_gateway.mcp.lenient import LenientStdioTransport if TYPE_CHECKING: from fastmcp import FastMCP from fastmcp.client.transports.base import ClientTransport from beaver_gateway.mcp.types import HttpMcp, StdioMcp def build_stdio_proxy(spec: StdioMcp) -> FastMCP: """Wrap a stdio subprocess MCP into a mountable ``FastMCPProxy``. ``spec.command`` is a non-empty tuple; the first element is the executable and the rest are CLI args. ``StdioTransport`` keeps the subprocess alive across calls. When ``spec.lenient`` is set we swap in :class:`LenientStdioTransport`, which silently drops non-JSON-RPC stdout lines instead of forwarding them as exceptions (lets us ingest MCPs that print ``Processing...`` chatter on stdout without leaking warnings to downstream UIs). """ if not spec.command: msg = f"stdio MCP {spec.name!r} has empty command" raise ValueError(msg) command, *args = spec.command cwd = str(spec.cwd) if spec.cwd is not None else None transport: ClientTransport if spec.lenient: transport = LenientStdioTransport( command=command, args=list(args), env=spec.env, cwd=cwd ) else: transport = StdioTransport( command=command, args=list(args), env=spec.env, cwd=cwd ) return create_proxy(Client(transport, name=spec.name)) def build_http_proxy(spec: HttpMcp) -> FastMCP: """Wrap a remote streamable-HTTP MCP into a mountable ``FastMCPProxy``. ``spec.headers`` are folded into the upstream client; pass per-call auth (e.g. ``X-Api-Key``) here rather than as ``auth`` when the upstream rejects Bearer headers. """ transport = StreamableHttpTransport( url=spec.url, auth=spec.auth, headers=spec.headers ) return create_proxy(Client(transport, name=spec.name))