Skip to content

Commit 6eae3af

Browse files
IMNMVclaude
andcommitted
CI: deterministic stdio handshake driver
The printf-pipe version closed stdin immediately, racing the server's shutdown-on-EOF against its handling of tools/list; on slow runners the response was lost and the job failed intermittently (KeyError: 2). The driver keeps stdin open until both responses are read. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 793ecbf commit 6eae3af

2 files changed

Lines changed: 62 additions & 18 deletions

File tree

‎.github/scripts/handshake.py‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env python3
2+
"""CI smoke test: real MCP stdio handshake against the installed bridge.
3+
4+
Drives the server as a subprocess and keeps stdin open until both
5+
responses have been read. Piping a fixed byte-blob in and closing stdin
6+
immediately races the server's shutdown-on-EOF against its handling of
7+
the final request, which fails intermittently on slow runners.
8+
"""
9+
10+
import json
11+
import subprocess
12+
import sys
13+
14+
SERVER = sys.argv[1] if len(sys.argv) > 1 else "smoke/bin/clauder-mcp"
15+
16+
proc = subprocess.Popen(
17+
[SERVER],
18+
stdin=subprocess.PIPE,
19+
stdout=subprocess.PIPE,
20+
stderr=subprocess.DEVNULL,
21+
text=True,
22+
)
23+
24+
25+
def send(obj):
26+
proc.stdin.write(json.dumps(obj) + "\n")
27+
proc.stdin.flush()
28+
29+
30+
def recv(want_id):
31+
while True:
32+
line = proc.stdout.readline()
33+
if not line:
34+
sys.exit(f"server closed stdout while waiting for id {want_id}")
35+
msg = json.loads(line)
36+
if msg.get("id") == want_id:
37+
return msg
38+
39+
40+
try:
41+
send({
42+
"jsonrpc": "2.0", "id": 1, "method": "initialize",
43+
"params": {
44+
"protocolVersion": "2025-06-18",
45+
"capabilities": {},
46+
"clientInfo": {"name": "ci", "version": "0"},
47+
},
48+
})
49+
r1 = recv(1)
50+
assert r1["result"]["serverInfo"]["name"] == "r-studio", r1
51+
52+
send({"jsonrpc": "2.0", "method": "notifications/initialized"})
53+
send({"jsonrpc": "2.0", "id": 2, "method": "tools/list"})
54+
r2 = recv(2)
55+
tools = r2["result"]["tools"]
56+
assert len(tools) >= 25, f"expected >= 25 tools, got {len(tools)}"
57+
print(f"handshake OK, {len(tools)} tools")
58+
finally:
59+
proc.stdin.close()
60+
proc.terminate()
61+
proc.wait(timeout=10)

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,4 @@ jobs:
5757
- name: Unit tests
5858
run: smoke/bin/python -m pytest clauder-mcp/tests -q
5959
- name: stdio initialize + tools/list handshake
60-
run: |
61-
printf '%s\n%s\n%s\n' \
62-
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"ci","version":"0"}}}' \
63-
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
64-
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
65-
| timeout 30 smoke/bin/clauder-mcp 2>/dev/null \
66-
| python3 -c '
67-
import json, sys
68-
seen = {}
69-
for line in sys.stdin:
70-
m = json.loads(line)
71-
if m.get("id") in (1, 2):
72-
seen[m["id"]] = m
73-
assert seen[1]["result"]["serverInfo"]["name"] == "r-studio", seen.get(1)
74-
tools = seen[2]["result"]["tools"]
75-
assert len(tools) >= 25, f"expected >= 25 tools, got {len(tools)}"
76-
print(f"handshake OK, {len(tools)} tools")
77-
'
60+
run: timeout 60 python3 .github/scripts/handshake.py smoke/bin/clauder-mcp

0 commit comments

Comments
 (0)