Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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.2
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
86 changes: 66 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,20 +51,66 @@ 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
if isinstance(value, list) and len(value) == 3:
try:
return [float(value[0]), float(value[1]), float(value[2])]
except Exception:
return default
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]
parts = [p.strip() for p in s.split(",")]
if len(parts) == 3:
try:
return [float(parts[0]), float(parts[1]), float(parts[2])]
except Exception:
return default
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:
# Validate parameter usage to prevent silent failures
if action == "find":
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[str,
"Build index for load/build settings actions (pass as quoted string, e.g., '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
4 changes: 2 additions & 2 deletions 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 All @@ -10,7 +10,7 @@
description="Manages shader scripts in Unity (create, read, update, delete)."
)
def manage_shader(
ctx: Context,
ctx: Context,x
action: Annotated[Literal['create', 'read', 'update', 'delete'], "Perform CRUD operations on shader scripts."],
name: Annotated[str, "Shader name (no .cs extension)"],
path: Annotated[str, "Asset path (default: \"Assets/\")"],
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[str, "Max messages to return (pass as quoted string, e.g., '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