feat: add streaming to markdown, fix raycast mcps exposing
This commit is contained in:
+46
-11
@@ -44,6 +44,8 @@ from beaver_gateway.settings import Settings
|
||||
from beaver_gateway.storage import Database
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.tools.base import Tool as FastMCPTool
|
||||
from starlette.applications import Starlette
|
||||
|
||||
from beaver_gateway.backends.base import Backend
|
||||
@@ -95,16 +97,26 @@ async def _async_main() -> None:
|
||||
stack.push_async_callback(token_store.stop)
|
||||
# Internal MCP URLs must exist before we construct any
|
||||
# ClaudeCodeBackendAdapter — adapters bake the URLs into their
|
||||
# ``BackendOptions.mcp_servers`` at construction time.
|
||||
internal_app, internal_urls = _build_internal_mcp(
|
||||
# ``BackendOptions.mcp_servers`` at construction time. The
|
||||
# ``mcp_servers`` map is used by the Raycast backend, which
|
||||
# needs in-process ``list_tools`` / ``call_tool`` access (the
|
||||
# Raycast wire has no native MCP concept).
|
||||
internal_app, internal_urls, mcp_servers = _build_internal_mcp(
|
||||
gateway.mcps, settings=settings
|
||||
)
|
||||
|
||||
# Prefetch tool catalogs for every MCP so RaycastAgent requests
|
||||
# don't pay a per-turn list_tools roundtrip and so a broken MCP
|
||||
# surfaces at startup instead of mid-conversation.
|
||||
mcp_tools = await _prefetch_mcp_tools(mcp_servers)
|
||||
|
||||
backends: dict[str, Backend] = await _build_backends(
|
||||
settings=settings,
|
||||
agents=agents,
|
||||
stack=stack,
|
||||
mcp_internal_urls=internal_urls,
|
||||
mcp_servers=mcp_servers,
|
||||
mcp_tools=mcp_tools,
|
||||
)
|
||||
|
||||
runtime = GatewayRuntime(
|
||||
@@ -152,20 +164,39 @@ async def _async_main() -> None:
|
||||
|
||||
def _build_internal_mcp(
|
||||
mcps: list[McpServerT], *, settings: Settings
|
||||
) -> tuple[Starlette | None, dict[str, str]]:
|
||||
"""Build the aggregator app + URL map, or return ``(None, {})``.
|
||||
) -> tuple[Starlette | None, dict[str, str], dict[str, FastMCP]]:
|
||||
"""Build the aggregator app + URL map + server map, or empty equivalents.
|
||||
|
||||
The URL map is always handed out (frontends may still introspect
|
||||
``runtime.mcp_internal_urls`` even if nothing is configured); the
|
||||
app is ``None`` when there are no MCPs to mount, so the caller
|
||||
skips the uvicorn task entirely.
|
||||
skips the uvicorn task entirely. The server map is the in-process
|
||||
handle the Raycast backend needs to splice MCP tools into its
|
||||
requests — empty when no MCPs are configured.
|
||||
"""
|
||||
if not mcps:
|
||||
return None, {}
|
||||
app, urls = build_internal_app(
|
||||
mcps, host="127.0.0.1", port=settings.internal_mcp_port
|
||||
)
|
||||
return app, urls
|
||||
return None, {}, {}
|
||||
return build_internal_app(mcps, host="127.0.0.1", port=settings.internal_mcp_port)
|
||||
|
||||
|
||||
async def _prefetch_mcp_tools(
|
||||
servers: dict[str, FastMCP],
|
||||
) -> dict[str, list[FastMCPTool]]:
|
||||
"""Eagerly enumerate tools per MCP so the Raycast loop has a static catalog.
|
||||
|
||||
Each underlying proxy is allowed to fail independently — a broken
|
||||
MCP shouldn't take down the whole gateway. The result has one entry
|
||||
per MCP that responded; agents that ``expose_mcps`` a missing entry
|
||||
will simply expose no tools from it (logged once per request).
|
||||
"""
|
||||
out: dict[str, list[FastMCPTool]] = {}
|
||||
for name, server in servers.items():
|
||||
try:
|
||||
out[name] = list(await server.list_tools())
|
||||
except Exception: # noqa: BLE001 — proxy can raise any transport error; we degrade per-MCP rather than fail the whole gateway
|
||||
_log.exception("failed to list tools for MCP %r — skipping", name)
|
||||
out[name] = []
|
||||
return out
|
||||
|
||||
|
||||
async def _serve_internal_mcp(app: Starlette, *, settings: Settings) -> None:
|
||||
@@ -196,6 +227,8 @@ async def _build_backends(
|
||||
agents: AgentRegistry,
|
||||
stack: AsyncExitStack,
|
||||
mcp_internal_urls: dict[str, str],
|
||||
mcp_servers: dict[str, FastMCP],
|
||||
mcp_tools: dict[str, list[FastMCPTool]],
|
||||
) -> dict[str, Backend]:
|
||||
"""Construct one backend per agent name.
|
||||
|
||||
@@ -216,7 +249,9 @@ async def _build_backends(
|
||||
if raycast_agents:
|
||||
client = await _try_open_raycast_client(settings, stack)
|
||||
if client is not None:
|
||||
raycast_backend = RaycastBackend(client)
|
||||
raycast_backend = RaycastBackend(
|
||||
client, mcp_servers=mcp_servers, mcp_tools=mcp_tools
|
||||
)
|
||||
for a in raycast_agents:
|
||||
backends[a.name] = raycast_backend
|
||||
|
||||
|
||||
Reference in New Issue
Block a user