Skip to content

Commit 85c101f

Browse files
Merge pull request #1204 from CoplayDev/release/v9.7.3
chore: bump version to 9.7.3
2 parents 78ee541 + 39e3e97 commit 85c101f

69 files changed

Lines changed: 4739 additions & 348 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/e2e-bridge.yml

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
name: E2E Bridge Smoke (deterministic, no LLM)
2+
3+
# Boots a headless Unity Editor, starts the Python MCP server's wire path, and
4+
# drives a fixed sequence of real tool calls with exact assertions
5+
# (Server/tests/e2e/bridge_smoke.py). Unlike claude-nl-suite.yml this needs
6+
# NO Anthropic API key -- it is deterministic and cheap, so it can gate PRs and
7+
# releases. It still needs Unity license secrets to boot the Editor.
8+
9+
on:
10+
workflow_dispatch:
11+
pull_request:
12+
paths:
13+
- "MCPForUnity/Editor/**"
14+
- "MCPForUnity/Runtime/**"
15+
- "Server/src/**"
16+
- "Server/tests/e2e/**"
17+
- "tools/local_harness.py"
18+
- ".github/workflows/e2e-bridge.yml"
19+
20+
permissions:
21+
contents: read
22+
23+
concurrency:
24+
group: ${{ github.workflow }}-${{ github.ref }}
25+
cancel-in-progress: true
26+
27+
env:
28+
UNITY_IMAGE: unityci/editor:ubuntu-2021.3.45f2-linux-il2cpp-3
29+
30+
jobs:
31+
e2e-bridge:
32+
runs-on: ubuntu-24.04
33+
timeout-minutes: 40
34+
steps:
35+
- name: Detect Unity license secrets
36+
id: detect
37+
env:
38+
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
39+
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
40+
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
41+
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
42+
run: |
43+
set -e
44+
if [ -n "$UNITY_LICENSE" ] || { [ -n "$UNITY_EMAIL" ] && [ -n "$UNITY_PASSWORD" ] && [ -n "$UNITY_SERIAL" ]; }; then
45+
echo "unity_ok=true" >> "$GITHUB_OUTPUT"
46+
else
47+
echo "unity_ok=false" >> "$GITHUB_OUTPUT"
48+
echo "::warning::Unity license secrets absent; E2E bridge smoke will be skipped (not failed)."
49+
fi
50+
51+
- uses: actions/checkout@v4
52+
if: steps.detect.outputs.unity_ok == 'true'
53+
with:
54+
fetch-depth: 0
55+
56+
- uses: astral-sh/setup-uv@v4
57+
if: steps.detect.outputs.unity_ok == 'true'
58+
with:
59+
python-version: "3.11"
60+
61+
- name: Install MCP server
62+
if: steps.detect.outputs.unity_ok == 'true'
63+
run: |
64+
set -eux
65+
uv venv
66+
echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> "$GITHUB_ENV"
67+
echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH"
68+
uv pip install -e Server
69+
70+
# --- License staging (mirrors claude-nl-suite.yml) ---
71+
- name: Decide license sources
72+
if: steps.detect.outputs.unity_ok == 'true'
73+
id: lic
74+
shell: bash
75+
env:
76+
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
77+
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
78+
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
79+
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
80+
run: |
81+
set -eu
82+
use_ulf=false; use_ebl=false
83+
[[ -n "${UNITY_LICENSE:-}" ]] && use_ulf=true
84+
[[ -n "${UNITY_EMAIL:-}" && -n "${UNITY_PASSWORD:-}" && -n "${UNITY_SERIAL:-}" ]] && use_ebl=true
85+
echo "use_ulf=$use_ulf" >> "$GITHUB_OUTPUT"
86+
echo "use_ebl=$use_ebl" >> "$GITHUB_OUTPUT"
87+
88+
- name: Stage Unity .ulf license (from secret)
89+
if: steps.detect.outputs.unity_ok == 'true' && steps.lic.outputs.use_ulf == 'true'
90+
id: ulf
91+
env:
92+
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
93+
shell: bash
94+
run: |
95+
set -eu
96+
mkdir -p "$RUNNER_TEMP/unity-license-ulf" "$RUNNER_TEMP/unity-local/Unity"
97+
f="$RUNNER_TEMP/unity-license-ulf/Unity_lic.ulf"
98+
if printf "%s" "$UNITY_LICENSE" | base64 -d - >/dev/null 2>&1; then
99+
printf "%s" "$UNITY_LICENSE" | base64 -d - > "$f"
100+
else
101+
printf "%s" "$UNITY_LICENSE" > "$f"
102+
fi
103+
chmod 600 "$f" || true
104+
if grep -qi '<Signature>' "$f"; then
105+
cp -f "$f" "$RUNNER_TEMP/unity-local/Unity/Unity_lic.ulf"
106+
echo "ok=true" >> "$GITHUB_OUTPUT"
107+
else
108+
echo "ok=false" >> "$GITHUB_OUTPUT"
109+
fi
110+
111+
- name: Activate Unity (EBL via container)
112+
if: steps.detect.outputs.unity_ok == 'true' && steps.lic.outputs.use_ebl == 'true'
113+
shell: bash
114+
env:
115+
UNITY_IMAGE: ${{ env.UNITY_IMAGE }}
116+
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
117+
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
118+
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
119+
run: |
120+
set -euo pipefail
121+
mkdir -p "$RUNNER_TEMP/unity-config" "$RUNNER_TEMP/unity-local"
122+
docker run --rm --network host \
123+
-e HOME=/root -e UNITY_EMAIL -e UNITY_PASSWORD -e UNITY_SERIAL \
124+
-v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \
125+
-v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \
126+
"$UNITY_IMAGE" bash -lc '
127+
set -euxo pipefail
128+
/opt/unity/Editor/Unity -batchmode -nographics -logFile - \
129+
-username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" -serial "$UNITY_SERIAL" -quit || true
130+
'
131+
132+
- name: Warm up project (import Library once)
133+
if: steps.detect.outputs.unity_ok == 'true'
134+
shell: bash
135+
env:
136+
UNITY_IMAGE: ${{ env.UNITY_IMAGE }}
137+
ULF_OK: ${{ steps.ulf.outputs.ok }}
138+
run: |
139+
set -euxo pipefail
140+
manual_args=()
141+
if [[ "${ULF_OK:-false}" == "true" ]]; then
142+
manual_args=(-manualLicenseFile "/root/.local/share/unity3d/Unity/Unity_lic.ulf")
143+
fi
144+
docker run --rm --network host \
145+
-e HOME=/root \
146+
-v "${{ github.workspace }}:${{ github.workspace }}" -w "${{ github.workspace }}" \
147+
-v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \
148+
-v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \
149+
-v "$RUNNER_TEMP/unity-cache:/root/.cache/unity3d" \
150+
"$UNITY_IMAGE" /opt/unity/Editor/Unity -batchmode -nographics -logFile - \
151+
-projectPath "${{ github.workspace }}/TestProjects/UnityMCPTests" \
152+
"${manual_args[@]}" -quit
153+
154+
- name: Clean old MCP status
155+
if: steps.detect.outputs.unity_ok == 'true'
156+
run: |
157+
set -eux
158+
mkdir -p "$GITHUB_WORKSPACE/.unity-mcp"
159+
rm -f "$GITHUB_WORKSPACE/.unity-mcp"/unity-mcp-status-*.json || true
160+
161+
- name: Run headless bridge harness (boot + wait + smoke/editmode/playmode)
162+
if: steps.detect.outputs.unity_ok == 'true'
163+
shell: bash
164+
env:
165+
UNITY_IMAGE: ${{ env.UNITY_IMAGE }}
166+
ULF_OK: ${{ steps.ulf.outputs.ok }}
167+
run: |
168+
set -euxo pipefail
169+
# In --ci mode the harness drives the DockerLauncher: it runs the same
170+
# docker container (repo .unity-mcp status dir, docker liveness/teardown,
171+
# log redaction), waits on the status file, derives the instance, then
172+
# runs the smoke + EditMode + PlayMode legs over the bridge.
173+
license_args=()
174+
if [[ "${ULF_OK:-false}" == "true" ]]; then
175+
license_args=(--editor-arg -manualLicenseFile \
176+
--editor-arg "/root/.local/share/unity3d/Unity/Unity_lic.ulf")
177+
fi
178+
python3 tools/local_harness.py --ci \
179+
--legs smoke,editmode,playmode \
180+
--project-path TestProjects/UnityMCPTests \
181+
--reports reports \
182+
"${license_args[@]}"
183+
184+
- name: Unity logs on failure
185+
if: failure() && steps.detect.outputs.unity_ok == 'true'
186+
run: docker logs unity-mcp --tail 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig' || true
187+
188+
- name: Upload E2E report
189+
if: always() && steps.detect.outputs.unity_ok == 'true'
190+
uses: actions/upload-artifact@v4
191+
with:
192+
name: e2e-bridge-report
193+
path: reports/junit-*.xml
194+
if-no-files-found: ignore

