Skip to content

Commit a59bcd3

Browse files
committed
CI fixes.
1 parent 97b8574 commit a59bcd3

File tree

8 files changed

+433
-15
lines changed

8 files changed

+433
-15
lines changed
Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
name: Claude NL/T No Unity (fake instance)
2+
3+
on: [workflow_dispatch]
4+
5+
permissions:
6+
contents: read
7+
checks: write
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
no-unity:
15+
runs-on: ubuntu-24.04 # Ubuntu 24.04 required for Claude Code installer
16+
timeout-minutes: 20
17+
steps:
18+
# ---------- Secrets check ----------
19+
- name: Detect secrets (outputs)
20+
id: detect
21+
env:
22+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
23+
run: |
24+
set -e
25+
if [ -n "$ANTHROPIC_API_KEY" ]; then echo "anthropic_ok=true" >> "$GITHUB_OUTPUT"; else echo "anthropic_ok=false" >> "$GITHUB_OUTPUT"; fi
26+
27+
- uses: actions/checkout@v4
28+
with:
29+
fetch-depth: 0
30+
31+
- uses: astral-sh/setup-uv@v4
32+
with:
33+
python-version: "3.11"
34+
35+
- name: Install MCP server deps
36+
run: |
37+
set -eux
38+
uv venv
39+
echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> "$GITHUB_ENV"
40+
echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH"
41+
if [ -f Server/pyproject.toml ]; then
42+
uv pip install -e Server
43+
elif [ -f Server/requirements.txt ]; then
44+
uv pip install -r Server/requirements.txt
45+
else
46+
echo "No MCP Python deps found" >&2
47+
exit 1
48+
fi
49+
50+
- name: Create fake Unity instance (status + listener)
51+
env:
52+
UNITY_PORT: 6400
53+
run: |
54+
set -euxo pipefail
55+
mkdir -p "$GITHUB_WORKSPACE/.unity-mcp"
56+
57+
HASH="$(python3 <<'PY'
58+
import hashlib, os
59+
path = os.path.join(os.environ['GITHUB_WORKSPACE'], 'TestProjects/UnityMCPTests/Assets')
60+
print(hashlib.sha1(path.encode()).hexdigest()[:8])
61+
PY
62+
)"
63+
export HASH
64+
echo "UNITY_MCP_DEFAULT_INSTANCE=UnityMCPTests@${HASH}" >> "$GITHUB_ENV"
65+
66+
python3 <<'PY'
67+
import json, os, time, pathlib
68+
ws = os.environ["GITHUB_WORKSPACE"]
69+
h = os.environ["HASH"]
70+
port = int(os.environ.get("UNITY_PORT", "6400"))
71+
status = {
72+
"unity_port": port,
73+
"reloading": False,
74+
"reason": "ready",
75+
"seq": 1,
76+
"project_path": f"{ws}/TestProjects/UnityMCPTests/Assets",
77+
"project_name": "UnityMCPTests",
78+
"unity_version": "fake-0.0.0",
79+
"last_heartbeat": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
80+
"instance_id": f"UnityMCPTests@{h}",
81+
}
82+
status_path = pathlib.Path(ws) / ".unity-mcp" / f"unity-mcp-status-{h}.json"
83+
status_path.write_text(json.dumps(status, indent=2) + "\n", encoding="utf-8")
84+
# Also mirror to home registry dir to satisfy any default lookups
85+
home_dir = pathlib.Path.home() / ".unity-mcp"
86+
home_dir.mkdir(parents=True, exist_ok=True)
87+
home_path = home_dir / f"unity-mcp-status-{h}.json"
88+
home_path.write_text(json.dumps(status, indent=2) + "\n", encoding="utf-8")
89+
# Optional port registry (legacy helpers)
90+
port_file = home_dir / f"unity-mcp-port-{h}.json"
91+
port_file.write_text(json.dumps({"unity_port": port}) + "\n", encoding="utf-8")
92+
PY
93+
94+
cat > /tmp/fake-unity-listener.py <<'PY'
95+
import socket, struct, os
96+
host = "0.0.0.0"
97+
port = int(os.environ.get("UNITY_PORT", "6400"))
98+
handshake = b"UnityMCP Ready\nFRAMING=1\n\n"
99+
def recv_exact(conn, n):
100+
data = b""
101+
while len(data) < n:
102+
chunk = conn.recv(n - len(data))
103+
if not chunk:
104+
return None
105+
data += chunk
106+
return data
107+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
108+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
109+
s.bind((host, port))
110+
s.listen(5)
111+
while True:
112+
conn, _ = s.accept()
113+
try:
114+
conn.sendall(handshake)
115+
header = recv_exact(conn, 8)
116+
if not header:
117+
# Legacy probe path: expect plain "ping" and reply JSON pong
118+
data = conn.recv(512)
119+
if data == b"ping":
120+
conn.sendall(b'{"message":"pong"}')
121+
conn.close(); continue
122+
length = struct.unpack('>Q', header)[0]
123+
payload = recv_exact(conn, length) or b""
124+
if payload == b"ping":
125+
resp = b'{"message":"pong"}'
126+
conn.sendall(struct.pack('>Q', len(resp)) + resp)
127+
finally:
128+
conn.close()
129+
PY
130+
nohup python3 /tmp/fake-unity-listener.py >/tmp/fake-unity-listener.log 2>&1 &
131+
132+
# Give listener a moment to bind before verification
133+
sleep 1
134+
135+
# Self-check: handshake + framed ping/pong against fake listener
136+
python3 <<'PY'
137+
import socket, struct, time, os, sys
138+
port = int(os.environ.get("UNITY_PORT", "6400"))
139+
s = socket.create_connection(("127.0.0.1", port), timeout=2)
140+
s.settimeout(2)
141+
handshake = s.recv(512)
142+
if b"FRAMING=1" not in handshake:
143+
print("::error::Fake listener handshake missing FRAMING=1")
144+
sys.exit(1)
145+
payload = b"ping"
146+
s.sendall(struct.pack(">Q", len(payload)) + payload)
147+
header = s.recv(8)
148+
if len(header) != 8:
149+
print("::error::Fake listener missing pong header")
150+
sys.exit(1)
151+
length = struct.unpack(">Q", header)[0]
152+
resp = s.recv(length)
153+
if b"pong" not in resp:
154+
print(f"::error::Fake listener pong mismatch: {resp!r}")
155+
sys.exit(1)
156+
s.close()
157+
PY
158+
159+
chmod -R a+rwx "$GITHUB_WORKSPACE/.unity-mcp"
160+
161+
- name: Write MCP config (.claude/mcp.json)
162+
run: |
163+
set -eux
164+
mkdir -p .claude
165+
python - <<'PY'
166+
import json
167+
import os
168+
from pathlib import Path
169+
170+
workspace = os.environ["GITHUB_WORKSPACE"]
171+
default_inst = os.environ.get("UNITY_MCP_DEFAULT_INSTANCE", "").strip()
172+
173+
cfg = {
174+
"mcpServers": {
175+
"unity": {
176+
"command": "uv",
177+
"args": [
178+
"run",
179+
"--active",
180+
"--directory",
181+
"Server",
182+
"mcp-for-unity",
183+
"--transport",
184+
"stdio",
185+
"--status-dir",
186+
f"{workspace}/.unity-mcp",
187+
],
188+
"transport": {"type": "stdio"},
189+
"env": {
190+
"PYTHONUNBUFFERED": "1",
191+
"MCP_LOG_LEVEL": "debug",
192+
"UNITY_PROJECT_ROOT": f"{workspace}/TestProjects/UnityMCPTests",
193+
"UNITY_MCP_STATUS_DIR": f"{workspace}/.unity-mcp",
194+
"UNITY_MCP_HOST": "127.0.0.1",
195+
"UNITY_MCP_SKIP_STARTUP_CONNECT": "1",
196+
},
197+
}
198+
}
199+
}
200+
201+
if default_inst:
202+
unity = cfg["mcpServers"]["unity"]
203+
unity["env"]["UNITY_MCP_DEFAULT_INSTANCE"] = default_inst
204+
if "--default-instance" not in unity["args"]:
205+
unity["args"] += ["--default-instance", default_inst]
206+
207+
path = Path(".claude/mcp.json")
208+
path.write_text(json.dumps(cfg, indent=2) + "\n")
209+
print(f"Wrote {path} (UNITY_MCP_DEFAULT_INSTANCE={default_inst or 'unset'})")
210+
PY
211+
212+
- name: Pin Claude tool permissions (.claude/settings.json)
213+
run: |
214+
set -eux
215+
mkdir -p .claude
216+
cat > .claude/settings.json <<'JSON'
217+
{
218+
"permissions": {
219+
"allow": [
220+
"mcp__unity",
221+
"Edit(reports/**)",
222+
"MultiEdit(reports/**)"
223+
],
224+
"deny": [
225+
"Bash",
226+
"WebFetch",
227+
"WebSearch",
228+
"Task",
229+
"TodoWrite",
230+
"NotebookEdit",
231+
"NotebookRead"
232+
]
233+
}
234+
}
235+
JSON
236+
237+
- name: Verify MCP fake Unity instance
238+
env:
239+
PYTHONUNBUFFERED: "1"
240+
MCP_LOG_LEVEL: debug
241+
UNITY_PROJECT_ROOT: ${{ github.workspace }}/TestProjects/UnityMCPTests
242+
UNITY_MCP_STATUS_DIR: ${{ github.workspace }}/.unity-mcp
243+
UNITY_MCP_HOST: 127.0.0.1
244+
UNITY_MCP_DEFAULT_INSTANCE: ${{ env.UNITY_MCP_DEFAULT_INSTANCE }}
245+
run: |
246+
set -euxo pipefail
247+
ls -la "$UNITY_MCP_STATUS_DIR"
248+
jq -r . "$UNITY_MCP_STATUS_DIR"/unity-mcp-status-*.json
249+
echo "--- Listener process ---"
250+
pgrep -fl fake-unity-listener || true
251+
echo "--- Socket listeners ---"
252+
ss -ltnp || true
253+
echo "--- Fake listener log ---"
254+
cat /tmp/fake-unity-listener.log || true
255+
256+
# Extra probe from this step
257+
python3 <<'PY'
258+
import socket, struct, sys, os
259+
port = int(os.environ.get("UNITY_PORT", "6400"))
260+
s = socket.create_connection(("127.0.0.1", port), timeout=2)
261+
s.settimeout(2)
262+
hs = s.recv(512)
263+
print("probe-handshake:", hs)
264+
s.sendall(struct.pack(">Q", 4) + b"ping")
265+
hdr = s.recv(8)
266+
print("probe-hdr-len:", len(hdr), "data:", hdr)
267+
if len(hdr) == 8:
268+
ln = struct.unpack(">Q", hdr)[0]
269+
resp = s.recv(ln)
270+
print("probe-resp:", resp)
271+
s.close()
272+
PY
273+
echo "--- Port discovery debug ---"
274+
python3 <<'PY'
275+
import glob, json, os, socket, struct
276+
from pathlib import Path
277+
status_dir = Path(os.environ["UNITY_MCP_STATUS_DIR"])
278+
files = sorted(glob.glob(str(status_dir / "unity-mcp-status-*.json")))
279+
print("status_dir", status_dir)
280+
print("files", files)
281+
for f in files:
282+
try:
283+
data = json.loads(Path(f).read_text())
284+
port = data.get("unity_port")
285+
print(f"file={f} port={port}")
286+
if isinstance(port, int):
287+
s = socket.create_connection(("127.0.0.1", port), timeout=2)
288+
hs = s.recv(512)
289+
s.sendall(struct.pack(">Q", 4) + b"ping")
290+
hdr = s.recv(8)
291+
resp = s.recv(struct.unpack(">Q", hdr)[0]) if len(hdr) == 8 else b""
292+
print("probe", f, "hs_ok", b"FRAMING=1" in hs, "hdr_len", len(hdr), "resp", resp)
293+
s.close()
294+
except Exception as e:
295+
print("probe error", f, e)
296+
PY
297+
298+
echo "--- PortDiscovery.discover_all_unity_instances ---"
299+
python3 - <<'PY'
300+
from transport.legacy.port_discovery import PortDiscovery
301+
inst = PortDiscovery.discover_all_unity_instances()
302+
print(inst)
303+
print("try_probe_direct", PortDiscovery._try_probe_unity_mcp(6400))
304+
print("discover_unity_port", PortDiscovery.discover_unity_port())
305+
PY
306+
307+
python - <<'PY'
308+
import json
309+
import subprocess
310+
cmd = [
311+
"uv", "run", "--active", "--directory", "Server", "python", "-c",
312+
"from transport.legacy.stdio_port_registry import stdio_port_registry; "
313+
"inst = stdio_port_registry.get_instances(force_refresh=True); "
314+
"import json; print(json.dumps([{'id':i.id,'port':i.port} for i in inst]))"
315+
]
316+
result = subprocess.run(cmd, capture_output=True, text=True)
317+
print(result.stdout.strip())
318+
if result.returncode != 0:
319+
print(result.stderr)
320+
raise SystemExit(1)
321+
try:
322+
data = json.loads(result.stdout.strip() or "[]")
323+
if not data:
324+
print("::error::No Unity instances discovered by MCP registry")
325+
raise SystemExit(1)
326+
except Exception as e:
327+
print(f"::error::Failed to parse instances: {e}")
328+
raise SystemExit(1)
329+
PY
330+
331+
- name: Claude smoke (fake Unity)
332+
uses: anthropics/claude-code-base-action@beta
333+
if: steps.detect.outputs.anthropic_ok == 'true'
334+
continue-on-error: true
335+
env:
336+
UNITY_MCP_DEFAULT_INSTANCE: ${{ env.UNITY_MCP_DEFAULT_INSTANCE }}
337+
with:
338+
use_node_cache: false
339+
prompt: |
340+
You are running against a fake Unity MCP instance for connectivity debug.
341+
- Call one unity tool (any) or report the registry instances you see.
342+
- If tools fail to connect, print the exact error and stop.
343+
- Do not write or edit files.
344+
mcp_config: .claude/mcp.json
345+
settings: .claude/settings.json
346+
allowed_tools: "mcp__unity"
347+
disallowed_tools: "Edit(reports/**),MultiEdit(reports/**),Bash,WebFetch,WebSearch,Task,TodoWrite,NotebookEdit,NotebookRead"
348+
max_turns: 1
349+
timeout_minutes: "5"
350+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
351+
352+
- name: Debug MCP server startup (after Claude)
353+
if: always()
354+
run: |
355+
set -eux
356+
echo "=== MCP Server Startup Debug Log ==="
357+
cat "$GITHUB_WORKSPACE/.unity-mcp/mcp-server-startup-debug.log" 2>/dev/null || echo "(no debug log found)"
358+
echo ""
359+
echo "=== Fake listener log ==="
360+
cat /tmp/fake-unity-listener.log 2>/dev/null || echo "(no listener log)"
361+

MCPForUnity/Editor/McpCiBoot.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System;
2+
using MCPForUnity.Editor.Constants;
3+
using MCPForUnity.Editor.Services.Transport.Transports;
4+
using UnityEditor;
5+
6+
namespace MCPForUnity.Editor
7+
{
8+
public static class McpCiBoot
9+
{
10+
public static void StartStdioForCi()
11+
{
12+
try
13+
{
14+
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, false);
15+
}
16+
catch { /* ignore */ }
17+
18+
StdioBridgeHost.StartAutoConnect();
19+
}
20+
}
21+
}
22+

MCPForUnity/Editor/McpCiBoot.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)