mcpforunityserver 8.7.1__tar.gz → 9.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/PKG-INFO +2 -2
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/README.md +1 -1
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/pyproject.toml +1 -1
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/main.py +4 -3
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/mcpforunityserver.egg-info/PKG-INFO +2 -2
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/mcpforunityserver.egg-info/SOURCES.txt +4 -3
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/mcpforunityserver.egg-info/top_level.txt +0 -1
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/custom_tool_service.py +13 -8
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/resources/active_tool.py +1 -1
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/resources/custom_tools.py +2 -2
- mcpforunityserver-8.7.1/src/services/resources/editor_state_v2.py → mcpforunityserver-9.0.0/src/services/resources/editor_state.py +151 -117
- mcpforunityserver-9.0.0/src/services/resources/gameobject.py +243 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/resources/layers.py +1 -1
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/resources/prefab_stage.py +1 -1
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/resources/project_info.py +1 -1
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/resources/selection.py +1 -1
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/resources/tags.py +1 -1
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/resources/unity_instances.py +1 -1
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/resources/windows.py +1 -1
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/state/external_changes_scanner.py +3 -4
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/batch_execute.py +24 -9
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/debug_request_context.py +8 -2
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/execute_custom_tool.py +6 -1
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/execute_menu_item.py +6 -3
- mcpforunityserver-9.0.0/src/services/tools/find_gameobjects.py +89 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/find_in_file.py +26 -19
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/manage_asset.py +13 -44
- mcpforunityserver-9.0.0/src/services/tools/manage_components.py +131 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/manage_editor.py +9 -8
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/manage_gameobject.py +115 -79
- mcpforunityserver-9.0.0/src/services/tools/manage_material.py +144 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/manage_prefabs.py +7 -1
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/manage_scene.py +30 -13
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/manage_script.py +62 -19
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/manage_scriptable_object.py +22 -10
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/manage_shader.py +8 -1
- mcpforunityserver-9.0.0/src/services/tools/manage_vfx.py +738 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/preflight.py +15 -12
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/read_console.py +11 -4
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/refresh_unity.py +24 -14
- mcpforunityserver-9.0.0/src/services/tools/run_tests.py +229 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/script_apply_edits.py +15 -7
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/set_active_instance.py +12 -7
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/utils.py +60 -6
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/transport/legacy/port_discovery.py +2 -2
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/transport/legacy/unity_connection.py +1 -1
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/transport/plugin_hub.py +24 -16
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/transport/unity_instance_middleware.py +4 -3
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/transport/unity_transport.py +2 -1
- mcpforunityserver-8.7.1/src/services/resources/editor_state.py +0 -51
- mcpforunityserver-8.7.1/src/services/tools/manage_material.py +0 -95
- mcpforunityserver-8.7.1/src/services/tools/run_tests.py +0 -120
- mcpforunityserver-8.7.1/src/services/tools/test_jobs.py +0 -94
- mcpforunityserver-8.7.1/src/transport/__init__.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/LICENSE +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/setup.cfg +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/__init__.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/core/__init__.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/core/config.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/core/logging_decorator.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/core/telemetry.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/core/telemetry_decorator.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/mcpforunityserver.egg-info/dependency_links.txt +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/mcpforunityserver.egg-info/entry_points.txt +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/mcpforunityserver.egg-info/requires.txt +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/models/__init__.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/models/models.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/models/unity_response.py +0 -0
- {mcpforunityserver-8.7.1/src/routes → mcpforunityserver-9.0.0/src/services}/__init__.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/registry/__init__.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/registry/resource_registry.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/registry/tool_registry.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/resources/__init__.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/resources/menu_items.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/resources/tests.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/services/tools/__init__.py +0 -0
- {mcpforunityserver-8.7.1/src/services → mcpforunityserver-9.0.0/src/transport}/__init__.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/transport/legacy/stdio_port_registry.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/transport/models.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/transport/plugin_registry.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/utils/module_discovery.py +0 -0
- {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/utils/reload_sentinel.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcpforunityserver
|
|
3
|
-
Version:
|
|
3
|
+
Version: 9.0.0
|
|
4
4
|
Summary: MCP for Unity Server: A Unity package for Unity Editor integration via the Model Context Protocol (MCP).
|
|
5
5
|
Author-email: Marcus Sanatan <msanatan@gmail.com>, David Sarno <david.sarno@gmail.com>, Wu Shutong <martinwfire@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -108,7 +108,7 @@ Use this to run the latest released version from the repository. Change the vers
|
|
|
108
108
|
"command": "uvx",
|
|
109
109
|
"args": [
|
|
110
110
|
"--from",
|
|
111
|
-
"git+https://github.com/CoplayDev/unity-mcp@
|
|
111
|
+
"git+https://github.com/CoplayDev/unity-mcp@v9.0.0#subdirectory=Server",
|
|
112
112
|
"mcp-for-unity",
|
|
113
113
|
"--transport",
|
|
114
114
|
"stdio"
|
|
@@ -69,7 +69,7 @@ Use this to run the latest released version from the repository. Change the vers
|
|
|
69
69
|
"command": "uvx",
|
|
70
70
|
"args": [
|
|
71
71
|
"--from",
|
|
72
|
-
"git+https://github.com/CoplayDev/unity-mcp@
|
|
72
|
+
"git+https://github.com/CoplayDev/unity-mcp@v9.0.0#subdirectory=Server",
|
|
73
73
|
"mcp-for-unity",
|
|
74
74
|
"--transport",
|
|
75
75
|
"stdio"
|
|
@@ -30,7 +30,8 @@ try: # pragma: no cover - startup safety guard
|
|
|
30
30
|
)
|
|
31
31
|
for _name in _typing_names:
|
|
32
32
|
if not hasattr(builtins, _name) and hasattr(_typing, _name):
|
|
33
|
-
|
|
33
|
+
# type: ignore[attr-defined]
|
|
34
|
+
setattr(builtins, _name, getattr(_typing, _name))
|
|
34
35
|
except Exception:
|
|
35
36
|
pass
|
|
36
37
|
|
|
@@ -234,10 +235,10 @@ mcp = FastMCP(
|
|
|
234
235
|
instructions="""
|
|
235
236
|
This server provides tools to interact with the Unity Game Engine Editor.
|
|
236
237
|
|
|
237
|
-
I have a dynamic tool system. Always check the
|
|
238
|
+
I have a dynamic tool system. Always check the mcpforunity://custom-tools resource first to see what special capabilities are available for the current project.
|
|
238
239
|
|
|
239
240
|
Targeting Unity instances:
|
|
240
|
-
- Use the resource
|
|
241
|
+
- Use the resource mcpforunity://instances to list active Unity sessions (Name@hash).
|
|
241
242
|
- When multiple instances are connected, call set_active_instance with the exact Name@hash before using tools/resources. The server will error if multiple are connected and no active instance is set.
|
|
242
243
|
|
|
243
244
|
Important Workflows:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcpforunityserver
|
|
3
|
-
Version:
|
|
3
|
+
Version: 9.0.0
|
|
4
4
|
Summary: MCP for Unity Server: A Unity package for Unity Editor integration via the Model Context Protocol (MCP).
|
|
5
5
|
Author-email: Marcus Sanatan <msanatan@gmail.com>, David Sarno <david.sarno@gmail.com>, Wu Shutong <martinwfire@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -108,7 +108,7 @@ Use this to run the latest released version from the repository. Change the vers
|
|
|
108
108
|
"command": "uvx",
|
|
109
109
|
"args": [
|
|
110
110
|
"--from",
|
|
111
|
-
"git+https://github.com/CoplayDev/unity-mcp@
|
|
111
|
+
"git+https://github.com/CoplayDev/unity-mcp@v9.0.0#subdirectory=Server",
|
|
112
112
|
"mcp-for-unity",
|
|
113
113
|
"--transport",
|
|
114
114
|
"stdio"
|
{mcpforunityserver-8.7.1 → mcpforunityserver-9.0.0}/src/mcpforunityserver.egg-info/SOURCES.txt
RENAMED
|
@@ -17,7 +17,6 @@ src/mcpforunityserver.egg-info/top_level.txt
|
|
|
17
17
|
src/models/__init__.py
|
|
18
18
|
src/models/models.py
|
|
19
19
|
src/models/unity_response.py
|
|
20
|
-
src/routes/__init__.py
|
|
21
20
|
src/services/__init__.py
|
|
22
21
|
src/services/custom_tool_service.py
|
|
23
22
|
src/services/registry/__init__.py
|
|
@@ -27,7 +26,7 @@ src/services/resources/__init__.py
|
|
|
27
26
|
src/services/resources/active_tool.py
|
|
28
27
|
src/services/resources/custom_tools.py
|
|
29
28
|
src/services/resources/editor_state.py
|
|
30
|
-
src/services/resources/
|
|
29
|
+
src/services/resources/gameobject.py
|
|
31
30
|
src/services/resources/layers.py
|
|
32
31
|
src/services/resources/menu_items.py
|
|
33
32
|
src/services/resources/prefab_stage.py
|
|
@@ -43,8 +42,10 @@ src/services/tools/batch_execute.py
|
|
|
43
42
|
src/services/tools/debug_request_context.py
|
|
44
43
|
src/services/tools/execute_custom_tool.py
|
|
45
44
|
src/services/tools/execute_menu_item.py
|
|
45
|
+
src/services/tools/find_gameobjects.py
|
|
46
46
|
src/services/tools/find_in_file.py
|
|
47
47
|
src/services/tools/manage_asset.py
|
|
48
|
+
src/services/tools/manage_components.py
|
|
48
49
|
src/services/tools/manage_editor.py
|
|
49
50
|
src/services/tools/manage_gameobject.py
|
|
50
51
|
src/services/tools/manage_material.py
|
|
@@ -53,13 +54,13 @@ src/services/tools/manage_scene.py
|
|
|
53
54
|
src/services/tools/manage_script.py
|
|
54
55
|
src/services/tools/manage_scriptable_object.py
|
|
55
56
|
src/services/tools/manage_shader.py
|
|
57
|
+
src/services/tools/manage_vfx.py
|
|
56
58
|
src/services/tools/preflight.py
|
|
57
59
|
src/services/tools/read_console.py
|
|
58
60
|
src/services/tools/refresh_unity.py
|
|
59
61
|
src/services/tools/run_tests.py
|
|
60
62
|
src/services/tools/script_apply_edits.py
|
|
61
63
|
src/services/tools/set_active_instance.py
|
|
62
|
-
src/services/tools/test_jobs.py
|
|
63
64
|
src/services/tools/utils.py
|
|
64
65
|
src/transport/__init__.py
|
|
65
66
|
src/transport/models.py
|
|
@@ -266,15 +266,14 @@ class CustomToolService:
|
|
|
266
266
|
return None
|
|
267
267
|
return {"message": str(response)}
|
|
268
268
|
|
|
269
|
-
def _safe_response(self, response):
|
|
270
|
-
if isinstance(response, dict):
|
|
271
|
-
return response
|
|
272
|
-
if response is None:
|
|
273
|
-
return None
|
|
274
|
-
return {"message": str(response)}
|
|
275
|
-
|
|
276
269
|
|
|
277
270
|
def compute_project_id(project_name: str, project_path: str) -> str:
|
|
271
|
+
"""
|
|
272
|
+
DEPRECATED: Computes a SHA256-based project ID.
|
|
273
|
+
This function is no longer used as of the multi-session fix.
|
|
274
|
+
Unity instances now use their native project_hash (SHA1-based) for consistency
|
|
275
|
+
across stdio and WebSocket transports.
|
|
276
|
+
"""
|
|
278
277
|
combined = f"{project_name}:{project_path}"
|
|
279
278
|
return sha256(combined.encode("utf-8")).hexdigest().upper()[:16]
|
|
280
279
|
|
|
@@ -307,7 +306,13 @@ def resolve_project_id_for_unity_instance(unity_instance: str | None) -> str | N
|
|
|
307
306
|
)
|
|
308
307
|
|
|
309
308
|
if target:
|
|
310
|
-
|
|
309
|
+
# Return the project_hash from Unity (not a computed SHA256 hash).
|
|
310
|
+
# This matches the hash Unity uses when registering tools via WebSocket.
|
|
311
|
+
if target.hash:
|
|
312
|
+
return target.hash
|
|
313
|
+
logger.warning(
|
|
314
|
+
f"Unity instance {target.id} has empty hash; cannot resolve project ID")
|
|
315
|
+
return None
|
|
311
316
|
except Exception:
|
|
312
317
|
logger.debug(
|
|
313
318
|
f"Failed to resolve project id via connection pool for {unity_instance}")
|
|
@@ -31,7 +31,7 @@ class ActiveToolResponse(MCPResponse):
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
@mcp_for_unity_resource(
|
|
34
|
-
uri="
|
|
34
|
+
uri="mcpforunity://editor/active-tool",
|
|
35
35
|
name="editor_active_tool",
|
|
36
36
|
description="Currently active editor tool (Move, Rotate, Scale, etc.) and transform handle settings."
|
|
37
37
|
)
|
|
@@ -22,7 +22,7 @@ class CustomToolsResourceResponse(MCPResponse):
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
@mcp_for_unity_resource(
|
|
25
|
-
uri="
|
|
25
|
+
uri="mcpforunity://custom-tools",
|
|
26
26
|
name="custom_tools",
|
|
27
27
|
description="Lists custom tools available for the active Unity project.",
|
|
28
28
|
)
|
|
@@ -31,7 +31,7 @@ async def get_custom_tools(ctx: Context) -> CustomToolsResourceResponse | MCPRes
|
|
|
31
31
|
if not unity_instance:
|
|
32
32
|
return MCPResponse(
|
|
33
33
|
success=False,
|
|
34
|
-
message="No active Unity instance. Call set_active_instance with Name@hash from
|
|
34
|
+
message="No active Unity instance. Call set_active_instance with Name@hash from mcpforunity://instances.",
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
project_id = resolve_project_id_for_unity_instance(unity_instance)
|
|
@@ -1,15 +1,119 @@
|
|
|
1
|
-
import time
|
|
2
1
|
import os
|
|
2
|
+
import time
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from fastmcp import Context
|
|
6
|
+
from pydantic import BaseModel
|
|
6
7
|
|
|
7
8
|
from models import MCPResponse
|
|
8
9
|
from services.registry import mcp_for_unity_resource
|
|
9
10
|
from services.tools import get_unity_instance_from_context
|
|
11
|
+
from services.state.external_changes_scanner import external_changes_scanner
|
|
10
12
|
import transport.unity_transport as unity_transport
|
|
11
13
|
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
12
|
-
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EditorStateUnity(BaseModel):
|
|
17
|
+
instance_id: str | None = None
|
|
18
|
+
unity_version: str | None = None
|
|
19
|
+
project_id: str | None = None
|
|
20
|
+
platform: str | None = None
|
|
21
|
+
is_batch_mode: bool | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EditorStatePlayMode(BaseModel):
|
|
25
|
+
is_playing: bool | None = None
|
|
26
|
+
is_paused: bool | None = None
|
|
27
|
+
is_changing: bool | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class EditorStateActiveScene(BaseModel):
|
|
31
|
+
path: str | None = None
|
|
32
|
+
guid: str | None = None
|
|
33
|
+
name: str | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EditorStateEditor(BaseModel):
|
|
37
|
+
is_focused: bool | None = None
|
|
38
|
+
play_mode: EditorStatePlayMode | None = None
|
|
39
|
+
active_scene: EditorStateActiveScene | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class EditorStateActivity(BaseModel):
|
|
43
|
+
phase: str | None = None
|
|
44
|
+
since_unix_ms: int | None = None
|
|
45
|
+
reasons: list[str] | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class EditorStateCompilation(BaseModel):
|
|
49
|
+
is_compiling: bool | None = None
|
|
50
|
+
is_domain_reload_pending: bool | None = None
|
|
51
|
+
last_compile_started_unix_ms: int | None = None
|
|
52
|
+
last_compile_finished_unix_ms: int | None = None
|
|
53
|
+
last_domain_reload_before_unix_ms: int | None = None
|
|
54
|
+
last_domain_reload_after_unix_ms: int | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class EditorStateRefresh(BaseModel):
|
|
58
|
+
is_refresh_in_progress: bool | None = None
|
|
59
|
+
last_refresh_requested_unix_ms: int | None = None
|
|
60
|
+
last_refresh_finished_unix_ms: int | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class EditorStateAssets(BaseModel):
|
|
64
|
+
is_updating: bool | None = None
|
|
65
|
+
external_changes_dirty: bool | None = None
|
|
66
|
+
external_changes_last_seen_unix_ms: int | None = None
|
|
67
|
+
external_changes_dirty_since_unix_ms: int | None = None
|
|
68
|
+
external_changes_last_cleared_unix_ms: int | None = None
|
|
69
|
+
refresh: EditorStateRefresh | None = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class EditorStateLastRun(BaseModel):
|
|
73
|
+
finished_unix_ms: int | None = None
|
|
74
|
+
result: str | None = None
|
|
75
|
+
counts: Any | None = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class EditorStateTests(BaseModel):
|
|
79
|
+
is_running: bool | None = None
|
|
80
|
+
mode: str | None = None
|
|
81
|
+
current_job_id: str | None = None
|
|
82
|
+
started_unix_ms: int | None = None
|
|
83
|
+
started_by: str | None = None
|
|
84
|
+
last_run: EditorStateLastRun | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class EditorStateTransport(BaseModel):
|
|
88
|
+
unity_bridge_connected: bool | None = None
|
|
89
|
+
last_message_unix_ms: int | None = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class EditorStateAdvice(BaseModel):
|
|
93
|
+
ready_for_tools: bool | None = None
|
|
94
|
+
blocking_reasons: list[str] | None = None
|
|
95
|
+
recommended_retry_after_ms: int | None = None
|
|
96
|
+
recommended_next_action: str | None = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class EditorStateStaleness(BaseModel):
|
|
100
|
+
age_ms: int | None = None
|
|
101
|
+
is_stale: bool | None = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class EditorStateData(BaseModel):
|
|
105
|
+
schema_version: str
|
|
106
|
+
observed_at_unix_ms: int
|
|
107
|
+
sequence: int
|
|
108
|
+
unity: EditorStateUnity | None = None
|
|
109
|
+
editor: EditorStateEditor | None = None
|
|
110
|
+
activity: EditorStateActivity | None = None
|
|
111
|
+
compilation: EditorStateCompilation | None = None
|
|
112
|
+
assets: EditorStateAssets | None = None
|
|
113
|
+
tests: EditorStateTests | None = None
|
|
114
|
+
transport: EditorStateTransport | None = None
|
|
115
|
+
advice: EditorStateAdvice | None = None
|
|
116
|
+
staleness: EditorStateStaleness | None = None
|
|
13
117
|
|
|
14
118
|
|
|
15
119
|
def _now_unix_ms() -> int:
|
|
@@ -21,13 +125,12 @@ def _in_pytest() -> bool:
|
|
|
21
125
|
return bool(os.environ.get("PYTEST_CURRENT_TEST"))
|
|
22
126
|
|
|
23
127
|
|
|
24
|
-
async def
|
|
128
|
+
async def infer_single_instance_id(ctx: Context) -> str | None:
|
|
25
129
|
"""
|
|
26
130
|
Best-effort: if exactly one Unity instance is connected, return its Name@hash id.
|
|
27
131
|
This makes editor_state outputs self-describing even when no explicit active instance is set.
|
|
28
132
|
"""
|
|
29
|
-
|
|
30
|
-
return None
|
|
133
|
+
await ctx.info("If exactly one Unity instance is connected, return its Name@hash id.")
|
|
31
134
|
|
|
32
135
|
try:
|
|
33
136
|
transport = unity_transport._current_transport()
|
|
@@ -40,7 +143,8 @@ async def _infer_single_instance_id(ctx: Context) -> str | None:
|
|
|
40
143
|
from transport.plugin_hub import PluginHub
|
|
41
144
|
|
|
42
145
|
sessions_data = await PluginHub.get_sessions()
|
|
43
|
-
sessions = sessions_data.sessions if hasattr(
|
|
146
|
+
sessions = sessions_data.sessions if hasattr(
|
|
147
|
+
sessions_data, "sessions") else {}
|
|
44
148
|
if isinstance(sessions, dict) and len(sessions) == 1:
|
|
45
149
|
session = next(iter(sessions.values()))
|
|
46
150
|
project = getattr(session, "project", None)
|
|
@@ -66,78 +170,6 @@ async def _infer_single_instance_id(ctx: Context) -> str | None:
|
|
|
66
170
|
return None
|
|
67
171
|
|
|
68
172
|
|
|
69
|
-
def _build_v2_from_legacy(legacy: dict[str, Any]) -> dict[str, Any]:
|
|
70
|
-
"""
|
|
71
|
-
Best-effort mapping from legacy get_editor_state payload into the v2 contract.
|
|
72
|
-
Legacy shape (Unity): {isPlaying,isPaused,isCompiling,isUpdating,timeSinceStartup,...}
|
|
73
|
-
"""
|
|
74
|
-
now_ms = _now_unix_ms()
|
|
75
|
-
# legacy may arrive already wrapped as MCPResponse-like {success,data:{...}}
|
|
76
|
-
state = legacy.get("data") if isinstance(legacy.get("data"), dict) else {}
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
"schema_version": "unity-mcp/editor_state@2",
|
|
80
|
-
"observed_at_unix_ms": now_ms,
|
|
81
|
-
"sequence": 0,
|
|
82
|
-
"unity": {
|
|
83
|
-
"instance_id": None,
|
|
84
|
-
"unity_version": None,
|
|
85
|
-
"project_id": None,
|
|
86
|
-
"platform": None,
|
|
87
|
-
"is_batch_mode": None,
|
|
88
|
-
},
|
|
89
|
-
"editor": {
|
|
90
|
-
"is_focused": None,
|
|
91
|
-
"play_mode": {
|
|
92
|
-
"is_playing": bool(state.get("isPlaying", False)),
|
|
93
|
-
"is_paused": bool(state.get("isPaused", False)),
|
|
94
|
-
"is_changing": None,
|
|
95
|
-
},
|
|
96
|
-
"active_scene": {
|
|
97
|
-
"path": None,
|
|
98
|
-
"guid": None,
|
|
99
|
-
"name": state.get("activeSceneName", "") or "",
|
|
100
|
-
},
|
|
101
|
-
"selection": {
|
|
102
|
-
"count": int(state.get("selectionCount", 0) or 0),
|
|
103
|
-
"active_object_name": state.get("activeObjectName", None),
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
"activity": {
|
|
107
|
-
"phase": "unknown",
|
|
108
|
-
"since_unix_ms": now_ms,
|
|
109
|
-
"reasons": ["legacy_fallback"],
|
|
110
|
-
},
|
|
111
|
-
"compilation": {
|
|
112
|
-
"is_compiling": bool(state.get("isCompiling", False)),
|
|
113
|
-
"is_domain_reload_pending": None,
|
|
114
|
-
"last_compile_started_unix_ms": None,
|
|
115
|
-
"last_compile_finished_unix_ms": None,
|
|
116
|
-
},
|
|
117
|
-
"assets": {
|
|
118
|
-
"is_updating": bool(state.get("isUpdating", False)),
|
|
119
|
-
"external_changes_dirty": False,
|
|
120
|
-
"external_changes_last_seen_unix_ms": None,
|
|
121
|
-
"refresh": {
|
|
122
|
-
"is_refresh_in_progress": False,
|
|
123
|
-
"last_refresh_requested_unix_ms": None,
|
|
124
|
-
"last_refresh_finished_unix_ms": None,
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
"tests": {
|
|
128
|
-
"is_running": False,
|
|
129
|
-
"mode": None,
|
|
130
|
-
"started_unix_ms": None,
|
|
131
|
-
"started_by": "unknown",
|
|
132
|
-
"last_run": None,
|
|
133
|
-
},
|
|
134
|
-
"transport": {
|
|
135
|
-
"unity_bridge_connected": None,
|
|
136
|
-
"last_message_unix_ms": None,
|
|
137
|
-
},
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
173
|
def _enrich_advice_and_staleness(state_v2: dict[str, Any]) -> dict[str, Any]:
|
|
142
174
|
now_ms = _now_unix_ms()
|
|
143
175
|
observed = state_v2.get("observed_at_unix_ms")
|
|
@@ -180,18 +212,17 @@ def _enrich_advice_and_staleness(state_v2: dict[str, Any]) -> dict[str, Any]:
|
|
|
180
212
|
|
|
181
213
|
|
|
182
214
|
@mcp_for_unity_resource(
|
|
183
|
-
uri="
|
|
184
|
-
name="
|
|
185
|
-
description="Canonical editor readiness snapshot
|
|
215
|
+
uri="mcpforunity://editor/state",
|
|
216
|
+
name="editor_state",
|
|
217
|
+
description="Canonical editor readiness snapshot. Includes advice and server-computed staleness.",
|
|
186
218
|
)
|
|
187
|
-
async def
|
|
219
|
+
async def get_editor_state(ctx: Context) -> MCPResponse:
|
|
188
220
|
unity_instance = get_unity_instance_from_context(ctx)
|
|
189
221
|
|
|
190
|
-
# Try v2 snapshot first (Unity-side cache will make this fast once implemented).
|
|
191
222
|
response = await unity_transport.send_with_unity_instance(
|
|
192
223
|
async_send_command_with_retry,
|
|
193
224
|
unity_instance,
|
|
194
|
-
"
|
|
225
|
+
"get_editor_state",
|
|
195
226
|
{},
|
|
196
227
|
)
|
|
197
228
|
|
|
@@ -199,26 +230,13 @@ async def get_editor_state_v2(ctx: Context) -> MCPResponse:
|
|
|
199
230
|
if isinstance(response, dict) and not response.get("success", True):
|
|
200
231
|
return MCPResponse(**response)
|
|
201
232
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
"get_editor_state",
|
|
208
|
-
{},
|
|
209
|
-
)
|
|
210
|
-
if isinstance(legacy, dict) and not legacy.get("success", True):
|
|
211
|
-
return MCPResponse(**legacy)
|
|
212
|
-
state_v2 = _build_v2_from_legacy(legacy if isinstance(legacy, dict) else {})
|
|
213
|
-
else:
|
|
214
|
-
state_v2 = response.get("data") if isinstance(response.get("data"), dict) else {}
|
|
215
|
-
# Ensure required v2 marker exists even if Unity returns partial.
|
|
216
|
-
state_v2.setdefault("schema_version", "unity-mcp/editor_state@2")
|
|
217
|
-
state_v2.setdefault("observed_at_unix_ms", _now_unix_ms())
|
|
218
|
-
state_v2.setdefault("sequence", 0)
|
|
233
|
+
state_v2 = response.get("data") if isinstance(
|
|
234
|
+
response, dict) and isinstance(response.get("data"), dict) else {}
|
|
235
|
+
state_v2.setdefault("schema_version", "unity-mcp/editor_state@2")
|
|
236
|
+
state_v2.setdefault("observed_at_unix_ms", _now_unix_ms())
|
|
237
|
+
state_v2.setdefault("sequence", 0)
|
|
219
238
|
|
|
220
239
|
# Ensure the returned snapshot is clearly associated with the targeted instance.
|
|
221
|
-
# (This matters when multiple Unity instances are connected and the client is polling readiness.)
|
|
222
240
|
unity_section = state_v2.get("unity")
|
|
223
241
|
if not isinstance(unity_section, dict):
|
|
224
242
|
unity_section = {}
|
|
@@ -228,24 +246,25 @@ async def get_editor_state_v2(ctx: Context) -> MCPResponse:
|
|
|
228
246
|
if unity_instance:
|
|
229
247
|
unity_section["instance_id"] = unity_instance
|
|
230
248
|
else:
|
|
231
|
-
inferred = await
|
|
249
|
+
inferred = await infer_single_instance_id(ctx)
|
|
232
250
|
if inferred:
|
|
233
251
|
unity_section["instance_id"] = inferred
|
|
234
252
|
|
|
235
253
|
# External change detection (server-side): compute per instance based on project root path.
|
|
236
|
-
# This helps detect stale assets when external tools edit the filesystem.
|
|
237
254
|
try:
|
|
238
255
|
instance_id = unity_section.get("instance_id")
|
|
239
256
|
if isinstance(instance_id, str) and instance_id.strip():
|
|
240
257
|
from services.resources.project_info import get_project_info
|
|
241
258
|
|
|
242
|
-
# Cache the project root for this instance (best-effort).
|
|
243
259
|
proj_resp = await get_project_info(ctx)
|
|
244
|
-
proj = proj_resp.model_dump() if hasattr(
|
|
260
|
+
proj = proj_resp.model_dump() if hasattr(
|
|
261
|
+
proj_resp, "model_dump") else proj_resp
|
|
245
262
|
proj_data = proj.get("data") if isinstance(proj, dict) else None
|
|
246
|
-
project_root = proj_data.get("projectRoot") if isinstance(
|
|
263
|
+
project_root = proj_data.get("projectRoot") if isinstance(
|
|
264
|
+
proj_data, dict) else None
|
|
247
265
|
if isinstance(project_root, str) and project_root.strip():
|
|
248
|
-
external_changes_scanner.set_project_root(
|
|
266
|
+
external_changes_scanner.set_project_root(
|
|
267
|
+
instance_id, project_root)
|
|
249
268
|
|
|
250
269
|
ext = external_changes_scanner.update_and_get(instance_id)
|
|
251
270
|
|
|
@@ -253,18 +272,33 @@ async def get_editor_state_v2(ctx: Context) -> MCPResponse:
|
|
|
253
272
|
if not isinstance(assets, dict):
|
|
254
273
|
assets = {}
|
|
255
274
|
state_v2["assets"] = assets
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
assets["
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
assets["external_changes_last_cleared_unix_ms"] = ext.get(
|
|
275
|
+
assets["external_changes_dirty"] = bool(
|
|
276
|
+
ext.get("external_changes_dirty", False))
|
|
277
|
+
assets["external_changes_last_seen_unix_ms"] = ext.get(
|
|
278
|
+
"external_changes_last_seen_unix_ms")
|
|
279
|
+
assets["external_changes_dirty_since_unix_ms"] = ext.get(
|
|
280
|
+
"dirty_since_unix_ms")
|
|
281
|
+
assets["external_changes_last_cleared_unix_ms"] = ext.get(
|
|
282
|
+
"last_cleared_unix_ms")
|
|
263
283
|
except Exception:
|
|
264
|
-
# Best-effort; do not fail readiness resource if filesystem scan can't run.
|
|
265
284
|
pass
|
|
266
285
|
|
|
267
286
|
state_v2 = _enrich_advice_and_staleness(state_v2)
|
|
268
|
-
return MCPResponse(success=True, message="Retrieved editor state (v2).", data=state_v2)
|
|
269
287
|
|
|
288
|
+
try:
|
|
289
|
+
if hasattr(EditorStateData, "model_validate"):
|
|
290
|
+
validated = EditorStateData.model_validate(state_v2)
|
|
291
|
+
else:
|
|
292
|
+
validated = EditorStateData.parse_obj(
|
|
293
|
+
state_v2) # type: ignore[attr-defined]
|
|
294
|
+
data = validated.model_dump() if hasattr(
|
|
295
|
+
validated, "model_dump") else validated.dict()
|
|
296
|
+
except Exception as e:
|
|
297
|
+
return MCPResponse(
|
|
298
|
+
success=False,
|
|
299
|
+
error="invalid_editor_state",
|
|
300
|
+
message=f"Editor state payload failed validation: {e}",
|
|
301
|
+
data={"raw": state_v2},
|
|
302
|
+
)
|
|
270
303
|
|
|
304
|
+
return MCPResponse(success=True, message="Retrieved editor state.", data=data)
|