Skip to content

CLI Internals

This page documents the CLI module architecture for contributors. For user-facing documentation, see the CLI Reference.


Module Structure

cli/
├── __init__.py       # CLI exports
├── main.py           # Typer app and command definitions
└── commands/
    ├── __init__.py   # Command exports
    ├── generate.py   # Generate command implementation
    ├── inspect.py    # Inspect command implementation
    ├── validate.py   # Validate command implementation
    └── serve.py      # Serve command implementation

CLI Application

The CLI is built with Typer, a modern CLI framework built on Click with type hints and auto-completion.

Entry Point

# src/abi_to_mcp/cli/main.py
import typer

app = typer.Typer(
    name="abi-to-mcp",
    help="UCAI - The ABI-to-MCP Server Generator",
    add_completion=False,
    no_args_is_help=True,
)

Registration

Commands are registered in pyproject.toml:

[project.scripts]
abi-to-mcp = "abi_to_mcp.cli.main:main"

Commands

Generate Command

The main command that orchestrates the full generation pipeline.

File: cli/commands/generate.py

@app.command()
def generate(
    source: str,          # ABI source (file or address)
    output: Path,         # Output directory
    network: str,         # Network name
    address: str,         # Contract address
    name: str,            # Server name
    read_only: bool,      # Read-only mode
    include_events: bool, # Include event resources
    simulation_default: bool,  # Default simulation mode
    force: bool,          # Overwrite existing
):
    """Generate an MCP server from a smart contract ABI."""

Pipeline Steps

  1. Fetch ABI - Uses fetcher registry
  2. Parse ABI - Extracts functions and events
  3. Map to MCP - Converts to tools and resources
  4. Generate Code - Renders templates
  5. Write Files - Outputs to disk

Implementation

async def _generate_async(source, output, network, ...):
    # Step 1: Fetch
    registry = create_default_registry()

    if is_valid_address(source):
        fetch_result = await registry.fetch(source, network=network)
    else:
        fetch_result = await registry.fetch(source)

    # Step 2: Parse
    parser = ABIParser()
    parsed = parser.parse(fetch_result.abi)

    # Step 3: Map
    type_mapper = TypeMapper()
    func_mapper = FunctionMapper(type_mapper)
    event_mapper = EventMapper(type_mapper)

    tools = [func_mapper.map_function(f) for f in parsed.functions]
    resources = [event_mapper.map_event(e) for e in parsed.events]

    # Step 4: Generate
    generator = MCPGenerator()
    server = generator.generate(parsed=parsed, tools=tools, ...)

    # Step 5: Write
    for file in server.files:
        (output / file.path).write_text(file.content)

Inspect Command

Analyzes an ABI without generating files.

File: cli/commands/inspect.py

@app.command()
def inspect(
    source: str,    # ABI source
    network: str,   # Network for lookups
):
    """Inspect an ABI and show what would be generated."""

Output

  • Detected ERC standard
  • Function count (read/write)
  • Event count
  • Error count
  • Constructor info
  • Estimated tool count

Validate Command

Validates ABI structure without processing.

File: cli/commands/validate.py

@app.command()
def validate(
    source: str,    # ABI file path
    strict: bool,   # Strict validation
):
    """Validate an ABI without generating."""

Validation Checks

Check Description
JSON Structure Valid JSON syntax
ABI Format Array of entry objects
Entry Types Valid function/event/error types
Type Recognition All Solidity types recognized
Strict Only
Duplicates No duplicate function names
Named Params All parameters named
Return Types Functions have return types

Serve Command

Runs a generated MCP server.

File: cli/commands/serve.py

@app.command()
def serve(
    directory: Path,    # Server directory
    port: int,          # HTTP port
    transport: str,     # Transport mode
):
    """Run a generated MCP server."""

Transport Modes

Mode Description
stdio Standard I/O (default, for Claude Desktop)
http HTTP server on specified port

Networks Command

Lists all supported networks.

@app.command()
def networks():
    """List all supported networks."""

Output Table

Network ID Name Chain ID Explorer
mainnet Ethereum Mainnet 1 etherscan.io
polygon Polygon Mainnet 137 polygonscan.com
... ... ... ...

Rich Output

The CLI uses Rich for enhanced terminal output.

Console

from rich.console import Console

console = Console()
console.print("[bold green]Success![/bold green]")

Progress Indicators

from rich.progress import Progress, SpinnerColumn, TextColumn

with Progress(
    SpinnerColumn(),
    TextColumn("[progress.description]{task.description}"),
) as progress:
    task = progress.add_task("Processing...", total=None)
    # ... do work ...
    progress.update(task, description="✓ Complete")

Tables

from rich.table import Table

table = Table(title="Results")
table.add_column("Name", style="cyan")
table.add_column("Type", style="green")
table.add_row("transfer", "write")
console.print(table)

Error Handling

Custom Exceptions

from abi_to_mcp.core.exceptions import ABIToMCPError

try:
    # CLI operation
except ABIToMCPError as e:
    rprint(f"[bold red]Error:[/bold red] {e.message}")
    raise SystemExit(1)
except Exception as e:
    rprint(f"[bold red]Unexpected error:[/bold red] {e}")
    raise SystemExit(1)

Exit Codes

Code Meaning
0 Success
1 Runtime error
2 Invalid arguments (Typer)

Testing CLI

Unit Tests

Test individual command functions:

# tests/unit/test_cli/test_generate.py
from abi_to_mcp.cli.commands.generate import generate

def test_generate_from_file(tmp_path, sample_abi_file):
    generate(
        source=str(sample_abi_file),
        output=tmp_path / "output",
        network="mainnet",
        contract_address="0x...",
        name="Test",
        read_only=False,
        include_events=True,
        simulation_default=True,
    )

    assert (tmp_path / "output" / "server.py").exists()

Integration Tests

Test full CLI invocation:

# tests/integration/test_cli.py
from typer.testing import CliRunner
from abi_to_mcp.cli.main import app

runner = CliRunner()

def test_version():
    result = runner.invoke(app, ["--version"])
    assert result.exit_code == 0
    assert "UCAI" in result.stdout

def test_generate_from_file(sample_abi_file, tmp_path):
    result = runner.invoke(app, [
        "generate",
        str(sample_abi_file),
        "-a", "0x1234...",
        "-o", str(tmp_path / "output"),
    ])
    assert result.exit_code == 0

Adding New Commands

  1. Create command file:
# src/abi_to_mcp/cli/commands/new_command.py
from rich.console import Console

console = Console()

def new_command(arg1: str, option1: bool) -> None:
    """Implementation of new command."""
    console.print(f"Running with {arg1}")
  1. Register in main.py:
@app.command()
def new_command(
    arg1: str = typer.Argument(..., help="Description"),
    option1: bool = typer.Option(False, "--flag", help="Flag"),
):
    """
    New command description.

    EXAMPLES:
        abi-to-mcp new-command foo --flag
    """
    from abi_to_mcp.cli.commands import new_command as cmd_new

    cmd_new(arg1=arg1, option1=option1)
  1. Export in commands/init.py:
from .new_command import new_command
  1. Add documentation in docs/docs/cli/new_command.md

  2. Add tests in tests/unit/test_cli/test_new_command.py


Configuration

Environment Variables

Variable Description
ETHERSCAN_API_KEY Etherscan API key
POLYGONSCAN_API_KEY Polygonscan API key
ARBISCAN_API_KEY Arbiscan API key
NO_COLOR Disable colored output
ABI_TO_MCP_LOG_LEVEL Log level (DEBUG, INFO, etc.)

Config Files

UCAI searches for config files in order:

  1. ./abi-to-mcp.toml
  2. ./abi-to-mcp.json
  3. ~/.config/abi-to-mcp/config.toml

Best Practices

  1. Use async where beneficial - Network operations are async
  2. Show progress - Long operations should show spinners
  3. Provide examples - Include EXAMPLES in docstrings
  4. Validate early - Check inputs before processing
  5. Exit cleanly - Use proper exit codes
  6. Test thoroughly - Both unit and integration tests

Auto-Generated API Reference

The following sections are automatically generated from source code docstrings using mkdocstrings.

CLI Main Module

abi_to_mcp.cli.main

Main CLI application for UCAI (abi-to-mcp).

This module defines the Typer CLI application with all commands.

version_callback(value)

Print version and exit.

main_callback(version=typer.Option(False, '--version', '-v', help='Show version and exit', callback=version_callback, is_eager=True))

UCAI - The ABI-to-MCP Server Generator.

Generate complete, production-ready MCP servers from any smart contract ABI. Supports local files, Etherscan, and Sourcify as ABI sources.

generate(source=typer.Argument(..., help='ABI source: file path or contract address (0x...)'), output=typer.Option(Path('./mcp-server'), '--output', '-o', help='Output directory for generated server'), network=typer.Option('mainnet', '--network', '-n', help='Network for contract lookups (mainnet, polygon, arbitrum, etc.)'), address=typer.Option(None, '--address', '-a', help='Contract address (required if source is a file)'), name=typer.Option(None, '--name', help='Server name (auto-detected if not provided)'), read_only=typer.Option(False, '--read-only', help='Only generate read operations (no write tools)'), include_events=typer.Option(True, '--events/--no-events', help='Include events as MCP resources'), simulation_default=typer.Option(True, '--simulate/--no-simulate', help='Default simulation mode for write operations'), force=typer.Option(False, '--force', '-f', help='Overwrite output directory if it exists'))

Generate an MCP server from a smart contract ABI.

EXAMPLES:

# From local ABI file
abi-to-mcp generate ./token.json -a 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48

# From Etherscan (mainnet)
abi-to-mcp generate 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48

# From Etherscan (Polygon)
abi-to-mcp generate 0x... -n polygon

# Read-only mode
abi-to-mcp generate ./abi.json -a 0x... --read-only

# Custom output directory
abi-to-mcp generate ./abi.json -a 0x... -o ./my-mcp-server
inspect(source=typer.Argument(..., help='ABI source: file path or contract address'), network=typer.Option('mainnet', '--network', '-n', help='Network for contract lookups'))

Inspect an ABI and show what would be generated.

Shows detected standard, functions, events, and estimated tool count without generating any files.

EXAMPLES:

abi-to-mcp inspect ./token.json

abi-to-mcp inspect 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
validate(source=typer.Argument(..., help='ABI source: file path'), strict=typer.Option(False, '--strict', help='Enable strict validation (check for duplicates, unnamed params)'))

Validate an ABI without generating.

Checks JSON structure, ABI format validity, and type recognition.

EXAMPLES:

abi-to-mcp validate ./token.json

abi-to-mcp validate ./token.json --strict
serve(directory=typer.Argument(Path('.'), help='Directory containing generated server'), port=typer.Option(8080, '--port', '-p', help='Port for HTTP transport'), transport=typer.Option('stdio', '--transport', '-t', help='Transport mode: stdio (default) or http'))

Run a generated MCP server.

EXAMPLES:

# Run in current directory (stdio mode)
abi-to-mcp serve

# Run specific directory
abi-to-mcp serve ./my-mcp-server

# Run with HTTP transport
abi-to-mcp serve --transport http --port 8080
networks()

List all supported networks.

main()

Main entry point.

CLI Utils Module

abi_to_mcp.cli.utils

CLI utilities and helpers.

handle_error(error, verbose=False)

Handle CLI errors with consistent formatting.

Parameters:

Name Type Description Default
error Exception

Exception to handle

required
verbose bool

Show full traceback

False
confirm(message, default=False)

Ask for user confirmation.

Parameters:

Name Type Description Default
message str

Confirmation message

required
default bool

Default value

False

Returns:

Type Description
bool

True if confirmed