Skip to content

Commit 64b1e84

Browse files
authored
Merge pull request #1053 from Scriptwonder/fix/restore-openclaw
Fix/restore openclaw
2 parents fa68b39 + 0bc8b4c commit 64b1e84

6 files changed

Lines changed: 430 additions & 6 deletions

File tree

Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using MCPForUnity.Editor.Constants;
5+
using MCPForUnity.Editor.Helpers;
6+
using MCPForUnity.Editor.Models;
7+
using MCPForUnity.Editor.Services;
8+
using Newtonsoft.Json;
9+
using Newtonsoft.Json.Linq;
10+
using UnityEditor;
11+
12+
namespace MCPForUnity.Editor.Clients.Configurators
13+
{
14+
/// <summary>
15+
/// Configurator for OpenClaw via the openclaw-mcp-bridge plugin.
16+
/// OpenClaw stores config at ~/.openclaw/openclaw.json.
17+
/// </summary>
18+
public class OpenClawConfigurator : McpClientConfiguratorBase
19+
{
20+
private const string PluginName = "openclaw-mcp-bridge";
21+
private const string ServerName = "unityMCP";
22+
private const string HttpTransportName = "http";
23+
private const string StdioTransportName = "stdio";
24+
private const string StdioUrl = "stdio://local";
25+
26+
public OpenClawConfigurator() : base(new McpClient
27+
{
28+
name = "OpenClaw",
29+
windowsConfigPath = BuildConfigPath(),
30+
macConfigPath = BuildConfigPath(),
31+
linuxConfigPath = BuildConfigPath()
32+
})
33+
{ }
34+
35+
private static string BuildConfigPath()
36+
{
37+
return Path.Combine(
38+
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
39+
".openclaw",
40+
"openclaw.json");
41+
}
42+
43+
public override string GetConfigPath() => CurrentOsPath();
44+
45+
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
46+
{
47+
try
48+
{
49+
string path = GetConfigPath();
50+
if (!File.Exists(path))
51+
{
52+
client.SetStatus(McpStatus.NotConfigured);
53+
client.configuredTransport = ConfiguredTransport.Unknown;
54+
return client.status;
55+
}
56+
57+
JObject root = LoadConfig(path);
58+
JObject pluginEntry = root["plugins"]?["entries"]?[PluginName] as JObject;
59+
JObject unityServer = FindUnityServer(pluginEntry?["config"]?["servers"]);
60+
61+
if (pluginEntry == null || unityServer == null)
62+
{
63+
client.SetStatus(McpStatus.MissingConfig);
64+
client.configuredTransport = ConfiguredTransport.Unknown;
65+
return client.status;
66+
}
67+
68+
if (!IsEnabled(pluginEntry) || !IsEnabled(unityServer))
69+
{
70+
client.SetStatus(McpStatus.NotConfigured);
71+
client.configuredTransport = ConfiguredTransport.Unknown;
72+
return client.status;
73+
}
74+
75+
bool matches = ServerMatchesCurrentEndpoint(unityServer);
76+
if (matches)
77+
{
78+
client.SetStatus(McpStatus.Configured);
79+
client.configuredTransport = ResolveTransport(unityServer);
80+
return client.status;
81+
}
82+
83+
if (attemptAutoRewrite)
84+
{
85+
Configure();
86+
}
87+
else
88+
{
89+
client.SetStatus(McpStatus.IncorrectPath);
90+
client.configuredTransport = ConfiguredTransport.Unknown;
91+
}
92+
}
93+
catch (Exception ex)
94+
{
95+
client.SetStatus(McpStatus.Error, ex.Message);
96+
client.configuredTransport = ConfiguredTransport.Unknown;
97+
}
98+
99+
return client.status;
100+
}
101+
102+
public override void Configure()
103+
{
104+
if (EditorPrefs.GetBool(EditorPrefKeys.LockCursorConfig, false))
105+
return;
106+
107+
string path = GetConfigPath();
108+
McpConfigurationHelper.EnsureConfigDirectoryExists(path);
109+
110+
JObject root = File.Exists(path) ? LoadConfig(path) : new JObject();
111+
112+
JObject plugins = root["plugins"] as JObject ?? new JObject();
113+
root["plugins"] = plugins;
114+
115+
JObject entries = plugins["entries"] as JObject ?? new JObject();
116+
plugins["entries"] = entries;
117+
118+
JObject pluginEntry = entries[PluginName] as JObject ?? new JObject();
119+
entries[PluginName] = pluginEntry;
120+
pluginEntry["enabled"] = true;
121+
122+
JObject pluginConfig = pluginEntry["config"] as JObject ?? new JObject();
123+
pluginEntry["config"] = pluginConfig;
124+
pluginConfig.Remove("timeout"); // removed in openclaw-mcp-bridge v2+
125+
pluginConfig.Remove("retries"); // removed in openclaw-mcp-bridge v2+
126+
pluginConfig["servers"] = UpsertUnityServer(pluginConfig["servers"]);
127+
128+
McpConfigurationHelper.WriteAtomicFile(path, root.ToString(Formatting.Indented));
129+
client.SetStatus(McpStatus.Configured);
130+
client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
131+
}
132+
133+
public override string GetManualSnippet()
134+
{
135+
JObject snippet = new JObject
136+
{
137+
["plugins"] = new JObject
138+
{
139+
["entries"] = new JObject
140+
{
141+
[PluginName] = new JObject
142+
{
143+
["enabled"] = true,
144+
["config"] = new JObject
145+
{
146+
["servers"] = new JObject
147+
{
148+
[ServerName] = BuildUnityServerEntry()
149+
}
150+
}
151+
}
152+
}
153+
}
154+
};
155+
156+
return snippet.ToString(Formatting.Indented);
157+
}
158+
159+
public override IList<string> GetInstallationSteps() => new List<string>
160+
{
161+
"Install OpenClaw",
162+
"Install the bridge plugin: npm install -g openclaw-mcp-bridge (or pnpm add -g openclaw-mcp-bridge)",
163+
"In MCP for Unity, choose OpenClaw and click Configure",
164+
"OpenClaw uses the currently selected MCP for Unity transport (HTTP or stdio)",
165+
"OpenClaw exposes a proxy tool such as unityMCP__call for Unity MCP access",
166+
"Restart OpenClaw if the plugin does not hot-reload the new config"
167+
};
168+
169+
private JObject LoadConfig(string path)
170+
{
171+
string text = File.ReadAllText(path);
172+
if (string.IsNullOrWhiteSpace(text))
173+
{
174+
return new JObject();
175+
}
176+
177+
try
178+
{
179+
return JsonConvert.DeserializeObject<JObject>(text) ?? new JObject();
180+
}
181+
catch (JsonException ex)
182+
{
183+
throw new InvalidOperationException(
184+
$"OpenClaw config contains non-JSON content and cannot be safely auto-edited: {ex.Message}");
185+
}
186+
}
187+
188+
private JObject FindUnityServer(JToken serversToken)
189+
{
190+
if (serversToken is JObject serverMap)
191+
{
192+
return serverMap[ServerName] as JObject;
193+
}
194+
195+
if (serversToken is JArray legacyServers)
196+
{
197+
foreach (JToken token in legacyServers)
198+
{
199+
JObject server = token as JObject;
200+
if (server == null)
201+
{
202+
continue;
203+
}
204+
205+
string name = server["name"]?.ToString();
206+
if (string.Equals(name, ServerName, StringComparison.OrdinalIgnoreCase))
207+
{
208+
return server;
209+
}
210+
}
211+
}
212+
213+
return null;
214+
}
215+
216+
private JObject UpsertUnityServer(JToken serversToken)
217+
{
218+
JObject servers = NormalizeServers(serversToken);
219+
JObject entry = servers[ServerName] as JObject ?? new JObject();
220+
JObject desiredEntry = BuildUnityServerEntry();
221+
222+
entry.Remove("name");
223+
entry.Remove("prefix");
224+
entry.Remove("healthCheck");
225+
entry.Remove("command");
226+
entry.Remove("args");
227+
entry.Remove("env");
228+
entry.Remove("connectTimeoutMs");
229+
230+
foreach (var property in desiredEntry.Properties())
231+
{
232+
entry[property.Name] = property.Value.DeepClone();
233+
}
234+
235+
servers[ServerName] = entry;
236+
237+
return servers;
238+
}
239+
240+
private static JObject NormalizeServers(JToken serversToken)
241+
{
242+
if (serversToken is JObject serverMap)
243+
{
244+
return serverMap;
245+
}
246+
247+
var normalized = new JObject();
248+
if (!(serversToken is JArray legacyServers))
249+
{
250+
return normalized;
251+
}
252+
253+
foreach (JToken token in legacyServers)
254+
{
255+
if (!(token is JObject legacyServer))
256+
{
257+
continue;
258+
}
259+
260+
string name = legacyServer["name"]?.ToString();
261+
if (string.IsNullOrWhiteSpace(name))
262+
{
263+
continue;
264+
}
265+
266+
normalized[name] = legacyServer;
267+
}
268+
269+
return normalized;
270+
}
271+
272+
private static JObject BuildUnityServerEntry()
273+
{
274+
ConfiguredTransport transport = HttpEndpointUtility.GetCurrentServerTransport();
275+
if (transport == ConfiguredTransport.Stdio)
276+
{
277+
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
278+
if (string.IsNullOrWhiteSpace(uvxPath))
279+
{
280+
throw new InvalidOperationException("uvx not found. Install uv/uvx or set the override in Advanced Settings.");
281+
}
282+
283+
var args = new JArray();
284+
foreach (string value in AssetPathUtility.GetUvxDevFlagsList())
285+
{
286+
args.Add(value);
287+
}
288+
foreach (string value in AssetPathUtility.GetBetaServerFromArgsList())
289+
{
290+
args.Add(value);
291+
}
292+
args.Add(packageName);
293+
args.Add("--transport");
294+
args.Add("stdio");
295+
296+
return new JObject
297+
{
298+
["enabled"] = true,
299+
["url"] = StdioUrl,
300+
["transport"] = StdioTransportName,
301+
["command"] = uvxPath,
302+
["args"] = args,
303+
["toolPrefix"] = ServerName,
304+
["requestTimeoutMs"] = 60000,
305+
["connectTimeoutMs"] = 15000
306+
};
307+
}
308+
309+
return new JObject
310+
{
311+
["enabled"] = true,
312+
["url"] = HttpEndpointUtility.GetMcpRpcUrl(),
313+
["transport"] = HttpTransportName,
314+
["toolPrefix"] = ServerName,
315+
["requestTimeoutMs"] = 30000
316+
};
317+
}
318+
319+
private bool ServerMatchesCurrentEndpoint(JObject server)
320+
{
321+
if (server == null)
322+
{
323+
return false;
324+
}
325+
326+
ConfiguredTransport expectedTransport = HttpEndpointUtility.GetCurrentServerTransport();
327+
ConfiguredTransport configuredTransport = ResolveTransport(server);
328+
if (configuredTransport != expectedTransport)
329+
{
330+
return false;
331+
}
332+
333+
if (configuredTransport == ConfiguredTransport.Stdio)
334+
{
335+
string configuredUrl = server["url"]?.ToString();
336+
string command = server["command"]?.ToString();
337+
if (!UrlsEqual(configuredUrl, StdioUrl) || string.IsNullOrWhiteSpace(command))
338+
{
339+
return false;
340+
}
341+
342+
// Validate the --from package source hasn't drifted (e.g. stable vs prerelease switch)
343+
string[] args = (server["args"] as JArray)?.ToObject<string[]>();
344+
string configuredSource = McpConfigurationHelper.ExtractUvxUrl(args);
345+
string expectedSource = GetExpectedPackageSourceForValidation();
346+
if (!string.IsNullOrEmpty(configuredSource) && !string.IsNullOrEmpty(expectedSource) &&
347+
!McpConfigurationHelper.PathsEqual(configuredSource, expectedSource))
348+
{
349+
return false;
350+
}
351+
}
352+
else
353+
{
354+
string configuredUrl = server["url"]?.ToString();
355+
if (string.IsNullOrWhiteSpace(configuredUrl) ||
356+
(!UrlsEqual(configuredUrl, HttpEndpointUtility.GetLocalMcpRpcUrl()) &&
357+
!UrlsEqual(configuredUrl, HttpEndpointUtility.GetRemoteMcpRpcUrl())))
358+
{
359+
return false;
360+
}
361+
}
362+
363+
string toolPrefix = server["toolPrefix"]?.ToString();
364+
return string.IsNullOrWhiteSpace(toolPrefix) ||
365+
string.Equals(toolPrefix, ServerName, StringComparison.OrdinalIgnoreCase);
366+
}
367+
368+
private static bool IsEnabled(JObject entry)
369+
{
370+
JToken enabledToken = entry["enabled"];
371+
return enabledToken == null || enabledToken.Type != JTokenType.Boolean || enabledToken.Value<bool>();
372+
}
373+
374+
private ConfiguredTransport ResolveTransport(JObject server)
375+
{
376+
string configuredTransport = server?["transport"]?.ToString();
377+
string configuredUrl = server?["url"]?.ToString();
378+
379+
if (string.Equals(configuredTransport, StdioTransportName, StringComparison.OrdinalIgnoreCase) ||
380+
UrlsEqual(configuredUrl, StdioUrl))
381+
{
382+
return ConfiguredTransport.Stdio;
383+
}
384+
385+
if (UrlsEqual(configuredUrl, HttpEndpointUtility.GetRemoteMcpRpcUrl()))
386+
{
387+
return ConfiguredTransport.HttpRemote;
388+
}
389+
390+
if (UrlsEqual(configuredUrl, HttpEndpointUtility.GetLocalMcpRpcUrl()))
391+
{
392+
return ConfiguredTransport.Http;
393+
}
394+
395+
return ConfiguredTransport.Unknown;
396+
}
397+
}
398+
}

0 commit comments

Comments
 (0)