Skip to content

Tool Invocations with hangar_call

Execute one or more tool invocations with a single unified API.

Overview

The hangar_call() tool is the unified API for all tool invocations. Whether you need a single call or parallel batch execution, the format is consistent.

Key benefits:

  • Unified API - One function for single calls and batches
  • Parallel execution - Multiple calls run concurrently
  • Automatic retry - Built-in retry with exponential backoff
  • Single-flight cold starts - Multiple calls to the same COLD MCP server trigger only one startup
  • Partial success handling - Failed calls don't block successful ones
  • Fail-fast mode - Optionally abort on first error
  • Circuit breaker integration - Respects MCP server health status

Basic Usage

Single Invocation

# Simple call
hangar_call(calls=[
    {"mcp_server": "math", "tool": "add", "arguments": {"a": 1, "b": 2}}
])

# With retry for reliability
hangar_call(
    calls=[{"mcp_server": "math", "tool": "add", "arguments": {"a": 1, "b": 2}}],
    max_retries=3
)

Batch Invocation (Parallel)

# Execute multiple calls in parallel - much faster than sequential
hangar_call(calls=[
    {"mcp_server": "math", "tool": "add", "arguments": {"a": 1, "b": 2}},
    {"mcp_server": "math", "tool": "multiply", "arguments": {"a": 3, "b": 4}},
    {"mcp_server": "fetch", "tool": "get", "arguments": {"url": "https://api.example.com"}},
])
# Total time: max(t1, t2, t3) instead of t1 + t2 + t3

API Reference

hangar_call

hangar_call(
    calls: list[dict],           # List of invocations to execute
    max_concurrency: int = 10,   # Max parallel invocations (1-20)
    timeout: float = 60.0,       # Global timeout in seconds (1-300)
    fail_fast: bool = False,     # Abort on first error
    max_retries: int = 1,        # Retry attempts per call (1-10)
) -> dict

Parameters:

Parameter Type Default Description
calls list[dict] required List of call specifications
max_concurrency int 10 Maximum parallel workers (1-20)
timeout float 60.0 Global timeout for entire batch (1-300s)
fail_fast bool False If True, abort remaining calls on first error
max_retries int 1 Retry attempts per call (1-10, default 1 = no retry)

Call specification:

Each item in calls must be a dictionary with:

Field Type Required Description
mcp_server str Yes MCP Server ID
tool str Yes Tool name
arguments dict Yes Tool arguments
timeout float No Per-call timeout (overrides global)

Response Schema

{
    "batch_id": "550e8400-e29b-41d4-a716-446655440000",  # UUID for tracing
    "success": True,          # True if ALL calls succeeded
    "total": 3,               # Total number of calls
    "succeeded": 3,           # Number of successful calls
    "failed": 0,              # Number of failed calls
    "elapsed_ms": 1234.5,     # Total batch execution time
    "results": [
        {
            "index": 0,                      # Original position in calls array
            "call_id": "...",                # UUID for per-call tracing
            "success": True,
            "result": {"sum": 3},            # Tool result if success
            "error": None,                   # Error message if failed
            "error_type": None,              # Error classification
            "elapsed_ms": 45.2,              # Individual call duration
            "retry_metadata": {              # Present if max_retries > 1
                "attempts": 2,
                "retries": ["TimeoutError"],
                "total_time_ms": 1234.5
            }
        },
        # ... more results
    ]
}

Examples

With Automatic Retry

# Retry on transient failures (recommended for unreliable mcp_servers)
hangar_call(
    calls=[
        {"mcp_server": "fetch", "tool": "get", "arguments": {"url": "https://api.example.com"}}
    ],
    max_retries=3
)

# Response includes retry metadata:
{
    "results": [{
        "success": True,
        "result": {...},
        "retry_metadata": {
            "attempts": 2,           # Succeeded on 2nd attempt
            "retries": ["TimeoutError"],  # 1st attempt failed with timeout
            "total_time_ms": 1500.0
        }
    }]
}

Mixed MCP servers (Parallel Cold Starts)

When calling multiple COLD MCP servers, they start in parallel:

hangar_call(calls=[
    {"mcp_server": "math", "tool": "add", "arguments": {"a": 1, "b": 2}},
    {"mcp_server": "sqlite", "tool": "query", "arguments": {"sql": "SELECT 1"}},
    {"mcp_server": "fetch", "tool": "get", "arguments": {"url": "https://api.github.com"}},
], max_concurrency=3)
# All 3 mcp_servers start simultaneously if COLD

Fail-Fast Mode

Stop processing on first error:

results = hangar_call(
    calls=[
        {"mcp_server": "math", "tool": "add", "arguments": {"a": 1, "b": 2}},
        {"mcp_server": "nonexistent", "tool": "foo", "arguments": {}},  # Will fail
        {"mcp_server": "math", "tool": "multiply", "arguments": {"a": 3, "b": 4}},
    ],
    fail_fast=True,
)
# If call #1 fails, call #2 won't execute

