Skip to content

Commit e4b131b

Browse files
committed
Update on three different changes
1. Add McpLog that could log mcp calls and errors under Asset/ 2. Solve issues CoplayDev#816 via incluging str in annotation, extracted AssignedObjectreference() helper that verify assignments and resolve component types from gameobjects.
1 parent 7e57986 commit e4b131b

11 files changed

Lines changed: 228 additions & 33 deletions

File tree

MCPForUnity/Editor/Constants/EditorPrefKeys.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,6 @@ internal static class EditorPrefKeys
6767
internal const string ApiKey = "MCPForUnity.ApiKey";
6868

6969
internal const string BatchExecuteMaxCommands = "MCPForUnity.BatchExecute.MaxCommands";
70+
internal const string LogRecordEnabled = "MCPForUnity.LogRecordEnabled";
7071
}
7172
}

MCPForUnity/Editor/Helpers/ComponentOps.cs

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -592,12 +592,14 @@ private static bool SetObjectReference(SerializedProperty prop, JToken value, ou
592592
error = $"No object found with instanceID {id}.";
593593
return false;
594594
}
595-
prop.objectReferenceValue = resolved;
596-
return true;
595+
return AssignObjectReference(prop, resolved, null, out error);
597596
}
598597

599598
if (value is JObject jObj)
600599
{
600+
// Optional component type filter — e.g. {"instanceID": 123, "component": "Button"}
601+
string componentFilter = jObj["component"]?.ToString();
602+
601603
var idToken = jObj["instanceID"];
602604
if (idToken != null)
603605
{
@@ -608,8 +610,7 @@ private static bool SetObjectReference(SerializedProperty prop, JToken value, ou
608610
error = $"No object found with instanceID {id}.";
609611
return false;
610612
}
611-
prop.objectReferenceValue = resolved;
612-
return true;
613+
return AssignObjectReference(prop, resolved, componentFilter, out error);
613614
}
614615

615616
var guidToken = jObj["guid"];
@@ -665,8 +666,8 @@ private static bool SetObjectReference(SerializedProperty prop, JToken value, ou
665666
return false;
666667
}
667668

668-
prop.objectReferenceValue = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
669-
return true;
669+
var loaded = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
670+
return AssignObjectReference(prop, loaded, componentFilter, out error);
670671
}
671672

672673
var pathToken = jObj["path"];
@@ -679,8 +680,7 @@ private static bool SetObjectReference(SerializedProperty prop, JToken value, ou
679680
error = $"No asset found at path '{pathToken}'.";
680681
return false;
681682
}
682-
prop.objectReferenceValue = resolved;
683-
return true;
683+
return AssignObjectReference(prop, resolved, componentFilter, out error);
684684
}
685685

686686
var nameToken = jObj["name"];
@@ -696,6 +696,16 @@ private static bool SetObjectReference(SerializedProperty prop, JToken value, ou
696696
if (value.Type == JTokenType.String)
697697
{
698698
string strVal = value.ToString();
699+
700+
// Try as instanceID if the string is purely numeric
701+
if (int.TryParse(strVal, out int parsedId))
702+
{
703+
var resolved = GameObjectLookup.ResolveInstanceID(parsedId);
704+
if (resolved != null)
705+
return AssignObjectReference(prop, resolved, null, out error);
706+
// Not a valid instanceID — fall through to path/name resolution
707+
}
708+
699709
if (strVal.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase) || strVal.Contains("/"))
700710
{
701711
string sanitized = AssetPathUtility.SanitizeAssetPath(strVal);
@@ -705,8 +715,7 @@ private static bool SetObjectReference(SerializedProperty prop, JToken value, ou
705715
error = $"No asset found at path '{strVal}'.";
706716
return false;
707717
}
708-
prop.objectReferenceValue = resolved;
709-
return true;
718+
return AssignObjectReference(prop, resolved, null, out error);
710719
}
711720

712721
// Fall back to scene hierarchy lookup by name.
@@ -717,6 +726,66 @@ private static bool SetObjectReference(SerializedProperty prop, JToken value, ou
717726
return false;
718727
}
719728

729+
/// <summary>
730+
/// Assigns a resolved object to a SerializedProperty, with automatic component fallback.
731+
/// If the resolved object is a GameObject but the property expects a Component type,
732+
/// searches the GameObject's components for a compatible one.
733+
/// Optionally filters by component type name (e.g. "Button", "Rigidbody").
734+
/// </summary>
735+
private static bool AssignObjectReference(SerializedProperty prop, UnityEngine.Object resolved, string componentFilter, out string error)
736+
{
737+
error = null;
738+
if (resolved == null)
739+
{
740+
error = "Resolved object is null.";
741+
return false;
742+
}
743+
744+
// If a component filter is specified and the resolved object is a GameObject,
745+
// find the specific component by type name.
746+
if (!string.IsNullOrEmpty(componentFilter) && resolved is GameObject filterGo)
747+
{
748+
var components = filterGo.GetComponents<Component>();
749+
foreach (var comp in components)
750+
{
751+
if (comp == null) continue;
752+
if (string.Equals(comp.GetType().Name, componentFilter, StringComparison.OrdinalIgnoreCase) ||
753+
string.Equals(comp.GetType().FullName, componentFilter, StringComparison.OrdinalIgnoreCase))
754+
{
755+
prop.objectReferenceValue = comp;
756+
if (prop.objectReferenceValue != null)
757+
return true;
758+
}
759+
}
760+
error = $"Component '{componentFilter}' not found on GameObject '{filterGo.name}'.";
761+
return false;
762+
}
763+
764+
// Try direct assignment first
765+
prop.objectReferenceValue = resolved;
766+
if (prop.objectReferenceValue != null)
767+
return true;
768+
769+
// If the resolved object is a GameObject but the property expects a Component,
770+
// try each component on the GameObject until one is accepted.
771+
if (resolved is GameObject go)
772+
{
773+
var components = go.GetComponents<Component>();
774+
foreach (var comp in components)
775+
{
776+
if (comp == null) continue;
777+
prop.objectReferenceValue = comp;
778+
if (prop.objectReferenceValue != null)
779+
return true;
780+
}
781+
error = $"GameObject '{go.name}' found but no compatible component for the property type.";
782+
return false;
783+
}
784+
785+
error = $"Object '{resolved.name}' (type: {resolved.GetType().Name}) is not compatible with the property type.";
786+
return false;
787+
}
788+
720789
/// <summary>
721790
/// Resolves a scene GameObject by name and assigns it (or a component on it)
722791
/// to a SerializedProperty. Uses GameObjectLookup for robust search
@@ -747,24 +816,7 @@ private static bool ResolveSceneObjectByName(SerializedProperty prop, string nam
747816
return false;
748817
}
749818

750-
// If the property accepts a GameObject directly, assign it.
751-
prop.objectReferenceValue = go;
752-
if (prop.objectReferenceValue != null)
753-
return true;
754-
755-
// The field type may expect a specific Component (e.g. Transform, Rigidbody).
756-
// Try each component on the GameObject until one is accepted.
757-
var components = go.GetComponents<Component>();
758-
foreach (var comp in components)
759-
{
760-
if (comp == null) continue;
761-
prop.objectReferenceValue = comp;
762-
if (prop.objectReferenceValue != null)
763-
return true;
764-
}
765-
766-
error = $"GameObject '{name}' found but no compatible component for property type.";
767-
return false;
819+
return AssignObjectReference(prop, go, null, out error);
768820
}
769821

