Skip to content

fix(core): wait for MCP servers only before a session's first request - #58374

Open
JoshuaMart wants to merge 5 commits into
anomalyco:devfrom
JoshuaMart:mcp-codemode-false
Open

JoshuaMart wants to merge 5 commits into
anomalyco:devfrom
JoshuaMart:mcp-codemode-false

Conversation

@JoshuaMart

@JoshuaMart JoshuaMart commented Oct 23, 2026 •

Copy link
Copy Markdown

Issue for this PR

Fixes #51812

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

MCP servers connect in the background, and SessionContext.select only waited for the initial tool registration, which sees only the servers already connected. So a session started while a server is still starting sends its first request without that server's tools. With Code Mode, later steps catch up. With codemode: false the tools are just missing, and an agent meant to call <server>_<tool> directly usually stops.

Startup stays non-blocking. Only a session's first model request (no assistant message yet) calls the new McpTool.ready:

  • It waits for servers still pending, each for at most its new timeout.wait. The default is 3 s, and it can be set globally under mcp.timeout or per server.
  • Then it compares the registry with the tools of every connected server, and refreshes it if they differ. This also covers a server that is already connected but whose debounced ToolsChanged refresh hasn't run yet (the ~100 ms gap reported on First prompt after start has no MCP tools: tool list does not wait for pending MCP connections #51812).
  • After the cap it goes on with whatever is registered.

Later steps keep using flush and never wait. A server that connects or fails earlier ends the wait early, so the 3 s cap only applies to a server that is still starting.

How did you verify your code works?

  • packages/core/test/mcp.test.ts:
    • a starting server is waited for, and its tool is listed directly and not in the Code Mode catalogue;
    • flush doesn't wait;
    • a connected server missing from the registry gets registered;
    • a hung server costs only its wait.
  • packages/core/test/session-generate.test.ts: the first select uses ready, and flush once an assistant message exists. All new tests pass with --rerun-each 10.
  • bun run script/test.ts in packages/core passes, except 7 shell tests that also fail on unmodified dev on my machine. bun run check passes.
  • End to end with a built binary, run --standalone, a stdio server that delays initialize, codemode: false, and a fake OpenAI-compatible provider that logs the tools it receives. Times are from CLI launch to the first request.
    • 2 s server: the first request has demo_echo, and the model calls it without execute. 2.5 s.
    • 20 s server: the first request has no demo_echo and leaves at 3.4 s, compared with 0.6 s before this change; it is 0.95 s with "wait": 500.
    • Follow-up steps don't wait.

Screenshots / recordings

N/A

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Fixes #51812

Related: #51089

Public API: adds optional MCP timeout.wait config key (default 3000 ms; backwards compatible)

MCP servers connect asynchronously, and McpTool.flush only waited for the
initial registration, which reads the servers already connected. A session
step made while a server was still starting went out without that server's
tools; they arrived only with the debounced refresh, on a later step.

With `codemode: false` this hides the tools entirely on the first step, so
an agent that relies on them without `execute` sees nothing to call.

Mcp.settle waits for servers still pending, and flush refreshes the tool
registry when it had to wait. Startup stays bounded by each server's
startup timeout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The fix works. I reproduced the bug with a stdio MCP server that waits 10s before answering initialize (codemode: false), submitting a prompt right away through the TUI with a simulated model. On dev, the first model request went out after ~0.2s without demo_echo. With this PR, it waited ~8.8s and included demo_echo. The new test fails on dev, passes with the PR and was stable over 10 reruns. The rest of test/mcp.test.ts passes except one MCP-descendants test, which fails on dev too. tsc -b tsconfig.tests.json in packages/core passes.

The public API is unchanged: Mcp.Interface.settle is internal to core.

A maintainer should make a design call here. On #51812 a team member said that not blocking on starting servers was intentional. With this change, every step waits silently for any server still in pending, for up to its startup timeout (30s by default). This applies to all servers, not only codemode: false ones, and the TUI shows nothing while it waits. #51812 also suggested limiting the wait to the first request and capping it at a few seconds. One more gap is noted inline.

Fixes #51812
Related: #51089

Comment thread packages/core/src/tool/mcp.ts Resolved
@JoshuaMart JoshuaMart closed this Oct 23, 2026
@JoshuaMart JoshuaMart reopened this Oct 23, 2026
Address review: the wait no longer runs on every step.

- Only a session's first model request (no assistant message yet) waits,
  through the new McpTool.ready. Later steps keep McpTool.flush and never
  wait.
- Each starting server is waited for at most its `timeout.wait`, which
  defaults to 3 s and can be set under `mcp.timeout` or per server.
- After the wait, the registry is compared with the tools of every
  connected server and refreshed when they differ. This also covers a
  server that is connected but whose debounced ToolsChanged refresh has
  not run yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new commit addresses both earlier points. Only a session's first request now waits, capped by timeout.wait (3s by default). ready now compares the connected tools with the registry instead of relying on server status, so a server that has just connected isn't missed.

