Skip to content

Commit f95a3f9

Browse files
liuzqkgithub-actions[bot]
authored andcommitted
Scope HTTP endpoint and server ownership per project
1 parent bd72241 commit f95a3f9

19 files changed

Lines changed: 1003 additions & 175 deletions

MCPForUnity/Editor/Helpers/CodexConfigHelper.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
using MCPForUnity.Editor.Constants;
66
using MCPForUnity.Editor.Services;
77
using MCPForUnity.External.Tommy;
8-
using UnityEditor;
9-
using UnityEngine;
108

119
namespace MCPForUnity.Editor.Helpers
1210
{
@@ -31,7 +29,7 @@ public static string BuildCodexServerBlock(string uvPath)
3129
var unityMCP = new TomlTable();
3230

3331
// Check transport preference
34-
bool useHttpTransport = EditorPrefs.GetBool(MCPForUnity.Editor.Constants.EditorPrefKeys.UseHttpTransport, true);
32+
bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;
3533

3634
if (useHttpTransport)
3735
{
@@ -88,7 +86,7 @@ public static string UpsertCodexServerBlock(string existingToml, string uvPath)
8886
// Parse existing TOML or create new root table
8987
var root = TryParseToml(existingToml) ?? new TomlTable();
9088

91-
bool useHttpTransport = EditorPrefs.GetBool(MCPForUnity.Editor.Constants.EditorPrefKeys.UseHttpTransport, true);
89+
bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;
9290

9391
// Ensure mcp_servers table exists
9492
if (!root.TryGetNode("mcp_servers", out var mcpServersNode) || !(mcpServersNode is TomlTable))
@@ -186,7 +184,7 @@ private static TomlTable CreateUnityMcpTable(string uvPath)
186184
var unityMCP = new TomlTable();
187185

188186
// Check transport preference
189-
bool useHttpTransport = EditorPrefs.GetBool(MCPForUnity.Editor.Constants.EditorPrefKeys.UseHttpTransport, true);
187+
bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;
190188

191189
if (useHttpTransport)
192190
{

MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,19 @@ public static class HttpEndpointUtility
2121
private const string RemotePrefKey = EditorPrefKeys.HttpRemoteBaseUrl;
2222
private const string DefaultLocalBaseUrl = "http://127.0.0.1:8080";
2323
private const string DefaultRemoteBaseUrl = "";
24+
private const string LocalBaseUrlEnvironmentVariable = "UNITY_MCP_HTTP_URL";
2425

2526
/// <summary>
2627
/// Returns the normalized base URL for the currently active HTTP scope.
2728
/// If the scope is "remote", returns the remote URL; otherwise returns the local URL.
2829
/// </summary>
2930
public static string GetBaseUrl()
3031
{
32+
if (TryGetLocalEnvironmentOverride(out string environmentOverride))
33+
{
34+
return environmentOverride;
35+
}
36+
3137
return IsRemoteScope() ? GetRemoteBaseUrl() : GetLocalBaseUrl();
3238
}
3339

@@ -51,7 +57,12 @@ public static void SaveBaseUrl(string userValue)
5157
/// </summary>
5258
public static string GetLocalBaseUrl()
5359
{
54-
string stored = EditorPrefs.GetString(LocalPrefKey, DefaultLocalBaseUrl);
60+
if (TryGetLocalEnvironmentOverride(out string environmentOverride))
61+
{
62+
return environmentOverride;
63+
}
64+
65+
string stored = ProjectScopedEditorPrefs.GetString(LocalPrefKey, DefaultLocalBaseUrl);
5566
return NormalizeBaseUrl(stored, DefaultLocalBaseUrl, remoteScope: false);
5667
}
5768

@@ -61,7 +72,7 @@ public static string GetLocalBaseUrl()
6172
public static void SaveLocalBaseUrl(string userValue)
6273
{
6374
string normalized = NormalizeBaseUrl(userValue, DefaultLocalBaseUrl, remoteScope: false);
64-
EditorPrefs.SetString(LocalPrefKey, normalized);
75+
ProjectScopedEditorPrefs.SetString(LocalPrefKey, normalized);
6576
}
6677

6778
/// <summary>
@@ -131,6 +142,11 @@ public static string GetRegisterToolsUrl()
131142
/// </summary>
132143
public static bool IsRemoteScope()
133144
{
145+
if (TryGetLocalEnvironmentOverride(out _))
146+
{
147+
return false;
148+
}
149+
134150
string scope = EditorConfigurationCache.Instance.HttpTransportScope;
135151
return string.Equals(scope, "remote", StringComparison.OrdinalIgnoreCase);
136152
}
@@ -227,6 +243,13 @@ public static bool IsHttpLocalUrlAllowedForLaunch(string url, out string error)
227243
return false;
228244
}
229245

246+
if (!uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase)
247+
&& !uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
248+
{
249+
error = $"Unsupported URL scheme '{uri.Scheme}'. Use http:// or https://.";
250+
return false;
251+
}
252+
230253
string host = uri.Host;
231254
if (IsLoopbackHost(host))
232255
{
@@ -350,6 +373,29 @@ private static string NormalizeBaseUrl(string value, string defaultUrl, bool rem
350373
return trimmed;
351374
}
352375

376+
private static bool TryGetLocalEnvironmentOverride(out string normalized)
377+
{
378+
string value = Environment.GetEnvironmentVariable(LocalBaseUrlEnvironmentVariable);
379+
if (string.IsNullOrWhiteSpace(value))
380+
{
381+
normalized = null;
382+
return false;
383+
}
384+
385+
string candidate = NormalizeBaseUrl(
386+
value,
387+
DefaultLocalBaseUrl,
388+
remoteScope: false);
389+
if (!IsHttpLocalUrlAllowedForLaunch(candidate, out _))
390+
{
391+
normalized = null;
392+
return false;
393+
}
394+
395+
normalized = candidate;
396+
return true;
397+
}
398+
353399
private static string AppendPathSegment(string baseUrl, string segment)
354400
{
355401
return $"{baseUrl.TrimEnd('/')}/{segment}";

MCPForUnity/Editor/Helpers/ProjectIdentityUtility.cs

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.IO;
34
using System.Security.Cryptography;
45
using System.Text;
6+
using System.Threading;
57
using MCPForUnity.Editor.Constants;
68
using UnityEditor;
79
using UnityEngine;
@@ -257,4 +259,191 @@ private static string TryComputeFallbackProjectHash()
257259
}
258260
}
259261
}
262+
263+
/// <summary>
264+
/// Stores connection and local-server lifecycle preferences per Unity project while
265+
/// retaining a single-owner read fallback for legacy machine-global preferences.
266+
/// </summary>
267+
internal static class ProjectScopedEditorPrefs
268+
{
269+
private const string MigrationOwnerKey = "MCPForUnity.ProjectScopedPrefs.MigrationOwner";
270+
private const string MigrationMutexName = "MCPForUnity.ProjectPreferenceMigration";
271+
private static readonly HashSet<string> KnownProjectScopedKeys = new HashSet<string>
272+
{
273+
EditorPrefKeys.UseHttpTransport,
274+
EditorPrefKeys.HttpBaseUrl,
275+
EditorPrefKeys.HttpTransportScope,
276+
EditorPrefKeys.LastLocalHttpServerPid,
277+
EditorPrefKeys.LastLocalHttpServerPort,
278+
EditorPrefKeys.LastLocalHttpServerStartedUtc,
279+
EditorPrefKeys.LastLocalHttpServerPidArgsHash,
280+
EditorPrefKeys.LastLocalHttpServerPidFilePath,
281+
EditorPrefKeys.LastLocalHttpServerInstanceToken,
282+
};
283+
284+
internal static string GetKey(string baseKey, string projectHash = null)
285+
{
286+
string hash = string.IsNullOrWhiteSpace(projectHash)
287+
? ProjectIdentityUtility.GetProjectHash()
288+
: projectHash;
289+
return $"{baseKey}.{hash}";
290+
}
291+
292+
internal static string GetMigrationOwnerKey()
293+
{
294+
return MigrationOwnerKey;
295+
}
296+
297+
internal static string ResolveStorageKey(string baseKey, string projectHash = null)
298+
{
299+
return KnownProjectScopedKeys.Contains(baseKey) ? GetKey(baseKey, projectHash) : baseKey;
300+
}
301+
302+
internal static bool HasKey(string baseKey)
303+
{
304+
return EditorPrefs.HasKey(ResolveStorageKey(baseKey));
305+
}
306+
307+
internal static bool GetBool(
308+
string baseKey,
309+
bool defaultValue,
310+
string projectHash = null,
311+
bool allowLegacyFallback = true)
312+
{
313+
string scopedKey = ResolveStorageKey(baseKey, projectHash);
314+
if (EditorPrefs.HasKey(scopedKey))
315+
{
316+
return EditorPrefs.GetBool(scopedKey, defaultValue);
317+
}
318+
319+
if (allowLegacyFallback
320+
&& EditorPrefs.HasKey(baseKey)
321+
&& TryClaimLegacyPreference(projectHash))
322+
{
323+
bool migrated = EditorPrefs.GetBool(baseKey, defaultValue);
324+
EditorPrefs.SetBool(scopedKey, migrated);
325+
return migrated;
326+
}
327+
328+
return defaultValue;
329+
}
330+
331+
internal static int GetInt(
332+
string baseKey,
333+
int defaultValue,
334+
string projectHash = null,
335+
bool allowLegacyFallback = true)
336+
{
337+
string scopedKey = ResolveStorageKey(baseKey, projectHash);
338+
if (EditorPrefs.HasKey(scopedKey))
339+
{
340+
return EditorPrefs.GetInt(scopedKey, defaultValue);
341+
}
342+
343+
if (allowLegacyFallback
344+
&& EditorPrefs.HasKey(baseKey)
345+
&& TryClaimLegacyPreference(projectHash))
346+
{
347+
int migrated = EditorPrefs.GetInt(baseKey, defaultValue);
348+
EditorPrefs.SetInt(scopedKey, migrated);
349+
return migrated;
350+
}
351+
352+
return defaultValue;
353+
}
354+
355+
internal static string GetString(
356+
string baseKey,
357+
string defaultValue,
358+
string projectHash = null,
359+
bool allowLegacyFallback = true)
360+
{
361+
string scopedKey = ResolveStorageKey(baseKey, projectHash);
362+
if (EditorPrefs.HasKey(scopedKey))
363+
{
364+
return EditorPrefs.GetString(scopedKey, defaultValue);
365+
}
366+
367+
if (allowLegacyFallback
368+
&& EditorPrefs.HasKey(baseKey)
369+
&& TryClaimLegacyPreference(projectHash))
370+
{
371+
string migrated = EditorPrefs.GetString(baseKey, defaultValue);
372+
EditorPrefs.SetString(scopedKey, migrated);
373+
return migrated;
374+
}
375+
376+
return defaultValue;
377+
}
378+
379+
internal static void SetBool(string baseKey, bool value)
380+
{
381+
EditorPrefs.SetBool(ResolveStorageKey(baseKey), value);
382+
}
383+
384+
internal static void SetInt(string baseKey, int value)
385+
{
386+
EditorPrefs.SetInt(ResolveStorageKey(baseKey), value);
387+
}
388+
389+
internal static void SetString(string baseKey, string value)
390+
{
391+
EditorPrefs.SetString(ResolveStorageKey(baseKey), value);
392+
}
393+
394+
internal static void DeleteKey(string baseKey)
395+
{
396+
EditorPrefs.DeleteKey(ResolveStorageKey(baseKey));
397+
}
398+
399+
private static bool TryClaimLegacyPreference(string projectHash)
400+
{
401+
string hash = string.IsNullOrWhiteSpace(projectHash)
402+
? ProjectIdentityUtility.GetProjectHash()
403+
: projectHash;
404+
string ownerKey = GetMigrationOwnerKey();
405+
406+
Mutex migrationMutex = null;
407+
bool lockTaken = false;
408+
try
409+
{
410+
migrationMutex = new Mutex(false, MigrationMutexName);
411+
try
412+
{
413+
lockTaken = migrationMutex.WaitOne(TimeSpan.FromSeconds(2));
414+
}
415+
catch (AbandonedMutexException)
416+
{
417+
lockTaken = true;
418+
}
419+
420+
if (!lockTaken)
421+
{
422+
return false;
423+
}
424+
425+
string owner = EditorPrefs.GetString(ownerKey, string.Empty);
426+
if (string.IsNullOrEmpty(owner))
427+
{
428+
EditorPrefs.SetString(ownerKey, hash);
429+
owner = EditorPrefs.GetString(ownerKey, string.Empty);
430+
}
431+
432+
return string.Equals(owner, hash, StringComparison.Ordinal);
433+
}
434+
catch
435+
{
436+
return false;
437+
}
438+
finally
439+
{
440+
if (lockTaken)
441+
{
442+
try { migrationMutex.ReleaseMutex(); } catch { }
443+
}
444+
445+
migrationMutex?.Dispose();
446+
}
447+
}
448+
}
260449
}

MCPForUnity/Editor/McpCiBoot.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
2-
using MCPForUnity.Editor.Constants;
2+
using MCPForUnity.Editor.Services;
33
using MCPForUnity.Editor.Services.Transport.Transports;
4-
using UnityEditor;
54

65
namespace MCPForUnity.Editor
76
{
@@ -11,7 +10,7 @@ public static void StartStdioForCi()
1110
{
1211
try
1312
{
14-
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, false);
13+
EditorConfigurationCache.Instance.SetUseHttpTransport(false);
1514
}
1615
catch { /* ignore */ }
1716

0 commit comments

Comments
 (0)