Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions handlers/agents/_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ def build_initial_context(user_id: int, chat_id: int, user_data: dict | None = N
"mcp__mcp-hummingbot__explore_dex_pools",
"mcp__mcp-hummingbot__explore_geckoterminal",
"mcp__mcp-hummingbot__manage_gateway_swaps",
"mcp__mcp-hummingbot__manage_gateway_clmm",
"mcp__mcp-hummingbot__manage_gateway_config",
"mcp__mcp-hummingbot__manage_gateway_container",
"mcp__mcp-hummingbot__search_history",
Expand Down
2 changes: 2 additions & 0 deletions mcp_servers/hummingbot_api/formatters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@

# Gateway formatters
from .gateway import (
format_gateway_clmm_result,
format_gateway_clmm_pool_result,
format_gateway_config_result,
format_gateway_container_result,
Expand Down Expand Up @@ -76,6 +77,7 @@
"format_gateway_container_result",
"format_gateway_config_result",
"format_gateway_swap_result",
"format_gateway_clmm_result",
"format_gateway_clmm_pool_result",
# Base utilities
"format_currency",
Expand Down
55 changes: 55 additions & 0 deletions mcp_servers/hummingbot_api/formatters/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,61 @@ def format_gateway_swap_result(action: str, result: dict[str, Any]) -> str:
return f"Gateway Swap Result: {result}"


def _extract_transaction_hash(result: dict[str, Any]) -> str | None:
return (
result.get("transaction_hash")
or result.get("tx_hash")
or result.get("txHash")
or result.get("signature")
or result.get("txSignature")
)


def format_gateway_clmm_result(action: str, result: dict[str, Any]) -> str:
"""Format Gateway CLMM position management results into a human-readable string."""
if action in ["open_position", "close_position", "collect_fees"]:
payload = result.get("result", {}) if isinstance(result, dict) else {}
if not isinstance(payload, dict):
return f"Gateway CLMM {action}: {payload}"

tx_hash = _extract_transaction_hash(payload)
position_address = (
payload.get("position_address")
or result.get("position_address")
or payload.get("nft_id")
)

lines = [f"Gateway CLMM {action.replace('_', ' ').title()} Result:"]
if position_address:
lines.append(f"Position: {position_address}")
if tx_hash:
lines.append(f"Transaction: {tx_hash}")
if not position_address and not tx_hash:
lines.append(str(payload))
return "\n".join(lines)

if action == "get_positions":
positions = result.get("result", [])
count = len(positions) if isinstance(positions, list) else 0
return f"Gateway CLMM Positions ({count} found):\n\n{positions}"

if action == "search":
search_result = result.get("result", {})
positions = search_result.get("data", []) if isinstance(search_result, dict) else []
pagination = result.get("pagination", {})
filters = result.get("filters", {})
return (
f"Gateway CLMM Position Search Result:\n"
f"Total Positions Found: {len(positions)}\n"
f"Limit: {pagination.get('limit', 'N/A')}, Offset: {pagination.get('offset', 'N/A')}\n"
f"Refresh: {pagination.get('refresh', False)}\n"
f"Filters: {filters if filters else 'None'}\n\n"
f"Positions: {positions}"
)

return f"Gateway CLMM Result: {result}"


def format_gateway_clmm_pool_result(action: str, result: dict[str, Any]) -> str:
"""Format gateway CLMM pool exploration results into a human-readable string."""
if action == "list_pools" and "pools_table" in result:
Expand Down
96 changes: 96 additions & 0 deletions mcp_servers/hummingbot_api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,3 +685,99 @@ class GatewayCLMMRequest(BaseModel):
default=False,
description="Return detailed table with more columns (default: False)"
)


class GatewayCLMMManageRequest(BaseModel):
"""Request model for Gateway CLMM liquidity position management."""

action: Literal["open_position", "close_position", "collect_fees", "get_positions", "search"] = Field(
description="Action to perform: open_position, close_position, collect_fees, get_positions, or search"
)

connector: str | None = Field(
default=None,
description="CLMM connector name. Examples: 'meteora', 'raydium', 'uniswap'"
)

network: str | None = Field(
default=None,
description="Network ID in 'chain-network' format. Examples: 'solana-mainnet-beta', 'ethereum-mainnet'"
)

pool_address: str | None = Field(
default=None,
description="Pool contract address. Required for open_position and get_positions"
)

position_address: str | None = Field(
default=None,
description="Position NFT/address. Required for close_position and collect_fees"
)

lower_price: str | None = Field(
default=None,
description="Lower price bound for open_position"
)

upper_price: str | None = Field(
default=None,
description="Upper price bound for open_position"
)

base_token_amount: str | None = Field(
default=None,
description="Base token amount for open_position"
)

quote_token_amount: str | None = Field(
default=None,
description="Quote token amount for open_position"
)

slippage_pct: str | None = Field(
default="1.0",
description="Slippage percentage tolerance for open_position (default: 1.0)"
)

wallet_address: str | None = Field(
default=None,
description="Wallet address for mutating CLMM actions (optional, uses default wallet if omitted)"
)

extra_params: dict[str, Any] | None = Field(
default=None,
description="Connector-specific parameters, such as {'strategyType': 0} for Meteora"
)

trading_pair: str | None = Field(
default=None,
description="Trading pair filter for search action"
)

status: Literal["OPEN", "CLOSED"] | None = Field(
default=None,
description="Position status filter for search action"
)

position_addresses: list[str] | None = Field(
default=None,
description="Specific position addresses to filter in search action"
)

limit: int = Field(
default=50,
ge=1,
le=1000,
description="Maximum number of results for search action (default: 50, max: 1000)"
)

offset: int = Field(
default=0,
ge=0,
description="Pagination offset for search action"
)

refresh: bool = Field(
default=False,
description="Refresh position data from Gateway before returning search results"
)
168 changes: 167 additions & 1 deletion mcp_servers/hummingbot_api/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
format_active_bots_as_table,
format_bot_logs_as_table,
format_connector_result,
format_gateway_clmm_result,
format_gateway_clmm_pool_result,
format_gateway_config_result,
format_gateway_container_result,
Expand All @@ -23,6 +24,7 @@
from mcp_servers.hummingbot_api.middleware import GATEWAY_LOG_HINT, handle_errors
from mcp_servers.hummingbot_api.schemas import (
GatewayCLMMRequest,
GatewayCLMMManageRequest,
GatewayConfigRequest,
GatewayContainerRequest,
GatewaySwapRequest,
Expand All @@ -41,7 +43,10 @@
manage_gateway_config as manage_gateway_config_impl,
manage_gateway_container as manage_gateway_container_impl,
)
from mcp_servers.hummingbot_api.tools.gateway_clmm import explore_gateway_clmm_pools as explore_gateway_clmm_pools_impl
from mcp_servers.hummingbot_api.tools.gateway_clmm import (
explore_gateway_clmm_pools as explore_gateway_clmm_pools_impl,
manage_gateway_clmm as manage_gateway_clmm_impl,
)
from mcp_servers.hummingbot_api.tools.gateway_swap import manage_gateway_swaps as manage_gateway_swaps_impl
from mcp_servers.hummingbot_api.tools.geckoterminal import explore_geckoterminal as explore_geckoterminal_impl
from mcp_servers.hummingbot_api.tools import history as history_tools
Expand Down Expand Up @@ -831,6 +836,54 @@ async def explore_dex_pools(
return format_gateway_clmm_pool_result(action, result)


@mcp.tool()
@handle_errors("manage Gateway CLMM positions", GATEWAY_LOG_HINT)
async def manage_gateway_clmm(
action: Literal["open_position", "close_position", "collect_fees", "get_positions", "search"],
connector: str | None = None,
network: str | None = None,
pool_address: str | None = None,
position_address: str | None = None,
lower_price: str | None = None,
upper_price: str | None = None,
base_token_amount: str | None = None,
quote_token_amount: str | None = None,
slippage_pct: str | None = "1.0",
wallet_address: str | None = None,
extra_params: dict[str, Any] | None = None,
trading_pair: str | None = None,
status: Literal["OPEN", "CLOSED"] | None = None,
position_addresses: list[str] | None = None,
limit: int = 50,
offset: int = 0,
refresh: bool = False,
) -> str:
"""Manage Gateway CLMM liquidity positions: open, close, collect fees, get positions, or search."""
request = GatewayCLMMManageRequest(
action=action,
connector=connector,
network=network,
pool_address=pool_address,
position_address=position_address,
lower_price=lower_price,
upper_price=upper_price,
base_token_amount=base_token_amount,
quote_token_amount=quote_token_amount,
slippage_pct=slippage_pct,
wallet_address=wallet_address,
extra_params=extra_params,
trading_pair=trading_pair,
status=status,
position_addresses=position_addresses,
limit=limit,
offset=offset,
refresh=refresh,
)
client = await hummingbot_client.get_client()
result = await manage_gateway_clmm_impl(client, request)
return format_gateway_clmm_result(action, result)


# GeckoTerminal Tools


Expand Down Expand Up @@ -933,6 +986,119 @@ async def run_backtest(
return result.get("formatted_output", str(result))


@mcp.tool()
@handle_errors("manage Gateway container", GATEWAY_LOG_HINT)
async def manage_gateway_container(
action: Literal["get_status", "start", "stop", "restart", "get_logs"],
config: dict[str, Any] | None = None,
tail: int | None = 100,
) -> str:
"""Manage Gateway container lifecycle: get_status, start, stop, restart, get_logs."""
request = GatewayContainerRequest(
action=action,
config=config,
tail=tail,
)
client = await hummingbot_client.get_client()
result = await manage_gateway_container_impl(client, request)
return format_gateway_container_result(result)


@mcp.tool()
@handle_errors("manage Gateway config", GATEWAY_LOG_HINT)
async def manage_gateway_config(
resource_type: Literal["chains", "networks", "tokens", "connectors", "pools", "wallets"],
action: Literal["list", "get", "update", "add", "delete"],
network_id: str | None = None,
connector_name: str | None = None,
config_updates: dict[str, Any] | None = None,
token_address: str | None = None,
token_symbol: str | None = None,
token_decimals: int | None = None,
token_name: str | None = None,
search: str | None = None,
pool_type: str | None = None,
network: str | None = None,
pool_base: str | None = None,
pool_quote: str | None = None,
pool_address: str | None = None,
chain: str | None = None,
private_key: str | None = None,
wallet_address: str | None = None,
) -> str:
"""Manage Gateway chains, networks, tokens, connectors, pools, and wallets."""
request = GatewayConfigRequest(
resource_type=resource_type,
action=action,
network_id=network_id,
connector_name=connector_name,
config_updates=config_updates,
token_address=token_address,
token_symbol=token_symbol,
token_decimals=token_decimals,
token_name=token_name,
search=search,
pool_type=pool_type,
network=network,
pool_base=pool_base,
pool_quote=pool_quote,
pool_address=pool_address,
chain=chain,
private_key=private_key,
wallet_address=wallet_address,
)
client = await hummingbot_client.get_client()
result = await manage_gateway_config_impl(client, request)
return format_gateway_config_result(result)


@mcp.tool()
@handle_errors("manage Gateway swaps", GATEWAY_LOG_HINT)
async def manage_gateway_swaps(
action: Literal["quote", "execute", "search", "get_status"],
connector: str | None = None,
network: str | None = None,
trading_pair: str | None = None,
side: Literal["BUY", "SELL"] | None = None,
amount: str | None = None,
slippage_pct: str | None = "1.0",
wallet_address: str | None = None,
transaction_hash: str | None = None,
search_network: str | None = None,
search_connector: str | None = None,
search_wallet_address: str | None = None,
search_trading_pair: str | None = None,
status: str | None = None,
start_time: int | None = None,
end_time: int | None = None,
limit: int | None = 50,
offset: int | None = 0,
) -> str:
"""Manage Gateway swap quote, execute, search, and transaction status."""
request = GatewaySwapRequest(
action=action,
connector=connector,
network=network,
trading_pair=trading_pair,
side=side,
amount=amount,
slippage_pct=slippage_pct,
wallet_address=wallet_address,
transaction_hash=transaction_hash,
search_network=search_network,
search_connector=search_connector,
search_wallet_address=search_wallet_address,
search_trading_pair=search_trading_pair,
status=status,
start_time=start_time,
end_time=end_time,
limit=limit,
offset=offset,
)
client = await hummingbot_client.get_client()
result = await manage_gateway_swaps_impl(client, request)
return format_gateway_swap_result(action, result)

@mcp.tool()
@handle_errors("manage backtest tasks")
async def manage_backtest_tasks(
Expand Down
Loading