diff --git a/README.md b/README.md index 00f4ebf..de10728 100644 --- a/README.md +++ b/README.md @@ -269,54 +269,33 @@ For cloud deployment where both Hummingbot API and MCP server run on the same se docker ps ``` -## Managing Multiple API Servers +## Server Configuration -The MCP server now supports managing multiple Hummingbot API servers. This is useful when you have multiple deployments or environments. +On first run, the server creates a default configuration from environment variables (or uses `http://localhost:8000` with default credentials). Configuration is stored in `~/.hummingbot_mcp/server.yml`. -### Initial Setup - -On first run, the server creates a default server from environment variables (or uses `http://localhost:8000` with default credentials). Configuration is stored in `~/.hummingbot_mcp/servers.yml`. - -### Using the configure_api_servers Tool +### Using the configure_server Tool ``` -# List all configured servers -configure_api_servers() +# Show the current server configuration +configure_server() -# Add a new server using full URL -configure_api_servers( - action="add", - name="production", - url="http://prod-server:8000", - username="admin", - password="secure_password" -) +# Update the host and port +configure_server(host="192.168.1.100", port=8001) -# Add a server using just port (defaults to localhost) -configure_api_servers( - action="add", - name="local_8001", - port=8001, - username="admin", - password="secure_password" -) +# Update credentials +configure_server(username="admin", password="secure_password") -# Add a server using default port 8000 -configure_api_servers( - action="add", - name="local_default", +# Update everything at once +configure_server( + name="production", + host="prod-server", + port=8000, username="admin", password="secure_password" ) - -# Switch to a different server -configure_api_servers(action="set_default", name="production") - -# Remove a server -configure_api_servers(action="remove", name="old_server") ``` -All subsequent API calls will use the currently selected default server. +Only the provided parameters are changed; omitted ones keep their current values. The client automatically reconnects after any update. ## Environment Variables @@ -332,7 +311,7 @@ The following environment variables can be set in your `.env` file for the MCP s | `HUMMINGBOT_RETRY_DELAY` | `2.0` | Delay between retries in seconds | | `HUMMINGBOT_LOG_LEVEL` | `INFO` | Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) | -**Note**: After initial setup, use the `configure_api_servers` tool to manage servers. Environment variables are only used to create the initial default server. +**Note**: After initial setup, use the `configure_server` tool to update the server connection. Environment variables are only used to create the initial default configuration. ## Requirements @@ -345,12 +324,10 @@ The following environment variables can be set in your `.env` file for the MCP s The MCP server provides tools for: ### Server Management -- **configure_api_servers**: Manage multiple Hummingbot API server connections - - List all configured servers - - Add new servers with credentials - - Set default server (automatically reconnects client) - - Remove servers - - Configuration persists in `~/.hummingbot_mcp/servers.yml` +- **configure_server**: View or update the active Hummingbot API server connection + - No parameters: show current server config + - Any parameters: update and reconnect + - Configuration persists in `~/.hummingbot_mcp/server.yml` ### Trading & Account Management - Account management and connector setup @@ -391,7 +368,7 @@ The error messages will include: - The exact URL being used - Your configured username (password is masked) - Specific suggestions on how to fix the issue -- References to tools like `configure_api_servers` +- References to tools like `configure_server` ### Common Solutions @@ -400,11 +377,11 @@ The error messages will include: - Verify the API is accessible at the configured URL 2. **Wrong Credentials**: - - Use `configure_api_servers` tool to update server credentials + - Use `configure_server` tool to update server credentials - Or check your `.env` file configuration 3. **Wrong URL**: - - Use `configure_api_servers` tool to update the server URL + - Use `configure_server` tool to update the server URL - For Docker on Mac/Windows, use `host.docker.internal` instead of `localhost` 4. **Docker Network Issues**: @@ -417,4 +394,4 @@ The MCP server will: - **Not retry** on authentication failures (401 errors) - it will immediately tell you the credentials are wrong - **Retry** on connection failures with helpful messages about what might be wrong - **Provide context** about whether you're running in Docker and suggest appropriate fixes -- **Guide you** to the right tools (`configure_api_servers`) to fix issues +- **Guide you** to the right tools (`configure_server`) to fix issues diff --git a/config_examples/claude_code_config.json b/config_examples/claude_code_config.json index c421814..8b0fc90 100644 --- a/config_examples/claude_code_config.json +++ b/config_examples/claude_code_config.json @@ -5,7 +5,7 @@ "command": "/Users/dman/.local/bin/uv", "args": [ "--directory", - "/Users/dman/Documents/code/mcp", + "/Users/dman/Documents/mcp", "run", "main.py" ], diff --git a/hummingbot_mcp/api_servers.py b/hummingbot_mcp/api_servers.py deleted file mode 100644 index ab50aab..0000000 --- a/hummingbot_mcp/api_servers.py +++ /dev/null @@ -1,263 +0,0 @@ -""" -API Servers configuration management -Manages multiple Hummingbot API server connections -""" - -import os -from pathlib import Path -from typing import Any - -import aiohttp -import yaml -from pydantic import BaseModel, Field, field_validator - - -class APIServer(BaseModel): - """API Server configuration""" - - name: str = Field(description="Unique name for this server") - url: str = Field(description="API URL") - username: str = Field(default="admin") - password: str = Field(default="admin") - is_default: bool = Field(default=False) - - @field_validator("url", mode="before") - def validate_url(cls, v): - if not v.startswith(("http://", "https://")): - raise ValueError("API URL must start with http:// or https://") - return v - - -class APIServersConfig: - """Manages API servers configuration""" - - def __init__(self, config_path: Path | None = None): - """ - Initialize API servers configuration - - Args: - config_path: Path to config file. Defaults to ~/.hummingbot_mcp/servers.yml - """ - if config_path is None: - # Use home directory for persistence - home = Path.home() - config_dir = home / ".hummingbot_mcp" - config_dir.mkdir(exist_ok=True) - config_path = config_dir / "servers.yml" - - self.config_path = config_path - self._servers: dict[str, APIServer] = {} - self._load_or_create_default() - - def _load_or_create_default(self): - """Load existing config or create default""" - if self.config_path.exists(): - self._load() - else: - # Create default server from env vars or defaults - default_server = APIServer( - name="default", - url=os.getenv("HUMMINGBOT_API_URL", "http://docker.host.internal:8000"), - username=os.getenv("HUMMINGBOT_USERNAME", "admin"), - password=os.getenv("HUMMINGBOT_PASSWORD", "admin"), - is_default=True, - ) - self._servers[default_server.name] = default_server - self._save() - - def _load(self): - """Load servers from YAML file""" - try: - with open(self.config_path) as f: - data = yaml.safe_load(f) or {} - servers_data = data.get("servers", []) - - self._servers = {} - for server_data in servers_data: - server = APIServer(**server_data) - self._servers[server.name] = server - - # Ensure at least one default - if not any(s.is_default for s in self._servers.values()): - if self._servers: - first_server = next(iter(self._servers.values())) - first_server.is_default = True - except Exception as e: - raise ValueError(f"Failed to load servers config: {e}") - - def _save(self): - """Save servers to YAML file""" - data = {"servers": [server.model_dump() for server in self._servers.values()]} - - with open(self.config_path, "w") as f: - yaml.dump(data, f, default_flow_style=False, sort_keys=False) - - def list_servers(self) -> dict[str, dict[str, Any]]: - """List all configured servers""" - return { - name: { - "url": server.url, - "username": server.username, - "is_default": server.is_default, - } - for name, server in self._servers.items() - } - - def add_server(self, name: str, url: str, username: str = "admin", password: str = "admin") -> str: - """ - Add or update a server - - Args: - name: Server name - url: API URL - username: API username - password: API password - - Returns: - Success message - """ - is_update = name in self._servers - was_default = self._servers[name].is_default if is_update else False - - server = APIServer(name=name, url=url, username=username, password=password, is_default=was_default) - self._servers[name] = server - self._save() - - return f"Server '{name}' {'updated' if is_update else 'added'} successfully" - - def set_default(self, name: str) -> str: - """ - Set a server as default - - Args: - name: Server name to set as default - - Returns: - Success message - """ - if name not in self._servers: - available = list(self._servers.keys()) - raise ValueError(f"Server '{name}' not found. Available servers: {available}") - - # Unset all defaults - for server in self._servers.values(): - server.is_default = False - - # Set new default - self._servers[name].is_default = True - self._save() - - return f"Server '{name}' is now the default" - - def get_default_server(self) -> APIServer: - """Get the default server configuration""" - for server in self._servers.values(): - if server.is_default: - return server - - # Fallback: return first server if no default set - if self._servers: - return next(iter(self._servers.values())) - - # This should never happen due to initialization - raise ValueError("No servers configured") - - def modify_server( - self, name: str, url: str | None = None, username: str | None = None, password: str | None = None - ) -> str: - """ - Modify an existing server configuration - - Args: - name: Server name to modify - url: New API URL (optional) - username: New API username (optional) - password: New API password (optional) - - Returns: - Success message - """ - if name not in self._servers: - available = list(self._servers.keys()) - raise ValueError(f"Server '{name}' not found. Available servers: {available}") - - server = self._servers[name] - modified_fields = [] - - if url is not None: - server.url = url - modified_fields.append("url") - if username is not None: - server.username = username - modified_fields.append("username") - if password is not None: - server.password = password - modified_fields.append("password") - - if not modified_fields: - return f"No changes specified for server '{name}'" - - self._save() - fields_str = ", ".join(modified_fields) - return f"Server '{name}' modified successfully ({fields_str} updated)" - - def remove_server(self, name: str) -> str: - """ - Remove a server - - Args: - name: Server name to remove - - Returns: - Success message - """ - if name not in self._servers: - raise ValueError(f"Server '{name}' not found") - - was_default = self._servers[name].is_default - del self._servers[name] - - # If we deleted the default, set a new one - if was_default and self._servers: - first_server = next(iter(self._servers.values())) - first_server.is_default = True - - self._save() - return f"Server '{name}' removed successfully" - - async def health_check(self, server_name: str | None = None) -> tuple[bool, str]: - """ - Check if a server is reachable and healthy - - Args: - server_name: Server name to check. If None, checks default server - - Returns: - Tuple of (is_healthy, message) - """ - if server_name is None: - server = self.get_default_server() - else: - if server_name not in self._servers: - return False, f"Server '{server_name}' not found" - server = self._servers[server_name] - - try: - timeout = aiohttp.ClientTimeout(total=10.0) - async with aiohttp.ClientSession(timeout=timeout) as session: - # Try root endpoint - async with session.get(server.url.rstrip('/')) as response: - if response.status == 200: - return True, f"Server '{server.name}' is healthy" - else: - return False, f"Server '{server.name}' returned status {response.status}" - except aiohttp.ClientConnectorError: - return False, f"Cannot connect to server '{server.name}' at {server.url}. Is it running?" - except TimeoutError: - return False, f"Server '{server.name}' timed out. Is it running?" - except Exception as e: - return False, f"Health check failed for server '{server.name}': {str(e)}" - - -# Global instance -api_servers_config = APIServersConfig() diff --git a/hummingbot_mcp/hummingbot_client.py b/hummingbot_mcp/hummingbot_client.py index 493f610..8ab90e1 100644 --- a/hummingbot_mcp/hummingbot_client.py +++ b/hummingbot_mcp/hummingbot_client.py @@ -74,7 +74,7 @@ async def initialize(self, force: bool = False) -> HummingbotAPIClient: f" - Password: {'*' * len(settings.api_password)}\n\n" f"šŸ’” Solutions:\n" f" 1. Verify your API credentials are correct\n" - f" 2. Use the 'configure_api_servers' tool to update server credentials\n" + f" 2. Use 'configure_server' tool with username/password to update credentials\n" f" 3. Check your Hummingbot API server configuration\n\n" f"Original error: {e}" ) @@ -97,7 +97,7 @@ async def initialize(self, force: bool = False) -> HummingbotAPIClient: f"šŸ’” Solutions:\n" f" 1. Ensure the Hummingbot API is running and accessible\n" f" 2. Verify the API URL is correct: {settings.api_url}\n" - f" 3. Use 'configure_api_servers' tool to update server configuration\n\n" + f" 3. Use 'configure_server' tool with host/port to update the connection\n\n" ) # Add Docker networking warning for localhost URLs @@ -109,10 +109,7 @@ async def initialize(self, force: bool = False) -> HummingbotAPIClient: f"You're running on {system} and trying to connect to 'localhost'.\n" f"Docker containers on Mac/Windows cannot access 'localhost' on the host.\n\n" f"šŸ’” Try using 'host.docker.internal' instead:\n" - f" Use the 'configure_api_servers' tool:\n" - f" action='modify'\n" - f" name=''\n" - f" host='host.docker.internal'\n\n" + f" Use 'configure_server' tool with host='host.docker.internal'\n\n" ) error_message += f"Original error: {last_error}" @@ -124,7 +121,7 @@ async def initialize(self, force: bool = False) -> HummingbotAPIClient: f"šŸ’” Solutions:\n" f" 1. Check if the API is running and accessible\n" f" 2. Verify your credentials are correct\n" - f" 3. Use 'configure_api_servers' tool for setup\n\n" + f" 3. Use 'configure_server' tool with host/port to configure the server\n\n" f"Original error: {last_error}" ) raise self._last_error diff --git a/hummingbot_mcp/server.py b/hummingbot_mcp/server.py index dc7e832..51a55cb 100644 --- a/hummingbot_mcp/server.py +++ b/hummingbot_mcp/server.py @@ -4,14 +4,11 @@ import asyncio import logging -import os -import platform import sys from typing import Any, Literal from mcp.server.fastmcp import FastMCP -from hummingbot_mcp.api_servers import api_servers_config from hummingbot_mcp.formatters import ( format_active_bots_as_table, format_bot_logs_as_table, @@ -103,150 +100,81 @@ async def setup_connector( @mcp.tool() -@handle_errors("configure API servers") -async def configure_api_servers( - action: str | None = None, +@handle_errors("configure server") +async def configure_server( name: str | None = None, host: str | None = None, port: int | None = None, username: str | None = None, password: str | None = None, ) -> str: - """Configure API servers using progressive disclosure. + """Configure the active Hummingbot API server connection. - This tool helps you manage multiple Hummingbot API servers with a simple flow: - 1. No parameters → List all configured servers - 2. action="add" + name + (optional host/port/username/password) → Add a new server - 3. action="modify" + name + (host/port/username/password) → Modify existing server (partial updates supported) - 4. action="set_default" + name → Set a server as default (reconnects client) - 5. action="remove" + name → Remove a server + This tool manages a single API server connection: + 1. No parameters → Show the current server configuration + 2. Any parameters → Update the server config and reconnect + + Only the provided parameters are changed; omitted ones keep their current values. Args: - action: Action to perform ('add', 'modify', 'set_default', 'remove'). Leave empty to list servers. - name: Server name (required for all actions) - host: API host (optional, defaults to 'localhost' for 'add'. Examples: 'localhost', 'host.docker.internal', '72.212.424.42') - port: API port (optional, defaults to 8000 for 'add') - username: API username (optional for 'add', defaults to 'admin'; optional for 'modify') - password: API password (optional for 'add', defaults to 'admin'; optional for 'modify') + name: Server label (e.g., 'macmini', 'production') + host: API host (e.g., 'localhost', 'host.docker.internal', '72.212.424.42') + port: API port (e.g., 8000) + username: API username + password: API password """ - # No action = list servers - if action is None: - servers = api_servers_config.list_servers() - result = "Configured API Servers:\n\n" - for server_name, server_info in servers.items(): - default_marker = " (DEFAULT)" if server_info["is_default"] else "" - result += f"- {server_name}{default_marker}\n" - result += f" URL: {server_info['url']}\n" - result += f" Username: {server_info['username']}\n\n" - return result - - # Validate name for all actions - if name is None: - return "Error: 'name' parameter is required for all actions" - - # Add server - if action == "add": - # Apply defaults and construct URL from host and port - if host is None: - host = "localhost" - if port is None: - port = 8000 - - url = f"http://{host}:{port}" - - result = api_servers_config.add_server( - name=name, - url=url, - username=username or "admin", - password=password or "admin", + from hummingbot_mcp.settings import ServerConfig, _load_server_config, save_server_config + + # No params → show active server + if name is None and host is None and port is None and username is None and password is None: + current = _load_server_config() + return ( + f"Active Server:\n\n" + f" Name: {current.name}\n" + f" URL: {current.url}\n" + f" Username: {current.username}\n" ) - # Add Docker networking warning for localhost URLs - if host == "localhost" and os.getenv("DOCKER_CONTAINER") == "true": - system = platform.system() - if system in ["Darwin", "Windows"]: - result += ( - "\n\nāš ļø Docker Networking Notice:\n" - f"You're running on {system} and using 'localhost' as the host.\n" - "Docker containers on Mac/Windows cannot access 'localhost' on the host.\n" - f"If connection fails, use 'host.docker.internal' instead:\n" - f" configure_api_servers(action='add', name='{name}', " - f"host='host.docker.internal', port={port}, ...)" - ) - - return result - - # Modify server - elif action == "modify": - # Construct URL from host and port if either is provided - url = None - if host is not None or port is not None: - # Get current server config to use existing values as defaults - servers = api_servers_config.list_servers() - if name not in servers: - return f"Error: Server '{name}' not found" - - current_server = servers[name] - current_url = current_server["url"] - - # Parse current URL to extract host and port - from urllib.parse import urlparse - parsed = urlparse(current_url) - current_host = parsed.hostname or "localhost" - current_port = parsed.port or 8000 - - # Use provided values or fall back to current values - final_host = host if host is not None else current_host - final_port = port if port is not None else current_port - - url = f"http://{final_host}:{final_port}" - - result = api_servers_config.modify_server(name=name, url=url, username=username, password=password) - - # Check if we modified the default server and need to reconnect - default_server = api_servers_config.get_default_server() - if default_server.name == name: - settings.reload_from_default_server() - await hummingbot_client.close() - try: - await hummingbot_client.initialize(force=True) - return f"{result}. Client reconnected successfully." - except Exception as e: - return f"{result}. Warning: Could not connect to server - {str(e)}" - - return result - - # Set default server - elif action == "set_default": - result = api_servers_config.set_default(name) - - # Reload settings and reconnect client - settings.reload_from_default_server() - await hummingbot_client.close() - try: - await hummingbot_client.initialize(force=True) - return f"{result}. Client reconnected successfully." - except Exception as e: - return f"{result}. Warning: Could not connect to server - {str(e)}" - - # Remove server - elif action == "remove": - result = api_servers_config.remove_server(name) - - # Reload settings and reconnect if there are remaining servers - try: - settings.reload_from_default_server() - await hummingbot_client.close() - await hummingbot_client.initialize(force=True) - default_server = api_servers_config.get_default_server() - result += f" New default is '{default_server.name}'." - except Exception: - pass - - return result + # Build new config with partial updates + current = _load_server_config() + + from urllib.parse import urlparse + parsed = urlparse(current.url) + current_host = parsed.hostname or "localhost" + current_port = parsed.port or 8000 + + final_name = name if name is not None else current.name + final_host = host if host is not None else current_host + final_port = port if port is not None else current_port + final_username = username if username is not None else current.username + final_password = password if password is not None else current.password + + new_config = ServerConfig( + name=final_name, + url=f"http://{final_host}:{final_port}", + username=final_username, + password=final_password, + ) - else: - return f"Error: Invalid action '{action}'. Use 'add', 'modify', 'set_default', or 'remove'" + # Persist and apply + save_server_config(new_config) + settings.reload_from_server_config(new_config) + await hummingbot_client.close() + + try: + await hummingbot_client.initialize(force=True) + return ( + f"Server '{new_config.name}' configured and connected successfully.\n\n" + f" URL: {new_config.url}\n" + f" Username: {new_config.username}\n" + ) + except Exception as e: + return ( + f"Server '{new_config.name}' configured but could not connect.\n\n" + f" URL: {new_config.url}\n" + f" Username: {new_config.username}\n\n" + f"Error: {str(e)}\n" + ) @mcp.tool() @@ -356,6 +284,32 @@ async def place_order( return f"Order Result: {result['result']}" +@mcp.tool() +@handle_errors("cancel order") +async def cancel_order( + connector_name: str, + order_id: str, + account_name: str = "master_account", +) -> str: + """Cancel an active order on an exchange. + + Use get_portfolio_overview (with include_active_orders=True) or search_history to find the order_id. + + Args: + connector_name: Exchange connector name (e.g., 'binance', 'binance_perpetual') + order_id: The client_order_id of the order to cancel + account_name: Account name (default: master_account) + """ + client = await hummingbot_client.get_client() + result = await trading_tools.cancel_order( + client=client, + connector_name=connector_name, + order_id=order_id, + account_name=account_name, + ) + return f"Cancel Order Result: {result['result']}" + + @mcp.tool() @handle_errors("set position mode and leverage") async def set_account_position_mode_and_leverage( @@ -1193,7 +1147,7 @@ async def main(): logger.info(f"Configured API URL: {settings.api_url}") logger.info(f"Default Account: {settings.default_account}") logger.info("Server will connect to API on first use (lazy initialization)") - logger.info("šŸ’” Use 'configure_api_servers' tool to manage API server connections") + logger.info("šŸ’” Use 'configure_server' tool to view or update the API server connection") # Run the server with FastMCP # Connection to API will happen lazily on first tool use diff --git a/hummingbot_mcp/settings.py b/hummingbot_mcp/settings.py index 3e1132f..98f2d7e 100644 --- a/hummingbot_mcp/settings.py +++ b/hummingbot_mcp/settings.py @@ -3,21 +3,66 @@ """ import os +from pathlib import Path import aiohttp +import yaml from pydantic import BaseModel, Field, field_validator -from hummingbot_mcp.api_servers import api_servers_config from hummingbot_mcp.exceptions import ConfigurationError +CONFIG_DIR = Path.home() / ".hummingbot_mcp" +SERVER_CONFIG_PATH = CONFIG_DIR / "server.yml" + + +class ServerConfig(BaseModel): + """Active server configuration""" + + name: str = Field(default="default") + url: str = Field(default="http://localhost:8000") + username: str = Field(default="admin") + password: str = Field(default="admin") + + @field_validator("url", mode="before") + def validate_url(cls, v): + if not v.startswith(("http://", "https://")): + raise ValueError("API URL must start with http:// or https://") + return v + + +def _load_server_config() -> ServerConfig: + """Load server config from ~/.hummingbot_mcp/server.yml, fallback to env vars.""" + if SERVER_CONFIG_PATH.exists(): + try: + with open(SERVER_CONFIG_PATH) as f: + data = yaml.safe_load(f) or {} + return ServerConfig(**data) + except Exception: + pass + + return ServerConfig( + name=os.getenv("HUMMINGBOT_SERVER_NAME", "default"), + url=os.getenv("HUMMINGBOT_API_URL", "http://localhost:8000"), + username=os.getenv("HUMMINGBOT_USERNAME", "admin"), + password=os.getenv("HUMMINGBOT_PASSWORD", "admin"), + ) + + +def save_server_config(config: ServerConfig): + """Persist the active server config to disk.""" + CONFIG_DIR.mkdir(exist_ok=True) + with open(SERVER_CONFIG_PATH, "w") as f: + yaml.dump(config.model_dump(), f, default_flow_style=False, sort_keys=False) + class Settings(BaseModel): """Application settings""" - # API Configuration - now loaded from api_servers_config + # API Configuration api_url: str = Field(default="http://localhost:8000") api_username: str = Field(default="admin") api_password: str = Field(default="admin") + server_name: str = Field(default="default") default_account: str = Field(default="master_account") # Connection settings @@ -46,27 +91,24 @@ def client_timeout(self) -> aiohttp.ClientTimeout: """Get aiohttp ClientTimeout object""" return aiohttp.ClientTimeout(total=self.connection_timeout) - def reload_from_default_server(self): - """Reload API settings from the default server configuration""" - try: - default_server = api_servers_config.get_default_server() - self.api_url = default_server.url - self.api_username = default_server.username - self.api_password = default_server.password - except Exception as e: - raise ConfigurationError(f"Failed to reload settings from default server: {e}") + def reload_from_server_config(self, config: ServerConfig): + """Reload API settings from a ServerConfig""" + self.api_url = config.url + self.api_username = config.username + self.api_password = config.password + self.server_name = config.name def get_settings() -> Settings: - """Get application settings from default API server configuration""" + """Get application settings from server configuration""" try: - # Load default server from api_servers_config - default_server = api_servers_config.get_default_server() + server_config = _load_server_config() return Settings( - api_url=default_server.url, - api_username=default_server.username, - api_password=default_server.password, + api_url=server_config.url, + api_username=server_config.username, + api_password=server_config.password, + server_name=server_config.name, connection_timeout=float(os.getenv("HUMMINGBOT_TIMEOUT", "30.0")), max_retries=int(os.getenv("HUMMINGBOT_MAX_RETRIES", "3")), retry_delay=float(os.getenv("HUMMINGBOT_RETRY_DELAY", "2.0")), diff --git a/hummingbot_mcp/tools/trading.py b/hummingbot_mcp/tools/trading.py index 5ca195e..3cc97eb 100644 --- a/hummingbot_mcp/tools/trading.py +++ b/hummingbot_mcp/tools/trading.py @@ -62,6 +62,33 @@ async def place_order( return {"result": result} +async def cancel_order( + client: Any, + connector_name: str, + order_id: str, + account_name: str = "master_account", +) -> dict[str, Any]: + """ + Cancel an active order on an exchange. + + Args: + client: Hummingbot API client + connector_name: Exchange connector name + order_id: The client_order_id of the order to cancel + account_name: Account name + + Returns: + Dictionary containing cancellation result + """ + result = await client.trading.cancel_order( + account_name=account_name, + connector_name=connector_name, + client_order_id=order_id, + ) + + return {"result": result} + + async def set_position_mode_and_leverage( client: Any, account_name: str,