Repository navigation
Breaking: keep Strands TypeScript MCP connections per StrandsPlugin - #371
Merged
Merged
Conversation
The MCP connection cache was one module-level map keyed by server name, so in a process with several Workers the first plugin to connect served every plugin that registered that name, with its server and credentials, and any Worker that stopped closed the connection under the others' calls. Each StrandsPlugin now keeps its own connections. Its runContext counts the Workers using the plugin, and the last one to stop closes them; a connection still in use by a call is closed when that call finishes.
campoy
marked this pull request as ready for review
November 2, 2026 00:43
brianstrauch
approved these changes
Nov 2, 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.
What was changed
@temporalio/strands-agentskeeps its MCP connections in eachStrandsPlugininstance instead of one module-level map keyed by server name. Before, in a process with several Workers, the first plugin to connect to a server name served every Worker that registered that name, with its own server and credentials, and any Worker that stopped closed the connection under the others' calls.temporal-mcp-client.ts: a newMcpConnectionsclass holds one plugin's connections by server name. Itsuse()runs one Worker and counts the Workers using the plugin; the last one to stop closes the connections. A connection that a call is still using when it is evicted is closed when that call finishes, like one retired after a failed call.buildListToolsActivityandbuildCallToolActivitytake the plugin'sMcpConnectionsinstead of the idle timeout, which moves into it._evictConnectionand the module-level map are gone. None of these are exported from the package index.plugin.ts: eachStrandsPluginbuilds its ownMcpConnections, passes it to its MCP Activities, and itsrunContextisconnections.use(next).This matches the Python plugin after #190, whose
temporalio-mcppool belongs to eachStrandsPluginand closes when the last Worker or Replayer using it ends.Breaking change (callers): two Workers in one process with separate
StrandsPlugininstances that map one server name to the same stateful MCP server share one connection and session today, so a run on one Worker can read session values a run on the other wrote. After this change each instance opens its own connection and session. Before upgrading, give such Workers the sameStrandsPlugininstance (for example on theNativeConnection). Code that importedbuildListToolsActivity,buildCallToolActivityor_evictConnectionfromlib/temporal-mcp-clientmust change; they are not exported from the package index.Also: an application that builds a new
StrandsPluginfrom the same options for each Worker now opens one connection per Worker for each server (for a stdio server, one process each) instead of one per process. A call that is still running after a forced Worker shutdown keeps its connection until the call ends; before, the Worker's stop closed it.Replay is not affected: Workflow code, Activity names, inputs and results are unchanged, and the change adds no events or commands to the history. Upgrade and rollback need no extra steps beyond the caller change above.
Why?
Closes #370. Two Workers in one process with their own
StrandsPluginsent MCP calls to another Worker's server or with its credentials, with no error, and stopping one Worker failed another Worker's in-flight MCP call, which then ran the tool twice.Checklist
Closes [Bug] strands-agents: MCP connections are shared across StrandsPlugin instances and Workers in one process #370
How was this tested:
Supplemental local runs in Docker (Node 24,
@temporalio/*1.24.0,@strands-agents/sdk1.19.0,@modelcontextprotocol/sdk1.32.0 from the lock, Temporal CLI 1.9.1), onmainat88745dc:make lint,make test(90 tests passed),make buildwithscripts/ci/check_npm_package.py, andscripts/ci/check_conventions.pypass. Prettier with the SDK's settings reports no changes in the touched files.main(onlytest-strands.tschanged) on their intended assertions: two plugins' calls are handled by['A', 'A']instead of['A', 'B'], and Worker A's stop disconnects the shared plugin's connection (['S']) while Worker B's call is in flight. Both pass with the change. The new eviction unit test uses the new class, so it cannot run onmain.server Afor plugin B's call andA disconnected during a callonmain; with the change each plugin reaches its own server and B's connection stays open.main(another Worker's credentials in both start orders, a held call while the other Worker stops, with separate plugins and with one shared plugin, a retry on the surviving Worker, and both Workers shut down together with a 6-second grace time) now show each Worker using its own server and key, and the call finishing on its first attempt with one tool run.Any docs updates needed?
The plugin README's MCP section. Release notes should name the caller change above.
Not covered:
main,temporalio-strands-agentsstill shares its_CONNECTIONScache across plugin instances, and on Breaking: run the Strands MCP activities through temporalio-mcp #190 it does not (the runs are summarized in [Bug] strands-agents: MCP connections are shared across StrandsPlugin instances and Workers in one process #370).StrandsPlugininstance on both the Worker and theNativeConnection; that case runs the plugin'srunContexttwice for one Worker, and the count still reaches zero only when that Worker stops.Open questions: none.