diff --git a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs index 03b753f..a5f0999 100644 --- a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs +++ b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs @@ -147,7 +147,15 @@ namespace UnityMcpBridge.Editor.Helpers { string packagePath = pkg.resolvedPath; // e.g., Library/PackageCache/... or local path - // Preferred: UnityMcpServer embedded alongside Editor/Runtime within the package + // Preferred: UnityMcpServer~ embedded alongside Editor/Runtime within the package (ignored by Unity import) + string embeddedTilde = Path.Combine(packagePath, "UnityMcpServer~", "src"); + if (Directory.Exists(embeddedTilde) && File.Exists(Path.Combine(embeddedTilde, "server.py"))) + { + srcPath = embeddedTilde; + return true; + } + + // Fallback: legacy non-tilde folder name inside the package string embedded = Path.Combine(packagePath, "UnityMcpServer", "src"); if (Directory.Exists(embedded) && File.Exists(Path.Combine(embedded, "server.py"))) { diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index f85aa6f..5dde570 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -1000,7 +1000,14 @@ namespace UnityMcpBridge.Editor.Windows { string packagePath = package.resolvedPath; - // Check for local package structure (UnityMcpServer/src) + // Preferred: check for tilde folder inside package + string packagedTildeDir = Path.Combine(packagePath, "UnityMcpServer~", "src"); + if (Directory.Exists(packagedTildeDir) && File.Exists(Path.Combine(packagedTildeDir, "server.py"))) + { + return packagedTildeDir; + } + + // Fallback: legacy local package structure (UnityMcpServer/src) string localPythonDir = Path.Combine(Path.GetDirectoryName(packagePath), "UnityMcpServer", "src"); if (Directory.Exists(localPythonDir) && File.Exists(Path.Combine(localPythonDir, "server.py"))) { diff --git a/UnityMcpBridge/UnityMcpServer.meta b/UnityMcpBridge/UnityMcpServer.meta deleted file mode 100644 index a391da2..0000000 --- a/UnityMcpBridge/UnityMcpServer.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 1f4e9c3e4a2b4e12a1b2c3d4e5f60789 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src.meta b/UnityMcpBridge/UnityMcpServer/src.meta deleted file mode 100644 index 8495be1..0000000 --- a/UnityMcpBridge/UnityMcpServer/src.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 661ad50b20643440fbed55a237c6db95 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/Dockerfile b/UnityMcpBridge/UnityMcpServer/src/Dockerfile deleted file mode 100644 index 3f884f3..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -FROM python:3.12-slim - -# Install required system dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ - git \ - && rm -rf /var/lib/apt/lists/* - -# Set working directory -WORKDIR /app - -# Install uv package manager -RUN pip install uv - -# Copy required files -COPY config.py /app/ -COPY server.py /app/ -COPY unity_connection.py /app/ -COPY pyproject.toml /app/ -COPY __init__.py /app/ -COPY tools/ /app/tools/ - -# Install dependencies using uv -RUN uv pip install --system -e . - - -# Command to run the server -CMD ["uv", "run", "server.py"] \ No newline at end of file diff --git a/UnityMcpBridge/UnityMcpServer/src/Dockerfile.meta b/UnityMcpBridge/UnityMcpServer/src/Dockerfile.meta deleted file mode 100644 index 8b821f0..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/Dockerfile.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 6fa88615288954da09edbaa8118d833d -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/__init__.py b/UnityMcpBridge/UnityMcpServer/src/__init__.py deleted file mode 100644 index 62e5cd1..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Unity MCP Server package. -""" \ No newline at end of file diff --git a/UnityMcpBridge/UnityMcpServer/src/__init__.py.meta b/UnityMcpBridge/UnityMcpServer/src/__init__.py.meta deleted file mode 100644 index 5cad7ab..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/__init__.py.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 59ba898760fd24167997d22d2705b8a4 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/config.py b/UnityMcpBridge/UnityMcpServer/src/config.py deleted file mode 100644 index 485b845..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/config.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -Configuration settings for the Unity MCP Server. -This file contains all configurable parameters for the server. -""" - -from dataclasses import dataclass - -@dataclass -class ServerConfig: - """Main configuration class for the MCP server.""" - - # Network settings - unity_host: str = "localhost" - unity_port: int = 6400 - mcp_port: int = 6500 - - # Connection settings - connection_timeout: float = 60.0 # default steady-state timeout; retries use shorter timeouts - buffer_size: int = 16 * 1024 * 1024 # 16MB buffer - - # Logging settings - log_level: str = "INFO" - log_format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - - # Server settings - max_retries: int = 8 - retry_delay: float = 0.5 - -# Create a global config instance -config = ServerConfig() \ No newline at end of file diff --git a/UnityMcpBridge/UnityMcpServer/src/config.py.meta b/UnityMcpBridge/UnityMcpServer/src/config.py.meta deleted file mode 100644 index 75f04e7..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/config.py.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 5516f911d79504c71976757e67ca228b -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/port_discovery.py b/UnityMcpBridge/UnityMcpServer/src/port_discovery.py deleted file mode 100644 index 9885533..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/port_discovery.py +++ /dev/null @@ -1,155 +0,0 @@ -""" -Port discovery utility for Unity MCP Server. - -What changed and why: -- Unity now writes a per-project port file named like - `~/.unity-mcp/unity-mcp-port-.json` to avoid projects overwriting - each other's saved port. The legacy file `unity-mcp-port.json` may still - exist. -- This module now scans for both patterns, prefers the most recently - modified file, and verifies that the port is actually a Unity MCP listener - (quick socket connect + ping) before choosing it. -""" - -import json -import os -import logging -from pathlib import Path -from typing import Optional, List -import glob -import socket - -logger = logging.getLogger("unity-mcp-server") - -class PortDiscovery: - """Handles port discovery from Unity Bridge registry""" - REGISTRY_FILE = "unity-mcp-port.json" # legacy single-project file - DEFAULT_PORT = 6400 - CONNECT_TIMEOUT = 0.3 # seconds, keep this snappy during discovery - - @staticmethod - def get_registry_path() -> Path: - """Get the path to the port registry file""" - return Path.home() / ".unity-mcp" / PortDiscovery.REGISTRY_FILE - - @staticmethod - def get_registry_dir() -> Path: - return Path.home() / ".unity-mcp" - - @staticmethod - def list_candidate_files() -> List[Path]: - """Return candidate registry files, newest first. - Includes hashed per-project files and the legacy file (if present). - """ - base = PortDiscovery.get_registry_dir() - hashed = sorted( - (Path(p) for p in glob.glob(str(base / "unity-mcp-port-*.json"))), - key=lambda p: p.stat().st_mtime, - reverse=True, - ) - legacy = PortDiscovery.get_registry_path() - if legacy.exists(): - # Put legacy at the end so hashed, per-project files win - hashed.append(legacy) - return hashed - - @staticmethod - def _try_probe_unity_mcp(port: int) -> bool: - """Quickly check if a Unity MCP listener is on this port. - Tries a short TCP connect, sends 'ping', expects a JSON 'pong'. - """ - 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: - return True - except Exception: - return False - except Exception: - return False - return False - - @staticmethod - def _read_latest_status() -> Optional[dict]: - try: - base = PortDiscovery.get_registry_dir() - status_files = sorted( - (Path(p) for p in glob.glob(str(base / "unity-mcp-status-*.json"))), - key=lambda p: p.stat().st_mtime, - reverse=True, - ) - if not status_files: - return None - with status_files[0].open('r') as f: - return json.load(f) - except Exception: - return None - - @staticmethod - def discover_unity_port() -> int: - """ - Discover Unity port by scanning per-project and legacy registry files. - Prefer the newest file whose port responds; fall back to first parsed - value; finally default to 6400. - - Returns: - Port number to connect to - """ - # Prefer the latest heartbeat status if it points to a responsive port - status = PortDiscovery._read_latest_status() - if status: - port = status.get('unity_port') - if isinstance(port, int) and PortDiscovery._try_probe_unity_mcp(port): - logger.info(f"Using Unity port from status: {port}") - return port - - candidates = PortDiscovery.list_candidate_files() - - first_seen_port: Optional[int] = None - - for path in candidates: - try: - with open(path, 'r') as f: - cfg = json.load(f) - unity_port = cfg.get('unity_port') - if isinstance(unity_port, int): - if first_seen_port is None: - first_seen_port = unity_port - if PortDiscovery._try_probe_unity_mcp(unity_port): - logger.info(f"Using Unity port from {path.name}: {unity_port}") - return unity_port - except Exception as e: - logger.warning(f"Could not read port registry {path}: {e}") - - if first_seen_port is not None: - logger.info(f"No responsive port found; using first seen value {first_seen_port}") - return first_seen_port - - # Fallback to default port - logger.info(f"No port registry found; using default port {PortDiscovery.DEFAULT_PORT}") - return PortDiscovery.DEFAULT_PORT - - @staticmethod - def get_port_config() -> Optional[dict]: - """ - Get the most relevant port configuration from registry. - Returns the most recent hashed file's config if present, - otherwise the legacy file's config. Returns None if nothing exists. - - Returns: - Port configuration dict or None if not found - """ - candidates = PortDiscovery.list_candidate_files() - if not candidates: - return None - for path in candidates: - try: - with open(path, 'r') as f: - return json.load(f) - except Exception as e: - logger.warning(f"Could not read port configuration {path}: {e}") - return None \ No newline at end of file diff --git a/UnityMcpBridge/UnityMcpServer/src/port_discovery.py.meta b/UnityMcpBridge/UnityMcpServer/src/port_discovery.py.meta deleted file mode 100644 index e792556..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/port_discovery.py.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 8d315755217ea4c36b221ac0461032ab -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/pyproject.toml b/UnityMcpBridge/UnityMcpServer/src/pyproject.toml deleted file mode 100644 index 2c05fb8..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/pyproject.toml +++ /dev/null @@ -1,15 +0,0 @@ -[project] -name = "UnityMcpServer" -version = "2.0.0" -description = "Unity MCP 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.4.1"] - -[build-system] -requires = ["setuptools>=64.0.0", "wheel"] -build-backend = "setuptools.build_meta" - -[tool.setuptools] -py-modules = ["config", "server", "unity_connection"] -packages = ["tools"] diff --git a/UnityMcpBridge/UnityMcpServer/src/pyproject.toml.meta b/UnityMcpBridge/UnityMcpServer/src/pyproject.toml.meta deleted file mode 100644 index 86408e1..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/pyproject.toml.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 66fbd8ab4fd094540ba73299b6a2424a -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/server.py b/UnityMcpBridge/UnityMcpServer/src/server.py deleted file mode 100644 index 55360b5..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/server.py +++ /dev/null @@ -1,73 +0,0 @@ -from mcp.server.fastmcp import FastMCP, Context, Image -import logging -from dataclasses import dataclass -from contextlib import asynccontextmanager -from typing import AsyncIterator, Dict, Any, List -from config import config -from tools import register_all_tools -from unity_connection import get_unity_connection, UnityConnection - -# Configure logging using settings from config -logging.basicConfig( - level=getattr(logging, config.log_level), - format=config.log_format -) -logger = logging.getLogger("unity-mcp-server") - -# Global connection state -_unity_connection: UnityConnection = None - -@asynccontextmanager -async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]: - """Handle server startup and shutdown.""" - global _unity_connection - logger.info("Unity MCP Server starting up") - try: - _unity_connection = get_unity_connection() - logger.info("Connected to Unity on startup") - except Exception as e: - logger.warning(f"Could not connect to Unity on startup: {str(e)}") - _unity_connection = None - try: - # Yield the connection object so it can be attached to the context - # The key 'bridge' matches how tools like read_console expect to access it (ctx.bridge) - yield {"bridge": _unity_connection} - finally: - if _unity_connection: - _unity_connection.disconnect() - _unity_connection = None - logger.info("Unity MCP Server shut down") - -# Initialize MCP server -mcp = FastMCP( - "unity-mcp-server", - description="Unity Editor integration via Model Context Protocol", - lifespan=server_lifespan -) - -# Register all tools -register_all_tools(mcp) - -# Asset Creation Strategy - -@mcp.prompt() -def asset_creation_strategy() -> str: - """Guide for discovering and using Unity MCP tools effectively.""" - return ( - "Available Unity MCP Server Tools:\\n\\n" - "- `manage_editor`: Controls editor state and queries info.\\n" - "- `execute_menu_item`: Executes Unity Editor menu items by path.\\n" - "- `read_console`: Reads or clears Unity console messages, with filtering options.\\n" - "- `manage_scene`: Manages scenes.\\n" - "- `manage_gameobject`: Manages GameObjects in the scene.\\n" - "- `manage_script`: Manages C# script files.\\n" - "- `manage_asset`: Manages prefabs and assets.\\n" - "- `manage_shader`: Manages shaders.\\n\\n" - "Tips:\\n" - "- Create prefabs for reusable GameObjects.\\n" - "- Always include a camera and main light in your scenes.\\n" - ) - -# Run the server -if __name__ == "__main__": - mcp.run(transport='stdio') diff --git a/UnityMcpBridge/UnityMcpServer/src/server.py.meta b/UnityMcpBridge/UnityMcpServer/src/server.py.meta deleted file mode 100644 index 4e1c95b..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/server.py.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 8ef892978afc74491b6cf65f40514e74 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/tools.meta b/UnityMcpBridge/UnityMcpServer/src/tools.meta deleted file mode 100644 index 0b8416a..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 205ac300b2209414f8b246354e853777 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/__init__.py b/UnityMcpBridge/UnityMcpServer/src/tools/__init__.py deleted file mode 100644 index 4d8d63c..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -from .manage_script import register_manage_script_tools -from .manage_scene import register_manage_scene_tools -from .manage_editor import register_manage_editor_tools -from .manage_gameobject import register_manage_gameobject_tools -from .manage_asset import register_manage_asset_tools -from .manage_shader import register_manage_shader_tools -from .read_console import register_read_console_tools -from .execute_menu_item import register_execute_menu_item_tools - -def register_all_tools(mcp): - """Register all refactored tools with the MCP server.""" - print("Registering Unity MCP Server refactored tools...") - register_manage_script_tools(mcp) - register_manage_scene_tools(mcp) - register_manage_editor_tools(mcp) - register_manage_gameobject_tools(mcp) - register_manage_asset_tools(mcp) - register_manage_shader_tools(mcp) - register_read_console_tools(mcp) - register_execute_menu_item_tools(mcp) - print("Unity MCP Server tool registration complete.") diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/__init__.py.meta b/UnityMcpBridge/UnityMcpServer/src/tools/__init__.py.meta deleted file mode 100644 index 56b0225..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/__init__.py.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 85da958dba57e47b9a2fa32a8abd61ef -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/execute_menu_item.py b/UnityMcpBridge/UnityMcpServer/src/tools/execute_menu_item.py deleted file mode 100644 index a4ebc67..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/execute_menu_item.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Defines the execute_menu_item tool for running Unity Editor menu commands. -""" -from typing import Dict, Any -from mcp.server.fastmcp import FastMCP, Context -from unity_connection import get_unity_connection # Import unity_connection module - -def register_execute_menu_item_tools(mcp: FastMCP): - """Registers the execute_menu_item tool with the MCP server.""" - - @mcp.tool() - async def execute_menu_item( - ctx: Context, - menu_path: str, - action: str = 'execute', - parameters: Dict[str, Any] = None, - ) -> Dict[str, Any]: - """Executes a Unity Editor menu item via its path (e.g., "File/Save Project"). - - Args: - ctx: The MCP context. - menu_path: The full path of the menu item to execute. - action: The operation to perform (default: 'execute'). - parameters: Optional parameters for the menu item (rarely used). - - Returns: - A dictionary indicating success or failure, with optional message/error. - """ - - action = action.lower() if action else 'execute' - - # Prepare parameters for the C# handler - params_dict = { - "action": action, - "menuPath": menu_path, - "parameters": parameters if parameters else {}, - } - - # Remove None values - params_dict = {k: v for k, v in params_dict.items() if v is not None} - - if "parameters" not in params_dict: - params_dict["parameters"] = {} # Ensure parameters dict exists - - # Get Unity connection and send the command - # We use the unity_connection module to communicate with Unity - unity_conn = get_unity_connection() - - # Send command to the ExecuteMenuItem C# handler - # The command type should match what the Unity side expects - return unity_conn.send_command("execute_menu_item", params_dict) \ No newline at end of file diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/execute_menu_item.py.meta b/UnityMcpBridge/UnityMcpServer/src/tools/execute_menu_item.py.meta deleted file mode 100644 index 16b394f..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/execute_menu_item.py.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 50ba0cffcdba2452a89ac372d67b4787 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/manage_asset.py b/UnityMcpBridge/UnityMcpServer/src/tools/manage_asset.py deleted file mode 100644 index dada66b..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/manage_asset.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -Defines the manage_asset tool for interacting with Unity assets. -""" -import asyncio # Added: Import asyncio for running sync code in async -from typing import Dict, Any -from mcp.server.fastmcp import FastMCP, Context -# from ..unity_connection import get_unity_connection # Original line that caused error -from unity_connection import get_unity_connection # Use absolute import relative to Python dir - -def register_manage_asset_tools(mcp: FastMCP): - """Registers the manage_asset tool with the MCP server.""" - - @mcp.tool() - async def manage_asset( - ctx: Context, - action: str, - path: str, - asset_type: str = None, - properties: Dict[str, Any] = None, - destination: str = None, - generate_preview: bool = False, - search_pattern: str = None, - filter_type: str = None, - filter_date_after: str = None, - page_size: int = None, - page_number: int = None - ) -> Dict[str, Any]: - """Performs asset operations (import, create, modify, delete, etc.) in Unity. - - Args: - ctx: The MCP context. - action: Operation to perform (e.g., 'import', 'create', 'modify', 'delete', 'duplicate', 'move', 'rename', 'search', 'get_info', 'create_folder', 'get_components'). - path: Asset path (e.g., "Materials/MyMaterial.mat") or search scope. - asset_type: Asset type (e.g., 'Material', 'Folder') - required for 'create'. - properties: Dictionary of properties for 'create'/'modify'. - example properties for Material: {"color": [1, 0, 0, 1], "shader": "Standard"}. - example properties for Texture: {"width": 1024, "height": 1024, "format": "RGBA32"}. - example properties for PhysicsMaterial: {"bounciness": 1.0, "staticFriction": 0.5, "dynamicFriction": 0.5}. - destination: Target path for 'duplicate'/'move'. - search_pattern: Search pattern (e.g., '*.prefab'). - filter_*: Filters for search (type, date). - page_*: Pagination for search. - - Returns: - A dictionary with operation results ('success', 'data', 'error'). - """ - # Ensure properties is a dict if None - if properties is None: - properties = {} - - # Prepare parameters for the C# handler - params_dict = { - "action": action.lower(), - "path": path, - "assetType": asset_type, - "properties": properties, - "destination": destination, - "generatePreview": generate_preview, - "searchPattern": search_pattern, - "filterType": filter_type, - "filterDateAfter": filter_date_after, - "pageSize": page_size, - "pageNumber": page_number - } - - # Remove None values to avoid sending unnecessary nulls - params_dict = {k: v for k, v in params_dict.items() if v is not None} - - # Get the current asyncio event loop - loop = asyncio.get_running_loop() - # Get the Unity connection instance - connection = get_unity_connection() - - # Run the synchronous send_command in the default executor (thread pool) - # This prevents blocking the main async event loop. - result = await loop.run_in_executor( - None, # Use default executor - connection.send_command, # The function to call - "manage_asset", # First argument for send_command - params_dict # Second argument for send_command - ) - # Return the result obtained from Unity - return result \ No newline at end of file diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/manage_asset.py.meta b/UnityMcpBridge/UnityMcpServer/src/tools/manage_asset.py.meta deleted file mode 100644 index e0372a4..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/manage_asset.py.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 7c0cfde2907ef4306b8a46c4b190f96a -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/manage_editor.py b/UnityMcpBridge/UnityMcpServer/src/tools/manage_editor.py deleted file mode 100644 index b256e6c..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/manage_editor.py +++ /dev/null @@ -1,53 +0,0 @@ -from mcp.server.fastmcp import FastMCP, Context -from typing import Dict, Any -from unity_connection import get_unity_connection - -def register_manage_editor_tools(mcp: FastMCP): - """Register all editor management tools with the MCP server.""" - - @mcp.tool() - def manage_editor( - ctx: Context, - action: str, - wait_for_completion: bool = None, - # --- Parameters for specific actions --- - tool_name: str = None, - tag_name: str = None, - layer_name: str = None, - ) -> Dict[str, Any]: - """Controls and queries the Unity editor's state and settings. - - Args: - action: Operation (e.g., 'play', 'pause', 'get_state', 'set_active_tool', 'add_tag'). - wait_for_completion: Optional. If True, waits for certain actions. - Action-specific arguments (e.g., tool_name, tag_name, layer_name). - - Returns: - Dictionary with operation results ('success', 'message', 'data'). - """ - try: - # Prepare parameters, removing None values - params = { - "action": action, - "waitForCompletion": wait_for_completion, - "toolName": tool_name, # Corrected parameter name to match C# - "tagName": tag_name, # Pass tag name - "layerName": layer_name, # Pass layer name - # Add other parameters based on the action being performed - # "width": width, - # "height": height, - # etc. - } - params = {k: v for k, v in params.items() if v is not None} - - # Send command to Unity - response = get_unity_connection().send_command("manage_editor", params) - - # Process response - if response.get("success"): - return {"success": True, "message": response.get("message", "Editor operation successful."), "data": response.get("data")} - else: - return {"success": False, "message": response.get("error", "An unknown error occurred during editor management.")} - - except Exception as e: - return {"success": False, "message": f"Python error managing editor: {str(e)}"} \ No newline at end of file diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/manage_editor.py.meta b/UnityMcpBridge/UnityMcpServer/src/tools/manage_editor.py.meta deleted file mode 100644 index 1f112d7..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/manage_editor.py.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 54f6646d00435410fb67cc17d095c977 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/manage_gameobject.py b/UnityMcpBridge/UnityMcpServer/src/tools/manage_gameobject.py deleted file mode 100644 index 83ab9c7..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/manage_gameobject.py +++ /dev/null @@ -1,138 +0,0 @@ -from mcp.server.fastmcp import FastMCP, Context -from typing import Dict, Any, List -from unity_connection import get_unity_connection - -def register_manage_gameobject_tools(mcp: FastMCP): - """Register all GameObject management tools with the MCP server.""" - - @mcp.tool() - def manage_gameobject( - ctx: Context, - action: str, - target: str = None, # GameObject identifier by name or path - search_method: str = None, - # --- Combined Parameters for Create/Modify --- - name: str = None, # Used for both 'create' (new object name) and 'modify' (rename) - tag: str = None, # Used for both 'create' (initial tag) and 'modify' (change tag) - parent: str = None, # Used for both 'create' (initial parent) and 'modify' (change parent) - position: List[float] = None, - rotation: List[float] = None, - scale: List[float] = None, - components_to_add: List[str] = None, # List of component names to add - primitive_type: str = None, - save_as_prefab: bool = False, - prefab_path: str = None, - prefab_folder: str = "Assets/Prefabs", - # --- Parameters for 'modify' --- - set_active: bool = None, - layer: str = None, # Layer name - components_to_remove: List[str] = None, - component_properties: Dict[str, Dict[str, Any]] = None, - # --- Parameters for 'find' --- - search_term: str = None, - find_all: bool = False, - search_in_children: bool = False, - search_inactive: bool = False, - # -- Component Management Arguments -- - component_name: str = None, - includeNonPublicSerialized: bool = None, # Controls serialization of private [SerializeField] fields - ) -> Dict[str, Any]: - """Manages GameObjects: create, modify, delete, find, and component operations. - - Args: - action: Operation (e.g., 'create', 'modify', 'find', 'add_component', 'remove_component', 'set_component_property', 'get_components'). - target: GameObject identifier (name or path string) for modify/delete/component actions. - search_method: How to find objects ('by_name', 'by_id', 'by_path', etc.). Used with 'find' and some 'target' lookups. - name: GameObject name - used for both 'create' (initial name) and 'modify' (rename). - tag: Tag name - used for both 'create' (initial tag) and 'modify' (change tag). - parent: Parent GameObject reference - used for both 'create' (initial parent) and 'modify' (change parent). - layer: Layer name - used for both 'create' (initial layer) and 'modify' (change layer). - component_properties: Dict mapping Component names to their properties to set. - Example: {"Rigidbody": {"mass": 10.0, "useGravity": True}}, - To set references: - - Use asset path string for Prefabs/Materials, e.g., {"MeshRenderer": {"material": "Assets/Materials/MyMat.mat"}} - - Use a dict for scene objects/components, e.g.: - {"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}} (assigns GameObject) - {"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}} (assigns Component) - Example set nested property: - - Access shared material: {"MeshRenderer": {"sharedMaterial.color": [1, 0, 0, 1]}} - components_to_add: List of component names to add. - Action-specific arguments (e.g., position, rotation, scale for create/modify; - component_name for component actions; - search_term, find_all for 'find'). - includeNonPublicSerialized: If True, includes private fields marked [SerializeField] in component data. - - Action-specific details: - - For 'get_components': - Required: target, search_method - Optional: includeNonPublicSerialized (defaults to True) - Returns all components on the target GameObject with their serialized data. - The search_method parameter determines how to find the target ('by_name', 'by_id', 'by_path'). - - Returns: - Dictionary with operation results ('success', 'message', 'data'). - For 'get_components', the 'data' field contains a dictionary of component names and their serialized properties. - """ - try: - # --- Early check for attempting to modify a prefab asset --- - # ---------------------------------------------------------- - - # Prepare parameters, removing None values - params = { - "action": action, - "target": target, - "searchMethod": search_method, - "name": name, - "tag": tag, - "parent": parent, - "position": position, - "rotation": rotation, - "scale": scale, - "componentsToAdd": components_to_add, - "primitiveType": primitive_type, - "saveAsPrefab": save_as_prefab, - "prefabPath": prefab_path, - "prefabFolder": prefab_folder, - "setActive": set_active, - "layer": layer, - "componentsToRemove": components_to_remove, - "componentProperties": component_properties, - "searchTerm": search_term, - "findAll": find_all, - "searchInChildren": search_in_children, - "searchInactive": search_inactive, - "componentName": component_name, - "includeNonPublicSerialized": includeNonPublicSerialized - } - params = {k: v for k, v in params.items() if v is not None} - - # --- Handle Prefab Path Logic --- - if action == "create" and params.get("saveAsPrefab"): # Check if 'saveAsPrefab' is explicitly True in params - if "prefabPath" not in params: - if "name" not in params or not params["name"]: - return {"success": False, "message": "Cannot create default prefab path: 'name' parameter is missing."} - # Use the provided prefab_folder (which has a default) and the name to construct the path - constructed_path = f"{prefab_folder}/{params['name']}.prefab" - # Ensure clean path separators (Unity prefers '/') - params["prefabPath"] = constructed_path.replace("\\", "/") - elif not params["prefabPath"].lower().endswith(".prefab"): - return {"success": False, "message": f"Invalid prefab_path: '{params['prefabPath']}' must end with .prefab"} - # Ensure prefab_folder itself isn't sent if prefabPath was constructed or provided - # The C# side only needs the final prefabPath - params.pop("prefab_folder", None) - # -------------------------------- - - # Send the command to Unity via the established connection - # Use the get_unity_connection function to retrieve the active connection instance - # Changed "MANAGE_GAMEOBJECT" to "manage_gameobject" to potentially match Unity expectation - response = get_unity_connection().send_command("manage_gameobject", params) - - # Check if the response indicates success - # If the response is not successful, raise an exception with the error message - if response.get("success"): - return {"success": True, "message": response.get("message", "GameObject operation successful."), "data": response.get("data")} - else: - return {"success": False, "message": response.get("error", "An unknown error occurred during GameObject management.")} - - except Exception as e: - return {"success": False, "message": f"Python error managing GameObject: {str(e)}"} \ No newline at end of file diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/manage_gameobject.py.meta b/UnityMcpBridge/UnityMcpServer/src/tools/manage_gameobject.py.meta deleted file mode 100644 index 9fc044f..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/manage_gameobject.py.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 393f17281b99c428dbe73ba8652b60f5 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/manage_scene.py b/UnityMcpBridge/UnityMcpServer/src/tools/manage_scene.py deleted file mode 100644 index 44981f6..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/manage_scene.py +++ /dev/null @@ -1,47 +0,0 @@ -from mcp.server.fastmcp import FastMCP, Context -from typing import Dict, Any -from unity_connection import get_unity_connection - -def register_manage_scene_tools(mcp: FastMCP): - """Register all scene management tools with the MCP server.""" - - @mcp.tool() - def manage_scene( - ctx: Context, - action: str, - name: str, - path: str, - build_index: int, - ) -> Dict[str, Any]: - """Manages Unity scenes (load, save, create, get hierarchy, etc.). - - Args: - action: Operation (e.g., 'load', 'save', 'create', 'get_hierarchy'). - name: Scene name (no extension) for create/load/save. - path: Asset path for scene operations (default: "Assets/"). - build_index: Build index for load/build settings actions. - # Add other action-specific args as needed (e.g., for hierarchy depth) - - Returns: - Dictionary with results ('success', 'message', 'data'). - """ - try: - params = { - "action": action, - "name": name, - "path": path, - "buildIndex": build_index - } - params = {k: v for k, v in params.items() if v is not None} - - # Send command to Unity - response = get_unity_connection().send_command("manage_scene", params) - - # Process response - if response.get("success"): - return {"success": True, "message": response.get("message", "Scene operation successful."), "data": response.get("data")} - else: - return {"success": False, "message": response.get("error", "An unknown error occurred during scene management.")} - - except Exception as e: - return {"success": False, "message": f"Python error managing scene: {str(e)}"} \ No newline at end of file diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/manage_scene.py.meta b/UnityMcpBridge/UnityMcpServer/src/tools/manage_scene.py.meta deleted file mode 100644 index a4feb8f..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/manage_scene.py.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: a744081d28b1e4ace9bfe8d6c4309640 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/manage_script.py b/UnityMcpBridge/UnityMcpServer/src/tools/manage_script.py deleted file mode 100644 index 22e0953..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/manage_script.py +++ /dev/null @@ -1,74 +0,0 @@ -from mcp.server.fastmcp import FastMCP, Context -from typing import Dict, Any -from unity_connection import get_unity_connection -import os -import base64 - -def register_manage_script_tools(mcp: FastMCP): - """Register all script management tools with the MCP server.""" - - @mcp.tool() - def manage_script( - ctx: Context, - action: str, - name: str, - path: str, - contents: str, - script_type: str, - namespace: str - ) -> Dict[str, Any]: - """Manages C# scripts in Unity (create, read, update, delete). - Make reference variables public for easier access in the Unity Editor. - - Args: - action: Operation ('create', 'read', 'update', 'delete'). - name: Script name (no .cs extension). - path: Asset path (default: "Assets/"). - contents: C# code for 'create'/'update'. - script_type: Type hint (e.g., 'MonoBehaviour'). - namespace: Script namespace. - - Returns: - Dictionary with results ('success', 'message', 'data'). - """ - try: - # Prepare parameters for Unity - params = { - "action": action, - "name": name, - "path": path, - "namespace": namespace, - "scriptType": script_type - } - - # Base64 encode the contents if they exist to avoid JSON escaping issues - if contents is not None: - if action in ['create', 'update']: - # Encode content for safer transmission - params["encodedContents"] = base64.b64encode(contents.encode('utf-8')).decode('utf-8') - params["contentsEncoded"] = True - else: - params["contents"] = contents - - # Remove None values so they don't get sent as null - params = {k: v for k, v in params.items() if v is not None} - - # Send command to Unity - response = get_unity_connection().send_command("manage_script", params) - - # Process response from Unity - if response.get("success"): - # If the response contains base64 encoded content, decode it - if response.get("data", {}).get("contentsEncoded"): - decoded_contents = base64.b64decode(response["data"]["encodedContents"]).decode('utf-8') - response["data"]["contents"] = decoded_contents - del response["data"]["encodedContents"] - del response["data"]["contentsEncoded"] - - return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")} - else: - return {"success": False, "message": response.get("error", "An unknown error occurred.")} - - except Exception as e: - # Handle Python-side errors (e.g., connection issues) - return {"success": False, "message": f"Python error managing script: {str(e)}"} \ No newline at end of file diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/manage_script.py.meta b/UnityMcpBridge/UnityMcpServer/src/tools/manage_script.py.meta deleted file mode 100644 index 8ec9f2e..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/manage_script.py.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 5f5d55725198d4d53afcd4565f402b9e -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/manage_shader.py b/UnityMcpBridge/UnityMcpServer/src/tools/manage_shader.py deleted file mode 100644 index c447a3a..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/manage_shader.py +++ /dev/null @@ -1,67 +0,0 @@ -from mcp.server.fastmcp import FastMCP, Context -from typing import Dict, Any -from unity_connection import get_unity_connection -import os -import base64 - -def register_manage_shader_tools(mcp: FastMCP): - """Register all shader script management tools with the MCP server.""" - - @mcp.tool() - def manage_shader( - ctx: Context, - action: str, - name: str, - path: str, - contents: str, - ) -> Dict[str, Any]: - """Manages shader scripts in Unity (create, read, update, delete). - - Args: - action: Operation ('create', 'read', 'update', 'delete'). - name: Shader name (no .cs extension). - path: Asset path (default: "Assets/"). - contents: Shader code for 'create'/'update'. - - Returns: - Dictionary with results ('success', 'message', 'data'). - """ - try: - # Prepare parameters for Unity - params = { - "action": action, - "name": name, - "path": path, - } - - # Base64 encode the contents if they exist to avoid JSON escaping issues - if contents is not None: - if action in ['create', 'update']: - # Encode content for safer transmission - params["encodedContents"] = base64.b64encode(contents.encode('utf-8')).decode('utf-8') - params["contentsEncoded"] = True - else: - params["contents"] = contents - - # Remove None values so they don't get sent as null - params = {k: v for k, v in params.items() if v is not None} - - # Send command to Unity - response = get_unity_connection().send_command("manage_shader", params) - - # Process response from Unity - if response.get("success"): - # If the response contains base64 encoded content, decode it - if response.get("data", {}).get("contentsEncoded"): - decoded_contents = base64.b64decode(response["data"]["encodedContents"]).decode('utf-8') - response["data"]["contents"] = decoded_contents - del response["data"]["encodedContents"] - del response["data"]["contentsEncoded"] - - return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")} - else: - return {"success": False, "message": response.get("error", "An unknown error occurred.")} - - except Exception as e: - # Handle Python-side errors (e.g., connection issues) - return {"success": False, "message": f"Python error managing shader: {str(e)}"} \ No newline at end of file diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/manage_shader.py.meta b/UnityMcpBridge/UnityMcpServer/src/tools/manage_shader.py.meta deleted file mode 100644 index bdadaaa..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/manage_shader.py.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 52a3e6faa53234aa08edf8163159c9af -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/read_console.py b/UnityMcpBridge/UnityMcpServer/src/tools/read_console.py deleted file mode 100644 index 3d4bd12..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/read_console.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Defines the read_console tool for accessing Unity Editor console messages. -""" -from typing import List, Dict, Any -from mcp.server.fastmcp import FastMCP, Context -from unity_connection import get_unity_connection - -def register_read_console_tools(mcp: FastMCP): - """Registers the read_console tool with the MCP server.""" - - @mcp.tool() - def read_console( - ctx: Context, - action: str = None, - types: List[str] = None, - count: int = None, - filter_text: str = None, - since_timestamp: str = None, - format: str = None, - include_stacktrace: bool = None - ) -> Dict[str, Any]: - """Gets messages from or clears the Unity Editor console. - - Args: - ctx: The MCP context. - action: Operation ('get' or 'clear'). - types: Message types to get ('error', 'warning', 'log', 'all'). - count: Max messages to return. - filter_text: Text filter for messages. - since_timestamp: Get messages after this timestamp (ISO 8601). - format: Output format ('plain', 'detailed', 'json'). - include_stacktrace: Include stack traces in output. - - Returns: - Dictionary with results. For 'get', includes 'data' (messages). - """ - - # Get the connection instance - bridge = get_unity_connection() - - # 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 - - # Normalize action if it's a string - if isinstance(action, str): - action = action.lower() - - # Prepare parameters for the C# handler - params_dict = { - "action": action, - "types": types, - "count": count, - "filterText": filter_text, - "sinceTimestamp": since_timestamp, - "format": format.lower() if isinstance(format, str) else format, - "includeStacktrace": include_stacktrace - } - - # Remove None values unless it's 'count' (as None might mean 'all') - params_dict = {k: v for k, v in params_dict.items() if v is not None or k == 'count'} - - # Add count back if it was None, explicitly sending null might be important for C# logic - if 'count' not in params_dict: - params_dict['count'] = None - - # Forward the command using the bridge's send_command method - return bridge.send_command("read_console", params_dict) \ No newline at end of file diff --git a/UnityMcpBridge/UnityMcpServer/src/tools/read_console.py.meta b/UnityMcpBridge/UnityMcpServer/src/tools/read_console.py.meta deleted file mode 100644 index 3ef3e8a..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/tools/read_console.py.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: a73ff5df6153548878e2656315e5db69 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/unity_connection.py b/UnityMcpBridge/UnityMcpServer/src/unity_connection.py deleted file mode 100644 index dbf7703..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/unity_connection.py +++ /dev/null @@ -1,239 +0,0 @@ -import socket -import json -import logging -from dataclasses import dataclass -from pathlib import Path -import time -from typing import Dict, Any -from config import config -from port_discovery import PortDiscovery - -# Configure logging using settings from config -logging.basicConfig( - level=getattr(logging, config.log_level), - format=config.log_format -) -logger = logging.getLogger("unity-mcp-server") - -@dataclass -class UnityConnection: - """Manages the socket connection to the Unity Editor.""" - host: str = config.unity_host - port: int = None # Will be set dynamically - sock: socket.socket = None # Socket for Unity communication - - def __post_init__(self): - """Set port from discovery if not explicitly provided""" - if self.port is None: - self.port = PortDiscovery.discover_unity_port() - - def connect(self) -> bool: - """Establish a connection to the Unity Editor.""" - if self.sock: - return True - try: - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.connect((self.host, self.port)) - logger.info(f"Connected to Unity at {self.host}:{self.port}") - return True - except Exception as e: - logger.error(f"Failed to connect to Unity: {str(e)}") - self.sock = None - return False - - def disconnect(self): - """Close the connection to the Unity Editor.""" - if self.sock: - try: - self.sock.close() - except Exception as e: - logger.error(f"Error disconnecting from Unity: {str(e)}") - finally: - self.sock = None - - def receive_full_response(self, sock, buffer_size=config.buffer_size) -> bytes: - """Receive a complete response from Unity, handling chunked data.""" - chunks = [] - sock.settimeout(config.connection_timeout) # Use timeout from config - try: - while True: - chunk = sock.recv(buffer_size) - if not chunk: - if not chunks: - raise Exception("Connection closed before receiving data") - break - chunks.append(chunk) - - # Process the data received so far - data = b''.join(chunks) - decoded_data = data.decode('utf-8') - - # Check if we've received a complete response - try: - # Special case for ping-pong - if decoded_data.strip().startswith('{"status":"success","result":{"message":"pong"'): - logger.debug("Received ping response") - return data - - # Handle escaped quotes in the content - if '"content":' in decoded_data: - # Find the content field and its value - content_start = decoded_data.find('"content":') + 9 - content_end = decoded_data.rfind('"', content_start) - if content_end > content_start: - # Replace escaped quotes in content with regular quotes - content = decoded_data[content_start:content_end] - content = content.replace('\\"', '"') - decoded_data = decoded_data[:content_start] + content + decoded_data[content_end:] - - # Validate JSON format - json.loads(decoded_data) - - # If we get here, we have valid JSON - logger.info(f"Received complete response ({len(data)} bytes)") - return data - except json.JSONDecodeError: - # We haven't received a complete valid JSON response yet - continue - except Exception as e: - logger.warning(f"Error processing response chunk: {str(e)}") - # Continue reading more chunks as this might not be the complete response - continue - except socket.timeout: - logger.warning("Socket timeout during receive") - raise Exception("Timeout receiving Unity response") - except Exception as e: - logger.error(f"Error during receive: {str(e)}") - raise - - def send_command(self, command_type: str, params: Dict[str, Any] = None) -> Dict[str, Any]: - """Send a command with retry/backoff and port rediscovery. Pings only when requested.""" - attempts = max(config.max_retries, 5) - base_backoff = max(0.5, config.retry_delay) - - def read_status_file() -> dict | None: - try: - status_files = sorted(Path.home().joinpath('.unity-mcp').glob('unity-mcp-status-*.json'), key=lambda p: p.stat().st_mtime, reverse=True) - if not status_files: - return None - latest = status_files[0] - with latest.open('r') as f: - return json.load(f) - except Exception: - return None - - last_short_timeout = None - - for attempt in range(attempts + 1): - try: - # Ensure connected - if not self.sock: - # During retries use short connect timeout - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.settimeout(1.0) - self.sock.connect((self.host, self.port)) - # restore steady-state timeout for receive - self.sock.settimeout(config.connection_timeout) - logger.info(f"Connected to Unity at {self.host}:{self.port}") - - # Build payload - if command_type == 'ping': - payload = b'ping' - else: - command = {"type": command_type, "params": params or {}} - payload = json.dumps(command, ensure_ascii=False).encode('utf-8') - - # Send - self.sock.sendall(payload) - - # During retry bursts use a short receive timeout - if attempt > 0 and last_short_timeout is None: - last_short_timeout = self.sock.gettimeout() - self.sock.settimeout(1.0) - response_data = self.receive_full_response(self.sock) - # restore steady-state timeout if changed - if last_short_timeout is not None: - self.sock.settimeout(config.connection_timeout) - last_short_timeout = None - - # Parse - if command_type == 'ping': - resp = json.loads(response_data.decode('utf-8')) - if resp.get('status') == 'success' and resp.get('result', {}).get('message') == 'pong': - return {"message": "pong"} - raise Exception("Ping unsuccessful") - - resp = json.loads(response_data.decode('utf-8')) - if resp.get('status') == 'error': - err = resp.get('error') or resp.get('message', 'Unknown Unity error') - raise Exception(err) - return resp.get('result', {}) - except Exception as e: - logger.warning(f"Unity communication attempt {attempt+1} failed: {e}") - try: - if self.sock: - self.sock.close() - finally: - self.sock = None - - # Re-discover port each time - try: - new_port = PortDiscovery.discover_unity_port() - if new_port != self.port: - logger.info(f"Unity port changed {self.port} -> {new_port}") - self.port = new_port - except Exception as de: - logger.debug(f"Port discovery failed: {de}") - - if attempt < attempts: - # If heartbeat indicates reload, keep retries snappy without spamming - status = read_status_file() - backoff = base_backoff * (2 ** attempt) - sleep_s = min(backoff, 3.0) - if status and (status.get('reloading') or status.get('unity_port') == self.port): - sleep_s = min(sleep_s, 0.8) - time.sleep(sleep_s) - continue - raise - -# Global Unity connection -_unity_connection = None - -def get_unity_connection() -> UnityConnection: - """Retrieve or establish a persistent Unity connection.""" - global _unity_connection - if _unity_connection is not None: - try: - # Try to ping with a short timeout to verify connection - result = _unity_connection.send_command("ping") - # If we get here, the connection is still valid - logger.debug("Reusing existing Unity connection") - return _unity_connection - except Exception as e: - logger.warning(f"Existing connection failed: {str(e)}") - try: - _unity_connection.disconnect() - except: - pass - _unity_connection = None - - # Create a new connection - logger.info("Creating new Unity connection") - _unity_connection = UnityConnection() - if not _unity_connection.connect(): - _unity_connection = None - raise ConnectionError("Could not connect to Unity. Ensure the Unity Editor and MCP Bridge are running.") - - try: - # Verify the new connection works - _unity_connection.send_command("ping") - logger.info("Successfully established new Unity connection") - return _unity_connection - except Exception as e: - logger.error(f"Could not verify new connection: {str(e)}") - try: - _unity_connection.disconnect() - except: - pass - _unity_connection = None - raise ConnectionError(f"Could not establish valid Unity connection: {str(e)}") diff --git a/UnityMcpBridge/UnityMcpServer/src/unity_connection.py.meta b/UnityMcpBridge/UnityMcpServer/src/unity_connection.py.meta deleted file mode 100644 index e26b032..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/unity_connection.py.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 2bba70ed632654291acae6c529d6ec79 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/UnityMcpServer/src/uv.lock b/UnityMcpBridge/UnityMcpServer/src/uv.lock deleted file mode 100644 index bc3e54c..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/uv.lock +++ /dev/null @@ -1,349 +0,0 @@ -version = 1 -revision = 1 -requires-python = ">=3.12" - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, -] - -[[package]] -name = "anyio" -version = "4.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, -] - -[[package]] -name = "certifi" -version = "2025.1.31" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, -] - -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, -] - -[[package]] -name = "h11" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, -] - -[[package]] -name = "httpcore" -version = "1.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, -] - -[[package]] -name = "httpx-sse" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, -] - -[[package]] -name = "mcp" -version = "1.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "uvicorn" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/cc/5c5bb19f1a0f8f89a95e25cb608b0b07009e81fd4b031e519335404e1422/mcp-1.4.1.tar.gz", hash = "sha256:b9655d2de6313f9d55a7d1df62b3c3fe27a530100cc85bf23729145b0dba4c7a", size = 154942 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/0e/885f156ade60108e67bf044fada5269da68e29d758a10b0c513f4d85dd76/mcp-1.4.1-py3-none-any.whl", hash = "sha256:a7716b1ec1c054e76f49806f7d96113b99fc1166fc9244c2c6f19867cb75b593", size = 72448 }, -] - -[package.optional-dependencies] -cli = [ - { name = "python-dotenv" }, - { name = "typer" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, -] - -[[package]] -name = "pydantic" -version = "2.10.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, -] - -[[package]] -name = "pydantic-core" -version = "2.27.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, - { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, - { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, - { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, - { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, - { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, - { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, - { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, - { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, - { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, - { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, - { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, - { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, - { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, - { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, - { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, - { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, - { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, - { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, - { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, - { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, - { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, - { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, - { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, - { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, - { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, - { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, - { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, -] - -[[package]] -name = "pydantic-settings" -version = "2.8.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, -] - -[[package]] -name = "pygments" -version = "2.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, -] - -[[package]] -name = "python-dotenv" -version = "1.0.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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, -] - -[[package]] -name = "rich" -version = "13.9.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, -] - -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, -] - -[[package]] -name = "sse-starlette" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, -] - -[[package]] -name = "starlette" -version = "0.46.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, -] - -[[package]] -name = "typer" -version = "0.15.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, -] - -[[package]] -name = "unitymcpserver" -version = "2.0.0" -source = { editable = "." } -dependencies = [ - { name = "httpx" }, - { name = "mcp", extra = ["cli"] }, -] - -[package.metadata] -requires-dist = [ - { name = "httpx", specifier = ">=0.27.2" }, - { name = "mcp", extras = ["cli"], specifier = ">=1.4.1" }, -] - -[[package]] -name = "uvicorn" -version = "0.34.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, -] diff --git a/UnityMcpBridge/UnityMcpServer/src/uv.lock.meta b/UnityMcpBridge/UnityMcpServer/src/uv.lock.meta deleted file mode 100644 index 4fa6853..0000000 --- a/UnityMcpBridge/UnityMcpServer/src/uv.lock.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 9c116a2a729ac40348fb4c81c93ea030 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/UnityMcpBridge/package.json b/UnityMcpBridge/package.json index 8471710..bbb5994 100644 --- a/UnityMcpBridge/package.json +++ b/UnityMcpBridge/package.json @@ -1,13 +1,4 @@ { -<<<<<<< HEAD - "name": "com.justinpbarnett.unity-mcp", - "version": "2.0.0", - "displayName": "Unity MCP Bridge", - "description": "A bridge that manages and communicates with the sister application, Unity MCP Server, which allows for communications with MCP Clients like Claude Desktop or Cursor.", - "unity": "2020.3", - "dependencies": { - "com.unity.nuget.newtonsoft-json": "3.0.2" -======= "name": "com.coplaydev.unity-mcp", "version": "1.0.0", "displayName": "Unity MCP Bridge", @@ -31,6 +22,5 @@ "name": "CoplayDev", "email": "support@coplay.dev", "url": "https://coplay.dev" ->>>>>>> upstream/main } } diff --git a/UnityMcpServer/src/.python-version b/UnityMcpServer/src/.python-version deleted file mode 100644 index e4fba21..0000000 --- a/UnityMcpServer/src/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.12 diff --git a/UnityMcpServer/src/Dockerfile b/UnityMcpServer/src/Dockerfile deleted file mode 100644 index 3f884f3..0000000 --- a/UnityMcpServer/src/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -FROM python:3.12-slim - -# Install required system dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ - git \ - && rm -rf /var/lib/apt/lists/* - -# Set working directory -WORKDIR /app - -# Install uv package manager -RUN pip install uv - -# Copy required files -COPY config.py /app/ -COPY server.py /app/ -COPY unity_connection.py /app/ -COPY pyproject.toml /app/ -COPY __init__.py /app/ -COPY tools/ /app/tools/ - -# Install dependencies using uv -RUN uv pip install --system -e . - - -# Command to run the server -CMD ["uv", "run", "server.py"] \ No newline at end of file diff --git a/UnityMcpServer/src/__init__.py b/UnityMcpServer/src/__init__.py deleted file mode 100644 index 62e5cd1..0000000 --- a/UnityMcpServer/src/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Unity MCP Server package. -""" \ No newline at end of file diff --git a/UnityMcpServer/src/config.py b/UnityMcpServer/src/config.py deleted file mode 100644 index 485b845..0000000 --- a/UnityMcpServer/src/config.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -Configuration settings for the Unity MCP Server. -This file contains all configurable parameters for the server. -""" - -from dataclasses import dataclass - -@dataclass -class ServerConfig: - """Main configuration class for the MCP server.""" - - # Network settings - unity_host: str = "localhost" - unity_port: int = 6400 - mcp_port: int = 6500 - - # Connection settings - connection_timeout: float = 60.0 # default steady-state timeout; retries use shorter timeouts - buffer_size: int = 16 * 1024 * 1024 # 16MB buffer - - # Logging settings - log_level: str = "INFO" - log_format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - - # Server settings - max_retries: int = 8 - retry_delay: float = 0.5 - -# Create a global config instance -config = ServerConfig() \ No newline at end of file diff --git a/UnityMcpServer/src/port_discovery.py b/UnityMcpServer/src/port_discovery.py deleted file mode 100644 index 9885533..0000000 --- a/UnityMcpServer/src/port_discovery.py +++ /dev/null @@ -1,155 +0,0 @@ -""" -Port discovery utility for Unity MCP Server. - -What changed and why: -- Unity now writes a per-project port file named like - `~/.unity-mcp/unity-mcp-port-.json` to avoid projects overwriting - each other's saved port. The legacy file `unity-mcp-port.json` may still - exist. -- This module now scans for both patterns, prefers the most recently - modified file, and verifies that the port is actually a Unity MCP listener - (quick socket connect + ping) before choosing it. -""" - -import json -import os -import logging -from pathlib import Path -from typing import Optional, List -import glob -import socket - -logger = logging.getLogger("unity-mcp-server") - -class PortDiscovery: - """Handles port discovery from Unity Bridge registry""" - REGISTRY_FILE = "unity-mcp-port.json" # legacy single-project file - DEFAULT_PORT = 6400 - CONNECT_TIMEOUT = 0.3 # seconds, keep this snappy during discovery - - @staticmethod - def get_registry_path() -> Path: - """Get the path to the port registry file""" - return Path.home() / ".unity-mcp" / PortDiscovery.REGISTRY_FILE - - @staticmethod - def get_registry_dir() -> Path: - return Path.home() / ".unity-mcp" - - @staticmethod - def list_candidate_files() -> List[Path]: - """Return candidate registry files, newest first. - Includes hashed per-project files and the legacy file (if present). - """ - base = PortDiscovery.get_registry_dir() - hashed = sorted( - (Path(p) for p in glob.glob(str(base / "unity-mcp-port-*.json"))), - key=lambda p: p.stat().st_mtime, - reverse=True, - ) - legacy = PortDiscovery.get_registry_path() - if legacy.exists(): - # Put legacy at the end so hashed, per-project files win - hashed.append(legacy) - return hashed - - @staticmethod - def _try_probe_unity_mcp(port: int) -> bool: - """Quickly check if a Unity MCP listener is on this port. - Tries a short TCP connect, sends 'ping', expects a JSON 'pong'. - """ - 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: - return True - except Exception: - return False - except Exception: - return False - return False - - @staticmethod - def _read_latest_status() -> Optional[dict]: - try: - base = PortDiscovery.get_registry_dir() - status_files = sorted( - (Path(p) for p in glob.glob(str(base / "unity-mcp-status-*.json"))), - key=lambda p: p.stat().st_mtime, - reverse=True, - ) - if not status_files: - return None - with status_files[0].open('r') as f: - return json.load(f) - except Exception: - return None - - @staticmethod - def discover_unity_port() -> int: - """ - Discover Unity port by scanning per-project and legacy registry files. - Prefer the newest file whose port responds; fall back to first parsed - value; finally default to 6400. - - Returns: - Port number to connect to - """ - # Prefer the latest heartbeat status if it points to a responsive port - status = PortDiscovery._read_latest_status() - if status: - port = status.get('unity_port') - if isinstance(port, int) and PortDiscovery._try_probe_unity_mcp(port): - logger.info(f"Using Unity port from status: {port}") - return port - - candidates = PortDiscovery.list_candidate_files() - - first_seen_port: Optional[int] = None - - for path in candidates: - try: - with open(path, 'r') as f: - cfg = json.load(f) - unity_port = cfg.get('unity_port') - if isinstance(unity_port, int): - if first_seen_port is None: - first_seen_port = unity_port - if PortDiscovery._try_probe_unity_mcp(unity_port): - logger.info(f"Using Unity port from {path.name}: {unity_port}") - return unity_port - except Exception as e: - logger.warning(f"Could not read port registry {path}: {e}") - - if first_seen_port is not None: - logger.info(f"No responsive port found; using first seen value {first_seen_port}") - return first_seen_port - - # Fallback to default port - logger.info(f"No port registry found; using default port {PortDiscovery.DEFAULT_PORT}") - return PortDiscovery.DEFAULT_PORT - - @staticmethod - def get_port_config() -> Optional[dict]: - """ - Get the most relevant port configuration from registry. - Returns the most recent hashed file's config if present, - otherwise the legacy file's config. Returns None if nothing exists. - - Returns: - Port configuration dict or None if not found - """ - candidates = PortDiscovery.list_candidate_files() - if not candidates: - return None - for path in candidates: - try: - with open(path, 'r') as f: - return json.load(f) - except Exception as e: - logger.warning(f"Could not read port configuration {path}: {e}") - return None \ No newline at end of file diff --git a/UnityMcpServer/src/pyproject.toml b/UnityMcpServer/src/pyproject.toml deleted file mode 100644 index 2c05fb8..0000000 --- a/UnityMcpServer/src/pyproject.toml +++ /dev/null @@ -1,15 +0,0 @@ -[project] -name = "UnityMcpServer" -version = "2.0.0" -description = "Unity MCP 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.4.1"] - -[build-system] -requires = ["setuptools>=64.0.0", "wheel"] -build-backend = "setuptools.build_meta" - -[tool.setuptools] -py-modules = ["config", "server", "unity_connection"] -packages = ["tools"] diff --git a/UnityMcpServer/src/server.py b/UnityMcpServer/src/server.py deleted file mode 100644 index 55360b5..0000000 --- a/UnityMcpServer/src/server.py +++ /dev/null @@ -1,73 +0,0 @@ -from mcp.server.fastmcp import FastMCP, Context, Image -import logging -from dataclasses import dataclass -from contextlib import asynccontextmanager -from typing import AsyncIterator, Dict, Any, List -from config import config -from tools import register_all_tools -from unity_connection import get_unity_connection, UnityConnection - -# Configure logging using settings from config -logging.basicConfig( - level=getattr(logging, config.log_level), - format=config.log_format -) -logger = logging.getLogger("unity-mcp-server") - -# Global connection state -_unity_connection: UnityConnection = None - -@asynccontextmanager -async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]: - """Handle server startup and shutdown.""" - global _unity_connection - logger.info("Unity MCP Server starting up") - try: - _unity_connection = get_unity_connection() - logger.info("Connected to Unity on startup") - except Exception as e: - logger.warning(f"Could not connect to Unity on startup: {str(e)}") - _unity_connection = None - try: - # Yield the connection object so it can be attached to the context - # The key 'bridge' matches how tools like read_console expect to access it (ctx.bridge) - yield {"bridge": _unity_connection} - finally: - if _unity_connection: - _unity_connection.disconnect() - _unity_connection = None - logger.info("Unity MCP Server shut down") - -# Initialize MCP server -mcp = FastMCP( - "unity-mcp-server", - description="Unity Editor integration via Model Context Protocol", - lifespan=server_lifespan -) - -# Register all tools -register_all_tools(mcp) - -# Asset Creation Strategy - -@mcp.prompt() -def asset_creation_strategy() -> str: - """Guide for discovering and using Unity MCP tools effectively.""" - return ( - "Available Unity MCP Server Tools:\\n\\n" - "- `manage_editor`: Controls editor state and queries info.\\n" - "- `execute_menu_item`: Executes Unity Editor menu items by path.\\n" - "- `read_console`: Reads or clears Unity console messages, with filtering options.\\n" - "- `manage_scene`: Manages scenes.\\n" - "- `manage_gameobject`: Manages GameObjects in the scene.\\n" - "- `manage_script`: Manages C# script files.\\n" - "- `manage_asset`: Manages prefabs and assets.\\n" - "- `manage_shader`: Manages shaders.\\n\\n" - "Tips:\\n" - "- Create prefabs for reusable GameObjects.\\n" - "- Always include a camera and main light in your scenes.\\n" - ) - -# Run the server -if __name__ == "__main__": - mcp.run(transport='stdio') diff --git a/UnityMcpServer/src/tools/__init__.py b/UnityMcpServer/src/tools/__init__.py deleted file mode 100644 index 4d8d63c..0000000 --- a/UnityMcpServer/src/tools/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -from .manage_script import register_manage_script_tools -from .manage_scene import register_manage_scene_tools -from .manage_editor import register_manage_editor_tools -from .manage_gameobject import register_manage_gameobject_tools -from .manage_asset import register_manage_asset_tools -from .manage_shader import register_manage_shader_tools -from .read_console import register_read_console_tools -from .execute_menu_item import register_execute_menu_item_tools - -def register_all_tools(mcp): - """Register all refactored tools with the MCP server.""" - print("Registering Unity MCP Server refactored tools...") - register_manage_script_tools(mcp) - register_manage_scene_tools(mcp) - register_manage_editor_tools(mcp) - register_manage_gameobject_tools(mcp) - register_manage_asset_tools(mcp) - register_manage_shader_tools(mcp) - register_read_console_tools(mcp) - register_execute_menu_item_tools(mcp) - print("Unity MCP Server tool registration complete.") diff --git a/UnityMcpServer/src/tools/execute_menu_item.py b/UnityMcpServer/src/tools/execute_menu_item.py deleted file mode 100644 index a4ebc67..0000000 --- a/UnityMcpServer/src/tools/execute_menu_item.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Defines the execute_menu_item tool for running Unity Editor menu commands. -""" -from typing import Dict, Any -from mcp.server.fastmcp import FastMCP, Context -from unity_connection import get_unity_connection # Import unity_connection module - -def register_execute_menu_item_tools(mcp: FastMCP): - """Registers the execute_menu_item tool with the MCP server.""" - - @mcp.tool() - async def execute_menu_item( - ctx: Context, - menu_path: str, - action: str = 'execute', - parameters: Dict[str, Any] = None, - ) -> Dict[str, Any]: - """Executes a Unity Editor menu item via its path (e.g., "File/Save Project"). - - Args: - ctx: The MCP context. - menu_path: The full path of the menu item to execute. - action: The operation to perform (default: 'execute'). - parameters: Optional parameters for the menu item (rarely used). - - Returns: - A dictionary indicating success or failure, with optional message/error. - """ - - action = action.lower() if action else 'execute' - - # Prepare parameters for the C# handler - params_dict = { - "action": action, - "menuPath": menu_path, - "parameters": parameters if parameters else {}, - } - - # Remove None values - params_dict = {k: v for k, v in params_dict.items() if v is not None} - - if "parameters" not in params_dict: - params_dict["parameters"] = {} # Ensure parameters dict exists - - # Get Unity connection and send the command - # We use the unity_connection module to communicate with Unity - unity_conn = get_unity_connection() - - # Send command to the ExecuteMenuItem C# handler - # The command type should match what the Unity side expects - return unity_conn.send_command("execute_menu_item", params_dict) \ No newline at end of file diff --git a/UnityMcpServer/src/tools/manage_asset.py b/UnityMcpServer/src/tools/manage_asset.py deleted file mode 100644 index dada66b..0000000 --- a/UnityMcpServer/src/tools/manage_asset.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -Defines the manage_asset tool for interacting with Unity assets. -""" -import asyncio # Added: Import asyncio for running sync code in async -from typing import Dict, Any -from mcp.server.fastmcp import FastMCP, Context -# from ..unity_connection import get_unity_connection # Original line that caused error -from unity_connection import get_unity_connection # Use absolute import relative to Python dir - -def register_manage_asset_tools(mcp: FastMCP): - """Registers the manage_asset tool with the MCP server.""" - - @mcp.tool() - async def manage_asset( - ctx: Context, - action: str, - path: str, - asset_type: str = None, - properties: Dict[str, Any] = None, - destination: str = None, - generate_preview: bool = False, - search_pattern: str = None, - filter_type: str = None, - filter_date_after: str = None, - page_size: int = None, - page_number: int = None - ) -> Dict[str, Any]: - """Performs asset operations (import, create, modify, delete, etc.) in Unity. - - Args: - ctx: The MCP context. - action: Operation to perform (e.g., 'import', 'create', 'modify', 'delete', 'duplicate', 'move', 'rename', 'search', 'get_info', 'create_folder', 'get_components'). - path: Asset path (e.g., "Materials/MyMaterial.mat") or search scope. - asset_type: Asset type (e.g., 'Material', 'Folder') - required for 'create'. - properties: Dictionary of properties for 'create'/'modify'. - example properties for Material: {"color": [1, 0, 0, 1], "shader": "Standard"}. - example properties for Texture: {"width": 1024, "height": 1024, "format": "RGBA32"}. - example properties for PhysicsMaterial: {"bounciness": 1.0, "staticFriction": 0.5, "dynamicFriction": 0.5}. - destination: Target path for 'duplicate'/'move'. - search_pattern: Search pattern (e.g., '*.prefab'). - filter_*: Filters for search (type, date). - page_*: Pagination for search. - - Returns: - A dictionary with operation results ('success', 'data', 'error'). - """ - # Ensure properties is a dict if None - if properties is None: - properties = {} - - # Prepare parameters for the C# handler - params_dict = { - "action": action.lower(), - "path": path, - "assetType": asset_type, - "properties": properties, - "destination": destination, - "generatePreview": generate_preview, - "searchPattern": search_pattern, - "filterType": filter_type, - "filterDateAfter": filter_date_after, - "pageSize": page_size, - "pageNumber": page_number - } - - # Remove None values to avoid sending unnecessary nulls - params_dict = {k: v for k, v in params_dict.items() if v is not None} - - # Get the current asyncio event loop - loop = asyncio.get_running_loop() - # Get the Unity connection instance - connection = get_unity_connection() - - # Run the synchronous send_command in the default executor (thread pool) - # This prevents blocking the main async event loop. - result = await loop.run_in_executor( - None, # Use default executor - connection.send_command, # The function to call - "manage_asset", # First argument for send_command - params_dict # Second argument for send_command - ) - # Return the result obtained from Unity - return result \ No newline at end of file diff --git a/UnityMcpServer/src/tools/manage_editor.py b/UnityMcpServer/src/tools/manage_editor.py deleted file mode 100644 index b256e6c..0000000 --- a/UnityMcpServer/src/tools/manage_editor.py +++ /dev/null @@ -1,53 +0,0 @@ -from mcp.server.fastmcp import FastMCP, Context -from typing import Dict, Any -from unity_connection import get_unity_connection - -def register_manage_editor_tools(mcp: FastMCP): - """Register all editor management tools with the MCP server.""" - - @mcp.tool() - def manage_editor( - ctx: Context, - action: str, - wait_for_completion: bool = None, - # --- Parameters for specific actions --- - tool_name: str = None, - tag_name: str = None, - layer_name: str = None, - ) -> Dict[str, Any]: - """Controls and queries the Unity editor's state and settings. - - Args: - action: Operation (e.g., 'play', 'pause', 'get_state', 'set_active_tool', 'add_tag'). - wait_for_completion: Optional. If True, waits for certain actions. - Action-specific arguments (e.g., tool_name, tag_name, layer_name). - - Returns: - Dictionary with operation results ('success', 'message', 'data'). - """ - try: - # Prepare parameters, removing None values - params = { - "action": action, - "waitForCompletion": wait_for_completion, - "toolName": tool_name, # Corrected parameter name to match C# - "tagName": tag_name, # Pass tag name - "layerName": layer_name, # Pass layer name - # Add other parameters based on the action being performed - # "width": width, - # "height": height, - # etc. - } - params = {k: v for k, v in params.items() if v is not None} - - # Send command to Unity - response = get_unity_connection().send_command("manage_editor", params) - - # Process response - if response.get("success"): - return {"success": True, "message": response.get("message", "Editor operation successful."), "data": response.get("data")} - else: - return {"success": False, "message": response.get("error", "An unknown error occurred during editor management.")} - - except Exception as e: - return {"success": False, "message": f"Python error managing editor: {str(e)}"} \ No newline at end of file diff --git a/UnityMcpServer/src/tools/manage_gameobject.py b/UnityMcpServer/src/tools/manage_gameobject.py deleted file mode 100644 index 83ab9c7..0000000 --- a/UnityMcpServer/src/tools/manage_gameobject.py +++ /dev/null @@ -1,138 +0,0 @@ -from mcp.server.fastmcp import FastMCP, Context -from typing import Dict, Any, List -from unity_connection import get_unity_connection - -def register_manage_gameobject_tools(mcp: FastMCP): - """Register all GameObject management tools with the MCP server.""" - - @mcp.tool() - def manage_gameobject( - ctx: Context, - action: str, - target: str = None, # GameObject identifier by name or path - search_method: str = None, - # --- Combined Parameters for Create/Modify --- - name: str = None, # Used for both 'create' (new object name) and 'modify' (rename) - tag: str = None, # Used for both 'create' (initial tag) and 'modify' (change tag) - parent: str = None, # Used for both 'create' (initial parent) and 'modify' (change parent) - position: List[float] = None, - rotation: List[float] = None, - scale: List[float] = None, - components_to_add: List[str] = None, # List of component names to add - primitive_type: str = None, - save_as_prefab: bool = False, - prefab_path: str = None, - prefab_folder: str = "Assets/Prefabs", - # --- Parameters for 'modify' --- - set_active: bool = None, - layer: str = None, # Layer name - components_to_remove: List[str] = None, - component_properties: Dict[str, Dict[str, Any]] = None, - # --- Parameters for 'find' --- - search_term: str = None, - find_all: bool = False, - search_in_children: bool = False, - search_inactive: bool = False, - # -- Component Management Arguments -- - component_name: str = None, - includeNonPublicSerialized: bool = None, # Controls serialization of private [SerializeField] fields - ) -> Dict[str, Any]: - """Manages GameObjects: create, modify, delete, find, and component operations. - - Args: - action: Operation (e.g., 'create', 'modify', 'find', 'add_component', 'remove_component', 'set_component_property', 'get_components'). - target: GameObject identifier (name or path string) for modify/delete/component actions. - search_method: How to find objects ('by_name', 'by_id', 'by_path', etc.). Used with 'find' and some 'target' lookups. - name: GameObject name - used for both 'create' (initial name) and 'modify' (rename). - tag: Tag name - used for both 'create' (initial tag) and 'modify' (change tag). - parent: Parent GameObject reference - used for both 'create' (initial parent) and 'modify' (change parent). - layer: Layer name - used for both 'create' (initial layer) and 'modify' (change layer). - component_properties: Dict mapping Component names to their properties to set. - Example: {"Rigidbody": {"mass": 10.0, "useGravity": True}}, - To set references: - - Use asset path string for Prefabs/Materials, e.g., {"MeshRenderer": {"material": "Assets/Materials/MyMat.mat"}} - - Use a dict for scene objects/components, e.g.: - {"MyScript": {"otherObject": {"find": "Player", "method": "by_name"}}} (assigns GameObject) - {"MyScript": {"playerHealth": {"find": "Player", "component": "HealthComponent"}}} (assigns Component) - Example set nested property: - - Access shared material: {"MeshRenderer": {"sharedMaterial.color": [1, 0, 0, 1]}} - components_to_add: List of component names to add. - Action-specific arguments (e.g., position, rotation, scale for create/modify; - component_name for component actions; - search_term, find_all for 'find'). - includeNonPublicSerialized: If True, includes private fields marked [SerializeField] in component data. - - Action-specific details: - - For 'get_components': - Required: target, search_method - Optional: includeNonPublicSerialized (defaults to True) - Returns all components on the target GameObject with their serialized data. - The search_method parameter determines how to find the target ('by_name', 'by_id', 'by_path'). - - Returns: - Dictionary with operation results ('success', 'message', 'data'). - For 'get_components', the 'data' field contains a dictionary of component names and their serialized properties. - """ - try: - # --- Early check for attempting to modify a prefab asset --- - # ---------------------------------------------------------- - - # Prepare parameters, removing None values - params = { - "action": action, - "target": target, - "searchMethod": search_method, - "name": name, - "tag": tag, - "parent": parent, - "position": position, - "rotation": rotation, - "scale": scale, - "componentsToAdd": components_to_add, - "primitiveType": primitive_type, - "saveAsPrefab": save_as_prefab, - "prefabPath": prefab_path, - "prefabFolder": prefab_folder, - "setActive": set_active, - "layer": layer, - "componentsToRemove": components_to_remove, - "componentProperties": component_properties, - "searchTerm": search_term, - "findAll": find_all, - "searchInChildren": search_in_children, - "searchInactive": search_inactive, - "componentName": component_name, - "includeNonPublicSerialized": includeNonPublicSerialized - } - params = {k: v for k, v in params.items() if v is not None} - - # --- Handle Prefab Path Logic --- - if action == "create" and params.get("saveAsPrefab"): # Check if 'saveAsPrefab' is explicitly True in params - if "prefabPath" not in params: - if "name" not in params or not params["name"]: - return {"success": False, "message": "Cannot create default prefab path: 'name' parameter is missing."} - # Use the provided prefab_folder (which has a default) and the name to construct the path - constructed_path = f"{prefab_folder}/{params['name']}.prefab" - # Ensure clean path separators (Unity prefers '/') - params["prefabPath"] = constructed_path.replace("\\", "/") - elif not params["prefabPath"].lower().endswith(".prefab"): - return {"success": False, "message": f"Invalid prefab_path: '{params['prefabPath']}' must end with .prefab"} - # Ensure prefab_folder itself isn't sent if prefabPath was constructed or provided - # The C# side only needs the final prefabPath - params.pop("prefab_folder", None) - # -------------------------------- - - # Send the command to Unity via the established connection - # Use the get_unity_connection function to retrieve the active connection instance - # Changed "MANAGE_GAMEOBJECT" to "manage_gameobject" to potentially match Unity expectation - response = get_unity_connection().send_command("manage_gameobject", params) - - # Check if the response indicates success - # If the response is not successful, raise an exception with the error message - if response.get("success"): - return {"success": True, "message": response.get("message", "GameObject operation successful."), "data": response.get("data")} - else: - return {"success": False, "message": response.get("error", "An unknown error occurred during GameObject management.")} - - except Exception as e: - return {"success": False, "message": f"Python error managing GameObject: {str(e)}"} \ No newline at end of file diff --git a/UnityMcpServer/src/tools/manage_scene.py b/UnityMcpServer/src/tools/manage_scene.py deleted file mode 100644 index 44981f6..0000000 --- a/UnityMcpServer/src/tools/manage_scene.py +++ /dev/null @@ -1,47 +0,0 @@ -from mcp.server.fastmcp import FastMCP, Context -from typing import Dict, Any -from unity_connection import get_unity_connection - -def register_manage_scene_tools(mcp: FastMCP): - """Register all scene management tools with the MCP server.""" - - @mcp.tool() - def manage_scene( - ctx: Context, - action: str, - name: str, - path: str, - build_index: int, - ) -> Dict[str, Any]: - """Manages Unity scenes (load, save, create, get hierarchy, etc.). - - Args: - action: Operation (e.g., 'load', 'save', 'create', 'get_hierarchy'). - name: Scene name (no extension) for create/load/save. - path: Asset path for scene operations (default: "Assets/"). - build_index: Build index for load/build settings actions. - # Add other action-specific args as needed (e.g., for hierarchy depth) - - Returns: - Dictionary with results ('success', 'message', 'data'). - """ - try: - params = { - "action": action, - "name": name, - "path": path, - "buildIndex": build_index - } - params = {k: v for k, v in params.items() if v is not None} - - # Send command to Unity - response = get_unity_connection().send_command("manage_scene", params) - - # Process response - if response.get("success"): - return {"success": True, "message": response.get("message", "Scene operation successful."), "data": response.get("data")} - else: - return {"success": False, "message": response.get("error", "An unknown error occurred during scene management.")} - - except Exception as e: - return {"success": False, "message": f"Python error managing scene: {str(e)}"} \ No newline at end of file diff --git a/UnityMcpServer/src/tools/manage_script.py b/UnityMcpServer/src/tools/manage_script.py deleted file mode 100644 index 22e0953..0000000 --- a/UnityMcpServer/src/tools/manage_script.py +++ /dev/null @@ -1,74 +0,0 @@ -from mcp.server.fastmcp import FastMCP, Context -from typing import Dict, Any -from unity_connection import get_unity_connection -import os -import base64 - -def register_manage_script_tools(mcp: FastMCP): - """Register all script management tools with the MCP server.""" - - @mcp.tool() - def manage_script( - ctx: Context, - action: str, - name: str, - path: str, - contents: str, - script_type: str, - namespace: str - ) -> Dict[str, Any]: - """Manages C# scripts in Unity (create, read, update, delete). - Make reference variables public for easier access in the Unity Editor. - - Args: - action: Operation ('create', 'read', 'update', 'delete'). - name: Script name (no .cs extension). - path: Asset path (default: "Assets/"). - contents: C# code for 'create'/'update'. - script_type: Type hint (e.g., 'MonoBehaviour'). - namespace: Script namespace. - - Returns: - Dictionary with results ('success', 'message', 'data'). - """ - try: - # Prepare parameters for Unity - params = { - "action": action, - "name": name, - "path": path, - "namespace": namespace, - "scriptType": script_type - } - - # Base64 encode the contents if they exist to avoid JSON escaping issues - if contents is not None: - if action in ['create', 'update']: - # Encode content for safer transmission - params["encodedContents"] = base64.b64encode(contents.encode('utf-8')).decode('utf-8') - params["contentsEncoded"] = True - else: - params["contents"] = contents - - # Remove None values so they don't get sent as null - params = {k: v for k, v in params.items() if v is not None} - - # Send command to Unity - response = get_unity_connection().send_command("manage_script", params) - - # Process response from Unity - if response.get("success"): - # If the response contains base64 encoded content, decode it - if response.get("data", {}).get("contentsEncoded"): - decoded_contents = base64.b64decode(response["data"]["encodedContents"]).decode('utf-8') - response["data"]["contents"] = decoded_contents - del response["data"]["encodedContents"] - del response["data"]["contentsEncoded"] - - return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")} - else: - return {"success": False, "message": response.get("error", "An unknown error occurred.")} - - except Exception as e: - # Handle Python-side errors (e.g., connection issues) - return {"success": False, "message": f"Python error managing script: {str(e)}"} \ No newline at end of file diff --git a/UnityMcpServer/src/tools/manage_shader.py b/UnityMcpServer/src/tools/manage_shader.py deleted file mode 100644 index c447a3a..0000000 --- a/UnityMcpServer/src/tools/manage_shader.py +++ /dev/null @@ -1,67 +0,0 @@ -from mcp.server.fastmcp import FastMCP, Context -from typing import Dict, Any -from unity_connection import get_unity_connection -import os -import base64 - -def register_manage_shader_tools(mcp: FastMCP): - """Register all shader script management tools with the MCP server.""" - - @mcp.tool() - def manage_shader( - ctx: Context, - action: str, - name: str, - path: str, - contents: str, - ) -> Dict[str, Any]: - """Manages shader scripts in Unity (create, read, update, delete). - - Args: - action: Operation ('create', 'read', 'update', 'delete'). - name: Shader name (no .cs extension). - path: Asset path (default: "Assets/"). - contents: Shader code for 'create'/'update'. - - Returns: - Dictionary with results ('success', 'message', 'data'). - """ - try: - # Prepare parameters for Unity - params = { - "action": action, - "name": name, - "path": path, - } - - # Base64 encode the contents if they exist to avoid JSON escaping issues - if contents is not None: - if action in ['create', 'update']: - # Encode content for safer transmission - params["encodedContents"] = base64.b64encode(contents.encode('utf-8')).decode('utf-8') - params["contentsEncoded"] = True - else: - params["contents"] = contents - - # Remove None values so they don't get sent as null - params = {k: v for k, v in params.items() if v is not None} - - # Send command to Unity - response = get_unity_connection().send_command("manage_shader", params) - - # Process response from Unity - if response.get("success"): - # If the response contains base64 encoded content, decode it - if response.get("data", {}).get("contentsEncoded"): - decoded_contents = base64.b64decode(response["data"]["encodedContents"]).decode('utf-8') - response["data"]["contents"] = decoded_contents - del response["data"]["encodedContents"] - del response["data"]["contentsEncoded"] - - return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")} - else: - return {"success": False, "message": response.get("error", "An unknown error occurred.")} - - except Exception as e: - # Handle Python-side errors (e.g., connection issues) - return {"success": False, "message": f"Python error managing shader: {str(e)}"} \ No newline at end of file diff --git a/UnityMcpServer/src/tools/read_console.py b/UnityMcpServer/src/tools/read_console.py deleted file mode 100644 index 3d4bd12..0000000 --- a/UnityMcpServer/src/tools/read_console.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Defines the read_console tool for accessing Unity Editor console messages. -""" -from typing import List, Dict, Any -from mcp.server.fastmcp import FastMCP, Context -from unity_connection import get_unity_connection - -def register_read_console_tools(mcp: FastMCP): - """Registers the read_console tool with the MCP server.""" - - @mcp.tool() - def read_console( - ctx: Context, - action: str = None, - types: List[str] = None, - count: int = None, - filter_text: str = None, - since_timestamp: str = None, - format: str = None, - include_stacktrace: bool = None - ) -> Dict[str, Any]: - """Gets messages from or clears the Unity Editor console. - - Args: - ctx: The MCP context. - action: Operation ('get' or 'clear'). - types: Message types to get ('error', 'warning', 'log', 'all'). - count: Max messages to return. - filter_text: Text filter for messages. - since_timestamp: Get messages after this timestamp (ISO 8601). - format: Output format ('plain', 'detailed', 'json'). - include_stacktrace: Include stack traces in output. - - Returns: - Dictionary with results. For 'get', includes 'data' (messages). - """ - - # Get the connection instance - bridge = get_unity_connection() - - # 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 - - # Normalize action if it's a string - if isinstance(action, str): - action = action.lower() - - # Prepare parameters for the C# handler - params_dict = { - "action": action, - "types": types, - "count": count, - "filterText": filter_text, - "sinceTimestamp": since_timestamp, - "format": format.lower() if isinstance(format, str) else format, - "includeStacktrace": include_stacktrace - } - - # Remove None values unless it's 'count' (as None might mean 'all') - params_dict = {k: v for k, v in params_dict.items() if v is not None or k == 'count'} - - # Add count back if it was None, explicitly sending null might be important for C# logic - if 'count' not in params_dict: - params_dict['count'] = None - - # Forward the command using the bridge's send_command method - return bridge.send_command("read_console", params_dict) \ No newline at end of file diff --git a/UnityMcpServer/src/unity_connection.py b/UnityMcpServer/src/unity_connection.py deleted file mode 100644 index dbf7703..0000000 --- a/UnityMcpServer/src/unity_connection.py +++ /dev/null @@ -1,239 +0,0 @@ -import socket -import json -import logging -from dataclasses import dataclass -from pathlib import Path -import time -from typing import Dict, Any -from config import config -from port_discovery import PortDiscovery - -# Configure logging using settings from config -logging.basicConfig( - level=getattr(logging, config.log_level), - format=config.log_format -) -logger = logging.getLogger("unity-mcp-server") - -@dataclass -class UnityConnection: - """Manages the socket connection to the Unity Editor.""" - host: str = config.unity_host - port: int = None # Will be set dynamically - sock: socket.socket = None # Socket for Unity communication - - def __post_init__(self): - """Set port from discovery if not explicitly provided""" - if self.port is None: - self.port = PortDiscovery.discover_unity_port() - - def connect(self) -> bool: - """Establish a connection to the Unity Editor.""" - if self.sock: - return True - try: - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.connect((self.host, self.port)) - logger.info(f"Connected to Unity at {self.host}:{self.port}") - return True - except Exception as e: - logger.error(f"Failed to connect to Unity: {str(e)}") - self.sock = None - return False - - def disconnect(self): - """Close the connection to the Unity Editor.""" - if self.sock: - try: - self.sock.close() - except Exception as e: - logger.error(f"Error disconnecting from Unity: {str(e)}") - finally: - self.sock = None - - def receive_full_response(self, sock, buffer_size=config.buffer_size) -> bytes: - """Receive a complete response from Unity, handling chunked data.""" - chunks = [] - sock.settimeout(config.connection_timeout) # Use timeout from config - try: - while True: - chunk = sock.recv(buffer_size) - if not chunk: - if not chunks: - raise Exception("Connection closed before receiving data") - break - chunks.append(chunk) - - # Process the data received so far - data = b''.join(chunks) - decoded_data = data.decode('utf-8') - - # Check if we've received a complete response - try: - # Special case for ping-pong - if decoded_data.strip().startswith('{"status":"success","result":{"message":"pong"'): - logger.debug("Received ping response") - return data - - # Handle escaped quotes in the content - if '"content":' in decoded_data: - # Find the content field and its value - content_start = decoded_data.find('"content":') + 9 - content_end = decoded_data.rfind('"', content_start) - if content_end > content_start: - # Replace escaped quotes in content with regular quotes - content = decoded_data[content_start:content_end] - content = content.replace('\\"', '"') - decoded_data = decoded_data[:content_start] + content + decoded_data[content_end:] - - # Validate JSON format - json.loads(decoded_data) - - # If we get here, we have valid JSON - logger.info(f"Received complete response ({len(data)} bytes)") - return data - except json.JSONDecodeError: - # We haven't received a complete valid JSON response yet - continue - except Exception as e: - logger.warning(f"Error processing response chunk: {str(e)}") - # Continue reading more chunks as this might not be the complete response - continue - except socket.timeout: - logger.warning("Socket timeout during receive") - raise Exception("Timeout receiving Unity response") - except Exception as e: - logger.error(f"Error during receive: {str(e)}") - raise - - def send_command(self, command_type: str, params: Dict[str, Any] = None) -> Dict[str, Any]: - """Send a command with retry/backoff and port rediscovery. Pings only when requested.""" - attempts = max(config.max_retries, 5) - base_backoff = max(0.5, config.retry_delay) - - def read_status_file() -> dict | None: - try: - status_files = sorted(Path.home().joinpath('.unity-mcp').glob('unity-mcp-status-*.json'), key=lambda p: p.stat().st_mtime, reverse=True) - if not status_files: - return None - latest = status_files[0] - with latest.open('r') as f: - return json.load(f) - except Exception: - return None - - last_short_timeout = None - - for attempt in range(attempts + 1): - try: - # Ensure connected - if not self.sock: - # During retries use short connect timeout - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.settimeout(1.0) - self.sock.connect((self.host, self.port)) - # restore steady-state timeout for receive - self.sock.settimeout(config.connection_timeout) - logger.info(f"Connected to Unity at {self.host}:{self.port}") - - # Build payload - if command_type == 'ping': - payload = b'ping' - else: - command = {"type": command_type, "params": params or {}} - payload = json.dumps(command, ensure_ascii=False).encode('utf-8') - - # Send - self.sock.sendall(payload) - - # During retry bursts use a short receive timeout - if attempt > 0 and last_short_timeout is None: - last_short_timeout = self.sock.gettimeout() - self.sock.settimeout(1.0) - response_data = self.receive_full_response(self.sock) - # restore steady-state timeout if changed - if last_short_timeout is not None: - self.sock.settimeout(config.connection_timeout) - last_short_timeout = None - - # Parse - if command_type == 'ping': - resp = json.loads(response_data.decode('utf-8')) - if resp.get('status') == 'success' and resp.get('result', {}).get('message') == 'pong': - return {"message": "pong"} - raise Exception("Ping unsuccessful") - - resp = json.loads(response_data.decode('utf-8')) - if resp.get('status') == 'error': - err = resp.get('error') or resp.get('message', 'Unknown Unity error') - raise Exception(err) - return resp.get('result', {}) - except Exception as e: - logger.warning(f"Unity communication attempt {attempt+1} failed: {e}") - try: - if self.sock: - self.sock.close() - finally: - self.sock = None - - # Re-discover port each time - try: - new_port = PortDiscovery.discover_unity_port() - if new_port != self.port: - logger.info(f"Unity port changed {self.port} -> {new_port}") - self.port = new_port - except Exception as de: - logger.debug(f"Port discovery failed: {de}") - - if attempt < attempts: - # If heartbeat indicates reload, keep retries snappy without spamming - status = read_status_file() - backoff = base_backoff * (2 ** attempt) - sleep_s = min(backoff, 3.0) - if status and (status.get('reloading') or status.get('unity_port') == self.port): - sleep_s = min(sleep_s, 0.8) - time.sleep(sleep_s) - continue - raise - -# Global Unity connection -_unity_connection = None - -def get_unity_connection() -> UnityConnection: - """Retrieve or establish a persistent Unity connection.""" - global _unity_connection - if _unity_connection is not None: - try: - # Try to ping with a short timeout to verify connection - result = _unity_connection.send_command("ping") - # If we get here, the connection is still valid - logger.debug("Reusing existing Unity connection") - return _unity_connection - except Exception as e: - logger.warning(f"Existing connection failed: {str(e)}") - try: - _unity_connection.disconnect() - except: - pass - _unity_connection = None - - # Create a new connection - logger.info("Creating new Unity connection") - _unity_connection = UnityConnection() - if not _unity_connection.connect(): - _unity_connection = None - raise ConnectionError("Could not connect to Unity. Ensure the Unity Editor and MCP Bridge are running.") - - try: - # Verify the new connection works - _unity_connection.send_command("ping") - logger.info("Successfully established new Unity connection") - return _unity_connection - except Exception as e: - logger.error(f"Could not verify new connection: {str(e)}") - try: - _unity_connection.disconnect() - except: - pass - _unity_connection = None - raise ConnectionError(f"Could not establish valid Unity connection: {str(e)}") diff --git a/UnityMcpServer/src/uv.lock b/UnityMcpServer/src/uv.lock deleted file mode 100644 index de0cd44..0000000 --- a/UnityMcpServer/src/uv.lock +++ /dev/null @@ -1,400 +0,0 @@ -version = 1 -revision = 1 -requires-python = ">=3.10" - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, -] - -[[package]] -name = "anyio" -version = "4.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "idna" }, - { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, -] - -[[package]] -name = "certifi" -version = "2025.1.31" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, -] - -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, -] - -[[package]] -name = "h11" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, -] - -[[package]] -name = "httpcore" -version = "1.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, -] - -[[package]] -name = "httpx-sse" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, -] - -[[package]] -name = "mcp" -version = "1.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "uvicorn" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/cc/5c5bb19f1a0f8f89a95e25cb608b0b07009e81fd4b031e519335404e1422/mcp-1.4.1.tar.gz", hash = "sha256:b9655d2de6313f9d55a7d1df62b3c3fe27a530100cc85bf23729145b0dba4c7a", size = 154942 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/0e/885f156ade60108e67bf044fada5269da68e29d758a10b0c513f4d85dd76/mcp-1.4.1-py3-none-any.whl", hash = "sha256:a7716b1ec1c054e76f49806f7d96113b99fc1166fc9244c2c6f19867cb75b593", size = 72448 }, -] - -[package.optional-dependencies] -cli = [ - { name = "python-dotenv" }, - { name = "typer" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, -] - -[[package]] -name = "pydantic" -version = "2.10.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, -] - -[[package]] -name = "pydantic-core" -version = "2.27.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, - { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, - { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, - { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, - { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, - { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, - { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, - { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, - { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, - { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, - { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, - { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, - { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, - { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, - { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, - { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, - { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, - { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, - { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, - { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, - { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, - { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, - { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, - { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, - { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, - { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, - { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, - { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, - { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, - { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, - { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, - { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, - { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, - { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, - { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, - { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, - { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, - { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, - { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, - { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, - { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, - { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, - { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, - { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, - { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, - { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, - { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, - { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, - { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, - { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, - { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, - { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, - { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, - { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, - { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, - { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, - { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, - { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, - { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, - { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, - { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, - { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, - { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, - { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, -] - -[[package]] -name = "pydantic-settings" -version = "2.8.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, -] - -[[package]] -name = "pygments" -version = "2.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, -] - -[[package]] -name = "python-dotenv" -version = "1.0.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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, -] - -[[package]] -name = "rich" -version = "13.9.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, -] - -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, -] - -[[package]] -name = "sse-starlette" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, -] - -[[package]] -name = "starlette" -version = "0.46.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, -] - -[[package]] -name = "typer" -version = "0.15.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, -] - -[[package]] -name = "unitymcpserver" -version = "2.0.0" -source = { editable = "." } -dependencies = [ - { name = "httpx" }, - { name = "mcp", extra = ["cli"] }, -] - -[package.metadata] -requires-dist = [ - { name = "httpx", specifier = ">=0.27.2" }, - { name = "mcp", extras = ["cli"], specifier = ">=1.4.1" }, -] - -[[package]] -name = "uvicorn" -version = "0.34.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, -] diff --git a/deploy-dev.bat b/deploy-dev.bat index 6a83fcf..2b04c22 100644 --- a/deploy-dev.bat +++ b/deploy-dev.bat @@ -9,7 +9,7 @@ echo. :: Configuration set "SCRIPT_DIR=%~dp0" set "BRIDGE_SOURCE=%SCRIPT_DIR%UnityMcpBridge" -set "SERVER_SOURCE=%SCRIPT_DIR%UnityMcpServer\src" +set "SERVER_SOURCE=%SCRIPT_DIR%UnityMcpBridge\UnityMcpServer~\src" set "DEFAULT_BACKUP_DIR=%USERPROFILE%\Desktop\unity-mcp-backup" set "DEFAULT_SERVER_PATH=%LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer\src" diff --git a/restore-dev.bat b/restore-dev.bat index 69d9312..553ccc1 100644 --- a/restore-dev.bat +++ b/restore-dev.bat @@ -5,6 +5,9 @@ echo =============================================== echo Unity MCP Development Restore Script echo =============================================== echo. +echo Note: The Python server is bundled under UnityMcpBridge\UnityMcpServer~ in the package. +echo This script restores your installed server path from backups, not the repo copy. +echo. :: Configuration set "DEFAULT_BACKUP_DIR=%USERPROFILE%\Desktop\unity-mcp-backup"