Skip to content

Commit 2619644

Browse files
committed
Update mirror backend with latest code
1 parent 2649d9c commit 2619644

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3108
-253
lines changed

Server/port_discovery.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import logging
1717
import os
1818
import struct
19-
from datetime import datetime, timezone
19+
from datetime import datetime
2020
from pathlib import Path
2121
import socket
2222
from typing import Optional, List, Dict
@@ -238,7 +238,7 @@ def discover_all_unity_instances() -> List[UnityInstanceInfo]:
238238
for status_file_path in status_files:
239239
try:
240240
status_path = Path(status_file_path)
241-
file_mtime = datetime.fromtimestamp(status_path.stat().st_mtime, tz=timezone.utc)
241+
file_mtime = datetime.fromtimestamp(status_path.stat().st_mtime)
242242

243243
with status_path.open('r') as f:
244244
data = json.load(f)
@@ -258,12 +258,7 @@ def discover_all_unity_instances() -> List[UnityInstanceInfo]:
258258
heartbeat_str = data.get('last_heartbeat')
259259
if heartbeat_str:
260260
try:
261-
parsed = datetime.fromisoformat(heartbeat_str.replace('Z', '+00:00'))
262-
# Normalize to UTC for consistent comparison
263-
if parsed.tzinfo is None:
264-
last_heartbeat = parsed.replace(tzinfo=timezone.utc)
265-
else:
266-
last_heartbeat = parsed.astimezone(timezone.utc)
261+
last_heartbeat = datetime.fromisoformat(heartbeat_str.replace('Z', '+00:00'))
267262
except Exception:
268263
pass
269264

Server/pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ py-modules = [
3535
"server",
3636
"telemetry",
3737
"telemetry_decorator",
38-
"unity_connection"
38+
"unity_connection",
39+
"unity_instance_middleware"
3940
]
40-
packages = ["tools", "resources", "registry"]
41+
packages = ["tools", "resources", "registry"]

Server/resources/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ def register_all_resources(mcp: FastMCP):
4949
has_query_params = '{?' in uri
5050

