Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
95499bf
chore: sync main (v9.6.8) into beta
actions-user Apr 27, 2026
a6ad10b
chore: set beta version to 9.6.9-beta.1 after release v9.6.8
actions-user Apr 27, 2026
2a6be79
Merge pull request #1087 from CoplayDev/sync/main-v9.6.8-into-beta-24…
github-actions[bot] Apr 27, 2026
56a14af
Fix game_view screenshots to capture UI Toolkit overlays
KennerMiner Mar 30, 2026
7aa4315
fix: address screenshot PR feedback
KennerMiner Apr 6, 2026
8d51737
Add configurable init_timeout for PlayMode test initialization
Apr 2, 2026
e8e3b45
Address code review: input validation and tests
Apr 2, 2026
3df5a84
Remove unused field, guard test against compile/update flakiness
Apr 2, 2026
233e88e
Clear SessionState in TestJobManagerInitTimeoutTests TearDown
Apr 29, 2026
dfc3485
Merge pull request #1021 from Emerix/feature/configurable-init-timeout
Scriptwonder Apr 29, 2026
ddc446b
chore: update Unity package to beta version 9.6.9-beta.2
actions-user Apr 29, 2026
3fc7a47
Merge pull request #1093 from CoplayDev/beta-version-9.6.9-beta.2-251…
Scriptwonder Apr 29, 2026
744eeb0
chore: update Unity package to beta version 9.6.9-beta.3
actions-user Apr 29, 2026
de37940
Merge pull request #1094 from CoplayDev/beta-version-9.6.9-beta.3-251…
github-actions[bot] Apr 29, 2026
1fba429
Update0503
Scriptwonder May 3, 2026
1b095fb
PatchFix
Scriptwonder May 3, 2026
99a4687
Merge pull request #1097 from Scriptwonder/chore/unity-version-compat…
Scriptwonder May 4, 2026
ced7bbf
chore: update Unity package to beta version 9.6.9-beta.4
actions-user May 4, 2026
9b0a662
Merge pull request #1098 from CoplayDev/beta-version-9.6.9-beta.4-252…
github-actions[bot] May 4, 2026
a90ab06
Merge branch 'beta' into feature/game-view-uitoolkit-screenshot-capture
Scriptwonder May 4, 2026
5a2a59f
Merge pull request #1040 from KennerMiner/feature/game-view-uitoolkit…
Scriptwonder May 4, 2026
0d51069
chore: update Unity package to beta version 9.6.9-beta.5
actions-user May 4, 2026
37ef016
Merge pull request #1099 from CoplayDev/beta-version-9.6.9-beta.5-252…
github-actions[bot] May 4, 2026
c298216
fix: unblock beta compile and gate releases on test success (#1103)
dsarno May 4, 2026
ea66360
chore: update Unity package to beta version 9.6.9-beta.6
actions-user May 4, 2026
56ee0b0
Merge pull request #1104 from CoplayDev/beta-version-9.6.9-beta.6-253…
github-actions[bot] May 4, 2026
8942e8b
fix: close 2022.3 compile gap in UnityFindObjectsCompat.FindAll (#1106)
dsarno May 5, 2026
ae3498c
ci(unity-tests): include MCPForUnity/Runtime/** in trigger paths (#1108)
dsarno May 5, 2026
680ba45
chore: update Unity package to beta version 9.6.9-beta.7
actions-user May 5, 2026
a2a5edf
Merge pull request #1109 from CoplayDev/beta-version-9.6.9-beta.7-253…
github-actions[bot] May 5, 2026
f7b126d
fix: enable ExclusiveAddressUse on Linux for reliable multi-instance …
emiapwil May 8, 2026
ae4d186
fix: reliable auto-start and multi-instance connection support
emiapwil May 8, 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
13 changes: 13 additions & 0 deletions .github/workflows/beta-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,21 @@ on:
- "MCPForUnity/**"

jobs:
unity_tests:
name: Unity tests gate
if: github.actor != 'github-actions[bot]'
uses: ./.github/workflows/unity-tests.yml
secrets: inherit

python_tests:
name: Python tests gate
if: github.actor != 'github-actions[bot]'
uses: ./.github/workflows/python-tests.yml

update_unity_beta_version:
name: Update Unity package to beta version
runs-on: ubuntu-latest
needs: [unity_tests, python_tests]
# Avoid running when the workflow's own automation merges the PR
# created by this workflow (prevents a version-bump loop).
if: github.actor != 'github-actions[bot]'
Expand Down Expand Up @@ -143,6 +155,7 @@ jobs:
publish_pypi_prerelease:
name: Publish beta to PyPI (pre-release)
runs-on: ubuntu-latest
needs: [unity_tests, python_tests]
# Avoid double-publish when the bot merges the version bump PR
if: github.actor != 'github-actions[bot]'
environment:
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,19 @@ on:
paths:
- Server/**
- .github/workflows/python-tests.yml
pull_request:
branches: [main, beta]
paths:
- Server/**
- .github/workflows/python-tests.yml
workflow_dispatch: {}
workflow_call:
inputs:
ref:
description: "Git ref to test (defaults to the triggering ref)."
type: string
required: false
default: ""

jobs:
test:
Expand All @@ -15,6 +27,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}

- name: Install uv
uses: astral-sh/setup-uv@v4
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,23 @@ on:
required: true

jobs:
unity_tests:
name: Unity tests gate
uses: ./.github/workflows/unity-tests.yml
with:
ref: beta
secrets: inherit

python_tests:
name: Python tests gate
uses: ./.github/workflows/python-tests.yml
with:
ref: beta

bump:
name: Bump version, tag, and create release
runs-on: ubuntu-latest
needs: [unity_tests, python_tests]
permissions:
contents: write
pull-requests: write
Expand Down
30 changes: 29 additions & 1 deletion .github/workflows/unity-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,43 @@ name: Unity Tests

on:
workflow_dispatch: {}
workflow_call:
inputs:
ref:
description: "Git ref to test (defaults to the triggering ref)."
type: string
required: false
default: ""
push:
branches: ["**"]
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

jobs:
testAllModes:
name: Test in ${{ matrix.testMode }}
runs-on: ubuntu-latest
permissions:
contents: read
if: >
github.event_name != 'pull_request_target' ||
(github.event.pull_request.head.repo.full_name != github.repository &&
github.event.label.name == 'safe-to-test')
strategy:
fail-fast: false
matrix:
Expand All @@ -27,6 +53,8 @@ jobs:
uses: actions/checkout@v4
with:
lfs: true
ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.ref }}
persist-credentials: false

- name: Detect Unity license secrets
id: detect
Expand Down Expand Up @@ -107,7 +135,7 @@ jobs:
fi
- uses: actions/upload-artifact@v4
if: always() && steps.detect.outputs.unity_ok == 'true'
if: always() && steps.detect.outputs.unity_ok == 'true' && steps.tests.outcome != 'skipped'
with:
name: Test results for ${{ matrix.testMode }}
path: ${{ steps.tests.outputs.artifactsPath }}
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ Use `CommandRegistry.InvokeCommandAsync` to call other tools from within a handl
var result = await CommandRegistry.InvokeCommandAsync("read_console", consoleParams);
```

### Unity API Compatibility Shims
We support a wide Unity version range (2021+ → 6.x → CoreCLR 6.8). When an API is renamed, deprecated, or removed across versions, **don't sprinkle `#if UNITY_x_y_OR_NEWER` at every call site** — add a shim in `MCPForUnity/Runtime/Helpers/Unity*Compat.cs` and route every caller through it.

The catalog of active shims, the policy for when to add one, what does NOT belong in a shim, and the reflection-cache pattern all live in **`MCPForUnity/Runtime/Helpers/UnityCompatShims.cs`** — the XML doc on that empty marker class is the source of truth and ships inside the UPM package, so end-users can `F12`/Go-to-definition into it. Sources for current deprecations: Unity 6.x upgrade guides and the [CoreCLR 2026 thread](https://discussions.unity.com/t/path-to-coreclr-2026-upgrade-guide/1714279).

## Commands

### Running Tests
Expand Down
22 changes: 13 additions & 9 deletions MCPForUnity/Editor/Helpers/EditorWindowScreenshotUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ namespace MCPForUnity.Editor.Helpers
/// </summary>
internal static class EditorWindowScreenshotUtility
{
private const string ScreenshotsFolderName = "Screenshots";
// Keep capture synchronous so callers can immediately return the screenshot payload.
// The short sleep gives Unity a chance to flush repaint work before GrabPixels reads the viewport.
private const int RepaintSettlingDelayMs = 75;
Expand All @@ -40,15 +39,16 @@ internal static class EditorWindowScreenshotUtility
/// <param name="maxResolution">Maximum edge length for the inline image payload.</param>
/// <param name="viewportWidth">Captured viewport width in pixels.</param>
/// <param name="viewportHeight">Captured viewport height in pixels.</param>
public static ScreenshotCaptureResult CaptureSceneViewViewportToAssets(
public static ScreenshotCaptureResult CaptureSceneViewViewportToProject(
SceneView sceneView,
string fileName,
int superSize,
bool ensureUniqueFileName,
bool includeImage,
int maxResolution,
out int viewportWidth,
out int viewportHeight)
out int viewportHeight,
string folderOverride = null)
{
if (sceneView == null)
throw new ArgumentNullException(nameof(sceneView));
Expand All @@ -70,7 +70,7 @@ public static ScreenshotCaptureResult CaptureSceneViewViewportToAssets(
{
captured = CaptureViewRect(sceneView, viewportRectPixels);

var result = PrepareCaptureResult(fileName, effectiveSuperSize, ensureUniqueFileName);
var result = PrepareCaptureResult(fileName, effectiveSuperSize, ensureUniqueFileName, folderOverride);
byte[] png = captured.EncodeToPNG();
File.WriteAllBytes(result.FullPath, png);

Expand All @@ -97,7 +97,7 @@ public static ScreenshotCaptureResult CaptureSceneViewViewportToAssets(

return new ScreenshotCaptureResult(
result.FullPath,
result.AssetsRelativePath,
result.ProjectRelativePath,
result.SuperSize,
false,
imageBase64,
Expand Down Expand Up @@ -317,11 +317,11 @@ private static void FlipTextureVertically(Texture2D texture)
texture.Apply();
}

private static ScreenshotCaptureResult PrepareCaptureResult(string fileName, int superSize, bool ensureUniqueFileName)
private static ScreenshotCaptureResult PrepareCaptureResult(string fileName, int superSize, bool ensureUniqueFileName, string folderOverride)
{
int size = Mathf.Max(1, superSize);
string resolvedName = BuildFileName(fileName);
string folder = Path.Combine(Application.dataPath, ScreenshotsFolderName);
string folder = ScreenshotUtility.ResolveFolderAbsolute(folderOverride);
Directory.CreateDirectory(folder);

string fullPath = Path.Combine(folder, resolvedName);
Expand All @@ -331,8 +331,12 @@ private static ScreenshotCaptureResult PrepareCaptureResult(string fileName, int
}

string normalizedFullPath = fullPath.Replace('\\', '/');
string assetsRelativePath = "Assets/" + normalizedFullPath.Substring(Application.dataPath.Length).TrimStart('/');
return new ScreenshotCaptureResult(normalizedFullPath, assetsRelativePath, size, false);
string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")).Replace('\\', '/');
string normalizedRoot = projectRoot.EndsWith("/") ? projectRoot : projectRoot + "/";
string projectRelativePath = normalizedFullPath.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase)
? normalizedFullPath.Substring(normalizedRoot.Length)
: normalizedFullPath;
return new ScreenshotCaptureResult(normalizedFullPath, projectRelativePath, size, false);
}

private static string BuildFileName(string fileName)
Expand Down
45 changes: 35 additions & 10 deletions MCPForUnity/Editor/Helpers/PortManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
Expand Down Expand Up @@ -33,10 +34,13 @@ public class PortConfig
public int unity_port;
public string created_date;
public string project_path;
public int pid;
}

/// <summary>
/// Get the port to use from storage, or return the default if none has been saved yet.
/// When the stored port is in use by another process (multi-instance scenario),
/// falls back to the default port instead of returning an occupied port.
/// </summary>
/// <returns>Port number to use</returns>
public static int GetPortWithFallback()
Expand All @@ -46,7 +50,15 @@ public static int GetPortWithFallback()
storedConfig.unity_port > 0 &&
string.Equals(storedConfig.project_path ?? string.Empty, Application.dataPath ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
return storedConfig.unity_port;
// Only return the stored port if it's actually available.
// When another instance of the same project is running, the stored
// port will be occupied; falling back to DefaultPort lets Start()'s
// SocketException handler pick a truly free port via DiscoverNewPort().
if (IsPortAvailable(storedConfig.unity_port))
{
return storedConfig.unity_port;
}
if (IsDebugEnabled()) McpLog.Info($"Stored port {storedConfig.unity_port} is occupied, falling back to default");
}

return DefaultPort;
Expand Down Expand Up @@ -122,12 +134,10 @@ public static bool IsPortAvailable(int port)
try
{
var testListener = new TcpListener(IPAddress.Loopback, port);
#if UNITY_EDITOR_OSX
// On macOS, SO_REUSEADDR (the default) lets multiple processes bind the same
// port — including AssetImportWorkers. ExclusiveAddressUse prevents this so
// the test bind fails when another process already holds the port.
// ExclusiveAddressUse prevents SO_REUSEADDR from allowing multiple
// processes (including AssetImportWorkers) to bind the same port.
// The test bind fails when another process already holds the port.
try { testListener.Server.ExclusiveAddressUse = true; } catch { }
#endif
testListener.Start();
testListener.Stop();
}
Expand Down Expand Up @@ -202,23 +212,38 @@ private static void SavePort(int port)
{
try
{
int pid = 0;
try { pid = System.Diagnostics.Process.GetCurrentProcess().Id; } catch { }

var portConfig = new PortConfig
{
unity_port = port,
created_date = DateTime.UtcNow.ToString("O"),
project_path = Application.dataPath
project_path = Application.dataPath,
pid = pid
};

string registryDir = GetRegistryDirectory();
Directory.CreateDirectory(registryDir);

string registryFile = GetRegistryFilePath();
string json = JsonConvert.SerializeObject(portConfig, Formatting.Indented);
byte[] utf8Bytes = new System.Text.UTF8Encoding(false).GetBytes(json);

// Write to hashed, project-scoped file
File.WriteAllText(registryFile, json, new System.Text.UTF8Encoding(false));
string registryFile = GetRegistryFilePath();
File.WriteAllBytes(registryFile, utf8Bytes);
// Also write to legacy stable filename to avoid hash/case drift across reloads
string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
File.WriteAllText(legacy, json, new System.Text.UTF8Encoding(false));
File.WriteAllBytes(legacy, utf8Bytes);

// Write instance-scoped file so multiple instances of the same project
// can be tracked independently.
if (pid > 0)
{
string hash = ComputeProjectHash(Application.dataPath);
string instanceFile = Path.Combine(registryDir, $"unity-mcp-port-{hash}-{pid}.json");
File.WriteAllBytes(instanceFile, utf8Bytes);
}

if (IsDebugEnabled()) McpLog.Info($"Saved port {port} to storage");
}
Expand Down
49 changes: 49 additions & 0 deletions MCPForUnity/Editor/Helpers/ScreenshotPreferences.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using MCPForUnity.Runtime.Helpers;
using UnityEditor;

namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Per-user EditorPrefs override for the default screenshot output folder.
/// Resolution priority used by callers:
/// 1. Per-call <c>output_folder</c> tool parameter
/// 2. <see cref="DefaultFolder"/> (this preference)
/// 3. <see cref="ScreenshotUtility.DefaultFolder"/> built-in fallback
/// </summary>
public static class ScreenshotPreferences
{
public const string EditorPrefsKey = "MCPForUnity_ScreenshotsFolder";

/// <summary>
/// User-configured default folder, or empty string when unset.
/// Stored as a project-relative path (e.g. "Assets/Screenshots", "Captures").
/// </summary>
public static string DefaultFolder
{
get => EditorPrefs.GetString(EditorPrefsKey, string.Empty);
set
{
if (string.IsNullOrWhiteSpace(value))
{
EditorPrefs.DeleteKey(EditorPrefsKey);
}
else
{
EditorPrefs.SetString(EditorPrefsKey, value.Trim());
}
}
}

/// <summary>
/// Resolves the effective folder: caller override → user pref → built-in default.
/// Returns a project-relative path string suitable for <see cref="ScreenshotUtility.ResolveFolderAbsolute"/>.
/// </summary>
public static string Resolve(string callerOverride)
{
if (!string.IsNullOrWhiteSpace(callerOverride)) return callerOverride.Trim();
string pref = DefaultFolder;
if (!string.IsNullOrWhiteSpace(pref)) return pref;
return ScreenshotUtility.DefaultFolder;
}
}
}
11 changes: 11 additions & 0 deletions MCPForUnity/Editor/Helpers/ScreenshotPreferences.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading