NousResearch/hermes-agent

Cron jobs lack MCP server initialization

Cluster 4219 · cron. Data as of

Nothing open on GitHub. Fix merged in #21354 on 7 May 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #21354. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #21354. That is separate from the fix event.
Still open
Nothing open on GitHub.

A bug where cron jobs cannot access MCP servers due to initialization order, along with related fixes. Includes a CLI display width fix bundled in one of the patches.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #21354 fix(cron): initialize MCP servers before constructing the cron AIAgent

    merged to main

    files: cron/scheduler.py, tests/cron/test_scheduler_mcp_init.py

    Show the diff
    • cron/scheduler.py
      @@ -1261,6 +1261,27 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
                   except Exception as e:
                       logger.debug("Job '%s': failed to load credential pool for %s: %s", job_id, runtime_provider, e)
       
      +        # Initialize MCP servers so configured mcp_servers are available to
      +        # the agent's tool registry before AIAgent is constructed. Without
      +        # this, cron jobs never saw any MCP tools — only the gateway / CLI
      +        # paths called discover_mcp_tools() at startup. Idempotent: subsequent
      +        # ticks short-circuit on already-connected servers inside
      +        # register_mcp_servers(). Non-fatal on failure: a broken MCP server
      +        # shouldn't kill an otherwise-working cron job. See #4219.
      +        try:
      +            from tools.mcp_tool import discover_mcp_tools
      +            _mcp_tools = discover_mcp_tools()
      +            if _mcp_tools:
      +                logger.info(
      +                    "Job '%s': %d MCP tool(s) available",
      +                    job_id, len(_mcp_tools),
      +                )
      +        except Exception as _mcp_exc:
      +            logger.warning(
      +                "Job '%s': MCP initialization failed (non-fatal): %s",
      +                job_id, _mcp_exc,
      +            )
      +
               agent = AIAgent(
                   model=model,
                   api_key=runtime.get("api_key"),
    • tests/cron/test_scheduler_mcp_init.py
      @@ -0,0 +1,140 @@
      +"""Regression tests for MCP server availability in cron jobs.
      +
      +Background
      +==========
      +``cron/scheduler.py:run_job()`` constructs ``AIAgent(...)`` directly without
      +calling ``discover_mcp_tools()`` — the initialization that CLI and gateway
      +paths do at startup. Cron jobs therefore never saw any MCP tools from
      +``mcp_servers`` in config.yaml. See #4219.
      +
      +The fix inserts ``discover_mcp_tools()`` before the ``AIAgent(...)`` call,
      +wrapped in try/except so a broken MCP server can't kill an otherwise
      +working cron job. ``discover_mcp_tools`` is idempotent — subsequent ticks
      +short-circuit on already-connected servers.
      +"""
      +
      +from __future__ import annotations
      +
      +from unittest.mock import patch, MagicMock
      +
      +import pytest
      +
      +
      +def test_run_job_calls_discover_mcp_tools_before_agent_construction():
      +    """The LLM-path branch of run_job must call discover_mcp_tools() before
      +    the AIAgent construction, so MCP tools are in the registry by the time
      +    the agent asks for its tool schema."""
      +    from cron import scheduler
      +
      +    job = {
      +        "id": "mcp-cron-test",
      +        "name": "mcp-cron-test",
      +        "prompt": "test",
      +    }
      +
      +    call_order = []
      +
      +    def fake_discover():
      +        call_order.append("discover_mcp_tools")
      +        return ["mcp_server1_tool"]
      +
      +    # AIAgent is a class; replace with a recording stub
      +    class _FakeAgent:
      +        def __init__(self, *args, **kwargs):
      +            call_order.append("AIAgent.__init__")
      +            self._kwargs = kwargs
      +            self._interrupt_requested = False
      +            self.quiet_mode = True
      +
      +        def run_conversation(self, *args, **kwargs):
      +            return {
      +                "final_response": "ok",
      +                "messages": [],
      +            }
      +
      +    with patch("tools.mcp_tool.discover_mcp_tools", side_effect=fake_discover), \
      +         patch("run_agent.AIAgent", _FakeAgent), \
      +         patch("cron.scheduler._resolve_cron_enabled_toolsets", return_value=None):
      +        scheduler.run_job(job)
      +
      +    # Discovery must be called, and must be called BEFORE agent construction.
      +    assert "discover_mcp_tools" in call_order, (
      +        "run_job did not call discover_mcp_tools — MCP tools unavailable in cron"
      +    )
      +    d_idx = call_order.index("discover_mcp_tools")
      +    a_idx = call_order.index("AIAgent.__init__")
      +    assert d_idx < a_idx, (
      +        f"discover_mcp_tools was called AFTER AIAgent construction "
      +        f"(indices discover={d_idx}, agent={a_idx}); MCP tools missed the "
      +        f"registry window. Full order: {call_order}"
      +    )
      +
      +
      +def test_run_job_tolerates_discover_mcp_tools_failure():
      +    """A broken MCP server must not kill an otherwise working cron job.
      +    discover_mcp_tools() raising should be caught and logged, and the agent
      +    should still run."""
      +    from cron import scheduler
      +
      +    job = {
      +        "id": "mcp-cron-fail",
      +        "name": "mcp-cron-fail",
      +        "prompt": "test",
      +    }
      +
      +    agent_was_constructed = []
      +
      +    class _FakeAgent:
      +        def __init__(self, *args, **kwargs):
      +            agent_was_constructed.append(True)
      +            self._interrupt_requested = False
      +            self.quiet_mode = True
      +
      +        def run_conversation(self, *args, **kwargs):
      +            return {"final_response": "ok", "messages": []}
      +
      +    def fake_discover_that_raises():
      +        raise RuntimeError("MCP server unreachable")
      +
      +    with patch(
      +        "tools.mcp_tool.discover_mcp_tools",
      +        side_effect=fake_discover_that_raises,
      +    ), patch("run_agent.AIAgent", _FakeAgent), \
      +         patch("cron.scheduler._resolve_cron_enabled_toolsets", return_value=None):
      +        # Should NOT raise
      +        success, doc, final_response, error = scheduler.run_job(job)
      +
      +    assert agent_was_constructed, (
      +        "AIAgent was not constructed after discover_mcp_tools raised — "
      +        "MCP failure incorrectly killed the cron job"
      +    )
      +
      +
      +def test_no_agent_cron_job_does_not_initialize_mcp():
      +    """Cron jobs with no_agent=True are script-only — no AIAgent, no MCP
      +    tools needed. We must NOT pay the MCP init cost for those."""
      +    from cron import scheduler
      +
      +    job = {
      +        "id": "noagent-job",
      +        "name": "noagent-job",
      +        "no_agent": True,
      +        "script": "/nonexistent/script.sh",
      +    }
      +
      +    discover_called = []
      +
      +    def fake_discover():
      +        discover_called.append(True)
      +        return []
      +
      +    # _run_job_script returns (ok, output); make it fail cleanly so we
      +    # don't need a real script file.
      +    with patch("tools.mcp_tool.discover_mcp_tools", side_effect=fake_discover), \
      +         patch("cron.scheduler._run_job_script", return_value=(False, "no such file")):
      +        scheduler.run_job(job)
      +
      +    assert not discover_called, (
      +        "discover_mcp_tools was called for a no_agent job — wasted MCP init "
      +        "for a script-only cron tick"
      +    )

1 PR closed without merging

From the close comments: done elsewhere 1

  • #4213 fix(cli): use display width for chat input; fix(cron): init MCP servers · done elsewhere
Closed issues · 1
  • Closed#4219Cron jobs cannot use skills, plugins, and MCP servers
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
cron
Merged PRs
#21354
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
7 May 2026

For your agent

The same facts, typed.