#!/usr/bin/env python3 """ Demo: make an MCP request against mcplocal using an McpToken bearer. This is the standalone counterpart to `mcpctl test mcp` — intended to show exactly what a non-Claude client (e.g. a vLLM-driven agent) would do. Usage: # Default: localhost mcplocal, sre project, token from $MCPCTL_TOKEN export MCPCTL_TOKEN=mcpctl_pat_... python3 scripts/demo-mcp-call.py # Custom URL/project/tool python3 scripts/demo-mcp-call.py \\ --url https://mcp.ad.itaz.eu \\ --project sre \\ --token "$MCPCTL_TOKEN" \\ --tool begin_session \\ --args '{"description":"hello"}' No third-party deps — pure stdlib. Mirrors the protocol that src/shared/src/mcp-http/index.ts implements on the TypeScript side. """ from __future__ import annotations import argparse import json import os import sys import urllib.error import urllib.request from typing import Any def _parse_sse(body: str) -> list[dict[str, Any]]: """Parse a text/event-stream body into a list of JSON-RPC messages.""" out: list[dict[str, Any]] = [] for line in body.splitlines(): if line.startswith("data: "): try: out.append(json.loads(line[6:])) except json.JSONDecodeError: pass return out class McpSession: def __init__(self, url: str, bearer: str | None = None, timeout: float = 30.0): self.url = url self.bearer = bearer self.timeout = timeout self.session_id: str | None = None self._next_id = 1 def _headers(self) -> dict[str, str]: h = { "Content-Type": "application/json", "Accept": "application/json, text/event-stream", } if self.bearer: h["Authorization"] = f"Bearer {self.bearer}" if self.session_id: h["mcp-session-id"] = self.session_id return h def send(self, method: str, params: dict[str, Any] | None = None) -> Any: rid = self._next_id self._next_id += 1 payload = {"jsonrpc": "2.0", "id": rid, "method": method, "params": params or {}} req = urllib.request.Request( self.url, data=json.dumps(payload).encode("utf-8"), headers=self._headers(), method="POST", ) try: with urllib.request.urlopen(req, timeout=self.timeout) as resp: body = resp.read().decode("utf-8") content_type = resp.headers.get("content-type", "") # First successful response carries the session id. if self.session_id is None: sid = resp.headers.get("mcp-session-id") if sid: self.session_id = sid messages: list[dict[str, Any]] = ( _parse_sse(body) if "text/event-stream" in content_type else [json.loads(body)] ) except urllib.error.HTTPError as e: err_body = e.read().decode("utf-8", errors="replace") raise SystemExit(f"HTTP {e.code} from {self.url}: {err_body}") from None except urllib.error.URLError as e: raise SystemExit(f"transport error reaching {self.url}: {e.reason}") from None # Pick the response matching our id; fall back to first message. matched = next((m for m in messages if m.get("id") == rid), messages[0] if messages else None) if matched is None: raise SystemExit(f"no response for {method}") if "error" in matched: err = matched["error"] raise SystemExit(f"MCP error {err.get('code')}: {err.get('message')}") return matched.get("result") def initialize(self) -> dict[str, Any]: return self.send( "initialize", { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "demo-mcp-call.py", "version": "1.0.0"}, }, ) def list_tools(self) -> list[dict[str, Any]]: result = self.send("tools/list") return result.get("tools", []) if isinstance(result, dict) else [] def call_tool(self, name: str, args: dict[str, Any]) -> Any: return self.send("tools/call", {"name": name, "arguments": args}) def main() -> int: ap = argparse.ArgumentParser(description="Demo MCP request via McpToken bearer.") ap.add_argument("--url", default=os.environ.get("MCPGW_URL", "http://localhost:3200"), help="Base URL of mcplocal (default: $MCPGW_URL or http://localhost:3200)") ap.add_argument("--project", default="sre", help="Project name (default: sre). Must match the token's bound project.") ap.add_argument("--token", default=os.environ.get("MCPCTL_TOKEN"), help="Raw mcpctl_pat_* bearer (default: $MCPCTL_TOKEN)") ap.add_argument("--tool", help="Optionally call a tool after tools/list") ap.add_argument("--args", default="{}", help="JSON-encoded arguments for --tool") ap.add_argument("--timeout", type=float, default=30.0) opts = ap.parse_args() if not opts.token: ap.error("--token or $MCPCTL_TOKEN required") endpoint = f"{opts.url.rstrip('/')}/projects/{opts.project}/mcp" print(f"→ POST {endpoint}") print(f" Bearer: {opts.token[:16]}…") print() sess = McpSession(endpoint, bearer=opts.token, timeout=opts.timeout) info = sess.initialize() server_info = info.get("serverInfo", {}) if isinstance(info, dict) else {} print(f"initialize: protocol={info.get('protocolVersion') if isinstance(info, dict) else '?'} " f"server={server_info.get('name', '?')}/{server_info.get('version', '?')} " f"sessionId={sess.session_id}") tools = sess.list_tools() print(f"tools/list: {len(tools)} tool(s)") for t in tools: desc = (t.get("description") or "").splitlines()[0][:80] print(f" - {t['name']} {desc}") if opts.tool: try: args = json.loads(opts.args) except json.JSONDecodeError as e: raise SystemExit(f"--args must be valid JSON: {e}") print(f"\ntools/call: {opts.tool} {args}") result = sess.call_tool(opts.tool, args) print(json.dumps(result, indent=2)[:2000]) return 0 if __name__ == "__main__": sys.exit(main())