Skip to content

Commit b963abb

Browse files
committed
update
1 parent 512412e commit b963abb

11 files changed

Lines changed: 298 additions & 62 deletions

File tree

MCPForUnity/Editor/Tools/FindGameObjects.cs

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,51 @@ public static object HandleCommand(JObject @params)
3333
// Parse search parameters
3434
string searchMethod = p.Get("searchMethod", "by_name");
3535

36+
// Search options (supports multiple parameter name variants)
37+
bool includeInactive = p.GetBool("includeInactive", false) ||
38+
p.GetBool("searchInactive", false);
39+
40+
// Pagination parameters using standard PaginationRequest
41+
var pagination = PaginationRequest.FromParams(@params, defaultPageSize: 50);
42+
pagination.PageSize = Mathf.Clamp(pagination.PageSize, 1, 500);
43+
44+
// Check for multi-term search (searchTerms array)
45+
JToken searchTermsToken = @params["searchTerms"];
46+
if (searchTermsToken != null && searchTermsToken.Type == JTokenType.Array)
47+
{
48+
try
49+
{
50+
var searchTerms = searchTermsToken.ToObject<string[]>();
51+
var resultsByTerm = new Dictionary<string, object>();
52+
53+
foreach (string term in searchTerms)
54+
{
55+
if (string.IsNullOrEmpty(term)) continue;
56+
var ids = GameObjectLookup.SearchGameObjects(searchMethod, term, includeInactive, 0);
57+
var paged = PaginationResponse<int>.Create(ids, pagination);
58+
resultsByTerm[term] = new
59+
{
60+
instanceIDs = paged.Items,
61+
totalCount = paged.TotalCount,
62+
hasMore = paged.HasMore,
63+
};
64+
}
65+
66+
return new SuccessResponse("Found GameObjects (multi-term)", new
67+
{
68+
results = resultsByTerm,
69+
searchMethod = searchMethod,
70+
pageSize = pagination.PageSize,
71+
});
72+
}
73+
catch (System.Exception ex)
74+
{
75+
McpLog.Error($"[FindGameObjects] Error in multi-term search: {ex.Message}");
76+
return new ErrorResponse($"Error searching GameObjects: {ex.Message}");
77+
}
78+
}
79+
80+
// Single-term search (original behavior)
3681
// Try searchTerm, search_term, or target (for backwards compatibility)
3782
string searchTerm = p.Get("searchTerm");
3883
if (string.IsNullOrEmpty(searchTerm))
@@ -45,19 +90,11 @@ public static object HandleCommand(JObject @params)
4590
return new ErrorResponse("'searchTerm' or 'target' parameter is required.");
4691
}
4792

48-
// Pagination parameters using standard PaginationRequest
49-
var pagination = PaginationRequest.FromParams(@params, defaultPageSize: 50);
50-
pagination.PageSize = Mathf.Clamp(pagination.PageSize, 1, 500);
51-
52-
// Search options (supports multiple parameter name variants)
53-
bool includeInactive = p.GetBool("includeInactive", false) ||
54-
p.GetBool("searchInactive", false);
55-
5693
try
5794
{
5895
// Get all matching instance IDs
5996
var allIds = GameObjectLookup.SearchGameObjects(searchMethod, searchTerm, includeInactive, 0);
60-
97+
6198
// Use standard pagination response
6299
var paginatedResult = PaginationResponse<int>.Create(allIds, pagination);
63100

MCPForUnity/Editor/Tools/ManageAsset.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -683,9 +683,9 @@ out DateTime parsedDate
683683
string.Join(" ", searchFilters),
684684
folderScope
685685
);
686-
List<object> results = new List<object>();
687-
int totalFound = 0;
688686

687+
// First pass: collect matching asset paths (lightweight — no asset loading)
688+
List<string> filteredPaths = new List<string>();
689689
foreach (string guid in guids)
690690
{
691691
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
@@ -699,18 +699,26 @@ out DateTime parsedDate
699699
Path.Combine(Directory.GetCurrentDirectory(), assetPath)
700700
);
701701
if (lastWriteTime <= filterDateAfter.Value)
702-
{
703-
continue; // Skip assets older than or equal to the filter date
704-
}
702+
continue;
705703
}
706704

707-
totalFound++; // Count matching assets before pagination
708-
results.Add(GetAssetData(assetPath, generatePreview));
705+
filteredPaths.Add(assetPath);
709706
}
710707

711-
// Apply pagination
708+
int totalFound = filteredPaths.Count;
709+
710+
// Paginate BEFORE loading asset data to avoid O(n) asset loads
712711
int startIndex = (pageNumber - 1) * pageSize;
713-
var pagedResults = results.Skip(startIndex).Take(pageSize).ToList();
712+
var pagedPaths = filteredPaths.Skip(startIndex).Take(pageSize).ToList();
713+
714+
// Only load full asset data for the current page
715+
List<object> pagedResults = new List<object>(pagedPaths.Count);
716+
foreach (string assetPath in pagedPaths)
717+
{
718+
var data = GetAssetData(assetPath, generatePreview);
719+
if (data != null)
720+
pagedResults.Add(data);
721+
}
714722