Per-Call Timeouts

Different timeouts for different calls:

hangar_call(calls=[
    {"mcp_server": "fetch", "tool": "get", "arguments": {"url": "..."}, "timeout": 5.0},
    {"mcp_server": "ml", "tool": "predict", "arguments": {...}, "timeout": 30.0},
], timeout=60.0)
# Effective timeout = min(per_call_timeout, remaining_global_timeout)

Circuit Breaker Behavior

If a MCP server's circuit breaker is OPEN, calls to it fail immediately:

results = hangar_call(calls=[
    {"mcp_server": "math", "tool": "add", "arguments": {"a": 1, "b": 2}},
    {"mcp_server": "unhealthy_mcp_server", "tool": "foo", "arguments": {}},  # CB OPEN
])
# Response:
{
    "success": False,  # Partial failure
    "succeeded": 1,
    "failed": 1,
    "results": [
        {"index": 0, "success": True, "result": {"sum": 3}, ...},
        {"index": 1, "success": False, "error": "Circuit breaker open", "error_type": "CircuitBreakerOpen", ...}
    ]
}

Behavior Details

Validation

Batch validation is eager - the entire batch is validated before any execution:

  • MCP Server existence
  • Tool existence (for MCP servers with predefined tools)
  • Argument types
  • Batch size limits
  • Timeout bounds

If validation fails, no calls are executed:

{
    "success": False,
    "error": "Validation failed",
    "validation_errors": [
        {"index": 0, "field": "mcp_server", "message": "McpServer 'foo' not found"}
    ]
}

Single-Flight Cold Starts

When multiple calls target the same COLD MCP server, the MCP server starts exactly once:

# 5 calls to COLD "math" mcp_server = 1 startup, then 5 parallel tool calls
hangar_call(calls=[
    {"mcp_server": "math", "tool": "add", "arguments": {"a": i, "b": 1}}
    for i in range(5)
])

Retry Behavior

When max_retries > 1:

  • Retries use exponential backoff
  • Only transient errors trigger retry (timeout, network errors, malformed JSON)
  • Permanent errors (validation, MCP server not found) do not retry
  • Each call retries independently within the batch

Timeout Resolution

Effective timeout per call = min(per_call_timeout, remaining_global_timeout)

Example:

  • Global timeout: 60s
  • Per-call timeout: 30s
  • Elapsed time: 50s
  • Effective timeout: min(30, 10) = 10s

Response Truncation

Large responses are truncated to prevent memory issues:

  • Max response per call: 10MB
  • Max total batch response: 50MB

Truncated responses have truncated: true flag:

{
    "index": 2,
    "success": True,
    "truncated": True,
    "truncated_reason": "response_size_exceeded",
    "original_size_bytes": 15728640,
    "result": None  # No partial data
}

Limits

Limit Value Behavior
Max calls per batch 100 Validation error
Max concurrency 20 Clamped to limit
Max timeout 300s Clamped to limit
Max retries 10 Clamped to limit
Max response per call 10MB Truncated
Max total response 50MB Truncated

Metrics

Prometheus metrics for batch operations:

mcp_hangar_batch_calls_total{result="success|partial|failure|validation_error"}
mcp_hangar_batch_size_bucket{}
mcp_hangar_batch_duration_seconds_bucket{}
mcp_hangar_batch_concurrency{}
mcp_hangar_batch_truncations_total{reason="per_call|total_size"}
mcp_hangar_batch_circuit_breaker_rejections_total{mcp_server="..."}
mcp_hangar_batch_cancellations_total{reason="timeout|fail_fast"}

Configuration

Optional configuration in config.yaml:

batch:
  max_calls: 100
  max_concurrency: 20
  default_timeout: 60.0
  max_timeout: 300.0
  max_response_size_bytes: 10485760      # 10MB per call
  max_total_response_size_bytes: 52428800  # 50MB total

Migration from Previous API

If you were using the previous tools, here's how to migrate:

Old API New API
registry_invoke(mcp_server, tool, arguments) hangar_call(calls=[{"mcp_server": ..., "tool": ..., "arguments": ...}])
registry_invoke_ex(..., max_retries=5) hangar_call(calls=[...], max_retries=5)
registry_invoke_stream(...) hangar_call(calls=[...]) (progress logged internally)
hangar_batch(calls=[...]) hangar_call(calls=[...])

Best Practices

  1. Use retry for external calls - Set max_retries=3 for fetch, database, and network operations
  2. Group related calls - Batch calls that can run independently
  3. Set appropriate timeouts - Use per-call timeouts for varying workloads
  4. Monitor metrics - Watch mcp_hangar_batch_duration_seconds and mcp_hangar_batch_size
  5. Handle partial failures - Check succeeded and failed counts
  6. Use fail-fast sparingly - Only when all-or-nothing is required

Limitations

  • No dependency ordering - Calls are independent; use sequential calls if you need result of A as input to B
  • No streaming - Results are returned when all calls complete