Skip to content

cap_mcp_client tools exposed to LLM but unusable: MCP instance is never initialized in edge_agent #129

Description

@lrl03y

Bug: cap_mcp_client tools exposed to the LLM but unusable because the MCP instance is never initialized

Environment

Item Value
Project espressif/esp-claw
Application application/edge_agent
Firmware version v0.1.0-14-g52352e5 (master HEAD 52352e5, merged 2026-06-18)
Image esp32_S3_DevKitC_1__v0.1.0-14-g52352e5__serial_jtag.bin.gz from the official web flasher
Board ESP32-S3-DevKitC-1 N16R8 clone (VCC-GND Studio YD-ESP32-23 V1.3 — same pinout)
MCP SDK espressif/mcp-c-sdk ^2.0.0 (resolved to v2.0.1)
ESP-IDF v5.5.4 (as bundled in the prebuilt image)
Transport tested streamable-http (FastMCP / Python mcp SDK, server on a LAN host, reachable, returns 200 on initialize)

Summary

After enabling cap_mcp_client at runtime via POST /api/config and adding it to llm_visible_cap_groups, the three MCP tools (mcp_list_tools, mcp_call_tool, mcp_discover) appear in the LLM tool list and the model calls them. Every call fails immediately on-device with:

E (XXXXXX) esp_mcp_mgr: esp_mcp_mgr_post_req(950): MCP instance not configured
I (XXXXXX) claw_core: tool_result request=N name=mcp_list_tools err=ESP_ERR_INVALID_STATE \
                     output=Error: MCP request failed (ESP_ERR_INVALID_STATE)

No HTTP request is ever sent to the remote MCP server (verified by tcpdump-equivalent on the server side: zero packets from the device IP, while local probes from the same machine succeed with HTTP 200).

The failure is therefore not a transport, network, firewall, DNS-rebinding-protection or protocol-version (SSE vs Streamable HTTP) issue. It happens locally on the device before any byte goes out.

