7.0 KiB
MCP Client Session Opener Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Create the clean source-provider MCP session opener in wf_sources_mcp and make one-shot adapter + persistent runtime borrow it, without moving runtime files yet.
Architecture: The previous slice introduced McpSourceConnection. This slice makes that seam useful by centralizing transport opening, auth injection, and ClientSession.initialize() in wf_sources_mcp.client. Existing wf_mcp.sdk.adapter.McpSdkAdapter and wf_mcp.runtime.factory.PersistentSessionFactory should call the shared opener instead of duplicating stdio/http setup.
Tech Stack: Python 3.14, MCP Python SDK, httpx, AnyIO/MCP async context managers, pytest, ruff, basedpyright.
Design Intent
Do not make a prettier copy of src/wf_mcp/runtime/factory.py. Keep the good ideas and isolate them:
- keep the actor/owner-task pattern for persistent sessions
- keep runtime fingerprinting behavior
- centralize transport opening and auth
- keep
wf_mcp.runtime.*in place for now - make future movement to
wf_sources_mcp.runtimemechanical
The desired flow:
ConnectionConfig
-> mcp_source_connection_from_connection_config()
-> open_mcp_session(McpSourceConnection, AuthRecord | None)
-> ClientSession
McpSdkAdapter: opens per operation
PersistentSessionFactory: opens once inside owner task
Important Constraint
StdioSourceTransport.cwd exists because old persistent runtime supports metadata["cwd"]. The shared opener must preserve that field. Otherwise stdio servers that need a working directory regress.
Task 1: Lock Down cwd Propagation
Files:
-
Modify:
src/wf_sources_mcp/transports.py -
Modify:
src/wf_sources_mcp/connections.py -
Test:
tests/wf_sources_mcp/test_connections.py -
Confirm
StdioSourceTransportexposescwd: str | None = None. -
Confirm legacy
ConnectionConfigconversion carriesmetadata["cwd"]intoStdioSourceTransport.cwd. -
Confirm tests assert cwd round-trips from legacy connection metadata.
-
Run:
uv run pytest tests/wf_sources_mcp/test_connections.py tests/wf_sources_mcp/test_source_registry.py -q
uv run basedpyright --level error src/wf_sources_mcp
Expected: pass.
Task 2: Create wf_sources_mcp.client.transport
Files:
- Create:
src/wf_sources_mcp/client/__init__.py - Create:
src/wf_sources_mcp/client/transport.py - Test:
tests/wf_sources_mcp/test_client_transport.py
Implement:
@asynccontextmanager
async def open_mcp_session(
connection: McpSourceConnection,
auth: AuthRecord | None,
) -> AsyncIterator[ClientSession]:
...
Behavior:
-
for
StdioSourceTransport:- merge
transport.envwithmcp_auth_env(auth), auth wins on duplicate keys - pass
command,args,env, andcwdtoStdioServerParameters - enter
stdio_client - enter
ClientSession - call
await session.initialize() - yield initialized session
- merge
-
for
HttpSourceTransport:- create
httpx.AsyncClient(headers=mcp_auth_headers(auth) or None) - enter
streamable_http_client(str(transport.url), http_client=http_client) - enter
ClientSession - call
await session.initialize() - yield initialized session
- create
-
unsupported transport:
- raise
ValueError(f"unsupported MCP transport {transport.kind!r}")
- raise
Testing guidance:
- Use monkeypatch/fakes for
stdio_client,streamable_http_client, andClientSession. - Do not start real subprocesses.
- Assert stdio env merge and cwd propagation.
- Assert HTTP headers propagation.
- Assert initialize is called before yielding.
Verification:
uv run pytest tests/wf_sources_mcp/test_client_transport.py -q
uv run ruff check src/wf_sources_mcp tests/wf_sources_mcp
uv run basedpyright --level error src/wf_sources_mcp
Task 3: Make McpSdkAdapter Use The Shared Opener
Files:
- Modify:
src/wf_mcp/sdk/adapter.py - Test:
tests/wf_mcp/test_sdk_adapter.py
Replace the private _session() transport-opening logic with:
from wf_sources_mcp.client import open_mcp_session
Then each operation should do:
async with open_mcp_session(connection, auth) as session:
...
Do not move McpSdkAdapter yet. This slice only removes duplicated opening logic.
Verification:
uv run pytest tests/wf_mcp/test_sdk_adapter.py tests/wf_sources_mcp/test_client_transport.py -q
uv run basedpyright --level error src/wf_mcp/sdk src/wf_sources_mcp
Task 4: Make PersistentSessionFactory Use The Shared Opener
Files:
- Modify:
src/wf_mcp/runtime/factory.py - Test:
tests/wf_mcp/test_stateful_runtime.py
The factory still receives legacy ConnectionConfig. Convert inside _create_with_stack():
source_connection = mcp_source_connection_from_connection_config(connection)
Then use the shared opener while preserving AsyncExitStack ownership:
session = await stack.enter_async_context(open_mcp_session(source_connection, auth))
return session
Do not remove _SessionOwner. The owner-task pattern is the important fix for AnyIO/MCP cancel-scope ownership.
Verification:
uv run pytest tests/wf_mcp/test_stateful_runtime.py -q
uv run basedpyright --level error src/wf_mcp/runtime src/wf_sources_mcp
Task 5: Verify Boundary And Update Docs
Files:
- Modify:
docs/current_roadmap.md - Modify:
docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md
Docs should say:
- shared MCP session opener exists in
wf_sources_mcp.client - one-shot adapter and persistent runtime both use it
- runtime files are still in
wf_mcpfor compatibility - next slice can move
PersistentSessionFactory,PersistentMcpSession, andMcpRuntimePooltowf_sources_mcp.runtime
Final verification:
uv run pytest tests/wf_sources_mcp tests/wf_mcp/test_sdk_adapter.py tests/wf_mcp/test_stateful_runtime.py tests/wf_mcp/service/test_upstream_transport.py -q
uv run ruff check src tests
uv run basedpyright --level error src
git diff --check
Expected:
- focused tests pass
- ruff passes
- basedpyright has 0 errors
- no whitespace errors
Non-Goals
- Do not move runtime files yet.
- Do not move
McpSdkAdapteryet. - Do not introduce WebSocket/SSE support.
- Do not implement reconnect/backoff policy.
- Do not broaden persistent runtime beyond existing
call_toolbehavior in this slice. - Do not change proxy/frontend MCP code.
Future Slice
After this plan:
- Move
PersistentSessionFactory,PersistentMcpSession, andMcpRuntimePoolintowf_sources_mcp.runtime. - Keep
wf_mcp.runtime.*shims. - Then move
McpSdkAdapterintowf_sources_mcp.sdk.adapter. - Only after those moves, consider a broader
McpClientSessionabstraction for persistentread_resource,get_prompt,invoke_method, andsend_notification.