Skip to content

Commit b75202a

Browse files
committed
add missing unity_instance parameter
1 parent d647d99 commit b75202a

File tree

5 files changed

+147
-18
lines changed

5 files changed

+147
-18
lines changed

MCPForUnity/Editor/MCPForUnityBridge.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,22 @@ public static void Stop()
489489
try { AssemblyReloadEvents.afterAssemblyReload -= OnAfterAssemblyReload; } catch { }
490490
try { EditorApplication.quitting -= Stop; } catch { }
491491

492+
// Clean up status file when Unity stops
493+
try
494+
{
495+
string statusDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-mcp");
496+
string statusFile = Path.Combine(statusDir, $"unity-mcp-status-{ComputeProjectHash(Application.dataPath)}.json");
497+
if (File.Exists(statusFile))
498+
{
499+
File.Delete(statusFile);
500+
if (IsDebugEnabled()) McpLog.Info($"Deleted status file: {statusFile}");
501+
}
502+
}
503+
catch (Exception ex)
504+
{
505+
if (IsDebugEnabled()) McpLog.Warn($"Failed to delete status file: {ex.Message}");
506+
}
507+
492508
if (IsDebugEnabled()) McpLog.Info("MCPForUnityBridge stopped.");
493509
}
494510

@@ -1199,13 +1215,38 @@ private static void WriteHeartbeat(bool reloading, string reason = null)
11991215
}
12001216
Directory.CreateDirectory(dir);
12011217
string filePath = Path.Combine(dir, $"unity-mcp-status-{ComputeProjectHash(Application.dataPath)}.json");
1218+
1219+
// Extract project name from path
1220+
string projectName = "Unknown";
1221+
try
1222+
{
1223+
string projectPath = Application.dataPath;
1224+
if (!string.IsNullOrEmpty(projectPath))
1225+
{
1226+
// Remove trailing /Assets or \Assets
1227+
projectPath = projectPath.TrimEnd('/', '\\');
1228+
if (projectPath.EndsWith("Assets", StringComparison.OrdinalIgnoreCase))
1229+
{
1230+
projectPath = projectPath.Substring(0, projectPath.Length - 6).TrimEnd('/', '\\');
1231+
}
1232+
projectName = Path.GetFileName(projectPath);
1233+
if (string.IsNullOrEmpty(projectName))
1234+
{
1235+
projectName = "Unknown";
1236+
}
1237+
}
1238+
}
1239+
catch { }
1240+
12021241
var payload = new
12031242
{
12041243
unity_port = currentUnityPort,
12051244
reloading,
12061245
reason = reason ?? (reloading ? "reloading" : "ready"),
12071246
seq = heartbeatSeq,
12081247
project_path = Application.dataPath,
1248+
project_name = projectName,
1249+
unity_version = Application.unityVersion,
12091250
last_heartbeat = DateTime.UtcNow.ToString("O")
12101251
};
12111252
File.WriteAllText(filePath, JsonConvert.SerializeObject(payload), new System.Text.UTF8Encoding(false));

MCPForUnity/UnityMcpServer~/src/server.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
from logging.handlers import RotatingFileHandler
55
import os
6+
import argparse
67
from contextlib import asynccontextmanager
78
from typing import AsyncIterator, Dict, Any
89
from config import config
@@ -199,6 +200,38 @@ def _emit_startup():
199200

200201
def main():
201202
"""Entry point for uvx and console scripts."""
203+
parser = argparse.ArgumentParser(
204+
description="MCP for Unity Server",
205+
formatter_class=argparse.RawDescriptionHelpFormatter,
206+
epilog="""
207+
Environment Variables:
208+
UNITY_MCP_DEFAULT_INSTANCE Default Unity instance to target (project name, hash, or 'Name@hash')
209+
UNITY_MCP_SKIP_STARTUP_CONNECT Skip initial Unity connection attempt (set to 1/true/yes/on)
210+
UNITY_MCP_TELEMETRY_ENABLED Enable telemetry (set to 1/true/yes/on)
211+
212+
Examples:
213+
# Use specific Unity project as default
214+
python -m src.server --default-instance "MyProject"
215+
216+
# Or use environment variable
217+
UNITY_MCP_DEFAULT_INSTANCE="MyProject" python -m src.server
218+
"""
219+
)
220+
parser.add_argument(
221+
"--default-instance",
222+
type=str,
223+
metavar="INSTANCE",
224+
help="Default Unity instance to target (project name, hash, or 'Name@hash'). "
225+
"Overrides UNITY_MCP_DEFAULT_INSTANCE environment variable."
226+
)
227+
228+
args = parser.parse_args()
229+
230+
# Set environment variable if --default-instance is provided
231+
if args.default_instance:
232+
os.environ["UNITY_MCP_DEFAULT_INSTANCE"] = args.default_instance
233+
logger.info(f"Using default Unity instance from command-line: {args.default_instance}")
234+
202235
mcp.run(transport='stdio')
203236

204237

MCPForUnity/UnityMcpServer~/src/tools/manage_script.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ def apply_text_edits(
8585
"Optional strict flag, used to enforce strict mode"] | None = None,
8686
options: Annotated[dict[str, Any],
8787
"Optional options, used to pass additional options to the script editor"] | None = None,
88+
unity_instance: Annotated[str,
89+
"Target Unity instance (project name, hash, or 'Name@hash'). If not specified, uses default instance."] | None = None,
8890
) -> dict[str, Any]:
8991
ctx.info(f"Processing apply_text_edits: {uri}")
9092
name, directory = _split_uri(uri)
@@ -107,7 +109,7 @@ def _needs_normalization(arr: list[dict[str, Any]]) -> bool:
107109
"action": "read",
108110
"name": name,
109111
"path": directory,
110-
})
112+
}, instance_id=unity_instance)
111113
if not (isinstance(read_resp, dict) and read_resp.get("success")):
112114
return read_resp if isinstance(read_resp, dict) else {"success": False, "message": str(read_resp)}
113115
data = read_resp.get("data", {})
@@ -304,7 +306,7 @@ def _le(a: tuple[int, int], b: tuple[int, int]) -> bool:
304306
"options": opts,
305307
}
306308
params = {k: v for k, v in params.items() if v is not None}
307-
resp = unity_connection.send_command_with_retry("manage_script", params)
309+
resp = unity_connection.send_command_with_retry("manage_script", params, instance_id=unity_instance)
308310
if isinstance(resp, dict):
309311
data = resp.setdefault("data", {})
310312
data.setdefault("normalizedEdits", normalized_edits)
@@ -341,6 +343,7 @@ def _flip_async():
341343
{"menuPath": "MCP/Flip Reload Sentinel"},
342344
max_retries=0,
343345
retry_ms=0,
346+
instance_id=unity_instance,
344347
)
345348
except Exception:
346349
pass
@@ -359,6 +362,8 @@ def create_script(
359362
contents: Annotated[str, "Contents of the script to create. Note, this is Base64 encoded over transport."],
360363
script_type: Annotated[str, "Script type (e.g., 'C#')"] | None = None,
361364
namespace: Annotated[str, "Namespace for the script"] | None = None,
365+
unity_instance: Annotated[str,
366+
"Target Unity instance (project name, hash, or 'Name@hash'). If not specified, uses default instance."] | None = None,
362367
) -> dict[str, Any]:
363368
ctx.info(f"Processing create_script: {path}")
364369
name = os.path.splitext(os.path.basename(path))[0]
@@ -386,22 +391,24 @@ def create_script(
386391
contents.encode("utf-8")).decode("utf-8")
387392
params["contentsEncoded"] = True
388393
params = {k: v for k, v in params.items() if v is not None}
389-
resp = unity_connection.send_command_with_retry("manage_script", params)
394+
resp = unity_connection.send_command_with_retry("manage_script", params, instance_id=unity_instance)
390395
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
391396

392397

393398
@mcp_for_unity_tool(description=("Delete a C# script by URI or Assets-relative path."))
394399
def delete_script(
395400
ctx: Context,
396-
uri: Annotated[str, "URI of the script to delete under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."]
401+
uri: Annotated[str, "URI of the script to delete under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."],
402+
unity_instance: Annotated[str,
403+
"Target Unity instance (project name, hash, or 'Name@hash'). If not specified, uses default instance."] | None = None,
397404
) -> dict[str, Any]:
398405
"""Delete a C# script by URI."""
399406
ctx.info(f"Processing delete_script: {uri}")
400407
name, directory = _split_uri(uri)
401408
if not directory or directory.split("/")[0].lower() != "assets":
402409
return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."}
403410
params = {"action": "delete", "name": name, "path": directory}
404-
resp = unity_connection.send_command_with_retry("manage_script", params)
411+
resp = unity_connection.send_command_with_retry("manage_script", params, instance_id=unity_instance)
405412
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
406413

407414

@@ -412,7 +419,9 @@ def validate_script(
412419
level: Annotated[Literal['basic', 'standard'],
413420
"Validation level"] = "basic",
414421
include_diagnostics: Annotated[bool,
415-
"Include full diagnostics and summary"] = False
422+
"Include full diagnostics and summary"] = False,
423+
unity_instance: Annotated[str,
424+
"Target Unity instance (project name, hash, or 'Name@hash'). If not specified, uses default instance."] | None = None,
416425
) -> dict[str, Any]:
417426
ctx.info(f"Processing validate_script: {uri}")
418427
name, directory = _split_uri(uri)
@@ -426,7 +435,7 @@ def validate_script(
426435
"path": directory,
427436
"level": level,
428437
}
429-
resp = unity_connection.send_command_with_retry("manage_script", params)
438+
resp = unity_connection.send_command_with_retry("manage_script", params, instance_id=unity_instance)
430439
if isinstance(resp, dict) and resp.get("success"):
431440
diags = resp.get("data", {}).get("diagnostics", []) or []
432441
warnings = sum(1 for d in diags if str(
@@ -537,13 +546,15 @@ def manage_script_capabilities(ctx: Context) -> dict[str, Any]:
537546
@mcp_for_unity_tool(description="Get SHA256 and basic metadata for a Unity C# script without returning file contents")
538547
def get_sha(
539548
ctx: Context,
540-
uri: Annotated[str, "URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."]
549+
uri: Annotated[str, "URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."],
550+
unity_instance: Annotated[str,
551+
"Target Unity instance (project name, hash, or 'Name@hash'). If not specified, uses default instance."] | None = None,
541552
) -> dict[str, Any]:
542553
ctx.info(f"Processing get_sha: {uri}")
543554
try:
544555
name, directory = _split_uri(uri)
545556
params = {"action": "get_sha", "name": name, "path": directory}
546-
resp = unity_connection.send_command_with_retry("manage_script", params)
557+
resp = unity_connection.send_command_with_retry("manage_script", params, instance_id=unity_instance)
547558
if isinstance(resp, dict) and resp.get("success"):
548559
data = resp.get("data", {})
549560
minimal = {"sha256": data.get(

Server/server.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
from logging.handlers import RotatingFileHandler
55
import os
6+
import argparse
67
from contextlib import asynccontextmanager
78
from typing import AsyncIterator, Dict, Any
89
from config import config
@@ -199,6 +200,38 @@ def _emit_startup():
199200

200201
def main():
201202
"""Entry point for uvx and console scripts."""
203+
parser = argparse.ArgumentParser(
204+
description="MCP for Unity Server",
205+
formatter_class=argparse.RawDescriptionHelpFormatter,
206+
epilog="""
207+
Environment Variables:
208+
UNITY_MCP_DEFAULT_INSTANCE Default Unity instance to target (project name, hash, or 'Name@hash')
209+
UNITY_MCP_SKIP_STARTUP_CONNECT Skip initial Unity connection attempt (set to 1/true/yes/on)
210+
UNITY_MCP_TELEMETRY_ENABLED Enable telemetry (set to 1/true/yes/on)
211+
212+
Examples:
213+
# Use specific Unity project as default
214+
python -m src.server --default-instance "MyProject"
215+
216+
# Or use environment variable
217+
UNITY_MCP_DEFAULT_INSTANCE="MyProject" python -m src.server
218+
"""
219+
)
220+
parser.add_argument(
221+
"--default-instance",
222+
type=str,
223+
metavar="INSTANCE",
224+
help="Default Unity instance to target (project name, hash, or 'Name@hash'). "
225+
"Overrides UNITY_MCP_DEFAULT_INSTANCE environment variable."
226+
)
227+
228+
args = parser.parse_args()
229+
230+
# Set environment variable if --default-instance is provided
231+
if args.default_instance:
232+
os.environ["UNITY_MCP_DEFAULT_INSTANCE"] = args.default_instance
233+
logger.info(f"Using default Unity instance from command-line: {args.default_instance}")
234+
202235
mcp.run(transport='stdio')
203236

204237

Server/tools/manage_script.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ def apply_text_edits(
8585
"Optional strict flag, used to enforce strict mode"] | None = None,
8686
options: Annotated[dict[str, Any],
8787
"Optional options, used to pass additional options to the script editor"] | None = None,
88+
unity_instance: Annotated[str,
89+
"Target Unity instance (project name, hash, or 'Name@hash'). If not specified, uses default instance."] | None = None,
8890
) -> dict[str, Any]:
8991
ctx.info(f"Processing apply_text_edits: {uri}")
9092
name, directory = _split_uri(uri)
@@ -107,7 +109,7 @@ def _needs_normalization(arr: list[dict[str, Any]]) -> bool:
107109
"action": "read",
108110
"name": name,
109111
"path": directory,
110-
})
112+
}, instance_id=unity_instance)
111113
if not (isinstance(read_resp, dict) and read_resp.get("success")):
112114
return read_resp if isinstance(read_resp, dict) else {"success": False, "message": str(read_resp)}
113115
data = read_resp.get("data", {})
@@ -304,7 +306,7 @@ def _le(a: tuple[int, int], b: tuple[int, int]) -> bool:
304306
"options": opts,
305307
}
306308
params = {k: v for k, v in params.items() if v is not None}
307-
resp = unity_connection.send_command_with_retry("manage_script", params)
309+
resp = unity_connection.send_command_with_retry("manage_script", params, instance_id=unity_instance)
308310
if isinstance(resp, dict):
309311
data = resp.setdefault("data", {})
310312
data.setdefault("normalizedEdits", normalized_edits)
@@ -341,6 +343,7 @@ def _flip_async():
341343
{"menuPath": "MCP/Flip Reload Sentinel"},
342344
max_retries=0,
343345
retry_ms=0,
346+
instance_id=unity_instance,
344347
)
345348
except Exception:
346349
pass
@@ -359,6 +362,8 @@ def create_script(
359362
contents: Annotated[str, "Contents of the script to create. Note, this is Base64 encoded over transport."],
360363
script_type: Annotated[str, "Script type (e.g., 'C#')"] | None = None,
361364
namespace: Annotated[str, "Namespace for the script"] | None = None,
365+
unity_instance: Annotated[str,
366+
"Target Unity instance (project name, hash, or 'Name@hash'). If not specified, uses default instance."] | None = None,
362367
) -> dict[str, Any]:
363368
ctx.info(f"Processing create_script: {path}")
364369
name = os.path.splitext(os.path.basename(path))[0]
@@ -386,22 +391,24 @@ def create_script(
386391
contents.encode("utf-8")).decode("utf-8")
387392
params["contentsEncoded"] = True
388393
params = {k: v for k, v in params.items() if v is not None}
389-
resp = unity_connection.send_command_with_retry("manage_script", params)
394+
resp = unity_connection.send_command_with_retry("manage_script", params, instance_id=unity_instance)
390395
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
391396

392397

393398
@mcp_for_unity_tool(description=("Delete a C# script by URI or Assets-relative path."))
394399
def delete_script(
395400
ctx: Context,
396-
uri: Annotated[str, "URI of the script to delete under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."]
401+
uri: Annotated[str, "URI of the script to delete under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."],
402+
unity_instance: Annotated[str,
403+
"Target Unity instance (project name, hash, or 'Name@hash'). If not specified, uses default instance."] | None = None,
397404
) -> dict[str, Any]:
398405
"""Delete a C# script by URI."""
399406
ctx.info(f"Processing delete_script: {uri}")
400407
name, directory = _split_uri(uri)
401408
if not directory or directory.split("/")[0].lower() != "assets":
402409
return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."}
403410
params = {"action": "delete", "name": name, "path": directory}
404-
resp = unity_connection.send_command_with_retry("manage_script", params)
411+
resp = unity_connection.send_command_with_retry("manage_script", params, instance_id=unity_instance)
405412
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
406413

407414

@@ -412,7 +419,9 @@ def validate_script(
412419
level: Annotated[Literal['basic', 'standard'],
413420
"Validation level"] = "basic",
414421
include_diagnostics: Annotated[bool,
415-
"Include full diagnostics and summary"] = False
422+
"Include full diagnostics and summary"] = False,
423+
unity_instance: Annotated[str,
424+
"Target Unity instance (project name, hash, or 'Name@hash'). If not specified, uses default instance."] | None = None,
416425
) -> dict[str, Any]:
417426
ctx.info(f"Processing validate_script: {uri}")
418427
name, directory = _split_uri(uri)
@@ -426,7 +435,7 @@ def validate_script(
426435
"path": directory,
427436
"level": level,
428437
}
429-
resp = unity_connection.send_command_with_retry("manage_script", params)
438+
resp = unity_connection.send_command_with_retry("manage_script", params, instance_id=unity_instance)
430439
if isinstance(resp, dict) and resp.get("success"):
431440
diags = resp.get("data", {}).get("diagnostics", []) or []
432441
warnings = sum(1 for d in diags if str(
@@ -537,13 +546,15 @@ def manage_script_capabilities(ctx: Context) -> dict[str, Any]:
537546
@mcp_for_unity_tool(description="Get SHA256 and basic metadata for a Unity C# script without returning file contents")
538547
def get_sha(
539548
ctx: Context,
540-
uri: Annotated[str, "URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."]
549+
uri: Annotated[str, "URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."],
550+
unity_instance: Annotated[str,
551+
"Target Unity instance (project name, hash, or 'Name@hash'). If not specified, uses default instance."] | None = None,
541552
) -> dict[str, Any]:
542553
ctx.info(f"Processing get_sha: {uri}")
543554
try:
544555
name, directory = _split_uri(uri)
545556
params = {"action": "get_sha", "name": name, "path": directory}
546-
resp = unity_connection.send_command_with_retry("manage_script", params)
557+
resp = unity_connection.send_command_with_retry("manage_script", params, instance_id=unity_instance)
547558
if isinstance(resp, dict) and resp.get("success"):
548559
data = resp.get("data", {})
549560
minimal = {"sha256": data.get(

0 commit comments

Comments
 (0)