Skip to content

feat(mcp): smart loading — connect_strategy, tool budget, per-session scoping, meta-tools - #66481

Closed
prismatic7 wants to merge 4 commits into
NousResearch:mainfrom
prismatic7:feat/mcp-smart-loading
Closed

prismatic7 wants to merge 4 commits into
NousResearch:mainfrom
prismatic7:feat/mcp-smart-loading

Conversation

@prismatic7

@prismatic7 prismatic7 commented Jul 17, 2026

Copy link
Copy Markdown

Summary

Four levers to control MCP server loading, reducing startup time and context-window bloat for users with many MCP servers. Closes umbrella issue #66473.

Changes

Lever 1: connect_strategy (per-server)

Three modes — startup (default, backward compatible), lazy (defer subprocess spawn until first call), on_demand (invisible until explicitly loaded).

Lever 2: mcp_tool_budget (global)

Caps MCP tool schemas in the LLM prompt. LRU eviction drops least-used servers' tools. Meta-tools always survive. Default: 0 (unlimited).

Lever 3: /mcp slash commands

/mcp list, /mcp enable <name>, /mcp disable <name>, /mcp load <name> — per-session server management. CLI-only (no gateway dispatcher yet).

Lever 4: Meta-tools (always available)

mcp_list_servers and mcp_load_server — agent-driven discovery and activation.

Bug fixes (discovered during audit)

  1. Lazy server limbo_activate_lazy_server removed from _lazy_servers before connecting; failed connections left unrecoverable
  2. /mcp disable nuked everything — called _clear_session_loaded_servers() which cleared ALL on_demand servers, not just the named one
  3. Stub tools invisible — lazy server stubs had no toolset alias registered
  4. Meta-tools in wrong toolset — registered into "mcp" which doesn't exist in any platform composite

