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,164 @@
1
+ """
2
+ Telemetry decorator for MCP for Unity tools
3
+ """
4
+
5
+ import functools
6
+ import inspect
7
+ import logging
8
+ import time
9
+ from typing import Callable, Any
10
+
11
+ from core.telemetry import record_resource_usage, record_tool_usage, record_milestone, MilestoneType
12
+
13
+ _log = logging.getLogger("unity-mcp-telemetry")
14
+ _decorator_log_count = 0
15
+
16
+
17
+ def telemetry_tool(tool_name: str):
18
+ """Decorator to add telemetry tracking to MCP tools"""
19
+ def decorator(func: Callable) -> Callable:
20
+ @functools.wraps(func)
21
+ def _sync_wrapper(*args, **kwargs) -> Any:
22
+ start_time = time.time()
23
+ success = False
24
+ error = None
25
+ # Extract sub-action (e.g., 'get_hierarchy') from bound args when available
26
+ sub_action = None
27
+ try:
28
+ sig = inspect.signature(func)
29
+ bound = sig.bind_partial(*args, **kwargs)
30
+ bound.apply_defaults()
31
+ sub_action = bound.arguments.get("action")
32
+ except Exception:
33
+ sub_action = None
34
+ try:
35
+ global _decorator_log_count
36
+ if _decorator_log_count < 10:
37
+ _log.info(f"telemetry_decorator sync: tool={tool_name}")
38
+ _decorator_log_count += 1
39
+ result = func(*args, **kwargs)
40
+ success = True
41
+ action_val = sub_action or kwargs.get("action")
42
+ try:
43
+ if tool_name == "manage_script" and action_val == "create":
44
+ record_milestone(MilestoneType.FIRST_SCRIPT_CREATION)
45
+ elif tool_name.startswith("manage_scene"):
46
+ record_milestone(
47
+ MilestoneType.FIRST_SCENE_MODIFICATION)
48
+ record_milestone(MilestoneType.FIRST_TOOL_USAGE)
49
+ except Exception:
50
+ _log.debug("milestone emit failed", exc_info=True)
51
+ return result
52
+ except Exception as e:
53
+ error = str(e)
54
+ raise
55
+ finally:
56
+ duration_ms = (time.time() - start_time) * 1000
57
+ try:
58
+ record_tool_usage(tool_name, success,
59
+ duration_ms, error, sub_action=sub_action)
60
+ except Exception:
61
+ _log.debug("record_tool_usage failed", exc_info=True)
62
+
63
+ @functools.wraps(func)
64
+ async def _async_wrapper(*args, **kwargs) -> Any:
65
+ start_time = time.time()
66
+ success = False
67
+ error = None
68
+ # Extract sub-action (e.g., 'get_hierarchy') from bound args when available
69
+ sub_action = None
70
+ try:
71
+ sig = inspect.signature(func)
72
+ bound = sig.bind_partial(*args, **kwargs)
73
+ bound.apply_defaults()
74
+ sub_action = bound.arguments.get("action")
75
+ except Exception:
76
+ sub_action = None
77
+ try:
78
+ global _decorator_log_count
79
+ if _decorator_log_count < 10:
80
+ _log.info(f"telemetry_decorator async: tool={tool_name}")
81
+ _decorator_log_count += 1
82
+ result = await func(*args, **kwargs)
83
+ success = True
84
+ action_val = sub_action or kwargs.get("action")
85
+ try:
86
+ if tool_name == "manage_script" and action_val == "create":
87
+ record_milestone(MilestoneType.FIRST_SCRIPT_CREATION)
88
+ elif tool_name.startswith("manage_scene"):
89
+ record_milestone(
90
+ MilestoneType.FIRST_SCENE_MODIFICATION)
91
+ record_milestone(MilestoneType.FIRST_TOOL_USAGE)
92
+ except Exception:
93
+ _log.debug("milestone emit failed", exc_info=True)
94
+ return result
95
+ except Exception as e:
96
+ error = str(e)
97
+ raise
98
+ finally:
99
+ duration_ms = (time.time() - start_time) * 1000
100
+ try:
101
+ record_tool_usage(tool_name, success,
102
+ duration_ms, error, sub_action=sub_action)
103
+ except Exception:
104
+ _log.debug("record_tool_usage failed", exc_info=True)
105
+
106
+ return _async_wrapper if inspect.iscoroutinefunction(func) else _sync_wrapper
107
+ return decorator
108
+
109
+
110
+ def telemetry_resource(resource_name: str):
111
+ """Decorator to add telemetry tracking to MCP resources"""
112
+ def decorator(func: Callable) -> Callable:
113
+ @functools.wraps(func)
114
+ def _sync_wrapper(*args, **kwargs) -> Any:
115
+ start_time = time.time()
116
+ success = False
117
+ error = None
118
+ try:
119
+ global _decorator_log_count
120
+ if _decorator_log_count < 10:
121
+ _log.info(
122
+ f"telemetry_decorator sync: resource={resource_name}")
123
+ _decorator_log_count += 1
124
+ result = func(*args, **kwargs)
125
+ success = True
126
+ return result
127
+ except Exception as e:
128
+ error = str(e)
129
+ raise
130
+ finally:
131
+ duration_ms = (time.time() - start_time) * 1000
132
+ try:
133
+ record_resource_usage(resource_name, success,
134
+ duration_ms, error)
135
+ except Exception:
136
+ _log.debug("record_resource_usage failed", exc_info=True)
137
+
138
+ @functools.wraps(func)
139
+ async def _async_wrapper(*args, **kwargs) -> Any:
140
+ start_time = time.time()
141
+ success = False
142
+ error = None
143
+ try:
144
+ global _decorator_log_count
145
+ if _decorator_log_count < 10:
146
+ _log.info(
147
+ f"telemetry_decorator async: resource={resource_name}")
148
+ _decorator_log_count += 1
149
+ result = await func(*args, **kwargs)
150
+ success = True
151
+ return result
152
+ except Exception as e:
153
+ error = str(e)
154
+ raise
155
+ finally:
156
+ duration_ms = (time.time() - start_time) * 1000
157
+ try:
158
+ record_resource_usage(resource_name, success,
159
+ duration_ms, error)
160
+ except Exception:
161
+ _log.debug("record_resource_usage failed", exc_info=True)
162
+
163
+ return _async_wrapper if inspect.iscoroutinefunction(func) else _sync_wrapper
164
+ return decorator
main.py ADDED
@@ -0,0 +1,411 @@
1
+ import argparse
2
+ import asyncio
3
+ import logging
4
+ from contextlib import asynccontextmanager
5
+ import os
6
+ import threading
7
+ import time
8
+ from typing import AsyncIterator, Any
9
+ from urllib.parse import urlparse
10
+
11
+ from fastmcp import FastMCP
12
+ from logging.handlers import RotatingFileHandler
13
+ from starlette.requests import Request
14
+ from starlette.responses import JSONResponse
15
+ from starlette.routing import WebSocketRoute
16
+
17
+ from core.config import config
18
+ from services.custom_tool_service import CustomToolService
19
+ from transport.plugin_hub import PluginHub
20
+ from transport.plugin_registry import PluginRegistry
21
+ from services.resources import register_all_resources
22
+ from core.telemetry import record_milestone, record_telemetry, MilestoneType, RecordType, get_package_version
23
+ from services.tools import register_all_tools
24
+ from transport.legacy.unity_connection import get_unity_connection_pool, UnityConnectionPool
25
+ from transport.unity_instance_middleware import (
26
+ UnityInstanceMiddleware,
27
+ get_unity_instance_middleware
28
+ )
29
+
30
+ # Configure logging using settings from config
31
+ logging.basicConfig(
32
+ level=getattr(logging, config.log_level),
33
+ format=config.log_format,
34
+ stream=None, # None -> defaults to sys.stderr; avoid stdout used by MCP stdio
35
+ force=True # Ensure our handler replaces any prior stdout handlers
36
+ )
37
+ logger = logging.getLogger("mcp-for-unity-server")
38
+
39
+ # Also write logs to a rotating file so logs are available when launched via stdio
40
+ try:
41
+ _log_dir = os.path.join(os.path.expanduser(
42
+ "~/Library/Application Support/UnityMCP"), "Logs")
43
+ os.makedirs(_log_dir, exist_ok=True)
44
+ _file_path = os.path.join(_log_dir, "unity_mcp_server.log")
45
+ _fh = RotatingFileHandler(
46
+ _file_path, maxBytes=512*1024, backupCount=2, encoding="utf-8")
47
+ _fh.setFormatter(logging.Formatter(config.log_format))
48
+ _fh.setLevel(getattr(logging, config.log_level))
49
+ logger.addHandler(_fh)
50
+ logger.propagate = False # Prevent double logging to root logger
51
+ # Also route telemetry logger to the same rotating file and normal level
52
+ try:
53
+ tlog = logging.getLogger("unity-mcp-telemetry")
54
+ tlog.setLevel(getattr(logging, config.log_level))
55
+ tlog.addHandler(_fh)
56
+ tlog.propagate = False # Prevent double logging for telemetry too
57
+ except Exception as exc:
58
+ # Never let logging setup break startup
59
+ logger.debug("Failed to configure telemetry logger", exc_info=exc)
60
+ except Exception as exc:
61
+ # Never let logging setup break startup
62
+ logger.debug("Failed to configure main logger file handler", exc_info=exc)
63
+ # Quieten noisy third-party loggers to avoid clutter during stdio handshake
64
+ for noisy in ("httpx", "urllib3", "mcp.server.lowlevel.server"):
65
+ try:
66
+ logging.getLogger(noisy).setLevel(
67
+ max(logging.WARNING, getattr(logging, config.log_level)))
68
+ logging.getLogger(noisy).propagate = False
69
+ except Exception:
70
+ pass
71
+
72
+ # Import telemetry only after logging is configured to ensure its logs use stderr and proper levels
73
+ # Ensure a slightly higher telemetry timeout unless explicitly overridden by env
74
+ try:
75
+
76
+ # Ensure generous timeout unless explicitly overridden by env
77
+ if not os.environ.get("UNITY_MCP_TELEMETRY_TIMEOUT"):
78
+ os.environ["UNITY_MCP_TELEMETRY_TIMEOUT"] = "5.0"
79
+ except Exception:
80
+ pass
81
+
82
+ # Global connection pool
83
+ _unity_connection_pool: UnityConnectionPool | None = None
84
+ _plugin_registry: PluginRegistry | None = None
85
+
86
+ # In-memory custom tool service initialized after MCP construction
87
+ custom_tool_service: CustomToolService | None = None
88
+
89
+
90
+ @asynccontextmanager
91
+ async def server_lifespan(server: FastMCP) -> AsyncIterator[dict[str, Any]]:
92
+ """Handle server startup and shutdown."""
93
+ global _unity_connection_pool
94
+ logger.info("MCP for Unity Server starting up")
95
+
96
+ # Register custom tool management endpoints with FastMCP
97
+ # Routes are declared globally below after FastMCP initialization
98
+
99
+ # Note: When using HTTP transport, FastMCP handles the HTTP server
100
+ # Tool registration will be handled through FastMCP endpoints
101
+ enable_http_server = os.environ.get(
102
+ "UNITY_MCP_ENABLE_HTTP_SERVER", "").lower() in ("1", "true", "yes", "on")
103
+ if enable_http_server:
104
+ http_host = os.environ.get("UNITY_MCP_HTTP_HOST", "localhost")
105
+ http_port = int(os.environ.get("UNITY_MCP_HTTP_PORT", "8080"))
106
+ logger.info(
107
+ f"HTTP tool registry will be available on http://{http_host}:{http_port}")
108
+
109
+ global _plugin_registry
110
+ if _plugin_registry is None:
111
+ _plugin_registry = PluginRegistry()
112
+ loop = asyncio.get_running_loop()
113
+ PluginHub.configure(_plugin_registry, loop)
114
+
115
+ # Record server startup telemetry
116
+ start_time = time.time()
117
+ start_clk = time.perf_counter()
118
+ server_version = get_package_version()
119
+ # Defer initial telemetry by 1s to avoid stdio handshake interference
120
+
121
+ def _emit_startup():
122
+ try:
123
+ record_telemetry(RecordType.STARTUP, {
124
+ "server_version": server_version,
125
+ "startup_time": start_time,
126
+ })
127
+ record_milestone(MilestoneType.FIRST_STARTUP)
128
+ except Exception:
129
+ logger.debug("Deferred startup telemetry failed", exc_info=True)
130
+ threading.Timer(1.0, _emit_startup).start()
131
+
132
+ try:
133
+ skip_connect = os.environ.get(
134
+ "UNITY_MCP_SKIP_STARTUP_CONNECT", "").lower() in ("1", "true", "yes", "on")
135
+ if skip_connect:
136
+ logger.info(
137
+ "Skipping Unity connection on startup (UNITY_MCP_SKIP_STARTUP_CONNECT=1)")
138
+ else:
139
+ # Initialize connection pool and discover instances
140
+ _unity_connection_pool = get_unity_connection_pool()
141
+ instances = _unity_connection_pool.discover_all_instances()
142
+
143
+ if instances:
144
+ logger.info(
145
+ f"Discovered {len(instances)} Unity instance(s): {[i.id for i in instances]}")
146
+
147
+ # Try to connect to default instance
148
+ try:
149
+ _unity_connection_pool.get_connection()
150
+ logger.info(
151
+ "Connected to default Unity instance on startup")
152
+
153
+ # Record successful Unity connection (deferred)
154
+ threading.Timer(1.0, lambda: record_telemetry(
155
+ RecordType.UNITY_CONNECTION,
156
+ {
157
+ "status": "connected",
158
+ "connection_time_ms": (time.perf_counter() - start_clk) * 1000,
159
+ "instance_count": len(instances)
160
+ }
161
+ )).start()
162
+ except Exception as e:
163
+ logger.warning(
164
+ f"Could not connect to default Unity instance: {e}")
165
+ else:
166
+ logger.warning("No Unity instances found on startup")
167
+
168
+ except ConnectionError as e:
169
+ logger.warning(f"Could not connect to Unity on startup: {e}")
170
+
171
+ # Record connection failure (deferred)
172
+ _err_msg = str(e)[:200]
173
+ threading.Timer(1.0, lambda: record_telemetry(
174
+ RecordType.UNITY_CONNECTION,
175
+ {
176
+ "status": "failed",
177
+ "error": _err_msg,
178
+ "connection_time_ms": (time.perf_counter() - start_clk) * 1000,
179
+ }
180
+ )).start()
181
+ except Exception as e:
182
+ logger.warning(f"Unexpected error connecting to Unity on startup: {e}")
183
+ _err_msg = str(e)[:200]
184
+ threading.Timer(1.0, lambda: record_telemetry(
185
+ RecordType.UNITY_CONNECTION,
186
+ {
187
+ "status": "failed",
188
+ "error": _err_msg,
189
+ "connection_time_ms": (time.perf_counter() - start_clk) * 1000,
190
+ }
191
+ )).start()
192
+
193
+ try:
194
+ # Yield shared state for lifespan consumers (e.g., middleware)
195
+ yield {
196
+ "pool": _unity_connection_pool,
197
+ "plugin_registry": _plugin_registry,
198
+ }
199
+ finally:
200
+ if _unity_connection_pool:
201
+ _unity_connection_pool.disconnect_all()
202
+ logger.info("MCP for Unity Server shut down")
203
+
204
+ # Initialize MCP server
205
+ mcp = FastMCP(
206
+ name="mcp-for-unity-server",
207
+ lifespan=server_lifespan,
208
+ instructions="""
209
+ This server provides tools to interact with the Unity Game Engine Editor.
210
+
211
+ I have a dynamic tool system. Always check the unity://custom-tools resource first to see what special capabilities are available for the current project.
212
+
213
+ Targeting Unity instances:
214
+ - Use the resource unity://instances to list active Unity sessions (Name@hash).
215
+ - 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.
216
+
217
+ Important Workflows:
218
+
219
+ Resources vs Tools:
220
+ - Use RESOURCES to read editor state (editor_state, project_info, project_tags, tests, etc)
221
+ - Use TOOLS to perform actions and mutations (manage_editor for play mode control, tag/layer management, etc)
222
+ - Always check related resources before modifying the engine state with tools
223
+
224
+ Script Management:
225
+ - After creating or modifying scripts (by your own tools or the `manage_script` tool) use `read_console` to check for compilation errors before proceeding
226
+ - Only after successful compilation can new components/types be used
227
+ - You can poll the `editor_state` resource's `isCompiling` field to check if the domain reload is complete
228
+
229
+ Scene Setup:
230
+ - Always include a Camera and main Light (Directional Light) in new scenes
231
+ - Create prefabs with `manage_asset` for reusable GameObjects
232
+ - Use `manage_scene` to load, save, and query scene information
233
+
234
+ Path Conventions:
235
+ - Unless specified otherwise, all paths are relative to the project's `Assets/` folder
236
+ - Use forward slashes (/) in paths for cross-platform compatibility
237
+
238
+ Console Monitoring:
239
+ - Check `read_console` regularly to catch errors, warnings, and compilation status
240
+ - Filter by log type (Error, Warning, Log) to focus on specific issues
241
+
242
+ Menu Items:
243
+ - Use `execute_menu_item` when you have read the menu items resource
244
+ - This lets you interact with Unity's menu system and third-party tools
245
+ """
246
+ )
247
+
248
+ custom_tool_service = CustomToolService(mcp)
249
+
250
+
251
+ @mcp.custom_route("/health", methods=["GET"])
252
+ async def health_http(_: Request) -> JSONResponse:
253
+ return JSONResponse({
254
+ "status": "healthy",
255
+ "timestamp": time.time(),
256
+ "message": "MCP for Unity server is running"
257
+ })
258
+
259
+
260
+ @mcp.custom_route("/plugin/sessions", methods=["GET"])
261
+ async def plugin_sessions_route(_: Request) -> JSONResponse:
262
+ data = await PluginHub.get_sessions()
263
+ return JSONResponse(data.model_dump())
264
+
265
+
266
+ # Initialize and register middleware for session-based Unity instance routing
267
+ # Using the singleton getter ensures we use the same instance everywhere
268
+ unity_middleware = get_unity_instance_middleware()
269
+ mcp.add_middleware(unity_middleware)
270
+ logger.info("Registered Unity instance middleware for session-based routing")
271
+
272
+ # Mount plugin websocket hub at /hub/plugin when HTTP transport is active
273
+ existing_routes = [
274
+ route for route in mcp._get_additional_http_routes()
275
+ if isinstance(route, WebSocketRoute) and route.path == "/hub/plugin"
276
+ ]
277
+ if not existing_routes:
278
+ mcp._additional_http_routes.append(
279
+ WebSocketRoute("/hub/plugin", PluginHub))
280
+
281
+ # Register all tools
282
+ register_all_tools(mcp)
283
+
284
+ # Register all resources
285
+ register_all_resources(mcp)
286
+
287
+
288
+ def main():
289
+ """Entry point for uvx and console scripts."""
290
+ parser = argparse.ArgumentParser(
291
+ description="MCP for Unity Server",
292
+ formatter_class=argparse.RawDescriptionHelpFormatter,
293
+ epilog="""
294
+ Environment Variables:
295
+ UNITY_MCP_DEFAULT_INSTANCE Default Unity instance to target (project name, hash, or 'Name@hash')
296
+ UNITY_MCP_SKIP_STARTUP_CONNECT Skip initial Unity connection attempt (set to 1/true/yes/on)
297
+ UNITY_MCP_TELEMETRY_ENABLED Enable telemetry (set to 1/true/yes/on)
298
+ UNITY_MCP_TRANSPORT Transport protocol: stdio or http (default: stdio)
299
+ UNITY_MCP_HTTP_URL HTTP server URL (default: http://localhost:8080)
300
+ UNITY_MCP_HTTP_HOST HTTP server host (overrides URL host)
301
+ UNITY_MCP_HTTP_PORT HTTP server port (overrides URL port)
302
+
303
+ Examples:
304
+ # Use specific Unity project as default
305
+ python -m src.server --default-instance "MyProject"
306
+
307
+ # Start with HTTP transport
308
+ python -m src.server --transport http --http-url http://localhost:8080
309
+
310
+ # Start with stdio transport (default)
311
+ python -m src.server --transport stdio
312
+
313
+ # Use environment variable for transport
314
+ UNITY_MCP_TRANSPORT=http UNITY_MCP_HTTP_URL=http://localhost:9000 python -m src.server
315
+ """
316
+ )
317
+ parser.add_argument(
318
+ "--default-instance",
319
+ type=str,
320
+ metavar="INSTANCE",
321
+ help="Default Unity instance to target (project name, hash, or 'Name@hash'). "
322
+ "Overrides UNITY_MCP_DEFAULT_INSTANCE environment variable."
323
+ )
324
+ parser.add_argument(
325
+ "--transport",
326
+ type=str,
327
+ choices=["stdio", "http"],
328
+ default="stdio",
329
+ help="Transport protocol to use: stdio or http (default: stdio). "
330
+ "Overrides UNITY_MCP_TRANSPORT environment variable."
331
+ )
332
+ parser.add_argument(
333
+ "--http-url",
334
+ type=str,
335
+ default="http://localhost:8080",
336
+ metavar="URL",
337
+ help="HTTP server URL (default: http://localhost:8080). "
338
+ "Can also set via UNITY_MCP_HTTP_URL environment variable."
339
+ )
340
+ parser.add_argument(
341
+ "--http-host",
342
+ type=str,
343
+ default=None,
344
+ metavar="HOST",
345
+ help="HTTP server host (overrides URL host). "
346
+ "Overrides UNITY_MCP_HTTP_HOST environment variable."
347
+ )
348
+ parser.add_argument(
349
+ "--http-port",
350
+ type=int,
351
+ default=None,
352
+ metavar="PORT",
353
+ help="HTTP server port (overrides URL port). "
354
+ "Overrides UNITY_MCP_HTTP_PORT environment variable."
355
+ )
356
+
357
+ args = parser.parse_args()
358
+
359
+ # Set environment variables from command line args
360
+ if args.default_instance:
361
+ os.environ["UNITY_MCP_DEFAULT_INSTANCE"] = args.default_instance
362
+ logger.info(
363
+ f"Using default Unity instance from command-line: {args.default_instance}")
364
+
365
+ # Set transport mode
366
+ transport_mode = args.transport or os.environ.get(
367
+ "UNITY_MCP_TRANSPORT", "stdio")
368
+ os.environ["UNITY_MCP_TRANSPORT"] = transport_mode
369
+ logger.info(f"Transport mode: {transport_mode}")
370
+
371
+ http_url = os.environ.get("UNITY_MCP_HTTP_URL", args.http_url)
372
+ parsed_url = urlparse(http_url)
373
+
374
+ # Allow individual host/port to override URL components
375
+ http_host = args.http_host or os.environ.get(
376
+ "UNITY_MCP_HTTP_HOST") or parsed_url.hostname or "localhost"
377
+ http_port = args.http_port or (int(os.environ.get("UNITY_MCP_HTTP_PORT")) if os.environ.get(
378
+ "UNITY_MCP_HTTP_PORT") else None) or parsed_url.port or 8080
379
+
380
+ os.environ["UNITY_MCP_HTTP_HOST"] = http_host
381
+ os.environ["UNITY_MCP_HTTP_PORT"] = str(http_port)
382
+
383
+ if args.http_url != "http://localhost:8080":
384
+ logger.info(f"HTTP URL set to: {http_url}")
385
+ if args.http_host:
386
+ logger.info(f"HTTP host override: {http_host}")
387
+ if args.http_port:
388
+ logger.info(f"HTTP port override: {http_port}")
389
+
390
+ # Determine transport mode
391
+ if transport_mode == 'http':
392
+ # Use HTTP transport for FastMCP
393
+ transport = 'http'
394
+ # Use the parsed host and port from URL/args
395
+ http_url = os.environ.get("UNITY_MCP_HTTP_URL", args.http_url)
396
+ parsed_url = urlparse(http_url)
397
+ host = args.http_host or os.environ.get(
398
+ "UNITY_MCP_HTTP_HOST") or parsed_url.hostname or "localhost"
399
+ port = args.http_port or (int(os.environ.get("UNITY_MCP_HTTP_PORT")) if os.environ.get(
400
+ "UNITY_MCP_HTTP_PORT") else None) or parsed_url.port or 8080
401
+ logger.info(f"Starting FastMCP with HTTP transport on {host}:{port}")
402
+ mcp.run(transport=transport, host=host, port=port)
403
+ else:
404
+ # Use stdio transport for traditional MCP
405
+ logger.info("Starting FastMCP with stdio transport")
406
+ mcp.run(transport='stdio')
407
+
408
+
409
+ # Run the server
410
+ if __name__ == "__main__":
411
+ main()