Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 27 additions & 17 deletions .github/workflows/beta-release.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Beta Release (TestPyPI)
name: Beta Release (PyPI Pre-release)

concurrency:
group: beta-release
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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/

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

1 change: 1 addition & 0 deletions MCPForUnity/Editor/Constants/EditorPrefKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ internal static class EditorPrefKeys
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";
Expand Down
77 changes: 77 additions & 0 deletions MCPForUnity/Editor/Helpers/AssetPathUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,83 @@ public static (string uvxPath, string fromUrl, string packageName) GetUvxCommand
return (uvxPath, fromUrl, packageName);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="quoteFromPath">Whether to quote the --from path (needed for command-line strings, not for arg lists)</param>
/// <returns>The package source arguments (e.g., "--prerelease explicit --from mcpforunityserver>=0.0.0a0")</returns>
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;
}

/// <summary>
/// Builds the uvx package source arguments as a list (for JSON config builders).
/// Priority: explicit fromUrl override > beta server mode > default package.
/// </summary>
/// <returns>List of arguments to add to uvx command</returns>
public static System.Collections.Generic.IList<string> GetBetaServerFromArgsList()
{
var args = new System.Collections.Generic.List<string>();

// 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;
}

/// <summary>
/// Determines whether uvx should use --no-cache --refresh flags.
/// Returns true if DevModeForceServerRefresh is enabled OR if the server URL is a local path.
Expand Down
9 changes: 5 additions & 4 deletions MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
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;
Expand Down Expand Up @@ -170,10 +170,11 @@ private static IList<string> BuildUvxArgs(string fromUrl, string packageName)
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);

Expand Down
8 changes: 6 additions & 2 deletions MCPForUnity/Editor/Services/ServerManagementService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1317,9 +1317,13 @@ private bool TryGetLocalHttpServerCommandParts(out string fileName, out string a
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class McpAdvancedSection
private Button clearGitUrlButton;
private Toggle debugLogsToggle;
private Toggle devModeForceRefreshToggle;
private Toggle useBetaServerToggle;
private TextField deploySourcePath;
private Button browseDeploySourceButton;
private Button clearDeploySourceButton;
Expand All @@ -42,6 +43,7 @@ public class McpAdvancedSection
public event Action OnGitUrlChanged;
public event Action OnHttpServerCommandUpdateRequested;
public event Action OnTestConnectionRequested;
public event Action<bool> OnBetaModeChanged;

public VisualElement Root { get; private set; }

Expand All @@ -64,6 +66,7 @@ private void CacheUIElements()
clearGitUrlButton = Root.Q<Button>("clear-git-url-button");
debugLogsToggle = Root.Q<Toggle>("debug-logs-toggle");
devModeForceRefreshToggle = Root.Q<Toggle>("dev-mode-force-refresh-toggle");
useBetaServerToggle = Root.Q<Toggle>("use-beta-server-toggle");
deploySourcePath = Root.Q<TextField>("deploy-source-path");
browseDeploySourceButton = Root.Q<Button>("browse-deploy-source-button");
clearDeploySourceButton = Root.Q<Button>("clear-deploy-source-button");
Expand Down Expand Up @@ -98,6 +101,13 @@ private void InitializeUI()
if (forceRefreshLabel != null)
forceRefreshLabel.tooltip = devModeForceRefreshToggle.tooltip;
}
if (useBetaServerToggle != null)
{
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.";
var betaServerLabel = useBetaServerToggle?.parent?.Q<Label>();
if (betaServerLabel != null)
betaServerLabel.tooltip = useBetaServerToggle.tooltip;
}
if (testConnectionButton != null)
testConnectionButton.tooltip = "Test the connection between Unity and the MCP server.";
if (deploySourcePath != null)
Expand Down Expand Up @@ -128,6 +138,7 @@ private void InitializeUI()
McpLog.SetDebugLoggingEnabled(debugEnabled);

devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
useBetaServerToggle.value = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true);
UpdatePathOverrides();
UpdateDeploymentSection();
}
Expand Down Expand Up @@ -172,6 +183,13 @@ private void RegisterCallbacks()
OnHttpServerCommandUpdateRequested?.Invoke();
});

useBetaServerToggle.RegisterValueChangedCallback(evt =>
{
EditorPrefs.SetBool(EditorPrefKeys.UseBetaServer, evt.newValue);
OnHttpServerCommandUpdateRequested?.Invoke();
OnBetaModeChanged?.Invoke(evt.newValue);
});

deploySourcePath.RegisterValueChangedCallback(evt =>
{
string path = evt.newValue?.Trim();
Expand Down Expand Up @@ -274,6 +292,7 @@ public void UpdatePathOverrides()
gitUrlOverride.value = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
debugLogsToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
useBetaServerToggle.value = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true);
UpdateDeploymentSection();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
<ui:Toggle name="dev-mode-force-refresh-toggle" class="setting-toggle" />
</ui:VisualElement>

<ui:VisualElement class="setting-row">
<ui:Label text="Use Beta Server:" class="setting-label" />
<ui:Toggle name="use-beta-server-toggle" class="setting-toggle" />
</ui:VisualElement>

<ui:VisualElement class="override-row" style="margin-top: 8px;">
<ui:Label text="Package Source:" class="override-label" />
</ui:VisualElement>
Expand Down
49 changes: 47 additions & 2 deletions MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public class EditorPrefsWindow : EditorWindow
// UI Elements
private ScrollView scrollView;
private VisualElement prefsContainer;
private TextField searchField;
private string searchFilter = "";

// Data
private List<EditorPrefItem> currentPrefs = new List<EditorPrefItem>();
Expand All @@ -40,6 +42,7 @@ public class EditorPrefsWindow : EditorWindow
{ EditorPrefKeys.CustomToolRegistrationEnabled, EditorPrefType.Bool },
{ EditorPrefKeys.TelemetryDisabled, EditorPrefType.Bool },
{ EditorPrefKeys.DevModeForceServerRefresh, EditorPrefType.Bool },
{ EditorPrefKeys.UseBetaServer, EditorPrefType.Bool },
{ EditorPrefKeys.ProjectScopedToolsLocalHttp, EditorPrefType.Bool },

// Integer prefs
Expand Down Expand Up @@ -105,7 +108,39 @@ public void CreateGUI()
}

visualTree.CloneTree(rootVisualElement);


// Add search bar container at the top
var searchContainer = new VisualElement();
searchContainer.style.flexDirection = FlexDirection.Row;
searchContainer.style.marginTop = 8;
searchContainer.style.marginBottom = 20;
searchContainer.style.marginLeft = 4;
searchContainer.style.marginRight = 4;

searchField = new TextField("Search");
searchField.style.flexGrow = 1;
searchField.style.height = 28;
searchField.style.paddingTop = 2;
searchField.style.paddingBottom = 2;
searchField.labelElement.style.unityFontStyleAndWeight = FontStyle.Bold;
searchField.RegisterValueChangedCallback(evt =>
{
searchFilter = evt.newValue ?? "";
RefreshPrefs();
});

var refreshButton = new Button(RefreshPrefs);
refreshButton.text = "↻";
refreshButton.tooltip = "Refresh prefs";
refreshButton.style.width = 30;
refreshButton.style.height = 28;
refreshButton.style.marginLeft = 6;
refreshButton.style.backgroundColor = new Color(0.9f, 0.5f, 0.1f);

searchContainer.Add(searchField);
searchContainer.Add(refreshButton);
rootVisualElement.Insert(0, searchContainer);

// Get references
scrollView = rootVisualElement.Q<ScrollView>("scroll-view");
prefsContainer = rootVisualElement.Q<VisualElement>("prefs-container");
Expand Down Expand Up @@ -154,13 +189,23 @@ private void RefreshPrefs()

// Sort keys
allKeys.Sort();


// Pre-trim filter once outside the loop
var filter = searchFilter?.Trim();

// Create items for existing prefs
foreach (var key in allKeys)
{
// Skip Customer UUID but show everything else that's defined
if (key != EditorPrefKeys.CustomerUuid)
{
// Apply search filter using OrdinalIgnoreCase for fewer allocations
if (!string.IsNullOrEmpty(filter) &&
key.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0)
{
continue;
}

var item = CreateEditorPrefItem(key);
if (item != null)
{
Expand Down
Loading