From 5bace6f7093563a669bfe46e95a1ce8836d66c73 Mon Sep 17 00:00:00 2001 From: cardosofede Date: Fri, 1 Aug 2025 00:22:20 +0300 Subject: [PATCH 01/14] (feat) add tool to get teh active bots --- hummingbot_mcp/server.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/hummingbot_mcp/server.py b/hummingbot_mcp/server.py index d708906..04c98ea 100644 --- a/hummingbot_mcp/server.py +++ b/hummingbot_mcp/server.py @@ -511,6 +511,19 @@ async def deploy_bot_with_controllers( logger.error(f"Failed to connect to Hummingbot API: {e}") raise ToolError("Failed to connect to Hummingbot API. Please ensure it is running and API credentials are correct.") +@mcp.tool() +async def get_active_bots_status(): + """ + Get the status of all active bots. Including the unrealized PnL, realized PnL, volume traded, latest logs, etc. + """ + try: + client = await hummingbot_client.get_client() + active_bots = await client.bot_orchestration.get_active_bots_status() + return f"Active Bots Status: {active_bots}" + except HBConnectionError as e: + logger.error(f"Failed to connect to Hummingbot API: {e}") + raise ToolError("Failed to connect to Hummingbot API. Please ensure it is running and API credentials are correct.") + async def main(): """Run the MCP server""" From f75740db8fff88b782209d939a6e044ff06c65e8 Mon Sep 17 00:00:00 2001 From: cardosofede Date: Fri, 1 Aug 2025 00:28:44 +0300 Subject: [PATCH 02/14] (feat) add tool to stop bot or controller --- hummingbot_mcp/server.py | 145 +++++++++++++++++++++++++-------------- 1 file changed, 94 insertions(+), 51 deletions(-) diff --git a/hummingbot_mcp/server.py b/hummingbot_mcp/server.py index 04c98ea..96cc5bc 100644 --- a/hummingbot_mcp/server.py +++ b/hummingbot_mcp/server.py @@ -31,10 +31,10 @@ @mcp.tool() async def setup_connector( - connector: str | None = None, - credentials: dict[str, Any] | None = None, - account: str | None = None, - confirm_override: bool | None = None, + connector: str | None = None, + credentials: dict[str, Any] | None = None, + account: str | None = None, + confirm_override: bool | None = None, ) -> str: """Setup a new exchange connector for an account with credentials using progressive disclosure. @@ -64,9 +64,10 @@ async def setup_connector( logger.error(f"setup_connector failed: {str(e)}", exc_info=True) raise ToolError(f"Failed to setup connector: {str(e)}") + @mcp.tool() async def get_portfolio_balances( - account_names: list[str] | None = None, connector_names: list[str] | None = None, as_distribution: bool = False + account_names: list[str] | None = None, connector_names: list[str] | None = None, as_distribution: bool = False ) -> str: """Get portfolio balances and holdings across all connected exchanges. @@ -87,7 +88,8 @@ async def get_portfolio_balances( client = await hummingbot_client.get_client() if as_distribution: # Get portfolio distribution - result = await client.portfolio.get_distribution(account_names=account_names, connector_names=connector_names) + result = await client.portfolio.get_distribution(account_names=account_names, + connector_names=connector_names) return f"Portfolio Distribution: {result}" account_info = await client.portfolio.get_state(account_names=account_names, connector_names=connector_names) return f"Account State: {account_info}" @@ -101,14 +103,14 @@ async def get_portfolio_balances( @mcp.tool() async def place_order( - connector_name: str, - trading_pair: str, - trade_type: str, - amount: str, - price: str | None = None, - order_type: str = "MARKET", - position_action: str | None = "OPEN", - account_name: str | None = "master_account", + connector_name: str, + trading_pair: str, + trade_type: str, + amount: str, + price: str | None = None, + order_type: str = "MARKET", + position_action: str | None = "OPEN", + account_name: str | None = "master_account", ) -> str: """Place a buy or sell order (supports USD values by adding at the start of the amount $). @@ -149,11 +151,11 @@ async def place_order( @mcp.tool() async def set_account_position_mode_and_leverage( - account_name: str, - connector_name: str, - trading_pair: str | None = None, - position_mode: str | None = None, - leverage: int | None = None, + account_name: str, + connector_name: str, + trading_pair: str | None = None, + position_mode: str | None = None, + leverage: int | None = None, ) -> str: """Set position mode and leverage for an account on a specific exchange. If position mode is not specified, will only set the leverage. If leverage is not specified, will only set the position mode. @@ -196,14 +198,14 @@ async def set_account_position_mode_and_leverage( @mcp.tool() async def get_orders( - account_names: list[str] | None = None, - connector_names: list[str] | None = None, - trading_pairs: list[str] | None = None, - status: Literal["OPEN", "FILLED", "CANCELED", "FAILED"] | None = None, - start_time: int | None = None, - end_time: int | None = None, - limit: int | None = 500, - cursor: str | None = None, + account_names: list[str] | None = None, + connector_names: list[str] | None = None, + trading_pairs: list[str] | None = None, + status: Literal["OPEN", "FILLED", "CANCELED", "FAILED"] | None = None, + start_time: int | None = None, + end_time: int | None = None, + limit: int | None = 500, + cursor: str | None = None, ) -> str: """Get the orders manged by the connected accounts. @@ -238,7 +240,7 @@ async def get_orders( @mcp.tool() async def get_positions( - account_names: list[str] | None = None, connector_names: list[str] | None = None, limit: int | None = 100 + account_names: list[str] | None = None, connector_names: list[str] | None = None, limit: int | None = 100 ) -> str: """Get the positions managed by the connected accounts. @@ -249,7 +251,8 @@ async def get_positions( """ try: client = await hummingbot_client.get_client() - result = await client.trading.get_positions(account_names=account_names, connector_names=connector_names, limit=limit) + result = await client.trading.get_positions(account_names=account_names, connector_names=connector_names, + limit=limit) return f"Position Management Result: {result}" except Exception as e: logger.error(f"manage_positions failed: {str(e)}", exc_info=True) @@ -301,7 +304,8 @@ async def get_candles(connector_name: str, trading_pair: str, interval: str = "1 elif interval.endswith("w"): max_records = 7 * days else: - raise ValueError(f"Unsupported interval format: {interval}. Use '1m', '5m', '15m', '30m', '1h', '4h', '1d', or '1w'.") + raise ValueError( + f"Unsupported interval format: {interval}. Use '1m', '5m', '15m', '30m', '1h', '4h', '1d', or '1w'.") max_records = int(max_records / int(interval[:-1])) if interval[:-1] else max_records candles = await client.market_data.get_candles( @@ -328,7 +332,8 @@ async def get_funding_rate(connector_name: str, trading_pair: str) -> str: f"Connector '{connector_name}' is not a perpetual connector. Funding rates are only available for" f"perpetual connectors." ) - funding_rate = await client.market_data.get_funding_info(connector_name=connector_name, trading_pair=trading_pair) + funding_rate = await client.market_data.get_funding_info(connector_name=connector_name, + trading_pair=trading_pair) return f"Funding Rate: {funding_rate}" except Exception as e: logger.error(f"get_funding_rate failed: {str(e)}", exc_info=True) @@ -337,11 +342,12 @@ async def get_funding_rate(connector_name: str, trading_pair: str) -> str: @mcp.tool() async def get_order_book( - connector_name: str, - trading_pair: str, - query_type: Literal["snapshot", "volume_for_price", "price_for_volume", "quote_volume_for_price", "price_for_quote_volume"], - query_value: float | None = None, - is_buy: bool = True, + connector_name: str, + trading_pair: str, + query_type: Literal[ + "snapshot", "volume_for_price", "price_for_volume", "quote_volume_for_price", "price_for_quote_volume"], + query_value: float | None = None, + is_buy: bool = True, ) -> str: """Get order book data for a trading pair on a specific exchange connector, if the query type is different than snapshot, you need to provide query_value and is_buy @@ -356,7 +362,8 @@ async def get_order_book( try: client = await hummingbot_client.get_client() if query_type == "snapshot": - order_book = await client.market_data.get_order_book(connector_name=connector_name, trading_pair=trading_pair) + order_book = await client.market_data.get_order_book(connector_name=connector_name, + trading_pair=trading_pair) return f"Order Book Snapshot: {order_book}" else: if query_value is None: @@ -387,9 +394,9 @@ async def get_order_book( @mcp.tool() async def manage_controller_configs( - action: Literal["list", "get", "upsert", "delete"], - config_name: str | None = None, - config_data: dict[str, Any] | None = None, + action: Literal["list", "get", "upsert", "delete"], + config_name: str | None = None, + config_data: dict[str, Any] | None = None, ) -> str: """ Manage controller configurations for Hummingbot MCP. If action is @@ -432,7 +439,9 @@ async def manage_controller_configs( raise ValueError("Invalid action. Must be 'list', 'get', 'upsert', or 'delete'.") except HBConnectionError as e: logger.error(f"Failed to connect to Hummingbot API: {e}") - raise ToolError("Failed to connect to Hummingbot API. Please ensure it is running and API credentials are correct.") + raise ToolError( + "Failed to connect to Hummingbot API. Please ensure it is running and API credentials are correct.") + @mcp.tool() async def manage_controllers( @@ -465,7 +474,8 @@ async def manage_controllers( result = await client.controllers.get_controller(controller_type, controller_name) return f"Controller code: {result}" elif action == "upsert": - result = await client.controllers.create_or_update_controller(controller_type, controller_name, controller_code) + result = await client.controllers.create_or_update_controller(controller_type, controller_name, + controller_code) return f"Upsert operation: {result}" elif action == "delete": result = await client.controllers.delete_controller(controller_type, controller_name) @@ -474,17 +484,18 @@ async def manage_controllers( raise ValueError("Invalid action. Must be 'list', 'get', 'upsert', or 'delete'.") except HBConnectionError as e: logger.error(f"Failed to connect to Hummingbot API: {e}") - raise ToolError("Failed to connect to Hummingbot API. Please ensure it is running and API credentials are correct.") + raise ToolError( + "Failed to connect to Hummingbot API. Please ensure it is running and API credentials are correct.") @mcp.tool() async def deploy_bot_with_controllers( - bot_name: str, - controller_configs: list[str], - account_name: str | None = "master_account", - max_global_drawdown_quote: float | None = None, - max_controller_drawdown_quote: float | None = None, - image: str = "hummingbot/hummingbot:latest", + bot_name: str, + controller_configs: list[str], + account_name: str | None = "master_account", + max_global_drawdown_quote: float | None = None, + max_controller_drawdown_quote: float | None = None, + image: str = "hummingbot/hummingbot:latest", ) -> str: """Deploy a bot with specified controller configurations. Args: @@ -509,7 +520,9 @@ async def deploy_bot_with_controllers( return f"Bot Deployment Result: {result}" except HBConnectionError as e: logger.error(f"Failed to connect to Hummingbot API: {e}") - raise ToolError("Failed to connect to Hummingbot API. Please ensure it is running and API credentials are correct.") + raise ToolError( + "Failed to connect to Hummingbot API. Please ensure it is running and API credentials are correct.") + @mcp.tool() async def get_active_bots_status(): @@ -522,7 +535,37 @@ async def get_active_bots_status(): return f"Active Bots Status: {active_bots}" except HBConnectionError as e: logger.error(f"Failed to connect to Hummingbot API: {e}") - raise ToolError("Failed to connect to Hummingbot API. Please ensure it is running and API credentials are correct.") + raise ToolError( + "Failed to connect to Hummingbot API. Please ensure it is running and API credentials are correct.") + + +@mcp.tool() +async def stop_bot_or_controller_execution( + bot_name: str, + controller_names: Optional[list[str]] = None, +): + """ + Stop and archive a bot forever or stop the execution of a controller of a runnning bot. If the controllers to stop + are not specified, it will stop the bot execution and archive it forever, if they are specified, will only stop + the execution of those controllers and the bot will still be running with the rest of the controllers. + Args: + bot_name: Name of the bot to stop + controller_names: List of controller names to stop (optional, if not provided will stop the bot execution) + """ + try: + client = await hummingbot_client.get_client() + if controller_names is None or len(controller_names) == 0: + result = await client.bot_orchestration.stop_and_archive_bot(bot_name) + return f"Bot execution stopped and archived: {result}" + else: + tasks = [client.controllers.update_bot_controller_config(bot_name, controller, {"manual_kill_switch": True}) + for controller in controller_names] + result = await asyncio.gather(*tasks) + return f"Controller execution stopped: {result}" + except HBConnectionError as e: + logger.error(f"Failed to connect to Hummingbot API: {e}") + raise ToolError( + "Failed to connect to Hummingbot API. Please ensure it is running and API credentials are correct.") async def main(): From 95751621604aeb14536f87a01d361bcf26634adf Mon Sep 17 00:00:00 2001 From: cardosofede Date: Fri, 1 Aug 2025 16:30:33 +0300 Subject: [PATCH 03/14] (feat) fix pyproject --- pyproject.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 75a5e62..fe395cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,11 +4,10 @@ version = "0.1.0" description = "MCP server for Hummingbot API integration - manage crypto trading across multiple exchanges" readme = "README.md" requires-python = ">=3.11" -license = { text = "MIT" } +license = "MIT" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -33,6 +32,9 @@ mcp-hummingbot = "hummingbot_mcp:main" requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" +[tool.setuptools] +packages = ["hummingbot_mcp"] + [tool.ruff] line-length = 140 From 04dbb5dba301c17263dfb4170fbc6f021699a924 Mon Sep 17 00:00:00 2001 From: cardosofede Date: Fri, 1 Aug 2025 16:30:42 +0300 Subject: [PATCH 04/14] (feat) refactor controllers tools --- hummingbot_mcp/server.py | 243 ++++++++++++++++++++++++++++----------- 1 file changed, 175 insertions(+), 68 deletions(-) diff --git a/hummingbot_mcp/server.py b/hummingbot_mcp/server.py index 96cc5bc..d1595f1 100644 --- a/hummingbot_mcp/server.py +++ b/hummingbot_mcp/server.py @@ -393,50 +393,107 @@ async def get_order_book( @mcp.tool() -async def manage_controller_configs( - action: Literal["list", "get", "upsert", "delete"], +async def explore_controllers( + action: Literal["list", "describe"] | None = None, + controller_type: Literal["directional_trading", "market_making", "generic"] | None = None, + controller_name: str | None = None, config_name: str | None = None, - config_data: dict[str, Any] | None = None, ) -> str: """ - Manage controller configurations for Hummingbot MCP. If action is - - 'list': will return all controller configs. - - 'get': will return the config for the given config_name. - - 'upsert': will create a controller config (if it doesn't exist) or update the config for the given config_name - with the provided config_data, is important to know that the config_name should be the same as the value of 'id' - in the config data. In order to create a config properly you can use the 'get' action to get the controller code - and understand how to configure it. - - 'delete': will delete the config for the given config_name. - Args: - action: Action to perform ('list', 'get', 'upsert', 'delete') - config_name: Name of the controller config to manage (required for 'get', 'upsert', 'delete') - config_data: Data for the controller config (required for 'upsert') + Explore and understand controllers and their configs. + + Use this tool to discover what's available and understand how things work. + + Progressive flow: + 1. No params → List all controllers grouped by type + their associated configs + 2. action="list" + controller_type → List controllers of that type with config counts + 3. action="describe" + controller_name → Show controller code + list its configs + explain parameters + 4. action="describe" + config_name → Show specific config details + which controller it uses + + Examples: + - explore_controllers() → See everything available + - explore_controllers(action="describe", controller_name="PMM_Simple") → Understand PMM_Simple + - explore_controllers(action="describe", config_name="pmm_btc_maker") → See config details """ try: client = await hummingbot_client.get_client() - if action == "list": + + if action is None: + # List all controllers and their configs + controllers = await client.controllers.list_controllers() configs = await client.controllers.list_controller_configs() - return f"Controller Configs: {configs}" - elif action == "get": - if not config_name: - raise ValueError("config_name is required for 'get' action") - config = await client.controllers.get_controller_config(config_name) - return f"Controller Config: {config}" - elif action == "upsert": - if not config_name or not config_data: - raise ValueError("config_name and config_data are required for 'upsert' action") - if "id" not in config_data or config_data["id"] != config_name: - config_data["id"] = config_name - result = await client.controllers.create_or_update_controller_config(config_name, config_data) - return f"Controller Config Upserted: {result}" - elif action == "delete": - if not config_name: - raise ValueError("config_name is required for 'delete' action") - result = await client.controllers.delete_controller_config(config_name) - await client.bot_orchestration.deploy_v2_controllers() - return f"Controller Config Deleted: {result}" + + result = "Available Controllers and Configs:\n\n" + for type_name in ["directional_trading", "market_making", "generic"]: + result += f"## {type_name.replace('_', ' ').title()}:\n" + type_controllers = controllers.get(type_name, []) + if type_controllers: + for controller in type_controllers: + # Find configs using this controller + controller_configs = [c for c in configs if c.get('controller_name') == controller] + result += f" - {controller} ({len(controller_configs)} configs)\n" + if controller_configs: + for config in controller_configs: + result += f" • {config.get('id', 'unknown')}\n" + else: + result += " No controllers available\n" + result += "\n" + return result + + elif action == "list": + if controller_type: + controllers = await client.controllers.list_controllers() + type_controllers = controllers.get(controller_type, []) + configs = await client.controllers.list_controller_configs() + + result = f"Controllers of type '{controller_type}':\n\n" + for controller in type_controllers: + controller_configs = [c for c in configs if c.get('controller_name') == controller] + result += f"- {controller} ({len(controller_configs)} configs)\n" + return result + else: + # Same as no params + return await explore_controllers() + + elif action == "describe": + if controller_name: + # Show controller code and its configs + # First, determine the controller type + controllers = await client.controllers.list_controllers() + controller_type_found = None + for ctype, clist in controllers.items(): + if controller_name in clist: + controller_type_found = ctype + break + + if not controller_type_found: + return f"Controller '{controller_name}' not found." + + code = await client.controllers.get_controller(controller_type_found, controller_name) + configs = await client.controllers.list_controller_configs() + controller_configs = [c for c in configs if c.get('controller_name') == controller_name] + + result = f"Controller: {controller_name}\n" + result += f"Type: {controller_type_found}\n\n" + result += "=== Controller Code ===\n" + result += code + "\n\n" + result += "=== Configs Using This Controller ===\n" + if controller_configs: + for config in controller_configs: + result += f"- {config.get('id', 'unknown')}\n" + else: + result += "No configs found for this controller\n" + return result + + elif config_name: + # Show config details + config = await client.controllers.get_controller_config(config_name) + return f"Config Details:\n{config}" + else: + return "Please specify either controller_name or config_name with action='describe'" else: - raise ValueError("Invalid action. Must be 'list', 'get', 'upsert', or 'delete'.") + return "Invalid action. Use 'list' or 'describe', or omit for overview." + except HBConnectionError as e: logger.error(f"Failed to connect to Hummingbot API: {e}") raise ToolError( @@ -444,44 +501,94 @@ async def manage_controller_configs( @mcp.tool() -async def manage_controllers( - action: Literal["list", "get", "upsert", "delete"], - controller_type: Optional[Literal["directional_trading", "market_making", "generic"]] = None, - controller_name: Optional[str] = None, - controller_code: Optional[str] = None, +async def modify_controllers( + action: Literal["upsert", "delete"], + target: Literal["controller", "config"], + # For controllers + controller_type: Literal["directional_trading", "market_making", "generic"] | None = None, + controller_name: str | None = None, + controller_code: str | None = None, + # For configs + config_name: str | None = None, + config_data: dict[str, Any] | None = None, + # Safety + confirm_override: bool = False, ) -> str: """ - Manage controller files (controllers are substrategies). - If action is: - - 'list': will show all the controllers available by type. - - 'get': will get the code of the controller, this will be really useful when trying to create a controller. - configuration since you can understand how each parameter is used. - - 'upsert': you can modify the code of a controller or add it if it doesn't exist. - - 'delete': delete a controller - + Create, update, or delete controllers and their configurations. + + Controllers = Strategy templates (Python code) + Configs = Strategy instances (parameter sets using a controller) + Args: - action: Action to perform ('list', 'get', 'upsert', 'delete') - controller_type: ("directional_trading", "market_making", "generic") is required for the actions 'get', 'upsert' and 'delete'. - controller_name: Name of the controller to manage (required for 'get', 'upsert', 'delete') - controller_code: Code to update, only required for the action 'upsert'. + action: "upsert" (create/update) or "delete" + target: "controller" (template) or "config" (instance) + confirm_override: Required True if overwriting existing + + Examples: + - Create new controller: modify_controllers("upsert", "controller", controller_type="market_making", ...) + - Create config: modify_controllers("upsert", "config", config_name="pmm_btc", config_data={...}) + - Delete config: modify_controllers("delete", "config", config_name="old_strategy") """ try: client = await hummingbot_client.get_client() - if action == "list": - result = await client.controllers.list_controllers() - return f"Available controllers: {result}" - elif action == "get": - result = await client.controllers.get_controller(controller_type, controller_name) - return f"Controller code: {result}" - elif action == "upsert": - result = await client.controllers.create_or_update_controller(controller_type, controller_name, - controller_code) - return f"Upsert operation: {result}" - elif action == "delete": - result = await client.controllers.delete_controller(controller_type, controller_name) - return f"Delete operation: {result}" + + if target == "controller": + if action == "upsert": + if not controller_type or not controller_name or not controller_code: + raise ValueError("controller_type, controller_name, and controller_code are required for controller upsert") + + # Check if controller exists + controllers = await client.controllers.list_controllers() + exists = controller_name in controllers.get(controller_type, []) + + if exists and not confirm_override: + return f"Controller '{controller_name}' already exists. Set confirm_override=True to update it." + + result = await client.controllers.create_or_update_controller( + controller_type, controller_name, controller_code + ) + return f"Controller {'updated' if exists else 'created'}: {result}" + + elif action == "delete": + if not controller_type or not controller_name: + raise ValueError("controller_type and controller_name are required for controller delete") + + result = await client.controllers.delete_controller(controller_type, controller_name) + return f"Controller deleted: {result}" + + elif target == "config": + if action == "upsert": + if not config_name or not config_data: + raise ValueError("config_name and config_data are required for config upsert") + + # Ensure config_data has the correct id + if "id" not in config_data or config_data["id"] != config_name: + config_data["id"] = config_name + + # Check if config exists + try: + existing = await client.controllers.get_controller_config(config_name) + exists = True + except: + exists = False + + if exists and not confirm_override: + return f"Config '{config_name}' already exists. Set confirm_override=True to update it." + + result = await client.controllers.create_or_update_controller_config(config_name, config_data) + return f"Config {'updated' if exists else 'created'}: {result}" + + elif action == "delete": + if not config_name: + raise ValueError("config_name is required for config delete") + + result = await client.controllers.delete_controller_config(config_name) + await client.bot_orchestration.deploy_v2_controllers() + return f"Config deleted: {result}" else: - raise ValueError("Invalid action. Must be 'list', 'get', 'upsert', or 'delete'.") + raise ValueError("Invalid target. Must be 'controller' or 'config'.") + except HBConnectionError as e: logger.error(f"Failed to connect to Hummingbot API: {e}") raise ToolError( @@ -540,7 +647,7 @@ async def get_active_bots_status(): @mcp.tool() -async def stop_bot_or_controller_execution( +async def stop_bot_or_controllers( bot_name: str, controller_names: Optional[list[str]] = None, ): From 76739a5b57aa25cb2d713501f64c8f9604077a64 Mon Sep 17 00:00:00 2001 From: cardosofede Date: Fri, 1 Aug 2025 18:33:44 +0300 Subject: [PATCH 05/14] (feat) return connected accounts in setup connector --- hummingbot_mcp/tools/account.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/hummingbot_mcp/tools/account.py b/hummingbot_mcp/tools/account.py index e004049..4254e22 100644 --- a/hummingbot_mcp/tools/account.py +++ b/hummingbot_mcp/tools/account.py @@ -1,7 +1,7 @@ """ Account management tools for Hummingbot MCP Server """ - +import asyncio import logging from typing import Any @@ -166,12 +166,19 @@ async def setup_connector(request: SetupConnectorRequest) -> dict[str, Any]: connector_names.append(c.name) else: connector_names.append(str(c)) + current_accounts_str = "Current accounts: " + accounts = await client.accounts.list_accounts() + credentials_tasks = [client.accounts.list_account_credentials(account_name=account_name) for account_name in accounts] + credentials = await asyncio.gather(*credentials_tasks) + for account, creds in zip(accounts, credentials): + current_accounts_str += f"{account}: {creds}), " return { "action": "list_connectors", "message": "Available exchange connectors:", "connectors": connector_names, "total_connectors": len(connector_names), + "current_accounts": current_accounts_str.strip(", "), "next_step": "Call again with 'connector' parameter to see required credentials for a specific exchange", "example": "Use connector='binance' to see Binance setup requirements", } From 78f08e9463ba222ae73c65d06b607e7cb48faf01 Mon Sep 17 00:00:00 2001 From: cardosofede Date: Fri, 1 Aug 2025 18:34:08 +0300 Subject: [PATCH 06/14] (feat) simplify and improve controller discovery --- hummingbot_mcp/server.py | 116 ++++++++++++++------------------------- 1 file changed, 41 insertions(+), 75 deletions(-) diff --git a/hummingbot_mcp/server.py b/hummingbot_mcp/server.py index d1595f1..ee23963 100644 --- a/hummingbot_mcp/server.py +++ b/hummingbot_mcp/server.py @@ -394,7 +394,7 @@ async def get_order_book( @mcp.tool() async def explore_controllers( - action: Literal["list", "describe"] | None = None, + action: Literal["list", "describe"], controller_type: Literal["directional_trading", "market_making", "generic"] | None = None, controller_name: str | None = None, config_name: str | None = None, @@ -405,92 +405,58 @@ async def explore_controllers( Use this tool to discover what's available and understand how things work. Progressive flow: - 1. No params → List all controllers grouped by type + their associated configs + 1. action="list" → List all controllers and their configs 2. action="list" + controller_type → List controllers of that type with config counts 3. action="describe" + controller_name → Show controller code + list its configs + explain parameters 4. action="describe" + config_name → Show specific config details + which controller it uses - - Examples: - - explore_controllers() → See everything available - - explore_controllers(action="describe", controller_name="PMM_Simple") → Understand PMM_Simple - - explore_controllers(action="describe", config_name="pmm_btc_maker") → See config details + + Args: + action: "list" to list controllers or "describe" to show details of a specific controller or config. + controller_type: Type of controller to filter by (optional, e.g., 'directional_trading', 'market_making', 'generic'). + controller_name: Name of the controller to describe (optional, only required for describe specific controller). + config_name: Name of the config to describe (optional, only required for describe specific config). """ try: client = await hummingbot_client.get_client() - - if action is None: - # List all controllers and their configs - controllers = await client.controllers.list_controllers() - configs = await client.controllers.list_controller_configs() - - result = "Available Controllers and Configs:\n\n" - for type_name in ["directional_trading", "market_making", "generic"]: - result += f"## {type_name.replace('_', ' ').title()}:\n" - type_controllers = controllers.get(type_name, []) - if type_controllers: - for controller in type_controllers: - # Find configs using this controller - controller_configs = [c for c in configs if c.get('controller_name') == controller] - result += f" - {controller} ({len(controller_configs)} configs)\n" - if controller_configs: - for config in controller_configs: - result += f" • {config.get('id', 'unknown')}\n" - else: - result += " No controllers available\n" - result += "\n" - return result - - elif action == "list": - if controller_type: - controllers = await client.controllers.list_controllers() - type_controllers = controllers.get(controller_type, []) - configs = await client.controllers.list_controller_configs() - - result = f"Controllers of type '{controller_type}':\n\n" - for controller in type_controllers: + # List all controllers and their configs + controllers = await client.controllers.list_controllers() + configs = await client.controllers.list_controller_configs() + result = "" + if action == "list": + result = "Available Controllers:\n\n" + for c_type, controllers in controllers.items(): + if controller_type is not None and c_type != controller_type: + continue + result += f"Controller Type: {c_type}\n" + for controller in controllers: controller_configs = [c for c in configs if c.get('controller_name') == controller] result += f"- {controller} ({len(controller_configs)} configs)\n" - return result - else: - # Same as no params - return await explore_controllers() - + if len(controller_configs) > 0: + result += " Configs:\n" + for config in controller_configs: + result += f" - {config.get('id', 'unknown')}\n" + return result elif action == "describe": - if controller_name: - # Show controller code and its configs + config = await client.controllers.get_controller_config(config_name) if config_name else None + if controller_name or config: + if controller_name != config.get("controller_name"): + controller_name = config.get("controller_name") + result += f"Controller name not matching, using config's controller name: {controller_name}\n" + # First, determine the controller type - controllers = await client.controllers.list_controllers() - controller_type_found = None - for ctype, clist in controllers.items(): - if controller_name in clist: - controller_type_found = ctype + controller_type = None + for c_type, controllers in controllers.items(): + if controller_name in controllers: + controller_type = c_type break - - if not controller_type_found: + if not controller_type: return f"Controller '{controller_name}' not found." - - code = await client.controllers.get_controller(controller_type_found, controller_name) - configs = await client.controllers.list_controller_configs() - controller_configs = [c for c in configs if c.get('controller_name') == controller_name] - - result = f"Controller: {controller_name}\n" - result += f"Type: {controller_type_found}\n\n" - result += "=== Controller Code ===\n" - result += code + "\n\n" - result += "=== Configs Using This Controller ===\n" - if controller_configs: - for config in controller_configs: - result += f"- {config.get('id', 'unknown')}\n" - else: - result += "No configs found for this controller\n" - return result - - elif config_name: - # Show config details - config = await client.controllers.get_controller_config(config_name) - return f"Config Details:\n{config}" - else: - return "Please specify either controller_name or config_name with action='describe'" + # Get controller code and configs + controller_code = await client.controllers.get_controller(controller_type, controller_name) + controller_configs = [c.get("id") for c in configs if c.get('controller_name') == controller_name] + result = f"Controller Code for {controller_name} ({controller_type}):\n{controller_code}\n\n" + result += f"All configs available for controller:\n {controller_configs}" + return result else: return "Invalid action. Use 'list' or 'describe', or omit for overview." From 0210d3cd60b3806268b34963e43b57edacfeffd5 Mon Sep 17 00:00:00 2001 From: cardosofede Date: Fri, 1 Aug 2025 20:02:36 +0300 Subject: [PATCH 07/14] (feat) fix minor errors on tool --- hummingbot_mcp/server.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/hummingbot_mcp/server.py b/hummingbot_mcp/server.py index ee23963..f9ee78d 100644 --- a/hummingbot_mcp/server.py +++ b/hummingbot_mcp/server.py @@ -432,30 +432,31 @@ async def explore_controllers( controller_configs = [c for c in configs if c.get('controller_name') == controller] result += f"- {controller} ({len(controller_configs)} configs)\n" if len(controller_configs) > 0: - result += " Configs:\n" for config in controller_configs: result += f" - {config.get('id', 'unknown')}\n" return result elif action == "describe": config = await client.controllers.get_controller_config(config_name) if config_name else None - if controller_name or config: + if config: if controller_name != config.get("controller_name"): controller_name = config.get("controller_name") result += f"Controller name not matching, using config's controller name: {controller_name}\n" - - # First, determine the controller type - controller_type = None - for c_type, controllers in controllers.items(): - if controller_name in controllers: - controller_type = c_type - break - if not controller_type: - return f"Controller '{controller_name}' not found." - # Get controller code and configs - controller_code = await client.controllers.get_controller(controller_type, controller_name) - controller_configs = [c.get("id") for c in configs if c.get('controller_name') == controller_name] - result = f"Controller Code for {controller_name} ({controller_type}):\n{controller_code}\n\n" - result += f"All configs available for controller:\n {controller_configs}" + result += f"Config Details for {config_name}:\n{config}\n\n" + if not controller_name: + return "Please provide a controller name to describe." + # First, determine the controller type + controller_type = None + for c_type, controllers in controllers.items(): + if controller_name in controllers: + controller_type = c_type + break + if not controller_type: + return f"Controller '{controller_name}' not found." + # Get controller code and configs + controller_code = await client.controllers.get_controller(controller_type, controller_name) + controller_configs = [c.get("id") for c in configs if c.get('controller_name') == controller_name] + result = f"Controller Code for {controller_name} ({controller_type}):\n{controller_code}\n\n" + result += f"All configs available for controller:\n {controller_configs}" return result else: return "Invalid action. Use 'list' or 'describe', or omit for overview." From 480a76983aac7f2cacef771d3ae3cc8f8a13093a Mon Sep 17 00:00:00 2001 From: cardosofede Date: Fri, 1 Aug 2025 20:26:27 +0300 Subject: [PATCH 08/14] (feat) improve modify controllers feature --- hummingbot_mcp/server.py | 65 ++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/hummingbot_mcp/server.py b/hummingbot_mcp/server.py index f9ee78d..fb78432 100644 --- a/hummingbot_mcp/server.py +++ b/hummingbot_mcp/server.py @@ -478,15 +478,18 @@ async def modify_controllers( # For configs config_name: str | None = None, config_data: dict[str, Any] | None = None, + # For configs in bots + bot_name: str | None = None, # Safety confirm_override: bool = False, ) -> str: """ - Create, update, or delete controllers and their configurations. - - Controllers = Strategy templates (Python code) - Configs = Strategy instances (parameter sets using a controller) + Create, update, or delete controllers and their configurations. If bot name is provided, it can only modify the config + in the bot deployed with that name. + Controllers = are essentially strategies that can be run in Hummingbot. + Configs = are the parameters that the controller uses to run. + Args: action: "upsert" (create/update) or "delete" target: "controller" (template) or "config" (instance) @@ -495,6 +498,7 @@ async def modify_controllers( Examples: - Create new controller: modify_controllers("upsert", "controller", controller_type="market_making", ...) - Create config: modify_controllers("upsert", "config", config_name="pmm_btc", config_data={...}) + - Modify config from bot: modify_controllers("upsert", "config", config_name="pmm_btc", config_data={...}, bot_name="my_bot") - Delete config: modify_controllers("delete", "config", config_name="old_strategy") """ try: @@ -510,7 +514,9 @@ async def modify_controllers( exists = controller_name in controllers.get(controller_type, []) if exists and not confirm_override: - return f"Controller '{controller_name}' already exists. Set confirm_override=True to update it." + controller_code = await client.controllers.get_controller(controller_type, controller_name) + return (f"Controller '{controller_name}' already exists and this is the current code: {controller_code}. " + f"Set confirm_override=True to update it.") result = await client.controllers.create_or_update_controller( controller_type, controller_name, controller_code @@ -528,23 +534,38 @@ async def modify_controllers( if action == "upsert": if not config_name or not config_data: raise ValueError("config_name and config_data are required for config upsert") - - # Ensure config_data has the correct id - if "id" not in config_data or config_data["id"] != config_name: - config_data["id"] = config_name - - # Check if config exists - try: - existing = await client.controllers.get_controller_config(config_name) - exists = True - except: - exists = False - - if exists and not confirm_override: - return f"Config '{config_name}' already exists. Set confirm_override=True to update it." - - result = await client.controllers.create_or_update_controller_config(config_name, config_data) - return f"Config {'updated' if exists else 'created'}: {result}" + + if bot_name: + if not confirm_override: + current_configs = await client.controllers.get_bot_controller_configs(bot_name) + config = next((c for c in current_configs if c.get("id") == config_name), None) + if config: + return (f"Config '{config_name}' already exists in bot '{bot_name}' with data: {config}. " + "Set confirm_override=True to update it.") + else: + update_op = await client.controllers.create_or_update_bot_controller_config(config_name, config_data) + return f"Config created in bot '{bot_name}': {update_op}" + else: + # Ensure config_data has the correct id + if "id" not in config_data or config_data["id"] != config_name: + config_data["id"] = config_name + update_op = await client.controllers.create_or_update_bot_controller_config(config_name, config_data) + return f"Config updated in bot '{bot_name}': {update_op}" + else: + # Ensure config_data has the correct id + if "id" not in config_data or config_data["id"] != config_name: + config_data["id"] = config_name + + controller_configs = await client.controllers.list_controller_configs() + exists = config_name in controller_configs + + if exists and not confirm_override: + existing_config = await client.controllers.get_controller_config(config_name) + return (f"Config '{config_name}' already exists with data: {existing_config}." + "Set confirm_override=True to update it.") + + result = await client.controllers.create_or_update_controller_config(config_name, config_data) + return f"Config {'updated' if exists else 'created'}: {result}" elif action == "delete": if not config_name: From 0623737483c30e0920d988e0b25403b3b76bf64d Mon Sep 17 00:00:00 2001 From: cardosofede Date: Mon, 4 Aug 2025 13:53:44 +0200 Subject: [PATCH 09/14] (feat) fix deploy typo --- hummingbot_mcp/server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hummingbot_mcp/server.py b/hummingbot_mcp/server.py index fb78432..6d881f7 100644 --- a/hummingbot_mcp/server.py +++ b/hummingbot_mcp/server.py @@ -586,7 +586,7 @@ async def modify_controllers( @mcp.tool() async def deploy_bot_with_controllers( bot_name: str, - controller_configs: list[str], + controllers_config: list[str], account_name: str | None = "master_account", max_global_drawdown_quote: float | None = None, max_controller_drawdown_quote: float | None = None, @@ -595,7 +595,7 @@ async def deploy_bot_with_controllers( """Deploy a bot with specified controller configurations. Args: bot_name: Name of the bot to deploy - controller_configs: List of controller configs to use for the bot deployment. + controllers_config: List of controller configs to use for the bot deployment. account_name: Account name to use for the bot (default: master_account) max_global_drawdown_quote: Maximum global drawdown in quote currency (optional) defaults to None. max_controller_drawdown_quote: Maximum drawdown per controller in quote currency (optional) defaults to None. @@ -606,7 +606,7 @@ async def deploy_bot_with_controllers( # Validate controller configs result = await client.bot_orchestration.deploy_v2_controllers( instance_name=bot_name, - controller_configs=controller_configs, + controllers_config=controllers_config, credentials_profile=account_name, max_global_drawdown_quote=max_global_drawdown_quote, max_controller_drawdown_quote=max_controller_drawdown_quote, From e5f8cb8df6f177e4dc8d0e620e49153dc508b9bb Mon Sep 17 00:00:00 2001 From: cardosofede Date: Mon, 4 Aug 2025 16:40:07 +0200 Subject: [PATCH 10/14] (feat) bump hbot api client version --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fe395cc..ca95a62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] dependencies = [ - "hummingbot-api-client==1.1.2", + "hummingbot-api-client==1.1.4", "mcp[cli]>=1.6.0", "pydantic>=2.11.2", "python-dotenv>=1.0.0", diff --git a/uv.lock b/uv.lock index 1a9bbb5..417e1f0 100644 --- a/uv.lock +++ b/uv.lock @@ -255,14 +255,14 @@ wheels = [ [[package]] name = "hummingbot-api-client" -version = "1.1.2" +version = "1.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/94/fa2ac304e43102d93bc57356c004d9d29cfba92e36c959e1cf8c702e0de1/hummingbot_api_client-1.1.2.tar.gz", hash = "sha256:39243802447240309d0e7c30321da63c6a2ae38c77487a68ebb706596af9232a", size = 24686 } +sdist = { url = "https://files.pythonhosted.org/packages/55/ec/86292a8bba4bba0146eb6b360a08f94da7ffd60780406bfb6b892cd49c37/hummingbot_api_client-1.1.4.tar.gz", hash = "sha256:9d2a48f505dc8cfe1d4fad3d410ae2b6d289726ca02cf87a941642d9192399b7", size = 27222 } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/39/23ce2aa90a083d602ec4f4761649cd5bf35e6736f9d958e4dcb38110e473/hummingbot_api_client-1.1.2-py3-none-any.whl", hash = "sha256:46d1c0f57f9bcf4f6ac8688b5c17b0459c2e0429dc9bb41911836e43f0325460", size = 29212 }, + { url = "https://files.pythonhosted.org/packages/95/95/ae3907af57760a93a6f05f27bc63966788cd951116c49f25a1ff7c75da3f/hummingbot_api_client-1.1.4-py3-none-any.whl", hash = "sha256:d464c90e4d01667c94b2ca4e809bc2a397bafea6544c8bda4d1a29cb3b9d11ca", size = 30502 }, ] [[package]] @@ -283,7 +283,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "hummingbot-api-client", specifier = "==1.1.2" }, + { name = "hummingbot-api-client", specifier = "==1.1.4" }, { name = "mcp", extras = ["cli"], specifier = ">=1.6.0" }, { name = "pydantic", specifier = ">=2.11.2" }, { name = "python-dotenv", specifier = ">=1.0.0" }, From c969e95d2b92cd4989e22ffad277bd2aa58cf4d3 Mon Sep 17 00:00:00 2001 From: cardosofede Date: Mon, 4 Aug 2025 16:40:26 +0200 Subject: [PATCH 11/14] (feat) add config validation before uploading --- hummingbot_mcp/server.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/hummingbot_mcp/server.py b/hummingbot_mcp/server.py index 6d881f7..372992c 100644 --- a/hummingbot_mcp/server.py +++ b/hummingbot_mcp/server.py @@ -456,7 +456,9 @@ async def explore_controllers( controller_code = await client.controllers.get_controller(controller_type, controller_name) controller_configs = [c.get("id") for c in configs if c.get('controller_name') == controller_name] result = f"Controller Code for {controller_name} ({controller_type}):\n{controller_code}\n\n" + template = await client.controllers.get_controller_config_template(controller_type, controller_name) result += f"All configs available for controller:\n {controller_configs}" + result += f"\n\nController Config Template:\n{template}\n\n" return result else: return "Invalid action. Use 'list' or 'describe', or omit for overview." @@ -535,6 +537,9 @@ async def modify_controllers( if not config_name or not config_data: raise ValueError("config_name and config_data are required for config upsert") + # validate config first + await client.controllers.validate_controller_config(controller_type, controller_name, config_name) + if bot_name: if not confirm_override: current_configs = await client.controllers.get_bot_controller_configs(bot_name) @@ -581,6 +586,9 @@ async def modify_controllers( logger.error(f"Failed to connect to Hummingbot API: {e}") raise ToolError( "Failed to connect to Hummingbot API. Please ensure it is running and API credentials are correct.") + except Exception as e: + logger.error(f"Failed request to Hummingbot API: {e}") + raise ToolError(f"Failed to modify controllers/configs: {str(e)}") @mcp.tool() From 51ff3678a5a2507f0fdf91a35fbd3d91c0faf1a4 Mon Sep 17 00:00:00 2001 From: cardosofede Date: Mon, 4 Aug 2025 18:47:48 +0200 Subject: [PATCH 12/14] (feat) updates on config --- hummingbot_mcp/server.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/hummingbot_mcp/server.py b/hummingbot_mcp/server.py index 372992c..542b6c4 100644 --- a/hummingbot_mcp/server.py +++ b/hummingbot_mcp/server.py @@ -410,6 +410,26 @@ async def explore_controllers( 3. action="describe" + controller_name → Show controller code + list its configs + explain parameters 4. action="describe" + config_name → Show specific config details + which controller it uses + Common Enum Values for Controller Configs: + + Position Mode (position_mode): + - "HEDGE" - Allows holding both long and short positions simultaneously + - "ONEWAY" - Allows only one direction position at a time + - Note: Use as string value, e.g., position_mode: "HEDGE" + + Trade Side (side): + - 1 or "BUY" - For long/buy positions + - 2 or "SELL" - For short/sell positions + - 3 - Other trade types + - Note: Numeric values are required for controller configs + + Order Type (order_type, open_order_type, take_profit_order_type, etc.): + - 1 or "MARKET" - Market order + - 2 or "LIMIT" - Limit order + - 3 or "LIMIT_MAKER" - Limit maker order (post-only) + - 4 - Other order types + - Note: Numeric values are required for controller configs + Args: action: "list" to list controllers or "describe" to show details of a specific controller or config. controller_type: Type of controller to filter by (optional, e.g., 'directional_trading', 'market_making', 'generic'). @@ -489,6 +509,10 @@ async def modify_controllers( Create, update, or delete controllers and their configurations. If bot name is provided, it can only modify the config in the bot deployed with that name. + IMPORTANT: When creating a config without specifying config_data details, you MUST first use the explore_controllers tool + with action="describe" and the controller_name to understand what parameters are required. The config_data must include + ALL relevant parameters for the controller to function properly. + Controllers = are essentially strategies that can be run in Hummingbot. Configs = are the parameters that the controller uses to run. @@ -496,6 +520,12 @@ async def modify_controllers( action: "upsert" (create/update) or "delete" target: "controller" (template) or "config" (instance) confirm_override: Required True if overwriting existing + config_data: For config creation, MUST contain all required controller parameters. Use explore_controllers first! + + Workflow for creating a config: + 1. Use explore_controllers(action="describe", controller_name="") to see required parameters + 2. Create config_data dict with ALL required parameters from the controller template + 3. Call modify_controllers with the complete config_data Examples: - Create new controller: modify_controllers("upsert", "controller", controller_type="market_making", ...) @@ -537,8 +567,15 @@ async def modify_controllers( if not config_name or not config_data: raise ValueError("config_name and config_data are required for config upsert") + # Extract controller_type and controller_name from config_data + config_controller_type = config_data.get("controller_type") + config_controller_name = config_data.get("controller_name") + + if not config_controller_type or not config_controller_name: + raise ValueError("config_data must include 'controller_type' and 'controller_name'") + # validate config first - await client.controllers.validate_controller_config(controller_type, controller_name, config_name) + await client.controllers.validate_controller_config(config_controller_type, config_controller_name, config_data) if bot_name: if not confirm_override: From 8eca42ffe7f38924b8f9bcf386053b7262291c0f Mon Sep 17 00:00:00 2001 From: david-hummingbot <85695272+david-hummingbot@users.noreply.github.com> Date: Sat, 9 Aug 2025 03:25:51 +0800 Subject: [PATCH 13/14] update Dockerfile - fix error in docker build --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index dd616ff..5778136 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /app RUN apt-get update && apt-get install -y git && \ apt-get clean && rm -rf /var/lib/apt/lists/* && \ pip install uv -COPY pyproject.toml uv.lock ./ +COPY pyproject.toml uv.lock hummingbot_mcp/ README.md ./ RUN uv venv && uv pip install . # Stage 2: Runtime @@ -27,4 +27,4 @@ COPY pyproject.toml ./ ENV DOCKER_CONTAINER=true # Run the MCP server using the pre-built venv -ENTRYPOINT ["/app/.venv/bin/python", "main.py"] \ No newline at end of file +ENTRYPOINT ["/app/.venv/bin/python", "main.py"] From a58367e5df42d8682b505c4939d9dc0345f9b6f1 Mon Sep 17 00:00:00 2001 From: david-hummingbot <85695272+david-hummingbot@users.noreply.github.com> Date: Sat, 9 Aug 2025 03:37:44 +0800 Subject: [PATCH 14/14] update Dockerfile --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5778136..a99b38b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,8 @@ WORKDIR /app RUN apt-get update && apt-get install -y git && \ apt-get clean && rm -rf /var/lib/apt/lists/* && \ pip install uv -COPY pyproject.toml uv.lock hummingbot_mcp/ README.md ./ +COPY pyproject.toml uv.lock README.md main.py ./ +COPY hummingbot_mcp/ ./hummingbot_mcp/ RUN uv venv && uv pip install . # Stage 2: Runtime