| """ |
| MCP server β adaptive UI surface (Path B). |
| |
| Tools return structuredContent that the adaptive.html widget renders |
| inline in MCP Apps-compatible hosts (ChatGPT, Claude, Goose, VS Code). |
| |
| Run: |
| python -m mcp_server.server # stdio (local MCP client) |
| python mcp-server/server.py --http # SSE on :3100 (remote clients) |
| """ |
| import json |
| import os |
| import sys |
| from pathlib import Path |
|
|
| import httpx |
| from dotenv import load_dotenv |
| from mcp.server.fastmcp import FastMCP |
|
|
| load_dotenv() |
|
|
| sys.path.insert(0, str(Path(__file__).parent.parent)) |
| from lib.router import route |
|
|
| HF_ENDPOINT_URL = os.getenv("HF_ENDPOINT_URL", "") |
| HF_TOKEN = os.getenv("HF_TOKEN", "") |
| WIDGET_PATH = Path(__file__).parent / "widget" / "adaptive.html" |
|
|
| mcp = FastMCP("adaptive-model", version="1.0.0") |
|
|
| |
| |
| |
|
|
| @mcp.resource("ui://widget/adaptive.html", mime_type="text/html") |
| def adaptive_widget() -> str: |
| return WIDGET_PATH.read_text(encoding="utf-8") |
|
|
|
|
| |
|
|
| @mcp.tool() |
| async def respond(messages: list[dict], mode: str = "auto") -> dict: |
| """ |
| Run the adaptive model for the current conversation turn. |
| |
| Args: |
| messages: Full conversation history, e.g. |
| [{"role": "user", "content": "Show me revenue trends"}] |
| mode: Adapter override β 'support' | 'analytics' | 'form' | 'auto' |
| |
| Returns a dict with: |
| content β text for the model to reason over |
| structuredContent β ui_spec for the widget to render interactively |
| """ |
| resolved = route(messages, None if mode == "auto" else mode) |
|
|
| headers = {"Content-Type": "application/json"} |
| if HF_TOKEN: |
| headers["Authorization"] = f"Bearer {HF_TOKEN}" |
|
|
| async with httpx.AsyncClient(timeout=60.0) as client: |
| r = await client.post( |
| HF_ENDPOINT_URL, |
| json={"inputs": {"messages": messages, "mode": resolved}}, |
| headers=headers, |
| ) |
| r.raise_for_status() |
| out = r.json() |
|
|
| text = out.get("text", "") |
| ui_spec = out.get("ui_spec") |
|
|
| result: dict = { |
| "content": [{"type": "text", "text": text}], |
| } |
| if ui_spec: |
| |
| result["structuredContent"] = {**ui_spec, "_widget": "ui://widget/adaptive.html"} |
|
|
| return result |
|
|
|
|
| @mcp.tool() |
| async def classify_mode(messages: list[dict]) -> dict: |
| """ |
| Return which adapter would handle this turn without running inference. |
| Useful for debugging routing decisions. |
| """ |
| adapter = route(messages, None) |
| return {"adapter": adapter} |
|
|
|
|
| |
|
|
| if __name__ == "__main__": |
| import argparse |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--http", action="store_true", |
| help="Serve over SSE on port 3100 instead of stdio") |
| args = parser.parse_args() |
|
|
| if args.http: |
| mcp.run(transport="sse", host="0.0.0.0", port=3100) |
| else: |
| mcp.run(transport="stdio") |
|
|