diff --git a/MCPForUnity/Editor/Clients/Configurators/OpenClawConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/OpenClawConfigurator.cs index 1d800332e..ab6914926 100644 --- a/MCPForUnity/Editor/Clients/Configurators/OpenClawConfigurator.cs +++ b/MCPForUnity/Editor/Clients/Configurators/OpenClawConfigurator.cs @@ -387,6 +387,11 @@ private ConfiguredTransport ResolveTransport(JObject server) return ConfiguredTransport.HttpRemote; } + if (UrlsEqual(configuredUrl, HttpEndpointUtility.GetLanMcpRpcUrl())) + { + return ConfiguredTransport.HttpLan; + } + if (UrlsEqual(configuredUrl, HttpEndpointUtility.GetLocalMcpRpcUrl())) { return ConfiguredTransport.Http; diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs index 7b7f9b8cd..7b7e11fd5 100644 --- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs @@ -203,10 +203,15 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) // Distinguish HTTP Local from HTTP Remote by matching against both URLs string localRpcUrl = HttpEndpointUtility.GetLocalMcpRpcUrl(); string remoteRpcUrl = HttpEndpointUtility.GetRemoteMcpRpcUrl(); + string lanRpcUrl = HttpEndpointUtility.GetLanMcpRpcUrl(); if (!string.IsNullOrEmpty(remoteRpcUrl) && UrlsEqual(configuredUrl, remoteRpcUrl)) { client.configuredTransport = Models.ConfiguredTransport.HttpRemote; } + else if (!string.IsNullOrEmpty(lanRpcUrl) && UrlsEqual(configuredUrl, lanRpcUrl)) + { + client.configuredTransport = Models.ConfiguredTransport.HttpLan; + } else { client.configuredTransport = Models.ConfiguredTransport.Http; @@ -377,10 +382,15 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) { // Distinguish HTTP Local from HTTP Remote string remoteRpcUrl = HttpEndpointUtility.GetRemoteMcpRpcUrl(); + string lanRpcUrl = HttpEndpointUtility.GetLanMcpRpcUrl(); if (!string.IsNullOrEmpty(remoteRpcUrl) && UrlsEqual(url, remoteRpcUrl)) { client.configuredTransport = Models.ConfiguredTransport.HttpRemote; } + else if (!string.IsNullOrEmpty(lanRpcUrl) && UrlsEqual(url, lanRpcUrl)) + { + client.configuredTransport = Models.ConfiguredTransport.HttpLan; + } else { client.configuredTransport = Models.ConfiguredTransport.Http; @@ -581,15 +591,16 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath(); RuntimePlatform platform = Application.platform; bool isRemoteScope = HttpEndpointUtility.IsRemoteScope(); + bool isLanScope = HttpEndpointUtility.IsLanScope(); // Get expected package source for the installed package version (matches what Register() would use) string expectedPackageSource = GetExpectedPackageSourceForValidation(); - return CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, platform, isRemoteScope, expectedPackageSource, attemptAutoRewrite, HasClientProjectDirOverride); + return CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, platform, isRemoteScope, isLanScope, expectedPackageSource, attemptAutoRewrite, HasClientProjectDirOverride); } /// /// Internal thread-safe version of CheckStatus. /// Can be called from background threads because all main-thread-only values are passed as parameters. - /// projectDir, useHttpTransport, claudePath, platform, isRemoteScope, and expectedPackageSource are REQUIRED + /// projectDir, useHttpTransport, claudePath, platform, isRemoteScope, isLanScope, and expectedPackageSource are REQUIRED /// (non-nullable where applicable) to enforce thread safety at compile time. /// NOTE: attemptAutoRewrite is NOT fully thread-safe because Configure() requires the main thread. /// When called from a background thread, pass attemptAutoRewrite=false and handle re-registration @@ -597,7 +608,7 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) /// internal McpStatus CheckStatusWithProjectDir( string projectDir, bool useHttpTransport, string claudePath, RuntimePlatform platform, - bool isRemoteScope, string expectedPackageSource, + bool isRemoteScope, bool isLanScope, string expectedPackageSource, bool attemptAutoRewrite = false, bool hasProjectDirOverride = false) { try @@ -647,7 +658,7 @@ internal McpStatus CheckStatusWithProjectDir( { client.configuredTransport = isRemoteScope ? Models.ConfiguredTransport.HttpRemote - : Models.ConfiguredTransport.Http; + : (isLanScope ? Models.ConfiguredTransport.HttpLan : Models.ConfiguredTransport.Http); } else if (registeredWithStdio) { diff --git a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs index d8aa8e3f5..8344b6982 100644 --- a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs +++ b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs @@ -7,7 +7,7 @@ namespace MCPForUnity.Editor.Constants internal static class EditorPrefKeys { internal const string UseHttpTransport = "MCPForUnity.UseHttpTransport"; - internal const string HttpTransportScope = "MCPForUnity.HttpTransportScope"; // "local" | "remote" + internal const string HttpTransportScope = "MCPForUnity.HttpTransportScope"; // "local" | "remote" | "lan" internal const string LastLocalHttpServerPid = "MCPForUnity.LocalHttpServer.LastPid"; internal const string LastLocalHttpServerPort = "MCPForUnity.LocalHttpServer.LastPort"; internal const string LastLocalHttpServerStartedUtc = "MCPForUnity.LocalHttpServer.LastStartedUtc"; @@ -26,6 +26,8 @@ internal static class EditorPrefKeys internal const string HttpBaseUrl = "MCPForUnity.HttpUrl"; internal const string HttpRemoteBaseUrl = "MCPForUnity.HttpRemoteUrl"; + internal const string HttpLanPublicBaseUrl = "MCPForUnity.HttpLanPublicUrl"; + internal const string HttpLanBindBaseUrl = "MCPForUnity.HttpLanBindUrl"; internal const string SessionId = "MCPForUnity.SessionId"; internal const string WebSocketUrlOverride = "MCPForUnity.WebSocketUrl"; internal const string GitUrlOverride = "MCPForUnity.GitUrlOverride"; diff --git a/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs b/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs index 4f8064612..e8d8a149b 100644 --- a/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs +++ b/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs @@ -19,8 +19,12 @@ public static class HttpEndpointUtility { private const string LocalPrefKey = EditorPrefKeys.HttpBaseUrl; private const string RemotePrefKey = EditorPrefKeys.HttpRemoteBaseUrl; + private const string LanPublicPrefKey = EditorPrefKeys.HttpLanPublicBaseUrl; + private const string LanBindPrefKey = EditorPrefKeys.HttpLanBindBaseUrl; private const string DefaultLocalBaseUrl = "http://127.0.0.1:8080"; private const string DefaultRemoteBaseUrl = ""; + private const string DefaultLanPublicBaseUrl = "http://192.168.1.10:8090"; + private const string DefaultLanBindBaseUrl = "http://0.0.0.0:8090"; /// /// Returns the normalized base URL for the currently active HTTP scope. @@ -28,7 +32,9 @@ public static class HttpEndpointUtility /// public static string GetBaseUrl() { - return IsRemoteScope() ? GetRemoteBaseUrl() : GetLocalBaseUrl(); + if (IsRemoteScope()) return GetRemoteBaseUrl(); + if (IsLanScope()) return GetLanPublicBaseUrl(); + return GetLocalBaseUrl(); } /// @@ -40,6 +46,10 @@ public static void SaveBaseUrl(string userValue) { SaveRemoteBaseUrl(userValue); } + else if (IsLanScope()) + { + SaveLanPublicBaseUrl(userValue); + } else { SaveLocalBaseUrl(userValue); @@ -78,6 +88,47 @@ public static string GetRemoteBaseUrl() return NormalizeBaseUrl(stored, DefaultRemoteBaseUrl, remoteScope: true); } + /// + /// Returns the normalized LAN public URL that remote clients should use. + /// + public static string GetLanPublicBaseUrl() + { + string stored = EditorPrefs.GetString(LanPublicPrefKey, DefaultLanPublicBaseUrl); + return NormalizeBaseUrl(stored, DefaultLanPublicBaseUrl, remoteScope: false); + } + + /// + /// Saves the LAN public URL and keeps the bind URL on 0.0.0.0 with the same port. + /// + public static void SaveLanPublicBaseUrl(string userValue) + { + string normalized = NormalizeBaseUrl(userValue, DefaultLanPublicBaseUrl, remoteScope: false); + EditorPrefs.SetString(LanPublicPrefKey, normalized); + + if (Uri.TryCreate(normalized, UriKind.Absolute, out var uri) && uri.Port > 0) + { + EditorPrefs.SetString(LanBindPrefKey, $"http://0.0.0.0:{uri.Port}"); + } + } + + /// + /// Returns the LAN bind URL used to launch the local server. + /// + public static string GetLanBindBaseUrl() + { + string stored = EditorPrefs.GetString(LanBindPrefKey, DefaultLanBindBaseUrl); + return NormalizeBaseUrl(stored, DefaultLanBindBaseUrl, remoteScope: false); + } + + /// + /// Returns the URL used to launch/probe/stop the local server process. + /// LAN mode binds all interfaces while exposing a separate public client URL. + /// + public static string GetLocalServerLaunchBaseUrl() + { + return IsLanScope() ? GetLanBindBaseUrl() : GetLocalBaseUrl(); + } + /// /// Saves a user-provided URL to the remote HTTP pref. /// @@ -108,6 +159,14 @@ public static string GetLocalMcpRpcUrl() return AppendPathSegment(GetLocalBaseUrl(), "mcp"); } + /// + /// Builds the LAN public JSON-RPC endpoint (public base + /mcp). + /// + public static string GetLanMcpRpcUrl() + { + return AppendPathSegment(GetLanPublicBaseUrl(), "mcp"); + } + /// /// Builds the remote JSON-RPC endpoint (remote base + /mcp). /// Returns empty string if no remote URL is configured. @@ -135,6 +194,15 @@ public static bool IsRemoteScope() return string.Equals(scope, "remote", StringComparison.OrdinalIgnoreCase); } + /// + /// Returns true if the active HTTP transport scope is "lan". + /// + public static bool IsLanScope() + { + string scope = EditorConfigurationCache.Instance.HttpTransportScope; + return string.Equals(scope, "lan", StringComparison.OrdinalIgnoreCase); + } + /// /// Returns the that matches the current server-side /// transport selection (Stdio, Http, or HttpRemote). @@ -144,6 +212,7 @@ public static ConfiguredTransport GetCurrentServerTransport() { bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport; if (!useHttp) return ConfiguredTransport.Stdio; + if (IsLanScope()) return ConfiguredTransport.HttpLan; return IsRemoteScope() ? ConfiguredTransport.HttpRemote : ConfiguredTransport.Http; } @@ -249,6 +318,46 @@ public static bool IsHttpLocalUrlAllowedForLaunch(string url, out string error) return false; } + /// + /// Returns true when the URL is acceptable for LAN HTTP launch. + /// LAN mode intentionally binds all interfaces for trusted private networks. + /// + public static bool IsHttpLanUrlAllowedForLaunch(string url, out string error) + { + error = null; + if (string.IsNullOrWhiteSpace(url)) + { + error = "LAN HTTP requires a bind URL such as http://0.0.0.0:8090."; + return false; + } + + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) + { + error = $"Invalid LAN HTTP bind URL: {url}"; + return false; + } + + if (!uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase)) + { + error = "LAN HTTP bind URL must use http://."; + return false; + } + + if (uri.Port <= 0) + { + error = "LAN HTTP bind URL requires an explicit port."; + return false; + } + + if (IsBindAllInterfacesHost(uri.Host)) + { + return true; + } + + error = "LAN HTTP server bind host must be 0.0.0.0 or ::."; + return false; + } + /// /// Returns true when remote URL is allowed by current security policy. /// HTTPS is required by default; HTTP needs explicit opt-in. diff --git a/MCPForUnity/Editor/Models/McpStatus.cs b/MCPForUnity/Editor/Models/McpStatus.cs index dfc04fdd0..bda0430da 100644 --- a/MCPForUnity/Editor/Models/McpStatus.cs +++ b/MCPForUnity/Editor/Models/McpStatus.cs @@ -25,7 +25,8 @@ public enum ConfiguredTransport Unknown, // Could not determine transport type Stdio, // Client configured for stdio transport Http, // Client configured for HTTP local transport - HttpRemote // Client configured for HTTP remote-hosted transport + HttpRemote, // Client configured for HTTP remote-hosted transport + HttpLan // Client configured for LAN HTTP transport } } diff --git a/MCPForUnity/Editor/Services/HttpAutoStartHandler.cs b/MCPForUnity/Editor/Services/HttpAutoStartHandler.cs index 981dc2141..6c10e6cab 100644 --- a/MCPForUnity/Editor/Services/HttpAutoStartHandler.cs +++ b/MCPForUnity/Editor/Services/HttpAutoStartHandler.cs @@ -73,8 +73,12 @@ private static async Task AutoStartAsync() { // For HTTP Local: launch the server process first, then connect the bridge. // This mirrors what the UI "Start Server" button does. - if (!HttpEndpointUtility.IsHttpLocalUrlAllowedForLaunch( - HttpEndpointUtility.GetLocalBaseUrl(), out string policyError)) + string launchBaseUrl = HttpEndpointUtility.GetLocalServerLaunchBaseUrl(); + string policyError; + bool launchAllowed = HttpEndpointUtility.IsLanScope() + ? HttpEndpointUtility.IsHttpLanUrlAllowedForLaunch(launchBaseUrl, out policyError) + : HttpEndpointUtility.IsHttpLocalUrlAllowedForLaunch(launchBaseUrl, out policyError); + if (!launchAllowed) { McpLog.Debug($"[HTTP Auto-Start] Local URL blocked by security policy: {policyError}"); return; diff --git a/MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs b/MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs index 9b4bd6143..8ca16c371 100644 --- a/MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs +++ b/MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs @@ -51,6 +51,7 @@ private static void OnEditorQuitting() bool httpLocalSelected = useHttp && (string.Equals(scope, "local", StringComparison.OrdinalIgnoreCase) + || string.Equals(scope, "lan", StringComparison.OrdinalIgnoreCase) || (string.IsNullOrEmpty(scope) && MCPServiceLocator.Server.IsLocalUrl())); if (httpLocalSelected) diff --git a/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs b/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs index 3e505634a..09243157b 100644 --- a/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs +++ b/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs @@ -30,8 +30,12 @@ public bool TryBuildCommand(out string fileName, out string arguments, out strin return false; } - string httpUrl = HttpEndpointUtility.GetLocalBaseUrl(); - if (!HttpEndpointUtility.IsHttpLocalUrlAllowedForLaunch(httpUrl, out string localUrlError)) + string httpUrl = HttpEndpointUtility.GetLocalServerLaunchBaseUrl(); + string localUrlError; + bool launchUrlAllowed = HttpEndpointUtility.IsLanScope() + ? HttpEndpointUtility.IsHttpLanUrlAllowedForLaunch(httpUrl, out localUrlError) + : HttpEndpointUtility.IsHttpLocalUrlAllowedForLaunch(httpUrl, out localUrlError); + if (!launchUrlAllowed) { error = string.IsNullOrEmpty(localUrlError) ? $"The configured URL ({httpUrl}) is not allowed for HTTP Local launch." diff --git a/MCPForUnity/Editor/Services/ServerManagementService.cs b/MCPForUnity/Editor/Services/ServerManagementService.cs index d1f6a0fc2..37a93381a 100644 --- a/MCPForUnity/Editor/Services/ServerManagementService.cs +++ b/MCPForUnity/Editor/Services/ServerManagementService.cs @@ -256,7 +256,7 @@ public bool StartLocalHttpServer(bool quiet = false) // If the port is still occupied, don't start and explain why (avoid confusing "refusing to stop" warnings). try { - string httpUrl = HttpEndpointUtility.GetLocalBaseUrl(); + string httpUrl = HttpEndpointUtility.GetLocalServerLaunchBaseUrl(); if (Uri.TryCreate(httpUrl, UriKind.Absolute, out var uri) && uri.Port > 0) { var remaining = GetListeningProcessIdsForPort(uri.Port); @@ -280,7 +280,7 @@ public bool StartLocalHttpServer(bool quiet = false) // Note: Dev mode cache-busting is handled by `uvx --no-cache --refresh` in the generated command. // Create a per-launch token + pidfile path so Stop can be deterministic without relying on port/PID heuristics. - string baseUrlForPid = HttpEndpointUtility.GetLocalBaseUrl(); + string baseUrlForPid = HttpEndpointUtility.GetLocalServerLaunchBaseUrl(); Uri.TryCreate(baseUrlForPid, UriKind.Absolute, out var uriForPid); int portForPid = uriForPid?.Port ?? 0; string instanceToken = Guid.NewGuid().ToString("N"); @@ -359,7 +359,7 @@ public bool StopManagedLocalHttpServer() int port = 0; if (!TryGetPortFromPidFilePath(pidFilePath, out port) || port <= 0) { - string baseUrl = HttpEndpointUtility.GetLocalBaseUrl(); + string baseUrl = HttpEndpointUtility.GetLocalServerLaunchBaseUrl(); if (IsLocalUrl(baseUrl) && Uri.TryCreate(baseUrl, UriKind.Absolute, out var uri) && uri.Port > 0) @@ -380,7 +380,7 @@ public bool IsLocalHttpServerRunning() { try { - string httpUrl = HttpEndpointUtility.GetLocalBaseUrl(); + string httpUrl = HttpEndpointUtility.GetLocalServerLaunchBaseUrl(); if (!IsLocalUrl(httpUrl)) { return false; @@ -442,7 +442,7 @@ public bool IsLocalHttpServerReachable() { try { - string httpUrl = HttpEndpointUtility.GetLocalBaseUrl(); + string httpUrl = HttpEndpointUtility.GetLocalServerLaunchBaseUrl(); if (!IsLocalUrl(httpUrl)) { return false; @@ -543,7 +543,7 @@ private static void AddHostCandidate(List hosts, string candidate) private bool StopLocalHttpServerInternal(bool quiet, int? portOverride = null, bool allowNonLocalUrl = false) { - string httpUrl = HttpEndpointUtility.GetLocalBaseUrl(); + string httpUrl = HttpEndpointUtility.GetLocalServerLaunchBaseUrl(); if (!allowNonLocalUrl && !IsLocalUrl(httpUrl)) { if (!quiet) @@ -898,7 +898,7 @@ private bool TryGetLocalHttpServerCommandParts(out string fileName, out string a /// public bool IsLocalUrl() { - string httpUrl = HttpEndpointUtility.GetLocalBaseUrl(); + string httpUrl = HttpEndpointUtility.GetLocalServerLaunchBaseUrl(); return IsLocalUrl(httpUrl); } @@ -933,8 +933,10 @@ public bool CanStartLocalServer() return false; } - string httpUrl = HttpEndpointUtility.GetLocalBaseUrl(); - return HttpEndpointUtility.IsHttpLocalUrlAllowedForLaunch(httpUrl, out _); + string httpUrl = HttpEndpointUtility.GetLocalServerLaunchBaseUrl(); + return HttpEndpointUtility.IsLanScope() + ? HttpEndpointUtility.IsHttpLanUrlAllowedForLaunch(httpUrl, out _) + : HttpEndpointUtility.IsHttpLocalUrlAllowedForLaunch(httpUrl, out _); } private System.Diagnostics.ProcessStartInfo CreateTerminalProcessStartInfo(string command) diff --git a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs index 294a50f94..25a3c8678 100644 --- a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs +++ b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs @@ -620,6 +620,7 @@ private void RefreshClaudeCliStatus(IMcpClientConfigurator client, bool forceImm string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath(); RuntimePlatform platform = Application.platform; bool isRemoteScope = HttpEndpointUtility.IsRemoteScope(); + bool isLanScope = HttpEndpointUtility.IsLanScope(); // Get expected package source based on installed package version and overrides. string expectedPackageSource = GetExpectedPackageSourceForCurrentPackage(); bool hasProjectDirOverride = ClaudeCliMcpConfigurator.HasClientProjectDirOverride; @@ -631,7 +632,7 @@ private void RefreshClaudeCliStatus(IMcpClientConfigurator client, bool forceImm if (client is ClaudeCliMcpConfigurator claudeConfigurator) { // Use thread-safe version with captured main-thread values - claudeConfigurator.CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, platform, isRemoteScope, expectedPackageSource, attemptAutoRewrite: false, hasProjectDirOverride: hasProjectDirOverride); + claudeConfigurator.CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, platform, isRemoteScope, isLanScope, expectedPackageSource, attemptAutoRewrite: false, hasProjectDirOverride: hasProjectDirOverride); } }).ContinueWith(t => { diff --git a/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs b/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs index ce0619866..3b89a30ab 100644 --- a/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs +++ b/MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs @@ -23,6 +23,7 @@ public class McpConnectionSection private enum TransportProtocol { HTTPLocal, + HTTPLan, HTTPRemote, Stdio } @@ -34,6 +35,7 @@ private enum TransportProtocol private VisualElement versionMismatchWarning; private Label versionMismatchText; private VisualElement httpUrlRow; + private VisualElement lanHttpWarning; private VisualElement httpServerControlRow; private Foldout manualCommandFoldout; private VisualElement httpServerCommandSection; @@ -92,6 +94,7 @@ private void CacheUIElements() versionMismatchWarning = Root.Q("version-mismatch-warning"); versionMismatchText = Root.Q