Skip to content

MCP Integration

mcesptool exists because of MCP. Without the Model Context Protocol, it would be just another esptool wrapper. MCP is what makes it possible for an LLM to detect a chip, flash firmware, and diagnose problems through natural conversation. This page explains how mcesptool uses the protocol and the framework it is built on.

The Model Context Protocol is an open specification that defines how LLM applications communicate with external tools and data sources. It establishes a JSON-RPC message format, a set of capability types (tools, resources, prompts), and transport mechanisms for exchanging those messages.

For mcesptool, MCP provides three essential things:

  1. A standard way to describe tools. Each tool has a name, a description, and a typed parameter schema. The LLM reads these descriptions to understand what operations are available.
  2. A structured invocation mechanism. The LLM calls tools by name with JSON parameters. The server executes the operation and returns a JSON result. No prompt engineering required to parse output.
  3. A transport layer. The MCP client (like Claude Code) spawns mcesptool as a subprocess and communicates over stdin/stdout using JSON-RPC. The server does not need to implement HTTP, manage authentication, or handle network concerns.

mcesptool is built on FastMCP, a Python framework that handles MCP protocol details so the server code can focus on tool implementations.

FastMCP provides:

  • Tool registration via the @app.tool() decorator, which inspects function signatures to auto-generate JSON Schema parameter descriptions.
  • Resource registration via @app.resource() for read-only data endpoints.
  • Transport management for stdio (the default), SSE, and HTTP transports.
  • Context objects injected into tool handlers for accessing MCP capabilities like progress reporting and client roots.

The server creates a FastMCP application instance and registers everything against it:

self.app = FastMCP("ESP Development Server")

That single object is the container for all tools, resources, and runtime configuration. Each component receives a reference to it and registers its own tools.

When you add mcesptool to Claude Code:

Terminal window
claude mcp add mcesptool -- uvx mcesptool

Claude Code spawns mcesptool as a child process. Communication happens over the process’s stdin and stdout using newline-delimited JSON-RPC messages. This is the stdio transport, and it is the default for MCP servers.

Stdio transport has several properties that matter for a hardware interaction tool:

  • No port conflicts. There is no HTTP server to bind, no port to configure, no firewall to open.
  • Process lifecycle management. The MCP client owns the server process. When Claude Code exits, it terminates the subprocess. No orphaned servers.
  • Implicit security. The server only communicates with its parent process. There is no network surface to attack.

The trade-off is that stdio is point-to-point. Only one client can connect to a given server instance. For mcesptool, this is actually desirable — you do not want two LLM sessions fighting over the same serial port.

mcesptool registers 40+ tools with FastMCP. Each tool is a function with typed parameters and a structured JSON return value. Here is what one looks like from the LLM’s perspective:

{
"name": "esp_detect_chip",
"description": "Detect ESP chip type and gather comprehensive information",
"inputSchema": {
"type": "object",
"properties": {
"port": {
"type": ["string", "null"],
"description": "Serial port (auto-detect if not specified)"
},
"baud_rate": {
"type": ["integer", "null"],
"description": "Connection baud rate"
},
"detailed": {
"type": "boolean",
"default": false,
"description": "Include detailed chip information and eFuse data"
}
}
}
}

The LLM sees the tool name, reads the description, understands the parameter types, and can call it:

{
"method": "tools/call",
"params": {
"name": "esp_detect_chip",
"arguments": {
"port": "/dev/ttyUSB0",
"detailed": true
}
}
}

The server executes the operation and returns structured data:

{
"success": true,
"port": "/dev/ttyUSB0",
"baud_rate": 460800,
"connection_time_seconds": 1.23,
"chip_info": {
"chip_type": "ESP32-S3",
"mac_address": "aa:bb:cc:dd:ee:ff",
"flash_size": "8MB",
"features": ["WiFi", "BLE", "Embedded PSRAM 8MB"]
}
}

This structured round-trip is what distinguishes MCP from ad-hoc tool use. The LLM does not need to parse CLI output or guess at formats. The JSON response has a consistent shape that the LLM can reason about directly.

