diff --git a/MCPForUnity/Editor/Tools/BatchExecute.cs b/MCPForUnity/Editor/Tools/BatchExecute.cs index aca4a1994..dbd1279ee 100644 --- a/MCPForUnity/Editor/Tools/BatchExecute.cs +++ b/MCPForUnity/Editor/Tools/BatchExecute.cs @@ -84,7 +84,7 @@ public static async Task HandleCommand(JObject @params) continue; } - string toolName = commandObj["tool"]?.ToString(); + string toolName = StripMcpPrefix(commandObj["tool"]?.ToString()); var rawParams = commandObj["params"] as JObject ?? new JObject(); var commandParams = NormalizeParameterKeys(rawParams); @@ -214,6 +214,32 @@ private static bool DetermineCallSucceeded(object result) return true; } + /// + /// Strip MCP server namespace prefix from a tool name. + /// Clients may send prefixed names like "UnityMCP:manage_gameobject" (colon format) + /// or "mcp__UnityMCP__manage_gameobject" (double-underscore, Claude Code style). + /// + internal static string StripMcpPrefix(string toolName) + { + if (string.IsNullOrEmpty(toolName)) + return toolName; + + // Double-underscore format: "mcp__ServerName__tool_name" + if (toolName.StartsWith("mcp__", StringComparison.Ordinal)) + { + int secondSep = toolName.IndexOf("__", 5, StringComparison.Ordinal); + if (secondSep >= 0 && secondSep + 2 < toolName.Length) + return toolName.Substring(secondSep + 2); + } + + // Colon format: "ServerName:tool_name" + int colonIndex = toolName.LastIndexOf(':'); + if (colonIndex >= 0 && colonIndex + 1 < toolName.Length) + return toolName.Substring(colonIndex + 1); + + return toolName; + } + private static JObject NormalizeParameterKeys(JObject source) { if (source == null) diff --git a/Server/src/services/tools/batch_execute.py b/Server/src/services/tools/batch_execute.py index 3849dae2a..71f8d65e4 100644 --- a/Server/src/services/tools/batch_execute.py +++ b/Server/src/services/tools/batch_execute.py @@ -24,6 +24,27 @@ _cached_max_commands: int | None = None +def strip_mcp_prefix(tool_name: str) -> str: + """Strip MCP server namespace prefix from a tool name. + + Clients may send prefixed tool names like: + - "UnityMCP:manage_gameobject" (colon-separated) + - "mcp__UnityMCP__manage_gameobject" (double-underscore, Claude Code style) + This normalizes them to the short form ("manage_gameobject"). + """ + # Double-underscore format: "mcp__ServerName__tool_name" + if tool_name.startswith("mcp__") and "__" in tool_name[5:]: + suffix = tool_name.split("__", 2)[-1] + if suffix: + return suffix + # Colon format: "ServerName:tool_name" + if ":" in tool_name: + suffix = tool_name.rsplit(":", 1)[-1] + if suffix: + return suffix + return tool_name + + async def _get_max_commands_from_editor_state(ctx: Context) -> int: """ Attempt to read the configured batch limit from the Unity editor state. @@ -110,6 +131,8 @@ async def batch_execute( raise ValueError( f"Command at index {index} is missing a valid 'tool' name") + tool_name = strip_mcp_prefix(tool_name) + if params is None: params = {} if not isinstance(params, dict): diff --git a/Server/tests/test_batch_strip_prefix.py b/Server/tests/test_batch_strip_prefix.py new file mode 100644 index 000000000..019513e54 --- /dev/null +++ b/Server/tests/test_batch_strip_prefix.py @@ -0,0 +1,33 @@ +"""Tests for strip_mcp_prefix in batch_execute.""" + +import pytest +from services.tools.batch_execute import strip_mcp_prefix + + +@pytest.mark.parametrize( + "input_name,expected", + [ + # Plain names pass through unchanged + ("manage_gameobject", "manage_gameobject"), + ("batch_execute", "batch_execute"), + # Colon format (e.g. Claude Desktop) + ("UnityMCP:manage_gameobject", "manage_gameobject"), + ("unityMCP:batch_execute", "batch_execute"), + # Double-underscore format (e.g. Claude Code) + ("mcp__UnityMCP__manage_gameobject", "manage_gameobject"), + ("mcp__UnityMCP__batch_execute", "batch_execute"), + ("mcp__UnityMCP__manage_scene", "manage_scene"), + # Tool name itself contains underscores — should be preserved + ("mcp__UnityMCP__manage_scriptable_object", "manage_scriptable_object"), + # Edge cases: trailing separators pass through unchanged (match C# behavior) + (":", ":"), + ("Server:", "Server:"), + ("mcp__Server__", "mcp__Server__"), + # Empty string passes through + ("", ""), + # Multiple colons: last segment used + ("a:b:manage_gameobject", "manage_gameobject"), + ], +) +def test_strip_mcp_prefix(input_name: str, expected: str): + assert strip_mcp_prefix(input_name) == expected diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BatchExecuteStripPrefixTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BatchExecuteStripPrefixTests.cs new file mode 100644 index 000000000..5d64e3631 --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BatchExecuteStripPrefixTests.cs @@ -0,0 +1,52 @@ +using NUnit.Framework; +using MCPForUnity.Editor.Tools; + +namespace MCPForUnityTests.Editor.Tools +{ + public class BatchExecuteStripPrefixTests + { + [Test] + public void StripMcpPrefix_PlainName_ReturnsUnchanged() + { + Assert.AreEqual("manage_gameobject", BatchExecute.StripMcpPrefix("manage_gameobject")); + } + + [Test] + public void StripMcpPrefix_ColonFormat_StripsPrefix() + { + Assert.AreEqual("manage_gameobject", BatchExecute.StripMcpPrefix("UnityMCP:manage_gameobject")); + } + + [Test] + public void StripMcpPrefix_DoubleUnderscoreFormat_StripsPrefix() + { + Assert.AreEqual("manage_gameobject", BatchExecute.StripMcpPrefix("mcp__UnityMCP__manage_gameobject")); + } + + [Test] + public void StripMcpPrefix_DoubleUnderscore_ToolWithUnderscores_PreservesToolName() + { + Assert.AreEqual("batch_execute", BatchExecute.StripMcpPrefix("mcp__UnityMCP__batch_execute")); + } + + [Test] + public void StripMcpPrefix_NullOrEmpty_ReturnsSame() + { + Assert.IsNull(BatchExecute.StripMcpPrefix(null)); + Assert.AreEqual("", BatchExecute.StripMcpPrefix("")); + } + + [Test] + public void StripMcpPrefix_ColonOnly_PassesThrough() + { + // Edge case: ":" alone — no valid suffix, passes through (will fail at CommandRegistry) + Assert.AreEqual(":", BatchExecute.StripMcpPrefix(":")); + } + + [Test] + public void StripMcpPrefix_MultipleColons_UsesLast() + { + Assert.AreEqual("manage_gameobject", BatchExecute.StripMcpPrefix("a:b:manage_gameobject")); + } + } +} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BatchExecuteStripPrefixTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BatchExecuteStripPrefixTests.cs.meta new file mode 100644 index 000000000..84e59077f --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BatchExecuteStripPrefixTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9424b9fc98b6cb947a6a6c7ffadc9d98 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: