diff --git a/MCPForUnity/Editor/Tools/JsonUtil.cs b/MCPForUnity/Editor/Tools/JsonUtil.cs
new file mode 100644
index 00000000..00cbe3f6
--- /dev/null
+++ b/MCPForUnity/Editor/Tools/JsonUtil.cs
@@ -0,0 +1,32 @@
+using Newtonsoft.Json.Linq;
+using UnityEngine;
+
+namespace MCPForUnity.Editor.Tools
+{
+ internal static class JsonUtil
+ {
+ ///
+ /// If @params[paramName] is a JSON string, parse it to a JObject in-place.
+ /// Logs a warning on parse failure and leaves the original value.
+ ///
+ internal static void CoerceJsonStringParameter(JObject @params, string paramName)
+ {
+ if (@params == null || string.IsNullOrEmpty(paramName)) return;
+ var token = @params[paramName];
+ if (token != null && token.Type == JTokenType.String)
+ {
+ try
+ {
+ var parsed = JObject.Parse(token.ToString());
+ @params[paramName] = parsed;
+ }
+ catch (Newtonsoft.Json.JsonReaderException e)
+ {
+ Debug.LogWarning($"[MCP] Could not parse '{paramName}' JSON string: {e.Message}");
+ }
+ }
+ }
+ }
+}
+
+
diff --git a/MCPForUnity/Editor/Tools/ManageAsset.cs b/MCPForUnity/Editor/Tools/ManageAsset.cs
index 1a952f37..dbf49f2a 100644
--- a/MCPForUnity/Editor/Tools/ManageAsset.cs
+++ b/MCPForUnity/Editor/Tools/ManageAsset.cs
@@ -63,6 +63,9 @@ public static object HandleCommand(JObject @params)
// Common parameters
string path = @params["path"]?.ToString();
+ // Coerce string JSON to JObject for 'properties' if provided as a JSON string
+ JsonUtil.CoerceJsonStringParameter(@params, "properties");
+
try
{
switch (action)
diff --git a/MCPForUnity/Editor/Tools/ManageGameObject.cs b/MCPForUnity/Editor/Tools/ManageGameObject.cs
index 40504a87..b6219ec1 100644
--- a/MCPForUnity/Editor/Tools/ManageGameObject.cs
+++ b/MCPForUnity/Editor/Tools/ManageGameObject.cs
@@ -66,6 +66,9 @@ public static object HandleCommand(JObject @params)
bool includeNonPublicSerialized = @params["includeNonPublicSerialized"]?.ToObject() ?? true; // Default to true
// --- End add parameter ---
+ // Coerce string JSON to JObject for 'componentProperties' if provided as a JSON string
+ JsonUtil.CoerceJsonStringParameter(@params, "componentProperties");
+
// --- Prefab Redirection Check ---
string targetPath =
targetToken?.Type == JTokenType.String ? targetToken.ToString() : null;
diff --git a/MCPForUnity/UnityMcpServer~/src/port_discovery.py b/MCPForUnity/UnityMcpServer~/src/port_discovery.py
index b936f967..c759e745 100644
--- a/MCPForUnity/UnityMcpServer~/src/port_discovery.py
+++ b/MCPForUnity/UnityMcpServer~/src/port_discovery.py
@@ -56,7 +56,7 @@ def list_candidate_files() -> List[Path]:
@staticmethod
def _try_probe_unity_mcp(port: int) -> bool:
"""Quickly check if a MCP for Unity listener is on this port.
- Tries a short TCP connect, sends 'ping', expects a JSON 'pong'.
+ Tries a short TCP connect, sends 'ping', expects Unity bridge welcome message.
"""
try:
with socket.create_connection(("127.0.0.1", port), PortDiscovery.CONNECT_TIMEOUT) as s:
@@ -64,8 +64,8 @@ def _try_probe_unity_mcp(port: int) -> bool:
try:
s.sendall(b"ping")
data = s.recv(512)
- # Minimal validation: look for a success pong response
- if data and b'"message":"pong"' in data:
+ # Check for Unity bridge welcome message format
+ if data and (b"WELCOME UNITY-MCP" in data or b'"message":"pong"' in data):
return True
except Exception:
return False
diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py
index 2d449206..4810daba 100644
--- a/MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py
+++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py
@@ -2,6 +2,7 @@
Defines the manage_asset tool for interacting with Unity assets.
"""
import asyncio
+import json
from typing import Annotated, Any, Literal
from fastmcp import Context
@@ -33,6 +34,14 @@ async def manage_asset(
page_number: Annotated[int | float | str, "Page number for pagination"] | None = None
) -> dict[str, Any]:
ctx.info(f"Processing manage_asset: {action}")
+ # Coerce 'properties' from JSON string to dict for client compatibility
+ if isinstance(properties, str):
+ try:
+ properties = json.loads(properties)
+ ctx.info("manage_asset: coerced properties from JSON string to dict")
+ except json.JSONDecodeError as e:
+ ctx.warn(f"manage_asset: failed to parse properties JSON string: {e}")
+ # Leave properties as-is; Unity side may handle defaults
# Ensure properties is a dict if None
if properties is None:
properties = {}
diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py
index 18caa1f5..f74d142b 100644
--- a/MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py
+++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py
@@ -1,3 +1,4 @@
+import json
from typing import Annotated, Any, Literal
from fastmcp import Context
@@ -42,8 +43,9 @@ def manage_gameobject(
layer: Annotated[str, "Layer name"] | None = None,
components_to_remove: Annotated[list[str],
"List of component names to remove"] | None = None,
- component_properties: Annotated[dict[str, dict[str, Any]],
+ component_properties: Annotated[dict[str, dict[str, Any]] | str,
"""Dictionary of component names to their properties to set. For example:
+ Can also be provided as a JSON string representation of the dict.
`{"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}}` assigns GameObject
`{"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}}` assigns Component
Example set nested property:
@@ -65,7 +67,7 @@ def manage_gameobject(
"Controls whether serialization of private [SerializeField] fields is included (accepts true/false or 'true'/'false')"] | None = None,
) -> dict[str, Any]:
ctx.info(f"Processing manage_gameobject: {action}")
-
+
# Coercers to tolerate stringified booleans and vectors
def _coerce_bool(value, default=None):
if value is None:
@@ -113,6 +115,13 @@ def _to_vec3(parts):
search_inactive = _coerce_bool(search_inactive)
includeNonPublicSerialized = _coerce_bool(includeNonPublicSerialized)
+ # Coerce 'component_properties' from JSON string to dict for client compatibility
+ if isinstance(component_properties, str):
+ try:
+ component_properties = json.loads(component_properties)
+ ctx.info("manage_gameobject: coerced component_properties from JSON string to dict")
+ except json.JSONDecodeError as e:
+ return {"success": False, "message": f"Invalid JSON in component_properties: {e}"}
try:
# Map tag to search_term when search_method is by_tag for backward compatibility
if action == "find" and search_method == "by_tag" and tag is not None and search_term is None:
diff --git a/MCPForUnity/UnityMcpServer~/src/uv.lock b/MCPForUnity/UnityMcpServer~/src/uv.lock
index b6d29e27..f10f1782 100644
--- a/MCPForUnity/UnityMcpServer~/src/uv.lock
+++ b/MCPForUnity/UnityMcpServer~/src/uv.lock
@@ -1,5 +1,5 @@
version = 1
-revision = 2
+revision = 3
requires-python = ">=3.10"
[[package]]
@@ -35,6 +35,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
]
+[[package]]
+name = "backports-asyncio-runner"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" },
+]
+
[[package]]
name = "certifi"
version = "2025.1.31"
@@ -132,6 +141,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
[[package]]
name = "jsonschema"
version = "4.25.1"
@@ -173,7 +191,7 @@ wheels = [
[[package]]
name = "mcp"
-version = "1.17.0"
+version = "1.18.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -188,9 +206,9 @@ dependencies = [
{ name = "starlette" },
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/5a/79/5724a540df19e192e8606c543cdcf162de8eb435077520cca150f7365ec0/mcp-1.17.0.tar.gz", hash = "sha256:1b57fabf3203240ccc48e39859faf3ae1ccb0b571ff798bbedae800c73c6df90", size = 477951, upload-time = "2025-10-10T12:16:44.519Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1a/e0/fe34ce16ea2bacce489ab859abd1b47ae28b438c3ef60b9c5eee6c02592f/mcp-1.18.0.tar.gz", hash = "sha256:aa278c44b1efc0a297f53b68df865b988e52dd08182d702019edcf33a8e109f6", size = 482926, upload-time = "2025-10-16T19:19:55.125Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1c/72/3751feae343a5ad07959df713907b5c3fbaed269d697a14b0c449080cf2e/mcp-1.17.0-py3-none-any.whl", hash = "sha256:0660ef275cada7a545af154db3082f176cf1d2681d5e35ae63e014faf0a35d40", size = 167737, upload-time = "2025-10-10T12:16:42.863Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/44/f5970e3e899803823826283a70b6003afd46f28e082544407e24575eccd3/mcp-1.18.0-py3-none-any.whl", hash = "sha256:42f10c270de18e7892fdf9da259029120b1ea23964ff688248c69db9d72b1d0a", size = 168762, upload-time = "2025-10-16T19:19:53.2Z" },
]
[package.optional-dependencies]
@@ -201,7 +219,7 @@ cli = [
[[package]]
name = "mcpforunityserver"
-version = "6.0.0"
+version = "6.2.1"
source = { editable = "." }
dependencies = [
{ name = "httpx" },
@@ -210,13 +228,22 @@ dependencies = [
{ name = "tomli" },
]
+[package.optional-dependencies]
+dev = [
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+]
+
[package.metadata]
requires-dist = [
{ name = "httpx", specifier = ">=0.27.2" },
{ name = "mcp", extras = ["cli"], specifier = ">=1.17.0" },
{ name = "pydantic", specifier = ">=2.12.0" },
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
+ { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" },
{ name = "tomli", specifier = ">=2.3.0" },
]
+provides-extras = ["dev"]
[[package]]
name = "mdurl"
@@ -227,6 +254,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
[[package]]
name = "pydantic"
version = "2.12.0"
@@ -374,13 +419,45 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
]
+[[package]]
+name = "pytest"
+version = "8.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
+]
+
+[[package]]
+name = "pytest-asyncio"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
+ { name = "pytest" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" },
+]
+
[[package]]
name = "python-dotenv"
-version = "1.0.1"
+version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115, upload-time = "2024-01-23T06:33:00.505Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863, upload-time = "2024-01-23T06:32:58.246Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
]
[[package]]
@@ -671,7 +748,7 @@ wheels = [
[[package]]
name = "typer"
-version = "0.19.2"
+version = "0.20.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
@@ -679,9 +756,9 @@ dependencies = [
{ name = "shellingham" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/8f/28/7c85c8032b91dbe79725b6f17d2fffc595dff06a35c7a30a37bef73a1ab4/typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37", size = 106492, upload-time = "2025-10-20T17:03:49.445Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" },
+ { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028, upload-time = "2025-10-20T17:03:47.617Z" },
]
[[package]]
diff --git a/TestProjects/UnityMCPTests/Assets/Editor.meta b/TestProjects/UnityMCPTests/Assets/Materials.meta
similarity index 77%
rename from TestProjects/UnityMCPTests/Assets/Editor.meta
rename to TestProjects/UnityMCPTests/Assets/Materials.meta
index 79828f3a..7ad588cf 100644
--- a/TestProjects/UnityMCPTests/Assets/Editor.meta
+++ b/TestProjects/UnityMCPTests/Assets/Materials.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 46421b2ea84fe4b1a903e2483cff3958
+guid: bacdb2f03a45d448888245e6ac9cca1b
folderAsset: yes
DefaultImporter:
externalObjects: {}
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/WriteToConfigTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/WriteToConfigTests.cs
index f34130d2..e371d9a3 100644
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/WriteToConfigTests.cs
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/WriteToConfigTests.cs
@@ -134,7 +134,12 @@ public void DoesNotAddEnvOrDisabled_ForTrae()
var configPath = Path.Combine(_tempRoot, "trae.json");
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
- var client = new McpClient { name = "Trae", mcpType = McpTypes.Trae };
+ if (!Enum.TryParse("Trae", out var traeValue))
+ {
+ Assert.Ignore("McpTypes.Trae not available in this package version; skipping test.");
+ }
+
+ var client = new McpClient { name = "Trae", mcpType = traeValue };
InvokeWriteToConfig(configPath, client);
var root = JObject.Parse(File.ReadAllText(configPath));
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/MCPToolParameterTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/MCPToolParameterTests.cs
new file mode 100644
index 00000000..a5d3c208
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/MCPToolParameterTests.cs
@@ -0,0 +1,169 @@
+using NUnit.Framework;
+using UnityEngine;
+using UnityEngine.TestTools;
+using System.Collections;
+using UnityEditor;
+using Newtonsoft.Json.Linq;
+using MCPForUnity.Editor.Tools;
+using System;
+
+namespace Tests.EditMode
+{
+ ///
+ /// Tests specifically for MCP tool parameter handling issues.
+ /// These tests focus on the actual problems we encountered:
+ /// 1. JSON parameter parsing in manage_asset and manage_gameobject tools
+ /// 2. Material creation with properties through MCP tools
+ /// 3. Material assignment through MCP tools
+ ///
+ public class MCPToolParameterTests
+ {
+ private const string TempDir = "Assets/Temp/MCPToolParameterTests";
+
+ [SetUp]
+ public void SetUp()
+ {
+ if (!AssetDatabase.IsValidFolder("Assets/Temp"))
+ {
+ AssetDatabase.CreateFolder("Assets", "Temp");
+ }
+ if (!AssetDatabase.IsValidFolder(TempDir))
+ {
+ AssetDatabase.CreateFolder("Assets/Temp", "MCPToolParameterTests");
+ }
+ }
+ [Test]
+ public void Test_ManageAsset_ShouldAcceptJSONProperties()
+ {
+ var matPath = $"{TempDir}/JsonMat_{Guid.NewGuid().ToString("N")}.mat";
+
+ // Build params with properties as a JSON string (to be coerced)
+ var p = new JObject
+ {
+ ["action"] = "create",
+ ["path"] = matPath,
+ ["assetType"] = "Material",
+ ["properties"] = "{\"shader\": \"Universal Render Pipeline/Lit\", \"color\": [0,0,1,1]}"
+ };
+
+ try
+ {
+ var raw = ManageAsset.HandleCommand(p);
+ var result = raw as JObject ?? JObject.FromObject(raw);
+ Assert.IsNotNull(result, "Handler should return a JObject result");
+ Assert.IsTrue(result!.Value("success"), result.ToString());
+
+ var mat = AssetDatabase.LoadAssetAtPath(matPath);
+ Assert.IsNotNull(mat, "Material should be created at path");
+ if (mat.HasProperty("_Color"))
+ {
+ Assert.AreEqual(Color.blue, mat.GetColor("_Color"));
+ }
+ }
+ finally
+ {
+ if (AssetDatabase.LoadAssetAtPath(matPath) != null)
+ {
+ AssetDatabase.DeleteAsset(matPath);
+ }
+ AssetDatabase.Refresh();
+ }
+ }
+
+ [Test]
+ public void Test_ManageGameObject_ShouldAcceptJSONComponentProperties()
+ {
+ var matPath = $"{TempDir}/JsonMat_{Guid.NewGuid().ToString("N")}.mat";
+
+ // Create a material first (object-typed properties)
+ var createMat = new JObject
+ {
+ ["action"] = "create",
+ ["path"] = matPath,
+ ["assetType"] = "Material",
+ ["properties"] = new JObject { ["shader"] = "Universal Render Pipeline/Lit", ["color"] = new JArray(0,0,1,1) }
+ };
+ var createMatRes = ManageAsset.HandleCommand(createMat);
+ var createMatObj = createMatRes as JObject ?? JObject.FromObject(createMatRes);
+ Assert.IsTrue(createMatObj.Value("success"), createMatObj.ToString());
+
+ // Create a sphere
+ var createGo = new JObject { ["action"] = "create", ["name"] = "MCPParamTestSphere", ["primitiveType"] = "Sphere" };
+ var createGoRes = ManageGameObject.HandleCommand(createGo);
+ var createGoObj = createGoRes as JObject ?? JObject.FromObject(createGoRes);
+ Assert.IsTrue(createGoObj.Value("success"), createGoObj.ToString());
+
+ try
+ {
+ // Assign material via JSON string componentProperties (coercion path)
+ var compJsonObj = new JObject { ["MeshRenderer"] = new JObject { ["sharedMaterial"] = matPath } };
+ var compJson = compJsonObj.ToString(Newtonsoft.Json.Formatting.None);
+ var modify = new JObject
+ {
+ ["action"] = "modify",
+ ["target"] = "MCPParamTestSphere",
+ ["searchMethod"] = "by_name",
+ ["componentProperties"] = compJson
+ };
+ var raw = ManageGameObject.HandleCommand(modify);
+ var result = raw as JObject ?? JObject.FromObject(raw);
+ Assert.IsTrue(result.Value("success"), result.ToString());
+ }
+ finally
+ {
+ var go = GameObject.Find("MCPParamTestSphere");
+ if (go != null) UnityEngine.Object.DestroyImmediate(go);
+ if (AssetDatabase.LoadAssetAtPath(matPath) != null) AssetDatabase.DeleteAsset(matPath);
+ AssetDatabase.Refresh();
+ }
+ }
+
+ [Test]
+ public void Test_JSONParsing_ShouldWorkInMCPTools()
+ {
+ var matPath = $"{TempDir}/JsonMat_{Guid.NewGuid().ToString("N")}.mat";
+
+ // manage_asset with JSON string properties (coercion path)
+ var createMat = new JObject
+ {
+ ["action"] = "create",
+ ["path"] = matPath,
+ ["assetType"] = "Material",
+ ["properties"] = "{\"shader\": \"Universal Render Pipeline/Lit\", \"color\": [0,0,1,1]}"
+ };
+ var createResRaw = ManageAsset.HandleCommand(createMat);
+ var createRes = createResRaw as JObject ?? JObject.FromObject(createResRaw);
+ Assert.IsTrue(createRes.Value("success"), createRes.ToString());
+
+ // Create sphere and assign material (object-typed componentProperties)
+ var go = new JObject { ["action"] = "create", ["name"] = "MCPParamJSONSphere", ["primitiveType"] = "Sphere" };
+ var goRes = ManageGameObject.HandleCommand(go);
+ var goObj = goRes as JObject ?? JObject.FromObject(goRes);
+ Assert.IsTrue(goObj.Value("success"), goObj.ToString());
+
+ try
+ {
+ var compJsonObj = new JObject { ["MeshRenderer"] = new JObject { ["sharedMaterial"] = matPath } };
+ var compJson = compJsonObj.ToString(Newtonsoft.Json.Formatting.None);
+ var modify = new JObject
+ {
+ ["action"] = "modify",
+ ["target"] = "MCPParamJSONSphere",
+ ["searchMethod"] = "by_name",
+ ["componentProperties"] = compJson
+ };
+ var modResRaw = ManageGameObject.HandleCommand(modify);
+ var modRes = modResRaw as JObject ?? JObject.FromObject(modResRaw);
+ Assert.IsTrue(modRes.Value("success"), modRes.ToString());
+ }
+ finally
+ {
+ var goObj2 = GameObject.Find("MCPParamJSONSphere");
+ if (goObj2 != null) UnityEngine.Object.DestroyImmediate(goObj2);
+ if (AssetDatabase.LoadAssetAtPath(matPath) != null) AssetDatabase.DeleteAsset(matPath);
+ AssetDatabase.Refresh();
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/MCPToolParameterTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/MCPToolParameterTests.cs.meta
new file mode 100644
index 00000000..36eb52b6
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/MCPToolParameterTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 80144860477bb4293acf4669566b27b8
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialParameterToolTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialParameterToolTests.cs
new file mode 100644
index 00000000..f37cbf90
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialParameterToolTests.cs
@@ -0,0 +1,198 @@
+using System;
+using System.IO;
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using UnityEditor;
+using UnityEngine;
+using MCPForUnity.Editor.Tools;
+
+namespace MCPForUnityTests.Editor.Tools
+{
+ public class MaterialParameterToolTests
+ {
+ private const string TempRoot = "Assets/Temp/MaterialParameterToolTests";
+ private string _matPath; // unique per test run
+ private GameObject _sphere;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _matPath = $"{TempRoot}/BlueURP_{Guid.NewGuid().ToString("N")}.mat";
+ if (!AssetDatabase.IsValidFolder("Assets/Temp"))
+ {
+ AssetDatabase.CreateFolder("Assets", "Temp");
+ }
+ if (!AssetDatabase.IsValidFolder(TempRoot))
+ {
+ AssetDatabase.CreateFolder("Assets/Temp", "MaterialParameterToolTests");
+ }
+ // Ensure any leftover material from previous runs is removed
+ if (AssetDatabase.LoadAssetAtPath(_matPath) != null)
+ {
+ AssetDatabase.DeleteAsset(_matPath);
+ AssetDatabase.Refresh();
+ }
+ // Hard-delete any stray files on disk (in case GUID lookup fails)
+ var abs = Path.Combine(Directory.GetCurrentDirectory(), _matPath);
+ try
+ {
+ if (File.Exists(abs)) File.Delete(abs);
+ if (File.Exists(abs + ".meta")) File.Delete(abs + ".meta");
+ }
+ catch { /* best-effort cleanup */ }
+ AssetDatabase.Refresh();
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ if (_sphere != null)
+ {
+ UnityEngine.Object.DestroyImmediate(_sphere);
+ _sphere = null;
+ }
+ if (AssetDatabase.LoadAssetAtPath(_matPath) != null)
+ {
+ AssetDatabase.DeleteAsset(_matPath);
+ }
+ AssetDatabase.Refresh();
+ }
+
+ private static JObject ToJObject(object result)
+ {
+ return result as JObject ?? JObject.FromObject(result);
+ }
+
+ [Test]
+ public void CreateMaterial_WithObjectProperties_SucceedsAndSetsColor()
+ {
+ // Ensure a clean state if a previous run left the asset behind (uses _matPath now)
+ if (AssetDatabase.LoadAssetAtPath(_matPath) != null)
+ {
+ AssetDatabase.DeleteAsset(_matPath);
+ AssetDatabase.Refresh();
+ }
+ var createParams = new JObject
+ {
+ ["action"] = "create",
+ ["path"] = _matPath,
+ ["assetType"] = "Material",
+ ["properties"] = new JObject
+ {
+ ["shader"] = "Universal Render Pipeline/Lit",
+ ["color"] = new JArray(0f, 0f, 1f, 1f)
+ }
+ };
+
+ var result = ToJObject(ManageAsset.HandleCommand(createParams));
+ Assert.IsTrue(result.Value("success"), result.Value("error"));
+
+ var mat = AssetDatabase.LoadAssetAtPath(_matPath);
+ Assert.IsNotNull(mat, "Material should exist at path.");
+ // Verify color if shader exposes _Color
+ if (mat.HasProperty("_Color"))
+ {
+ Assert.AreEqual(Color.blue, mat.GetColor("_Color"));
+ }
+ }
+
+ private void CreateTestMaterial()
+ {
+ var createParams = new JObject
+ {
+ ["action"] = "create",
+ ["path"] = _matPath,
+ ["assetType"] = "Material",
+ ["properties"] = new JObject
+ {
+ ["shader"] = "Universal Render Pipeline/Lit",
+ ["color"] = new JArray(0f, 0f, 1f, 1f)
+ }
+ };
+ var result = ToJObject(ManageAsset.HandleCommand(createParams));
+ Assert.IsTrue(result.Value("success"), result.Value("error"));
+ }
+
+ private void CreateSphere()
+ {
+ var createGo = new JObject
+ {
+ ["action"] = "create",
+ ["name"] = "ToolTestSphere",
+ ["primitiveType"] = "Sphere"
+ };
+ var createGoResult = ToJObject(ManageGameObject.HandleCommand(createGo));
+ Assert.IsTrue(createGoResult.Value("success"), createGoResult.Value("error"));
+ _sphere = GameObject.Find("ToolTestSphere");
+ Assert.IsNotNull(_sphere, "Sphere should be created.");
+ }
+
+ [Test]
+ public void AssignMaterial_ToSphere_UsingComponentPropertiesObject_Succeeds()
+ {
+ CreateTestMaterial();
+ CreateSphere();
+
+ // Create a sphere via handler
+
+
+ // Assign material via object-typed componentProperties
+ var modifyParams = new JObject
+ {
+ ["action"] = "modify",
+ ["target"] = "ToolTestSphere",
+ ["searchMethod"] = "by_name",
+ ["componentProperties"] = new JObject
+ {
+ ["MeshRenderer"] = new JObject
+ {
+ ["sharedMaterial"] = _matPath
+ }
+ }
+ };
+
+ var modifyResult = ToJObject(ManageGameObject.HandleCommand(modifyParams));
+ Assert.IsTrue(modifyResult.Value("success"), modifyResult.Value("error"));
+
+ var renderer = _sphere.GetComponent();
+ Assert.IsNotNull(renderer, "Sphere should have MeshRenderer.");
+ Assert.IsNotNull(renderer.sharedMaterial, "sharedMaterial should be assigned.");
+ StringAssert.StartsWith("BlueURP_", renderer.sharedMaterial.name);
+ }
+
+ [Test]
+ public void ReadRendererData_DoesNotInstantiateMaterial_AndIncludesSharedMaterial()
+ {
+ CreateTestMaterial();
+ CreateSphere();
+ var modifyParams = new JObject
+ {
+ ["action"] = "modify",
+ ["target"] = "ToolTestSphere",
+ ["searchMethod"] = "by_name",
+ ["componentProperties"] = new JObject
+ {
+ ["MeshRenderer"] = new JObject { ["sharedMaterial"] = _matPath }
+ }
+ };
+ var modifyResult = ToJObject(ManageGameObject.HandleCommand(modifyParams));
+ Assert.IsTrue(modifyResult.Value("success"), modifyResult.Value("error"));
+
+ var renderer = _sphere.GetComponent();
+ int beforeId = renderer.sharedMaterial != null ? renderer.sharedMaterial.GetInstanceID() : 0;
+
+ var data = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(renderer) as System.Collections.Generic.Dictionary;
+ Assert.IsNotNull(data, "Serializer should return data.");
+
+ int afterId = renderer.sharedMaterial != null ? renderer.sharedMaterial.GetInstanceID() : 0;
+ Assert.AreEqual(beforeId, afterId, "sharedMaterial instance must not change (no instantiation in EditMode).");
+
+ if (data.TryGetValue("properties", out var propsObj) && propsObj is System.Collections.Generic.Dictionary props)
+ {
+ Assert.IsTrue(
+ props.ContainsKey("sharedMaterial") || props.ContainsKey("material") || props.ContainsKey("sharedMaterials") || props.ContainsKey("materials"),
+ "Serialized data should include material info.");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialParameterToolTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialParameterToolTests.cs.meta
new file mode 100644
index 00000000..266ae453
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialParameterToolTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bd76b616a816c47a79c4a3da4c307cff
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/TestProjects/UnityMCPTests/Packages/manifest.json b/TestProjects/UnityMCPTests/Packages/manifest.json
index b8bbe318..0bd78a67 100644
--- a/TestProjects/UnityMCPTests/Packages/manifest.json
+++ b/TestProjects/UnityMCPTests/Packages/manifest.json
@@ -1,6 +1,7 @@
{
"dependencies": {
"com.coplaydev.unity-mcp": "file:../../../MCPForUnity",
+ "com.unity.ai.navigation": "1.1.4",
"com.unity.collab-proxy": "2.5.2",
"com.unity.feature.development": "1.0.1",
"com.unity.ide.rider": "3.0.31",
@@ -9,7 +10,7 @@
"com.unity.ide.windsurf": "https://github.com/Asuta/com.unity.ide.windsurf.git",
"com.unity.test-framework": "1.1.33",
"com.unity.textmeshpro": "3.0.6",
- "com.unity.timeline": "1.6.5",
+ "com.unity.timeline": "1.7.5",
"com.unity.ugui": "1.0.0",
"com.unity.visualscripting": "1.9.4",
"com.unity.modules.ai": "1.0.0",
diff --git a/TestProjects/UnityMCPTests/ProjectSettings/Packages/com.unity.testtools.codecoverage/Settings.json b/TestProjects/UnityMCPTests/ProjectSettings/Packages/com.unity.testtools.codecoverage/Settings.json
index ad11087f..3c7b4c18 100644
--- a/TestProjects/UnityMCPTests/ProjectSettings/Packages/com.unity.testtools.codecoverage/Settings.json
+++ b/TestProjects/UnityMCPTests/ProjectSettings/Packages/com.unity.testtools.codecoverage/Settings.json
@@ -1,6 +1,4 @@
{
- "m_Name": "Settings",
- "m_Path": "ProjectSettings/Packages/com.unity.testtools.codecoverage/Settings.json",
"m_Dictionary": {
"m_DictionaryValues": []
}
diff --git a/TestProjects/UnityMCPTests/ProjectSettings/ProjectVersion.txt b/TestProjects/UnityMCPTests/ProjectSettings/ProjectVersion.txt
index 1a62a673..105db72a 100644
--- a/TestProjects/UnityMCPTests/ProjectSettings/ProjectVersion.txt
+++ b/TestProjects/UnityMCPTests/ProjectSettings/ProjectVersion.txt
@@ -1,2 +1,2 @@
-m_EditorVersion: 2021.3.45f2
-m_EditorVersionWithRevision: 2021.3.45f2 (88f88f591b2e)
+m_EditorVersion: 2022.3.6f1
+m_EditorVersionWithRevision: 2022.3.6f1 (b9e6e7e9fa2d)