mcpforunityserver 8.2.3__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.
- __init__.py +0 -0
- core/__init__.py +0 -0
- core/config.py +56 -0
- core/logging_decorator.py +37 -0
- core/telemetry.py +533 -0
- core/telemetry_decorator.py +164 -0
- main.py +411 -0
- mcpforunityserver-8.2.3.dist-info/METADATA +222 -0
- mcpforunityserver-8.2.3.dist-info/RECORD +65 -0
- mcpforunityserver-8.2.3.dist-info/WHEEL +5 -0
- mcpforunityserver-8.2.3.dist-info/entry_points.txt +2 -0
- mcpforunityserver-8.2.3.dist-info/licenses/LICENSE +21 -0
- mcpforunityserver-8.2.3.dist-info/top_level.txt +8 -0
- models/__init__.py +4 -0
- models/models.py +56 -0
- models/unity_response.py +47 -0
- routes/__init__.py +0 -0
- services/__init__.py +0 -0
- services/custom_tool_service.py +339 -0
- services/registry/__init__.py +22 -0
- services/registry/resource_registry.py +53 -0
- services/registry/tool_registry.py +51 -0
- services/resources/__init__.py +81 -0
- services/resources/active_tool.py +47 -0
- services/resources/custom_tools.py +57 -0
- services/resources/editor_state.py +42 -0
- services/resources/layers.py +29 -0
- services/resources/menu_items.py +34 -0
- services/resources/prefab_stage.py +39 -0
- services/resources/project_info.py +39 -0
- services/resources/selection.py +55 -0
- services/resources/tags.py +30 -0
- services/resources/tests.py +55 -0
- services/resources/unity_instances.py +122 -0
- services/resources/windows.py +47 -0
- services/tools/__init__.py +76 -0
- services/tools/batch_execute.py +78 -0
- services/tools/debug_request_context.py +71 -0
- services/tools/execute_custom_tool.py +38 -0
- services/tools/execute_menu_item.py +29 -0
- services/tools/find_in_file.py +174 -0
- services/tools/manage_asset.py +129 -0
- services/tools/manage_editor.py +63 -0
- services/tools/manage_gameobject.py +240 -0
- services/tools/manage_material.py +95 -0
- services/tools/manage_prefabs.py +62 -0
- services/tools/manage_scene.py +75 -0
- services/tools/manage_script.py +602 -0
- services/tools/manage_shader.py +64 -0
- services/tools/read_console.py +115 -0
- services/tools/run_tests.py +108 -0
- services/tools/script_apply_edits.py +998 -0
- services/tools/set_active_instance.py +112 -0
- services/tools/utils.py +60 -0
- transport/__init__.py +0 -0
- transport/legacy/port_discovery.py +329 -0
- transport/legacy/stdio_port_registry.py +65 -0
- transport/legacy/unity_connection.py +785 -0
- transport/models.py +62 -0
- transport/plugin_hub.py +412 -0
- transport/plugin_registry.py +123 -0
- transport/unity_instance_middleware.py +141 -0
- transport/unity_transport.py +103 -0
- utils/module_discovery.py +55 -0
- utils/reload_sentinel.py +9 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Defines the read_console tool for accessing Unity Editor console messages.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Annotated, Any, Literal
|
|
5
|
+
|
|
6
|
+
from fastmcp import Context
|
|
7
|
+
from services.registry import mcp_for_unity_tool
|
|
8
|
+
from services.tools import get_unity_instance_from_context
|
|
9
|
+
from transport.unity_transport import send_with_unity_instance
|
|
10
|
+
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@mcp_for_unity_tool(
|
|
14
|
+
description="Gets messages from or clears the Unity Editor console. Note: For maximum client compatibility, pass count as a quoted string (e.g., '5')."
|
|
15
|
+
)
|
|
16
|
+
async def read_console(
|
|
17
|
+
ctx: Context,
|
|
18
|
+
action: Annotated[Literal['get', 'clear'],
|
|
19
|
+
"Get or clear the Unity Editor console. Defaults to 'get' if omitted."] | None = None,
|
|
20
|
+
types: Annotated[list[Literal['error', 'warning',
|
|
21
|
+
'log', 'all']], "Message types to get"] | None = None,
|
|
22
|
+
count: Annotated[int | str,
|
|
23
|
+
"Max messages to return (accepts int or string, e.g., 5 or '5')"] | None = None,
|
|
24
|
+
filter_text: Annotated[str, "Text filter for messages"] | None = None,
|
|
25
|
+
since_timestamp: Annotated[str,
|
|
26
|
+
"Get messages after this timestamp (ISO 8601)"] | None = None,
|
|
27
|
+
format: Annotated[Literal['plain', 'detailed',
|
|
28
|
+
'json'], "Output format"] | None = None,
|
|
29
|
+
include_stacktrace: Annotated[bool | str,
|
|
30
|
+
"Include stack traces in output (accepts true/false or 'true'/'false')"] | None = None,
|
|
31
|
+
) -> dict[str, Any]:
|
|
32
|
+
# Get active instance from session state
|
|
33
|
+
# Removed session_state import
|
|
34
|
+
unity_instance = get_unity_instance_from_context(ctx)
|
|
35
|
+
# Set defaults if values are None
|
|
36
|
+
action = action if action is not None else 'get'
|
|
37
|
+
types = types if types is not None else ['error', 'warning', 'log']
|
|
38
|
+
format = format if format is not None else 'detailed'
|
|
39
|
+
# Coerce booleans defensively (strings like 'true'/'false')
|
|
40
|
+
|
|
41
|
+
def _coerce_bool(value, default=None):
|
|
42
|
+
if value is None:
|
|
43
|
+
return default
|
|
44
|
+
if isinstance(value, bool):
|
|
45
|
+
return value
|
|
46
|
+
if isinstance(value, str):
|
|
47
|
+
v = value.strip().lower()
|
|
48
|
+
if v in ("true", "1", "yes", "on"):
|
|
49
|
+
return True
|
|
50
|
+
if v in ("false", "0", "no", "off"):
|
|
51
|
+
return False
|
|
52
|
+
return bool(value)
|
|
53
|
+
|
|
54
|
+
include_stacktrace = _coerce_bool(include_stacktrace, True)
|
|
55
|
+
|
|
56
|
+
# Normalize action if it's a string
|
|
57
|
+
if isinstance(action, str):
|
|
58
|
+
action = action.lower()
|
|
59
|
+
|
|
60
|
+
# Coerce count defensively (string/float -> int)
|
|
61
|
+
def _coerce_int(value, default=None):
|
|
62
|
+
if value is None:
|
|
63
|
+
return default
|
|
64
|
+
try:
|
|
65
|
+
if isinstance(value, bool):
|
|
66
|
+
return default
|
|
67
|
+
if isinstance(value, int):
|
|
68
|
+
return int(value)
|
|
69
|
+
s = str(value).strip()
|
|
70
|
+
if s.lower() in ("", "none", "null"):
|
|
71
|
+
return default
|
|
72
|
+
return int(float(s))
|
|
73
|
+
except Exception:
|
|
74
|
+
return default
|
|
75
|
+
|
|
76
|
+
count = _coerce_int(count)
|
|
77
|
+
|
|
78
|
+
# Prepare parameters for the C# handler
|
|
79
|
+
params_dict = {
|
|
80
|
+
"action": action,
|
|
81
|
+
"types": types,
|
|
82
|
+
"count": count,
|
|
83
|
+
"filterText": filter_text,
|
|
84
|
+
"sinceTimestamp": since_timestamp,
|
|
85
|
+
"format": format.lower() if isinstance(format, str) else format,
|
|
86
|
+
"includeStacktrace": include_stacktrace
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Remove None values unless it's 'count' (as None might mean 'all')
|
|
90
|
+
params_dict = {k: v for k, v in params_dict.items()
|
|
91
|
+
if v is not None or k == 'count'}
|
|
92
|
+
|
|
93
|
+
# Add count back if it was None, explicitly sending null might be important for C# logic
|
|
94
|
+
if 'count' not in params_dict:
|
|
95
|
+
params_dict['count'] = None
|
|
96
|
+
|
|
97
|
+
# Use centralized retry helper with instance routing
|
|
98
|
+
resp = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "read_console", params_dict)
|
|
99
|
+
if isinstance(resp, dict) and resp.get("success") and not include_stacktrace:
|
|
100
|
+
# Strip stacktrace fields from returned lines if present
|
|
101
|
+
try:
|
|
102
|
+
data = resp.get("data")
|
|
103
|
+
# Handle standard format: {"data": {"lines": [...]}}
|
|
104
|
+
if isinstance(data, dict) and "lines" in data and isinstance(data["lines"], list):
|
|
105
|
+
for line in data["lines"]:
|
|
106
|
+
if isinstance(line, dict) and "stacktrace" in line:
|
|
107
|
+
line.pop("stacktrace", None)
|
|
108
|
+
# Handle legacy/direct list format if any
|
|
109
|
+
elif isinstance(data, list):
|
|
110
|
+
for line in data:
|
|
111
|
+
if isinstance(line, dict) and "stacktrace" in line:
|
|
112
|
+
line.pop("stacktrace", None)
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Tool for executing Unity Test Runner suites."""
|
|
2
|
+
from typing import Annotated, Literal, Any
|
|
3
|
+
|
|
4
|
+
from fastmcp import Context
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from models import MCPResponse
|
|
8
|
+
from services.registry import mcp_for_unity_tool
|
|
9
|
+
from services.tools import get_unity_instance_from_context
|
|
10
|
+
from transport.unity_transport import send_with_unity_instance
|
|
11
|
+
from transport.legacy.unity_connection import async_send_command_with_retry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RunTestsSummary(BaseModel):
|
|
15
|
+
total: int
|
|
16
|
+
passed: int
|
|
17
|
+
failed: int
|
|
18
|
+
skipped: int
|
|
19
|
+
durationSeconds: float
|
|
20
|
+
resultState: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RunTestsTestResult(BaseModel):
|
|
24
|
+
name: str
|
|
25
|
+
fullName: str
|
|
26
|
+
state: str
|
|
27
|
+
durationSeconds: float
|
|
28
|
+
message: str | None = None
|
|
29
|
+
stackTrace: str | None = None
|
|
30
|
+
output: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RunTestsResult(BaseModel):
|
|
34
|
+
mode: str
|
|
35
|
+
summary: RunTestsSummary
|
|
36
|
+
results: list[RunTestsTestResult]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RunTestsResponse(MCPResponse):
|
|
40
|
+
data: RunTestsResult | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@mcp_for_unity_tool(
|
|
44
|
+
description="Runs Unity tests for the specified mode"
|
|
45
|
+
)
|
|
46
|
+
async def run_tests(
|
|
47
|
+
ctx: Context,
|
|
48
|
+
mode: Annotated[Literal["EditMode", "PlayMode"], "Unity test mode to run"] = "EditMode",
|
|
49
|
+
timeout_seconds: Annotated[int | str, "Optional timeout in seconds for the test run"] | None = None,
|
|
50
|
+
test_names: Annotated[list[str] | str, "Full names of specific tests to run (e.g., 'MyNamespace.MyTests.TestMethod')"] | None = None,
|
|
51
|
+
group_names: Annotated[list[str] | str, "Same as test_names, except it allows for Regex"] | None = None,
|
|
52
|
+
category_names: Annotated[list[str] | str, "NUnit category names to filter by (tests marked with [Category] attribute)"] | None = None,
|
|
53
|
+
assembly_names: Annotated[list[str] | str, "Assembly names to filter tests by"] | None = None,
|
|
54
|
+
) -> RunTestsResponse:
|
|
55
|
+
unity_instance = get_unity_instance_from_context(ctx)
|
|
56
|
+
|
|
57
|
+
# Coerce timeout defensively (string/float -> int)
|
|
58
|
+
def _coerce_int(value, default=None):
|
|
59
|
+
if value is None:
|
|
60
|
+
return default
|
|
61
|
+
try:
|
|
62
|
+
if isinstance(value, bool):
|
|
63
|
+
return default
|
|
64
|
+
if isinstance(value, int):
|
|
65
|
+
return int(value)
|
|
66
|
+
s = str(value).strip()
|
|
67
|
+
if s.lower() in ("", "none", "null"):
|
|
68
|
+
return default
|
|
69
|
+
return int(float(s))
|
|
70
|
+
except Exception:
|
|
71
|
+
return default
|
|
72
|
+
|
|
73
|
+
# Coerce string or list to list of strings
|
|
74
|
+
def _coerce_string_list(value) -> list[str] | None:
|
|
75
|
+
if value is None:
|
|
76
|
+
return None
|
|
77
|
+
if isinstance(value, str):
|
|
78
|
+
return [value] if value.strip() else None
|
|
79
|
+
if isinstance(value, list):
|
|
80
|
+
result = [str(v).strip() for v in value if v and str(v).strip()]
|
|
81
|
+
return result if result else None
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
params: dict[str, Any] = {"mode": mode}
|
|
85
|
+
ts = _coerce_int(timeout_seconds)
|
|
86
|
+
if ts is not None:
|
|
87
|
+
params["timeoutSeconds"] = ts
|
|
88
|
+
|
|
89
|
+
# Add filter parameters if provided
|
|
90
|
+
test_names_list = _coerce_string_list(test_names)
|
|
91
|
+
if test_names_list:
|
|
92
|
+
params["testNames"] = test_names_list
|
|
93
|
+
|
|
94
|
+
group_names_list = _coerce_string_list(group_names)
|
|
95
|
+
if group_names_list:
|
|
96
|
+
params["groupNames"] = group_names_list
|
|
97
|
+
|
|
98
|
+
category_names_list = _coerce_string_list(category_names)
|
|
99
|
+
if category_names_list:
|
|
100
|
+
params["categoryNames"] = category_names_list
|
|
101
|
+
|
|
102
|
+
assembly_names_list = _coerce_string_list(assembly_names)
|
|
103
|
+
if assembly_names_list:
|
|
104
|
+
params["assemblyNames"] = assembly_names_list
|
|
105
|
+
|
|
106
|
+
response = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "run_tests", params)
|
|
107
|
+
await ctx.info(f'Response {response}')
|
|
108
|
+
return RunTestsResponse(**response) if isinstance(response, dict) else response
|