mcpforunityserver 8.7.1__tar.gz → 9.0.1__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.
Files changed (82) hide show
  1. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/PKG-INFO +2 -2
  2. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/README.md +1 -1
  3. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/pyproject.toml +1 -1
  4. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/main.py +4 -3
  5. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/mcpforunityserver.egg-info/PKG-INFO +2 -2
  6. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/mcpforunityserver.egg-info/SOURCES.txt +4 -3
  7. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/mcpforunityserver.egg-info/top_level.txt +0 -1
  8. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/custom_tool_service.py +13 -8
  9. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/resources/active_tool.py +1 -1
  10. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/resources/custom_tools.py +2 -2
  11. mcpforunityserver-8.7.1/src/services/resources/editor_state_v2.py → mcpforunityserver-9.0.1/src/services/resources/editor_state.py +151 -117
  12. mcpforunityserver-9.0.1/src/services/resources/gameobject.py +243 -0
  13. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/resources/layers.py +1 -1
  14. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/resources/prefab_stage.py +1 -1
  15. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/resources/project_info.py +1 -1
  16. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/resources/selection.py +1 -1
  17. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/resources/tags.py +1 -1
  18. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/resources/unity_instances.py +1 -1
  19. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/resources/windows.py +1 -1
  20. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/state/external_changes_scanner.py +3 -4
  21. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/batch_execute.py +24 -9
  22. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/debug_request_context.py +8 -2
  23. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/execute_custom_tool.py +6 -1
  24. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/execute_menu_item.py +6 -3
  25. mcpforunityserver-9.0.1/src/services/tools/find_gameobjects.py +89 -0
  26. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/find_in_file.py +26 -19
  27. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/manage_asset.py +13 -44
  28. mcpforunityserver-9.0.1/src/services/tools/manage_components.py +131 -0
  29. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/manage_editor.py +9 -8
  30. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/manage_gameobject.py +115 -79
  31. mcpforunityserver-9.0.1/src/services/tools/manage_material.py +144 -0
  32. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/manage_prefabs.py +7 -1
  33. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/manage_scene.py +30 -13
  34. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/manage_script.py +62 -19
  35. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/manage_scriptable_object.py +22 -10
  36. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/manage_shader.py +8 -1
  37. mcpforunityserver-9.0.1/src/services/tools/manage_vfx.py +738 -0
  38. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/preflight.py +15 -12
  39. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/read_console.py +11 -4
  40. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/refresh_unity.py +24 -14
  41. mcpforunityserver-9.0.1/src/services/tools/run_tests.py +229 -0
  42. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/script_apply_edits.py +15 -7
  43. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/set_active_instance.py +12 -7
  44. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/utils.py +60 -6
  45. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/transport/legacy/port_discovery.py +2 -2
  46. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/transport/legacy/unity_connection.py +1 -1
  47. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/transport/plugin_hub.py +24 -16
  48. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/transport/unity_instance_middleware.py +4 -3
  49. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/transport/unity_transport.py +2 -1
  50. mcpforunityserver-8.7.1/src/services/resources/editor_state.py +0 -51
  51. mcpforunityserver-8.7.1/src/services/tools/manage_material.py +0 -95
  52. mcpforunityserver-8.7.1/src/services/tools/run_tests.py +0 -120
  53. mcpforunityserver-8.7.1/src/services/tools/test_jobs.py +0 -94
  54. mcpforunityserver-8.7.1/src/transport/__init__.py +0 -0
  55. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/LICENSE +0 -0
  56. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/setup.cfg +0 -0
  57. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/__init__.py +0 -0
  58. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/core/__init__.py +0 -0
  59. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/core/config.py +0 -0
  60. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/core/logging_decorator.py +0 -0
  61. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/core/telemetry.py +0 -0
  62. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/core/telemetry_decorator.py +0 -0
  63. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/mcpforunityserver.egg-info/dependency_links.txt +0 -0
  64. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/mcpforunityserver.egg-info/entry_points.txt +0 -0
  65. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/mcpforunityserver.egg-info/requires.txt +0 -0
  66. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/models/__init__.py +0 -0
  67. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/models/models.py +0 -0
  68. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/models/unity_response.py +0 -0
  69. {mcpforunityserver-8.7.1/src/routes → mcpforunityserver-9.0.1/src/services}/__init__.py +0 -0
  70. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/registry/__init__.py +0 -0
  71. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/registry/resource_registry.py +0 -0
  72. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/registry/tool_registry.py +0 -0
  73. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/resources/__init__.py +0 -0
  74. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/resources/menu_items.py +0 -0
  75. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/resources/tests.py +0 -0
  76. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/services/tools/__init__.py +0 -0
  77. {mcpforunityserver-8.7.1/src/services → mcpforunityserver-9.0.1/src/transport}/__init__.py +0 -0
  78. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/transport/legacy/stdio_port_registry.py +0 -0
  79. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/transport/models.py +0 -0
  80. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/transport/plugin_registry.py +0 -0
  81. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/utils/module_discovery.py +0 -0
  82. {mcpforunityserver-8.7.1 → mcpforunityserver-9.0.1}/src/utils/reload_sentinel.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcpforunityserver
3
- Version: 8.7.1
3
+ Version: 9.0.1
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@v8.7.1#subdirectory=Server",
111
+ "git+https://github.com/CoplayDev/unity-mcp@v9.0.1#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@v8.7.1#subdirectory=Server",
72
+ "git+https://github.com/CoplayDev/unity-mcp@v9.0.1#subdirectory=Server",
73
73
  "mcp-for-unity",
74
74
  "--transport",
75
75
  "stdio"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcpforunityserver"
3
- version = "8.7.1"
3
+ version = "9.0.1"
4
4
  description = "MCP for Unity Server: A Unity package for Unity Editor integration via the Model Context Protocol (MCP)."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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
- setattr(builtins, _name, getattr(_typing, _name)) # type: ignore[attr-defined]
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 unity://custom-tools resource first to see what special capabilities are available for the current project.
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 unity://instances to list active Unity sessions (Name@hash).
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: 8.7.1
3
+ Version: 9.0.1
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@v8.7.1#subdirectory=Server",
111
+ "git+https://github.com/CoplayDev/unity-mcp@v9.0.1#subdirectory=Server",
112
112
  "mcp-for-unity",
113
113
  "--transport",
114
114
  "stdio"
@@ -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/editor_state_v2.py
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
@@ -2,7 +2,6 @@ __init__
2
2
  core
3
3
  main
4
4
  models
5
- routes
6
5
  services
7
6
  transport
8
7
  utils
@@ -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
- return compute_project_id(target.name, target.path)
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="unity://editor/active-tool",
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="unity://custom-tools",
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 unity://instances.",
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
- from services.state.external_changes_scanner import external_changes_scanner
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 _infer_single_instance_id(ctx: Context) -> str | None:
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
- if _in_pytest():
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(sessions_data, "sessions") else {}
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="unity://editor_state",
184
- name="editor_state_v2",
185
- description="Canonical editor readiness snapshot (v2). Includes advice and server-computed staleness.",
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 get_editor_state_v2(ctx: Context) -> MCPResponse:
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
- "get_editor_state_v2",
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
- # If v2 is unavailable (older plugin), fall back to legacy get_editor_state and map.
203
- if not (isinstance(response, dict) and isinstance(response.get("data"), dict) and response["data"].get("schema_version")):
204
- legacy = await unity_transport.send_with_unity_instance(
205
- async_send_command_with_retry,
206
- unity_instance,
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 _infer_single_instance_id(ctx)
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(proj_resp, "model_dump") else proj_resp
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(proj_data, dict) else None
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(instance_id, 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
- # IMPORTANT: Unity's cached snapshot may include placeholder defaults; the server scanner is authoritative
257
- # for external changes (filesystem edits outside Unity). Always overwrite these fields from the scanner.
258
- assets["external_changes_dirty"] = bool(ext.get("external_changes_dirty", False))
259
- assets["external_changes_last_seen_unix_ms"] = ext.get("external_changes_last_seen_unix_ms")
260
- # Extra bookkeeping fields (server-only) are safe to add under assets.
261
- assets["external_changes_dirty_since_unix_ms"] = ext.get("dirty_since_unix_ms")
262
- assets["external_changes_last_cleared_unix_ms"] = ext.get("last_cleared_unix_ms")
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)