Skip to content

Commit be7ade8

Browse files
committed
2 parents 15c35ae + fdf8482 commit be7ade8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2007
-202
lines changed

MCPForUnity/Editor/Data/McpClients.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4-
using System.Runtime.InteropServices;
54
using MCPForUnity.Editor.Models;
65

76
namespace MCPForUnity.Editor.Data
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using UnityEngine;
5+
6+
namespace MCPForUnity.Editor.Data
7+
{
8+
/// <summary>
9+
/// Registry of Python tool files to sync to the MCP server.
10+
/// Add your Python files here - they can be stored anywhere in your project.
11+
/// </summary>
12+
[CreateAssetMenu(fileName = "PythonTools", menuName = "MCP For Unity/Python Tools")]
13+
public class PythonToolsAsset : ScriptableObject
14+
{
15+
[Tooltip("Add Python files (.py) to sync to the MCP server. Files can be located anywhere in your project.")]
16+
public List<TextAsset> pythonFiles = new List<TextAsset>();
17+
18+
[Header("Sync Options")]
19+
[Tooltip("Use content hashing to detect changes (recommended). If false, always copies on startup.")]
20+
public bool useContentHashing = true;
21+
22+
[Header("Sync State (Read-only)")]
23+
[Tooltip("Internal tracking - do not modify")]
24+
public List<PythonFileState> fileStates = new List<PythonFileState>();
25+
26+
/// <summary>
27+
/// Gets all valid Python files (filters out null/missing references)
28+
/// </summary>
29+
public IEnumerable<TextAsset> GetValidFiles()
30+
{
31+
return pythonFiles.Where(f => f != null);
32+
}
33+
34+
/// <summary>
35+
/// Checks if a file needs syncing
36+
/// </summary>
37+
public bool NeedsSync(TextAsset file, string currentHash)
38+
{
39+
if (!useContentHashing) return true; // Always sync if hashing disabled
40+
41+
var state = fileStates.FirstOrDefault(s => s.assetGuid == GetAssetGuid(file));
42+
return state == null || state.contentHash != currentHash;
43+
}
44+
45+
/// <summary>
46+
/// Records that a file was synced
47+
/// </summary>
48+
public void RecordSync(TextAsset file, string hash)
49+
{
50+
string guid = GetAssetGuid(file);
51+
var state = fileStates.FirstOrDefault(s => s.assetGuid == guid);
52+
53+
if (state == null)
54+
{
55+
state = new PythonFileState { assetGuid = guid };
56+
fileStates.Add(state);
57+
}
58+
59+
state.contentHash = hash;
60+
state.lastSyncTime = DateTime.UtcNow;
61+
state.fileName = file.name;
62+
}
63+
64+
/// <summary>
65+
/// Removes state entries for files no longer in the list
66+
/// </summary>
67+
public void CleanupStaleStates()
68+
{
69+
var validGuids = new HashSet<string>(GetValidFiles().Select(GetAssetGuid));
70+
fileStates.RemoveAll(s => !validGuids.Contains(s.assetGuid));
71+
}
72+
73+
private string GetAssetGuid(TextAsset asset)
74+
{
75+
return UnityEditor.AssetDatabase.AssetPathToGUID(UnityEditor.AssetDatabase.GetAssetPath(asset));
76+
}
77+
78+
/// <summary>
79+
/// Called when the asset is modified in the Inspector
80+
/// Triggers sync to handle file additions/removals
81+
/// </summary>
82+
private void OnValidate()
83+
{
84+
// Cleanup stale states immediately
85+
CleanupStaleStates();
86+
87+
// Trigger sync after a delay to handle file removals
88+
// Delay ensures the asset is saved before sync runs
89+
UnityEditor.EditorApplication.delayCall += () =>
90+
{
91+
if (this != null) // Check if asset still exists
92+
{
93+
MCPForUnity.Editor.Helpers.PythonToolSyncProcessor.SyncAllTools();
94+
}
95+
};
96+
}
97+
}
98+
99+
[Serializable]
100+
public class PythonFileState
101+
{
102+
public string assetGuid;
103+
public string fileName;
104+
public string contentHash;
105+
public DateTime lastSyncTime;
106+
}
107+
}

