19 KiB
Content Access Stateful Runtime Routing Implementation Plan
Historical: This plan has been implemented. The
StatefulMcpRuntimeprotocol is inwf_sources_mcp.sdk, upstream transport prefers it for content reads, andWfMcpServicewires the configured runtime pool as bothtool_executorandstateful_runtime.
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: Route broker content reads (read_resource, render_prompt) through a stateful MCP runtime when one is configured, while preserving one-shot adapter fallback.
Architecture: Config-built services already pass McpRuntimePool as tool_executor; that runtime now supports call_tool, read_resource, and get_prompt. This slice makes the policy explicit with a StatefulMcpRuntime protocol instead of ad-hoc hasattr or string dispatch. Discovery/catalog refresh remains one-shot adapter based; content access prefers stateful runtime and falls back to the adapter only when no stateful runtime is configured.
Tech Stack: Python 3.14, MCP Python SDK, pytest/pytest-asyncio, ruff, basedpyright.
File Structure
- Modify
src/wf_sources_mcp/sdk/protocols.py- Add
StatefulMcpRuntimeprotocol withcall_tool,read_resource,get_prompt.
- Add
- Modify
src/wf_sources_mcp/sdk/__init__.py- Export
StatefulMcpRuntime.
- Export
- Modify
src/wf_mcp/sdk/base.py- Re-export
StatefulMcpRuntimethrough compatibility shim.
- Re-export
- Modify
src/wf_mcp/sdk/__init__.py- Re-export
StatefulMcpRuntimethrough old package.
- Re-export
- Modify
src/wf_mcp/broker/service/upstream_transport.py- Add
stateful_runtime: StatefulMcpRuntime | None. - Keep
tool_executorcompatibility but derive stateful runtime from it when possible. - Prefer stateful runtime in
read_resourceandrender_prompt. - Keep adapter fallback.
- Add
- Modify
src/wf_mcp/broker/service/core.py- Pass the configured runtime pool as both
tool_executorandstateful_runtimewhere available.
- Pass the configured runtime pool as both
- Modify
src/wf_mcp/broker/config.py- If construction currently passes only
tool_executor=McpRuntimePool(...), update service construction if needed.
- If construction currently passes only
- Add/modify tests:
tests/wf_sources_mcp/test_sdk_protocols.pytests/wf_mcp/test_compat_imports.pytests/wf_mcp/service/test_upstream_transport.pytests/wf_mcp/service/test_content_access.py
Hard Boundaries
- Do not route catalog refresh/discovery listing through stateful runtime in this slice.
- Do not use
getattr(runtime, "read_resource")dispatch. - Do not remove adapter fallback.
- Do not add runtime raw method or notification support.
- Do not assume
list_resources/list_promptsare stateless.
Task 1: Add StatefulMcpRuntime Protocol
Files:
-
Modify:
src/wf_sources_mcp/sdk/protocols.py -
Modify:
src/wf_sources_mcp/sdk/__init__.py -
Modify:
tests/wf_sources_mcp/test_sdk_protocols.py -
Step 1: Add protocol test
Append to tests/wf_sources_mcp/test_sdk_protocols.py:
def test_stateful_mcp_runtime_protocol_shape() -> None:
from wf_sources_mcp.sdk import StatefulMcpRuntime, ToolExecutor
assert StatefulMcpRuntime.__name__ == "StatefulMcpRuntime"
assert ToolExecutor.__name__ == "ToolExecutor"
- Step 2: Add the protocol
In src/wf_sources_mcp/sdk/protocols.py, add after ToolExecutor:
class StatefulMcpRuntime(ToolExecutor, Protocol):
"""Stateful execution/read boundary for configured MCP sources.
Implementations keep source session state across calls. Discovery/catalog
refresh may still use one-shot adapters by policy.
"""
async def read_resource(
self,
connection: McpSourceConnection,
auth: AuthRecord | None,
uri: str,
) -> dict[str, Any]: ...
async def get_prompt(
self,
connection: McpSourceConnection,
auth: AuthRecord | None,
prompt_name: str,
arguments: dict[str, str] | None = None,
) -> dict[str, Any]: ...
Update __all__:
__all__ = [
"BackendAdapter",
"StatefulMcpRuntime",
"ToolCallResult",
"ToolExecutor",
]
- Step 3: Export protocol from package root
In src/wf_sources_mcp/sdk/__init__.py, import/export StatefulMcpRuntime:
from .protocols import BackendAdapter, StatefulMcpRuntime, ToolCallResult, ToolExecutor
and include "StatefulMcpRuntime" in __all__.
- Step 4: Run protocol tests
Run:
uv run pytest tests/wf_sources_mcp/test_sdk_protocols.py -q
Expected: pass.
Task 2: Preserve Compatibility Exports
Files:
-
Modify:
src/wf_mcp/sdk/base.py -
Modify:
src/wf_mcp/sdk/__init__.py -
Modify:
tests/wf_mcp/test_compat_imports.py -
Step 1: Update compatibility shims
In src/wf_mcp/sdk/base.py, re-export StatefulMcpRuntime with the existing protocol exports:
from wf_sources_mcp.sdk import BackendAdapter, StatefulMcpRuntime, ToolCallResult
__all__ = ["BackendAdapter", "StatefulMcpRuntime", "ToolCallResult"]
In src/wf_mcp/sdk/__init__.py, update imports and __all__:
from wf_sources_mcp.sdk import (
BackendAdapter,
McpSdkAdapter,
StatefulMcpRuntime,
ToolCallResult,
)
__all__ = ["BackendAdapter", "McpSdkAdapter", "StatefulMcpRuntime", "ToolCallResult"]
- Step 2: Extend compatibility test
In tests/wf_mcp/test_compat_imports.py, extend test_wf_mcp_sdk_protocol_shims_reexport_wf_sources_mcp_sdk:
from wf_mcp.sdk import StatefulMcpRuntime as CompatStatefulMcpRuntime
from wf_mcp.sdk.base import StatefulMcpRuntime as CompatBaseStatefulMcpRuntime
from wf_sources_mcp.sdk import StatefulMcpRuntime
assert CompatStatefulMcpRuntime is StatefulMcpRuntime
assert CompatBaseStatefulMcpRuntime is StatefulMcpRuntime
- Step 3: Run compatibility test
Run:
uv run pytest tests/wf_mcp/test_compat_imports.py -q
Expected: pass.
Task 3: Add Stateful Runtime Routing to Upstream Transport
Files:
-
Modify:
src/wf_mcp/broker/service/upstream_transport.py -
Modify:
tests/wf_mcp/service/test_upstream_transport.py -
Step 1: Add fake stateful runtime and adapter tests
Append to tests/wf_mcp/service/test_upstream_transport.py:
class _StatefulRuntime:
def __init__(self) -> None:
self.resources: list[tuple[str, str]] = []
self.prompts: list[tuple[str, str, dict[str, str] | None]] = []
async def call_tool(self, connection, auth, tool_name, payload):
raise AssertionError("not used by these tests")
async def read_resource(self, connection, auth, uri: str):
self.resources.append((connection.id, uri))
return {"contents": [{"uri": uri, "text": "stateful resource"}]}
async def get_prompt(
self,
connection,
auth,
prompt_name: str,
arguments: dict[str, str] | None = None,
):
self.prompts.append((connection.id, prompt_name, arguments))
return {
"messages": [
{
"role": "user",
"content": {"type": "text", "text": "stateful prompt"},
}
]
}
class _ExplodingContentAdapter(FakeAdapter):
async def read_resource(self, connection, auth, uri):
raise AssertionError("adapter read_resource should not be used")
async def get_prompt(self, connection, auth, prompt_name, arguments=None):
raise AssertionError("adapter get_prompt should not be used")
Add tests:
async def test_upstream_transport_prefers_stateful_runtime_for_resource_reads(
tmp_path: Path,
) -> None:
events: list[McpEvent] = []
runtime = _StatefulRuntime()
transport = UpstreamTransportService(
auth_store=FileStore(tmp_path),
catalog_store=FileStore(tmp_path),
event_sink=events.append,
stateful_runtime=runtime,
)
transport.register_adapter("demo", _ExplodingContentAdapter())
connection = ConnectionConfig(
id="demo.personal",
server="demo",
account="personal",
metadata=_fake_transport_metadata(),
)
result = await transport.read_resource(
connection,
"demo.personal.resource.welcome",
"fixture://docs/welcome",
)
assert result["contents"][0]["text"] == "stateful resource"
assert runtime.resources == [("demo.personal", "fixture://docs/welcome")]
assert [event.kind for event in events] == [
"resource_read_started",
"resource_read_completed",
]
async def test_upstream_transport_prefers_stateful_runtime_for_prompts(
tmp_path: Path,
) -> None:
events: list[McpEvent] = []
runtime = _StatefulRuntime()
transport = UpstreamTransportService(
auth_store=FileStore(tmp_path),
catalog_store=FileStore(tmp_path),
event_sink=events.append,
stateful_runtime=runtime,
)
transport.register_adapter("demo", _ExplodingContentAdapter())
connection = ConnectionConfig(
id="demo.personal",
server="demo",
account="personal",
metadata=_fake_transport_metadata(),
)
result = await transport.render_prompt(
connection,
"demo.personal.prompt.summarize",
"prompt.summarize",
{"text": "hello"},
)
assert result["messages"][0]["content"]["text"] == "stateful prompt"
assert runtime.prompts == [
("demo.personal", "prompt.summarize", {"text": "hello"})
]
assert [event.kind for event in events] == [
"prompt_get_started",
"prompt_get_completed",
]
- Step 2: Add
stateful_runtimefield and import protocol
In src/wf_mcp/broker/service/upstream_transport.py, change import:
from wf_sources_mcp.sdk import BackendAdapter, StatefulMcpRuntime, ToolExecutor
Add field to UpstreamTransportService:
stateful_runtime: StatefulMcpRuntime | None = None
- Step 3: Prefer stateful runtime for resource reads
In read_resource, replace:
adapter = require_adapter(connection, self.adapters)
auth = self.load_connection_auth(connection)
# Compatibility boundary...
source_connection = mcp_source_connection_from_connection_config(connection)
with:
auth = self.load_connection_auth(connection)
# Compatibility boundary: broker callers still pass ConnectionConfig.
source_connection = mcp_source_connection_from_connection_config(connection)
Then replace the execution line:
result = await adapter.read_resource(source_connection, auth, uri)
with:
if self.stateful_runtime is not None:
result = await self.stateful_runtime.read_resource(
source_connection,
auth,
uri,
)
else:
adapter = require_adapter(connection, self.adapters)
result = await adapter.read_resource(source_connection, auth, uri)
- Step 4: Prefer stateful runtime for prompt gets
In render_prompt, remove early adapter lookup and replace execution:
if self.stateful_runtime is not None:
result = await self.stateful_runtime.get_prompt(
source_connection,
auth,
local_name,
arguments,
)
else:
adapter = require_adapter(connection, self.adapters)
result = await adapter.get_prompt(source_connection, auth, local_name, arguments)
Keep existing start/completed events unchanged.
- Step 5: Run upstream transport tests
Run:
uv run pytest tests/wf_mcp/service/test_upstream_transport.py -q
Expected: pass.
Task 4: Wire Configured Runtime as Stateful Runtime
Files:
-
Modify:
src/wf_mcp/broker/service/core.py -
Modify:
src/wf_mcp/broker/config.pyif needed. -
Modify:
tests/wf_mcp/service/test_content_access.py -
Step 1: Wire service field
In src/wf_mcp/broker/service/core.py, when constructing UpstreamTransportService, pass:
stateful_runtime=self.tool_executor
if isinstance(self.tool_executor, object)
else None,
Do not use that exact isinstance(..., object) if basedpyright dislikes it. Prefer a simple assignment after adding a stateful_runtime field to WfMcpService if needed:
stateful_runtime: StatefulMcpRuntime | None = None
Then:
stateful_runtime = self.stateful_runtime
self.upstream = UpstreamTransportService(
auth_store=auth_store,
catalog_store=catalog_store,
event_sink=self.events.record_event,
tool_executor=self.tool_executor,
stateful_runtime=stateful_runtime,
)
For config-built services that construct McpRuntimePool, set both:
runtime_pool = McpRuntimePool(runtime_factory.create)
service = WfMcpService(
...,
tool_executor=runtime_pool,
stateful_runtime=runtime_pool,
)
If current build_service_from_config already constructs the pool inline, refactor to a local runtime_pool variable.
- Step 2: Add content access service test with stateful runtime
Append to tests/wf_mcp/service/test_content_access.py:
class _StatefulRuntime:
def __init__(self) -> None:
self.resources: list[str] = []
self.prompts: list[str] = []
async def call_tool(self, connection, auth, tool_name, payload):
raise AssertionError("not used")
async def read_resource(self, connection, auth, uri: str):
self.resources.append(uri)
return {"contents": [{"uri": uri, "text": "stateful resource"}]}
async def get_prompt(self, connection, auth, prompt_name, arguments=None):
self.prompts.append(prompt_name)
return {
"messages": [
{
"role": "user",
"content": {"type": "text", "text": "stateful prompt"},
}
]
}
async def test_content_access_uses_stateful_runtime_for_upstream_content() -> None:
runtime = _StatefulRuntime()
service = WfMcpService(
store=FileStore(local_temp_root() / "content_stateful_runtime"),
tool_executor=runtime,
stateful_runtime=runtime,
)
service.register_connection(
ConnectionConfig(
id="demo.personal",
server="demo",
account="personal",
metadata={"transport": "stdio", "command": "fake-mcp-server"},
)
)
service.register_adapter("demo", FakeAdapter())
await service.refresh_connection_catalog("demo.personal")
resource = await service.content_access.read_resource(
"demo.personal.resource.welcome"
)
prompt = await service.content_access.render_prompt(
"demo.personal.prompt.summarize",
arguments={"text": "hello"},
)
assert resource["contents"][0]["text"] == "stateful resource"
assert prompt["messages"][0]["content"]["text"] == "stateful prompt"
assert runtime.resources == ["fixture://resource/welcome"]
assert runtime.prompts == ["prompt.summarize"]
Adjust expected URI if FakeAdapter catalog fixture uses a different resource URI; read it from the failure and keep the exact catalog URI.
- Step 3: Run content tests
Run:
uv run pytest tests/wf_mcp/service/test_content_access.py tests/wf_mcp/service/test_upstream_transport.py -q
Expected: pass.
Task 5: Docs and Plan Archive
Files:
-
Modify:
docs/current_roadmap.md -
Modify:
docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md -
Move after completion:
docs/superpowers/plans/2026-06-07-content-access-stateful-runtime-routing.mdtodocs/historical/superpowers/plans/2026-06-07-content-access-stateful-runtime-routing.md -
Step 1: Update roadmap
In docs/current_roadmap.md, under the MCP upstream source runtime cleanup bullets, add:
- Completed: broker content access now prefers a configured stateful MCP
runtime for `read_resource` and `get_prompt`, with one-shot adapter fallback.
Catalog refresh/discovery remains one-shot by policy.
- Step 2: Update long-lived API boundary spec
In docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md, add after the prompt runtime item:
13. Complete: broker content access now prefers configured `StatefulMcpRuntime`
for resource and prompt reads, falling back to the one-shot adapter when no
stateful runtime is configured. Catalog refresh/discovery remains one-shot.
Renumber following item if needed.
- Step 3: Archive completed plan after implementation
Run only after all code/tests pass:
git mv docs/superpowers/plans/2026-06-07-content-access-stateful-runtime-routing.md docs/historical/superpowers/plans/2026-06-07-content-access-stateful-runtime-routing.md
Task 6: Final Verification
Files:
-
All changed files.
-
Step 1: Run focused tests
Run:
uv run pytest tests/wf_sources_mcp tests/wf_mcp/service/test_upstream_transport.py tests/wf_mcp/service/test_content_access.py tests/wf_mcp/test_compat_imports.py -q
Expected: pass.
- Step 2: Run source typecheck
Run:
uv run basedpyright --level error src
Expected: 0 errors, 0 warnings, 0 notes.
- Step 3: Run focused test typecheck
Run:
uv run basedpyright --level error tests/wf_mcp/service/test_upstream_transport.py tests/wf_mcp/service/test_content_access.py tests/wf_sources_mcp/test_sdk_protocols.py
Expected: 0 errors, 0 warnings, 0 notes.
- Step 4: Run focused lint
Run:
uv run ruff check src/wf_sources_mcp/sdk src/wf_mcp/sdk src/wf_mcp/broker/service/upstream_transport.py src/wf_mcp/broker/service/core.py tests/wf_mcp/service/test_upstream_transport.py tests/wf_mcp/service/test_content_access.py
Expected: All checks passed!
- Step 5: Check no discovery routing changed
Run:
rg -n "stateful_runtime\\.(list_tools|list_resources|list_prompts)" src
Expected: no matches.
- Step 6: Check whitespace
Run:
git diff --check
Expected: no whitespace errors. CRLF warnings are acceptable on Windows.
Self-Review
- Spec coverage: The plan makes stateful-vs-one-shot routing explicit and only for content reads.
- Placeholder scan: No placeholder steps remain.
- Type consistency:
StatefulMcpRuntimeis a protocol satisfied byMcpRuntimePool. - Policy boundary: discovery/catalog refresh still uses one-shot adapter; no assumption that resource/prompt listing is stateless.