I re-ran the end-to-end check through the TUI with a simulated model and a slow stdio server (codemode: false):

  • 2s server: dev sends the first request without demo_echo; this PR waits about 0.7s and includes it.
  • 10s server, default wait: the first request goes out after about 3.2s without it, as intended.
  • 10s server with wait: 15000: the first request includes it.

test/mcp.test.ts and test/session-generate.test.ts pass, except the MCP-descendants test, which also fails on dev. tsc -b tsconfig.tests.json in packages/core passes.

Public API: adds an optional timeout.wait (milliseconds, default 3000) under mcp.timeout and mcp.servers.<name>.timeout, with the generated client types and docs updated. It's backwards compatible.

No new concerns.

@JoshuaMart JoshuaMart changed the title fix(core): wait for starting MCP servers before the first request fix(core): wait for MCP servers only before a session's first request Oct 23, 2026

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new commit only merges upstream dev. The change itself is the same as the version I reviewed last time, apart from the regenerated packages/protocol/openapi.json, which now lists the optional MCP timeout.wait key.

I re-ran the end-to-end check through the TUI with a simulated model:

  • A server that takes 2s to start gets its tool into the first request.
  • A server that takes 10s is skipped after the default 3s wait.

test/mcp.test.ts and test/session-generate.test.ts pass, except the MCP-descendants test, which also fails on dev. tsc -b tsconfig.tests.json in packages/core passes.

Public API: adds an optional timeout.wait (milliseconds, default 3000) under mcp.timeout and mcp.servers.<name>.timeout, with the OpenAPI spec, generated client types and docs updated. It's backwards compatible.

No new concerns.

@aguilaair

Copy link
Copy Markdown

A case this leaves out: sessions that already have assistant messages.

We run opencode serve (2.0.30) in a sandbox and resume existing sessions in it. We hit the ~100 ms gap described on #51812 in two places, and both happen after the session's first model request, so McpTool.ready doesn't run for them:

  1. Resuming a session in a fresh process. We restart opencode serve on an existing session and send the next prompt. GET /api/mcp shows connected, but the first step is built before the debounced ToolsChanged reload, so the agent gets No tool named "terrascope_logic_get" is currently available and a retry one turn later works.
  2. Replacing a server mid-session with PUT /api/experimental/mcp/:server (we rotate the bearer token in its headers). The PUT returns once the server reconnects, and the next prompt has the same gap. That's mcp.add resolves befor the session tool registry has the new server's tools #51089.

For now we work around it by waiting 1 s before the prompt whenever we saw the server connect, or just replaced it. It would help if the registry check in ready (refresh when the registry doesn't match the connected servers' tools) also ran on later requests, or at least after the server set or a server's status changes. A prompt to a server that has been connected for a while would still not need to wait.

Address review: two cases still missed connected tools.

- A session resumed in a fresh process already has assistant messages, so
  it skipped the startup wait. The wait now runs on a session's first
  request in this Location instead.
- After a server connects or is replaced (PUT /api/experimental/mcp), the
  next request could still run before the debounced ToolsChanged reload.
  Every request now compares the registry with the connected servers'
  tools and reloads when they differ. It never waits for servers that are
  still starting.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@JoshuaMart

Copy link
Copy Markdown
Author

Thanks @aguilaair both cases were fixed in c63c0da:

Resuming in a fresh process: the startup wait now runs on a session’s first request in the process, not only on its very first model request. A resumed session waits for servers still starting (within timeout.wait) on its first prompt after a restart.

Later requests and replaced servers: every request now compares the tool registry with the connected servers’ tools and reloads it when they differ, without waiting. That closes the ~100 ms gap after a server connects and after PUT /api/experimental/mcp/:server. A server that has been connected for a while costs nothing.

Checked end to end with opencode serve: after restarting it, run -c on an existing session gets the MCP tool on its first request, and so does a prompt right after replacing the server with the PUT. Before this change, both were missing it. Your 1 s workaround shouldn’t be needed with this.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new commit looks sound. The startup wait now applies to a session's first request in this process, so resumed sessions get it too. Every later request picks up tools from servers that have connected since, without waiting for ones still starting.

I re-ran the end-to-end check through the TUI with a simulated model on the PR:

  • A stdio server that takes 2s to start gets demo_echo into the first request.
  • A server that takes 10s is skipped after the default 3s wait.

I didn't re-run dev this time; it sent the first request without demo_echo in my earlier runs. I didn't test the resumed-session and mid-session paths end to end; the new unit tests cover them.

test/mcp.test.ts and test/session-generate.test.ts pass, except the MCP-descendants test, which also failed on dev earlier. tsc -b tsconfig.tests.json in packages/core passes.

Public API: unchanged from before. It adds an optional timeout.wait (milliseconds, default 3000) under mcp.timeout and mcp.servers.<name>.timeout, with the OpenAPI spec, generated client types and docs updated. It's backwards compatible.

No blocking concerns. McpTool.flush is no longer called outside tests and could be removed.

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.

First prompt after start has no MCP tools: tool list does not wait for pending MCP connections

2 participants