Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ TestProjects/UnityMCPTests/Packages/packages-lock.json
# Backup artifacts
*.backup
*.backup.meta

.wt-origin-main/
6 changes: 6 additions & 0 deletions MCPForUnity/UnityMcpServer~/src/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ dependencies = [
"tomli>=2.3.0",
]

[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-anyio>=0.6.0",
]

[build-system]
requires = ["setuptools>=64.0.0", "wheel"]
build-backend = "setuptools.build_meta"
Expand Down
16 changes: 10 additions & 6 deletions MCPForUnity/UnityMcpServer~/src/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,11 @@ def __init__(self):
"""
server_config = None
for modname in (
# Prefer plain module to respect test-time overrides and sys.path injection
"config",
"src.config",
"MCPForUnity.UnityMcpServer~.src.config",
"MCPForUnity.UnityMcpServer.src.config",
Comment on lines +97 to 100
Copy link

Copilot AI Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module resolution order change is significant but lacks explanation. Add a comment explaining why plain 'config' is now preferred over package-qualified paths, especially regarding the test-time override behavior mentioned in the comment.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment added to explain

"src.config",
"config",
):
try:
mod = importlib.import_module(modname)
Expand All @@ -116,10 +117,13 @@ def __init__(self):
server_config, "telemetry_endpoint", None)
default_ep = cfg_default or "https://api-prod.coplay.dev/telemetry/events"
self.default_endpoint = default_ep
self.endpoint = self._validated_endpoint(
os.environ.get("UNITY_MCP_TELEMETRY_ENDPOINT", default_ep),
default_ep,
)
# Prefer config default; allow explicit env override only when set
env_ep = os.environ.get("UNITY_MCP_TELEMETRY_ENDPOINT")
if env_ep is not None and env_ep != "":
self.endpoint = self._validated_endpoint(env_ep, default_ep)
else:
# Validate config-provided default as well to enforce scheme/host rules
self.endpoint = self._validated_endpoint(default_ep, default_ep)
try:
logger.info(
"Telemetry configured: endpoint=%s (default=%s), timeout_env=%s",
Expand Down
18 changes: 9 additions & 9 deletions MCPForUnity/UnityMcpServer~/src/tools/manage_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from mcp.server.fastmcp import FastMCP, Context

from registry import mcp_for_unity_tool
from unity_connection import send_command_with_retry
import unity_connection


def _split_uri(uri: str) -> tuple[str, str]:
Expand Down Expand Up @@ -103,7 +103,7 @@ def _needs_normalization(arr: list[dict[str, Any]]) -> bool:
warnings: list[str] = []
if _needs_normalization(edits):
# Read file to support index->line/col conversion when needed
read_resp = send_command_with_retry("manage_script", {
read_resp = unity_connection.send_command_with_retry("manage_script", {
"action": "read",
"name": name,
"path": directory,
Expand Down Expand Up @@ -304,7 +304,7 @@ def _le(a: tuple[int, int], b: tuple[int, int]) -> bool:
"options": opts,
}
params = {k: v for k, v in params.items() if v is not None}
resp = send_command_with_retry("manage_script", params)
resp = unity_connection.send_command_with_retry("manage_script", params)
if isinstance(resp, dict):
data = resp.setdefault("data", {})
data.setdefault("normalizedEdits", normalized_edits)
Expand Down Expand Up @@ -336,7 +336,7 @@ def _flip_async():
st = _latest_status()
if st and st.get("reloading"):
return
send_command_with_retry(
unity_connection.send_command_with_retry(
"execute_menu_item",
{"menuPath": "MCP/Flip Reload Sentinel"},
max_retries=0,
Expand Down Expand Up @@ -386,7 +386,7 @@ def create_script(
contents.encode("utf-8")).decode("utf-8")
params["contentsEncoded"] = True
params = {k: v for k, v in params.items() if v is not None}
resp = send_command_with_retry("manage_script", params)
resp = unity_connection.send_command_with_retry("manage_script", params)
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}


Expand All @@ -401,7 +401,7 @@ def delete_script(
if not directory or directory.split("/")[0].lower() != "assets":
return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."}
params = {"action": "delete", "name": name, "path": directory}
resp = send_command_with_retry("manage_script", params)
resp = unity_connection.send_command_with_retry("manage_script", params)
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}


Expand All @@ -426,7 +426,7 @@ def validate_script(
"path": directory,
"level": level,
}
resp = send_command_with_retry("manage_script", params)
resp = unity_connection.send_command_with_retry("manage_script", params)
if isinstance(resp, dict) and resp.get("success"):
diags = resp.get("data", {}).get("diagnostics", []) or []
warnings = sum(1 for d in diags if str(
Expand Down Expand Up @@ -473,7 +473,7 @@ def manage_script(

params = {k: v for k, v in params.items() if v is not None}

response = send_command_with_retry("manage_script", params)
response = unity_connection.send_command_with_retry("manage_script", params)

if isinstance(response, dict):
if response.get("success"):
Expand Down Expand Up @@ -541,7 +541,7 @@ def get_sha(
try:
name, directory = _split_uri(uri)
params = {"action": "get_sha", "name": name, "path": directory}
resp = send_command_with_retry("manage_script", params)
resp = unity_connection.send_command_with_retry("manage_script", params)
if isinstance(resp, dict) and resp.get("success"):
data = resp.get("data", {})
minimal = {"sha256": data.get(
Expand Down
2 changes: 1 addition & 1 deletion MCPForUnity/UnityMcpServer~/src/tools/read_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
)
def read_console(
ctx: Context,
action: Annotated[Literal['get', 'clear'], "Get or clear the Unity Editor console."],
action: Annotated[Literal['get', 'clear'], "Get or clear the Unity Editor console."] | None = None,
types: Annotated[list[Literal['error', 'warning',
'log', 'all']], "Message types to get"] | None = None,
count: Annotated[int, "Max messages to return"] | None = None,
Expand Down
8 changes: 8 additions & 0 deletions TestProjects/UnityMCPTests/Assets/Temp.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions UnityMcpBridge/UnityMcpServer~/src/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
def pytest_ignore_collect(path, config):
# Avoid duplicate import mismatches between Bridge and MCPForUnity copies
p = str(path)
return p.endswith("test_telemetry.py")
18 changes: 9 additions & 9 deletions UnityMcpBridge/UnityMcpServer~/src/tools/manage_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from mcp.server.fastmcp import FastMCP, Context

from registry import mcp_for_unity_tool
from unity_connection import send_command_with_retry
import unity_connection


def _split_uri(uri: str) -> tuple[str, str]:
Expand Down Expand Up @@ -103,7 +103,7 @@ def _needs_normalization(arr: list[dict[str, Any]]) -> bool:
warnings: list[str] = []
if _needs_normalization(edits):
# Read file to support index->line/col conversion when needed
read_resp = send_command_with_retry("manage_script", {
read_resp = unity_connection.send_command_with_retry("manage_script", {
"action": "read",
"name": name,
"path": directory,
Expand Down Expand Up @@ -304,7 +304,7 @@ def _le(a: tuple[int, int], b: tuple[int, int]) -> bool:
"options": opts,
}
params = {k: v for k, v in params.items() if v is not None}
resp = send_command_with_retry("manage_script", params)
resp = unity_connection.send_command_with_retry("manage_script", params)
if isinstance(resp, dict):
data = resp.setdefault("data", {})
data.setdefault("normalizedEdits", normalized_edits)
Expand Down Expand Up @@ -336,7 +336,7 @@ def _flip_async():
st = _latest_status()
if st and st.get("reloading"):
return
send_command_with_retry(
unity_connection.send_command_with_retry(
"execute_menu_item",
{"menuPath": "MCP/Flip Reload Sentinel"},
max_retries=0,
Expand Down Expand Up @@ -386,7 +386,7 @@ def create_script(
contents.encode("utf-8")).decode("utf-8")
params["contentsEncoded"] = True
params = {k: v for k, v in params.items() if v is not None}
resp = send_command_with_retry("manage_script", params)
resp = unity_connection.send_command_with_retry("manage_script", params)
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}


Expand All @@ -401,7 +401,7 @@ def delete_script(
if not directory or directory.split("/")[0].lower() != "assets":
return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."}
params = {"action": "delete", "name": name, "path": directory}
resp = send_command_with_retry("manage_script", params)
resp = unity_connection.send_command_with_retry("manage_script", params)
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}


Expand All @@ -426,7 +426,7 @@ def validate_script(
"path": directory,
"level": level,
}
resp = send_command_with_retry("manage_script", params)
resp = unity_connection.send_command_with_retry("manage_script", params)
if isinstance(resp, dict) and resp.get("success"):
diags = resp.get("data", {}).get("diagnostics", []) or []
warnings = sum(1 for d in diags if str(
Expand Down Expand Up @@ -473,7 +473,7 @@ def manage_script(

params = {k: v for k, v in params.items() if v is not None}

response = send_command_with_retry("manage_script", params)
response = unity_connection.send_command_with_retry("manage_script", params)

if isinstance(response, dict):
if response.get("success"):
Expand Down Expand Up @@ -541,7 +541,7 @@ def get_sha(
try:
name, directory = _split_uri(uri)
params = {"action": "get_sha", "name": name, "path": directory}
resp = send_command_with_retry("manage_script", params)
resp = unity_connection.send_command_with_retry("manage_script", params)
if isinstance(resp, dict) and resp.get("success"):
data = resp.get("data", {})
minimal = {"sha256": data.get(
Expand Down
30 changes: 30 additions & 0 deletions docs/README-DEV.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,36 @@

Welcome to the MCP for Unity development environment! This directory contains tools and utilities to streamline MCP for Unity core development.

## 🛠️ Development Setup

### Installing Development Dependencies

To contribute or run tests, you need to install the development dependencies:

```bash
# Navigate to the server source directory
cd MCPForUnity/UnityMcpServer~/src

# Install the package in editable mode with dev dependencies
pip install -e .[dev]
```

This installs:
- **Runtime dependencies**: `httpx`, `mcp[cli]`, `pydantic`, `tomli`
- **Development dependencies**: `pytest`, `pytest-anyio`

### Running Tests

```bash
# From the repo root
pytest tests/ -v
```

Or if you prefer using Python module syntax:
```bash
python -m pytest tests/ -v
```

## 🚀 Available Development Features

### ✅ Development Deployment Scripts
Expand Down
4 changes: 4 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[pytest]
testpaths = tests
norecursedirs = UnityMcpBridge MCPForUnity

21 changes: 21 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,24 @@
os.environ.setdefault("DISABLE_TELEMETRY", "true")
os.environ.setdefault("UNITY_MCP_DISABLE_TELEMETRY", "true")
os.environ.setdefault("MCP_DISABLE_TELEMETRY", "true")

# Avoid collecting tests under the two 'src' package folders to prevent
# duplicate-package import conflicts (two different 'src' packages).
collect_ignore = [
"UnityMcpBridge/UnityMcpServer~/src",
"MCPForUnity/UnityMcpServer~/src",
]
collect_ignore_glob = [
"UnityMcpBridge/UnityMcpServer~/src/*",
"MCPForUnity/UnityMcpServer~/src/*",
]

def pytest_ignore_collect(path):
p = str(path)
norm = p.replace("\\", "/")
return (
"/UnityMcpBridge/UnityMcpServer~/src/" in norm
or "/MCPForUnity/UnityMcpServer~/src/" in norm
or norm.endswith("UnityMcpBridge/UnityMcpServer~/src")
or norm.endswith("MCPForUnity/UnityMcpServer~/src")
)
Loading