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
-
Flash a stock edge_agent image (verified with the v0.1.0-14 binary from the official web flasher).
-
Provision Wi-Fi as usual.
-
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
-
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.
...
-
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).
-
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):
-
The client builds its manager config without setting .instance — components/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);
...
}
-
The only place that calls esp_mcp_create() is the server cap — components/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.
-
cap_mcp_server is not exposed via enabled_cap_groups in edge_agent — components/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.
-
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_init → cap_mcp_lua_tools_init → cap_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.
Bug:
cap_mcp_clienttools exposed to the LLM but unusable because the MCP instance is never initializedEnvironment
espressif/esp-clawapplication/edge_agentv0.1.0-14-g52352e5(master HEAD52352e5, merged 2026-06-18)esp32_S3_DevKitC_1__v0.1.0-14-g52352e5__serial_jtag.bin.gzfrom the official web flasherespressif/mcp-c-sdk ^2.0.0(resolved to v2.0.1)streamable-http(FastMCP / PythonmcpSDK, server on a LAN host, reachable, returns 200 oninitialize)Summary
After enabling
cap_mcp_clientat runtime viaPOST /api/configand adding it tollm_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: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
Flash a stock
edge_agentimage (verified with the v0.1.0-14 binary from the official web flasher).Provision Wi-Fi as usual.
Enable the client cap and make it visible to the LLM:
After reboot, confirm registration via the serial REPL:
Stand up any reachable MCP server (we used FastMCP / Python
mcpSDK withtransport="streamable-http", port 8770, DNS rebinding protection disabled, verified reachable from the device's subnet withcurl -X POST .../mcpreturning HTTP 200 and a validinitializeresponse).Ask the LLM to call one of the MCP tools, e.g.:
Actual result
mcp_call_toolandmcp_discovershow the same behavior.mcp_discoverreturns(no mcp servers discovered)(mDNS), which is a separate code path and not affected.Expected result
Once
cap_mcp_clientis enabled and visible, the three MCP client tools should be operational and able to talk to a configured MCP endpoint, or — ifcap_mcp_clientcannot work standalone —edge_agentshould refuse to start the cap, hide the tools from the LLM, or return a clearer error thanESP_ERR_INVALID_STATE.Root-cause analysis
Looking at the source on
master(HEAD52352e5):The client builds its manager config without setting
.instance—components/claw_capabilities/cap_mcp_client/src/cap_mcp_client_core.c,cap_mcp_mgr_create():The only place that calls
esp_mcp_create()is the server cap —components/claw_capabilities/cap_mcp_server/src/cap_mcp_server.c:107:This is the only call site for
esp_mcp_createin 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 whatesp_mcp_mgr_post_req(950): MCP instance not configuredreports.cap_mcp_serveris not exposed viaenabled_cap_groupsinedge_agent—components/common/app_claw/app_capabilities.c:777only lists the client entry:No corresponding
cap_mcp_serverrow exists. Addingcap_mcp_servertoenabled_cap_groupsis accepted by/api/config({"ok":true,"applied":1}) but the cap does not actually appear incap groupsafter reboot. Empirically confirmed on the device.The reference
mcp_server_pointapp initializes the server manually frommain.c, as documented inapplication/mcp_server_point/README.md:application/edge_agent/main.cdoes not perform any equivalent initialization, so onedge_agentthe global MCP engine instance is never created.Net effect: on
edge_agent, enablingcap_mcp_clientregisters the three tools and makes them visible to the LLM, but every invocation fails insidemcp-c-sdkbecause no global MCP instance exists. There is no runtime knob inedge_agentto fix this — it requires recompiling and patchingmain.c.Possible fixes (in increasing order of scope)
Option A — make
cap_mcp_clientself-sufficient. Incap_mcp_client_core.c::cap_mcp_mgr_create(or in the cap'sgroup_init), lazilyesp_mcp_create()a private instance if none is registered, and bind it tomgr_config.instancefor the HTTP client transport. Tear it down ingroup_deinit.Option B — expose
cap_mcp_serverto runtime configuration. Add acap_mcp_serverrow to theapp_capabilities.ctable guarded byCONFIG_APP_CLAW_CAP_MCP_SERVER, so users can enable both caps fromenabled_cap_groupsand the server'sesp_mcp_create()provides the global instance the client relies on. (This is closest to the architecturemcp_server_pointalready documents.)Option C — document and gate. If using the client requires the server, mark
CONFIG_APP_CLAW_CAP_MCP_CLIENTas depending onCONFIG_APP_CLAW_CAP_MCP_SERVERin Kconfig, document the constraint indocs/.../cap-mcp.mdx, and either fail-fast atcap_mcp_clientregistration or hide the tools from the LLM when no MCP instance exists, so the user gets a clear signal instead ofESP_ERR_INVALID_STATEper 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_clientisdefault yin Kconfig.cap groupsandcap listboth confirm the three tools.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.