forked from molecule-ai/molecule-core
fix(external-connect): use molecule-mcp wrapper in Codex/OpenClaw templates (#2957)
The External Connect modal's Codex and OpenClaw tabs were rendering this MCP server config: command = "python3" args = ["-m", "molecule_runtime.a2a_mcp_server"] That spawns the bare MCP dispatcher with no presence wiring. The ``molecule-mcp`` console-script wrapper (mcp_cli.main) is what calls ``POST /registry/register`` at startup and runs the 20s heartbeat thread alongside the MCP stdio loop. Without the wrapper, the canvas flips the workspace back to ``awaiting_agent`` (OFFLINE) within 60-90s — even while tools work — because nothing is heartbeating. Operator-side this looks like: the workspace is registered and tools work fine when invoked, but the canvas shows "offline" / "Restart" CTA, peer agents see the workspace as awaiting_agent in list_peers output, and inbound A2A delivery silently fails the readiness check. A new external-Codex operator (#2957) hit this and spent debugging time on what should have been a copy-paste install. Fix: switch both Codex and OpenClaw templates to ``command = "molecule-mcp"`` / ``args = []``, matching the universal MCP template that already handles this correctly. Inline comment in each template explains the wrapper-vs-bare-module tradeoff so a future template author doesn't regress to the shorter form. Hermes-channel intentionally still spawns the bare module — the hermes plugin owns the platform plugin path and runs its own register_platform/heartbeat code in-process; double-heartbeating would race. Universal/Codex/OpenClaw all need the wrapper. Regression gate: TestExternalMcpTemplates_UseMoleculeMcpWrapper asserts the three templates that must use the wrapper actually do, and explicitly fails on the old ``-m molecule_runtime.a2a_mcp_server`` shape. Verified the test FAILS on pre-fix source by stashing only external_connection.go and re-running. Source: molecule-core#2957 issue 1 (item 4 of the report — the ``(codex returned empty output)`` / opaque-canvas-error / stale- session items live in codex-channel-molecule and are tracked separately). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ada27fdb5d
commit
eab36e217e
@ -423,14 +423,23 @@ mkdir -p ~/.codex
|
||||
# (then open ~/.codex/config.toml in your editor and paste:)
|
||||
#
|
||||
# [mcp_servers.molecule]
|
||||
# command = "python3"
|
||||
# args = ["-m", "molecule_runtime.a2a_mcp_server"]
|
||||
# command = "molecule-mcp"
|
||||
# args = []
|
||||
# startup_timeout_sec = 30
|
||||
#
|
||||
# [mcp_servers.molecule.env]
|
||||
# WORKSPACE_ID = "{{WORKSPACE_ID}}"
|
||||
# PLATFORM_URL = "{{PLATFORM_URL}}"
|
||||
# MOLECULE_WORKSPACE_TOKEN = "<paste from create response>"
|
||||
#
|
||||
# Use the "molecule-mcp" console-script wrapper (NOT
|
||||
# "python3 -m molecule_runtime.a2a_mcp_server"). The wrapper is what
|
||||
# keeps the workspace ALIVE on the canvas: it POSTs /registry/register
|
||||
# at startup and runs a 20s heartbeat thread alongside the MCP stdio
|
||||
# loop. The bare a2a_mcp_server module exposes tools but does NOT
|
||||
# heartbeat — pointing codex at it leaves the canvas showing this
|
||||
# workspace as awaiting_agent (OFFLINE) within 60-90s even while
|
||||
# tools work.
|
||||
|
||||
# 3. Run the bridge daemon as a durable background process — this
|
||||
# is the INBOUND path. Long-polls the platform inbox and runs
|
||||
@ -507,11 +516,20 @@ pip install molecule-ai-workspace-runtime
|
||||
|
||||
# 3. Wire the molecule MCP server. {{WORKSPACE_ID}} + {{PLATFORM_URL}}
|
||||
# are stamped server-side; paste the auth token before running.
|
||||
#
|
||||
# Use the "molecule-mcp" console-script wrapper (NOT
|
||||
# "python3 -m molecule_runtime.a2a_mcp_server"). The wrapper is what
|
||||
# keeps the workspace ALIVE on the canvas: it POSTs /registry/register
|
||||
# at startup and runs a 20s heartbeat thread alongside the MCP stdio
|
||||
# loop. The bare a2a_mcp_server module exposes tools but does NOT
|
||||
# heartbeat — pointing openclaw at it leaves the canvas showing this
|
||||
# workspace as awaiting_agent (OFFLINE) within 60-90s even while
|
||||
# tools work.
|
||||
WORKSPACE_TOKEN="<paste from create response>"
|
||||
openclaw mcp set molecule "$(cat <<EOF
|
||||
{
|
||||
"command": "python3",
|
||||
"args": ["-m", "molecule_runtime.a2a_mcp_server"],
|
||||
"command": "molecule-mcp",
|
||||
"args": [],
|
||||
"env": {
|
||||
"WORKSPACE_ID": "{{WORKSPACE_ID}}",
|
||||
"PLATFORM_URL": "{{PLATFORM_URL}}",
|
||||
|
||||
@ -38,3 +38,40 @@ func TestExternalTemplates_NoMoleculeOrgIDPlaceholder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestExternalMcpTemplates_UseMoleculeMcpWrapper pins the invariant
|
||||
// that operator-facing snippets configuring an MCP server entry point
|
||||
// use the ``molecule-mcp`` console-script wrapper (mcp_cli.main),
|
||||
// NOT the bare ``a2a_mcp_server`` module.
|
||||
//
|
||||
// Why: a2a_mcp_server exposes the MCP tools but does NOT call
|
||||
// /registry/register or run the 20s heartbeat thread. mcp_cli wraps
|
||||
// it with both, which is what flips the canvas presence indicator
|
||||
// from awaiting_agent (OFFLINE) to online and keeps it that way.
|
||||
// Originally tracked by molecule-core#2957 — operator hit the
|
||||
// silent-OFFLINE failure mode when the Codex tab pointed at the bare
|
||||
// module.
|
||||
//
|
||||
// The hermes-channel template intentionally uses the bare module: it
|
||||
// owns the platform plugin path and runs its own
|
||||
// register_platform/heartbeat code in-process, so wrapping with
|
||||
// mcp_cli would double-heartbeat. universalMcp / codex / openclaw
|
||||
// must all use the wrapper.
|
||||
func TestExternalMcpTemplates_UseMoleculeMcpWrapper(t *testing.T) {
|
||||
mustUseWrapper := map[string]string{
|
||||
"externalUniversalMcpTemplate": externalUniversalMcpTemplate,
|
||||
"externalCodexTemplate": externalCodexTemplate,
|
||||
"externalOpenClawTemplate": externalOpenClawTemplate,
|
||||
}
|
||||
for name, body := range mustUseWrapper {
|
||||
if !strings.Contains(body, "molecule-mcp") {
|
||||
t.Errorf("%s does not reference 'molecule-mcp' — operator-facing MCP snippets must point at the heartbeat-wrapping console script, not the bare a2a_mcp_server module (#2957)", name)
|
||||
}
|
||||
if strings.Contains(body, `"-m", "molecule_runtime.a2a_mcp_server"`) {
|
||||
t.Errorf("%s spawns 'python3 -m molecule_runtime.a2a_mcp_server' — that bypasses the standalone register/heartbeat wrapper, leaving the canvas showing the workspace OFFLINE (#2957). Use 'molecule-mcp' instead.", name)
|
||||
}
|
||||
if strings.Contains(body, `["-m", "molecule_runtime.a2a_mcp_server"]`) {
|
||||
t.Errorf("%s spawns 'python3 -m molecule_runtime.a2a_mcp_server' — that bypasses the standalone register/heartbeat wrapper, leaving the canvas showing the workspace OFFLINE (#2957). Use 'molecule-mcp' instead.", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user