Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4881453
Support Unity 6 memory snapshot capture API
DLSinnocence May 14, 2026
d9431c5
Guard memory snapshot overload invocation
DLSinnocence May 14, 2026
21953a0
fix: resolve unqualified generic names in unity_reflect; fix URP/ProB…
Scriptwonder Jun 7, 2026
7b73f3e
Merge remote-tracking branch 'upstream/beta' into chore/triage-quickw…
Scriptwonder Jun 7, 2026
eb9a4d6
Merge remote-tracking branch 'upstream/beta' into chore/triage-quickw…
Scriptwonder Jun 14, 2026
d109211
fix(server): headless local HTTP server launch with per-port logs and…
Scriptwonder Jun 14, 2026
afa447e
fix(client): configure Kilo Code with its kilo.jsonc MCP format (#1120)
Scriptwonder Jun 15, 2026
89f27f1
test(e2e): add headless bridge harness and deterministic no-LLM CI smoke
Scriptwonder Jun 14, 2026
555158d
test(server): enforce tool/test symmetry with a shrink-only guard
Scriptwonder Jun 14, 2026
614382c
chore(server): sync uv.lock to package version 9.7.1
Scriptwonder Jun 14, 2026
0c2edf2
chore: ignore local .mcp.json dev config
Scriptwonder Jun 15, 2026
60f43e0
chore(tests): add missing EditMode test .meta sidecars; ignore test-p…
Scriptwonder Jun 15, 2026
86d6f7a
fix(harness): address Copilot/CodeRabbit review nits
Scriptwonder Jun 15, 2026
34d1498
fix(harness): reject invalid --legs values at the CLI (CodeRabbit)
Scriptwonder Jun 15, 2026
142760c
Merge pull request #1201 from Scriptwonder/chore/triage-quickwins-and…
Scriptwonder Jun 15, 2026
2ccebf0
chore: update Unity package to beta version 9.7.2-beta.12
actions-user Jun 15, 2026
b5ca356
Merge pull request #1202 from CoplayDev/beta-version-9.7.2-beta.12-27…
github-actions[bot] Jun 15, 2026
5699560
Merge pull request #1125 from DLSinnocence/fix/memory-profiler-coremo…
Scriptwonder Jun 15, 2026
28f2213
chore: update Unity package to beta version 9.7.2-beta.13
actions-user Jun 15, 2026
6bbf68e
Merge pull request #1203 from CoplayDev/beta-version-9.7.2-beta.13-27…
github-actions[bot] Jun 15, 2026
39e3e97
chore: bump version to 9.7.3
actions-user Jun 15, 2026
85c101f
Merge pull request #1204 from CoplayDev/release/v9.7.3
github-actions[bot] Jun 15, 2026
3b53d86
chore: sync main (v9.7.3) into beta
actions-user Jun 15, 2026
d3c399d
chore: set beta version to 9.7.4-beta.1 after release v9.7.3
actions-user Jun 15, 2026
dccecd6
Merge pull request #1205 from CoplayDev/sync/main-v9.7.3-into-beta-27…
github-actions[bot] Jun 15, 2026
8a609a3
fix(transport): debounce orphan-session teardown so server restarts d…
dsarno Jun 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 194 additions & 0 deletions .github/workflows/e2e-bridge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
name: E2E Bridge Smoke (deterministic, no LLM)

# Boots a headless Unity Editor, starts the Python MCP server's wire path, and
# drives a fixed sequence of real tool calls with exact assertions
# (Server/tests/e2e/bridge_smoke.py). Unlike claude-nl-suite.yml this needs
# NO Anthropic API key -- it is deterministic and cheap, so it can gate PRs and
# releases. It still needs Unity license secrets to boot the Editor.

on:
workflow_dispatch:
pull_request:
paths:
- "MCPForUnity/Editor/**"
- "MCPForUnity/Runtime/**"
- "Server/src/**"
- "Server/tests/e2e/**"
- "tools/local_harness.py"
- ".github/workflows/e2e-bridge.yml"

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
UNITY_IMAGE: unityci/editor:ubuntu-2021.3.45f2-linux-il2cpp-3

jobs:
e2e-bridge:
runs-on: ubuntu-24.04
timeout-minutes: 40
steps:
- name: Detect Unity license secrets
id: detect
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
run: |
set -e
if [ -n "$UNITY_LICENSE" ] || { [ -n "$UNITY_EMAIL" ] && [ -n "$UNITY_PASSWORD" ] && [ -n "$UNITY_SERIAL" ]; }; then
echo "unity_ok=true" >> "$GITHUB_OUTPUT"
else
echo "unity_ok=false" >> "$GITHUB_OUTPUT"
echo "::warning::Unity license secrets absent; E2E bridge smoke will be skipped (not failed)."
fi

- uses: actions/checkout@v4
if: steps.detect.outputs.unity_ok == 'true'
with:
fetch-depth: 0

- uses: astral-sh/setup-uv@v4
if: steps.detect.outputs.unity_ok == 'true'
with:
python-version: "3.11"

- name: Install MCP server
if: steps.detect.outputs.unity_ok == 'true'
run: |
set -eux
uv venv
echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> "$GITHUB_ENV"
echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH"
uv pip install -e Server

# --- License staging (mirrors claude-nl-suite.yml) ---
- name: Decide license sources
if: steps.detect.outputs.unity_ok == 'true'
id: lic
shell: bash
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
run: |
set -eu
use_ulf=false; use_ebl=false
[[ -n "${UNITY_LICENSE:-}" ]] && use_ulf=true
[[ -n "${UNITY_EMAIL:-}" && -n "${UNITY_PASSWORD:-}" && -n "${UNITY_SERIAL:-}" ]] && use_ebl=true
echo "use_ulf=$use_ulf" >> "$GITHUB_OUTPUT"
echo "use_ebl=$use_ebl" >> "$GITHUB_OUTPUT"

- name: Stage Unity .ulf license (from secret)
if: steps.detect.outputs.unity_ok == 'true' && steps.lic.outputs.use_ulf == 'true'
id: ulf
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
shell: bash
run: |
set -eu
mkdir -p "$RUNNER_TEMP/unity-license-ulf" "$RUNNER_TEMP/unity-local/Unity"
f="$RUNNER_TEMP/unity-license-ulf/Unity_lic.ulf"
if printf "%s" "$UNITY_LICENSE" | base64 -d - >/dev/null 2>&1; then
printf "%s" "$UNITY_LICENSE" | base64 -d - > "$f"
else
printf "%s" "$UNITY_LICENSE" > "$f"
fi
chmod 600 "$f" || true
if grep -qi '<Signature>' "$f"; then
cp -f "$f" "$RUNNER_TEMP/unity-local/Unity/Unity_lic.ulf"
echo "ok=true" >> "$GITHUB_OUTPUT"
else
echo "ok=false" >> "$GITHUB_OUTPUT"
fi

- name: Activate Unity (EBL via container)
if: steps.detect.outputs.unity_ok == 'true' && steps.lic.outputs.use_ebl == 'true'
shell: bash
env:
UNITY_IMAGE: ${{ env.UNITY_IMAGE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/unity-config" "$RUNNER_TEMP/unity-local"
docker run --rm --network host \
-e HOME=/root -e UNITY_EMAIL -e UNITY_PASSWORD -e UNITY_SERIAL \
-v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \
-v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \
"$UNITY_IMAGE" bash -lc '
set -euxo pipefail
/opt/unity/Editor/Unity -batchmode -nographics -logFile - \
-username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" -serial "$UNITY_SERIAL" -quit || true
'

- name: Warm up project (import Library once)
if: steps.detect.outputs.unity_ok == 'true'
shell: bash
env:
UNITY_IMAGE: ${{ env.UNITY_IMAGE }}
ULF_OK: ${{ steps.ulf.outputs.ok }}
run: |
set -euxo pipefail
manual_args=()
if [[ "${ULF_OK:-false}" == "true" ]]; then
manual_args=(-manualLicenseFile "/root/.local/share/unity3d/Unity/Unity_lic.ulf")
fi
docker run --rm --network host \
-e HOME=/root \
-v "${{ github.workspace }}:${{ github.workspace }}" -w "${{ github.workspace }}" \
-v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \
-v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \
-v "$RUNNER_TEMP/unity-cache:/root/.cache/unity3d" \
"$UNITY_IMAGE" /opt/unity/Editor/Unity -batchmode -nographics -logFile - \
-projectPath "${{ github.workspace }}/TestProjects/UnityMCPTests" \
"${manual_args[@]}" -quit

- name: Clean old MCP status
if: steps.detect.outputs.unity_ok == 'true'
run: |
set -eux
mkdir -p "$GITHUB_WORKSPACE/.unity-mcp"
rm -f "$GITHUB_WORKSPACE/.unity-mcp"/unity-mcp-status-*.json || true

- name: Run headless bridge harness (boot + wait + smoke/editmode/playmode)
if: steps.detect.outputs.unity_ok == 'true'
shell: bash
env:
UNITY_IMAGE: ${{ env.UNITY_IMAGE }}
ULF_OK: ${{ steps.ulf.outputs.ok }}
run: |
set -euxo pipefail
# In --ci mode the harness drives the DockerLauncher: it runs the same
# docker container (repo .unity-mcp status dir, docker liveness/teardown,
# log redaction), waits on the status file, derives the instance, then
# runs the smoke + EditMode + PlayMode legs over the bridge.
license_args=()
if [[ "${ULF_OK:-false}" == "true" ]]; then
license_args=(--editor-arg -manualLicenseFile \
--editor-arg "/root/.local/share/unity3d/Unity/Unity_lic.ulf")
fi
python3 tools/local_harness.py --ci \
--legs smoke,editmode,playmode \
--project-path TestProjects/UnityMCPTests \
--reports reports \
"${license_args[@]}"

- name: Unity logs on failure
if: failure() && steps.detect.outputs.unity_ok == 'true'
run: docker logs unity-mcp --tail 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig' || true

- name: Upload E2E report
if: always() && steps.detect.outputs.unity_ok == 'true'
uses: actions/upload-artifact@v4
with:
name: e2e-bridge-report
path: reports/junit-*.xml
if-no-files-found: ignore
7 changes: 7 additions & 0 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ on:
branches-ignore: [beta, main]
paths:
- Server/**
- tools/**
- .github/workflows/python-tests.yml
pull_request:
branches: [main, beta]
paths:
- Server/**
- tools/**
- .github/workflows/python-tests.yml
workflow_dispatch: {}
workflow_call:
Expand Down Expand Up @@ -53,6 +55,11 @@ jobs:
cd Server
uv run pytest tests/ -v --tb=short --cov --cov-report=xml --cov-report=html --cov-report=term

- name: Run local harness unit tests (hermetic, no Unity)
run: |
cd Server
uv run python -m pytest "$GITHUB_WORKSPACE/tools/tests/" -v --tb=short

- name: Upload coverage reports
uses: codecov/codecov-action@v4
if: always()
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,4 @@ tools/.unity-check-logs/

# Ignore the .claude directory, since it might contain local/project-level setting such as deny and allowlist.
/.claude
.mcp.json
11 changes: 11 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,17 @@ tools/check-unity-versions.sh # compile-only across installed Unity Hu
tools/check-unity-versions.sh --full # full EditMode test run
```

#### Local headless test harness
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`):

```bash
python tools/local_harness.py
```

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).

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).

### Local Development
1. Set **Server Source Override** in MCP for Unity Advanced Settings to your local `Server/` path
2. Enable **Dev Mode** checkbox to force fresh installs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public ClineConfigurator() : base(new McpClient
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
HttpTypeValue = "streamableHttp",
DefaultUnityFields = { { "disabled", false }, { "autoApprove", new object[] { } } }
})
{ }
Expand Down
27 changes: 18 additions & 9 deletions MCPForUnity/Editor/Clients/Configurators/KiloCodeConfigurator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,29 @@ public class KiloCodeConfigurator : JsonFileMcpConfigurator
public KiloCodeConfigurator() : base(new McpClient
{
name = "Kilo Code",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "globalStorage", "kilocode.kilo-code", "settings", "mcp_settings.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "globalStorage", "kilocode.kilo-code", "settings", "mcp_settings.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "globalStorage", "kilocode.kilo-code", "settings", "mcp_settings.json"),
IsVsCodeLayout = false
// Kilo Code v7.0.33+ moved MCP config out of the VS Code extension's
// globalStorage/mcp_settings.json to a CLI-style kilo.jsonc under ~/.config/kilo.
// The new schema (https://app.kilo.ai/config.json) uses an "mcp" container,
// type:"remote" for HTTP servers, type:"local" for stdio, and an "enabled" flag.
// ~/.config/kilo/kilo.jsonc on every OS (UserProfile resolves to C:\Users\<user> on Windows).
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kilo", "kilo.jsonc"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kilo", "kilo.jsonc"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kilo", "kilo.jsonc"),
IsVsCodeLayout = false,
ServerContainerKey = "mcp",
HttpTypeValue = "remote",
StdioTypeValue = "local",
SchemaUrl = "https://app.kilo.ai/config.json",
DefaultUnityFields = { { "enabled", true } }
})
{ }

public override IList<string> GetInstallationSteps() => new List<string>
{
"Install Kilo Code extension in VS Code",
"Open Kilo Code settings (gear icon in sidebar)",
"Navigate to MCP Servers section and click 'Edit Global MCP Settings'\nOR open the config file at the path above",
"Paste the configuration JSON into the mcpServers object",
"Save and restart VS Code"
"Install or update Kilo Code (v7.0.33 or newer)",
"Open the Kilo Code MCP Servers view\nOR edit the config file at the path above (~/.config/kilo/kilo.jsonc)",
"Paste the configuration JSON into the \"mcp\" object",
"Save and restart Kilo Code"
};
}
}
15 changes: 10 additions & 5 deletions MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,12 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
JToken unityToken = null;
if (rootConfig != null)
{
string containerKey = string.IsNullOrEmpty(client.ServerContainerKey)
? "mcpServers" : client.ServerContainerKey;
unityToken = client.IsVsCodeLayout
? rootConfig["servers"]?["unityMCP"]
?? rootConfig["mcp"]?["servers"]?["unityMCP"]
: rootConfig["mcpServers"]?["unityMCP"];
: rootConfig[containerKey]?["unityMCP"];
}

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

/// <summary>
/// Removes the unityMCP entry from the client's JSON config (both VS Code-style
/// `servers` / `mcp.servers` layouts and the standard `mcpServers` layout). Leaves
/// the file in place so we don't clobber other servers the user has configured.
/// Removes the unityMCP entry from the client's JSON config (VS Code-style
/// `servers` / `mcp.servers` layouts, the standard `mcpServers` layout, or a
/// client-specific container such as Kilo's `mcp`). Leaves the file in place so we
/// don't clobber other servers the user has configured.
/// </summary>
public override void Unregister()
{
Expand Down Expand Up @@ -392,7 +395,9 @@ public override void Unregister()
}
else
{
if ((root["mcpServers"] as JObject)?.Remove("unityMCP") == true) removed = true;
string containerKey = string.IsNullOrEmpty(client.ServerContainerKey)
? "mcpServers" : client.ServerContainerKey;
if ((root[containerKey] as JObject)?.Remove("unityMCP") == true) removed = true;
}

if (removed)
Expand Down
1 change: 1 addition & 0 deletions MCPForUnity/Editor/Constants/EditorPrefKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ internal static class EditorPrefKeys
internal const string ApiKey = "MCPForUnity.ApiKey";

internal const string AutoStartOnLoad = "MCPForUnity.AutoStartOnLoad";
internal const string HttpServerLaunchConfirmed = "MCPForUnity.HttpServerLaunchConfirmed";
internal const string BatchExecuteMaxCommands = "MCPForUnity.BatchExecute.MaxCommands";
internal const string LogRecordEnabled = "MCPForUnity.LogRecordEnabled";

Expand Down
Loading
Loading