.github/workflows/python-tests.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ on:
99
branches-ignore: [beta, main]
1010
paths:
1111
- Server/**
12+
- tools/**
1213
- .github/workflows/python-tests.yml
1314
pull_request:
1415
branches: [main, beta]
1516
paths:
1617
- Server/**
18+
- tools/**
1719
- .github/workflows/python-tests.yml
1820
workflow_dispatch: {}
1921
workflow_call:
@@ -53,6 +55,11 @@ jobs:
5355
cd Server
5456
uv run pytest tests/ -v --tb=short --cov --cov-report=xml --cov-report=html --cov-report=term
5557
58+
- name: Run local harness unit tests (hermetic, no Unity)
59+
run: |
60+
cd Server
61+
uv run python -m pytest "$GITHUB_WORKSPACE/tools/tests/" -v --tb=short
62+
5663
- name: Upload coverage reports
5764
uses: codecov/codecov-action@v4
5865
if: always()

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,4 @@ tools/.unity-check-logs/
6868

6969
# Ignore the .claude directory, since it might contain local/project-level setting such as deny and allowlist.
7070
/.claude
71+
.mcp.json

CLAUDE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,17 @@ tools/check-unity-versions.sh # compile-only across installed Unity Hu
167167
tools/check-unity-versions.sh --full # full EditMode test run
168168
```
169169

170+
#### Local headless test harness
171+
One command boots a headless Hub-licensed Editor against `TestProjects/UnityMCPTests` and runs the smoke + EditMode + PlayMode legs over the bridge — the same entrypoint CI uses (`.github/workflows/e2e-bridge.yml`):
172+
173+
```bash
174+
python tools/local_harness.py
175+
```
176+
177+
Key flags: `--legs smoke,editmode,playmode` (subset to run), `--project-path` (target project, default `TestProjects/UnityMCPTests`), `--reuse` (attach to an already-resident bridge instead of booting one), `--keep-alive` (leave the Editor running after the legs), `--no-warmup` (skip the warm-up import phase).
178+
179+
Exit codes: `0` pass, `1` blocking-leg regression, `2` bridge unreachable / setup failure, `3` project does not compile, `4` no Unity license / Hub seat, `5` Editor binary/version not found. Requires a Hub-activated Editor locally (no ULF/serial).
180+
170181
### Local Development
171182
1. Set **Server Source Override** in MCP for Unity Advanced Settings to your local `Server/` path
172183
2. Enable **Dev Mode** checkbox to force fresh installs

MCPForUnity/Editor/Clients/Configurators/ClineConfigurator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public ClineConfigurator() : base(new McpClient
1313
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1414
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1515
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
16+
HttpTypeValue = "streamableHttp",
1617
DefaultUnityFields = { { "disabled", false }, { "autoApprove", new object[] { } } }
1718
})
1819
{ }

MCPForUnity/Editor/Clients/Configurators/KiloCodeConfigurator.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,29 @@ public class KiloCodeConfigurator : JsonFileMcpConfigurator
1010
public KiloCodeConfigurator() : base(new McpClient
1111
{
1212
name = "Kilo Code",
13-
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "globalStorage", "kilocode.kilo-code", "settings", "mcp_settings.json"),
14-
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "globalStorage", "kilocode.kilo-code", "settings", "mcp_settings.json"),
15-
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "globalStorage", "kilocode.kilo-code", "settings", "mcp_settings.json"),
16-
IsVsCodeLayout = false
13+
// Kilo Code v7.0.33+ moved MCP config out of the VS Code extension's
14+
// globalStorage/mcp_settings.json to a CLI-style kilo.jsonc under ~/.config/kilo.
15+
// The new schema (https://app.kilo.ai/config.json) uses an "mcp" container,
16+
// type:"remote" for HTTP servers, type:"local" for stdio, and an "enabled" flag.
17+
// ~/.config/kilo/kilo.jsonc on every OS (UserProfile resolves to C:\Users\<user> on Windows).
18+
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kilo", "kilo.jsonc"),
19+
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kilo", "kilo.jsonc"),
20+
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kilo", "kilo.jsonc"),
21+
IsVsCodeLayout = false,
22+
ServerContainerKey = "mcp",
23+
HttpTypeValue = "remote",
24+
StdioTypeValue = "local",
25+
SchemaUrl = "https://app.kilo.ai/config.json",
26+
DefaultUnityFields = { { "enabled", true } }
1727
})
1828
{ }
1929

2030
public override IList<string> GetInstallationSteps() => new List<string>
2131
{
22-
"Install Kilo Code extension in VS Code",
23-
"Open Kilo Code settings (gear icon in sidebar)",
24-
"Navigate to MCP Servers section and click 'Edit Global MCP Settings'\nOR open the config file at the path above",
25-
"Paste the configuration JSON into the mcpServers object",
26-
"Save and restart VS Code"
32+
"Install or update Kilo Code (v7.0.33 or newer)",
33+
"Open the Kilo Code MCP Servers view\nOR edit the config file at the path above (~/.config/kilo/kilo.jsonc)",
34+
"Paste the configuration JSON into the \"mcp\" object",
35+
"Save and restart Kilo Code"
2736
};
2837
}
2938
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using MCPForUnity.Editor.Models;
5+
6+
namespace MCPForUnity.Editor.Clients.Configurators
7+
{
8+
/// <summary>
9+
/// Kimi Code CLI MCP client configurator.
10+
/// Kimi Code uses a JSON-based configuration file with mcpServers section.
11+
/// Config path: ~/.kimi/mcp.json
12+
///
13+
/// Kimi Code supports both stdio (uvx) and HTTP transport modes.
14+
/// Default: stdio mode (works without Unity Editor for basic operations)
15+
/// HTTP mode: requires Unity Editor running with MCP HTTP server started
16+
/// </summary>
17+
public class KimiCodeConfigurator : JsonFileMcpConfigurator
18+
{
19+
public KimiCodeConfigurator() : base(new McpClient
20+
{
21+
name = "Kimi Code",
22+
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kimi", "mcp.json"),
23+
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kimi", "mcp.json"),
24+
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kimi", "mcp.json"),
25+
SupportsHttpTransport = true,
26+
})
27+
{ }
28+
29+
public override IList<string> GetInstallationSteps() => new List<string>
30+
{
31+
"Ensure Kimi Code CLI is installed (pip install kimi-cli or see https://github.com/MoonshotAI/kimi-cli)",
32+
"Click 'Auto Configure' to automatically add UnityMCP to ~/.kimi/mcp.json",
33+
"OR click 'Manual Setup' to copy the configuration JSON",
34+
"Open ~/.kimi/mcp.json and paste the configuration",
35+
"Save and restart Kimi Code CLI",
36+
"Use 'kimi mcp list' to verify Unity MCP is connected",
37+
"Note: For full functionality, open Unity Editor and start HTTP server"
38+
};
39+
}
40+
}

MCPForUnity/Editor/Clients/Configurators/KimiCodeConfigurator.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.

MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,12 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
179179
JToken unityToken = null;
180180
if (rootConfig != null)
181181
{
182+
string containerKey = string.IsNullOrEmpty(client.ServerContainerKey)
183+
? "mcpServers" : client.ServerContainerKey;
182184
unityToken = client.IsVsCodeLayout
183185
? rootConfig["servers"]?["unityMCP"]
184186
?? rootConfig["mcp"]?["servers"]?["unityMCP"]
185-
: rootConfig["mcpServers"]?["unityMCP"];
187+
: rootConfig[containerKey]?["unityMCP"];
186188
}
187189

188190
if (unityToken is JObject unityObj)
@@ -360,9 +362,10 @@ public override string GetConfigureActionLabel()
360362
=> client.status == McpStatus.Configured ? "Unregister" : "Configure";
361363

362364
/// <summary>
363-
/// Removes the unityMCP entry from the client's JSON config (both VS Code-style
364-
/// `servers` / `mcp.servers` layouts and the standard `mcpServers` layout). Leaves
365-
/// the file in place so we don't clobber other servers the user has configured.
365+
/// Removes the unityMCP entry from the client's JSON config (VS Code-style
366+
/// `servers` / `mcp.servers` layouts, the standard `mcpServers` layout, or a
367+
/// client-specific container such as Kilo's `mcp`). Leaves the file in place so we
368+
/// don't clobber other servers the user has configured.
366369
/// </summary>
367370
public override void Unregister()
368371
{
@@ -392,7 +395,9 @@ public override void Unregister()
392395
}
393396
else
394397
{
395-
if ((root["mcpServers"] as JObject)?.Remove("unityMCP") == true) removed = true;
398+
string containerKey = string.IsNullOrEmpty(client.ServerContainerKey)
399+
? "mcpServers" : client.ServerContainerKey;
400+
if ((root[containerKey] as JObject)?.Remove("unityMCP") == true) removed = true;
396401
}
397402

398403
if (removed)

MCPForUnity/Editor/Constants/EditorPrefKeys.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ internal static class EditorPrefKeys
6868
internal const string ApiKey = "MCPForUnity.ApiKey";
6969

7070
internal const string AutoStartOnLoad = "MCPForUnity.AutoStartOnLoad";
71+
internal const string HttpServerLaunchConfirmed = "MCPForUnity.HttpServerLaunchConfirmed";
7172
internal const string BatchExecuteMaxCommands = "MCPForUnity.BatchExecute.MaxCommands";
7273
internal const string LogRecordEnabled = "MCPForUnity.LogRecordEnabled";
7374

0 commit comments

Comments
 (0)