Skip to content

Commit 17c6a36

Browse files
dsarnomsanatanclaude
authored
feat: Add beta server mode with PyPI pre-release support (#640)
* feat: add TestPyPI toggle for pre-release server package testing - Add UseTestPyPI editor preference key - Add TestPyPI toggle to Advanced settings UI with tooltip - Configure uvx to use test.pypi.org when TestPyPI mode enabled - Skip version pinning in TestPyPI mode to get latest pre-release - Update ConfigJsonBuilder to handle TestPyPI index URL * Update .meta file * fix: Use PyPI pre-release versions instead of TestPyPI for beta server TestPyPI has polluted packages (broken httpx, mcp, fastapi) that cause server startup failures. Switch to publishing beta versions directly to PyPI as pre-releases (e.g., 9.3.0b20260127). Key changes: - beta-release.yml: Publish to PyPI instead of TestPyPI, use beta suffix - Use --prerelease explicit with version specifier (>=0.0.0a0) to only get prereleases of our package, not broken dependency prereleases - Default "Use Beta Server" toggle to true on beta branch - Rename UI label from "Use TestPyPI" to "Use Beta Server" - Add UseTestPyPI to EditorPrefsWindow known prefs - Add search field and refresh button to EditorPrefsWindow Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: Add beta mode indicator to UI badge and server version logging - Show "β" suffix on version badge when beta server mode is enabled - Badge updates dynamically when toggle changes - Add server version to startup log: "MCP for Unity Server v9.2.0 starting up" - Add version field to /health endpoint response Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: Rename UseTestPyPI to UseBetaServer and fix EditorPrefs margin - Rename EditorPref key from UseTestPyPI to UseBetaServer for clarity - Rename all related variables and UXML element names - Increase bottom margin on EditorPrefs search bar to prevent clipping first entry Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: Address code review feedback - Centralize beta server uvx args in AssetPathUtility.GetBetaServerFromArgs() to avoid duplication between HTTP and stdio transports - Cache server version at startup instead of calling get_package_version() on every /health request - Add robustness to beta version parsing in workflow: strip existing pre-release suffix and validate X.Y.Z format before parsing Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Prioritize explicit fromUrl override and optimize search filter - GetBetaServerFromArgs/GetBetaServerFromArgsList now check for explicit GitUrlOverride before applying beta server mode, ensuring local dev paths and custom URLs are honored - EditorPrefsWindow search filter uses IndexOf with OrdinalIgnoreCase instead of ToLowerInvariant().Contains() for fewer allocations Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Marcus Sanatan <msanatan@gmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 7685db8 commit 17c6a36

11 files changed

Lines changed: 221 additions & 35 deletions

File tree

‎.github/workflows/beta-release.yml‎

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Beta Release (TestPyPI)
1+
name: Beta Release (PyPI Pre-release)
22

33
concurrency:
44
group: beta-release
@@ -12,12 +12,12 @@ on:
1212
- "Server/**"
1313

1414
jobs:
15-
publish_testpypi:
16-
name: Publish beta to TestPyPI
15+
publish_pypi_prerelease:
16+
name: Publish beta to PyPI (pre-release)
1717
runs-on: ubuntu-latest
1818
environment:
19-
name: testpypi
20-
url: https://test.pypi.org/p/mcpforunityserver
19+
name: pypi
20+
url: https://pypi.org/p/mcpforunityserver
2121
permissions:
2222
contents: read
2323
id-token: write
@@ -33,27 +33,38 @@ jobs:
3333
enable-cache: true
3434
cache-dependency-glob: "Server/uv.lock"
3535

36-
- name: Generate dev version
36+
- name: Generate beta version
3737
id: version
3838
shell: bash
3939
run: |
4040
set -euo pipefail
41-
BASE_VERSION=$(grep -oP '(?<=version = ")[^"]+' Server/pyproject.toml)
42-
# Use date for unique dev version (PEP 440 compliant: X.Y.Z.devN)
43-
# Note: PyPI/TestPyPI don't support local version identifiers (+...)
44-
DEV_NUMBER="$(date +%Y%m%d%H%M%S)"
45-
DEV_VERSION="${BASE_VERSION}.dev${DEV_NUMBER}"
41+
RAW_VERSION=$(grep -oP '(?<=version = ")[^"]+' Server/pyproject.toml)
42+
# Strip any existing pre-release suffix (a, b, rc, dev, post) for safe parsing
43+
# e.g., "9.2.0b1" -> "9.2.0", "9.2.0.dev1" -> "9.2.0"
44+
BASE_VERSION=$(echo "$RAW_VERSION" | sed -E 's/(a|b|rc|\.dev|\.post)[0-9]+$//')
45+
# Validate we have a proper X.Y.Z format
46+
if ! [[ "$BASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
47+
echo "Error: Could not parse version '$RAW_VERSION' -> '$BASE_VERSION'" >&2
48+
exit 1
49+
fi
50+
# Bump minor version and use beta suffix (PEP 440 compliant: X.Y+1.0bN)
51+
# This ensures beta is "newer" than the stable release
52+
IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE_VERSION"
53+
NEXT_MINOR=$((MINOR + 1))
54+
BETA_NUMBER="$(date +%Y%m%d%H%M%S)"
55+
BETA_VERSION="${MAJOR}.${NEXT_MINOR}.0b${BETA_NUMBER}"
56+
echo "Raw version: $RAW_VERSION"
4657
echo "Base version: $BASE_VERSION"
47-
echo "Dev version: $DEV_VERSION"
48-
echo "dev_version=$DEV_VERSION" >> "$GITHUB_OUTPUT"
58+
echo "Beta version: $BETA_VERSION"
59+
echo "beta_version=$BETA_VERSION" >> "$GITHUB_OUTPUT"
4960
5061
- name: Update version for beta release
5162
env:
52-
DEV_VERSION: ${{ steps.version.outputs.dev_version }}
63+
BETA_VERSION: ${{ steps.version.outputs.beta_version }}
5364
shell: bash
5465
run: |
5566
set -euo pipefail
56-
sed -i "s/^version = .*/version = \"${DEV_VERSION}\"/" Server/pyproject.toml
67+
sed -i "s/^version = .*/version = \"${BETA_VERSION}\"/" Server/pyproject.toml
5768
echo "Updated pyproject.toml:"
5869
grep "^version" Server/pyproject.toml
5970
@@ -62,8 +73,7 @@ jobs:
6273
run: uv build
6374
working-directory: ./Server
6475

65-
- name: Publish distribution to TestPyPI
76+
- name: Publish distribution to PyPI
6677
uses: pypa/gh-action-pypi-publish@release/v1
6778
with:
6879
packages-dir: Server/dist/
69-
repository-url: https://test.pypi.org/legacy/

‎MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs.meta‎

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎MCPForUnity/Editor/Constants/EditorPrefKeys.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ internal static class EditorPrefKeys
2828
internal const string WebSocketUrlOverride = "MCPForUnity.WebSocketUrl";
2929
internal const string GitUrlOverride = "MCPForUnity.GitUrlOverride";
3030
internal const string DevModeForceServerRefresh = "MCPForUnity.DevModeForceServerRefresh";
31+
internal const string UseBetaServer = "MCPForUnity.UseBetaServer";
3132
internal const string ProjectScopedToolsLocalHttp = "MCPForUnity.ProjectScopedTools.LocalHttp";
3233

3334
internal const string PackageDeploySourcePath = "MCPForUnity.PackageDeploy.SourcePath";

‎MCPForUnity/Editor/Helpers/AssetPathUtility.cs‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,83 @@ public static (string uvxPath, string fromUrl, string packageName) GetUvxCommand
243243
return (uvxPath, fromUrl, packageName);
244244
}
245245

246+
/// <summary>
247+
/// Builds the uvx package source arguments for the MCP server.
248+
/// Handles beta server mode (prerelease from PyPI) vs standard mode (pinned version or override).
249+
/// Centralizes the prerelease logic to avoid duplication between HTTP and stdio transports.
250+
/// Priority: explicit fromUrl override > beta server mode > default package.
251+
/// </summary>
252+
/// <param name="quoteFromPath">Whether to quote the --from path (needed for command-line strings, not for arg lists)</param>
253+
/// <returns>The package source arguments (e.g., "--prerelease explicit --from mcpforunityserver>=0.0.0a0")</returns>
254+
public static string GetBetaServerFromArgs(bool quoteFromPath = false)
255+
{
256+
// Explicit override (local path, git URL, etc.) always wins
257+
string fromUrl = GetMcpServerPackageSource();
258+
string overrideUrl = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
259+
if (!string.IsNullOrEmpty(overrideUrl))
260+
{
261+
return $"--from {fromUrl}";
262+
}
263+
264+
// Beta server mode: use prerelease from PyPI
265+
bool useBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true);
266+
if (useBetaServer)
267+
{
268+
// Use --prerelease explicit with version specifier to only get prereleases of our package,
269+
// not of dependencies (which can be broken on PyPI).
270+
string fromValue = quoteFromPath ? "\"mcpforunityserver>=0.0.0a0\"" : "mcpforunityserver>=0.0.0a0";
271+
return $"--prerelease explicit --from {fromValue}";
272+
}
273+
274+
// Standard mode: use pinned version from package.json
275+
if (!string.IsNullOrEmpty(fromUrl))
276+
{
277+
return $"--from {fromUrl}";
278+
}
279+
280+
return string.Empty;
281+
}
282+
283+
/// <summary>
284+
/// Builds the uvx package source arguments as a list (for JSON config builders).
285+
/// Priority: explicit fromUrl override > beta server mode > default package.
286+
/// </summary>
287+
/// <returns>List of arguments to add to uvx command</returns>
288+
public static System.Collections.Generic.IList<string> GetBetaServerFromArgsList()
289+
{
290+
var args = new System.Collections.Generic.List<string>();
291+
292+
// Explicit override (local path, git URL, etc.) always wins
293+
string fromUrl = GetMcpServerPackageSource();
294+
string overrideUrl = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
295+
if (!string.IsNullOrEmpty(overrideUrl))
296+
{
297+
args.Add("--from");
298+
args.Add(fromUrl);
299+
return args;
300+
}
301+
302+
// Beta server mode: use prerelease from PyPI
303+
bool useBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true);
304+
if (useBetaServer)
305+
{
306+
args.Add("--prerelease");
307+
args.Add("explicit");
308+
args.Add("--from");
309+
args.Add("mcpforunityserver>=0.0.0a0");
310+
return args;
311+
}
312+
313+
// Standard mode: use pinned version from package.json
314+
if (!string.IsNullOrEmpty(fromUrl))
315+
{
316+
args.Add("--from");
317+
args.Add(fromUrl);
318+
}
319+
320+
return args;
321+
}
322+
246323
/// <summary>
247324
/// Determines whether uvx should use --no-cache --refresh flags.
248325
/// Returns true if DevModeForceServerRefresh is enabled OR if the server URL is a local path.