Steps to reproduce

  1. Flash a stock edge_agent image (verified with the v0.1.0-14 binary from the official web flasher).

  2. Provision Wi-Fi as usual.

  3. Enable the client cap and make it visible to the LLM:

    curl -X POST -H "Content-Type: application/json" \
         -d '{"enabled_cap_groups":"cap_im_tg,cap_files,cap_lua,cap_skill,claw_memory,cap_agent_mgr,cap_mcp_client",
              "llm_visible_cap_groups":"cap_mcp_client"}' \
         http://<board-ip>/api/config
    
    curl -X POST http://<board-ip>/api/restart
  4. After reboot, confirm registration via the serial REPL:

    app> cap groups
    ...
    cap_mcp_client state=started descriptors=3 plugin=- version=-
    ...
    
    app> cap list
    ...
    mcp_list_tools [mcp] List tools from a remote MCP server.
    mcp_call_tool  [mcp] Call a tool on a remote MCP server.
    mcp_discover   [mcp] Discover MCP servers advertised on the local network.
    ...
    
  5. Stand up any reachable MCP server (we used FastMCP / Python mcp SDK with transport="streamable-http", port 8770, DNS rebinding protection disabled, verified reachable from the device's subnet with curl -X POST .../mcp returning HTTP 200 and a valid initialize response).

  6. Ask the LLM to call one of the MCP tools, e.g.:

    app> ask_once Call mcp_list_tools with server_url=http://<server-ip>:8770/mcp and endpoint=/mcp
    

Actual result

I claw_core: tool_call request=N name=mcp_list_tools args={"server_url":"http://...","endpoint":"/mcp"}
E esp_mcp_mgr: esp_mcp_mgr_post_req(950): MCP instance not configured
I claw_core: tool_result request=N name=mcp_list_tools err=ESP_ERR_INVALID_STATE \
             output=Error: MCP request failed (ESP_ERR_INVALID_STATE)

mcp_call_tool and mcp_discover show the same behavior. mcp_discover returns (no mcp servers discovered) (mDNS), which is a separate code path and not affected.

Expected result

Once cap_mcp_client is enabled and visible, the three MCP client tools should be operational and able to talk to a configured MCP endpoint, or — if cap_mcp_client cannot work standalone — edge_agent should refuse to start the cap, hide the tools from the LLM, or return a clearer error than ESP_ERR_INVALID_STATE.

Root-cause analysis

Looking at the source on master (HEAD 52352e5):

  1. The client builds its manager config without setting .instancecomponents/claw_capabilities/cap_mcp_client/src/cap_mcp_client_core.c, cap_mcp_mgr_create():

    static esp_err_t cap_mcp_mgr_create(const char *server_url,
                                        const char *endpoint,
                                        esp_mcp_mgr_handle_t *mgr_out)
    {
        esp_http_client_config_t http_config = {
            .url = server_url,
            .timeout_ms = CAP_MCP_HTTP_TIMEOUT_MS,
            .buffer_size = 4096,
            .keep_alive_enable = true,
        };
        esp_mcp_mgr_config_t mgr_config = {
            .transport = esp_mcp_transport_http_client,
            .config = &http_config,
            // .instance is intentionally not set
        };
        ...
        err = esp_mcp_mgr_init(mgr_config, mgr_out);
        ...
    }
  2. The only place that calls esp_mcp_create() is the server capcomponents/claw_capabilities/cap_mcp_server/src/cap_mcp_server.c:107:

    ESP_RETURN_ON_ERROR(esp_mcp_create(&s_mcp), TAG, "Failed to create MCP engine");

    This is the only call site for esp_mcp_create in the whole tree (grep -r esp_mcp_create components/ application/ returns just this file). Without it the manager has no MCP engine instance attached, which appears to be exactly what esp_mcp_mgr_post_req(950): MCP instance not configured reports.

  3. cap_mcp_server is not exposed via enabled_cap_groups in edge_agentcomponents/common/app_claw/app_capabilities.c:777 only lists the client entry:

    #if CONFIG_APP_CLAW_CAP_MCP_CLIENT
        { "cap_mcp_client", "MCP Client", "Register MCP client cap", false, NULL, app_cap_register_mcp_client },
    #endif

    No corresponding cap_mcp_server row exists. Adding cap_mcp_server to enabled_cap_groups is accepted by /api/config ({"ok":true,"applied":1}) but the cap does not actually appear in cap groups after reboot. Empirically confirmed on the device.

  4. The reference mcp_server_point app initializes the server manually from main.c, as documented in application/mcp_server_point/README.md:

    cap_mcp_server (outside app_claw cap groups): cap_mcp_server_initcap_mcp_lua_tools_initcap_mcp_server_start.

    application/edge_agent/main.c does not perform any equivalent initialization, so on edge_agent the global MCP engine instance is never created.

Net effect: on edge_agent, enabling cap_mcp_client registers the three tools and makes them visible to the LLM, but every invocation fails inside mcp-c-sdk because no global MCP instance exists. There is no runtime knob in edge_agent to fix this — it requires recompiling and patching main.c.

Possible fixes (in increasing order of scope)

  • Option A — make cap_mcp_client self-sufficient. In cap_mcp_client_core.c::cap_mcp_mgr_create (or in the cap's group_init), lazily esp_mcp_create() a private instance if none is registered, and bind it to mgr_config.instance for the HTTP client transport. Tear it down in group_deinit.

  • Option B — expose cap_mcp_server to runtime configuration. Add a cap_mcp_server row to the app_capabilities.c table guarded by CONFIG_APP_CLAW_CAP_MCP_SERVER, so users can enable both caps from enabled_cap_groups and the server's esp_mcp_create() provides the global instance the client relies on. (This is closest to the architecture mcp_server_point already documents.)

  • Option C — document and gate. If using the client requires the server, mark CONFIG_APP_CLAW_CAP_MCP_CLIENT as depending on CONFIG_APP_CLAW_CAP_MCP_SERVER in Kconfig, document the constraint in docs/.../cap-mcp.mdx, and either fail-fast at cap_mcp_client registration or hide the tools from the LLM when no MCP instance exists, so the user gets a clear signal instead of ESP_ERR_INVALID_STATE per call.

We'd happily test a patched build on the same hardware to confirm any of these.

Why this matters

From an integrator's point of view the current behavior is silently wrong:

  • cap_mcp_client is default y in Kconfig.
  • It registers cleanly. cap groups and cap list both confirm the three tools.
  • The LLM picks them up and uses them autonomously when prompted.
  • Every call errors out at runtime, but only after the LLM has already burned tokens and the user has watched it try.

A clearer failure mode — or, better, a self-sufficient client cap — would close this gap.

Thanks for the work on ESP-Claw. Happy to provide additional logs, run a custom build, or share the FastMCP server config we used for the reproduction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions