mcpforunityserver 9.3.0__py3-none-any.whl → 9.3.0b20260128055651__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.
- cli/commands/editor.py +27 -1
- cli/commands/prefab.py +134 -12
- cli/commands/texture.py +538 -0
- cli/commands/tool.py +61 -0
- cli/commands/vfx.py +51 -15
- cli/main.py +33 -0
- cli/utils/connection.py +37 -0
- cli/utils/suggestions.py +34 -0
- main.py +125 -6
- {mcpforunityserver-9.3.0.dist-info → mcpforunityserver-9.3.0b20260128055651.dist-info}/METADATA +2 -2
- {mcpforunityserver-9.3.0.dist-info → mcpforunityserver-9.3.0b20260128055651.dist-info}/RECORD +22 -17
- services/resources/prefab.py +191 -0
- services/tools/manage_components.py +1 -1
- services/tools/manage_gameobject.py +43 -23
- services/tools/manage_material.py +2 -2
- services/tools/manage_prefabs.py +128 -31
- services/tools/manage_texture.py +667 -0
- services/tools/manage_vfx.py +15 -633
- {mcpforunityserver-9.3.0.dist-info → mcpforunityserver-9.3.0b20260128055651.dist-info}/WHEEL +0 -0
- {mcpforunityserver-9.3.0.dist-info → mcpforunityserver-9.3.0b20260128055651.dist-info}/entry_points.txt +0 -0
- {mcpforunityserver-9.3.0.dist-info → mcpforunityserver-9.3.0b20260128055651.dist-info}/licenses/LICENSE +0 -0
- {mcpforunityserver-9.3.0.dist-info → mcpforunityserver-9.3.0b20260128055651.dist-info}/top_level.txt +0 -0
cli/commands/vfx.py
CHANGED
|
@@ -10,6 +10,27 @@ from cli.utils.output import format_output, print_error, print_success
|
|
|
10
10
|
from cli.utils.connection import run_command, UnityConnectionError
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
_VFX_TOP_LEVEL_KEYS = {"action", "target", "searchMethod", "properties"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _normalize_vfx_params(params: dict[str, Any]) -> dict[str, Any]:
|
|
17
|
+
params = dict(params)
|
|
18
|
+
properties: dict[str, Any] = {}
|
|
19
|
+
for key in list(params.keys()):
|
|
20
|
+
if key in _VFX_TOP_LEVEL_KEYS:
|
|
21
|
+
continue
|
|
22
|
+
properties[key] = params.pop(key)
|
|
23
|
+
|
|
24
|
+
if properties:
|
|
25
|
+
existing = params.get("properties")
|
|
26
|
+
if isinstance(existing, dict):
|
|
27
|
+
params["properties"] = {**properties, **existing}
|
|
28
|
+
else:
|
|
29
|
+
params["properties"] = properties
|
|
30
|
+
|
|
31
|
+
return {k: v for k, v in params.items() if v is not None}
|
|
32
|
+
|
|
33
|
+
|
|
13
34
|
@click.group()
|
|
14
35
|
def vfx():
|
|
15
36
|
"""VFX operations - particle systems, line renderers, trails."""
|
|
@@ -43,7 +64,8 @@ def particle_info(target: str, search_method: Optional[str]):
|
|
|
43
64
|
params["searchMethod"] = search_method
|
|
44
65
|
|
|
45
66
|
try:
|
|
46
|
-
result = run_command(
|
|
67
|
+
result = run_command(
|
|
68
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
47
69
|
click.echo(format_output(result, config.format))
|
|
48
70
|
except UnityConnectionError as e:
|
|
49
71
|
print_error(str(e))
|
|
@@ -70,7 +92,8 @@ def particle_play(target: str, with_children: bool, search_method: Optional[str]
|
|
|
70
92
|
params["searchMethod"] = search_method
|
|
71
93
|
|
|
72
94
|
try:
|
|
73
|
-
result = run_command(
|
|
95
|
+
result = run_command(
|
|
96
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
74
97
|
click.echo(format_output(result, config.format))
|
|
75
98
|
if result.get("success"):
|
|
76
99
|
print_success(f"Playing particle system: {target}")
|
|
@@ -93,7 +116,8 @@ def particle_stop(target: str, with_children: bool, search_method: Optional[str]
|
|
|
93
116
|
params["searchMethod"] = search_method
|
|
94
117
|
|
|
95
118
|
try:
|
|
96
|
-
result = run_command(
|
|
119
|
+
result = run_command(
|
|
120
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
97
121
|
click.echo(format_output(result, config.format))
|
|
98
122
|
if result.get("success"):
|
|
99
123
|
print_success(f"Stopped particle system: {target}")
|
|
@@ -113,7 +137,8 @@ def particle_pause(target: str, search_method: Optional[str]):
|
|
|
113
137
|
params["searchMethod"] = search_method
|
|
114
138
|
|
|
115
139
|
try:
|
|
116
|
-
result = run_command(
|
|
140
|
+
result = run_command(
|
|
141
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
117
142
|
click.echo(format_output(result, config.format))
|
|
118
143
|
except UnityConnectionError as e:
|
|
119
144
|
print_error(str(e))
|
|
@@ -134,7 +159,8 @@ def particle_restart(target: str, with_children: bool, search_method: Optional[s
|
|
|
134
159
|
params["searchMethod"] = search_method
|
|
135
160
|
|
|
136
161
|
try:
|
|
137
|
-
result = run_command(
|
|
162
|
+
result = run_command(
|
|
163
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
138
164
|
click.echo(format_output(result, config.format))
|
|
139
165
|
except UnityConnectionError as e:
|
|
140
166
|
print_error(str(e))
|
|
@@ -155,7 +181,8 @@ def particle_clear(target: str, with_children: bool, search_method: Optional[str
|
|
|
155
181
|
params["searchMethod"] = search_method
|
|
156
182
|
|
|
157
183
|
try:
|
|
158
|
-
result = run_command(
|
|
184
|
+
result = run_command(
|
|
185
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
159
186
|
click.echo(format_output(result, config.format))
|
|
160
187
|
except UnityConnectionError as e:
|
|
161
188
|
print_error(str(e))
|
|
@@ -188,7 +215,8 @@ def line_info(target: str, search_method: Optional[str]):
|
|
|
188
215
|
params["searchMethod"] = search_method
|
|
189
216
|
|
|
190
217
|
try:
|
|
191
|
-
result = run_command(
|
|
218
|
+
result = run_command(
|
|
219
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
192
220
|
click.echo(format_output(result, config.format))
|
|
193
221
|
except UnityConnectionError as e:
|
|
194
222
|
print_error(str(e))
|
|
@@ -223,7 +251,8 @@ def line_set_positions(target: str, positions: str, search_method: Optional[str]
|
|
|
223
251
|
params["searchMethod"] = search_method
|
|
224
252
|
|
|
225
253
|
try:
|
|
226
|
-
result = run_command(
|
|
254
|
+
result = run_command(
|
|
255
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
227
256
|
click.echo(format_output(result, config.format))
|
|
228
257
|
except UnityConnectionError as e:
|
|
229
258
|
print_error(str(e))
|
|
@@ -253,7 +282,8 @@ def line_create_line(target: str, start: Tuple[float, float, float], end: Tuple[
|
|
|
253
282
|
params["searchMethod"] = search_method
|
|
254
283
|
|
|
255
284
|
try:
|
|
256
|
-
result = run_command(
|
|
285
|
+
result = run_command(
|
|
286
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
257
287
|
click.echo(format_output(result, config.format))
|
|
258
288
|
except UnityConnectionError as e:
|
|
259
289
|
print_error(str(e))
|
|
@@ -286,7 +316,8 @@ def line_create_circle(target: str, center: Tuple[float, float, float], radius:
|
|
|
286
316
|
params["searchMethod"] = search_method
|
|
287
317
|
|
|
288
318
|
try:
|
|
289
|
-
result = run_command(
|
|
319
|
+
result = run_command(
|
|
320
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
290
321
|
click.echo(format_output(result, config.format))
|
|
291
322
|
except UnityConnectionError as e:
|
|
292
323
|
print_error(str(e))
|
|
@@ -304,7 +335,8 @@ def line_clear(target: str, search_method: Optional[str]):
|
|
|
304
335
|
params["searchMethod"] = search_method
|
|
305
336
|
|
|
306
337
|
try:
|
|
307
|
-
result = run_command(
|
|
338
|
+
result = run_command(
|
|
339
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
308
340
|
click.echo(format_output(result, config.format))
|
|
309
341
|
except UnityConnectionError as e:
|
|
310
342
|
print_error(str(e))
|
|
@@ -332,7 +364,8 @@ def trail_info(target: str, search_method: Optional[str]):
|
|
|
332
364
|
params["searchMethod"] = search_method
|
|
333
365
|
|
|
334
366
|
try:
|
|
335
|
-
result = run_command(
|
|
367
|
+
result = run_command(
|
|
368
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
336
369
|
click.echo(format_output(result, config.format))
|
|
337
370
|
except UnityConnectionError as e:
|
|
338
371
|
print_error(str(e))
|
|
@@ -360,7 +393,8 @@ def trail_set_time(target: str, duration: float, search_method: Optional[str]):
|
|
|
360
393
|
params["searchMethod"] = search_method
|
|
361
394
|
|
|
362
395
|
try:
|
|
363
|
-
result = run_command(
|
|
396
|
+
result = run_command(
|
|
397
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
364
398
|
click.echo(format_output(result, config.format))
|
|
365
399
|
except UnityConnectionError as e:
|
|
366
400
|
print_error(str(e))
|
|
@@ -378,7 +412,8 @@ def trail_clear(target: str, search_method: Optional[str]):
|
|
|
378
412
|
params["searchMethod"] = search_method
|
|
379
413
|
|
|
380
414
|
try:
|
|
381
|
-
result = run_command(
|
|
415
|
+
result = run_command(
|
|
416
|
+
"manage_vfx", _normalize_vfx_params(params), config)
|
|
382
417
|
click.echo(format_output(result, config.format))
|
|
383
418
|
except UnityConnectionError as e:
|
|
384
419
|
print_error(str(e))
|
|
@@ -432,7 +467,8 @@ def vfx_raw(action: str, target: Optional[str], params: str, search_method: Opti
|
|
|
432
467
|
# Merge extra params
|
|
433
468
|
request_params.update(extra_params)
|
|
434
469
|
try:
|
|
435
|
-
result = run_command(
|
|
470
|
+
result = run_command(
|
|
471
|
+
"manage_vfx", _normalize_vfx_params(request_params), config)
|
|
436
472
|
click.echo(format_output(result, config.format))
|
|
437
473
|
except UnityConnectionError as e:
|
|
438
474
|
print_error(str(e))
|
cli/main.py
CHANGED
|
@@ -8,6 +8,7 @@ from typing import Optional
|
|
|
8
8
|
|
|
9
9
|
from cli import __version__
|
|
10
10
|
from cli.utils.config import CLIConfig, set_config, get_config
|
|
11
|
+
from cli.utils.suggestions import suggest_matches, format_suggestions
|
|
11
12
|
from cli.utils.output import format_output, print_error, print_success, print_info
|
|
12
13
|
from cli.utils.connection import (
|
|
13
14
|
run_command,
|
|
@@ -28,6 +29,35 @@ class Context:
|
|
|
28
29
|
pass_context = click.make_pass_decorator(Context, ensure=True)
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
_ORIGINAL_RESOLVE_COMMAND = click.Group.resolve_command
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _resolve_command_with_suggestions(self: click.Group, ctx: click.Context, args: list[str]):
|
|
36
|
+
try:
|
|
37
|
+
return _ORIGINAL_RESOLVE_COMMAND(self, ctx, args)
|
|
38
|
+
except click.exceptions.NoSuchCommand as e:
|
|
39
|
+
if not args or args[0].startswith("-"):
|
|
40
|
+
raise
|
|
41
|
+
matches = suggest_matches(args[0], self.list_commands(ctx))
|
|
42
|
+
suggestion = format_suggestions(matches)
|
|
43
|
+
if suggestion:
|
|
44
|
+
message = f"{e}\n{suggestion}"
|
|
45
|
+
raise click.exceptions.UsageError(message, ctx=ctx)
|
|
46
|
+
raise
|
|
47
|
+
except click.exceptions.UsageError as e:
|
|
48
|
+
if args and not args[0].startswith("-") and "No such command" in str(e):
|
|
49
|
+
matches = suggest_matches(args[0], self.list_commands(ctx))
|
|
50
|
+
suggestion = format_suggestions(matches)
|
|
51
|
+
if suggestion:
|
|
52
|
+
message = f"{e}\n{suggestion}"
|
|
53
|
+
raise click.exceptions.UsageError(message, ctx=ctx)
|
|
54
|
+
raise
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Install suggestion handling for all CLI command groups.
|
|
58
|
+
click.Group.resolve_command = _resolve_command_with_suggestions # type: ignore[assignment]
|
|
59
|
+
|
|
60
|
+
|
|
31
61
|
@click.group()
|
|
32
62
|
@click.version_option(version=__version__, prog_name="unity-mcp")
|
|
33
63
|
@click.option(
|
|
@@ -212,6 +242,8 @@ def register_commands():
|
|
|
212
242
|
cli.add_command(command)
|
|
213
243
|
|
|
214
244
|
optional_commands = [
|
|
245
|
+
("cli.commands.tool", "tool"),
|
|
246
|
+
("cli.commands.tool", "custom_tool"),
|
|
215
247
|
("cli.commands.gameobject", "gameobject"),
|
|
216
248
|
("cli.commands.component", "component"),
|
|
217
249
|
("cli.commands.scene", "scene"),
|
|
@@ -229,6 +261,7 @@ def register_commands():
|
|
|
229
261
|
("cli.commands.shader", "shader"),
|
|
230
262
|
("cli.commands.vfx", "vfx"),
|
|
231
263
|
("cli.commands.batch", "batch"),
|
|
264
|
+
("cli.commands.texture", "texture"),
|
|
232
265
|
]
|
|
233
266
|
|
|
234
267
|
for module_name, command_name in optional_commands:
|
cli/utils/connection.py
CHANGED
|
@@ -189,3 +189,40 @@ async def list_unity_instances(config: Optional[CLIConfig] = None) -> Dict[str,
|
|
|
189
189
|
def run_list_instances(config: Optional[CLIConfig] = None) -> Dict[str, Any]:
|
|
190
190
|
"""Synchronous wrapper for list_unity_instances."""
|
|
191
191
|
return asyncio.run(list_unity_instances(config))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def list_custom_tools(config: Optional[CLIConfig] = None) -> Dict[str, Any]:
|
|
195
|
+
"""List custom tools registered for the active Unity project."""
|
|
196
|
+
cfg = config or get_config()
|
|
197
|
+
url = f"http://{cfg.host}:{cfg.port}/api/custom-tools"
|
|
198
|
+
params: Dict[str, Any] = {}
|
|
199
|
+
if cfg.unity_instance:
|
|
200
|
+
params["instance"] = cfg.unity_instance
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
async with httpx.AsyncClient() as client:
|
|
204
|
+
response = await client.get(url, params=params, timeout=cfg.timeout)
|
|
205
|
+
response.raise_for_status()
|
|
206
|
+
return response.json()
|
|
207
|
+
except httpx.ConnectError as e:
|
|
208
|
+
raise UnityConnectionError(
|
|
209
|
+
f"Cannot connect to Unity MCP server at {cfg.host}:{cfg.port}. "
|
|
210
|
+
f"Make sure the server is running and Unity is connected.\n"
|
|
211
|
+
f"Error: {e}"
|
|
212
|
+
)
|
|
213
|
+
except httpx.TimeoutException:
|
|
214
|
+
raise UnityConnectionError(
|
|
215
|
+
f"Connection to Unity timed out after {cfg.timeout}s. "
|
|
216
|
+
f"Unity may be busy or unresponsive."
|
|
217
|
+
)
|
|
218
|
+
except httpx.HTTPStatusError as e:
|
|
219
|
+
raise UnityConnectionError(
|
|
220
|
+
f"HTTP error from server: {e.response.status_code} - {e.response.text}"
|
|
221
|
+
)
|
|
222
|
+
except Exception as e:
|
|
223
|
+
raise UnityConnectionError(f"Unexpected error: {e}")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def run_list_custom_tools(config: Optional[CLIConfig] = None) -> Dict[str, Any]:
|
|
227
|
+
"""Synchronous wrapper for list_custom_tools."""
|
|
228
|
+
return asyncio.run(list_custom_tools(config))
|
cli/utils/suggestions.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Helpers for CLI suggestion messages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import difflib
|
|
6
|
+
from typing import Iterable, List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def suggest_matches(
|
|
10
|
+
value: str,
|
|
11
|
+
choices: Iterable[str],
|
|
12
|
+
*,
|
|
13
|
+
limit: int = 3,
|
|
14
|
+
cutoff: float = 0.6,
|
|
15
|
+
) -> List[str]:
|
|
16
|
+
"""Return close matches for a value from a list of choices."""
|
|
17
|
+
try:
|
|
18
|
+
normalized = [c for c in choices if isinstance(c, str)]
|
|
19
|
+
except Exception:
|
|
20
|
+
normalized = []
|
|
21
|
+
if not value or not normalized:
|
|
22
|
+
return []
|
|
23
|
+
return difflib.get_close_matches(value, normalized, n=limit, cutoff=cutoff)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def format_suggestions(matches: Iterable[str]) -> str | None:
|
|
27
|
+
"""Format matches into a CLI-friendly suggestion string."""
|
|
28
|
+
items = [m for m in matches if m]
|
|
29
|
+
if not items:
|
|
30
|
+
return None
|
|
31
|
+
if len(items) == 1:
|
|
32
|
+
return f"Did you mean: {items[0]}"
|
|
33
|
+
joined = ", ".join(items)
|
|
34
|
+
return f"Did you mean one of: {joined}"
|
main.py
CHANGED
|
@@ -9,7 +9,10 @@ from core.telemetry import record_milestone, record_telemetry, MilestoneType, Re
|
|
|
9
9
|
from services.resources import register_all_resources
|
|
10
10
|
from transport.plugin_registry import PluginRegistry
|
|
11
11
|
from transport.plugin_hub import PluginHub
|
|
12
|
-
from services.custom_tool_service import
|
|
12
|
+
from services.custom_tool_service import (
|
|
13
|
+
CustomToolService,
|
|
14
|
+
resolve_project_id_for_unity_instance,
|
|
15
|
+
)
|
|
13
16
|
from core.config import config
|
|
14
17
|
from starlette.routing import WebSocketRoute
|
|
15
18
|
from starlette.responses import JSONResponse
|
|
@@ -123,6 +126,9 @@ except Exception:
|
|
|
123
126
|
_unity_connection_pool: UnityConnectionPool | None = None
|
|
124
127
|
_plugin_registry: PluginRegistry | None = None
|
|
125
128
|
|
|
129
|
+
# Cached server version (set at startup to avoid repeated I/O)
|
|
130
|
+
_server_version: str | None = None
|
|
131
|
+
|
|
126
132
|
# In-memory custom tool service initialized after MCP construction
|
|
127
133
|
custom_tool_service: CustomToolService | None = None
|
|
128
134
|
|
|
@@ -130,8 +136,9 @@ custom_tool_service: CustomToolService | None = None
|
|
|
130
136
|
@asynccontextmanager
|
|
131
137
|
async def server_lifespan(server: FastMCP) -> AsyncIterator[dict[str, Any]]:
|
|
132
138
|
"""Handle server startup and shutdown."""
|
|
133
|
-
global _unity_connection_pool
|
|
134
|
-
|
|
139
|
+
global _unity_connection_pool, _server_version
|
|
140
|
+
_server_version = get_package_version()
|
|
141
|
+
logger.info(f"MCP for Unity Server v{_server_version} starting up")
|
|
135
142
|
|
|
136
143
|
# Register custom tool management endpoints with FastMCP
|
|
137
144
|
# Routes are declared globally below after FastMCP initialization
|
|
@@ -155,13 +162,12 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[dict[str, Any]]:
|
|
|
155
162
|
# Record server startup telemetry
|
|
156
163
|
start_time = time.time()
|
|
157
164
|
start_clk = time.perf_counter()
|
|
158
|
-
server_version = get_package_version()
|
|
159
165
|
# Defer initial telemetry by 1s to avoid stdio handshake interference
|
|
160
166
|
|
|
161
167
|
def _emit_startup():
|
|
162
168
|
try:
|
|
163
169
|
record_telemetry(RecordType.STARTUP, {
|
|
164
|
-
"server_version":
|
|
170
|
+
"server_version": _server_version,
|
|
165
171
|
"startup_time": start_time,
|
|
166
172
|
})
|
|
167
173
|
record_milestone(MilestoneType.FIRST_STARTUP)
|
|
@@ -322,9 +328,18 @@ def create_mcp_server(project_scoped_tools: bool) -> FastMCP:
|
|
|
322
328
|
return JSONResponse({
|
|
323
329
|
"status": "healthy",
|
|
324
330
|
"timestamp": time.time(),
|
|
331
|
+
"version": _server_version or "unknown",
|
|
325
332
|
"message": "MCP for Unity server is running"
|
|
326
333
|
})
|
|
327
334
|
|
|
335
|
+
def _normalize_instance_token(instance_token: str | None) -> tuple[str | None, str | None]:
|
|
336
|
+
if not instance_token:
|
|
337
|
+
return None, None
|
|
338
|
+
if "@" in instance_token:
|
|
339
|
+
name_part, _, hash_part = instance_token.partition("@")
|
|
340
|
+
return (name_part or None), (hash_part or None)
|
|
341
|
+
return None, instance_token
|
|
342
|
+
|
|
328
343
|
@mcp.custom_route("/api/command", methods=["POST"])
|
|
329
344
|
async def cli_command_route(request: Request) -> JSONResponse:
|
|
330
345
|
"""REST endpoint for CLI commands to Unity."""
|
|
@@ -348,11 +363,14 @@ def create_mcp_server(project_scoped_tools: bool) -> FastMCP:
|
|
|
348
363
|
|
|
349
364
|
# Find target session
|
|
350
365
|
session_id = None
|
|
366
|
+
session_details = None
|
|
367
|
+
instance_name, instance_hash = _normalize_instance_token(unity_instance)
|
|
351
368
|
if unity_instance:
|
|
352
369
|
# Try to match by hash or project name
|
|
353
370
|
for sid, details in sessions.sessions.items():
|
|
354
|
-
if details.hash ==
|
|
371
|
+
if details.hash == instance_hash or details.project in (instance_name, unity_instance):
|
|
355
372
|
session_id = sid
|
|
373
|
+
session_details = details
|
|
356
374
|
break
|
|
357
375
|
|
|
358
376
|
# If a specific unity_instance was requested but not found, return an error
|
|
@@ -367,6 +385,46 @@ def create_mcp_server(project_scoped_tools: bool) -> FastMCP:
|
|
|
367
385
|
else:
|
|
368
386
|
# No specific unity_instance requested: use first available session
|
|
369
387
|
session_id = next(iter(sessions.sessions.keys()))
|
|
388
|
+
session_details = sessions.sessions.get(session_id)
|
|
389
|
+
|
|
390
|
+
if command_type == "execute_custom_tool":
|
|
391
|
+
tool_name = None
|
|
392
|
+
tool_params = {}
|
|
393
|
+
if isinstance(params, dict):
|
|
394
|
+
tool_name = params.get("tool_name") or params.get("name")
|
|
395
|
+
tool_params = params.get("parameters") or params.get("params") or {}
|
|
396
|
+
|
|
397
|
+
if not tool_name:
|
|
398
|
+
return JSONResponse(
|
|
399
|
+
{"success": False, "error": "Missing 'tool_name' for execute_custom_tool"},
|
|
400
|
+
status_code=400,
|
|
401
|
+
)
|
|
402
|
+
if tool_params is None:
|
|
403
|
+
tool_params = {}
|
|
404
|
+
if not isinstance(tool_params, dict):
|
|
405
|
+
return JSONResponse(
|
|
406
|
+
{"success": False, "error": "Tool parameters must be an object/dict"},
|
|
407
|
+
status_code=400,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Prefer a concrete hash for project-scoped tools.
|
|
411
|
+
unity_instance_hint = unity_instance
|
|
412
|
+
if session_details and session_details.hash:
|
|
413
|
+
unity_instance_hint = session_details.hash
|
|
414
|
+
|
|
415
|
+
project_id = resolve_project_id_for_unity_instance(
|
|
416
|
+
unity_instance_hint)
|
|
417
|
+
if not project_id:
|
|
418
|
+
return JSONResponse(
|
|
419
|
+
{"success": False, "error": "Could not resolve project id for custom tool"},
|
|
420
|
+
status_code=400,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
service = CustomToolService.get_instance()
|
|
424
|
+
result = await service.execute_tool(
|
|
425
|
+
project_id, tool_name, unity_instance_hint, tool_params
|
|
426
|
+
)
|
|
427
|
+
return JSONResponse(result.model_dump())
|
|
370
428
|
|
|
371
429
|
# Send command to Unity
|
|
372
430
|
result = await PluginHub.send_command(session_id, command_type, params)
|
|
@@ -376,6 +434,67 @@ def create_mcp_server(project_scoped_tools: bool) -> FastMCP:
|
|
|
376
434
|
logger.error(f"CLI command error: {e}")
|
|
377
435
|
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
|
|
378
436
|
|
|
437
|
+
@mcp.custom_route("/api/custom-tools", methods=["GET"])
|
|
438
|
+
async def cli_custom_tools_route(request: Request) -> JSONResponse:
|
|
439
|
+
"""REST endpoint to list custom tools for the active Unity project."""
|
|
440
|
+
try:
|
|
441
|
+
unity_instance = request.query_params.get("instance")
|
|
442
|
+
instance_name, instance_hash = _normalize_instance_token(unity_instance)
|
|
443
|
+
|
|
444
|
+
sessions = await PluginHub.get_sessions()
|
|
445
|
+
if not sessions.sessions:
|
|
446
|
+
return JSONResponse({
|
|
447
|
+
"success": False,
|
|
448
|
+
"error": "No Unity instances connected. Make sure Unity is running with MCP plugin."
|
|
449
|
+
}, status_code=503)
|
|
450
|
+
|
|
451
|
+
session_details = None
|
|
452
|
+
if unity_instance:
|
|
453
|
+
# Try to match by hash or project name
|
|
454
|
+
for _, details in sessions.sessions.items():
|
|
455
|
+
if details.hash == instance_hash or details.project in (instance_name, unity_instance):
|
|
456
|
+
session_details = details
|
|
457
|
+
break
|
|
458
|
+
if not session_details:
|
|
459
|
+
return JSONResponse(
|
|
460
|
+
{
|
|
461
|
+
"success": False,
|
|
462
|
+
"error": f"Unity instance '{unity_instance}' not found",
|
|
463
|
+
},
|
|
464
|
+
status_code=404,
|
|
465
|
+
)
|
|
466
|
+
else:
|
|
467
|
+
# No specific unity_instance requested: use first available session
|
|
468
|
+
session_details = next(iter(sessions.sessions.values()))
|
|
469
|
+
|
|
470
|
+
unity_instance_hint = unity_instance
|
|
471
|
+
if session_details and session_details.hash:
|
|
472
|
+
unity_instance_hint = session_details.hash
|
|
473
|
+
|
|
474
|
+
project_id = resolve_project_id_for_unity_instance(
|
|
475
|
+
unity_instance_hint)
|
|
476
|
+
if not project_id:
|
|
477
|
+
return JSONResponse(
|
|
478
|
+
{"success": False, "error": "Could not resolve project id for custom tools"},
|
|
479
|
+
status_code=400,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
service = CustomToolService.get_instance()
|
|
483
|
+
tools = await service.list_registered_tools(project_id)
|
|
484
|
+
tools_payload = [
|
|
485
|
+
tool.model_dump() if hasattr(tool, "model_dump") else tool for tool in tools
|
|
486
|
+
]
|
|
487
|
+
|
|
488
|
+
return JSONResponse({
|
|
489
|
+
"success": True,
|
|
490
|
+
"project_id": project_id,
|
|
491
|
+
"tool_count": len(tools_payload),
|
|
492
|
+
"tools": tools_payload,
|
|
493
|
+
})
|
|
494
|
+
except Exception as e:
|
|
495
|
+
logger.error(f"CLI custom tools error: {e}")
|
|
496
|
+
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
|
|
497
|
+
|
|
379
498
|
@mcp.custom_route("/api/instances", methods=["GET"])
|
|
380
499
|
async def cli_instances_route(_: Request) -> JSONResponse:
|
|
381
500
|
"""REST endpoint to list connected Unity instances."""
|
{mcpforunityserver-9.3.0.dist-info → mcpforunityserver-9.3.0b20260128055651.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcpforunityserver
|
|
3
|
-
Version: 9.3.
|
|
3
|
+
Version: 9.3.0b20260128055651
|
|
4
4
|
Summary: MCP for Unity Server: A Unity package for Unity Editor integration via the Model Context Protocol (MCP).
|
|
5
5
|
Author-email: Marcus Sanatan <msanatan@gmail.com>, David Sarno <david.sarno@gmail.com>, Wu Shutong <martinwfire@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -110,7 +110,7 @@ Use this to run the latest released version from the repository. Change the vers
|
|
|
110
110
|
"command": "uvx",
|
|
111
111
|
"args": [
|
|
112
112
|
"--from",
|
|
113
|
-
"git+https://github.com/CoplayDev/unity-mcp@v9.
|
|
113
|
+
"git+https://github.com/CoplayDev/unity-mcp@v9.2.0#subdirectory=Server",
|
|
114
114
|
"mcp-for-unity",
|
|
115
115
|
"--transport",
|
|
116
116
|
"stdio"
|
{mcpforunityserver-9.3.0.dist-info → mcpforunityserver-9.3.0b20260128055651.dist-info}/RECORD
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
main.py,sha256=
|
|
1
|
+
main.py,sha256=EoHA0upWjtQzuoOgN5BfNmGL6bIVnFQjRW5PHO5EmjY,29265
|
|
2
2
|
cli/__init__.py,sha256=f2HjXqR9d8Uhibru211t9HPpdrb_1vdDC2v_NwF_eqA,63
|
|
3
|
-
cli/main.py,sha256=
|
|
3
|
+
cli/main.py,sha256=V_VFa8tA-CDHNv9J5NzNSLxRuEGjRVZWDe4xn6rYdog,8457
|
|
4
4
|
cli/commands/__init__.py,sha256=xQHf6o0afDV2HsU9gwSxjcrzS41cMCSGZyWYWxblPIk,69
|
|
5
5
|
cli/commands/animation.py,sha256=emBE5oKhFQNU8V2ENm9E5N4Grj0Tah9H0X7fF6grQdk,2442
|
|
6
6
|
cli/commands/asset.py,sha256=V1xzLgBPhdRzXsnj9Wt2HnJYo_8hT3RqoVnR2WrLP5w,7988
|
|
@@ -8,27 +8,30 @@ cli/commands/audio.py,sha256=qJ-Whc8aH7oUgT79O_RRRo-lAVktFqtC5pgbyG2bRNo,3333
|
|
|
8
8
|
cli/commands/batch.py,sha256=rMe8BDsthZ0AwaDrFoj6Kxl4xAVNRIlKSCcJ5eSagyY,5732
|
|
9
9
|
cli/commands/code.py,sha256=FGV8IDx6eFhcEmc6jREQwHwoOdiUyhY8d6Hy7KN4cTw,5624
|
|
10
10
|
cli/commands/component.py,sha256=uIOtno1T2mPF3rnW2OymetggScqtWrs_Th06FI7FISQ,6327
|
|
11
|
-
cli/commands/editor.py,sha256=
|
|
11
|
+
cli/commands/editor.py,sha256=oM1g8DNoJZ6slOSfNJYbLN02rQVmIX_a2QLhecc_Qog,14586
|
|
12
12
|
cli/commands/gameobject.py,sha256=b7ZxHXyIgUOvjYhHmKavigs-wfxGB6NhDMqqRyEGtNY,13643
|
|
13
13
|
cli/commands/instance.py,sha256=J6uQrNIEWbnJT-Y09ICTA9R11lgtPQflBbmTrBr5bg8,3041
|
|
14
14
|
cli/commands/lighting.py,sha256=eBvSDhQ5jkoUJJ4sito0yFxXwJv0JlpT4iD-D6Q2Pak,3869
|
|
15
15
|
cli/commands/material.py,sha256=51uxeoTgqnnMuUQUbhBTdMdI70kU4pOCH6GUIy2OjQI,7847
|
|
16
|
-
cli/commands/prefab.py,sha256=
|
|
16
|
+
cli/commands/prefab.py,sha256=E6aWXKyosJH0pJPK8krsRYUrZhHjnCm3iUpIAy4dkes,8177
|
|
17
17
|
cli/commands/scene.py,sha256=P08rud-6FZaO8Tw9jnP0xcS043Bf5IAooGbEDZPVBqw,6274
|
|
18
18
|
cli/commands/script.py,sha256=Yf9o00irn4wf0cbsE665mxJehwtiIr0y3IHKLyvYhgY,6434
|
|
19
19
|
cli/commands/shader.py,sha256=CwIIgyrU9OosVmidD6E9Txmn6Yyo4rDJBubrBchAlVw,6380
|
|
20
|
+
cli/commands/texture.py,sha256=qkvxb94W2B4oqyCi0WI0Cvwvvch5sp-UenBH5xMHnNY,18251
|
|
21
|
+
cli/commands/tool.py,sha256=9JQSUNPinLoDfP1T-STjcrn9A_UdIbGBr_c5G7X4r7k,1754
|
|
20
22
|
cli/commands/ui.py,sha256=JDfAXE3ba45r41Svfop-fiy4p8C0gxE4ekJ8aFRG7aI,7627
|
|
21
|
-
cli/commands/vfx.py,sha256=
|
|
23
|
+
cli/commands/vfx.py,sha256=tmHdaGDUABJ339Ia2Y4MTqr72UnoUOf_LxY69qUnAPg,16373
|
|
22
24
|
cli/utils/__init__.py,sha256=Gbm9hYC7UqwloFwdirXgo6z1iBktR9Y96o3bQcrYudc,613
|
|
23
25
|
cli/utils/config.py,sha256=_k3XAFmXG22sv8tYIb5JmO46kNl3T1sGqFptySAayfc,1550
|
|
24
|
-
cli/utils/connection.py,sha256=
|
|
26
|
+
cli/utils/connection.py,sha256=T9xmjfil0TAYJg5ZAbeqTtnmIhv5angQNG5vw40Ines,7619
|
|
25
27
|
cli/utils/output.py,sha256=96daU55ta_hl7UeOhNh5Iy7OJ4psbdR9Nfx1-q2k3xA,6370
|
|
28
|
+
cli/utils/suggestions.py,sha256=n6KG3Mrvub28X9rPFYFLRTtZ6HePp3PhhAeojG2WOJw,929
|
|
26
29
|
core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
30
|
core/config.py,sha256=czkTtNji1crQcQbUvmdx4OL7f-RBqkVhj_PtHh-w7rs,1623
|
|
28
31
|
core/logging_decorator.py,sha256=D9CD7rFvQz-MBG-G4inizQj0Ivr6dfc9RBmTrw7q8mI,1383
|
|
29
32
|
core/telemetry.py,sha256=eHjYgzd8f7eTwSwF2Kbi8D4TtJIcdaDjKLeo1c-0hVA,19829
|
|
30
33
|
core/telemetry_decorator.py,sha256=ycSTrzVNCDQHSd-xmIWOpVfKFURPxpiZe_XkOQAGDAo,6705
|
|
31
|
-
mcpforunityserver-9.3.
|
|
34
|
+
mcpforunityserver-9.3.0b20260128055651.dist-info/licenses/LICENSE,sha256=bv5lDJZQEqxBgjjc1rkRbkEwpSIHF-8N-1Od0VnEJFw,1066
|
|
32
35
|
models/__init__.py,sha256=JlscZkGWE9TRmSoBi99v_LSl8OAFNGmr8463PYkXin4,179
|
|
33
36
|
models/models.py,sha256=heXuvdBtdats1SGwW8wKFFHM0qR4hA6A7qETn5s9BZ0,1827
|
|
34
37
|
models/unity_response.py,sha256=oJ1PTsnNc5VBC-9OgM59C0C-R9N-GdmEdmz_yph4GSU,1454
|
|
@@ -44,6 +47,7 @@ services/resources/editor_state.py,sha256=pQdcsWGcKV7-6icpcVXtFD35CHUXodANc0jXkl
|
|
|
44
47
|
services/resources/gameobject.py,sha256=RM28kfsV208zdTy-549U2_nwSPiAHYo6SqXy22k4tC8,9116
|
|
45
48
|
services/resources/layers.py,sha256=wE-mSgZsknGrXKu-0Cppv6NeijszD7beFf88dizT0ZI,1086
|
|
46
49
|
services/resources/menu_items.py,sha256=9SNycjwTXoeS1ZHra0Y1fTyCjSEdPCo34JyxtuqauG8,1021
|
|
50
|
+
services/resources/prefab.py,sha256=z5mTWNke5MQauovbgdNa3HRe5-5B6K7FBspp-OtfGHM,7303
|
|
47
51
|
services/resources/prefab_stage.py,sha256=RyVskG-P9lb4szbsTDhPpyDMb0ptLskr0BnoYJylhw0,1388
|
|
48
52
|
services/resources/project_info.py,sha256=ggiUj9rJUvIddxorKu9yqJiHTWOnxyywkjjsKXhIyqA,1329
|
|
49
53
|
services/resources/selection.py,sha256=MALwKkM9xsKing2bALNVTVLWzDTE_b26EVbnVUGZivU,1845
|
|
@@ -60,16 +64,17 @@ services/tools/execute_menu_item.py,sha256=k4J89LlXmEGyo9z3NK8Q0vREIzr11ucF_9tN_
|
|
|
60
64
|
services/tools/find_gameobjects.py,sha256=Qpfd_oQG0fluz8S1CfriGh1FmLnZ080-ZEZOrAsij8U,3602
|
|
61
65
|
services/tools/find_in_file.py,sha256=SxhMeo8lRrt0OiGApGZSFUnq671bxVfK8qgAsHxLua8,6493
|
|
62
66
|
services/tools/manage_asset.py,sha256=St_iWQWg9icztnRthU78t6JNhJN0AlC6ELiZhn-SNZU,5990
|
|
63
|
-
services/tools/manage_components.py,sha256=
|
|
67
|
+
services/tools/manage_components.py,sha256=2_nKPk9iPAf5VyYiXuRxSkN8U76VNQbMtE68UTPngrw,5061
|
|
64
68
|
services/tools/manage_editor.py,sha256=ShvlSBQRfoNQ0DvqBWak_Hi3MB7tv2WkMKEhrKQipk0,3279
|
|
65
|
-
services/tools/manage_gameobject.py,sha256=
|
|
66
|
-
services/tools/manage_material.py,sha256=
|
|
67
|
-
services/tools/manage_prefabs.py,sha256=
|
|
69
|
+
services/tools/manage_gameobject.py,sha256=AXHT4fcrxvsaX53bypKoz3egY2uFI5kf7JCn2SizfB4,16604
|
|
70
|
+
services/tools/manage_material.py,sha256=Zt-tqGRCmOKTmttsu5yeudFNWzkDBkeuf44av06g-w0,5548
|
|
71
|
+
services/tools/manage_prefabs.py,sha256=mGGuYYpB2b9OV0fxNOtI8WnTZj9KjF7A3Isdzx8GGuI,6973
|
|
68
72
|
services/tools/manage_scene.py,sha256=-ARtRuj7ZNk_14lmMSORnQs0qTAYKBTPtUfk0sNDo6A,5370
|
|
69
73
|
services/tools/manage_script.py,sha256=MzPw0xXjtbdjEyjvUfLem9fa3GVE-WGvCr4WEVfW9Cs,28461
|
|
70
74
|
services/tools/manage_scriptable_object.py,sha256=tezG_mbGzPLNpL3F7l5JJLyyjJN3rJi1thGMU8cpOC4,3659
|
|
71
75
|
services/tools/manage_shader.py,sha256=bucRKzQww7opy6DK5nf6isVaEECWWqJ-DVkFulp8CV8,3185
|
|
72
|
-
services/tools/
|
|
76
|
+
services/tools/manage_texture.py,sha256=ap2WolIJw2iVnLyAHhY6WahiGNLmtejJX7k0kq1zWrc,25932
|
|
77
|
+
services/tools/manage_vfx.py,sha256=7KFbRohF8EzaD0m7vVIEwjUz-QwC7NEXS5cVcU6Die0,4710
|
|
73
78
|
services/tools/preflight.py,sha256=0nvo0BmZMdIGop1Ha_vypkjn2VLiRvskF0uxh_SlZgE,4162
|
|
74
79
|
services/tools/read_console.py,sha256=ps23debJcQkj3Ap-MqTYVhopYnKGspJs9QHLJHZAAkE,6826
|
|
75
80
|
services/tools/refresh_unity.py,sha256=KrRA8bmLkDLFO1XBv2NmagQAp1dmyaXdUAap567Hcv4,7100
|
|
@@ -89,8 +94,8 @@ transport/legacy/unity_connection.py,sha256=FE9ZQfYMhHvIxBycr_DjI3BKvuEdORXuABnC
|
|
|
89
94
|
utils/focus_nudge.py,sha256=HaTOSI7wzDmdRviodUHx2oQFPIL_jSwubai3YkDJbH0,9910
|
|
90
95
|
utils/module_discovery.py,sha256=My48ofB1BUqxiBoAZAGbEaLQYdsrDhMm8MayBP_bUSQ,2005
|
|
91
96
|
utils/reload_sentinel.py,sha256=s1xMWhl-r2XwN0OUbiUv_VGUy8TvLtV5bkql-5n2DT0,373
|
|
92
|
-
mcpforunityserver-9.3.
|
|
93
|
-
mcpforunityserver-9.3.
|
|
94
|
-
mcpforunityserver-9.3.
|
|
95
|
-
mcpforunityserver-9.3.
|
|
96
|
-
mcpforunityserver-9.3.
|
|
97
|
+
mcpforunityserver-9.3.0b20260128055651.dist-info/METADATA,sha256=oYHDprvEMpsJnYVleQbe99kXV7CRCOoPY32jTDkZ7Jc,5804
|
|
98
|
+
mcpforunityserver-9.3.0b20260128055651.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
99
|
+
mcpforunityserver-9.3.0b20260128055651.dist-info/entry_points.txt,sha256=pPm70RXQvkt3uBhPOtViDa47ZTA03RaQ6rwXvyi8oiI,70
|
|
100
|
+
mcpforunityserver-9.3.0b20260128055651.dist-info/top_level.txt,sha256=3-A65WsmBO6UZYH8O5mINdyhhZ63SDssr8LncRd1PSQ,46
|
|
101
|
+
mcpforunityserver-9.3.0b20260128055651.dist-info/RECORD,,
|