Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
7912891
chore: sync main (v10.1.0) into beta
actions-user Jul 13, 2026
64e61ee
chore: set beta version to 10.1.1-beta.1 after release v10.1.0
actions-user Jul 13, 2026
bd72241
Merge pull request #1271 from CoplayDev/sync/main-v10.1.0-into-beta-2…
github-actions[bot] Jul 13, 2026
d395363
docs: fix typos in v8 migration guide
anantsharmamc Jul 21, 2026
b954897
fix: redirect stdin from NUL when launching the server on Windows (#1…
Scriptwonder Jul 28, 2026
777e8a9
fix: trust the pipeline flag when a domain reload is deferred (#1276)
Scriptwonder Jul 28, 2026
4ff8ff8
fix: advertise Codex as stdio-only (#1193)
Scriptwonder Jul 28, 2026
82b1b73
fix: correct inverted run_command arguments in the camera CLI
Scriptwonder Jul 28, 2026
2b2ca8a
feat: add a clear_stuck escape hatch to run_tests (#1272)
Scriptwonder Jul 28, 2026
172d3e2
chore: refresh uv.lock to match the pyproject version
Scriptwonder Jul 28, 2026
aaef1df
test: report unavailable-pipeline graphics tests as Skipped, not Inco…
Scriptwonder Jul 28, 2026
a36c991
docs: regenerate tool reference for run_tests clear_stuck
Scriptwonder Jul 28, 2026
e2aacdf
fix: camera CLI discarded its output and ignored --format
Scriptwonder Jul 28, 2026
afc51b5
fix: force stdio in the Codex manual snippet, assert the exact transp…
Scriptwonder Jul 28, 2026
e8fe7fe
Merge pull request #1294 from Scriptwonder/test/graphics-assume-to-ig…
Scriptwonder Jul 28, 2026
5496c12
Merge pull request #1293 from Scriptwonder/fix/server-cli-run-tests-a…
Scriptwonder Jul 28, 2026
334f3c0
Merge pull request #1292 from Scriptwonder/fix/1193-codex-stdio-only
Scriptwonder Jul 28, 2026
70a96ec
Merge pull request #1290 from Scriptwonder/fix/1279-windows-headless-…
Scriptwonder Jul 28, 2026
94f192b
Merge pull request #1291 from Scriptwonder/fix/1276-deferred-reload-c…
Scriptwonder Jul 28, 2026
2d3b099
chore: update Unity package to beta version 10.1.1-beta.2
actions-user Jul 28, 2026
5fdf544
Merge pull request #1296 from CoplayDev/beta-version-10.1.1-beta.2-30…
github-actions[bot] Jul 28, 2026
fc70dda
Merge pull request #1281 from ananttheant/fix/typos-migration-v8-docs
Scriptwonder Jul 28, 2026
503d938
fix: make manage_gameobject component properties reachable on create
asavs Jul 28, 2026
835bfcd
fix: stop 34 tools forcing an approval prompt on every call (#1288)
Scriptwonder Jul 29, 2026
267b465
fix: restore HTTP transport for Codex (#1193)
Scriptwonder Jul 29, 2026
69267c5
fix(server): address resources by URI in agent-facing prose
KamilDev Aug 1, 2026
a0e489b
test: cover agent-facing markdown, drop dead type-hint guard
KamilDev Aug 1, 2026
5f8c806
Merge pull request #1305 from Scriptwonder/fix/1193-restore-codex-http
Scriptwonder Aug 2, 2026
c82502f
test: fail on duplicate tool registrations instead of silently collap…
Scriptwonder Aug 2, 2026
5aa8e66
chore: update Unity package to beta version 10.1.1-beta.3
actions-user Aug 2, 2026
fbc4894
Merge pull request #1306 from CoplayDev/beta-version-10.1.1-beta.3-30…
github-actions[bot] Aug 2, 2026
f7ef566
Merge pull request #1304 from Scriptwonder/fix/1288-tool-annotations
Scriptwonder Aug 2, 2026
aa0f872
chore: update Unity package to beta version 10.1.1-beta.4
actions-user Aug 2, 2026
ffd7ff7
Merge pull request #1307 from CoplayDev/beta-version-10.1.1-beta.4-30…
github-actions[bot] Aug 2, 2026
0b1f50d
ci: retire safe-to-test gate, make skipped Unity checks visible
Scriptwonder Aug 2, 2026
32428e8
Merge pull request #1302 from KamilDev/fix/resource-uri-prose
Scriptwonder Aug 2, 2026
25e5f7e
Merge pull request #1308 from Scriptwonder/fix/ci-fork-pr-signal
Scriptwonder Aug 2, 2026
d3810df
chore: update Unity package to beta version 10.1.1-beta.5
actions-user Aug 2, 2026
61c4122
Merge pull request #1309 from CoplayDev/beta-version-10.1.1-beta.5-30…
github-actions[bot] Aug 2, 2026
07e8680
Merge pull request #1298 from asavs/fix/gameobject-create-component-p…
Scriptwonder Aug 2, 2026
ee0bf57
chore: update Unity package to beta version 10.1.1-beta.6
actions-user Aug 2, 2026
c36b0e8
Merge pull request #1310 from CoplayDev/beta-version-10.1.1-beta.6-30…
github-actions[bot] Aug 2, 2026
1ad15ae
chore: bump version to 10.1.2
actions-user Aug 2, 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
12 changes: 11 additions & 1 deletion .github/workflows/e2e-bridge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,17 @@ jobs:
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)."
echo "::warning::E2E bridge smoke SKIPPED - no license secrets in scope (normal for fork PRs). This check is NOT a pass: nothing was booted or exercised."
# Every step below is gated on unity_ok, so the job reports a green check
# having run nothing at all. Say so plainly on the run page.
{
echo "## :warning: E2E bridge smoke was SKIPPED"
echo
echo "No Unity license secrets were in scope, so **no Editor was booted and no tool call was exercised**."
echo "The green check means the job exited cleanly - **not** that the bridge works."
echo
echo "GitHub withholds repository secrets from workflow runs triggered by a fork's pull request."
} >> "$GITHUB_STEP_SUMMARY"
fi

- uses: actions/checkout@v4
Expand Down
77 changes: 28 additions & 49 deletions .github/workflows/unity-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,23 @@ on:
# Same-repo PRs get a unity-tests status check on every open / push via this trigger
# (mirrors python-tests.yml). Fork PRs ALSO fire this trigger but run in the fork's
# context without secrets — the detect step downstream writes unity_ok=false and the
# job exits clean with a "missing license secrets" notice so the status check still
# appears. Maintainers apply 'safe-to-test' to invoke pull_request_target below for
# a real fork-PR test run.
# job reports a green check having compiled and tested nothing. That skip is stated
# loudly in the job's step summary so it is never mistaken for a pass.
#
# There is deliberately no pull_request_target trigger here. Running fork-authored
# C# through game-ci/unity-test-runner with UNITY_* secrets in scope is the classic
# "pwn request" shape — an [InitializeOnLoad] script in the PR is enough to read
# them. To test a fork PR, review the diff and push its branch into this repo; the
# push trigger above then runs the full suite in a genuinely trusted context.
pull_request:
branches: [main, beta]
paths:
- TestProjects/UnityMCPTests/**
- MCPForUnity/Editor/**
- MCPForUnity/Runtime/**
- .github/workflows/unity-tests.yml
# Fork PRs: maintainer applies the 'safe-to-test' label after reviewing
# the diff. The workflow runs with UNITY_LICENSE in scope against the
# PR's head SHA. Re-pushed commits do NOT auto-trigger — maintainer must
# remove and re-apply the label to re-run after additional review.
pull_request_target:
types: [labeled]
branches: [main, beta]
paths:
- TestProjects/UnityMCPTests/**
- MCPForUnity/Editor/**
- MCPForUnity/Runtime/**
- .github/workflows/unity-tests.yml

# Dedup runs for the same branch across push / pull_request / pull_request_target / workflow_call.
# Dedup runs for the same branch across push / pull_request / workflow_call.
# Same-repo PRs would otherwise fire both push (on the branch SHA) AND pull_request (on the PR);
# concurrency keeps only the newer in-flight run per branch.
concurrency:
Expand All @@ -61,23 +54,6 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
# Gate (mirrored by testAllModes below):
# - Always run for non-PR triggers (push / workflow_call / workflow_dispatch).
# - Fork PRs: require 'safe-to-test' to be applied (existing secret-safety gate);
# 'full-matrix' may be added on top to opt into the full 4-version matrix.
# - In-repo PRs: only re-run via pull_request_target when 'full-matrix' is the
# label that just fired (the push-event run already covered the default leg).
if: >
github.event_name != 'pull_request_target' ||
(
github.event.pull_request.head.repo.full_name != github.repository &&
contains(github.event.pull_request.labels.*.name, 'safe-to-test') &&
(github.event.label.name == 'safe-to-test' || github.event.label.name == 'full-matrix')
) ||
(
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.label.name == 'full-matrix'
)
outputs:
versions: ${{ steps.set.outputs.versions }}
steps:
Expand All @@ -97,12 +73,13 @@ jobs:
run: |
set -euo pipefail
# Full matrix on: beta push, workflow_call (release pipelines), workflow_dispatch,
# or any PR (pull_request OR pull_request_target) labeled with 'full-matrix'.
# or a PR carrying the 'full-matrix' label. Note the label is only read when the
# workflow fires, so applying it to an open PR takes effect on the next push.
# Default (single defaultVersion from tools/unity-versions.json) otherwise — fast PR feedback.
if [[ "$EVENT_NAME" == "workflow_dispatch" ]] || \
[[ "$EVENT_NAME" == "workflow_call" ]] || \
{ [[ "$EVENT_NAME" == "push" ]] && [[ "$GH_REF" == "refs/heads/beta" ]]; } || \
{ { [[ "$EVENT_NAME" == "pull_request" ]] || [[ "$EVENT_NAME" == "pull_request_target" ]]; } && [[ "$FULL_MATRIX_LABEL" == "true" ]]; }; then
{ [[ "$EVENT_NAME" == "pull_request" ]] && [[ "$FULL_MATRIX_LABEL" == "true" ]]; }; then
versions=$(jq -c '[.versions[].id]' tools/unity-versions.json)
echo "Trigger '$EVENT_NAME' on ref '$GH_REF' (full_matrix_label=$FULL_MATRIX_LABEL) → full matrix: $versions"
else
Expand All @@ -117,17 +94,6 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
if: >
github.event_name != 'pull_request_target' ||
(
github.event.pull_request.head.repo.full_name != github.repository &&
contains(github.event.pull_request.labels.*.name, 'safe-to-test') &&
(github.event.label.name == 'safe-to-test' || github.event.label.name == 'full-matrix')
) ||
(
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.label.name == 'full-matrix'
)
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -159,10 +125,23 @@ jobs:
echo "unity_ok=false" >> "$GITHUB_OUTPUT"
fi

# A skipped run and a real pass both report a green check, because step-level
# `if:` conditions produce step-conclusion `skipped`, which contributes nothing
# to the job conclusion. Make the difference unmissable on the run page so a
# reviewer never reads this green check as "the code compiled".
- name: Skip Unity tests (missing license secrets)
if: steps.detect.outputs.unity_ok != 'true'
run: |
echo "Unity license secrets missing; skipping Unity tests."
echo "::warning::Unity tests SKIPPED - no license secrets in scope (normal for fork PRs). This check is NOT a pass: nothing was compiled or tested."
{
echo "## :warning: Unity tests were SKIPPED"
echo
echo "No Unity license secrets were in scope for this run, so **no C# was compiled and no test was executed**."
echo "The green check means the job exited cleanly - **not** that this code works."
echo
echo "GitHub withholds repository secrets from workflow runs triggered by a fork's pull request."
echo "To get real signal, a maintainer must run the suite against this code from a trusted context."
} >> "$GITHUB_STEP_SUMMARY"

- uses: actions/cache@v4
with:
Expand Down Expand Up @@ -218,8 +197,8 @@ jobs:
fi
python3 - "$RESULTS_XML" <<'PY'
import sys, xml.etree.ElementTree as ET
# Escape workflow-command payloads so test-controlled XML (under pull_request_target this
# is fork-supplied) can't break annotation rendering or inject extra workflow commands.
# Escape workflow-command payloads so test-controlled XML can't break annotation
# rendering or inject extra workflow commands.
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
def esc_data(s):
return s.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
Expand Down
29 changes: 12 additions & 17 deletions MCPForUnity/Editor/Services/EditorStateCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,8 @@ static EditorStateCache()
EditorApplication.playModeStateChanged += _ => ForceUpdate("playmode");

// Tracks whether an assembly compilation is actually running, for
// GetActualIsCompiling's Play-mode check. Statics reset on domain reload
// and this [InitializeOnLoad] ctor re-subscribes, so the flag is per-domain.
// GetActualIsCompiling. Statics reset on domain reload and this
// [InitializeOnLoad] ctor re-subscribes, so the flag is per-domain.
UnityEditor.Compilation.CompilationPipeline.compilationStarted += _ => _pipelineCompilationRunning = true;
UnityEditor.Compilation.CompilationPipeline.compilationFinished += _ => _pipelineCompilationRunning = false;

Expand All @@ -288,7 +288,7 @@ private static void OnUpdate()
{
// Throttle to reduce overhead while keeping the snapshot fresh enough for polling clients.
double now = EditorApplication.timeSinceStartup;
// Use GetActualIsCompiling() to avoid Play mode false positives (issue #582)
// Use GetActualIsCompiling() to avoid isCompiling false positives (issues #549, #1276)
bool isCompiling = GetActualIsCompiling();

// Check for compilation edge transitions (always update on these)
Expand Down Expand Up @@ -543,10 +543,12 @@ public static JObject GetSnapshot()
private static bool _pipelineCompilationRunning;

/// <summary>
/// Returns the actual compilation state, working around a known Unity quirk where
/// EditorApplication.isCompiling can return false positives in Play mode (e.g. a
/// recompile deferred by Recompile-After-Finished-Playing keeps it true for the
/// whole play session). See: https://github.com/CoplayDev/unity-mcp/issues/549
/// Returns the actual compilation state, working around known Unity quirks where
/// EditorApplication.isCompiling reports false positives while no compilation is
/// running: a recompile deferred by Recompile-After-Finished-Playing keeps it true
/// for the whole play session (issue #549), and a project holding
/// EditorApplication.LockReloadAssemblies keeps it true until the lock is released
/// (issue #1276). In both cases the event-tracked pipeline flag is authoritative.
/// </summary>
internal static bool GetActualIsCompiling()
{
Expand All @@ -556,16 +558,9 @@ internal static bool GetActualIsCompiling()
return false;
}

// In Play mode, trust the event-tracked pipeline state instead: a deferred
// recompile keeps EditorApplication.isCompiling true without any compilation
// actually running.
if (EditorApplication.isPlaying)
{
return _pipelineCompilationRunning;
}

// Outside Play mode the raw signal is reliable.
return true;
// Otherwise trust the event-tracked pipeline state: isCompiling stays true for as
// long as an assembly reload is deferred, with no compilation actually running.
return _pipelineCompilationRunning;
}
}
}
Expand Down
7 changes: 5 additions & 2 deletions MCPForUnity/Editor/Services/Server/TerminalLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,13 @@ public System.Diagnostics.ProcessStartInfo CreateHeadlessProcessStartInfo(string
}

#if UNITY_EDITOR_WIN
// cmd.exe /c "<command> >> "<log>" 2>&1"
// cmd.exe /c "<command> < NUL >> "<log>" 2>&1"
// The whole payload after /c is wrapped in one outer pair of quotes; cmd strips the
// outermost quotes, so inner quotes around the log path survive for paths with spaces.
string winRedirect = $"{command} >> \"{logFilePath}\" 2>&1";
// stdin is redirected from NUL because the Editor is a console-less GUI process: with
// CreateNoWindow and no console handle, uvx.exe would inherit an invalid stdin and die
// with "The handle is invalid. (os error 6)" before launching the server.
string winRedirect = $"{command} < NUL >> \"{logFilePath}\" 2>&1";
return new System.Diagnostics.ProcessStartInfo
{
FileName = "cmd.exe",
Expand Down
10 changes: 2 additions & 8 deletions MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,8 @@ private static void OnAfterAssemblyReload()
}

// If the editor is not compiling, attempt an immediate restart without relying on editor focus.
bool isCompiling = EditorApplication.isCompiling;
try
{
var pipeline = Type.GetType("UnityEditor.Compilation.CompilationPipeline, UnityEditor");
var prop = pipeline?.GetProperty("isCompiling", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
if (prop != null) isCompiling |= (bool)prop.GetValue(null);
}
catch { }
// Routed through EditorStateCache so a deferred reload (issue #1276) does not block resume.
bool isCompiling = EditorStateCache.GetActualIsCompiling();

if (!isCompiling)
{
Expand Down
4 changes: 2 additions & 2 deletions MCPForUnity/Editor/Services/TestJobManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ internal static TestJob GetJob(string jobId)
{
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
long initTimeout = job.InitTimeoutMs > 0 ? job.InitTimeoutMs : DefaultInitializationTimeoutMs;
if (!EditorApplication.isCompiling && !EditorApplication.isUpdating && now - job.StartedUnixMs > initTimeout)
if (!EditorStateCache.GetActualIsCompiling() && !EditorApplication.isUpdating && now - job.StartedUnixMs > initTimeout)
{
McpLog.Warn($"[TestJobManager] Job {jobId} failed to initialize within {initTimeout}ms, auto-failing");
job.Status = TestJobStatus.Failed;
Expand Down Expand Up @@ -589,7 +589,7 @@ private static string GetBlockedReason(TestJob job)
return "editor_unfocused";
}

if (EditorApplication.isCompiling)
if (EditorStateCache.GetActualIsCompiling())
{
return "compiling";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,24 +237,10 @@ private static void EnsureStartedOnEditorIdle()
}
}

private static bool IsCompiling()
{
if (EditorApplication.isCompiling)
{
return true;
}
try
{
Type pipeline = Type.GetType("UnityEditor.Compilation.CompilationPipeline, UnityEditor");
var prop = pipeline?.GetProperty("isCompiling", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
if (prop != null)
{
return (bool)prop.GetValue(null);
}
}
catch { }
return false;
}
// Routed through EditorStateCache so a deferred domain reload (issue #1276) does not
// pin the bridge off: raw EditorApplication.isCompiling stays true for as long as the
// reload is held, and this gates bridge startup.
private static bool IsCompiling() => EditorStateCache.GetActualIsCompiling();

public static void Start()
{
Expand Down
73 changes: 73 additions & 0 deletions MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,79 @@ internal static object RemoveComponentInternal(GameObject targetGo, string typeN
}
}

/// <summary>
/// Applies a "componentProperties" object (as accepted by 'modify') to every named
/// component already present on <paramref name="targetGo"/>. Shared by 'create' and
/// 'modify' so the argument behaves identically on both actions.
/// </summary>
/// <param name="modified">Set to true if at least one property was set successfully.</param>
/// <returns>An ErrorResponse aggregating any per-component failures, or null if all
/// (or none) of the requested properties were applied successfully.</returns>
internal static object ApplyComponentProperties(GameObject targetGo, JObject componentPropertiesObj, out bool modified)
{
modified = false;
if (componentPropertiesObj == null)
{
return null;
}

var componentErrors = new List<object>();
foreach (var prop in componentPropertiesObj.Properties())
{
string compName = prop.Name;
JObject propertiesToSet = prop.Value as JObject;
if (propertiesToSet != null)
{
var setResult = SetComponentPropertiesInternal(targetGo, compName, propertiesToSet);
if (setResult != null)
{
componentErrors.Add(setResult);
}
else
{
modified = true;
}
}
}

if (componentErrors.Count == 0)
{
return null;
}

var aggregatedErrors = new List<string>();
foreach (var errorObj in componentErrors)
{
try
{
var dataProp = errorObj?.GetType().GetProperty("data");
var dataVal = dataProp?.GetValue(errorObj);
if (dataVal != null)
{
var errorsProp = dataVal.GetType().GetProperty("errors");
var errorsEnum = errorsProp?.GetValue(dataVal) as System.Collections.IEnumerable;
if (errorsEnum != null)
{
foreach (var item in errorsEnum)
{
var s = item?.ToString();
if (!string.IsNullOrEmpty(s)) aggregatedErrors.Add(s);
}
}
}
}
catch (Exception ex)
{
McpLog.Warn($"[ManageGameObject] Error aggregating component errors: {ex.Message}");
}
}

return new ErrorResponse(
$"One or more component property operations failed on '{targetGo.name}'.",
new { componentErrors = componentErrors, errors = aggregatedErrors }
);
}

internal static object SetComponentPropertiesInternal(GameObject targetGo, string componentTypeName, JObject properties, Component targetComponentInstance = null)
{
Component targetComponent = targetComponentInstance;
Expand Down
10 changes: 10 additions & 0 deletions MCPForUnity/Editor/Tools/GameObjects/GameObjectCreate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,16 @@ internal static object Handle(JObject @params)
}
}

// Set Component Properties (same "componentProperties" argument 'modify' consumes,
// applied here so it also works at creation time)
var componentPropertiesError = GameObjectComponentHelpers.ApplyComponentProperties(
newGo, @params["componentProperties"] as JObject, out _);
if (componentPropertiesError != null)
{
UnityEngine.Object.DestroyImmediate(newGo);
return componentPropertiesError;
}

// Save as Prefab ONLY if we *created* a new object AND saveAsPrefab is true
GameObject finalInstance = newGo;
if (createdNewObject && saveAsPrefab)
Expand Down
Loading
Loading