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
5 changes: 3 additions & 2 deletions MCPForUnity/UnityMcpServer~/src/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
[project]
name = "MCPForUnityServer"
version = "6.2.1"
version = "6.2.2"
description = "MCP for Unity Server: A Unity package for Unity Editor integration via the Model Context Protocol (MCP)."
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"httpx>=0.27.2",
"mcp[cli]>=1.17.0",
"fastmcp>=2.12.5",
"mcp>=1.16.0",
"pydantic>=2.12.0",
"tomli>=2.3.0",
]
Expand Down
2 changes: 1 addition & 1 deletion MCPForUnity/UnityMcpServer~/src/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging
from pathlib import Path

from mcp.server.fastmcp import FastMCP
from fastmcp import FastMCP
from telemetry_decorator import telemetry_resource

from registry import get_registered_resources
Expand Down
2 changes: 1 addition & 1 deletion MCPForUnity/UnityMcpServer~/src/server.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from telemetry import record_telemetry, record_milestone, RecordType, MilestoneType
from mcp.server.fastmcp import FastMCP
from fastmcp import FastMCP
import logging
from logging.handlers import RotatingFileHandler
import os
Expand Down
2 changes: 1 addition & 1 deletion MCPForUnity/UnityMcpServer~/src/server_version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6.2.1
6.2.3
2 changes: 1 addition & 1 deletion MCPForUnity/UnityMcpServer~/src/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging
from pathlib import Path

from mcp.server.fastmcp import FastMCP
from fastmcp import FastMCP
from telemetry_decorator import telemetry_tool

from registry import get_registered_tools
Expand Down
2 changes: 1 addition & 1 deletion MCPForUnity/UnityMcpServer~/src/tools/execute_menu_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""
from typing import Annotated, Any

from mcp.server.fastmcp import Context
from fastmcp import Context

from models import MCPResponse
from registry import mcp_for_unity_tool
Expand Down
6 changes: 3 additions & 3 deletions MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import asyncio
from typing import Annotated, Any, Literal

from mcp.server.fastmcp import Context
from fastmcp import Context
from registry import mcp_for_unity_tool
from unity_connection import async_send_command_with_retry

Expand All @@ -29,8 +29,8 @@ async def manage_asset(
filter_type: Annotated[str, "Filter type for search"] | None = None,
filter_date_after: Annotated[str,
"Date after which to filter"] | None = None,
page_size: Annotated[int, "Page size for pagination"] | None = None,
page_number: Annotated[int, "Page number for pagination"] | None = None
page_size: Annotated[int | float | str, "Page size for pagination"] | None = None,
page_number: Annotated[int | float | str, "Page number for pagination"] | None = None
) -> dict[str, Any]:
ctx.info(f"Processing manage_asset: {action}")
# Ensure properties is a dict if None
Expand Down
25 changes: 21 additions & 4 deletions MCPForUnity/UnityMcpServer~/src/tools/manage_editor.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
from typing import Annotated, Any, Literal

from mcp.server.fastmcp import Context
from fastmcp import Context
from registry import mcp_for_unity_tool
from telemetry import is_telemetry_enabled, record_tool_usage
from unity_connection import send_command_with_retry


