fix+refactor(mcp): lifecycle tests, cancelPending fix, Effect migration#19042
Merged
kitlangton merged 20 commits intodevfrom Mar 26, 2026
Merged
fix+refactor(mcp): lifecycle tests, cancelPending fix, Effect migration#19042kitlangton merged 20 commits intodevfrom
kitlangton merged 20 commits intodevfrom
Conversation
McpOAuthCallback.cancelPending(mcpName) was a no-op because pendingAuths is keyed by oauthState (a hex string), not mcpName. Add a reverse index so removeAuth() can actually cancel in-flight OAuth flows. Add comprehensive MCP lifecycle tests covering connect/disconnect, add/replace, prompts, resources, disabled servers, error paths, and tool name sanitization. Two tests for tools() transient-failure recovery are marked .todo — the current behavior permanently deletes clients on listTools() failure with no reconnect path; to be fixed during Effect migration.
Migrate MCP from Instance.state() to Effect Service/Layer/InstanceState pattern with makeRunPromise and async facades. - InstanceState for per-directory lifecycle with ScopedCache - Effect.fn tracing on all service methods - Extract closeClient helper (was duplicated 3x) - Extract collectFromConnected helper (prompts/resources were identical) - Fix readResource log messages (said "prompt" instead of "resource") - Fix finishAuthImpl not updating state after OAuth reconnect - OAuth impl stays as plain async (inherently imperative)
- Replace descendants() with ChildProcessSpawner + Effect.scoped
- Move descendants inside layer to close over spawner
- Replace Promise.all with Effect.forEach({concurrency: "unbounded"})
in state init, finalizer, and tools()
- Use Effect.tryPromise for closeClient and tools listTools
- Use Effect.try for process.kill in finalizer
- Remove Process import (replaced by ChildProcessSpawner)
- collectFromConnected now returns Effect instead of Promise
- convertMcpTool is sync — drop async, use plain for loop - Extract withClient helper for getPrompt/readResource (tryPromise) - Extract createAndStore for add/connect/finishAuth shared pattern - Break removeAuth into individual Effect steps - tools() inner loop uses plain for (convertMcpTool is sync now)
Replace Filesystem.readJson/writeJson with AppFileSystem Effect service. All auth persistence is now proper Effect with tracing. Async facades preserved for McpOAuthProvider and CLI backward compat.
When tools() encounters a listTools() failure, fork a background reconnection fiber via createAndStore() instead of permanently removing the server. The current turn gets no tools from that server, but the background fiber re-establishes the connection so tools are available on the next turn. Previously, a single transient error (network blip, server restart) would permanently delete the MCP server from the session with no recovery path.
Replace startAuthImpl/authenticateImpl/finishAuthImpl plain async functions with Effect.fn methods inside the layer. OAuth state management now uses yield* auth.* directly instead of async McpAuth.* facades. Eliminates ~8 Effect.promise wrappers. Extract getMcpConfig helper for shared config lookup pattern. finishAuth now calls createAndStore directly (no intermediate status).
- Push Effect.orDie into McpAuth.Service interface (no error channel) eliminates 11 .pipe(Effect.orDie) at call sites - Return oauthState from startAuth, eliminate redundant read-back - Use Option.isNone() instead of ._tag === "None" - Reuse getMcpConfig in connect - Unify fetchPromptsForClient/fetchResourcesForClient into fetchFromClient - Use Effect.callback for browser subprocess detection (was Promise wrapping event listeners inside Effect.tryPromise) - Simplify closeClient to Effect.promise with .catch - Use plain try/catch for process.kill (was Effect.try with ignore)
- Remove redundant auth.clearOAuthState after auth.remove in removeAuth - Remove dead status assignment in create() (assigned then immediately returned a new identical object) - Fix mcpNameToState leak on successful/error OAuth callback completion - Extract mcpConfig lookup once in tools() loop (was looked up twice)
…ation tools() no longer calls listTools() on every server each turn. Instead, tool definitions are fetched once at connect time and cached in state. The ToolListChangedNotification handler refreshes the cache when a server reports its tools have changed. - Add defs to State, populated by create() and createAndStore() - watch() registers per-client notification handler with staleness guards - tools() reads from s.defs (zero network calls on hot path) - closeClient() clears defs to keep cache and client lifecycle in sync - Close old client on ALL failure paths, not just disabled - Tests verify caching (listToolsCalls stays at 1) and notification refresh
opencode-agent bot
added a commit
that referenced
this pull request
Mar 25, 2026
…x, Effect migration
Deduplicate the repetitive get→mutate→set pattern used by updateTokens, updateClientInfo, updateCodeVerifier, updateOAuthState, clearCodeVerifier, and clearOAuthState into two generic helpers.
opencode-agent bot
added a commit
that referenced
this pull request
Mar 25, 2026
…x, Effect migration
opencode-agent bot
added a commit
that referenced
this pull request
Mar 25, 2026
…x, Effect migration
opencode-agent bot
added a commit
that referenced
this pull request
Mar 25, 2026
…x, Effect migration
opencode-agent bot
added a commit
that referenced
this pull request
Mar 25, 2026
…x, Effect migration
opencode-agent bot
added a commit
that referenced
this pull request
Mar 25, 2026
…x, Effect migration
opencode-agent bot
added a commit
that referenced
this pull request
Mar 25, 2026
…x, Effect migration
opencode-agent bot
added a commit
that referenced
this pull request
Mar 25, 2026
…x, Effect migration
opencode-agent bot
added a commit
that referenced
this pull request
Mar 25, 2026
…x, Effect migration
cainiao1992
pushed a commit
to cainiao1992/opencode
that referenced
this pull request
Mar 26, 2026
Kizunad
pushed a commit
to Kizunad/opencode
that referenced
this pull request
Mar 26, 2026
Andres77872
pushed a commit
to Andres77872/opencode
that referenced
this pull request
Mar 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bug fixes:
listTools()failed (network blip, server restart), the client was permanently deleted with no recovery. Now forks a background reconnection fiber — tools come back on the next LLM turn.cancelPendingkey mismatch:McpOAuthCallback.cancelPending(mcpName)was a no-op becausependingAuthsis keyed byoauthState, notmcpName. Added a reverse index soremoveAuth()can actually cancel in-flight OAuth flows.readResourcelog messages: Copy-paste bug — said "prompt" instead of "resource".finishAuthnot updating state: After OAuth reconnect, the new client wasn't being stored in InstanceState.mcpNameToStatememory leak: Reverse index entries weren't cleaned up on successful/error OAuth callback.Effect migration:
Instance.state()to EffectService/Layer/InstanceStatewithmakeRunPromiseand async facadesMcpAuthwithAppFileSystem(no error channel —orDieinside the service)McpAuth.Serviceas a layer dependency (directyield* auth.*instead ofEffect.promise)startAuthImpl/authenticateImpl/finishAuthImpl)ChildProcessSpawnerfordescendants()(replacesProcess.lines)Effect.callbackfor browser subprocess detectionEffect.forEachwith{concurrency: "unbounded"}for init, finalizer, and tool listingCleanup:
closeClient,createAndStore,getMcpConfig,withClient,collectFromConnected,fetchFromClienthelpersconvertMcpToolwas async for no reason — now syncfetchPromptsForClient/fetchResourcesForClientTests:
cancelPendingfix verification)Test plan