715723
return new SuccessResponse(
716724
$"Found {totalFound} asset(s). Returning page {pageNumber} ({pagedResults.Count} assets).",
@@ -719,6 +727,7 @@ out DateTime parsedDate
719727
totalAssets = totalFound,
720728
pageSize = pageSize,
721729
pageNumber = pageNumber,
730+
totalPages = (int)Math.Ceiling((double)totalFound / pageSize),
722731
assets = pagedResults,
723732
}
724733
);

MCPForUnity/Editor/Tools/ManageScene.cs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ public static object HandleCommand(JObject @params)
218218
return ga;
219219
case "get_build_settings":
220220
return GetBuildSettingsScenes();
221+
case "get_stats":
222+
return GetSceneStats();
221223
case "screenshot":
222224
return CaptureScreenshot(cmd);
223225
case "scene_view_frame":
@@ -413,9 +415,17 @@ private static object SaveScene(string fullPath, string relativePath)
413415
if (saved)
414416
{
415417
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
418+
// Re-fetch scene to get accurate isDirty after save
419+
currentScene = EditorSceneManager.GetActiveScene();
416420
return new SuccessResponse(
417421
$"Scene '{currentScene.name}' saved successfully to '{finalPath}'.",
418-
new { path = finalPath, name = currentScene.name }
422+
new
423+
{
424+
path = finalPath,
425+
name = currentScene.name,
426+
isDirty = currentScene.isDirty,
427+
savedAt = DateTime.UtcNow.ToString("o"),
428+
}
419429
);
420430
}
421431
else
@@ -1256,6 +1266,71 @@ private static object GetBuildSettingsScenes()
12561266
}
12571267
}
12581268