MCPForUnity/Editor/Data/PythonToolsAsset.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

MCPForUnity/Editor/Helpers/CodexConfigHelper.cs

Lines changed: 68 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
5-
using System.Text;
6-
using System.Text.RegularExpressions;
75
using MCPForUnity.External.Tommy;
8-
using Newtonsoft.Json;
96

107
namespace MCPForUnity.Editor.Helpers
118
{
@@ -42,108 +39,107 @@ public static bool IsCodexConfigured(string pythonDir)
4239

4340
public static string BuildCodexServerBlock(string uvPath, string serverSrc)
4441
{
45-
string argsArray = FormatTomlStringArray(new[] { "run", "--directory", serverSrc, "server.py" });
46-
return $"[mcp_servers.unityMCP]{Environment.NewLine}" +
47-
$"command = \"{EscapeTomlString(uvPath)}\"{Environment.NewLine}" +
48-
$"args = {argsArray}";
42+
var table = new TomlTable();
43+
var mcpServers = new TomlTable();
44+
45+
mcpServers["unityMCP"] = CreateUnityMcpTable(uvPath, serverSrc);
46+
table["mcp_servers"] = mcpServers;
47+
48+
using var writer = new StringWriter();
49+
table.WriteTo(writer);
50+
return writer.ToString();
4951
}
5052

51-
public static string UpsertCodexServerBlock(string existingToml, string newBlock)
53+
public static string UpsertCodexServerBlock(string existingToml, string uvPath, string serverSrc)
5254
{
53-
if (string.IsNullOrWhiteSpace(existingToml))
55+
// Parse existing TOML or create new root table
56+
var root = TryParseToml(existingToml) ?? new TomlTable();
57+
58+
// Ensure mcp_servers table exists
59+
if (!root.TryGetNode("mcp_servers", out var mcpServersNode) || !(mcpServersNode is TomlTable))
5460
{
55-
return newBlock.TrimEnd() + Environment.NewLine;
61+
root["mcp_servers"] = new TomlTable();
5662
}
63+
var mcpServers = root["mcp_servers"] as TomlTable;
5764

58-
StringBuilder sb = new StringBuilder();
59-
using StringReader reader = new StringReader(existingToml);
60-
string line;
61-
bool inTarget = false;
62-
bool replaced = false;
63-
while ((line = reader.ReadLine()) != null)
64-
{
65-
string trimmed = line.Trim();
66-
bool isSection = trimmed.StartsWith("[") && trimmed.EndsWith("]") && !trimmed.StartsWith("[[");
67-
if (isSection)
68-
{
69-
bool isTarget = string.Equals(trimmed, "[mcp_servers.unityMCP]", StringComparison.OrdinalIgnoreCase);
70-
if (isTarget)
71-
{
72-
if (!replaced)
73-
{
74-
if (sb.Length > 0 && sb[^1] != '\n') sb.AppendLine();
75-
sb.AppendLine(newBlock.TrimEnd());
76-
replaced = true;
77-
}
78-
inTarget = true;
79-
continue;
80-
}
65+
// Create or update unityMCP table
66+
mcpServers["unityMCP"] = CreateUnityMcpTable(uvPath, serverSrc);
8167

82-
if (inTarget)
83-
{
84-
inTarget = false;
85-
}
86-
}
68+
// Serialize back to TOML
69+
using var writer = new StringWriter();
70+
root.WriteTo(writer);
71+
return writer.ToString();
72+
}
8773

88-
if (inTarget)
89-
{
90-
continue;
91-
}
74+
public static bool TryParseCodexServer(string toml, out string command, out string[] args)
75+
{
76+
command = null;
77+
args = null;
9278

93-
sb.AppendLine(line);
79+
var root = TryParseToml(toml);
80+
if (root == null) return false;
81+
82+
if (!TryGetTable(root, "mcp_servers", out var servers)
83+
&& !TryGetTable(root, "mcpServers", out servers))
84+
{
85+
return false;
9486
}
9587

96-
if (!replaced)
88+
if (!TryGetTable(servers, "unityMCP", out var unity))
9789
{
98-
if (sb.Length > 0 && sb[^1] != '\n') sb.AppendLine();
99-
sb.AppendLine(newBlock.TrimEnd());
90+
return false;
10091
}
10192

102-
return sb.ToString().TrimEnd() + Environment.NewLine;
93+
command = GetTomlString(unity, "command");
94+
args = GetTomlStringArray(unity, "args");
95+
96+
return !string.IsNullOrEmpty(command) && args != null;
10397
}
10498

105-
public static bool TryParseCodexServer(string toml, out string command, out string[] args)
99+
/// <summary>
100+
/// Safely parses TOML string, returning null on failure
101+
/// </summary>
102+
private static TomlTable TryParseToml(string toml)
106103
{
107-
command = null;
108-
args = null;
109-
if (string.IsNullOrWhiteSpace(toml)) return false;
104+
if (string.IsNullOrWhiteSpace(toml)) return null;
110105

111106
try
112107
{
113108
using var reader = new StringReader(toml);
114-
TomlTable root = TOML.Parse(reader);
115-
if (root == null) return false;
116-
117-
if (!TryGetTable(root, "mcp_servers", out var servers)
118-
&& !TryGetTable(root, "mcpServers", out servers))
119-
{
120-
return false;
121-
}
122-
123-
if (!TryGetTable(servers, "unityMCP", out var unity))
124-
{
125-
return false;
126-
}
127-
128-
command = GetTomlString(unity, "command");
129-
args = GetTomlStringArray(unity, "args");
130-
131-
return !string.IsNullOrEmpty(command) && args != null;
109+
return TOML.Parse(reader);
132110
}
133111
catch (TomlParseException)
134112
{
135-
return false;
113+
return null;
136114
}
137115
catch (TomlSyntaxException)
138116
{
139-
return false;
117+
return null;
140118
}
141119
catch (FormatException)
142120
{
143-
return false;
121+
return null;
144122
}
145123
}
146124

125+
/// <summary>
126+
/// Creates a TomlTable for the unityMCP server configuration
127+
/// </summary>
128+
private static TomlTable CreateUnityMcpTable(string uvPath, string serverSrc)
129+
{
130+
var unityMCP = new TomlTable();
131+
unityMCP["command"] = new TomlString { Value = uvPath };
132+
133+
var argsArray = new TomlArray();
134+
argsArray.Add(new TomlString { Value = "run" });
135+
argsArray.Add(new TomlString { Value = "--directory" });
136+
argsArray.Add(new TomlString { Value = serverSrc });
137+
argsArray.Add(new TomlString { Value = "server.py" });
138+
unityMCP["args"] = argsArray;
139+
140+
return unityMCP;
141+
}
142+
147143
private static bool TryGetTable(TomlTable parent, string key, out TomlTable table)
148144
{
149145
table = null;
@@ -211,33 +207,5 @@ private static string[] GetTomlStringArray(TomlTable table, string key)
211207

212208
return null;
213209
}
214-
215-
private static string FormatTomlStringArray(IEnumerable<string> values)
216-
{
217-
if (values == null) return "[]";
218-
StringBuilder sb = new StringBuilder();
219-
sb.Append('[');
220-
bool first = true;
221-
foreach (string value in values)
222-
{
223-
if (!first)
224-
{
225-
sb.Append(", ");
226-
}
227-
sb.Append('"').Append(EscapeTomlString(value ?? string.Empty)).Append('"');
228-
first = false;
229-
}
230-
sb.Append(']');
231-
return sb.ToString();
232-
}
233-
234-
private static string EscapeTomlString(string value)
235-
{
236-
if (string.IsNullOrEmpty(value)) return string.Empty;
237-
return value
238-
.Replace("\\", "\\\\")
239-
.Replace("\"", "\\\"");
240-
}
241-
242210
}
243211
}

MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,7 @@ public static string ConfigureCodexClient(string pythonDir, string configPath, M
205205
return "Configured successfully";
206206
}
207207

208-
string codexBlock = CodexConfigHelper.BuildCodexServerBlock(uvPath, serverSrc);
209-
string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, codexBlock);
208+
string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, uvPath, serverSrc);
210209

211210
McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml);
212211

0 commit comments

Comments
 (0)