‎MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
5-
using MCPForUnity.Editor.Constants;
65
using MCPForUnity.Editor.Clients.Configurators;
6+
using MCPForUnity.Editor.Constants;
77
using MCPForUnity.Editor.Helpers;
88
using MCPForUnity.Editor.Models;
99
using Newtonsoft.Json;
@@ -170,10 +170,11 @@ private static IList<string> BuildUvxArgs(string fromUrl, string packageName)
170170
args.Add("--no-cache");
171171
args.Add("--refresh");
172172
}
173-
if (!string.IsNullOrEmpty(fromUrl))
173+
174+
// Use centralized helper for beta server / prerelease args
175+
foreach (var arg in AssetPathUtility.GetBetaServerFromArgsList())
174176
{
175-
args.Add("--from");
176-
args.Add(fromUrl);
177+
args.Add(arg);
177178
}
178179
args.Add(packageName);
179180

‎MCPForUnity/Editor/Services/ServerManagementService.cs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,9 +1317,13 @@ private bool TryGetLocalHttpServerCommandParts(out string fileName, out string a
13171317
true
13181318
);
13191319
string scopedFlag = projectScopedTools ? " --project-scoped-tools" : string.Empty;
1320-
string args = string.IsNullOrEmpty(fromUrl)
1320+
1321+
// Use centralized helper for beta server / prerelease args
1322+
string fromArgs = AssetPathUtility.GetBetaServerFromArgs(quoteFromPath: true);
1323+
1324+
string args = string.IsNullOrEmpty(fromArgs)
13211325
? $"{devFlags}{packageName} --transport http --http-url {httpUrl}{scopedFlag}"
1322-
: $"{devFlags}--from {fromUrl} {packageName} --transport http --http-url {httpUrl}{scopedFlag}";
1326+
: $"{devFlags}{fromArgs} {packageName} --transport http --http-url {httpUrl}{scopedFlag}";
13231327

13241328
fileName = uvxPath;
13251329
arguments = args;

‎MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class McpAdvancedSection
2626
private Button clearGitUrlButton;
2727
private Toggle debugLogsToggle;
2828
private Toggle devModeForceRefreshToggle;
29+
private Toggle useBetaServerToggle;
2930
private TextField deploySourcePath;
3031
private Button browseDeploySourceButton;
3132
private Button clearDeploySourceButton;
@@ -42,6 +43,7 @@ public class McpAdvancedSection
4243
public event Action OnGitUrlChanged;
4344
public event Action OnHttpServerCommandUpdateRequested;
4445
public event Action OnTestConnectionRequested;
46+
public event Action<bool> OnBetaModeChanged;
4547

4648
public VisualElement Root { get; private set; }
4749

@@ -64,6 +66,7 @@ private void CacheUIElements()
6466
clearGitUrlButton = Root.Q<Button>("clear-git-url-button");
6567
debugLogsToggle = Root.Q<Toggle>("debug-logs-toggle");
6668
devModeForceRefreshToggle = Root.Q<Toggle>("dev-mode-force-refresh-toggle");
69+
useBetaServerToggle = Root.Q<Toggle>("use-beta-server-toggle");
6770
deploySourcePath = Root.Q<TextField>("deploy-source-path");
6871
browseDeploySourceButton = Root.Q<Button>("browse-deploy-source-button");
6972
clearDeploySourceButton = Root.Q<Button>("clear-deploy-source-button");
@@ -98,6 +101,13 @@ private void InitializeUI()
98101
if (forceRefreshLabel != null)
99102
forceRefreshLabel.tooltip = devModeForceRefreshToggle.tooltip;
100103
}
104+
if (useBetaServerToggle != null)
105+
{
106+
useBetaServerToggle.tooltip = "When enabled, uvx will fetch the latest beta server version from PyPI. Enable this on the beta branch to get the matching server version.";
107+
var betaServerLabel = useBetaServerToggle?.parent?.Q<Label>();
108+
if (betaServerLabel != null)
109+
betaServerLabel.tooltip = useBetaServerToggle.tooltip;
110+
}
101111
if (testConnectionButton != null)
102112
testConnectionButton.tooltip = "Test the connection between Unity and the MCP server.";
103113
if (deploySourcePath != null)
@@ -128,6 +138,7 @@ private void InitializeUI()
128138
McpLog.SetDebugLoggingEnabled(debugEnabled);
129139

