From 18d904cfc1def584a13d23bc50f7e140844fda84 Mon Sep 17 00:00:00 2001 From: rabbitblood Date: Thu, 16 Apr 2026 13:28:57 -0700 Subject: [PATCH] fix: MCP server path resolution + absolute imports (2nd half of #507) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The a2a MCP subprocess was launched with a hard-coded /app/a2a_mcp_server.py path that only existed in the legacy workspace-template layout. Current templates copy adapter.py into /app but not the MCP server script, so claude-code's mcp_servers={"a2a": ...} config spawned a non-existent file, the server never registered any tools, and every agent reported that search_memory / commit_memory / list_peers / delegate_task / send_message_to_user were unavailable in the tool registry. Surfaced this cycle after the CRLF hook fix (PR molecule-core#508 + plugin repo's .gitattributes) unblocked the primary (no response generated) symptom. Before that, agents crashed before the missing-MCP issue was observable — the two bugs stacked. Changes ------- * executor_helpers._default_mcp_server_path: resolves the installed molecule_runtime.a2a_mcp_server module's __file__ so the path is always correct regardless of template layout. Legacy /app path kept as last-resort fallback for any old images still in rotation. * a2a_mcp_server.py, a2a_tools.py, a2a_client.py: convert bare module imports (from a2a_tools import ...) to absolute (from molecule_runtime.a2a_tools import ...). Previously this worked only when main.py injected the package dir onto sys.path; the MCP subprocess doesn't go through main.py, so the bare imports would fail. Added a sys.path shim at the top of a2a_mcp_server.py so running as a standalone script (python path/to/a2a_mcp_server.py) still works — the subprocess can now locate the package root automatically. * consolidation.py, heartbeat.py, main.py: same bare-to-absolute conversion for platform_auth imports (unblocks the same class of failure if any of these modules are imported from a non-main.py entrypoint in the future). Verification ------------ Deployed the updated files into ws-8010dbd0 (PM) and ran an isolated sdk.query() as agent user. SystemMessage.init.mcp_servers now reports [{'name': 'a2a', 'status': 'connected'}] and the tools list includes all 8 mcp__a2a__* entries: mcp__a2a__check_task_status, mcp__a2a__commit_memory, mcp__a2a__delegate_task, mcp__a2a__delegate_task_async, mcp__a2a__get_workspace_info, mcp__a2a__list_peers, mcp__a2a__recall_memory, mcp__a2a__send_message_to_user Rolled the in-container hotfix across all 22 workspaces pending release (docker cp the 4 changed files into each site-packages/molecule_runtime/). Fixes Molecule-AI/molecule-core#507 (secondary) Co-Authored-By: Claude Opus 4.6 (1M context) --- molecule_runtime/a2a_client.py | 2 +- molecule_runtime/a2a_mcp_server.py | 22 ++++++++++++++++--- molecule_runtime/a2a_tools.py | 4 ++-- molecule_runtime/consolidation.py | 2 +- molecule_runtime/executor_helpers.py | 33 ++++++++++++++++++++++++++-- molecule_runtime/heartbeat.py | 2 +- molecule_runtime/main.py | 2 +- 7 files changed, 56 insertions(+), 11 deletions(-) diff --git a/molecule_runtime/a2a_client.py b/molecule_runtime/a2a_client.py index 0ae6dd2..5e0b057 100644 --- a/molecule_runtime/a2a_client.py +++ b/molecule_runtime/a2a_client.py @@ -10,7 +10,7 @@ import uuid import httpx -from platform_auth import auth_headers +from molecule_runtime.platform_auth import auth_headers logger = logging.getLogger(__name__) diff --git a/molecule_runtime/a2a_mcp_server.py b/molecule_runtime/a2a_mcp_server.py index 29ca254..3f25cb9 100644 --- a/molecule_runtime/a2a_mcp_server.py +++ b/molecule_runtime/a2a_mcp_server.py @@ -17,7 +17,23 @@ import json import logging import sys -from a2a_tools import ( +# Absolute imports so the installed-package location works too. Previously +# the script relied on `/app` being on sys.path (legacy template layout), +# which broke silently when the current template dropped that copy — +# claude-code then initialised with zero MCP tools and every agent +# reported "search_memory / commit_memory / list_peers / delegate_task +# not available" (second half of #507). The /app launch path is still +# supported via a sys.path shim below for anyone running the script +# with `python /app/a2a_mcp_server.py`. +import os as _os +if __package__ in (None, ""): + # Running as a script (python path/to/a2a_mcp_server.py) — put the + # package root on sys.path so the absolute imports below resolve. + _pkg_root = _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))) + if _pkg_root not in sys.path: + sys.path.insert(0, _pkg_root) + +from molecule_runtime.a2a_tools import ( tool_check_task_status, tool_commit_memory, tool_delegate_task, @@ -32,7 +48,7 @@ logger = logging.getLogger(__name__) # Re-export constants and client functions so existing imports # (e.g. tests that do `import a2a_mcp_server`) still work. -from a2a_client import ( # noqa: F401, E402 +from molecule_runtime.a2a_client import ( # noqa: F401, E402 PLATFORM_URL, WORKSPACE_ID, _A2A_ERROR_PREFIX, @@ -42,7 +58,7 @@ from a2a_client import ( # noqa: F401, E402 get_workspace_info, send_a2a_message, ) -from a2a_tools import report_activity # noqa: F401, E402 +from molecule_runtime.a2a_tools import report_activity # noqa: F401, E402 # --- Tool definitions (schemas) --- diff --git a/molecule_runtime/a2a_tools.py b/molecule_runtime/a2a_tools.py index 6ba37a0..b6cda40 100644 --- a/molecule_runtime/a2a_tools.py +++ b/molecule_runtime/a2a_tools.py @@ -8,7 +8,7 @@ import uuid import httpx -from a2a_client import ( +from molecule_runtime.a2a_client import ( PLATFORM_URL, WORKSPACE_ID, _A2A_ERROR_PREFIX, @@ -24,7 +24,7 @@ def _auth_headers_for_heartbeat() -> dict[str, str]: """Return Phase 30.1 auth headers; tolerate platform_auth being absent in older installs (e.g. during rolling upgrade).""" try: - from platform_auth import auth_headers + from molecule_runtime.platform_auth import auth_headers return auth_headers() except Exception: return {} diff --git a/molecule_runtime/consolidation.py b/molecule_runtime/consolidation.py index 38e4b58..ae792db 100644 --- a/molecule_runtime/consolidation.py +++ b/molecule_runtime/consolidation.py @@ -14,7 +14,7 @@ import os import httpx -from platform_auth import auth_headers +from molecule_runtime.platform_auth import auth_headers logger = logging.getLogger(__name__) diff --git a/molecule_runtime/executor_helpers.py b/molecule_runtime/executor_helpers.py index c435f84..19fb4a5 100644 --- a/molecule_runtime/executor_helpers.py +++ b/molecule_runtime/executor_helpers.py @@ -36,7 +36,10 @@ logger = logging.getLogger(__name__) WORKSPACE_MOUNT = "/workspace" CONFIG_MOUNT = "/configs" -DEFAULT_MCP_SERVER_PATH = "/app/a2a_mcp_server.py" +# Legacy template layout copied a2a_mcp_server.py into /app. Current +# templates don't — the script lives inside the installed runtime package. +# Kept as a last-resort fallback only. +LEGACY_MCP_SERVER_PATH = "/app/a2a_mcp_server.py" DEFAULT_DELEGATION_RESULTS_FILE = "/tmp/delegation_results.jsonl" PLATFORM_HTTP_TIMEOUT_S = 5.0 MEMORY_RECALL_LIMIT = 10 @@ -44,12 +47,38 @@ MEMORY_CONTENT_MAX_CHARS = 200 BRIEF_SUMMARY_MAX_LEN = 80 +def _default_mcp_server_path() -> str: + """Resolve the installed ``a2a_mcp_server.py`` path from the package. + + Fix for the secondary half of #507: when agents started producing text + again (after the CRLF hook fix), the a2a MCP server failed to start + because the hard-coded ``/app/a2a_mcp_server.py`` doesn't exist in the + current workspace-template image — the template's Dockerfile copies + ``adapter.py`` into /app but not the MCP server script. claude-code + then initialised with zero MCP tools, so every agent reported + "search_memory / commit_memory / list_peers / delegate_task not + available" on the first post-fix pulse. + + Resolve the path from the package itself so it always points at the + real installed script, regardless of which template layout imported + the runtime. Legacy /app/ path kept only as last-resort fallback. + """ + try: + from molecule_runtime import a2a_mcp_server as _mcp_mod + path = getattr(_mcp_mod, "__file__", None) + if path and os.path.isfile(path): + return path + except Exception: + pass + return LEGACY_MCP_SERVER_PATH + + def get_mcp_server_path() -> str: """Return the path to the stdio MCP server script. Overridable via A2A_MCP_SERVER_PATH for tests and non-default layouts. """ - return os.environ.get("A2A_MCP_SERVER_PATH", DEFAULT_MCP_SERVER_PATH) + return os.environ.get("A2A_MCP_SERVER_PATH", _default_mcp_server_path()) # ======================================================================== diff --git a/molecule_runtime/heartbeat.py b/molecule_runtime/heartbeat.py index a67bec7..194d52e 100644 --- a/molecule_runtime/heartbeat.py +++ b/molecule_runtime/heartbeat.py @@ -17,7 +17,7 @@ from pathlib import Path import httpx -from platform_auth import auth_headers +from molecule_runtime.platform_auth import auth_headers logger = logging.getLogger(__name__) diff --git a/molecule_runtime/main.py b/molecule_runtime/main.py index 07fbe86..df4e1d2 100644 --- a/molecule_runtime/main.py +++ b/molecule_runtime/main.py @@ -39,7 +39,7 @@ from initial_prompt import ( mark_initial_prompt_attempted, resolve_initial_prompt_marker, ) -from platform_auth import auth_headers +from molecule_runtime.platform_auth import auth_headers def get_machine_ip() -> str: # pragma: no cover