@mcp_for_unity_tool(
description="Controls and queries the Unity editor's state and settings"
description="Controls and queries the Unity editor's state and settings. Tip: pass booleans as true/false; if your client only sends strings, 'true'/'false' are accepted."
)
def manage_editor(
ctx: Context,
action: Annotated[Literal["telemetry_status", "telemetry_ping", "play", "pause", "stop", "get_state", "get_project_root", "get_windows",
"get_active_tool", "get_selection", "get_prefab_stage", "set_active_tool", "add_tag", "remove_tag", "get_tags", "add_layer", "remove_layer", "get_layers"], "Get and update the Unity Editor state."],
wait_for_completion: Annotated[bool,
"Optional. If True, waits for certain actions"] | None = None,
wait_for_completion: Annotated[bool | str,
"Optional. If True, waits for certain actions (accepts true/false or 'true'/'false')"] | None = None,
tool_name: Annotated[str,
"Tool name when setting active tool"] | None = None,
tag_name: Annotated[str,
Expand All @@ -23,6 +23,23 @@ def manage_editor(
"Layer name when adding and removing layers"] | None = None,
) -> dict[str, Any]:
ctx.info(f"Processing manage_editor: {action}")

# Coerce boolean parameters defensively to tolerate 'true'/'false' strings
def _coerce_bool(value, default=None):
if value is None:
return default
if isinstance(value, bool):
return value
if isinstance(value, str):
v = value.strip().lower()
if v in ("true", "1", "yes", "on"): # common truthy strings
return True
if v in ("false", "0", "no", "off"):
return False
return bool(value)

wait_for_completion = _coerce_bool(wait_for_completion)

try:
# Diagnostics: quick telemetry checks
if action == "telemetry_status":
Expand Down
92 changes: 72 additions & 20 deletions MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from typing import Annotated, Any, Literal

from mcp.server.fastmcp import Context
from fastmcp import Context
from registry import mcp_for_unity_tool
from unity_connection import send_command_with_retry


@mcp_for_unity_tool(
description="Manage GameObjects. Note: for 'get_components', the `data` field contains a dictionary of component names and their serialized properties. For 'get_component', specify 'component_name' to retrieve only that component's serialized data."
description="Manage GameObjects. For booleans, send true/false; if your client only sends strings, 'true'/'false' are accepted. Vectors may be [x,y,z] or a string like '[x,y,z]'. For 'get_components', the `data` field contains a dictionary of component names and their serialized properties. For 'get_component', specify 'component_name' to retrieve only that component's serialized data."
)
def manage_gameobject(
ctx: Context,
Expand All @@ -21,24 +21,24 @@ def manage_gameobject(
"Tag name - used for both 'create' (initial tag) and 'modify' (change tag)"] | None = None,
parent: Annotated[str,
"Parent GameObject reference - used for both 'create' (initial parent) and 'modify' (change parent)"] | None = None,
position: Annotated[list[float],
"Position - used for both 'create' (initial position) and 'modify' (change position)"] | None = None,
rotation: Annotated[list[float],
"Rotation - used for both 'create' (initial rotation) and 'modify' (change rotation)"] | None = None,
scale: Annotated[list[float],
"Scale - used for both 'create' (initial scale) and 'modify' (change scale)"] | None = None,
position: Annotated[list[float] | str,
"Position - [x,y,z] or string '[x,y,z]' for client compatibility"] | None = None,
rotation: Annotated[list[float] | str,
"Rotation - [x,y,z] or string '[x,y,z]' for client compatibility"] | None = None,
scale: Annotated[list[float] | str,
"Scale - [x,y,z] or string '[x,y,z]' for client compatibility"] | None = None,
components_to_add: Annotated[list[str],
"List of component names to add"] | None = None,
primitive_type: Annotated[str,
"Primitive type for 'create' action"] | None = None,
save_as_prefab: Annotated[bool,
"If True, saves the created GameObject as a prefab"] | None = None,
save_as_prefab: Annotated[bool | str,
"If True, saves the created GameObject as a prefab (accepts true/false or 'true'/'false')"] | None = None,
prefab_path: Annotated[str, "Path for prefab creation"] | None = None,
prefab_folder: Annotated[str,
"Folder for prefab creation"] | None = None,
# --- Parameters for 'modify' ---
set_active: Annotated[bool,
"If True, sets the GameObject active"] | None = None,
set_active: Annotated[bool | str,
"If True, sets the GameObject active (accepts true/false or 'true'/'false')"] | None = None,
layer: Annotated[str, "Layer name"] | None = None,
components_to_remove: Annotated[list[str],
"List of component names to remove"] | None = None,
Expand All @@ -51,21 +51,73 @@ def manage_gameobject(
# --- Parameters for 'find' ---
search_term: Annotated[str,
"Search term for 'find' action ONLY. Use this (not 'name') when searching for GameObjects."] | None = None,
find_all: Annotated[bool,
"If True, finds all GameObjects matching the search term"] | None = None,
search_in_children: Annotated[bool,
"If True, searches in children of the GameObject"] | None = None,
search_inactive: Annotated[bool,
"If True, searches inactive GameObjects"] | None = None,
find_all: Annotated[bool | str,
"If True, finds all GameObjects matching the search term (accepts true/false or 'true'/'false')"] | None = None,
search_in_children: Annotated[bool | str,
"If True, searches in children of the GameObject (accepts true/false or 'true'/'false')"] | None = None,
search_inactive: Annotated[bool | str,
"If True, searches inactive GameObjects (accepts true/false or 'true'/'false')"] | None = None,
# -- Component Management Arguments --
component_name: Annotated[str,
"Component name for 'add_component' and 'remove_component' actions"] | None = None,
# Controls whether serialization of private [SerializeField] fields is included
includeNonPublicSerialized: Annotated[bool,
"Controls whether serialization of private [SerializeField] fields is included"] | None = None,
includeNonPublicSerialized: Annotated[bool | str,
"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:
return default
if isinstance(value, bool):
return value
if isinstance(value, str):
v = value.strip().lower()
if v in ("true", "1", "yes", "on"):
return True
if v in ("false", "0", "no", "off"):
return False
return bool(value)

def _coerce_vec(value, default=None):
if value is None:
return default
import math
def _to_vec3(parts):
try:
vec = [float(parts[0]), float(parts[1]), float(parts[2])]
except (ValueError, TypeError):
return default
return vec if all(math.isfinite(n) for n in vec) else default
if isinstance(value, list) and len(value) == 3:
return _to_vec3(value)
if isinstance(value, str):
s = value.strip()
# minimal tolerant parse for "[x,y,z]" or "x,y,z"
if s.startswith("[") and s.endswith("]"):
s = s[1:-1]
# support "x,y,z" and "x y z"
parts = [p.strip() for p in (s.split(",") if "," in s else s.split())]
if len(parts) == 3:
return _to_vec3(parts)
return default

position = _coerce_vec(position, default=position)
rotation = _coerce_vec(rotation, default=rotation)
scale = _coerce_vec(scale, default=scale)
save_as_prefab = _coerce_bool(save_as_prefab)
set_active = _coerce_bool(set_active)
find_all = _coerce_bool(find_all)
search_in_children = _coerce_bool(search_in_children)
search_inactive = _coerce_bool(search_inactive)
includeNonPublicSerialized = _coerce_bool(includeNonPublicSerialized)

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:
search_term = tag

# Validate parameter usage to prevent silent failures
if action == "find":
if name is not None:
Expand Down
2 changes: 1 addition & 1 deletion MCPForUnity/UnityMcpServer~/src/tools/manage_prefabs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Annotated, Any, Literal

from mcp.server.fastmcp import Context
from fastmcp import Context
from registry import mcp_for_unity_tool
from unity_connection import send_command_with_retry

Expand Down
8 changes: 4 additions & 4 deletions MCPForUnity/UnityMcpServer~/src/tools/manage_scene.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
from typing import Annotated, Literal, Any

from mcp.server.fastmcp import Context
from fastmcp import Context
from registry import mcp_for_unity_tool
from unity_connection import send_command_with_retry


@mcp_for_unity_tool(description="Manage Unity scenes")
@mcp_for_unity_tool(description="Manage Unity scenes. Tip: For broad client compatibility, pass build_index as a quoted string (e.g., '0').")
def manage_scene(
ctx: Context,
action: Annotated[Literal["create", "load", "save", "get_hierarchy", "get_active", "get_build_settings"], "Perform CRUD operations on Unity scenes."],
name: Annotated[str,
"Scene name. Not required get_active/get_build_settings"] | None = None,
path: Annotated[str,
"Asset path for scene operations (default: 'Assets/')"] | None = None,
build_index: Annotated[int,
"Build index for load/build settings actions"] | None = None,
build_index: Annotated[int | str,
"Build index for load/build settings actions (accepts int or string, e.g., 0 or '0')"] | None = None,
) -> dict[str, Any]:
ctx.info(f"Processing manage_scene: {action}")
try:
Expand Down
2 changes: 1 addition & 1 deletion MCPForUnity/UnityMcpServer~/src/tools/manage_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Annotated, Any, Literal
from urllib.parse import urlparse, unquote

from mcp.server.fastmcp import FastMCP, Context
from fastmcp import FastMCP, Context

from registry import mcp_for_unity_tool
import unity_connection
Expand Down
2 changes: 1 addition & 1 deletion MCPForUnity/UnityMcpServer~/src/tools/manage_shader.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import base64
from typing import Annotated, Any, Literal

from mcp.server.fastmcp import Context
from fastmcp import Context
from registry import mcp_for_unity_tool
from unity_connection import send_command_with_retry

Expand Down
26 changes: 20 additions & 6 deletions MCPForUnity/UnityMcpServer~/src/tools/read_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,48 @@
"""
from typing import Annotated, Any, Literal

from mcp.server.fastmcp import Context
from fastmcp import Context
from registry import mcp_for_unity_tool
from unity_connection import send_command_with_retry


@mcp_for_unity_tool(
description="Gets messages from or clears the Unity Editor console."
description="Gets messages from or clears the Unity Editor console. Note: For maximum client compatibility, pass count as a quoted string (e.g., '5')."
)
def read_console(
ctx: Context,
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,
count: Annotated[int | str, "Max messages to return (accepts int or string, e.g., 5 or '5')"] | None = None,
filter_text: Annotated[str, "Text filter for messages"] | None = None,
since_timestamp: Annotated[str,
"Get messages after this timestamp (ISO 8601)"] | None = None,
format: Annotated[Literal['plain', 'detailed',
'json'], "Output format"] | None = None,
include_stacktrace: Annotated[bool,
"Include stack traces in output"] | None = None
include_stacktrace: Annotated[bool | str,
"Include stack traces in output (accepts true/false or 'true'/'false')"] | None = None
) -> dict[str, Any]:
ctx.info(f"Processing read_console: {action}")
# Set defaults if values are None
action = action if action is not None else 'get'
types = types if types is not None else ['error', 'warning', 'log']
format = format if format is not None else 'detailed'
include_stacktrace = include_stacktrace if include_stacktrace is not None else True
# Coerce booleans defensively (strings like 'true'/'false')
def _coerce_bool(value, default=None):
if value is None:
return default
if isinstance(value, bool):
return value
if isinstance(value, str):
v = value.strip().lower()
if v in ("true", "1", "yes", "on"):
return True
if v in ("false", "0", "no", "off"):
return False
return bool(value)

include_stacktrace = _coerce_bool(include_stacktrace, True)

# Normalize action if it's a string
if isinstance(action, str):
Expand Down
Loading