130140
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
141+
useBetaServerToggle.value = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true);
131142
UpdatePathOverrides();
132143
UpdateDeploymentSection();
133144
}
@@ -172,6 +183,13 @@ private void RegisterCallbacks()
172183
OnHttpServerCommandUpdateRequested?.Invoke();
173184
});
174185

186+
useBetaServerToggle.RegisterValueChangedCallback(evt =>
187+
{
188+
EditorPrefs.SetBool(EditorPrefKeys.UseBetaServer, evt.newValue);
189+
OnHttpServerCommandUpdateRequested?.Invoke();
190+
OnBetaModeChanged?.Invoke(evt.newValue);
191+
});
192+
175193
deploySourcePath.RegisterValueChangedCallback(evt =>
176194
{
177195
string path = evt.newValue?.Trim();
@@ -274,6 +292,7 @@ public void UpdatePathOverrides()
274292
gitUrlOverride.value = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
275293
debugLogsToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
276294
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
295+
useBetaServerToggle.value = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true);
277296
UpdateDeploymentSection();
278297
}
279298

‎MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.uxml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
<ui:Toggle name="dev-mode-force-refresh-toggle" class="setting-toggle" />
4242
</ui:VisualElement>
4343

44+
<ui:VisualElement class="setting-row">
45+
<ui:Label text="Use Beta Server:" class="setting-label" />
46+
<ui:Toggle name="use-beta-server-toggle" class="setting-toggle" />
47+
</ui:VisualElement>
48+
4449
<ui:VisualElement class="override-row" style="margin-top: 8px;">
4550
<ui:Label text="Package Source:" class="override-label" />
4651
</ui:VisualElement>

