"""Drive /mcp through the app's ASGI callable, as a client's HTTP request would. Quart's test_client never reaches /mcp (it is dispatched before Quart's pipeline), so MCP tests call app.asgi_app directly. """ from __future__ import annotations import asyncio import json async def asgi_request(app, method, path, headers=None, body=None): raw = [(k.lower().encode(), v.encode()) for k, v in (headers or {}).items()] if body is not None: raw.append((b"content-length", str(len(body)).encode())) scope = { "type": "http", "asgi": {"version": "3.0", "spec_version": "2.3"}, "http_version": "1.1", "method": method, "scheme": "http", "path": path, "raw_path": path.encode(), "query_string": b"", "root_path": "", "headers": raw, "client": ("testclient", 50000), "server": ("testserver", 80), } pending = [{"type": "http.request", "body": body or b"", "more_body": False}] sent = [] responded = asyncio.Event() # Like a real client, stay connected until the response is complete. An # SSE response listens for http.disconnect and cancels its stream on it, # so reporting a disconnect as soon as the body is read empties the reply. async def receive(): if pending: return pending.pop(0) await responded.wait() return {"type": "http.disconnect"} async def send(message): sent.append(message) if message["type"] == "http.response.body" and not message.get("more_body"): responded.set() await asyncio.wait_for(app.asgi_app(scope, receive, send), timeout=15) start = next(m for m in sent if m["type"] == "http.response.start") data = b"".join(m.get("body", b"") for m in sent if m["type"] == "http.response.body") return start["status"], dict(start.get("headers", [])), data def rpc_result(data: bytes) -> dict: """The JSON-RPC message from a JSON or a single-event SSE response body.""" text = data.decode() if text.lstrip().startswith("{"): return json.loads(text) lines = [ln[5:].strip() for ln in text.splitlines() if ln.startswith("data:")] if not lines: raise AssertionError(f"no JSON-RPC message in response body: {text!r}") return json.loads(lines[-1]) def rpc(method, params=None, id_=1) -> bytes: return json.dumps({"jsonrpc": "2.0", "id": id_, "method": method, "params": params or {}}).encode() HEADERS = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"}