63 lines
2.3 KiB
Python
63 lines
2.3 KiB
Python
"""Proxy ``FastMCP`` servers for user-declared external MCPs (``stdio``/``http``).
|
|
|
|
Built via ``fastmcp.server.create_proxy``; the proxy lazily opens its
|
|
transport on first request, so unused MCPs cost nothing until routed to.
|
|
"""
|
|
|
|
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))
|