‎MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs‎

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public class EditorPrefsWindow : EditorWindow
1919
// UI Elements
2020
private ScrollView scrollView;
2121
private VisualElement prefsContainer;
22+
private TextField searchField;
23+
private string searchFilter = "";
2224

2325
// Data
2426
private List<EditorPrefItem> currentPrefs = new List<EditorPrefItem>();
@@ -40,6 +42,7 @@ public class EditorPrefsWindow : EditorWindow
4042
{ EditorPrefKeys.CustomToolRegistrationEnabled, EditorPrefType.Bool },
4143
{ EditorPrefKeys.TelemetryDisabled, EditorPrefType.Bool },
4244
{ EditorPrefKeys.DevModeForceServerRefresh, EditorPrefType.Bool },
45+
{ EditorPrefKeys.UseBetaServer, EditorPrefType.Bool },
4346
{ EditorPrefKeys.ProjectScopedToolsLocalHttp, EditorPrefType.Bool },
4447

4548
// Integer prefs
@@ -105,7 +108,39 @@ public void CreateGUI()
105108
}
106109

107110
visualTree.CloneTree(rootVisualElement);
108-
111+
112+
// Add search bar container at the top
113+
var searchContainer = new VisualElement();
114+
searchContainer.style.flexDirection = FlexDirection.Row;
115+
searchContainer.style.marginTop = 8;
116+
searchContainer.style.marginBottom = 20;
117+
searchContainer.style.marginLeft = 4;
118+
searchContainer.style.marginRight = 4;
119+
120+
searchField = new TextField("Search");
121+
searchField.style.flexGrow = 1;
122+
searchField.style.height = 28;
123+
searchField.style.paddingTop = 2;
124+
searchField.style.paddingBottom = 2;
125+
searchField.labelElement.style.unityFontStyleAndWeight = FontStyle.Bold;
126+
searchField.RegisterValueChangedCallback(evt =>
127+
{
128+
searchFilter = evt.newValue ?? "";
129+
RefreshPrefs();
130+
});
131+
132+
var refreshButton = new Button(RefreshPrefs);
133+
refreshButton.text = "↻";
134+
refreshButton.tooltip = "Refresh prefs";
135+
refreshButton.style.width = 30;
136+
refreshButton.style.height = 28;
137+
refreshButton.style.marginLeft = 6;
138+
refreshButton.style.backgroundColor = new Color(0.9f, 0.5f, 0.1f);
139+
140+
searchContainer.Add(searchField);
141+
searchContainer.Add(refreshButton);
142+
rootVisualElement.Insert(0, searchContainer);
143+
109144
// Get references
110145
scrollView = rootVisualElement.Q<ScrollView>("scroll-view");
111146
prefsContainer = rootVisualElement.Q<VisualElement>("prefs-container");
@@ -154,13 +189,23 @@ private void RefreshPrefs()
154189

155190
// Sort keys
156191
allKeys.Sort();
157-
192+
193+
// Pre-trim filter once outside the loop
194+
var filter = searchFilter?.Trim();
195+
158196
// Create items for existing prefs
159197
foreach (var key in allKeys)
160198
{
161199
// Skip Customer UUID but show everything else that's defined
162200
if (key != EditorPrefKeys.CustomerUuid)
163201
{
202+
// Apply search filter using OrdinalIgnoreCase for fewer allocations
203+
if (!string.IsNullOrEmpty(filter) &&
204+
key.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0)
205+
{
206+
continue;
207+
}
208+
164209
var item = CreateEditorPrefItem(key);
165210
if (item != null)
166211
{

0 commit comments

Comments
 (0)