mcpforunityserver 9.4.0b20260203025228__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.
Files changed (105) hide show
  1. cli/__init__.py +3 -0
  2. cli/commands/__init__.py +3 -0
  3. cli/commands/animation.py +84 -0
  4. cli/commands/asset.py +280 -0
  5. cli/commands/audio.py +125 -0
  6. cli/commands/batch.py +171 -0
  7. cli/commands/code.py +182 -0
  8. cli/commands/component.py +190 -0
  9. cli/commands/editor.py +447 -0
  10. cli/commands/gameobject.py +487 -0
  11. cli/commands/instance.py +93 -0
  12. cli/commands/lighting.py +123 -0
  13. cli/commands/material.py +239 -0
  14. cli/commands/prefab.py +248 -0
  15. cli/commands/scene.py +231 -0
  16. cli/commands/script.py +222 -0
  17. cli/commands/shader.py +226 -0
  18. cli/commands/texture.py +540 -0
  19. cli/commands/tool.py +58 -0
  20. cli/commands/ui.py +258 -0
  21. cli/commands/vfx.py +421 -0
  22. cli/main.py +281 -0
  23. cli/utils/__init__.py +31 -0
  24. cli/utils/config.py +58 -0
  25. cli/utils/confirmation.py +37 -0
  26. cli/utils/connection.py +254 -0
  27. cli/utils/constants.py +23 -0
  28. cli/utils/output.py +195 -0
  29. cli/utils/parsers.py +112 -0
  30. cli/utils/suggestions.py +34 -0
  31. core/__init__.py +0 -0
  32. core/config.py +67 -0
  33. core/constants.py +4 -0
  34. core/logging_decorator.py +37 -0
  35. core/telemetry.py +551 -0
  36. core/telemetry_decorator.py +164 -0
  37. main.py +845 -0
  38. mcpforunityserver-9.4.0b20260203025228.dist-info/METADATA +328 -0
  39. mcpforunityserver-9.4.0b20260203025228.dist-info/RECORD +105 -0
  40. mcpforunityserver-9.4.0b20260203025228.dist-info/WHEEL +5 -0
  41. mcpforunityserver-9.4.0b20260203025228.dist-info/entry_points.txt +3 -0
  42. mcpforunityserver-9.4.0b20260203025228.dist-info/licenses/LICENSE +21 -0
  43. mcpforunityserver-9.4.0b20260203025228.dist-info/top_level.txt +7 -0
  44. models/__init__.py +4 -0
  45. models/models.py +56 -0
  46. models/unity_response.py +70 -0
  47. services/__init__.py +0 -0
  48. services/api_key_service.py +235 -0
  49. services/custom_tool_service.py +499 -0
  50. services/registry/__init__.py +22 -0
  51. services/registry/resource_registry.py +53 -0
  52. services/registry/tool_registry.py +51 -0
  53. services/resources/__init__.py +86 -0
  54. services/resources/active_tool.py +48 -0
  55. services/resources/custom_tools.py +57 -0
  56. services/resources/editor_state.py +304 -0
  57. services/resources/gameobject.py +243 -0
  58. services/resources/layers.py +30 -0
  59. services/resources/menu_items.py +35 -0
  60. services/resources/prefab.py +191 -0
  61. services/resources/prefab_stage.py +40 -0
  62. services/resources/project_info.py +40 -0
  63. services/resources/selection.py +56 -0
  64. services/resources/tags.py +31 -0
  65. services/resources/tests.py +88 -0
  66. services/resources/unity_instances.py +125 -0
  67. services/resources/windows.py +48 -0
  68. services/state/external_changes_scanner.py +245 -0
  69. services/tools/__init__.py +83 -0
  70. services/tools/batch_execute.py +93 -0
  71. services/tools/debug_request_context.py +86 -0
  72. services/tools/execute_custom_tool.py +43 -0
  73. services/tools/execute_menu_item.py +32 -0
  74. services/tools/find_gameobjects.py +110 -0
  75. services/tools/find_in_file.py +181 -0
  76. services/tools/manage_asset.py +119 -0
  77. services/tools/manage_components.py +131 -0
  78. services/tools/manage_editor.py +64 -0
  79. services/tools/manage_gameobject.py +260 -0
  80. services/tools/manage_material.py +111 -0
  81. services/tools/manage_prefabs.py +209 -0
  82. services/tools/manage_scene.py +111 -0
  83. services/tools/manage_script.py +645 -0
  84. services/tools/manage_scriptable_object.py +87 -0
  85. services/tools/manage_shader.py +71 -0
  86. services/tools/manage_texture.py +581 -0
  87. services/tools/manage_vfx.py +120 -0
  88. services/tools/preflight.py +110 -0
  89. services/tools/read_console.py +151 -0
  90. services/tools/refresh_unity.py +153 -0
  91. services/tools/run_tests.py +317 -0
  92. services/tools/script_apply_edits.py +1006 -0
  93. services/tools/set_active_instance.py +120 -0
  94. services/tools/utils.py +348 -0
  95. transport/__init__.py +0 -0
  96. transport/legacy/port_discovery.py +329 -0
  97. transport/legacy/stdio_port_registry.py +65 -0
  98. transport/legacy/unity_connection.py +910 -0
  99. transport/models.py +68 -0
  100. transport/plugin_hub.py +787 -0
  101. transport/plugin_registry.py +182 -0
  102. transport/unity_instance_middleware.py +262 -0
  103. transport/unity_transport.py +94 -0
  104. utils/focus_nudge.py +589 -0
  105. utils/module_discovery.py +55 -0