770822
/// <summary>
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using System;
2+
using System.IO;
3+
using MCPForUnity.Editor.Constants;
4+
using Newtonsoft.Json;
5+
using Newtonsoft.Json.Linq;
6+
using UnityEditor;
7+
using UnityEngine;
8+
9+
namespace MCPForUnity.Editor.Helpers
10+
{
11+
internal static class McpLogRecord
12+
{
13+
private static readonly string LogPath = Path.Combine(Application.dataPath, "mcp.log");
14+
private static readonly string ErrorLogPath = Path.Combine(Application.dataPath, "mcpError.log");
15+
private const long MaxLogSizeBytes = 1024 * 1024; // 1 MB
16+
private static bool _sessionStarted;
17+
18+
internal static bool IsEnabled
19+
{
20+
get => EditorPrefs.GetBool(EditorPrefKeys.LogRecordEnabled, false);
21+
set => EditorPrefs.SetBool(EditorPrefKeys.LogRecordEnabled, value);
22+
}
23+
24+
internal static void Log(string commandType, JObject parameters, string type, string status, long durationMs, string error = null)
25+
{
26+
if (!IsEnabled) return;
27+
28+
try
29+
{
30+
if (!_sessionStarted)
31+
{
32+
_sessionStarted = true;
33+
RotateIfNeeded(LogPath);
34+
var sessionEntry = new JObject
35+
{
36+
["ts"] = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"),
37+
["event"] = "session_start",
38+
["unity"] = Application.unityVersion
39+
};
40+
AppendLine(LogPath, sessionEntry.ToString(Formatting.None));
41+
}
42+
43+
var entry = new JObject
44+
{
45+
["ts"] = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"),
46+
["tool"] = commandType,
47+
["type"] = type,
48+
["status"] = status,
49+
["ms"] = durationMs
50+
};
51+
52+
var action = parameters?.Value<string>("action");
53+
if (!string.IsNullOrEmpty(action))
54+
entry["action"] = action;
55+
56+
if (parameters != null)
57+
entry["params"] = parameters;
58+
59+
if (error != null)
60+
entry["error"] = error;
61+
62+
var line = entry.ToString(Formatting.None);
63+
AppendLine(LogPath, line);
64+
65+
if (status == "ERROR")
66+
{
67+
RotateIfNeeded(ErrorLogPath);
68+
AppendLine(ErrorLogPath, line);
69+
}
70+
}
71+
catch (Exception ex)
72+
{
73+
McpLog.Warn($"[McpLogRecord] Failed to write log: {ex.Message}");
74+
}
75+
}
76+
77+
private static void AppendLine(string path, string line)
78+
{
79+
File.AppendAllText(path, line + Environment.NewLine);
80+
}
81+
82+
private static void RotateIfNeeded(string path)
83+
{
84+
try
85+
{
86+
if (!File.Exists(path)) return;
87+
var info = new FileInfo(path);
88+
if (info.Length <= MaxLogSizeBytes) return;
89+
90+
var lines = File.ReadAllLines(path);
91+
var half = lines.Length / 2;
92+
File.WriteAllLines(path, lines[half..]);
93+
}
94+
catch
95+
{
96+
// Best-effort rotation
97+
}
98+
}
99+
}
100+
}

MCPForUnity/Editor/Helpers/McpLogRecord.cs.meta

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

MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,18 +361,31 @@ private static void ProcessCommand(string id, PendingCommand pending)
361361
return;
362362
}
363363

364+
var logType = resourceMeta != null ? "resource" : toolMeta != null ? "tool" : "unknown";
365+
var sw = McpLogRecord.IsEnabled ? System.Diagnostics.Stopwatch.StartNew() : null;
364366
var result = CommandRegistry.ExecuteCommand(command.type, parameters, pending.CompletionSource);
365367