Maintainer review fixes (teknium1's hermes-sweeper)

  1. Session isolation_session_loaded_servers changed from a process-global set to Dict[session_id, set], keyed by a contextvars.ContextVar. Each asyncio task gets its own session identity, so one session's load/disable no longer affects every session.

  2. Lazy stub deregistration — after _activate_lazy_server() succeeds, the mcp__activate_* stub tool is deregistered via registry.deregister(). A subsequent stub call no longer fails silently.

  3. Agent tool refresh — added _refresh_active_agent_tools() helper that calls refresh_agent_mcp_tools() on the active agent after both lazy activation and on_demand load. Best-effort: if no active agent is found, tools are picked up on the next agent build (next-turn activation contract).

  4. /mcp CLI-only guard — added cli_only=True to the /mcp CommandDef. Prevents gateway/TUI from trying to dispatch an unimplemented route.

Files modified

File Changes
tools/mcp_tool.py connect_strategy parsing, lazy/on_demand tracking, meta-tools, session isolation, stub deregistration, agent refresh (+54/-14)
model_tools.py mcp_tool_budget enforcement, LRU tracking (+64)
toolsets.py Meta-tools added to _HERMES_CORE_TOOLS (+4/-1)
hermes_cli/config.py mcp_tool_budget default (+6)
hermes_cli/commands.py /mcp slash command registration, CLI-only guard (+4)
cli.py /mcp command handler (+104)

Backward Compatibility

  • connect_strategy defaults to startup — existing configs unchanged
  • mcp_tool_budget defaults to 0 (unlimited) — existing behavior preserved
  • enabled: false still works as before
  • idle_timeout_seconds / max_lifetime_seconds still work for recycling connected servers
  • Session identity contextvar defaults to "" — sessions that don't set it share a global namespace (backward compatible)

Related Issues

Three new per-task fields on delegate_task batch items:

- **toolsets** (string[]): restrict which tools a subagent loads.
  Previously hardcoded to None (inherit parent), now the model can
  narrow toolsets per task. Intersection with parent toolsets and
  blocked-tool stripping are handled by the existing validation chain.

- **persona** (string): injected into the child's system prompt as a
  YOUR ROLE block to specialize behavior (e.g. 'web researcher',
  'senior engineer') without cramming instructions into context.

- **timeout** (number): per-task wall-clock cap in seconds. Overrides
  the global delegation.child_timeout_seconds for that subagent.
  None = inherit the global default (no timeout by default).

All three are backward-compatible: single-task mode is unchanged,
and batch tasks without the new fields default to None with
existing inheritance behaviour preserved.
… scoping, meta-tools

Four levers to control MCP server loading, reducing startup time and
context-window bloat for users with many MCP servers.

Lever 1 — connect_strategy (per-server):
  startup (default): connect at Hermes startup, register all tools
  lazy: register tool schemas but defer subprocess spawn until first call
  on_demand: invisible until explicitly loaded via /mcp load or mcp_load_server

Lever 2 — mcp_tool_budget (global):
  Caps the number of MCP tool schemas in the LLM prompt.
  LRU eviction drops least-used servers' tools when budget exceeded.
  Meta-tools (mcp_list_servers, mcp_load_server) always survive eviction.
  Default: 0 (unlimited, backward compatible).

Lever 3 — /mcp slash commands:
  /mcp list              → show all servers, status, tool counts, strategy
  /mcp enable <name>     → activate a lazy/on_demand server now
  /mcp disable <name>    → mark an on_demand server inactive for this session
  /mcp load <name>       → same as enable

Lever 4 — meta-tools (always available):
  mcp_list_servers — returns all configured servers with status/strategy
  mcp_load_server  — activates a lazy or on_demand server mid-session

Bug fixes discovered during audit:
- _activate_lazy_server removed from _lazy_servers before connecting,
  leaving failed connections in limbo (not retryable, not connected)
- /mcp disable called _clear_session_loaded_servers() which cleared ALL
  on_demand servers, not just the named one
- Lazy server stub tools had no toolset alias registered, making them
  invisible to the platform resolver
- Meta-tools registered into toolset 'mcp' which doesn't exist in any
  platform composite — tools were invisible to the LLM

Refs: NousResearch#66473 (umbrella), NousResearch#63626 (lazy MCP), NousResearch#6839 (tool schema deferral),
      NousResearch#45955 (per-session scoping)
@alt-glitch alt-glitch added type/feature New feature or request comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/tools Tool registry, model_tools, toolsets tool/mcp MCP client and OAuth tool/delegate Subagent delegation area/config Config system, migrations, profiles P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state needs-decision Awaiting maintainer decision before any implementation labels Jul 17, 2026
@prismatic7
prismatic7 marked this pull request as ready for review July 18, 2026 06:02

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling the MCP startup and schema-footprint problem. This needs revision before it can safely land.

Problems

  • tools/mcp_tool.py:447-501 calls the state per-session, but _session_loaded_servers is one process-global set keyed only by server name. _make_check_fn() then uses it for visibility, so one session's load/disable operation changes every session's MCP surface.
  • tools/mcp_tool.py:5188 activates the lazy server, while :520-522 removes it from _lazy_servers; the mcp__activate_* stub registered at :5198-5206 is never deregistered. A later stub call therefore fails instead of being replaced as the docstring promises.
  • tools/mcp_tool.py:6271-6275 says the loaded tools are now available but does not refresh the active agent's snapshot. The existing helper documents that agent.tools is snapshotted once and that refresh callers own the cache contract (tools/mcp_tool.py:5524-5556).
  • hermes_cli/commands.py:217-219 makes /mcp gateway-visible, but gateway/run.py has no matching dispatcher branch.

Suggested changes

  • Key visibility by real session identity and add multi-session isolation tests.
  • Atomically replace/deregister the lazy stub after activation.
  • Define a cache-safe next-turn activation contract or add an explicitly consented refresh path.
  • Make /mcp CLI-only or implement its gateway/TUI routes and tests.

Automated hermes-sweeper review.

Comment thread tools/mcp_tool.py
Comment thread tools/mcp_tool.py
Comment thread tools/mcp_tool.py
Comment thread hermes_cli/commands.py
@prismatic7
prismatic7 force-pushed the feat/mcp-smart-loading branch from 7001926 to 8b5d110 Compare July 18, 2026 23:21
@teknium1 teknium1 added sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 18, 2026
@prismatic7

Copy link
Copy Markdown
Author

All resolved. Thanks for the feedback!

@teknium1 teknium1 added the area/sessions Session lifecycle, resume, persistence, history label Jul 19, 2026
@prismatic7
prismatic7 force-pushed the feat/mcp-smart-loading branch from 31e9c8a to 7a00d5d Compare July 20, 2026 10:52
@carljborg

Copy link
Copy Markdown
Contributor

Strong +1 on connect_strategy: lazy from a live deployment — and a use case that I think sharpens the motivation beyond "many servers": MCP servers backed by a slow/serverless upstream.

We run an agentmail-mcp stdio bridge whose upstream is a hosted, serverless MCP endpoint (mcp.agentmail.to) that cold-starts in ~7–18s and occasionally times out entirely. Under the current eager startup strategy it fails the initial handshake, burns its 3 retries, and parks — silently disabling the email tools for the whole session, even though the endpoint answers fine a few seconds later when actually used. (It also happened to trip the kanban-reaper race #63728 in the gateway, but that's orthogonal — even with that fixed it still parks, because a cold serverless endpoint just can't complete a handshake inside the startup window.)

connect_strategy: lazy is the correct fix here: register the stub tools at startup so the model can call them, and only pay the connection cost on first use — when the endpoint is far more likely to be warm, and a slow/failed connect degrades a single tool call instead of killing the whole server for the session. That's the difference between "email tools dead all session" and "email tools work when the user asks for email."

So this lever isn't only a startup-time / context-footprint optimization for large fleets — it's arguably the right default handling for any MCP server whose upstream is on-demand/serverless. Thanks for building it; happy to test a revised revision against our (reliably-cold) setup if that's useful for the needs-decision.

@teknium1

Copy link
Copy Markdown
Collaborator

Thanks for the thorough audit and the four-lever build here — real effort went into this, and the bug fixes you surfaced along the way (lazy-server limbo, /mcp disable over-clearing, stub deregistration) are genuinely useful findings.

Closing this without merging, because the design has diverged from where we're taking MCP loading:

Context bloat (Levers 2, 3, 4 — tool budget, /mcp session scoping, meta-tools): already solved on main by the tool_search progressive-disclosure system (tools/tool_search.py, config tools.tool_search, auto mode). When deferrable MCP/plugin schemas exceed the context threshold they're replaced with tool_search / tool_describe / tool_call bridge tools and surfaced on demand. That fully covers the schema-footprint problem the tool budget + LRU eviction + mcp_list_servers/mcp_load_server meta-tools were built for, without the per-session registry-visibility toggling (which is cache- and session-state-hostile — the sweeper flagged exactly that).

Startup time (Lever 1 — connect_strategy: lazy): we're going a different direction than lazy. Rather than deferring connection to first tool call (which trades startup cost for a slow/park-prone first call), the plan is to keep enabled MCP servers always connected but owned by the gateway process instead of the per-session agent — pay the npx/handshake cost once at gateway boot, and have every session reuse the already-live connections. That eliminates per-session respawn cost without the lazy tradeoff.

I've re-scoped the umbrella issue #66473 to track that gateway-attached-persistent-MCP design.

Your commits keep their authorship in your branch. If you want to take a run at the gateway-attached approach, #66473 is the place — happy to have you on it.

@teknium1 teknium1 closed this Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles area/sessions Session lifecycle, resume, persistence, history comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/tools Tool registry, model_tools, toolsets needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/delegate Subagent delegation tool/mcp MCP client and OAuth type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants