[
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish to PyPI\n\non:\n  release:\n    types: [published]\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n    - name: Set up Python\n      uses: actions/setup-python@v4\n      with:\n        python-version: '3.10'\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install build\n    - name: Build package\n      run: python -m build\n    - name: Publish package\n      uses: pypa/gh-action-pypi-publish@v1.8.10\n      with:\n        password: ${{ secrets.PYPI_API_TOKEN }} "
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: Publish to PyPI\n\non:\n  release:\n    types: [created]\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v3\n    \n    - name: Set up Python\n      uses: actions/setup-python@v4\n      with:\n        python-version: '3.x'\n    \n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install build twine\n    \n    - name: Build package\n      run: python -m build\n    \n    - name: Publish to PyPI\n      env:\n        TWINE_USERNAME: __token__\n        TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}\n      run: |\n        python -m twine upload dist/* "
  },
  {
    "path": ".gitignore",
    "content": "# Python\n__pycache__/\n*.py[cod]\n*$py.class\n*.so\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# Virtual Environment\nvenv/\nENV/\nenv/\n.env\n.venv\nenv.bak/\nvenv.bak/\n\n# IDE specific files\n.idea/\n.vscode/\n*.swp\n*.swo\n.DS_Store\n*.sublime-workspace\n*.sublime-project\n\n# Testing\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n.pytest_cache/\nhtmlcov/\n\n# Distribution / packaging\n.Python\n*.manifest\n*.spec\npip-log.txt\npip-delete-this-directory.txt\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Logs and databases\n*.log\n*.sqlite\n*.db\n\nuv.lock"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 VAST-AI-Research\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Tripo MCP Server\n\nTripo MCP provides an interface between AI assistants and [Tripo AI](https://www.tripo3d.ai) via [Model Context Protocol (MCP)](https://github.com/anthropics/anthropic-cookbook/tree/main/mcp). \n\n> **Note:** This project is in alpha. Currently, it supports Tripo Blender Addon integration.\n\n## Current Features\n\n- Generate 3D asset from natural language using Tripo's API and import to Blender\n- Compatible with Claude and other MCP-enabled AI assistants\n\n## Quick Start\n\n### Prerequisites\n- Python 3.10+\n- [Blender](https://www.blender.org/download/)\n- [Tripo AI Blender Addon](https://www.tripo3d.ai/app/home)\n- Claude for Desktop or Cursor IDE\n\n### Installation\n\n1. Install Tripo AI Blender Addon from [Tripo AI's website](https://www.tripo3d.ai/app/home)\n\n2. Configure the MCP server in Claude Desktop or Cursor.\n\n    * `pip install uv`\n    * set mcp in cursor\n    ```json\n    {\n      \"mcpServers\": {\n        \"tripo-mcp\": {\n          \"command\": \"uvx\",\n          \"args\": [\n            \"tripo-mcp\"\n          ]\n        }\n      }\n    }\n    ```\n\n    * Then you will get a green dot like this:\n      ![img](succeed.jpg)\n\n### Usage\n\n1. Enable Tripo AI Blender Addon and start blender mcp server.\n\n2. Chat using cursor or claude. E.g., \"Generate a 3D model of a futuristic chair\".\n\n## Acknowledgements\n\n- **[Tripo AI](https://www.tripo3d.ai)**\n- **[blender-mcp](https://github.com/ahujasid/blender-mcp)** by [Siddharth Ahuja](https://github.com/ahujasid)\n\n**Special Thanks**  \nSpecial thanks to Siddharth Ahuja for the blender-mcp project, which provided inspiring ideas for MCP + 3D.\n"
  },
  {
    "path": "main.py",
    "content": "from src.server import main as server_main\n\n\nif __name__ == \"__main__\":\n    \"\"\"Entry point for the Tripo MCP package\"\"\"\n    server_main()"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"tripo-mcp\"\nversion = \"0.1.2\"\ndescription = \"MCP (Model Control Protocol) integration for Tripo\"\nauthors = [\n    {name = \"Allen Dang\", email = \"allen@vastai3d.com\"},\n    {name = \"pookiefoof\", email = \"caoyanpei@gmail.com\"},\n    {name = \"Ding Liang\", email = \"liangding@vastai3d.com\"},\n]\nlicense = {text = \"MIT\"}\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nkeywords = [\"mcp\", \"blender\", \"3d\", \"automation\"]\nclassifiers = [\n    \"Development Status :: 3 - Alpha\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n]\n\ndependencies = [\n    \"tripo3d>=0.2.0\",\n    \"mcp[cli]>=1.4.1\",\n]\n\n[project.optional-dependencies]\ndev = [\n]\n\n[project.scripts]\ntripo-mcp = \"server:main\"\n\n[build-system]\nrequires = [\"setuptools>=61.0\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n"
  },
  {
    "path": "src/server.py",
    "content": "\"\"\"\nMIT License\n\nCopyright (c) 2025 Siddharth Ahuja\nCopyright (c) 2025 for additions\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\nThis file is based on work by Siddharth Ahuja, with additional contributions\nfor Tripo MCP functionality.\n\"\"\"\n\nfrom mcp.server.fastmcp import FastMCP, Context\nimport sys\nfrom pathlib import Path\nfrom typing import Dict, Any\nimport socket\nimport json\nimport logging\nfrom dataclasses import dataclass\nfrom contextlib import asynccontextmanager\nfrom typing import AsyncIterator, Dict, Any, List\n\n# Configure logging\nlogging.basicConfig(\n    level=logging.INFO, format=\"%(asctime)s - %(name)s - %(levelname)s - %(message)s\"\n)\nlogger = logging.getLogger(\"BlenderMCPServer\")\n\nsys.path.insert(0, str(Path(__file__).parent.parent))\n\nfrom tripo3d import TripoClient, TaskStatus\n\n\n@dataclass\nclass BlenderConnection:\n    host: str\n    port: int\n    sock: socket.socket = (\n        None  # Changed from 'socket' to 'sock' to avoid naming conflict\n    )\n\n    def connect(self) -> bool:\n        \"\"\"Connect to the Blender addon socket server\"\"\"\n        if self.sock:\n            return True\n\n        try:\n            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n            self.sock.connect((self.host, self.port))\n            logger.info(f\"Connected to Blender at {self.host}:{self.port}\")\n            return True\n        except Exception as e:\n            logger.error(f\"Failed to connect to Blender: {str(e)}\")\n            self.sock = None\n            return False\n\n    def disconnect(self):\n        \"\"\"Disconnect from the Blender addon\"\"\"\n        if self.sock:\n            try:\n                self.sock.close()\n            except Exception as e:\n                logger.error(f\"Error disconnecting from Blender: {str(e)}\")\n            finally:\n                self.sock = None\n\n    def receive_full_response(self, sock, buffer_size=8192):\n        \"\"\"Receive the complete response, potentially in multiple chunks\"\"\"\n        chunks = []\n        # Use a consistent timeout value that matches the addon's timeout\n        sock.settimeout(15.0)  # Match the addon's timeout\n\n        try:\n            while True:\n                try:\n                    chunk = sock.recv(buffer_size)\n                    if not chunk:\n                        # If we get an empty chunk, the connection might be closed\n                        if (\n                            not chunks\n                        ):  # If we haven't received anything yet, this is an error\n                            raise Exception(\n                                \"Connection closed before receiving any data\"\n                            )\n                        break\n\n                    chunks.append(chunk)\n\n                    # Check if we've received a complete JSON object\n                    try:\n                        data = b\"\".join(chunks)\n                        json.loads(data.decode(\"utf-8\"))\n                        # If we get here, it parsed successfully\n                        logger.info(f\"Received complete response ({len(data)} bytes)\")\n                        return data\n                    except json.JSONDecodeError:\n                        # Incomplete JSON, continue receiving\n                        continue\n                except socket.timeout:\n                    # If we hit a timeout during receiving, break the loop and try to use what we have\n                    logger.warning(\"Socket timeout during chunked receive\")\n                    break\n                except (ConnectionError, BrokenPipeError, ConnectionResetError) as e:\n                    logger.error(f\"Socket connection error during receive: {str(e)}\")\n                    raise  # Re-raise to be handled by the caller\n        except socket.timeout:\n            logger.warning(\"Socket timeout during chunked receive\")\n        except Exception as e:\n            logger.error(f\"Error during receive: {str(e)}\")\n            raise\n\n        # If we get here, we either timed out or broke out of the loop\n        # Try to use what we have\n        if chunks:\n            data = b\"\".join(chunks)\n            logger.info(f\"Returning data after receive completion ({len(data)} bytes)\")\n            try:\n                # Try to parse what we have\n                json.loads(data.decode(\"utf-8\"))\n                return data\n            except json.JSONDecodeError:\n                # If we can't parse it, it's incomplete\n                raise Exception(\"Incomplete JSON response received\")\n        else:\n            raise Exception(\"No data received\")\n\n    def send_command(\n        self, command_type: str, params: Dict[str, Any] = None\n    ) -> Dict[str, Any]:\n        \"\"\"Send a command to Blender and return the response\"\"\"\n        if not self.sock and not self.connect():\n            raise ConnectionError(\"Not connected to Blender\")\n\n        command = {\"type\": command_type, \"params\": params or {}}\n\n        try:\n            # Log the command being sent\n            logger.info(f\"Sending command: {command_type} with params: {params}\")\n\n            # Send the command\n            self.sock.sendall(json.dumps(command).encode(\"utf-8\"))\n            logger.info(f\"Command sent, waiting for response...\")\n\n            # Set a timeout for receiving - use the same timeout as in receive_full_response\n            self.sock.settimeout(15.0)  # Match the addon's timeout\n\n            # Receive the response using the improved receive_full_response method\n            response_data = self.receive_full_response(self.sock)\n            logger.info(f\"Received {len(response_data)} bytes of data\")\n\n            response = json.loads(response_data.decode(\"utf-8\"))\n            logger.info(f\"Response parsed, status: {response.get('status', 'unknown')}\")\n\n            if response.get(\"status\") == \"error\":\n                logger.error(f\"Blender error: {response.get('message')}\")\n                raise Exception(response.get(\"message\", \"Unknown error from Blender\"))\n\n            return response.get(\"result\", {})\n        except socket.timeout:\n            logger.error(\"Socket timeout while waiting for response from Blender\")\n            # Don't try to reconnect here - let the get_blender_connection handle reconnection\n            # Just invalidate the current socket so it will be recreated next time\n            self.sock = None\n            raise Exception(\n                \"Timeout waiting for Blender response - try simplifying your request\"\n            )\n        except (ConnectionError, BrokenPipeError, ConnectionResetError) as e:\n            logger.error(f\"Socket connection error: {str(e)}\")\n            self.sock = None\n            raise Exception(f\"Connection to Blender lost: {str(e)}\")\n        except json.JSONDecodeError as e:\n            logger.error(f\"Invalid JSON response from Blender: {str(e)}\")\n            # Try to log what was received\n            if \"response_data\" in locals() and response_data:\n                logger.error(f\"Raw response (first 200 bytes): {response_data[:200]}\")\n            raise Exception(f\"Invalid response from Blender: {str(e)}\")\n        except Exception as e:\n            logger.error(f\"Error communicating with Blender: {str(e)}\")\n            # Don't try to reconnect here - let the get_blender_connection handle reconnection\n            self.sock = None\n            raise Exception(f\"Communication error with Blender: {str(e)}\")\n\n\n@asynccontextmanager\nasync def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:\n    \"\"\"Manage server startup and shutdown lifecycle\"\"\"\n    # We don't need to create a connection here since we're using the global connection\n    # for resources and tools\n\n    try:\n        # Just log that we're starting up\n        logger.info(\"Blender server starting up\")\n\n        # Try to connect to Blender on startup to verify it's available\n        try:\n            # This will initialize the global connection if needed\n            blender = get_blender_connection()\n            logger.info(\"Successfully connected to Blender on startup\")\n        except Exception as e:\n            logger.warning(f\"Could not connect to Blender on startup: {str(e)}\")\n            logger.warning(\n                \"Make sure the Blender addon is running before using Blender resources or tools\"\n            )\n\n        # Return an empty context - we're using the global connection\n        yield {}\n    finally:\n        # Clean up the global connection on shutdown\n        global _blender_connection\n        if _blender_connection:\n            logger.info(\"Disconnecting from Blender on shutdown\")\n            _blender_connection.disconnect()\n            _blender_connection = None\n        logger.info(\"BlenderMCP server shut down\")\n\n\nmcp = FastMCP(\n    name=\"Tripo MCP\",\n    instructions=\"MCP for Tripo Blender addon\",\n    lifespan=server_lifespan,\n    # host=\"127.0.0.1\",\n    # port=8392,\n)\n\n_blender_connection = None\n_polyhaven_enabled = False  # Add this global variable\n_tripo_apikey = \"\"\n\n\ndef get_blender_connection():\n    \"\"\"Get or create a persistent Blender connection\"\"\"\n    global _blender_connection, _polyhaven_enabled, _tripo_apikey  # Add _polyhaven_enabled to globals\n\n    # If we have an existing connection, check if it's still valid\n    if _blender_connection is not None:\n        try:\n            # First check if PolyHaven is enabled by sending a ping command\n            result = _blender_connection.send_command(\"get_tripo_apikey\")\n            _tripo_apikey = result.get(\"api_key\", \"\")\n            result = _blender_connection.send_command(\"get_polyhaven_status\")\n            # Store the PolyHaven status globally\n            _polyhaven_enabled = result.get(\"enabled\", False)\n\n            return _blender_connection\n        except Exception as e:\n            # Connection is dead, close it and create a new one\n            logger.warning(f\"Existing connection is no longer valid: {str(e)}\")\n            try:\n                _blender_connection.disconnect()\n            except:\n                pass\n            _blender_connection = None\n\n    # Create a new connection if needed\n    if _blender_connection is None:\n        _blender_connection = BlenderConnection(host=\"localhost\", port=9876)\n        if not _blender_connection.connect():\n            logger.error(\"Failed to connect to Blender\")\n            _blender_connection = None\n            raise Exception(\n                \"Could not connect to Blender. Make sure the Blender addon is running.\"\n            )\n        logger.info(\"Created new persistent connection to Blender\")\n\n    return _blender_connection\n\n\n@mcp.tool()\ndef get_scene_info(ctx: Context) -> str:\n    \"\"\"Get detailed information about the current Blender scene\"\"\"\n    try:\n        blender = get_blender_connection()\n        result = blender.send_command(\"get_scene_info\")\n\n        # Just return the JSON representation of what Blender sent us\n        return json.dumps(result, indent=2)\n    except Exception as e:\n        logger.error(f\"Error getting scene info from Blender: {str(e)}\")\n        return f\"Error getting scene info: {str(e)}\"\n\n\n@mcp.tool()\ndef get_object_info(ctx: Context, object_name: str) -> str:\n    \"\"\"\n    Get detailed information about a specific object in the Blender scene.\n\n    Parameters:\n    - object_name: The name of the object to get information about\n    \"\"\"\n    try:\n        blender = get_blender_connection()\n        result = blender.send_command(\"get_object_info\", {\"name\": object_name})\n\n        # Just return the JSON representation of what Blender sent us\n        return json.dumps(result, indent=2)\n    except Exception as e:\n        logger.error(f\"Error getting object info from Blender: {str(e)}\")\n        return f\"Error getting object info: {str(e)}\"\n\n\n@mcp.tool()\ndef create_object(\n    ctx: Context,\n    type: str = \"CUBE\",\n    name: str = None,\n    location: List[float] = None,\n    rotation: List[float] = None,\n    scale: List[float] = None,\n) -> str:\n    \"\"\"\n    Create a new object in the Blender scene.\n\n    Parameters:\n    - type: Object type (CUBE, SPHERE, CYLINDER, PLANE, CONE, TORUS, EMPTY, CAMERA, LIGHT)\n    - name: Optional name for the object\n    - location: Optional [x, y, z] location coordinates\n    - rotation: Optional [x, y, z] rotation in radians\n    - scale: Optional [x, y, z] scale factors\n    \"\"\"\n    try:\n        # Get the global connection\n        blender = get_blender_connection()\n\n        # Set default values for missing parameters\n        loc = location or [0, 0, 0]\n        rot = rotation or [0, 0, 0]\n        sc = scale or [1, 1, 1]\n\n        params = {\"type\": type, \"location\": loc, \"rotation\": rot, \"scale\": sc}\n\n        if name:\n            params[\"name\"] = name\n\n        result = blender.send_command(\"create_object\", params)\n        return f\"Created {type} object: {result['name']}\"\n    except Exception as e:\n        logger.error(f\"Error creating object: {str(e)}\")\n        return f\"Error creating object: {str(e)}\"\n\n\n@mcp.tool()\ndef modify_object(\n    ctx: Context,\n    name: str,\n    location: List[float] = None,\n    rotation: List[float] = None,\n    scale: List[float] = None,\n    visible: bool = None,\n) -> str:\n    \"\"\"\n    Modify an existing object in the Blender scene.\n\n    Parameters:\n    - name: Name of the object to modify\n    - location: Optional [x, y, z] location coordinates\n    - rotation: Optional [x, y, z] rotation in radians\n    - scale: Optional [x, y, z] scale factors\n    - visible: Optional boolean to set visibility\n    \"\"\"\n    try:\n        # Get the global connection\n        blender = get_blender_connection()\n\n        params = {\"name\": name}\n\n        if location is not None:\n            params[\"location\"] = location\n        if rotation is not None:\n            params[\"rotation\"] = rotation\n        if scale is not None:\n            params[\"scale\"] = scale\n        if visible is not None:\n            params[\"visible\"] = visible\n\n        result = blender.send_command(\"modify_object\", params)\n        return f\"Modified object: {result['name']}\"\n    except Exception as e:\n        logger.error(f\"Error modifying object: {str(e)}\")\n        return f\"Error modifying object: {str(e)}\"\n\n\n@mcp.tool()\ndef delete_object(ctx: Context, name: str) -> str:\n    \"\"\"\n    Delete an object from the Blender scene.\n\n    Parameters:\n    - name: Name of the object to delete\n    \"\"\"\n    try:\n        # Get the global connection\n        blender = get_blender_connection()\n\n        result = blender.send_command(\"delete_object\", {\"name\": name})\n        return f\"Deleted object: {name}\"\n    except Exception as e:\n        logger.error(f\"Error deleting object: {str(e)}\")\n        return f\"Error deleting object: {str(e)}\"\n\n\n@mcp.tool()\ndef set_material(\n    ctx: Context, object_name: str, material_name: str = None, color: List[float] = None\n) -> str:\n    \"\"\"\n    Set or create a material for an object.\n\n    Parameters:\n    - object_name: Name of the object to apply the material to\n    - material_name: Optional name of the material to use or create\n    - color: Optional [R, G, B] color values (0.0-1.0)\n    \"\"\"\n    try:\n        # Get the global connection\n        blender = get_blender_connection()\n\n        params = {\"object_name\": object_name}\n\n        if material_name:\n            params[\"material_name\"] = material_name\n        if color:\n            params[\"color\"] = color\n\n        result = blender.send_command(\"set_material\", params)\n        return f\"Applied material to {object_name}: {result.get('material_name', 'unknown')}\"\n    except Exception as e:\n        logger.error(f\"Error setting material: {str(e)}\")\n        return f\"Error setting material: {str(e)}\"\n\n\n@mcp.tool()\ndef execute_blender_code(ctx: Context, code: str) -> str:\n    \"\"\"\n    Execute arbitrary Python code in Blender.\n\n    Parameters:\n    - code: The Python code to execute\n    \"\"\"\n    try:\n        # Get the global connection\n        blender = get_blender_connection()\n\n        result = blender.send_command(\"execute_code\", {\"code\": code})\n        return f\"Code executed successfully: {result.get('result', '')}\"\n    except Exception as e:\n        logger.error(f\"Error executing code: {str(e)}\")\n        return f\"Error executing code: {str(e)}\"\n\n\n@mcp.tool()\ndef get_polyhaven_categories(ctx: Context, asset_type: str = \"hdris\") -> str:\n    \"\"\"\n    Get a list of categories for a specific asset type on Polyhaven.\n\n    Parameters:\n    - asset_type: The type of asset to get categories for (hdris, textures, models, all)\n    \"\"\"\n    try:\n        blender = get_blender_connection()\n        if not _polyhaven_enabled:\n            return \"PolyHaven integration is disabled. Select it in the sidebar in BlenderMCP, then run it again.\"\n        result = blender.send_command(\n            \"get_polyhaven_categories\", {\"asset_type\": asset_type}\n        )\n\n        if \"error\" in result:\n            return f\"Error: {result['error']}\"\n\n        # Format the categories in a more readable way\n        categories = result[\"categories\"]\n        formatted_output = f\"Categories for {asset_type}:\\n\\n\"\n\n        # Sort categories by count (descending)\n        sorted_categories = sorted(categories.items(), key=lambda x: x[1], reverse=True)\n\n        for category, count in sorted_categories:\n            formatted_output += f\"- {category}: {count} assets\\n\"\n\n        return formatted_output\n    except Exception as e:\n        logger.error(f\"Error getting Polyhaven categories: {str(e)}\")\n        return f\"Error getting Polyhaven categories: {str(e)}\"\n\n\n@mcp.tool()\ndef search_polyhaven_assets(\n    ctx: Context, asset_type: str = \"all\", categories: str = None\n) -> str:\n    \"\"\"\n    Search for assets on Polyhaven with optional filtering.\n\n    Parameters:\n    - asset_type: Type of assets to search for (hdris, textures, models, all)\n    - categories: Optional comma-separated list of categories to filter by\n\n    Returns a list of matching assets with basic information.\n    \"\"\"\n    try:\n        blender = get_blender_connection()\n        result = blender.send_command(\n            \"search_polyhaven_assets\",\n            {\"asset_type\": asset_type, \"categories\": categories},\n        )\n\n        if \"error\" in result:\n            return f\"Error: {result['error']}\"\n\n        # Format the assets in a more readable way\n        assets = result[\"assets\"]\n        total_count = result[\"total_count\"]\n        returned_count = result[\"returned_count\"]\n\n        formatted_output = f\"Found {total_count} assets\"\n        if categories:\n            formatted_output += f\" in categories: {categories}\"\n        formatted_output += f\"\\nShowing {returned_count} assets:\\n\\n\"\n\n        # Sort assets by download count (popularity)\n        sorted_assets = sorted(\n            assets.items(), key=lambda x: x[1].get(\"download_count\", 0), reverse=True\n        )\n\n        for asset_id, asset_data in sorted_assets:\n            formatted_output += (\n                f\"- {asset_data.get('name', asset_id)} (ID: {asset_id})\\n\"\n            )\n            formatted_output += (\n                f\"  Type: {['HDRI', 'Texture', 'Model'][asset_data.get('type', 0)]}\\n\"\n            )\n            formatted_output += (\n                f\"  Categories: {', '.join(asset_data.get('categories', []))}\\n\"\n            )\n            formatted_output += (\n                f\"  Downloads: {asset_data.get('download_count', 'Unknown')}\\n\\n\"\n            )\n\n        return formatted_output\n    except Exception as e:\n        logger.error(f\"Error searching Polyhaven assets: {str(e)}\")\n        return f\"Error searching Polyhaven assets: {str(e)}\"\n\n\n@mcp.tool()\ndef download_polyhaven_asset(\n    ctx: Context,\n    asset_id: str,\n    asset_type: str,\n    resolution: str = \"1k\",\n    file_format: str = None,\n) -> str:\n    \"\"\"\n    Download and import a Polyhaven asset into Blender.\n\n    Parameters:\n    - asset_id: The ID of the asset to download\n    - asset_type: The type of asset (hdris, textures, models)\n    - resolution: The resolution to download (e.g., 1k, 2k, 4k)\n    - file_format: Optional file format (e.g., hdr, exr for HDRIs; jpg, png for textures; gltf, fbx for models)\n\n    Returns a message indicating success or failure.\n    \"\"\"\n    try:\n        blender = get_blender_connection()\n        result = blender.send_command(\n            \"download_polyhaven_asset\",\n            {\n                \"asset_id\": asset_id,\n                \"asset_type\": asset_type,\n                \"resolution\": resolution,\n                \"file_format\": file_format,\n            },\n        )\n\n        if \"error\" in result:\n            return f\"Error: {result['error']}\"\n\n        if result.get(\"success\"):\n            message = result.get(\n                \"message\", \"Asset downloaded and imported successfully\"\n            )\n\n            # Add additional information based on asset type\n            if asset_type == \"hdris\":\n                return f\"{message}. The HDRI has been set as the world environment.\"\n            elif asset_type == \"textures\":\n                material_name = result.get(\"material\", \"\")\n                maps = \", \".join(result.get(\"maps\", []))\n                return (\n                    f\"{message}. Created material '{material_name}' with maps: {maps}.\"\n                )\n            elif asset_type == \"models\":\n                return f\"{message}. The model has been imported into the current scene.\"\n            else:\n                return message\n        else:\n            return f\"Failed to download asset: {result.get('message', 'Unknown error')}\"\n    except Exception as e:\n        logger.error(f\"Error downloading Polyhaven asset: {str(e)}\")\n        return f\"Error downloading Polyhaven asset: {str(e)}\"\n\n\n@mcp.tool()\ndef set_texture(ctx: Context, object_name: str, texture_id: str) -> str:\n    \"\"\"\n    Apply a previously downloaded Polyhaven texture to an object.\n\n    Parameters:\n    - object_name: Name of the object to apply the texture to\n    - texture_id: ID of the Polyhaven texture to apply (must be downloaded first)\n\n    Returns a message indicating success or failure.\n    \"\"\"\n    try:\n        # Get the global connection\n        blender = get_blender_connection()\n\n        result = blender.send_command(\n            \"set_texture\", {\"object_name\": object_name, \"texture_id\": texture_id}\n        )\n\n        if \"error\" in result:\n            return f\"Error: {result['error']}\"\n\n        if result.get(\"success\"):\n            material_name = result.get(\"material\", \"\")\n            maps = \", \".join(result.get(\"maps\", []))\n\n            # Add detailed material info\n            material_info = result.get(\"material_info\", {})\n            node_count = material_info.get(\"node_count\", 0)\n            has_nodes = material_info.get(\"has_nodes\", False)\n            texture_nodes = material_info.get(\"texture_nodes\", [])\n\n            output = f\"Successfully applied texture '{texture_id}' to {object_name}.\\n\"\n            output += f\"Using material '{material_name}' with maps: {maps}.\\n\\n\"\n            output += f\"Material has nodes: {has_nodes}\\n\"\n            output += f\"Total node count: {node_count}\\n\\n\"\n\n            if texture_nodes:\n                output += \"Texture nodes:\\n\"\n                for node in texture_nodes:\n                    output += f\"- {node['name']} using image: {node['image']}\\n\"\n                    if node[\"connections\"]:\n                        output += \"  Connections:\\n\"\n                        for conn in node[\"connections\"]:\n                            output += f\"    {conn}\\n\"\n            else:\n                output += \"No texture nodes found in the material.\\n\"\n\n            return output\n        else:\n            return f\"Failed to apply texture: {result.get('message', 'Unknown error')}\"\n    except Exception as e:\n        logger.error(f\"Error applying texture: {str(e)}\")\n        return f\"Error applying texture: {str(e)}\"\n\n\n@mcp.tool()\ndef get_polyhaven_status(ctx: Context) -> str:\n    \"\"\"\n    Check if PolyHaven integration is enabled in Blender.\n    Returns a message indicating whether PolyHaven features are available.\n    \"\"\"\n    try:\n        blender = get_blender_connection()\n        result = blender.send_command(\"get_polyhaven_status\")\n        enabled = result.get(\"enabled\", False)\n        message = result.get(\"message\", \"\")\n\n        return message\n    except Exception as e:\n        logger.error(f\"Error checking PolyHaven status: {str(e)}\")\n        return f\"Error checking PolyHaven status: {str(e)}\"\n\n\n@mcp.prompt()\ndef asset_creation_strategy() -> str:\n    \"\"\"Defines the preferred strategy for creating assets in Blender\"\"\"\n    return \"\"\"When creating 3D content in Blender, always start by checking if PolyHaven is available:\n\n    0. Before anything, always check the scene from get_scene_info()\n    1. First use get_polyhaven_status() to verify if PolyHaven integration is enabled.\n\n    2. If PolyHaven is enabled:\n       - For objects/models: Use download_polyhaven_asset() with asset_type=\"models\"\n       - For materials/textures: Use download_polyhaven_asset() with asset_type=\"textures\"\n       - For environment lighting: Use download_polyhaven_asset() with asset_type=\"hdris\"\n\n    3. If PolyHaven is disabled or when falling back to basic tools:\n       - create_object() for basic primitives (CUBE, SPHERE, CYLINDER, etc.)\n       - set_material() for basic colors and materials\n\n    Only fall back to basic creation tools when:\n    - PolyHaven is disabled\n    - A simple primitive is explicitly requested\n    - No suitable PolyHaven asset exists\n    - The task specifically requires a basic material/color\n    \"\"\"\n\n\n@mcp.tool()\nasync def create_3d_model_from_text(\n    describe_the_look_of_object: str, face_limit: int = -1\n) -> Dict[str, Any]:\n    \"\"\"\n    Create a 3D model from a text description using the Tripo API.\n\n    IMPORTANT: This tool initiates a 3D model generation task but does NOT wait for completion.\n    After calling this tool, you MUST repeatedly call the get_task_status tool with the returned\n    task_id until the task status is SUCCESS or a terminal error state.\n\n    Typical workflow:\n    1. Call create_3d_model_from_text to start the task\n    2. Get the task_id from the response\n    3. Call get_task_status with the task_id\n    4. If status is not SUCCESS, wait a moment and call get_task_status again\n    5. Repeat until status is SUCCESS or a terminal error state\n    6. When status is SUCCESS, use the pbr_model_url from the response\n\n    Args:\n        describe_the_look_of_object: A detailed description of the object to generate.\n        face_limit: The maximum number of faces in the model.\n        auto_size: Whether to automatically size the model.\n\n    Returns:\n        A dictionary containing the task ID and instructions for checking the status.\n    \"\"\"\n    # Get the API key from environment variable\n    # api_key = os.environ.get(\"TRIPO_API_KEY\")\n    get_blender_connection()\n    blender = get_blender_connection()\n    api_key = _tripo_apikey\n    if not api_key:\n        raise ValueError(\n            \"It may take some more time to correctly re-enter the tripo function. You can first check the scene and then try the tripo function again later\"\n        )\n\n    # Create the Tripo client\n    async with TripoClient(api_key=api_key) as client:\n        # Create a text-to-model task\n        task_id = await client.text_to_model(\n            prompt=describe_the_look_of_object,\n            face_limit=face_limit,\n        )\n\n        # Get initial task status\n        task = await client.get_task(task_id)\n\n        # Return immediately with task ID and status\n        return {\n            \"task_id\": task_id,\n            \"status\": str(task.status),\n            \"progress\": task.progress,\n            \"message\": \"Task created successfully. The 3D model generation is in progress.\",\n            \"next_step\": \"You MUST now call get_task_status with this task_id to check progress.\",\n            \"important_note\": \"3D model generation takes 3-5 minutes. You need to repeatedly call get_task_status until completion.\",\n            \"workflow\": [\n                \"1. You've completed this step by calling create_3d_model_from_text\",\n                \"2. Now call get_task_status with task_id: \" + task_id,\n                \"3. If status is not SUCCESS, wait and call get_task_status again\",\n                \"4. When status is SUCCESS, use the pbr_model_url from the response\",\n            ],\n        }\n\n\n@mcp.tool()\nasync def create_3d_model_from_image(\n    image: str, face_limit: int = -1\n) -> Dict[str, Any]:\n    \"\"\"\n    Create a 3D model from an image using the Tripo API.\n\n    IMPORTANT: This tool initiates a 3D model generation task but does NOT wait for completion.\n    After calling this tool, you MUST repeatedly call the get_task_status tool with the returned\n    task_id until the task status is SUCCESS or a terminal error state.\n\n    Typical workflow:\n    1. Call create_3d_model_from_image to start the task\n    2. Get the task_id from the response\n    3. Call get_task_status with the task_id\n    4. If status is not SUCCESS, wait a moment and call get_task_status again\n    5. Repeat until status is SUCCESS or a terminal error state\n    6. When status is SUCCESS, use the pbr_model_url from the response\n\n    Args:\n        image: The local path or url to the image file.\n        face_limit: The maximum number of faces in the model.\n        auto_size: Whether to automatically size the model.\n\n    Returns:\n        A dictionary containing the task ID and instructions for checking the status.\n    \"\"\"\n    # Get the API key from environment variable\n    # api_key = os.environ.get(\"TRIPO_API_KEY\")\n    get_blender_connection()\n    api_key = _tripo_apikey\n    if not api_key:\n        raise ValueError(\n            \"It may take some more time to correctly re-enter the tripo function. You can first check the scene and then try the tripo function again later\"\n        )\n\n    # Create the Tripo client\n    async with TripoClient(api_key=api_key) as client:\n        # Create a text-to-model task\n        task_id = await client.image_to_model(\n            image=image,\n            face_limit=face_limit,\n        )\n\n        # Get initial task status\n        task = await client.get_task(task_id)\n\n        # Return immediately with task ID and status\n        return {\n            \"task_id\": task_id,\n            \"status\": str(task.status),\n            \"progress\": task.progress,\n            \"message\": \"Task created successfully. The 3D model generation is in progress.\",\n            \"next_step\": \"You MUST now call get_task_status with this task_id to check progress.\",\n            \"important_note\": \"3D model generation takes 3-5 minutes. You need to repeatedly call get_task_status until completion.\",\n            \"workflow\": [\n                \"1. You've completed this step by calling create_3d_model_from_image\",\n                \"2. Now call get_task_status with task_id: \" + task_id,\n                \"3. If status is not SUCCESS, wait and call get_task_status again\",\n                \"4. When status is SUCCESS, use the pbr_model_url from the response\",\n            ],\n        }\n\n\n@mcp.tool()\ndef import_tripo_glb_model(ctx: Context, glb_url: str) -> str:\n    \"\"\"\n    Import a GLB model from URL into Blender scene\n\n    Parameters:\n    - glb_url: Download URL of the GLB model file\n\n    Returns:\n    Result message of the import operation\n    \"\"\"\n    try:\n        blender = get_blender_connection()\n        result = blender.send_command(\"import_tripo_glb_model\", {\"url\": glb_url})\n\n        if \"error\" in result:\n            return f\"Import failed: {result['error']}\"\n\n        if result.get(\"status\") == \"success\":\n            output = [\"Successfully imported models:\"]\n            for model in result.get(\"models\", []):\n                dim = model[\"dimensions\"]\n                output.append(\n                    f\"• {model['name']} | Dimensions: \"\n                    f\"{dim['x']} x {dim['y']} x {dim['z']} meters\"\n                )\n\n            if not output:\n                output.append(\"No models found in imported file\")\n\n            return \"\\n\".join(output)\n        else:\n            return f\"Import failed: {result.get('message', 'Unknown error')}\"\n\n    except Exception as e:\n        logger.error(f\"Error importing GLB model: {str(e)}\")\n        return f\"GLB model import failed: {str(e)}\"\n\n\n@mcp.tool()\nasync def get_task_status(task_id: str) -> Dict[str, Any]:\n    \"\"\"\n    Get the status of a 3D model generation task.\n\n    IMPORTANT: This tool checks the status of a task started by create_3d_model_from_text.\n    You may need to call this tool MULTIPLE TIMES until the task completes.\n\n    Typical workflow:\n    1. Call this tool with the task_id from create_3d_model_from_text\n    2. Check the status in the response:\n       - If status is SUCCESS, the task is complete and you can use the pbr_model_url\n       - If status is FAILED, CANCELLED, BANNED, or EXPIRED, the task failed\n       - If status is anything else, the task is still in progress\n    3. If the task is still in progress, wait a moment and call this tool again\n\n    Args:\n        task_id: The ID of the task to check (obtained from create_3d_model_from_text).\n\n    Returns:\n        A dictionary containing the task status and other information.\n    \"\"\"\n    # Get the API key from environment variable\n    # api_key = os.environ.get(\"TRIPO_API_KEY\")\n    get_blender_connection()\n    api_key = _tripo_apikey\n    if not api_key:\n        raise ValueError(\n            \"It may take some more time to correctly re-enter the tripo function. You can first check the scene and then try the tripo function again later\"\n        )\n\n    # Create the Tripo client\n    async with TripoClient(api_key=api_key) as client:\n        # Get task status\n        task = await client.get_task(task_id)\n\n        # Ensure task is not None\n        if task is None:\n            raise ValueError(\n                f\"Failed to retrieve task information for task ID: {task_id}\"\n            )\n\n        # Create result dictionary\n        result = {\n            \"task_id\": task_id,\n            \"status\": str(task.status),\n            \"progress\": task.progress,\n        }\n\n        # Add output fields if task is successful and output is available\n        if task.status == TaskStatus.SUCCESS and task.output:\n            result.update(\n                {\n                    \"base_model_url\": task.output.base_model,\n                    \"model_url\": task.output.model,\n                    \"pbr_model_url\": task.output.pbr_model,\n                    \"rendered_image_url\": task.output.rendered_image,\n                    \"message\": \"Task completed successfully! You can now use the pbr_model_url.\",\n                    \"next_step\": \"Use the pbr_model_url to access the 3D model, download it through import_tripo_glb_model tool\",\n                }\n            )\n\n            if not task.output.pbr_model:\n                result[\"warning\"] = (\n                    \"Model generated but PBR model URL is not available.\"\n                )\n        elif task.status == TaskStatus.SUCCESS:\n            result[\"message\"] = (\n                \"Task completed successfully but no output data is available.\"\n            )\n            result[\"next_step\"] = (\n                \"Try creating a new model with a different description.\"\n            )\n        elif task.status in (\n            TaskStatus.FAILED,\n            TaskStatus.CANCELLED,\n            TaskStatus.BANNED,\n            TaskStatus.EXPIRED,\n        ):\n            result[\"message\"] = f\"Task failed with status: {task.status}\"\n            result[\"next_step\"] = (\n                \"Try creating a new model with a different description.\"\n            )\n        else:\n            result[\"message\"] = (\n                f\"Task is still in progress. Current status: {task.status}, Progress: {task.progress}%\"\n            )\n            result[\"next_step\"] = (\n                \"IMPORTANT: You must call get_task_status again with this task_id to continue checking progress.\"\n            )\n            result[\"wait_message\"] = (\n                \"3D model generation typically takes 3-5 minutes. Please be patient and keep checking.\"\n            )\n\n        return result\n\n\ndef main():\n    # mcp.run(\"sse\")\n    mcp.run(transport=\"stdio\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  }
]