"""End-to-end stdio verification. Boots the MCP server as a subprocess, exchanges real JSON-RPC 2.0 messages over stdio (the same transport Claude Desktop / mcp-cli use), and verifies: 1. The initialize handshake completes and reports our 6 tools. 2. tools/list returns the same 6 tools with proper JSON Schema. 3. tools/call (list_repos) hits the real Gitea instance and returns data. This is the closest you can get to "what does a real MCP client see?" without installing one. If this script passes, the server speaks the protocol correctly. """ from __future__ import annotations import json import os import subprocess import sys from pathlib import Path from typing import Any def run_e2e() -> None: env = os.environ.copy() if "GITEA_URL" not in env or "GITEA_TOKEN" not in env: print("ERROR: GITEA_URL and GITEA_TOKEN must be in env.", file=sys.stderr) sys.exit(2) cmd = ["uv", "run", "homelab-mcp-gitea"] proc = subprocess.Popen( cmd, cwd=Path(__file__).parent.parent, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ) def send(msg: dict[str, Any]) -> dict[str, Any]: line = json.dumps(msg) + "\n" assert proc.stdin is not None proc.stdin.write(line) proc.stdin.flush() assert proc.stdout is not None response_line = proc.stdout.readline() return json.loads(response_line) try: # 1) initialize init_resp = send( { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "stdio-verifier", "version": "0.1.0"}, }, } ) assert "result" in init_resp, f"initialize failed: {init_resp}" server_info = init_resp["result"]["serverInfo"] print(f"server: {server_info['name']} v{server_info['version']}") # MCP requires the initialized notification before tool calls assert proc.stdin is not None proc.stdin.write( json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n" ) proc.stdin.flush() # 2) tools/list tools_resp = send({"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}) assert "result" in tools_resp, f"tools/list failed: {tools_resp}" tools = tools_resp["result"]["tools"] tool_names = sorted(t["name"] for t in tools) print(f"tools registered ({len(tools)}): {tool_names}") expected = { "create_issue", "get_file", "get_repo", "list_open_prs", "list_repos", "merge_pr", } assert set(tool_names) == expected, f"unexpected tools: {tool_names}" # 3) tools/call list_repos call_resp = send( { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "list_repos", "arguments": {"limit": 3}}, } ) assert "result" in call_resp, f"tools/call failed: {call_resp}" content = call_resp["result"]["content"] assert content and content[0]["type"] == "text" text = content[0]["text"] print("list_repos returned:") for line in text.splitlines()[:5]: print(f" {line}") print("\nALL CHECKS PASSED — MCP server speaks the protocol correctly.") finally: proc.terminate() try: proc.wait(timeout=3) except subprocess.TimeoutExpired: proc.kill() stderr = proc.stderr.read() if proc.stderr else "" if stderr.strip(): print("\n--- server stderr ---") print(stderr) if __name__ == "__main__": run_e2e()