In addition to tools (which perform operations), mcesptool registers three MCP resources that provide read-only server state:

Resource URIContent
esp://server/statusUptime, component count, production mode flag
esp://configCurrent server configuration (paths, baud rate, timeouts)
esp://capabilitiesFeature availability matrix (which chips, which operations, QEMU/IDF status)

Resources differ from tools in two ways. First, they do not take parameters — they are simple data endpoints. Second, they are designed for passive consumption. An MCP client can subscribe to resources and re-read them periodically without triggering side effects.

In practice, the LLM uses resources to understand the server’s current state before deciding which tools to call. For example, reading esp://capabilities lets the LLM know whether QEMU emulation is available before it tries to start a virtual device.

Most tool handlers accept a FastMCP Context parameter:

async def detect_chip(context: Context, port: str | None = None, ...) -> dict:

The Context object provides access to MCP capabilities that go beyond simple request/response:

  • context.list_roots() — Discover directories that the MCP client has granted access to. mcesptool uses this to find project roots.
  • Progress reporting — Tools can report progress during long operations. (Currently, mcesptool’s subprocess pattern makes real-time progress difficult, but the mechanism is available.)
  • Logging — Tools can emit log messages back to the MCP client for display.

The initialize_with_context() method on the config object shows how roots integration works:

async def initialize_with_context(self, context: Context) -> bool:
mcp_roots = await context.list_roots()
if mcp_roots:
for root in mcp_roots:
root_path = Path(root.get("uri", "").replace("file://", ""))
if root_path.exists():
self.project_roots.append(root_path)
return True

When the MCP client provides roots (directories it considers part of the current workspace), mcesptool adds them to its list of known project directories. This means the server can find ESP project files relative to the user’s workspace without requiring explicit path configuration.

mcesptool follows a “detect and adapt” configuration approach rather than requiring explicit setup:

Auto-detection

esptool is found via PATH. ESP-IDF is discovered at common install locations (~/esp/esp-idf, /opt/esp-idf). QEMU binaries are found in ~/.espressif/tools/. Serial ports are probed at OS-appropriate paths.

Environment overrides

Every auto-detected value can be overridden with an environment variable (ESPTOOL_PATH, ESP_IDF_PATH, QEMU_XTENSA_PATH, etc.). This handles non-standard installations without requiring a configuration file.

MCP root augmentation

Project roots from the MCP client’s workspace supplement the auto-detected and environment-configured paths. This creates a layered configuration model: defaults, then environment, then runtime context.

Graceful degradation

If QEMU binaries are absent, QEMU tools do not register. If ESP-IDF is not found, IDF integration tools are skipped. The server starts with whatever capabilities are available rather than failing because an optional dependency is missing.

This philosophy reflects the reality that mcesptool’s environment varies widely. A developer working on a laptop with a single ESP32 on a USB port has different needs than a CI pipeline programming boards in batch. The configuration system accommodates both without requiring either to write a config file.

The interaction pattern between an LLM and mcesptool typically follows a discovery-then-action sequence:

  1. The LLM reads the tool list to understand available capabilities.
  2. It calls esp_scan_ports or esp_detect_chip to discover what hardware is present.
  3. Based on the discovery results, it selects appropriate tools for the user’s request.
  4. It calls tools in sequence, using the output of each call to inform the next.

For example, a user who says “flash my firmware” triggers the LLM to:

  1. Scan for connected devices.
  2. Detect the chip type to confirm compatibility.
  3. Flash the firmware with the appropriate address and settings.
  4. Verify the flash contents.

Each step is a separate tool call. The LLM decides the sequence, handles errors, and communicates results to the user. mcesptool provides the capabilities; the LLM provides the orchestration.

This division of responsibility is central to the MCP model. The server does not try to be a workflow engine. It offers atomic operations with clear inputs and outputs, and trusts the LLM to compose them into coherent workflows based on the user’s intent.