5151
if has_query_params:
52-
# Register template with query parameter support
5352
wrapped_template = telemetry_resource(resource_name)(func)
5453
wrapped_template = mcp.resource(
5554
uri=uri,
@@ -61,7 +60,6 @@ def register_all_resources(mcp: FastMCP):
6160
registered_count += 1
6261
resource_info['func'] = wrapped_template
6362
else:
64-
# No query parameters, register as-is
6563
wrapped = telemetry_resource(resource_name)(func)
6664
wrapped = mcp.resource(
6765
uri=uri,

Server/resources/active_tool.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from pydantic import BaseModel
2+
from fastmcp import Context
3+
4+
from models import MCPResponse
5+
from registry import mcp_for_unity_resource
6+
from tools import get_unity_instance_from_context, async_send_with_unity_instance
7+
from unity_connection import async_send_command_with_retry
8+
9+
10+
class Vector3(BaseModel):
11+
"""3D vector."""
12+
x: float = 0.0
13+
y: float = 0.0
14+
z: float = 0.0
15+
16+
17+
class ActiveToolData(BaseModel):
18+
"""Active tool data fields."""
19+
activeTool: str = ""
20+
isCustom: bool = False
21+
pivotMode: str = ""
22+
pivotRotation: str = ""
23+
handleRotation: Vector3 = Vector3()
24+
handlePosition: Vector3 = Vector3()
25+
26+
27+
class ActiveToolResponse(MCPResponse):
28+
"""Information about the currently active editor tool."""
29+
data: ActiveToolData = ActiveToolData()
30+
31+
32+
@mcp_for_unity_resource(
33+
uri="unity://editor/active-tool",
34+
name="editor_active_tool",
35+
description="Currently active editor tool (Move, Rotate, Scale, etc.) and transform handle settings."
36+
)
37+
async def get_active_tool(ctx: Context) -> ActiveToolResponse | MCPResponse:
38+
"""Get active editor tool information."""
39+
unity_instance = get_unity_instance_from_context(ctx)
40+
response = await async_send_with_unity_instance(
41+
async_send_command_with_retry,
42+
unity_instance,
43+
"get_active_tool",
44+
{}
45+
)
46+
return ActiveToolResponse(**response) if isinstance(response, dict) else response

Server/resources/editor_state.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from pydantic import BaseModel
2+
from fastmcp import Context
3+
4+
from models import MCPResponse
5+
from registry import mcp_for_unity_resource
6+
from tools import get_unity_instance_from_context, async_send_with_unity_instance
7+
from unity_connection import async_send_command_with_retry
8+
9+
10+
class EditorStateData(BaseModel):
11+
"""Editor state data fields."""
12+
isPlaying: bool = False
13+
isPaused: bool = False
14+
isCompiling: bool = False
15+
isUpdating: bool = False
16+
timeSinceStartup: float = 0.0
17+
activeSceneName: str = ""
18+
selectionCount: int = 0
19+
activeObjectName: str | None = None
20+
21+
22+
class EditorStateResponse(MCPResponse):
23+
"""Dynamic editor state information that changes frequently."""
24+
data: EditorStateData = EditorStateData()
25+
26+
27+
@mcp_for_unity_resource(
28+
uri="unity://editor/state",
29+
name="editor_state",
30+
description="Current editor runtime state including play mode, compilation status, active scene, and selection summary. Refresh frequently for up-to-date information."
31+
)
32+
async def get_editor_state(ctx: Context) -> EditorStateResponse | MCPResponse:
33+
"""Get current editor runtime state."""
34+
unity_instance = get_unity_instance_from_context(ctx)
35+
response = await async_send_with_unity_instance(
36+
async_send_command_with_retry,
37+
unity_instance,
38+
"get_editor_state",
39+
{}
40+
)
41+
return EditorStateResponse(**response) if isinstance(response, dict) else response

Server/resources/layers.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from fastmcp import Context
2+
3+
from models import MCPResponse
4+
from registry import mcp_for_unity_resource
5+
from tools import get_unity_instance_from_context, async_send_with_unity_instance
6+
from unity_connection import async_send_command_with_retry
7+
8+
9+
class LayersResponse(MCPResponse):
10+
"""Dictionary of layer indices to layer names."""
11+
data: dict[int, str] = {}
12+
13+
14+
@mcp_for_unity_resource(
15+
uri="unity://project/layers",
16+
name="project_layers",
17+
description="All layers defined in the project's TagManager with their indices (0-31). Read this before using add_layer or remove_layer tools."
18+
)
19+
async def get_layers(ctx: Context) -> LayersResponse | MCPResponse:
20+
"""Get all project layers with their indices."""
21+
unity_instance = get_unity_instance_from_context(ctx)
22+
response = await async_send_with_unity_instance(
23+
async_send_command_with_retry,
24+
unity_instance,
25+
"get_layers",
26+
{}
27+
)
28+
return LayersResponse(**response) if isinstance(response, dict) else response

Server/resources/menu_items.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ class GetMenuItemsResponse(MCPResponse):
1212

1313
@mcp_for_unity_resource(
1414
uri="mcpforunity://menu-items",
15-
name="get_menu_items",
15+
name="menu_items",
1616
description="Provides a list of all menu items."
1717
)
18-
async def get_menu_items(ctx: Context) -> GetMenuItemsResponse:
18+
async def get_menu_items(ctx: Context) -> GetMenuItemsResponse | MCPResponse:
1919
"""Provides a list of all menu items.
2020
"""
2121
unity_instance = get_unity_instance_from_context(ctx)

Server/resources/prefab_stage.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from pydantic import BaseModel
2+
from fastmcp import Context
3+
4+
from models import MCPResponse
5+
from registry import mcp_for_unity_resource
6+
from tools import get_unity_instance_from_context, async_send_with_unity_instance
7+
from unity_connection import async_send_command_with_retry
8+
9+
10+
class PrefabStageData(BaseModel):
11+
"""Prefab stage data fields."""
12+
isOpen: bool = False
13+
assetPath: str | None = None
14+
prefabRootName: str | None = None
15+
mode: str | None = None
16+
isDirty: bool = False
17+
18+
19+
class PrefabStageResponse(MCPResponse):
20+
"""Information about the current prefab editing context."""
21+
data: PrefabStageData = PrefabStageData()
22+
23+
24+
@mcp_for_unity_resource(
25+
uri="unity://editor/prefab-stage",
26+
name="editor_prefab_stage",
27+
description="Current prefab editing context if a prefab is open in isolation mode. Returns isOpen=false if no prefab is being edited."
28+
)
29+
async def get_prefab_stage(ctx: Context) -> PrefabStageResponse | MCPResponse:
30+
"""Get current prefab stage information."""
31+
unity_instance = get_unity_instance_from_context(ctx)
32+
response = await async_send_with_unity_instance(
33+
async_send_command_with_retry,
34+
unity_instance,
35+
"get_prefab_stage",
36+
{}
37+
)
38+
return PrefabStageResponse(**response) if isinstance(response, dict) else response

Server/resources/project_info.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from pydantic import BaseModel
2+
from fastmcp import Context
3+
4+
from models import MCPResponse
5+
from registry import mcp_for_unity_resource
6+
from tools import get_unity_instance_from_context, async_send_with_unity_instance
7+
from unity_connection import async_send_command_with_retry
8+
9+
10+
class ProjectInfoData(BaseModel):
11+
"""Project info data fields."""
12+
projectRoot: str = ""
13+
projectName: str = ""
14+
unityVersion: str = ""
15+
platform: str = ""
16+
assetsPath: str = ""
17+
18+
19+
class ProjectInfoResponse(MCPResponse):
20+
"""Static project configuration information."""
21+
data: ProjectInfoData = ProjectInfoData()
22+
23+
24+
@mcp_for_unity_resource(
25+
uri="unity://project/info",
26+
name="project_info",
27+
description="Static project information including root path, Unity version, and platform. This data rarely changes."
28+
)
29+
async def get_project_info(ctx: Context) -> ProjectInfoResponse | MCPResponse:
30+
"""Get static project configuration information."""
31+
unity_instance = get_unity_instance_from_context(ctx)
32+
response = await async_send_with_unity_instance(
33+
async_send_command_with_retry,
34+
unity_instance,
35+
"get_project_info",
36+
{}
37+
)
38+
return ProjectInfoResponse(**response) if isinstance(response, dict) else response

Server/resources/selection.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from pydantic import BaseModel
2+
from fastmcp import Context
3+
4+
from models import MCPResponse
5+
from registry import mcp_for_unity_resource
6+
from tools import get_unity_instance_from_context, async_send_with_unity_instance
7+
from unity_connection import async_send_command_with_retry
8+
9+
10+
class SelectionObjectInfo(BaseModel):
11+
"""Information about a selected object."""
12+
name: str | None = None
13+
type: str | None = None
14+
instanceID: int | None = None
15+
16+
17+
class SelectionGameObjectInfo(BaseModel):
18+
"""Information about a selected GameObject."""
19+
name: str | None = None
20+
instanceID: int | None = None
21+
22+
23+
class SelectionData(BaseModel):
24+
"""Selection data fields."""
25+
activeObject: str | None = None
26+
activeGameObject: str | None = None
27+
activeTransform: str | None = None
28+
activeInstanceID: int = 0
29+
count: int = 0
30+
objects: list[SelectionObjectInfo] = []
31+
gameObjects: list[SelectionGameObjectInfo] = []
32+
assetGUIDs: list[str] = []
33+
34+
35+
class SelectionResponse(MCPResponse):
36+
"""Detailed information about the current editor selection."""
37+
data: SelectionData = SelectionData()
38+
39+
40+
@mcp_for_unity_resource(
41+
uri="unity://editor/selection",
42+
name="editor_selection",
43+
description="Detailed information about currently selected objects in the editor, including GameObjects, assets, and their properties."
44+
)
45+
async def get_selection(ctx: Context) -> SelectionResponse | MCPResponse:
46+
"""Get detailed editor selection information."""
47+
unity_instance = get_unity_instance_from_context(ctx)
48+
response = await async_send_with_unity_instance(
49+
async_send_command_with_retry,
50+
unity_instance,
51+
"get_selection",
52+
{}
53+
)
54+
return SelectionResponse(**response) if isinstance(response, dict) else response

0 commit comments

Comments
 (0)