1269+
private static object GetSceneStats()
1270+
{
1271+
try
1272+
{
1273+
Scene activeScene = EditorSceneManager.GetActiveScene();
1274+
if (!activeScene.IsValid() || !activeScene.isLoaded)
1275+
return new ErrorResponse("No valid and loaded scene is active.");
1276+
1277+
var roots = activeScene.GetRootGameObjects();
1278+
int totalObjects = 0;
1279+
int rendererCount = 0;
1280+
int lightCount = 0;
1281+
int cameraCount = 0;
1282+
int canvasCount = 0;
1283+
int scriptedObjectCount = 0;
1284+
int rigidbodyCount = 0;
1285+
int colliderCount = 0;
1286+
int audioSourceCount = 0;
1287+
1288+
void CountRecursive(GameObject go)
1289+
{
1290+
totalObjects++;
1291+
if (go.GetComponent<Renderer>()) rendererCount++;
1292+
if (go.GetComponent<Light>()) lightCount++;
1293+
if (go.GetComponent<Camera>()) cameraCount++;
1294+
if (go.GetComponent<Canvas>()) canvasCount++;
1295+
if (go.GetComponent<Rigidbody>() || go.GetComponent<Rigidbody2D>()) rigidbodyCount++;
1296+
if (go.GetComponent<Collider>() || go.GetComponent<Collider2D>()) colliderCount++;
1297+
if (go.GetComponent<AudioSource>()) audioSourceCount++;
1298+
var scripts = go.GetComponents<MonoBehaviour>();
1299+
if (scripts != null && scripts.Length > 0) scriptedObjectCount++;
1300+
foreach (Transform child in go.transform)
1301+
{
1302+
if (child != null) CountRecursive(child.gameObject);
1303+
}
1304+
}
1305+
1306+
foreach (var root in roots)
1307+
{
1308+
if (root != null) CountRecursive(root);
1309+
}
1310+
1311+
return new SuccessResponse("Scene statistics retrieved.", new
1312+
{
1313+
sceneName = activeScene.name,
1314+
scenePath = activeScene.path,
1315+
isDirty = activeScene.isDirty,
1316+
rootCount = roots.Length,
1317+
totalObjects,
1318+
rendererCount,
1319+
lightCount,
1320+
cameraCount,
1321+
canvasCount,
1322+
scriptedObjectCount,
1323+
rigidbodyCount,
1324+
colliderCount,
1325+
audioSourceCount,
1326+
});
1327+
}
1328+
catch (Exception e)
1329+
{
1330+
return new ErrorResponse($"Error getting scene stats: {e.Message}");
1331+
}
1332+
}
1333+
12591334
private static object GetSceneHierarchyPaged(SceneCommand cmd)
12601335
{
12611336
try

Server/src/cli/commands/scene.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ def active():
9393
click.echo(format_output(result, config.format))
9494

9595

96+
@scene.command("stats")
97+
@handle_unity_errors
98+
def stats():
99+
"""Get scene statistics (object counts, component counts, etc.)."""
100+
config = get_config()
101+
result = run_command("manage_scene", {"action": "get_stats"}, config)
102+
click.echo(format_output(result, config.format))
103+
104+
96105
@scene.command("load")
97106
@click.argument("scene")
98107
@click.option(

Server/src/models/models.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ class MCPResponse(BaseModel):
1010
data: Any | None = None
1111
# Optional hint for clients about how to handle the response.
1212
# Supported values:
13-
# - "retry": Unity is temporarily reloading; call should be retried politely.
13+
# - "retry": Unity is temporarily unavailable; call should be retried politely.
14+
# - "connection_lost": The connection to Unity was lost mid-request.
15+
# Distinct from a Unity-side error — the request may not have been received.
16+
# - "timeout": Unity did not respond within the expected time.
17+
# Check if Unity is compiling or unresponsive before retrying.
1418
hint: str | None = None
1519

1620

Server/src/services/tools/find_gameobjects.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,23 @@
2020
"Returns instance IDs only (paginated). "
2121
"Then use mcpforunity://scene/gameobject/{id} resource for full data, "
2222
"or mcpforunity://scene/gameobject/{id}/components for component details. "
23-
"For CRUD operations (create/modify/delete), use manage_gameobject instead."
23+
"For CRUD operations (create/modify/delete), use manage_gameobject instead. "
24+
"Supports multi-term search: pass search_terms (list) instead of search_term "
25+
"to search for multiple objects in a single call. Returns results keyed by term."
2426
)
2527
)
2628
async def find_gameobjects(
2729
ctx: Context,
2830
search_term: Annotated[
29-
str,
30-
Field(description="The value to search for (name, tag, layer name, component type, or path)")
31-
],
31+
str | None,
32+
Field(description="The value to search for (name, tag, layer name, component type, or path). "
33+
"Use search_terms instead for multi-term search.")
34+
] = None,
35+
search_terms: Annotated[
36+
list[str] | None,
37+
Field(description="List of values to search for in a single call. "
38+
"Returns results keyed by term. Mutually exclusive with search_term.")
39+
] = None,
3240
search_method: Annotated[
3341
Literal["by_name", "by_tag", "by_layer", "by_component", "by_path", "by_id"],
3442
Field(
@@ -72,10 +80,10 @@ async def find_gameobjects(
7280
unity_instance = await get_unity_instance_from_context(ctx)
7381

7482
# Validate required parameters before preflight I/O
75-
if not search_term:
83+
if not search_term and not search_terms:
7684
return {
7785
"success": False,
78-
"message": "Missing required parameter 'search_term'. Specify what to search for."
86+
"message": "Missing required parameter: provide 'search_term' or 'search_terms'."
7987
}
8088

8189
gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True)
@@ -88,13 +96,19 @@ async def find_gameobjects(
8896
cursor = coerce_int(cursor, default=0)
8997

9098
try:
91-
params = {
99+
params: dict[str, Any] = {
92100
"searchMethod": search_method,
93-
"searchTerm": search_term,
94101
"includeInactive": include_inactive,
95102
"pageSize": page_size,
96103
"cursor": cursor,
97104
}
105+
106+
# Multi-term search
107+
if search_terms:
108+
params["searchTerms"] = search_terms
109+
else:
110+
params["searchTerm"] = search_term
111+
98112
params = {k: v for k, v in params.items() if v is not None}
99113

100114
response = await send_with_unity_instance(

Server/src/services/tools/manage_scene.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
@mcp_for_unity_tool(
1616
description=(
1717
"Performs CRUD operations on Unity scenes. "
18-
"Read-only actions: get_hierarchy, get_active, get_build_settings, screenshot, scene_view_frame. "
18+
"Read-only actions: get_hierarchy, get_active, get_build_settings, get_stats, screenshot, scene_view_frame. "
19+
"get_stats returns lightweight scene statistics (rootCount, totalObjects, rendererCount, lightCount, cameraCount, etc.) — use for quick scene validation. "
1920
"Modifying actions: create, load, save. "
2021
"screenshot supports include_image=true to return an inline base64 PNG for AI vision. "
2122
"screenshot with batch='surround' captures 6 angles around the scene (no file saved) for comprehensive scene understanding. "
@@ -36,6 +37,7 @@ async def manage_scene(
3637
"get_hierarchy",
3738
"get_active",
3839
"get_build_settings",
40+
"get_stats",
3941
"screenshot",
4042
"scene_view_frame",
4143
], "Perform CRUD operations on Unity scenes, capture screenshots, and control the Scene View camera."],

0 commit comments

Comments
 (0)