@@ -0,0 +1,86 @@
1
+ """
2
+ MCP Resources package - Auto-discovers and registers all resources in this directory.
3
+ """
4
+ import inspect
5
+ import logging
6
+ from pathlib import Path
7
+
8
+ from fastmcp import FastMCP
9
+ from core.telemetry_decorator import telemetry_resource
10
+ from core.logging_decorator import log_execution
11
+
12
+ from services.registry import get_registered_resources
13
+ from utils.module_discovery import discover_modules
14
+
15
+ logger = logging.getLogger("mcp-for-unity-server")
16
+
17
+ # Export decorator for easy imports within tools
18
+ __all__ = ['register_all_resources']
19
+
20
+
21
+ def register_all_resources(mcp: FastMCP, *, project_scoped_tools: bool = True):
22
+ """
23
+ Auto-discover and register all resources in the resources/ directory.
24
+
25
+ Any .py file in this directory or subdirectories with @mcp_for_unity_resource decorated
26
+ functions will be automatically registered.
27
+ """
28
+ logger.info("Auto-discovering MCP for Unity Server resources...")
29
+ # Dynamic import of all modules in this directory
30
+ resources_dir = Path(__file__).parent
31
+
32
+ # Discover and import all modules
33
+ list(discover_modules(resources_dir, __package__))
34
+
35
+ resources = get_registered_resources()
36
+
37
+ if not resources:
38
+ logger.warning("No MCP resources registered!")
39
+ return
40
+
41
+ registered_count = 0
42
+ for resource_info in resources:
43
+ func = resource_info['func']
44
+ uri = resource_info['uri']
45
+ resource_name = resource_info['name']
46
+ description = resource_info['description']
47
+ kwargs = resource_info['kwargs']
48
+
49
+ if not project_scoped_tools and resource_name == "custom_tools":
50
+ logger.info(
51
+ "Skipping custom_tools resource registration (project-scoped tools disabled)")
52
+ continue
53
+
54
+ # Check if URI contains query parameters (e.g., {?unity_instance})
55
+ has_query_params = '{?' in uri
56
+
57
+ if has_query_params:
58
+ wrapped_template = log_execution(resource_name, "Resource")(func)
59
+ wrapped_template = telemetry_resource(
60
+ resource_name)(wrapped_template)
61
+ wrapped_template = mcp.resource(
62
+ uri=uri,
63
+ name=resource_name,
64
+ description=description,
65
+ **kwargs,
66
+ )(wrapped_template)
67
+ logger.debug(
68
+ f"Registered resource template: {resource_name} - {uri}")
69
+ registered_count += 1
70
+ resource_info['func'] = wrapped_template
71
+ else:
72
+ wrapped = log_execution(resource_name, "Resource")(func)
73
+ wrapped = telemetry_resource(resource_name)(wrapped)
74
+ wrapped = mcp.resource(
75
+ uri=uri,
76
+ name=resource_name,
77
+ description=description,
78
+ **kwargs,
79
+ )(wrapped)
80
+ resource_info['func'] = wrapped
81
+ logger.debug(
82
+ f"Registered resource: {resource_name} - {description}")
83
+ registered_count += 1
84
+
85
+ logger.info(
86
+ f"Registered {registered_count} MCP resources ({len(resources)} unique)")
@@ -0,0 +1,48 @@
1
+ from pydantic import BaseModel
2
+ from fastmcp import Context
3
+
4
+ from models import MCPResponse
5
+ from models.unity_response import parse_resource_response
6
+ from services.registry import mcp_for_unity_resource
7
+ from services.tools import get_unity_instance_from_context
8
+ from transport.unity_transport import send_with_unity_instance
9
+ from transport.legacy.unity_connection import async_send_command_with_retry
10
+
11
+
12
+ class Vector3(BaseModel):
13
+ """3D vector."""
14
+ x: float = 0.0
15
+ y: float = 0.0
16
+ z: float = 0.0
17
+
18
+
19
+ class ActiveToolData(BaseModel):
20
+ """Active tool data fields."""
21
+ activeTool: str = ""
22
+ isCustom: bool = False
23
+ pivotMode: str = ""
24
+ pivotRotation: str = ""
25
+ handleRotation: Vector3 = Vector3()
26
+ handlePosition: Vector3 = Vector3()
27
+
28
+
29
+ class ActiveToolResponse(MCPResponse):
30
+ """Information about the currently active editor tool."""
31
+ data: ActiveToolData = ActiveToolData()
32
+
33
+
34
+ @mcp_for_unity_resource(
35
+ uri="mcpforunity://editor/active-tool",
36
+ name="editor_active_tool",
37
+ description="Currently active editor tool (Move, Rotate, Scale, etc.) and transform handle settings.\n\nURI: mcpforunity://editor/active-tool"
38
+ )
39
+ async def get_active_tool(ctx: Context) -> ActiveToolResponse | MCPResponse:
40
+ """Get active editor tool information."""
41
+ unity_instance = get_unity_instance_from_context(ctx)
42
+ response = await send_with_unity_instance(
43
+ async_send_command_with_retry,
44
+ unity_instance,
45
+ "get_active_tool",
46
+ {}
47
+ )
48
+ return parse_resource_response(response, ActiveToolResponse)
@@ -0,0 +1,57 @@
1
+ from fastmcp import Context
2
+ from pydantic import BaseModel
3
+
4
+ from models import MCPResponse
5
+ from services.custom_tool_service import (
6
+ CustomToolService,
7
+ resolve_project_id_for_unity_instance,
8
+ ToolDefinitionModel,
9
+ )
10
+ from services.registry import mcp_for_unity_resource
11
+ from services.tools import get_unity_instance_from_context
12
+
13
+
14
+ class CustomToolsData(BaseModel):
15
+ project_id: str
16
+ tool_count: int
17
+ tools: list[ToolDefinitionModel]
18
+
19
+
20
+ class CustomToolsResourceResponse(MCPResponse):
21
+ data: CustomToolsData | None = None
22
+
23
+
24
+ @mcp_for_unity_resource(
25
+ uri="mcpforunity://custom-tools",
26
+ name="custom_tools",
27
+ description="Lists custom tools available for the active Unity project.\n\nURI: mcpforunity://custom-tools",
28
+ )
29
+ async def get_custom_tools(ctx: Context) -> CustomToolsResourceResponse | MCPResponse:
30
+ unity_instance = get_unity_instance_from_context(ctx)
31
+ if not unity_instance:
32
+ return MCPResponse(
33
+ success=False,
34
+ message="No active Unity instance. Call set_active_instance with Name@hash from mcpforunity://instances.",
35
+ )
36
+
37
+ project_id = resolve_project_id_for_unity_instance(unity_instance)
38
+ if project_id is None:
39
+ return MCPResponse(
40
+ success=False,
41
+ message=f"Could not resolve project id for {unity_instance}. Ensure Unity is running and reachable.",
42
+ )
43
+
44
+ service = CustomToolService.get_instance()
45
+ tools = await service.list_registered_tools(project_id)
46
+
47
+ data = CustomToolsData(
48
+ project_id=project_id,
49
+ tool_count=len(tools),
50
+ tools=tools,
51
+ )
52
+
53
+ return CustomToolsResourceResponse(
54
+ success=True,
55
+ message="Custom tools retrieved successfully.",
56
+ data=data,
57
+ )
@@ -0,0 +1,304 @@
1
+ import os
2
+ import time
3
+ from typing import Any
4
+
5
+ from fastmcp import Context
6
+ from pydantic import BaseModel
7
+
8
+ from core.config import config
9
+ from models import MCPResponse
10
+ from services.registry import mcp_for_unity_resource
11
+ from services.tools import get_unity_instance_from_context
12
+ from services.state.external_changes_scanner import external_changes_scanner
13
+ import transport.unity_transport as unity_transport
14
+ from transport.legacy.unity_connection import async_send_command_with_retry
15
+ from transport.plugin_hub import PluginHub
16
+
17
+
18
+ class EditorStateUnity(BaseModel):
19
+ instance_id: str | None = None
20
+ unity_version: str | None = None
21
+ project_id: str | None = None
22
+ platform: str | None = None
23
+ is_batch_mode: bool | None = None
24
+
25
+
26
+ class EditorStatePlayMode(BaseModel):
27
+ is_playing: bool | None = None
28
+ is_paused: bool | None = None
29
+ is_changing: bool | None = None
30
+
31
+
32
+ class EditorStateActiveScene(BaseModel):
33
+ path: str | None = None
34
+ guid: str | None = None
35
+ name: str | None = None
36
+
37
+
38
+ class EditorStateEditor(BaseModel):
39
+ is_focused: bool | None = None
40
+ play_mode: EditorStatePlayMode | None = None
41
+ active_scene: EditorStateActiveScene | None = None
42
+
43
+
44
+ class EditorStateActivity(BaseModel):
45
+ phase: str | None = None
46
+ since_unix_ms: int | None = None
47
+ reasons: list[str] | None = None
48
+
49
+
50
+ class EditorStateCompilation(BaseModel):
51
+ is_compiling: bool | None = None
52
+ is_domain_reload_pending: bool | None = None
53
+ last_compile_started_unix_ms: int | None = None
54
+ last_compile_finished_unix_ms: int | None = None
55
+ last_domain_reload_before_unix_ms: int | None = None
56
+ last_domain_reload_after_unix_ms: int | None = None
57
+
58
+
59
+ class EditorStateRefresh(BaseModel):
60
+ is_refresh_in_progress: bool | None = None
61
+ last_refresh_requested_unix_ms: int | None = None
62
+ last_refresh_finished_unix_ms: int | None = None
63
+
64
+
65
+ class EditorStateAssets(BaseModel):
66
+ is_updating: bool | None = None
67
+ external_changes_dirty: bool | None = None
68
+ external_changes_last_seen_unix_ms: int | None = None
69
+ external_changes_dirty_since_unix_ms: int | None = None
70
+ external_changes_last_cleared_unix_ms: int | None = None
71
+ refresh: EditorStateRefresh | None = None
72
+
73
+
74
+ class EditorStateLastRun(BaseModel):
75
+ finished_unix_ms: int | None = None
76
+ result: str | None = None
77
+ counts: Any | None = None
78
+
79
+
80
+ class EditorStateTests(BaseModel):
81
+ is_running: bool | None = None
82
+ mode: str | None = None
83
+ current_job_id: str | None = None
84
+ started_unix_ms: int | None = None
85
+ started_by: str | None = None
86
+ last_run: EditorStateLastRun | None = None
87
+
88
+
89
+ class EditorStateTransport(BaseModel):
90
+ unity_bridge_connected: bool | None = None
91
+ last_message_unix_ms: int | None = None
92
+
93
+
94
+ class EditorStateAdvice(BaseModel):
95
+ ready_for_tools: bool | None = None
96
+ blocking_reasons: list[str] | None = None
97
+ recommended_retry_after_ms: int | None = None
98
+ recommended_next_action: str | None = None
99
+
100
+
101
+ class EditorStateStaleness(BaseModel):
102
+ age_ms: int | None = None
103
+ is_stale: bool | None = None
104
+
105
+
106
+ class EditorStateData(BaseModel):
107
+ schema_version: str
108
+ observed_at_unix_ms: int
109
+ sequence: int
110
+ unity: EditorStateUnity | None = None
111
+ editor: EditorStateEditor | None = None
112
+ activity: EditorStateActivity | None = None
113
+ compilation: EditorStateCompilation | None = None
114
+ assets: EditorStateAssets | None = None
115
+ tests: EditorStateTests | None = None
116
+ transport: EditorStateTransport | None = None
117
+ advice: EditorStateAdvice | None = None
118
+ staleness: EditorStateStaleness | None = None
119
+
120
+
121
+ def _now_unix_ms() -> int:
122
+ return int(time.time() * 1000)
123
+
124
+
125
+ def _in_pytest() -> bool:
126
+ # Avoid instance-discovery side effects during the Python integration test suite.
127
+ return bool(os.environ.get("PYTEST_CURRENT_TEST"))
128
+
129
+
130
+ async def infer_single_instance_id(ctx: Context) -> str | None:
131
+ """
132
+ Best-effort: if exactly one Unity instance is connected, return its Name@hash id.
133
+ This makes editor_state outputs self-describing even when no explicit active instance is set.
134
+ """
135
+ await ctx.info("If exactly one Unity instance is connected, return its Name@hash id.")
136
+
137
+ transport = (config.transport_mode or "stdio").lower()
138
+
139
+ if transport == "http":
140
+ # HTTP/WebSocket transport: derive from PluginHub sessions.
141
+ try:
142
+ # In remote-hosted mode, filter sessions by user_id
143
+ user_id = ctx.get_state(
144
+ "user_id") if config.http_remote_hosted else None
145
+ sessions_data = await PluginHub.get_sessions(user_id=user_id)
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
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
184
+
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
212
+
213
+
214
+ @mcp_for_unity_resource(
215
+ uri="mcpforunity://editor/state",
216
+ name="editor_state",
217
+ description="Canonical editor readiness snapshot. Includes advice and server-computed staleness.\n\nURI: mcpforunity://editor/state",
218
+ )
219
+ async def get_editor_state(ctx: Context) -> MCPResponse:
220
+ unity_instance = get_unity_instance_from_context(ctx)
221
+
222
+ response = await unity_transport.send_with_unity_instance(
223
+ async_send_command_with_retry,
224
+ unity_instance,
225
+ "get_editor_state",
226
+ {},
227
+ )
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)