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
32 changes: 32 additions & 0 deletions MCPForUnity/Editor/Tools/JsonUtil.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Newtonsoft.Json.Linq;
using UnityEngine;

namespace MCPForUnity.Editor.Tools
{
internal static class JsonUtil
{
/// <summary>
/// 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.
/// </summary>
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}");
}
}
}
}
}


3 changes: 3 additions & 0 deletions MCPForUnity/Editor/Tools/ManageAsset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions MCPForUnity/Editor/Tools/ManageGameObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ public static object HandleCommand(JObject @params)
bool includeNonPublicSerialized = @params["includeNonPublicSerialized"]?.ToObject<bool>() ?? 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;
Expand Down
6 changes: 3 additions & 3 deletions MCPForUnity/UnityMcpServer~/src/port_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,16 @@ 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:
s.settimeout(PortDiscovery.CONNECT_TIMEOUT)
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
Expand Down
9 changes: 9 additions & 0 deletions MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {}
Expand Down
13 changes: 11 additions & 2 deletions MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from typing import Annotated, Any, Literal

from fastmcp import Context
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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}"}
Comment on lines +118 to +124
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Also validate parsed type is a dict.

Parsing a valid JSON array/string would slip through; ensure the result is a dict before proceeding.

 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}"}
+    if component_properties is not None and not isinstance(component_properties, dict):
+        return {"success": False, "message": "component_properties must be a JSON object (dict)."}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 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}"}
# 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}"}
if component_properties is not None and not isinstance(component_properties, dict):
return {"success": False, "message": "component_properties must be a JSON object (dict)."}
🤖 Prompt for AI Agents
In MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py around lines 118
to 124, the code JSON-decodes component_properties but doesn't verify the
decoded type; after json.loads succeeds, check that component_properties is a
dict (isinstance(component_properties, dict)) and if it is not, return a failure
response (e.g., {"success": False, "message": "component_properties must be a
JSON object/dict"}) instead of proceeding; keep the existing JSONDecodeError
handling and log unchanged but add a distinct log/return for non-dict parsed
values.

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:
Expand Down
99 changes: 88 additions & 11 deletions MCPForUnity/UnityMcpServer~/src/uv.lock

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.

Original file line number Diff line number Diff line change
Expand Up @@ -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<McpTypes>("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));
Expand Down
Loading