Skip to content

feat(harness): share MCP servers by scope and close idle ones - #83

Draft
sanchitrk wants to merge 5 commits into
claude/mcp-resourcesfrom
claude/mcp-scopes
Draft

sanchitrk wants to merge 5 commits into
claude/mcp-resourcesfrom
claude/mcp-scopes

Conversation

@sanchitrk

@sanchitrk sanchitrk commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Stacked on #81. Decides who shares a running MCP server and how long it lives. Until now every session ran its own servers until the process exited.

Scopes (scope in mcp.json, default session)

Scope Shared by stdio cwd / roots on a local sandbox
session one session the session's directory
space sessions in one space (sandbox + folder) the space's folder
app every session where the app started, no roots
  • When the sandbox isn't local, session and space servers start in hostDir, else hostCwd, and get no roots. MCP stays host-only.
  • An instance is keyed by its scope plus everything its connection is made from (config minus exposure, cwd, roots). Sessions that resolve different settings for a server, for example through different project files, never share it.
  • Exposure stays each session's own. Two sessions can share an instance and declare different tools.

Lifetime

  • Each drain leases its session's instances for as long as it runs (runner/execute.ts, next to the mount).
  • An instance no session uses closes at once, for example when a server is removed or its settings change.
  • An instance no drain holds closes after an idle timeout: 10 minutes, or the Harness.layer option mcpIdleTimeout. The next exchange that needs it starts it again.
  • Space changes need no hooks. After rehome or absorb, a session's next exchange resolves a different key and drops the old instance; an archived env's instances idle out.

Also

  • A session's servers keep their config order across exchanges, so a restarted server's tools no longer move to the end of the list. That's more stable for prompt caching.
  • Docs: "Sharing a Server" and idle close on the MCP page.

Review fixes (Codex, gpt-6.1-sol high)

  • Name in the key. An instance key includes the server's name. Two entries with the same settings are two instances, and renaming a server can't close the instance its new name uses.
  • Exact lease counts. Leases count each instance once per session, so an alias can't drive a count negative and keep a server from idling out.
  • Atomic bookkeeping. Reconcile updates every membership and lease count in one step and closes unused instances on their own, so an interrupted exchange can't leave a shared instance counting a session it no longer has.
  • Idle timer from the start. An instance created with no drain holding it still gets an idle timer.
  • Canonical keys. Keys are canonical JSON, so the same env / headers in another order share an instance.
  • Dismissed: a lease arriving mid-reconcile. The coordinator runs one drain per session, and reconcile runs inside it. The atomic bookkeeping removes the window anyway.
  • Test: the scope test adds twin, a server with mine's exact settings. It runs as its own process, and everything still idles out.

Tagged errors

Every MCP failure is now a Schema.TaggedError (UnavailableError, ToolCallError, ResourceError, ValueError, OutputError; connect attempts use ConnectError), as opencode does. The tool adapter turns error.message into model text only at the edge. The model-facing text is unchanged, and no artifact moved.

Pi's exposure values

mcp.json accepts every exposure Pi does (direct, hidden, codemode, deferred, and codemode-deferred as an alias), so entries copied from Pi stay valid.

  • Until codemode and tool_search exist, codemode / deferred tools aren't declared, and the session is warned once with Pi's wording.
  • Resource tools are declared when a server with resources is direct, and reach every server with resources that isn't hidden.
  • The default stays direct for now, and flips to codemode (Pi's default) with the codemode work.

CLI end-to-end

packages/codework/test/cli.mcp.test.ts spawns the real codework run with mcp.json, a fake OpenAI-compatible provider and a hand-written stdio MCP server. It checks that:

  • the tool is declared with the server's schema
  • the call's result reaches the model
  • the server ran in the session's directory
  • the server is gone after the CLI exits

Artifact: cli.mcp.json.

Test plan

  • test/mcp.session.test.ts gets a new scope test, through the full harness with real stdio servers. Sessions A and B share a space and C is in another, each calling a session, a space and an app server:
    • session: three processes
    • space: A and B share one, C has its own
    • app: one process for all three
    • After the drains end, every instance idles out and its process is gone. A later run starts fresh instances.
    • Artifact: mcp.session.scopes.json, stable across runs.
  • Lifecycle artifact: only the more stable tool order changed.
  • Full harness suite: 525 passed, 34 skipped (live/remote)
  • vp check and vp run typecheck

🤖 Generated with Claude Code

sanchitrk and others added 5 commits October 15, 2026 09:33
- A server's scope decides who shares a running instance: session (the
  default), space (sessions in one space) or app (every session). An
  instance is keyed by its scope and everything its connection is made
  from, so sessions resolving different settings never share one.
- Exposure stays each session's own; sessions sharing an instance can
  declare different tools.
- Each drain leases its session's instances. An instance no session uses
  closes at once; one no drain holds closes after an idle timeout
  (10 minutes, Harness option mcpIdleTimeout).
- space servers start in the space's folder and are sent it as root on a
  local sandbox; app servers start where the app started, with no roots.
- A session's servers keep their config order across exchanges, so a
  restarted server no longer moves its tools to the end.
- Docs: sharing a server, and idle close.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… interrupts

- An instance key includes the server's name, so two entries with the same
  settings are two instances and a renamed server never closes the
  instance its new name uses.
- Leases count each instance once per session, so an alias can no longer
  drive a count negative and keep a server from idling out.
- Reconcile updates every membership and lease count in one step and
  closes unused instances on their own, so an interrupted exchange cannot
  leave a shared instance holding a session it no longer has.
- An instance that starts with no drain holding it still gets an idle
  timer.
- Keys are canonical JSON, so the same env or headers in another order
  share an instance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every MCP failure is a Schema.TaggedError with the message the model
reads, turned into a tool result only at the edge:

- UnavailableError: disabled, shut down, not connected, needs sign-in,
  or a tool no longer offered.
- ToolCallError: a tool call or resource request failed on the server.
- ResourceError: unusable resource-tool arguments, or a server without
  resources.
- ValueError: an env or headers value that does not resolve.
- OutputError: output that could not be saved.

Connect attempts fail with ConnectError. The text the model sees is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codework run reads mcp.json next to the user settings, starts a stdio MCP
server written against the wire protocol, declares its tool to a fake
OpenAI-compatible provider with the server's schema, returns the call's
result to the model, and stops the server when the CLI exits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mcp.json accepts every exposure Pi does: direct, hidden, codemode and
deferred, with codemode-deferred read as codemode, so entries copied
from Pi stay valid. Until codemode and tool_search exist, codemode and
deferred tools are not declared and a session is warned once that they
cannot be reached. Resource tools are declared when a server with
resources is direct, and reach every server with resources that is not
hidden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant