2026-01-07 13:33:20 +08:00
|
|
|
#!/usr/bin/env python3
|
2026-01-31 09:31:23 +08:00
|
|
|
"""Prepare MCPForUnity for Asset Store upload.
|
|
|
|
|
|
|
|
|
|
Usage:
|
|
|
|
|
python tools/prepare_unity_asset_store_release.py \
|
|
|
|
|
--remote-url https://your.remote.endpoint/ \
|
|
|
|
|
--asset-project /path/to/AssetStoreUploads \
|
|
|
|
|
--backup
|
|
|
|
|
"""
|
2026-01-07 13:33:20 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import datetime as dt
|
|
|
|
|
import re
|
|
|
|
|
import shutil
|
|
|
|
|
import tempfile
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
REPO_ROOT_DEFAULT = Path(__file__).resolve(
|
|
|
|
|
).parents[1] # adjust if you place elsewhere
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def read_text(path: Path) -> str:
|
|
|
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def write_text(path: Path, text: str) -> None:
|
|
|
|
|
path.write_text(text, encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def replace_once(path: Path, pattern: str, repl: str) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Regex replace exactly once, else raise.
|
|
|
|
|
"""
|
|
|
|
|
original = read_text(path)
|
|
|
|
|
new, n = re.subn(pattern, repl, original, flags=re.MULTILINE)
|
|
|
|
|
if n != 1:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
f"{path}: expected 1 replacement for pattern, got {n}")
|
|
|
|
|
if new != original:
|
|
|
|
|
write_text(path, new)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def remove_line_exact(path: Path, line: str) -> None:
|
|
|
|
|
original = read_text(path)
|
|
|
|
|
lines = original.splitlines(keepends=True)
|
|
|
|
|
|
|
|
|
|
removed = 0
|
|
|
|
|
kept: list[str] = []
|
|
|
|
|
for l in lines:
|
|
|
|
|
if l.strip() == line:
|
|
|
|
|
removed += 1
|
|
|
|
|
continue
|
|
|
|
|
kept.append(l)
|
|
|
|
|
|
|
|
|
|
if removed != 1:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
f"{path}: expected to remove exactly 1 line '{line}', removed {removed}")
|
|
|
|
|
|
|
|
|
|
write_text(path, "".join(kept))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def backup_dir(src: Path, backup_root: Path) -> Path:
|
|
|
|
|
ts = dt.datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
|
|
|
backup_path = backup_root / f"{src.name}.backup.{ts}"
|
|
|
|
|
shutil.copytree(src, backup_path)
|
|
|
|
|
return backup_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
|
description="Prepare MCPForUnity for Asset Store upload.")
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--repo-root",
|
|
|
|
|
default=str(REPO_ROOT_DEFAULT),
|
|
|
|
|
help="Path to unity-mcp repo root (default: inferred from script location).",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--asset-project",
|
|
|
|
|
default=None,
|
|
|
|
|
help="Path to the Unity project used for Asset Store uploads.",
|
|
|
|
|
)
|
2026-01-31 09:31:23 +08:00
|
|
|
parser.add_argument(
|
|
|
|
|
"--remote-url",
|
|
|
|
|
required=True,
|
|
|
|
|
help="Remote MCP HTTP base URL to set as default for Asset Store builds.",
|
|
|
|
|
)
|
2026-01-07 13:33:20 +08:00
|
|
|
parser.add_argument(
|
|
|
|
|
"--backup",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Backup existing Assets/MCPForUnity before replacing.",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--dry-run",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Only validate that operations would succeed; do not write/copy/delete.",
|
|
|
|
|
)
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
repo_root = Path(args.repo_root).expanduser().resolve()
|
|
|
|
|
asset_project = Path(args.asset_project).expanduser().resolve(
|
|
|
|
|
) if args.asset_project else (repo_root / "TestProjects" / "AssetStoreUploads")
|
2026-01-31 09:31:23 +08:00
|
|
|
remote_url = args.remote_url.strip()
|
|
|
|
|
if not remote_url:
|
|
|
|
|
raise RuntimeError("--remote-url must be a non-empty URL")
|
2026-01-07 13:33:20 +08:00
|
|
|
|
|
|
|
|
source_mcp = repo_root / "MCPForUnity"
|
|
|
|
|
if not source_mcp.is_dir():
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
f"Source MCPForUnity folder not found: {source_mcp}")
|
|
|
|
|
|
|
|
|
|
assets_dir = asset_project / "Assets"
|
|
|
|
|
if not assets_dir.is_dir():
|
|
|
|
|
raise RuntimeError(f"Assets folder not found: {assets_dir}")
|
|
|
|
|
|
|
|
|
|
dest_mcp = assets_dir / "MCPForUnity"
|
|
|
|
|
|
|
|
|
|
if args.dry_run:
|
|
|
|
|
print("[dry-run] Validated paths. No changes applied.")
|
|
|
|
|
print("[dry-run] Would stage a temporary copy of MCPForUnity and apply Asset Store edits there.")
|
|
|
|
|
print(
|
|
|
|
|
f"[dry-run] Would replace:\n- {dest_mcp}\n with\n- {source_mcp}")
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
# 1) Stage a temporary copy of MCPForUnity and apply Asset Store-specific edits there.
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix="mcpforunity_assetstore_") as tmpdir:
|
|
|
|
|
staged_mcp = Path(tmpdir) / "MCPForUnity"
|
|
|
|
|
shutil.copytree(source_mcp, staged_mcp)
|
|
|
|
|
|
|
|
|
|
setup_service = staged_mcp / "Editor" / "Setup" / "SetupWindowService.cs"
|
|
|
|
|
menu_file = staged_mcp / "Editor" / "MenuItems" / "MCPForUnityMenu.cs"
|
|
|
|
|
http_util = staged_mcp / "Editor" / "Helpers" / "HttpEndpointUtility.cs"
|
|
|
|
|
connection_section = staged_mcp / "Editor" / "Windows" / \
|
|
|
|
|
"Components" / "Connection" / "McpConnectionSection.cs"
|
|
|
|
|
|
|
|
|
|
for f in (setup_service, menu_file, http_util, connection_section):
|
|
|
|
|
if not f.is_file():
|
|
|
|
|
raise RuntimeError(f"Expected file not found: {f}")
|
|
|
|
|
|
|
|
|
|
# Remove auto-popup setup window for Asset Store packaging
|
|
|
|
|
remove_line_exact(setup_service, "[InitializeOnLoad]")
|
|
|
|
|
|
2026-01-31 09:31:23 +08:00
|
|
|
# Set default remote base URL to the hosted endpoint
|
2026-01-07 13:33:20 +08:00
|
|
|
replace_once(
|
|
|
|
|
http_util,
|
2026-01-31 09:31:23 +08:00
|
|
|
r'private const string DefaultRemoteBaseUrl = "";',
|
|
|
|
|
f'private const string DefaultRemoteBaseUrl = "{remote_url}";',
|
2026-01-07 13:33:20 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Default transport to HTTP Remote and persist inferred scope when missing
|
|
|
|
|
replace_once(
|
|
|
|
|
connection_section,
|
|
|
|
|
r'transportDropdown\.Init\(TransportProtocol\.HTTPLocal\);',
|
|
|
|
|
'transportDropdown.Init(TransportProtocol.HTTPRemote);',
|
|
|
|
|
)
|
2026-01-31 09:31:23 +08:00
|
|
|
replace_once(
|
|
|
|
|
connection_section,
|
|
|
|
|
r'scope = MCPServiceLocator\.Server\.IsLocalUrl\(\) \? "local" : "remote";',
|
|
|
|
|
'scope = "remote";',
|
|
|
|
|
)
|
2026-01-07 13:33:20 +08:00
|
|
|
|
|
|
|
|
# 2) Replace Assets/MCPForUnity in the target project
|
|
|
|
|
if dest_mcp.exists():
|
|
|
|
|
if args.backup:
|
|
|
|
|
backup_root = asset_project / "AssetStoreBackups"
|
|
|
|
|
backup_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
backup_path = backup_dir(dest_mcp, backup_root)
|
|
|
|
|
print(f"Backed up existing folder to: {backup_path}")
|
|
|
|
|
|
|
|
|
|
shutil.rmtree(dest_mcp)
|
|
|
|
|
|
|
|
|
|
shutil.copytree(staged_mcp, dest_mcp)
|
|
|
|
|
|
|
|
|
|
print("Done.")
|
|
|
|
|
print(f"- Source (unchanged): {source_mcp}")
|
|
|
|
|
print(f"- Updated Asset Store project folder: {dest_mcp}")
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|