mcpforunityserver 8.7.1__py3-none-any.whl → 9.0.1__py3-none-any.whl
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.
- main.py +4 -3
- {mcpforunityserver-8.7.1.dist-info → mcpforunityserver-9.0.1.dist-info}/METADATA +2 -2
- mcpforunityserver-9.0.1.dist-info/RECORD +72 -0
- {mcpforunityserver-8.7.1.dist-info → mcpforunityserver-9.0.1.dist-info}/top_level.txt +0 -1
- services/custom_tool_service.py +13 -8
- services/resources/active_tool.py +1 -1
- services/resources/custom_tools.py +2 -2
- services/resources/editor_state.py +283 -30
- services/resources/gameobject.py +243 -0
- services/resources/layers.py +1 -1
- services/resources/prefab_stage.py +1 -1
- services/resources/project_info.py +1 -1
- services/resources/selection.py +1 -1
- services/resources/tags.py +1 -1
- services/resources/unity_instances.py +1 -1
- services/resources/windows.py +1 -1
- services/state/external_changes_scanner.py +3 -4
- services/tools/batch_execute.py +24 -9
- services/tools/debug_request_context.py +8 -2
- services/tools/execute_custom_tool.py +6 -1
- services/tools/execute_menu_item.py +6 -3
- services/tools/find_gameobjects.py +89 -0
- services/tools/find_in_file.py +26 -19
- services/tools/manage_asset.py +13 -44
- services/tools/manage_components.py +131 -0
- services/tools/manage_editor.py +9 -8
- services/tools/manage_gameobject.py +115 -79
- services/tools/manage_material.py +80 -31
- services/tools/manage_prefabs.py +7 -1
- services/tools/manage_scene.py +30 -13
- services/tools/manage_script.py +62 -19
- services/tools/manage_scriptable_object.py +22 -10
- services/tools/manage_shader.py +8 -1
- services/tools/manage_vfx.py +738 -0
- services/tools/preflight.py +15 -12
- services/tools/read_console.py +11 -4
- services/tools/refresh_unity.py +24 -14
- services/tools/run_tests.py +162 -53
- services/tools/script_apply_edits.py +15 -7
- services/tools/set_active_instance.py +12 -7
- services/tools/utils.py +60 -6
- transport/legacy/port_discovery.py +2 -2
- transport/legacy/unity_connection.py +1 -1
- transport/plugin_hub.py +24 -16
- transport/unity_instance_middleware.py +4 -3
- transport/unity_transport.py +2 -1
- mcpforunityserver-8.7.1.dist-info/RECORD +0 -71
- routes/__init__.py +0 -0
- services/resources/editor_state_v2.py +0 -270
- services/tools/test_jobs.py +0 -94
- {mcpforunityserver-8.7.1.dist-info → mcpforunityserver-9.0.1.dist-info}/WHEEL +0 -0
- {mcpforunityserver-8.7.1.dist-info → mcpforunityserver-9.0.1.dist-info}/entry_points.txt +0 -0
- {mcpforunityserver-8.7.1.dist-info → mcpforunityserver-9.0.1.dist-info}/licenses/LICENSE +0 -0
main.py
CHANGED
|
@@ -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.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@
|
|
111
|
+
"git+https://github.com/CoplayDev/unity-mcp@v9.0.1#subdirectory=Server",
|
|
112
112
|
"mcp-for-unity",
|
|
113
113
|
"--transport",
|
|
114
114
|
"stdio"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
main.py,sha256=2JiIIoWXnhEhEOg-YN-KWNG-tB_uGscR1U7dK_hQO88,19207
|
|
3
|
+
core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
core/config.py,sha256=czkTtNji1crQcQbUvmdx4OL7f-RBqkVhj_PtHh-w7rs,1623
|
|
5
|
+
core/logging_decorator.py,sha256=D9CD7rFvQz-MBG-G4inizQj0Ivr6dfc9RBmTrw7q8mI,1383
|
|
6
|
+
core/telemetry.py,sha256=eHjYgzd8f7eTwSwF2Kbi8D4TtJIcdaDjKLeo1c-0hVA,19829
|
|
7
|
+
core/telemetry_decorator.py,sha256=ycSTrzVNCDQHSd-xmIWOpVfKFURPxpiZe_XkOQAGDAo,6705
|
|
8
|
+
mcpforunityserver-9.0.1.dist-info/licenses/LICENSE,sha256=bv5lDJZQEqxBgjjc1rkRbkEwpSIHF-8N-1Od0VnEJFw,1066
|
|
9
|
+
models/__init__.py,sha256=JlscZkGWE9TRmSoBi99v_LSl8OAFNGmr8463PYkXin4,179
|
|
10
|
+
models/models.py,sha256=heXuvdBtdats1SGwW8wKFFHM0qR4hA6A7qETn5s9BZ0,1827
|
|
11
|
+
models/unity_response.py,sha256=oJ1PTsnNc5VBC-9OgM59C0C-R9N-GdmEdmz_yph4GSU,1454
|
|
12
|
+
services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
services/custom_tool_service.py,sha256=VYqKcP0BZLTo9SGyNMtoLhFbzRlF8oCeMjeNdTScJiU,12320
|
|
14
|
+
services/registry/__init__.py,sha256=QCwcYThvGF0kBt3WR6DBskdyxkegJC7NymEChgJA-YM,470
|
|
15
|
+
services/registry/resource_registry.py,sha256=T_Kznqgvt5kKgV7mU85nb0LlFuB4rg-Tm4Cjhxt-IcI,1467
|
|
16
|
+
services/registry/tool_registry.py,sha256=9tMwOP07JE92QFYUS4KvoysO0qC9pkBD5B79kjRsSPw,1304
|
|
17
|
+
services/resources/__init__.py,sha256=O5heeMcgCswnQX1qG2nNtMeAZIaLut734qD7t5UsA0k,2801
|
|
18
|
+
services/resources/active_tool.py,sha256=zDuWRK1uz853TrMNv0w8vhZVOxemDPoI4QAkXSIezN8,1480
|
|
19
|
+
services/resources/custom_tools.py,sha256=3t0mKAL9PkJbv8S4DpRFU8D-NlRWkCd2geO6QnlQo7I,1716
|
|
20
|
+
services/resources/editor_state.py,sha256=pQdcsWGcKV7-6icpcVXtFD35CHUXodANc0jXkljVdLs,10823
|
|
21
|
+
services/resources/gameobject.py,sha256=RM28kfsV208zdTy-549U2_nwSPiAHYo6SqXy22k4tC8,9116
|
|
22
|
+
services/resources/layers.py,sha256=wE-mSgZsknGrXKu-0Cppv6NeijszD7beFf88dizT0ZI,1086
|
|
23
|
+
services/resources/menu_items.py,sha256=9SNycjwTXoeS1ZHra0Y1fTyCjSEdPCo34JyxtuqauG8,1021
|
|
24
|
+
services/resources/prefab_stage.py,sha256=RyVskG-P9lb4szbsTDhPpyDMb0ptLskr0BnoYJylhw0,1388
|
|
25
|
+
services/resources/project_info.py,sha256=ggiUj9rJUvIddxorKu9yqJiHTWOnxyywkjjsKXhIyqA,1329
|
|
26
|
+
services/resources/selection.py,sha256=MALwKkM9xsKing2bALNVTVLWzDTE_b26EVbnVUGZivU,1845
|
|
27
|
+
services/resources/tags.py,sha256=IKZWiZhBO_HkJqFXqBvWeIcMxhGN_QXkonzuAEFsEfg,1055
|
|
28
|
+
services/resources/tests.py,sha256=xDvvgesPSU93nLD_ERQopOpkpq69pbMEqmFsJd0jekI,2063
|
|
29
|
+
services/resources/unity_instances.py,sha256=XRR5YCDe8v_FXG45VlSdEPaqu7Qlbnm4NYIRzK5brjc,4354
|
|
30
|
+
services/resources/windows.py,sha256=FyzPEtEmfKiXYh1lviemZ7-bFyjkAR61_seSTXQA9rk,1433
|
|
31
|
+
services/state/external_changes_scanner.py,sha256=ZiXu8ZcK5B-hv7CaJLmnEIa9JxzgOBpdmrsRDY2eK5I,9052
|
|
32
|
+
services/tools/__init__.py,sha256=3Qav7fAowZ1_TbDRdZQQQES53gv2lTs-2D7PGECnlbM,2353
|
|
33
|
+
services/tools/batch_execute.py,sha256=hjh67kgWvQDHyGd2N-Tfezv9WAj5x_pWTt_Vybmmq7s,3501
|
|
34
|
+
services/tools/debug_request_context.py,sha256=Duq5xiuSmRO5GdvWAlZhCfOfmrwvK7gGkRC4wYnXmXk,2907
|
|
35
|
+
services/tools/execute_custom_tool.py,sha256=hiZbm2A9t84f92jitzvkE2G4CMOIUiDVm7u5B8K-RbU,1527
|
|
36
|
+
services/tools/execute_menu_item.py,sha256=k4J89LlXmEGyo9z3NK8Q0vREIzr11ucF_9tN_JeQq9M,1248
|
|
37
|
+
services/tools/find_gameobjects.py,sha256=Qpfd_oQG0fluz8S1CfriGh1FmLnZ080-ZEZOrAsij8U,3602
|
|
38
|
+
services/tools/find_in_file.py,sha256=SxhMeo8lRrt0OiGApGZSFUnq671bxVfK8qgAsHxLua8,6493
|
|
39
|
+
services/tools/manage_asset.py,sha256=St_iWQWg9icztnRthU78t6JNhJN0AlC6ELiZhn-SNZU,5990
|
|
40
|
+
services/tools/manage_components.py,sha256=Z74QWrOdJyvQBRqIk3ZNYhUxyyH-R0Er1cEkt-70Hcg,5025
|
|
41
|
+
services/tools/manage_editor.py,sha256=ShvlSBQRfoNQ0DvqBWak_Hi3MB7tv2WkMKEhrKQipk0,3279
|
|
42
|
+
services/tools/manage_gameobject.py,sha256=fOuaYoQAZ1OgCnfSMZboqISRUr6bIuaBcG6XV-iVN_M,15092
|
|
43
|
+
services/tools/manage_material.py,sha256=0i5gsTXkahzq_5qEByXvsMHxMpWuMBJH3HzbEr8GQns,5520
|
|
44
|
+
services/tools/manage_prefabs.py,sha256=5waHRvxbdVA9N60tvf9m1f9NTRPspyxZOh4GZfuCfII,3179
|
|
45
|
+
services/tools/manage_scene.py,sha256=-ARtRuj7ZNk_14lmMSORnQs0qTAYKBTPtUfk0sNDo6A,5370
|
|
46
|
+
services/tools/manage_script.py,sha256=MzPw0xXjtbdjEyjvUfLem9fa3GVE-WGvCr4WEVfW9Cs,28461
|
|
47
|
+
services/tools/manage_scriptable_object.py,sha256=tezG_mbGzPLNpL3F7l5JJLyyjJN3rJi1thGMU8cpOC4,3659
|
|
48
|
+
services/tools/manage_shader.py,sha256=bucRKzQww7opy6DK5nf6isVaEECWWqJ-DVkFulp8CV8,3185
|
|
49
|
+
services/tools/manage_vfx.py,sha256=eeqf4xUYw_yT2rALIGHrHLJCpemx9H__S3zCjj_GZsI,34054
|
|
50
|
+
services/tools/preflight.py,sha256=0nvo0BmZMdIGop1Ha_vypkjn2VLiRvskF0uxh_SlZgE,4162
|
|
51
|
+
services/tools/read_console.py,sha256=k1brS1yR-wyMgdhL6TPL-j4KJCD0CKSWrHYiTh-gj9Y,5452
|
|
52
|
+
services/tools/refresh_unity.py,sha256=IcShwasfveDGxXQ3YdbaQP3ICCt8e8O_q_NsnDa8glw,4054
|
|
53
|
+
services/tools/run_tests.py,sha256=9M9noRsZWjqcCfUWo5XVVtGNggxg5HpPvmkobs2lu-A,8082
|
|
54
|
+
services/tools/script_apply_edits.py,sha256=0f-SaP5NUYGuivl4CWHjR8F-CXUpt3-5qkHpf_edn1U,47677
|
|
55
|
+
services/tools/set_active_instance.py,sha256=pdmC1SxFijyzzjeEyC2N1bXk-GNMu_iXsbCieIpa-R4,4242
|
|
56
|
+
services/tools/utils.py,sha256=uk--6w_-O0eVAxczackXbgKde2ONmsgci43G3wY7dfA,4258
|
|
57
|
+
transport/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
58
|
+
transport/models.py,sha256=6wp7wsmSaeeJEvUGXPF1m6zuJnxJ1NJlCC4YZ9oQIq0,1226
|
|
59
|
+
transport/plugin_hub.py,sha256=ku8CdK6kdxUrRMd9zjUn7fFARx8rMZ2FWD7zWGuN2Ys,23083
|
|
60
|
+
transport/plugin_registry.py,sha256=nW-7O7PN0QUgSWivZTkpAVKKq9ZOe2b2yeIdpaNt_3I,4359
|
|
61
|
+
transport/unity_instance_middleware.py,sha256=DD8gs-peMRmRJz9CYwaHEh4m75LTYPDjVuKuw9sArBw,10438
|
|
62
|
+
transport/unity_transport.py,sha256=G6aMC1qR31YZOBZs4fxQbSQBHuXBP1d5Qn0MJaB3yGs,3908
|
|
63
|
+
transport/legacy/port_discovery.py,sha256=JDSCqXLodfTT7fOsE0DFC1jJ3QsU6hVaYQb7x7FgdxY,12728
|
|
64
|
+
transport/legacy/stdio_port_registry.py,sha256=j4iARuP6wetppNDG8qKeuvo1bJKcSlgEhZvSyl_uf0A,2313
|
|
65
|
+
transport/legacy/unity_connection.py,sha256=UZ6tztBO9VlXNiV0jN66k5QMrtTIGAOdGxwtcLnkXLU,35808
|
|
66
|
+
utils/module_discovery.py,sha256=My48ofB1BUqxiBoAZAGbEaLQYdsrDhMm8MayBP_bUSQ,2005
|
|
67
|
+
utils/reload_sentinel.py,sha256=s1xMWhl-r2XwN0OUbiUv_VGUy8TvLtV5bkql-5n2DT0,373
|
|
68
|
+
mcpforunityserver-9.0.1.dist-info/METADATA,sha256=-IGwo77e2WQGmCqFm3r3rXDfnJeiPoBZLm5az-5kmmc,5712
|
|
69
|
+
mcpforunityserver-9.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
70
|
+
mcpforunityserver-9.0.1.dist-info/entry_points.txt,sha256=vCtkqw-J9t4pj7JwUZcB54_keVx7DpAR3fYzK6i-s6g,44
|
|
71
|
+
mcpforunityserver-9.0.1.dist-info/top_level.txt,sha256=UYGWDnyTlnS7PnuZNw8-gM_jWcdmcHwffK_2yBRl6Cc,51
|
|
72
|
+
mcpforunityserver-9.0.1.dist-info/RECORD,,
|
services/custom_tool_service.py
CHANGED
|
@@ -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,51 +1,304 @@
|
|
|
1
|
-
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
2
5
|
from fastmcp import Context
|
|
6
|
+
from pydantic import BaseModel
|
|
3
7
|
|
|
4
8
|
from models import MCPResponse
|
|
5
9
|
from services.registry import mcp_for_unity_resource
|
|
6
10
|
from services.tools import get_unity_instance_from_context
|
|
7
|
-
from
|
|
11
|
+
from services.state.external_changes_scanner import external_changes_scanner
|
|
12
|
+
import transport.unity_transport as unity_transport
|
|
8
13
|
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
9
14
|
|
|
10
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
|
+
|
|
11
104
|
class EditorStateData(BaseModel):
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _now_unix_ms() -> int:
|
|
120
|
+
return int(time.time() * 1000)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _in_pytest() -> bool:
|
|
124
|
+
# Avoid instance-discovery side effects during the Python integration test suite.
|
|
125
|
+
return bool(os.environ.get("PYTEST_CURRENT_TEST"))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def infer_single_instance_id(ctx: Context) -> str | None:
|
|
129
|
+
"""
|
|
130
|
+
Best-effort: if exactly one Unity instance is connected, return its Name@hash id.
|
|
131
|
+
This makes editor_state outputs self-describing even when no explicit active instance is set.
|
|
132
|
+
"""
|
|
133
|
+
await ctx.info("If exactly one Unity instance is connected, return its Name@hash id.")
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
transport = unity_transport._current_transport()
|
|
137
|
+
except Exception:
|
|
138
|
+
transport = None
|
|
139
|
+
|
|
140
|
+
if transport == "http":
|
|
141
|
+
# HTTP/WebSocket transport: derive from PluginHub sessions.
|
|
142
|
+
try:
|
|
143
|
+
from transport.plugin_hub import PluginHub
|
|
144
|
+
|
|
145
|
+
sessions_data = await PluginHub.get_sessions()
|
|
146
|
+
sessions = sessions_data.sessions if hasattr(
|
|
147
|
+
sessions_data, "sessions") else {}
|
|
148
|
+
if isinstance(sessions, dict) and len(sessions) == 1:
|
|
149
|
+
session = next(iter(sessions.values()))
|
|
150
|
+
project = getattr(session, "project", None)
|
|
151
|
+
project_hash = getattr(session, "hash", None)
|
|
152
|
+
if project and project_hash:
|
|
153
|
+
return f"{project}@{project_hash}"
|
|
154
|
+
except Exception:
|
|
155
|
+
return None
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
# Stdio/TCP transport: derive from connection pool discovery.
|
|
159
|
+
try:
|
|
160
|
+
from transport.legacy.unity_connection import get_unity_connection_pool
|
|
161
|
+
|
|
162
|
+
pool = get_unity_connection_pool()
|
|
163
|
+
instances = pool.discover_all_instances(force_refresh=False)
|
|
164
|
+
if isinstance(instances, list) and len(instances) == 1:
|
|
165
|
+
inst = instances[0]
|
|
166
|
+
inst_id = getattr(inst, "id", None)
|
|
167
|
+
return str(inst_id) if inst_id else None
|
|
168
|
+
except Exception:
|
|
169
|
+
return None
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _enrich_advice_and_staleness(state_v2: dict[str, Any]) -> dict[str, Any]:
|
|
174
|
+
now_ms = _now_unix_ms()
|
|
175
|
+
observed = state_v2.get("observed_at_unix_ms")
|
|
176
|
+
try:
|
|
177
|
+
observed_ms = int(observed)
|
|
178
|
+
except Exception:
|
|
179
|
+
observed_ms = now_ms
|
|
21
180
|
|
|
181
|
+
age_ms = max(0, now_ms - observed_ms)
|
|
182
|
+
# Conservative default: treat >2s as stale (covers common unfocused-editor throttling).
|
|
183
|
+
is_stale = age_ms > 2000
|
|
22
184
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
185
|
+
compilation = state_v2.get("compilation") or {}
|
|
186
|
+
tests = state_v2.get("tests") or {}
|
|
187
|
+
assets = state_v2.get("assets") or {}
|
|
188
|
+
refresh = (assets.get("refresh") or {}) if isinstance(assets, dict) else {}
|
|
189
|
+
|
|
190
|
+
blocking: list[str] = []
|
|
191
|
+
if compilation.get("is_compiling") is True:
|
|
192
|
+
blocking.append("compiling")
|
|
193
|
+
if compilation.get("is_domain_reload_pending") is True:
|
|
194
|
+
blocking.append("domain_reload")
|
|
195
|
+
if tests.get("is_running") is True:
|
|
196
|
+
blocking.append("running_tests")
|
|
197
|
+
if refresh.get("is_refresh_in_progress") is True:
|
|
198
|
+
blocking.append("asset_refresh")
|
|
199
|
+
if is_stale:
|
|
200
|
+
blocking.append("stale_status")
|
|
201
|
+
|
|
202
|
+
ready_for_tools = len(blocking) == 0
|
|
203
|
+
|
|
204
|
+
state_v2["advice"] = {
|
|
205
|
+
"ready_for_tools": ready_for_tools,
|
|
206
|
+
"blocking_reasons": blocking,
|
|
207
|
+
"recommended_retry_after_ms": 0 if ready_for_tools else 500,
|
|
208
|
+
"recommended_next_action": "none" if ready_for_tools else "retry_later",
|
|
209
|
+
}
|
|
210
|
+
state_v2["staleness"] = {"age_ms": age_ms, "is_stale": is_stale}
|
|
211
|
+
return state_v2
|
|
26
212
|
|
|
27
213
|
|
|
28
214
|
@mcp_for_unity_resource(
|
|
29
|
-
uri="
|
|
215
|
+
uri="mcpforunity://editor/state",
|
|
30
216
|
name="editor_state",
|
|
31
|
-
description="
|
|
217
|
+
description="Canonical editor readiness snapshot. Includes advice and server-computed staleness.",
|
|
32
218
|
)
|
|
33
|
-
async def get_editor_state(ctx: Context) ->
|
|
34
|
-
"""Get current editor runtime state."""
|
|
219
|
+
async def get_editor_state(ctx: Context) -> MCPResponse:
|
|
35
220
|
unity_instance = get_unity_instance_from_context(ctx)
|
|
36
|
-
|
|
221
|
+
|
|
222
|
+
response = await unity_transport.send_with_unity_instance(
|
|
37
223
|
async_send_command_with_retry,
|
|
38
224
|
unity_instance,
|
|
39
225
|
"get_editor_state",
|
|
40
|
-
{}
|
|
226
|
+
{},
|
|
41
227
|
)
|
|
42
|
-
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
228
|
+
|
|
229
|
+
# If Unity returns a structured retry hint or error, surface it directly.
|
|
230
|
+
if isinstance(response, dict) and not response.get("success", True):
|
|
231
|
+
return MCPResponse(**response)
|
|
232
|
+
|
|
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)
|
|
238
|
+
|
|
239
|
+
# Ensure the returned snapshot is clearly associated with the targeted instance.
|
|
240
|
+
unity_section = state_v2.get("unity")
|
|
241
|
+
if not isinstance(unity_section, dict):
|
|
242
|
+
unity_section = {}
|
|
243
|
+
state_v2["unity"] = unity_section
|
|
244
|
+
current_instance_id = unity_section.get("instance_id")
|
|
245
|
+
if current_instance_id in (None, ""):
|
|
246
|
+
if unity_instance:
|
|
247
|
+
unity_section["instance_id"] = unity_instance
|
|
248
|
+
else:
|
|
249
|
+
inferred = await infer_single_instance_id(ctx)
|
|
250
|
+
if inferred:
|
|
251
|
+
unity_section["instance_id"] = inferred
|
|
252
|
+
|
|
253
|
+
# External change detection (server-side): compute per instance based on project root path.
|
|
254
|
+
try:
|
|
255
|
+
instance_id = unity_section.get("instance_id")
|
|
256
|
+
if isinstance(instance_id, str) and instance_id.strip():
|
|
257
|
+
from services.resources.project_info import get_project_info
|
|
258
|
+
|
|
259
|
+
proj_resp = await get_project_info(ctx)
|
|
260
|
+
proj = proj_resp.model_dump() if hasattr(
|
|
261
|
+
proj_resp, "model_dump") else proj_resp
|
|
262
|
+
proj_data = proj.get("data") if isinstance(proj, dict) else None
|
|
263
|
+
project_root = proj_data.get("projectRoot") if isinstance(
|
|
264
|
+
proj_data, dict) else None
|
|
265
|
+
if isinstance(project_root, str) and project_root.strip():
|
|
266
|
+
external_changes_scanner.set_project_root(
|
|
267
|
+
instance_id, project_root)
|
|
268
|
+
|
|
269
|
+
ext = external_changes_scanner.update_and_get(instance_id)
|
|
270
|
+
|
|
271
|
+
assets = state_v2.get("assets")
|
|
272
|
+
if not isinstance(assets, dict):
|
|
273
|
+
assets = {}
|
|
274
|
+
state_v2["assets"] = assets
|
|
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")
|
|
283
|
+
except Exception:
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
state_v2 = _enrich_advice_and_staleness(state_v2)
|
|
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
|
+
)
|
|
303
|
+
|
|
304
|
+
return MCPResponse(success=True, message="Retrieved editor state.", data=data)
|