joduor's picture
init: adaptive-model β€” handler, gateway, mcp-server, training
1de92bf verified
Raw
History Blame Contribute Delete
3.69 kB
"""
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")
# ── widget resource ───────────────────────────────────────────────────────────
# MCP Apps hosts load this URI into an iframe when the tool result carries
# a _widget key in structuredContent.
@mcp.resource("ui://widget/adaptive.html", mime_type="text/html")
def adaptive_widget() -> str:
return WIDGET_PATH.read_text(encoding="utf-8")
# ── tools ─────────────────────────────────────────────────────────────────────
@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:
# _widget tells MCP Apps hosts which resource URI to iframe
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}
# ── entry-point ───────────────────────────────────────────────────────────────
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")