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.
Files changed (65) hide show
  1. __init__.py +0 -0
  2. core/__init__.py +0 -0
  3. core/config.py +56 -0
  4. core/logging_decorator.py +37 -0
  5. core/telemetry.py +533 -0
  6. core/telemetry_decorator.py +164 -0
  7. main.py +411 -0
  8. mcpforunityserver-8.2.3.dist-info/METADATA +222 -0
  9. mcpforunityserver-8.2.3.dist-info/RECORD +65 -0
  10. mcpforunityserver-8.2.3.dist-info/WHEEL +5 -0
  11. mcpforunityserver-8.2.3.dist-info/entry_points.txt +2 -0
  12. mcpforunityserver-8.2.3.dist-info/licenses/LICENSE +21 -0
  13. mcpforunityserver-8.2.3.dist-info/top_level.txt +8 -0
  14. models/__init__.py +4 -0
  15. models/models.py +56 -0
  16. models/unity_response.py +47 -0
  17. routes/__init__.py +0 -0
  18. services/__init__.py +0 -0
  19. services/custom_tool_service.py +339 -0
  20. services/registry/__init__.py +22 -0
  21. services/registry/resource_registry.py +53 -0
  22. services/registry/tool_registry.py +51 -0
  23. services/resources/__init__.py +81 -0
  24. services/resources/active_tool.py +47 -0
  25. services/resources/custom_tools.py +57 -0
  26. services/resources/editor_state.py +42 -0
  27. services/resources/layers.py +29 -0
  28. services/resources/menu_items.py +34 -0
  29. services/resources/prefab_stage.py +39 -0
  30. services/resources/project_info.py +39 -0
  31. services/resources/selection.py +55 -0
  32. services/resources/tags.py +30 -0
  33. services/resources/tests.py +55 -0
  34. services/resources/unity_instances.py +122 -0
  35. services/resources/windows.py +47 -0
  36. services/tools/__init__.py +76 -0
  37. services/tools/batch_execute.py +78 -0
  38. services/tools/debug_request_context.py +71 -0
  39. services/tools/execute_custom_tool.py +38 -0
  40. services/tools/execute_menu_item.py +29 -0
  41. services/tools/find_in_file.py +174 -0
  42. services/tools/manage_asset.py +129 -0
  43. services/tools/manage_editor.py +63 -0
  44. services/tools/manage_gameobject.py +240 -0
  45. services/tools/manage_material.py +95 -0
  46. services/tools/manage_prefabs.py +62 -0
  47. services/tools/manage_scene.py +75 -0
  48. services/tools/manage_script.py +602 -0
  49. services/tools/manage_shader.py +64 -0
  50. services/tools/read_console.py +115 -0
  51. services/tools/run_tests.py +108 -0
  52. services/tools/script_apply_edits.py +998 -0
  53. services/tools/set_active_instance.py +112 -0
  54. services/tools/utils.py +60 -0
  55. transport/__init__.py +0 -0
  56. transport/legacy/port_discovery.py +329 -0
  57. transport/legacy/stdio_port_registry.py +65 -0
  58. transport/legacy/unity_connection.py +785 -0
  59. transport/models.py +62 -0
  60. transport/plugin_hub.py +412 -0
  61. transport/plugin_registry.py +123 -0
  62. transport/unity_instance_middleware.py +141 -0
  63. transport/unity_transport.py +103 -0
  64. utils/module_discovery.py +55 -0
  65. 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