MCPcopy Create free account
hub / github.com/ScriptedAlchemy/tracedecay / McpClient

Class McpClient

scripts/mcp_probe/probe.py:62–217  ·  view source on GitHub ↗

Thin JSON-RPC-over-stdio client. Responses are routed back to the awaiting caller by id, so a late response from a previously-timed-out request cannot be misread as the response to a later request.

Source from the content-addressed store, hash-verified

60
61
62class McpClient:
63 """Thin JSON-RPC-over-stdio client. Responses are routed back to the
64 awaiting caller by id, so a late response from a previously-timed-out
65 request cannot be misread as the response to a later request."""
66
67 def __init__(self, repo_path: str, repo_name: str = "unknown"):
68 # Capture stderr per-repo. Earlier revisions piped to DEVNULL, which
69 # made server crashes during init look like a BrokenPipeError from
70 # the probe driver — totally unactionable. Real causes (missing
71 # .tracedecay/tracedecay.db, unreadable DB, OOM on large repos like
72 # chromium) showed up on stderr but were silently discarded.
73 STDERR_DIR.mkdir(parents=True, exist_ok=True)
74 self.stderr_path = STDERR_DIR / f"{repo_name}.stderr"
75 self.stderr_file = open(self.stderr_path, "wb")
76 self.proc = subprocess.Popen(
77 [str(BIN), "serve"],
78 cwd=repo_path,
79 stdin=subprocess.PIPE,
80 stdout=subprocess.PIPE,
81 stderr=self.stderr_file,
82 bufsize=0,
83 )
84 self._id = 0
85 self._buf = b""
86 self._stale: set[int] = set() # ids whose original caller already gave up
87
88 # Initialize handshake — fail loud, fail early. Earlier the code
89 # ignored a missing/error response and tried to push the
90 # "initialized" notification regardless; if the server had already
91 # exited, that second write blew up the whole probe with a bare
92 # BrokenPipeError instead of skipping the repo.
93 try:
94 self._send({
95 "jsonrpc": "2.0",
96 "method": "initialize",
97 "params": {
98 "protocolVersion": "2024-11-05",
99 "capabilities": {},
100 "clientInfo": {"name": "tracedecay-probe", "version": "1"},
101 },
102 "id": self._next(),
103 })
104 except (BrokenPipeError, OSError) as exc:
105 raise McpInitError(repo_name, self.stderr_path,
106 f"server died before initialize ({exc})") from exc
107
108 resp = self._recv_id(self._id, timeout=15)
109 if resp is None:
110 raise McpInitError(repo_name, self.stderr_path,
111 "server closed stdout during initialize")
112 if isinstance(resp, dict) and resp.get("_timeout"):
113 raise McpInitError(repo_name, self.stderr_path,
114 "initialize timed out after 15s")
115 if isinstance(resp, dict) and "error" in resp:
116 err = resp["error"]
117 msg = err.get("message", str(err)) if isinstance(err, dict) else str(err)
118 raise McpInitError(repo_name, self.stderr_path,
119 f"initialize returned error: {msg[:200]}")

Callers 3

run_tool_freshFunction · 0.90
mainFunction · 0.90
mainFunction · 0.85

Calls

no outgoing calls

Tested by

no test coverage detected