From 17c6a36c8dcdda49017001abcce158e75b3d7d05 Mon Sep 17 00:00:00 2001 From: dsarno Date: Tue, 27 Jan 2026 11:34:11 -0800 Subject: [PATCH] feat: Add beta server mode with PyPI pre-release support (#640) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 * 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 * 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 * 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 --------- Co-authored-by: Marcus Sanatan Co-authored-by: Claude Sonnet 4.5 --- .github/workflows/beta-release.yml | 44 +++++++---- .../OpenCodeConfigurator.cs.meta | 11 ++- .../Editor/Constants/EditorPrefKeys.cs | 1 + .../Editor/Helpers/AssetPathUtility.cs | 77 +++++++++++++++++++ .../Editor/Helpers/ConfigJsonBuilder.cs | 9 ++- .../Services/ServerManagementService.cs | 8 +- .../Components/Advanced/McpAdvancedSection.cs | 19 +++++ .../Advanced/McpAdvancedSection.uxml | 5 ++ .../Windows/EditorPrefs/EditorPrefsWindow.cs | 49 +++++++++++- .../Editor/Windows/MCPForUnityEditorWindow.cs | 21 +++-- Server/src/main.py | 12 ++- 11 files changed, 221 insertions(+), 35 deletions(-) diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml index 8467b02..b316c31 100644 --- a/.github/workflows/beta-release.yml +++ b/.github/workflows/beta-release.yml @@ -1,4 +1,4 @@ -name: Beta Release (TestPyPI) +name: Beta Release (PyPI Pre-release) concurrency: group: beta-release @@ -12,12 +12,12 @@ on: - "Server/**" jobs: - publish_testpypi: - name: Publish beta to TestPyPI + publish_pypi_prerelease: + name: Publish beta to PyPI (pre-release) runs-on: ubuntu-latest environment: - name: testpypi - url: https://test.pypi.org/p/mcpforunityserver + name: pypi + url: https://pypi.org/p/mcpforunityserver permissions: contents: read id-token: write @@ -33,27 +33,38 @@ jobs: enable-cache: true cache-dependency-glob: "Server/uv.lock" - - name: Generate dev version + - name: Generate beta version id: version shell: bash run: | set -euo pipefail - BASE_VERSION=$(grep -oP '(?<=version = ")[^"]+' Server/pyproject.toml) - # Use date for unique dev version (PEP 440 compliant: X.Y.Z.devN) - # Note: PyPI/TestPyPI don't support local version identifiers (+...) - DEV_NUMBER="$(date +%Y%m%d%H%M%S)" - DEV_VERSION="${BASE_VERSION}.dev${DEV_NUMBER}" + RAW_VERSION=$(grep -oP '(?<=version = ")[^"]+' Server/pyproject.toml) + # Strip any existing pre-release suffix (a, b, rc, dev, post) for safe parsing + # e.g., "9.2.0b1" -> "9.2.0", "9.2.0.dev1" -> "9.2.0" + BASE_VERSION=$(echo "$RAW_VERSION" | sed -E 's/(a|b|rc|\.dev|\.post)[0-9]+$//') + # Validate we have a proper X.Y.Z format + if ! [[ "$BASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Could not parse version '$RAW_VERSION' -> '$BASE_VERSION'" >&2 + exit 1 + fi + # Bump minor version and use beta suffix (PEP 440 compliant: X.Y+1.0bN) + # This ensures beta is "newer" than the stable release + IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE_VERSION" + NEXT_MINOR=$((MINOR + 1)) + BETA_NUMBER="$(date +%Y%m%d%H%M%S)" + BETA_VERSION="${MAJOR}.${NEXT_MINOR}.0b${BETA_NUMBER}" + echo "Raw version: $RAW_VERSION" echo "Base version: $BASE_VERSION" - echo "Dev version: $DEV_VERSION" - echo "dev_version=$DEV_VERSION" >> "$GITHUB_OUTPUT" + echo "Beta version: $BETA_VERSION" + echo "beta_version=$BETA_VERSION" >> "$GITHUB_OUTPUT" - name: Update version for beta release env: - DEV_VERSION: ${{ steps.version.outputs.dev_version }} + BETA_VERSION: ${{ steps.version.outputs.beta_version }} shell: bash run: | set -euo pipefail - sed -i "s/^version = .*/version = \"${DEV_VERSION}\"/" Server/pyproject.toml + sed -i "s/^version = .*/version = \"${BETA_VERSION}\"/" Server/pyproject.toml echo "Updated pyproject.toml:" grep "^version" Server/pyproject.toml @@ -62,8 +73,7 @@ jobs: run: uv build working-directory: ./Server - - name: Publish distribution to TestPyPI + - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: Server/dist/ - repository-url: https://test.pypi.org/legacy/ diff --git a/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs.meta b/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs.meta index 8094fb1..c3b334d 100644 --- a/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs.meta +++ b/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs.meta @@ -1,2 +1,11 @@ fileFormatVersion: 2 -guid: 489f99ffb7e6743e88e3203552c8b37b \ No newline at end of file +guid: 489f99ffb7e6743e88e3203552c8b37b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs index a6e81b4..60f81c1 100644 --- a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs +++ b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs @@ -28,6 +28,7 @@ namespace MCPForUnity.Editor.Constants internal const string WebSocketUrlOverride = "MCPForUnity.WebSocketUrl"; internal const string GitUrlOverride = "MCPForUnity.GitUrlOverride"; internal const string DevModeForceServerRefresh = "MCPForUnity.DevModeForceServerRefresh"; + internal const string UseBetaServer = "MCPForUnity.UseBetaServer"; internal const string ProjectScopedToolsLocalHttp = "MCPForUnity.ProjectScopedTools.LocalHttp"; internal const string PackageDeploySourcePath = "MCPForUnity.PackageDeploy.SourcePath"; diff --git a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs index c4169da..69c017e 100644 --- a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs +++ b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs @@ -243,6 +243,83 @@ namespace MCPForUnity.Editor.Helpers return (uvxPath, fromUrl, packageName); } + /// + /// Builds the uvx package source arguments for the MCP server. + /// Handles beta server mode (prerelease from PyPI) vs standard mode (pinned version or override). + /// Centralizes the prerelease logic to avoid duplication between HTTP and stdio transports. + /// Priority: explicit fromUrl override > beta server mode > default package. + /// + /// Whether to quote the --from path (needed for command-line strings, not for arg lists) + /// The package source arguments (e.g., "--prerelease explicit --from mcpforunityserver>=0.0.0a0") + public static string GetBetaServerFromArgs(bool quoteFromPath = false) + { + // Explicit override (local path, git URL, etc.) always wins + string fromUrl = GetMcpServerPackageSource(); + string overrideUrl = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, ""); + if (!string.IsNullOrEmpty(overrideUrl)) + { + return $"--from {fromUrl}"; + } + + // Beta server mode: use prerelease from PyPI + bool useBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true); + if (useBetaServer) + { + // Use --prerelease explicit with version specifier to only get prereleases of our package, + // not of dependencies (which can be broken on PyPI). + string fromValue = quoteFromPath ? "\"mcpforunityserver>=0.0.0a0\"" : "mcpforunityserver>=0.0.0a0"; + return $"--prerelease explicit --from {fromValue}"; + } + + // Standard mode: use pinned version from package.json + if (!string.IsNullOrEmpty(fromUrl)) + { + return $"--from {fromUrl}"; + } + + return string.Empty; + } + + /// + /// Builds the uvx package source arguments as a list (for JSON config builders). + /// Priority: explicit fromUrl override > beta server mode > default package. + /// + /// List of arguments to add to uvx command + public static System.Collections.Generic.IList GetBetaServerFromArgsList() + { + var args = new System.Collections.Generic.List(); + + // Explicit override (local path, git URL, etc.) always wins + string fromUrl = GetMcpServerPackageSource(); + string overrideUrl = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, ""); + if (!string.IsNullOrEmpty(overrideUrl)) + { + args.Add("--from"); + args.Add(fromUrl); + return args; + } + + // Beta server mode: use prerelease from PyPI + bool useBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true); + if (useBetaServer) + { + args.Add("--prerelease"); + args.Add("explicit"); + args.Add("--from"); + args.Add("mcpforunityserver>=0.0.0a0"); + return args; + } + + // Standard mode: use pinned version from package.json + if (!string.IsNullOrEmpty(fromUrl)) + { + args.Add("--from"); + args.Add(fromUrl); + } + + return args; + } + /// /// Determines whether uvx should use --no-cache --refresh flags. /// Returns true if DevModeForceServerRefresh is enabled OR if the server URL is a local path. diff --git a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs index dd00d73..118ef08 100644 --- a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs +++ b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using MCPForUnity.Editor.Constants; using MCPForUnity.Editor.Clients.Configurators; +using MCPForUnity.Editor.Constants; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Models; using Newtonsoft.Json; @@ -170,10 +170,11 @@ namespace MCPForUnity.Editor.Helpers args.Add("--no-cache"); args.Add("--refresh"); } - if (!string.IsNullOrEmpty(fromUrl)) + + // Use centralized helper for beta server / prerelease args + foreach (var arg in AssetPathUtility.GetBetaServerFromArgsList()) { - args.Add("--from"); - args.Add(fromUrl); + args.Add(arg); } args.Add(packageName); diff --git a/MCPForUnity/Editor/Services/ServerManagementService.cs b/MCPForUnity/Editor/Services/ServerManagementService.cs index b9e5ecd..bb51283 100644 --- a/MCPForUnity/Editor/Services/ServerManagementService.cs +++ b/MCPForUnity/Editor/Services/ServerManagementService.cs @@ -1317,9 +1317,13 @@ namespace MCPForUnity.Editor.Services true ); string scopedFlag = projectScopedTools ? " --project-scoped-tools" : string.Empty; - string args = string.IsNullOrEmpty(fromUrl) + + // Use centralized helper for beta server / prerelease args + string fromArgs = AssetPathUtility.GetBetaServerFromArgs(quoteFromPath: true); + + string args = string.IsNullOrEmpty(fromArgs) ? $"{devFlags}{packageName} --transport http --http-url {httpUrl}{scopedFlag}" - : $"{devFlags}--from {fromUrl} {packageName} --transport http --http-url {httpUrl}{scopedFlag}"; + : $"{devFlags}{fromArgs} {packageName} --transport http --http-url {httpUrl}{scopedFlag}"; fileName = uvxPath; arguments = args; diff --git a/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs b/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs index 26f6101..8898fcf 100644 --- a/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs +++ b/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs @@ -26,6 +26,7 @@ namespace MCPForUnity.Editor.Windows.Components.Advanced private Button clearGitUrlButton; private Toggle debugLogsToggle; private Toggle devModeForceRefreshToggle; + private Toggle useBetaServerToggle; private TextField deploySourcePath; private Button browseDeploySourceButton; private Button clearDeploySourceButton; @@ -42,6 +43,7 @@ namespace MCPForUnity.Editor.Windows.Components.Advanced public event Action OnGitUrlChanged; public event Action OnHttpServerCommandUpdateRequested; public event Action OnTestConnectionRequested; + public event Action OnBetaModeChanged; public VisualElement Root { get; private set; } @@ -64,6 +66,7 @@ namespace MCPForUnity.Editor.Windows.Components.Advanced clearGitUrlButton = Root.Q