366368
if (result == null)
367369
{
368370
// Async command – cleanup after completion on next editor frame to preserve order.
369-
pending.CompletionSource.Task.ContinueWith(_ =>
371+
var capturedType = command.type;
372+
var capturedParams = parameters;
373+
var capturedLogType = logType;
374+
pending.CompletionSource.Task.ContinueWith(t =>
370375
{
376+
sw?.Stop();
377+
McpLogRecord.Log(capturedType, capturedParams, capturedLogType,
378+
t.IsFaulted ? "ERROR" : "SUCCESS",
379+
sw?.ElapsedMilliseconds ?? 0,
380+
t.IsFaulted ? t.Exception?.InnerException?.Message : null);
371381
EditorApplication.delayCall += () => RemovePending(id, pending);
372382
}, TaskScheduler.Default);
373383
return;
374384
}
375385

386+
sw?.Stop();
387+
McpLogRecord.Log(command.type, parameters, logType, "SUCCESS", sw?.ElapsedMilliseconds ?? 0);
388+
376389
var response = new { status = "success", result };
377390
pending.TrySetResult(JsonConvert.SerializeObject(response));
378391
RemovePending(id, pending);

MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class McpAdvancedSection
2525
private Button browseGitUrlButton;
2626
private Button clearGitUrlButton;
2727
private Toggle debugLogsToggle;
28+
private Toggle logRecordToggle;
2829
private Toggle devModeForceRefreshToggle;
2930
private Toggle allowLanHttpBindToggle;
3031
private Toggle allowInsecureRemoteHttpToggle;
@@ -66,6 +67,7 @@ private void CacheUIElements()
6667
browseGitUrlButton = Root.Q<Button>("browse-git-url-button");
6768
clearGitUrlButton = Root.Q<Button>("clear-git-url-button");
6869
debugLogsToggle = Root.Q<Toggle>("debug-logs-toggle");
70+
logRecordToggle = Root.Q<Toggle>("log-record-toggle");
6971
devModeForceRefreshToggle = Root.Q<Toggle>("dev-mode-force-refresh-toggle");
7072
allowLanHttpBindToggle = Root.Q<Toggle>("allow-lan-http-bind-toggle");
7173
allowInsecureRemoteHttpToggle = Root.Q<Toggle>("allow-insecure-remote-http-toggle");
@@ -96,6 +98,13 @@ private void InitializeUI()
9698
if (debugLabel != null)
9799
debugLabel.tooltip = debugLogsToggle.tooltip;
98100
}
101+
if (logRecordToggle != null)
102+
{
103+
logRecordToggle.tooltip = "Log every MCP tool execution (tool, action, status, duration) to Assets/mcp.log.";
104+
var logRecordLabel = logRecordToggle?.parent?.Q<Label>();
105+
if (logRecordLabel != null)
106+
logRecordLabel.tooltip = logRecordToggle.tooltip;
107+
}
99108
if (devModeForceRefreshToggle != null)
100109
{
101110
devModeForceRefreshToggle.tooltip = "When enabled, generated uvx commands add '--no-cache --refresh' before launching (slower startup, but avoids stale cached builds while iterating on the Server).";
@@ -146,6 +155,9 @@ private void InitializeUI()
146155
debugLogsToggle.value = debugEnabled;
147156
McpLog.SetDebugLoggingEnabled(debugEnabled);
148157

158+
if (logRecordToggle != null)
159+
logRecordToggle.value = McpLogRecord.IsEnabled;
160+
149161
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
150162
if (allowLanHttpBindToggle != null)
151163
{
@@ -199,6 +211,14 @@ private void RegisterCallbacks()
199211
McpLog.SetDebugLoggingEnabled(evt.newValue);
200212
});
201213

214+
if (logRecordToggle != null)
215+
{
216+
logRecordToggle.RegisterValueChangedCallback(evt =>
217+
{
218+
McpLogRecord.IsEnabled = evt.newValue;
219+
});
220+
}
221+
202222
devModeForceRefreshToggle.RegisterValueChangedCallback(evt =>
203223
{
204224
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, evt.newValue);
@@ -324,6 +344,8 @@ public void UpdatePathOverrides()
324344

325345
gitUrlOverride.value = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
326346
debugLogsToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
347+
if (logRecordToggle != null)
348+
logRecordToggle.value = McpLogRecord.IsEnabled;
327349
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
328350
if (allowLanHttpBindToggle != null)
329351
{

MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.uxml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
<ui:Toggle name="debug-logs-toggle" class="setting-toggle" />
2828
</ui:VisualElement>
2929

30+
<ui:VisualElement class="setting-row">
31+
<ui:Label text="Log Record (Assets/mcp.log):" class="setting-label" />
32+
<ui:Toggle name="log-record-toggle" class="setting-toggle" />
33+
</ui:VisualElement>
34+
3035
<ui:VisualElement class="setting-row">
3136
<ui:Label text="Server Health:" class="setting-label" />
3237
<ui:VisualElement class="status-container">

Server/src/services/tools/manage_asset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ async def manage_asset(
3333
path: Annotated[str, "Asset path (e.g., 'Materials/MyMaterial.mat') or search scope (e.g., 'Assets')."],
3434
asset_type: Annotated[str,
3535
"Asset type (e.g., 'Material', 'Folder') - required for 'create'. Note: For ScriptableObjects, use manage_scriptable_object."] | None = None,
36-
properties: Annotated[dict[str, Any],
36+
properties: Annotated[dict[str, Any] | str,
3737
"Dictionary of properties for 'create'/'modify'. Keys are property names, values are property values."] | None = None,
3838
destination: Annotated[str,
3939
"Target path for 'duplicate'/'move'."] | None = None,

0 commit comments

Comments
 (0)