# MCP Runtime Read Resource 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:** Add persistent MCP runtime support for `read_resource()` by routing it through the existing owner-task operation queue and `McpSourceClient`. **Architecture:** This is the first non-tool runtime operation. It must stay a thin wrapper over `McpSourceClient.read_resource()` so raw MCP SDK types and transport ownership do not leak. `PersistentMcpSession` and `McpRuntimePool` gain `read_resource`; `get_prompt`, `invoke_method`, and `send_notification` remain out of scope. **Tech Stack:** Python 3.14, MCP Python SDK, pytest/pytest-asyncio, ruff, basedpyright. --- ## File Structure - Modify `src/wf_sources_mcp/runtime/factory.py` - Add `_SessionOwner.read_resource(uri)`. - Implement through `submit(operation="read_resource", run=lambda client: client.read_resource(uri))`. - Modify `src/wf_sources_mcp/runtime/session.py` - Add `RawResourceReader` callback type. - Add `read_resource_callback` field. - Add `read_resource(uri)` method. - Keep fallback `client.read_resource()` serialization for injected/fake sessions. - Modify `src/wf_sources_mcp/runtime/pool.py` - Add `McpRuntimePool.read_resource(connection, auth, uri)`. - Modify tests: - `tests/wf_sources_mcp/test_runtime.py` - `tests/wf_mcp/test_stateful_runtime.py` only if type compatibility requires it. - Update docs: - `docs/current_roadmap.md` - `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md` ## Hard Boundaries - Do not add persistent runtime `get_prompt`, `invoke_method`, or `send_notification`. - Do not expose raw `ClientSession` outside the owner task. - Do not return raw MCP SDK result models from `read_resource`; return serialized `dict[str, Any]`. - Do not alter `McpSourceClient.read_resource()` behavior. - Do not dispatch by string; `operation="read_resource"` is metadata only. --- ### Task 1: Add Persistent Read Tests **Files:** - Modify: `tests/wf_sources_mcp/test_runtime.py` - [ ] **Step 1: Extend fake client with resource reads** In `tests/wf_sources_mcp/test_runtime.py`, import `AnyUrl`: ```python from pydantic import AnyUrl ``` Update `_FakeFactory._create_with_stack()`'s `_FakeClient` class: ```python class _FakeClient: async def call_tool( self, tool_name: str, payload: dict[str, object] ) -> RawCallToolResult: return await factory._call_tool(tool_name, payload) async def read_resource(self, uri: AnyUrl): return type( "ReadResourceResult", (), { "model_dump": lambda _self, **_kwargs: { "contents": [{"uri": str(uri), "text": "resource text"}] } }, )() ``` - [ ] **Step 2: Add session-level persistent read test** Append to `tests/wf_sources_mcp/test_runtime.py`: ```python @pytest.mark.asyncio async def test_persistent_session_factory_routes_resource_reads_through_owner() -> None: factory = _FakeFactory() connection = _connection() session = await factory.create(connection, None) await session.call_tool("echo", {"text": "one"}) resource_payload = await session.read_resource("fixture://docs/welcome") await session.close() assert factory.created_connections == [connection] assert factory.calls == [("echo", {"text": "one"})] assert resource_payload == { "contents": [ {"uri": "fixture://docs/welcome", "text": "resource text"}, ] } ``` - [ ] **Step 3: Add pool-level persistent read test** Append: ```python @pytest.mark.asyncio async def test_runtime_pool_reuses_session_for_tool_and_resource_read() -> None: factory = _FakeFactory() pool = McpRuntimePool(factory.create) connection = _connection() tool_result = await pool.call_tool(connection, None, "echo", {"text": "one"}) resource_payload = await pool.read_resource( connection, None, "fixture://docs/welcome", ) await pool.close_all() assert tool_result.output == {"echoed": "one"} assert resource_payload["contents"][0]["text"] == "resource text" assert factory.created_connections == [connection] ``` - [ ] **Step 4: Update public surface test** Update `test_persistent_session_public_runtime_is_tool_call_only` to allow `read_resource` and still forbid the rest: ```python def test_persistent_session_public_runtime_exposes_only_tool_and_resource_read() -> None: public_operations = { name for name in dir(PersistentMcpSession) if not name.startswith("_") and callable(getattr(PersistentMcpSession, name)) } assert "call_tool" in public_operations assert "read_resource" in public_operations assert "get_prompt" not in public_operations assert "invoke_method" not in public_operations assert "send_notification" not in public_operations ``` - [ ] **Step 5: Run tests and confirm failure** Run: ```bash uv run pytest tests/wf_sources_mcp/test_runtime.py -q ``` Expected: fail because `PersistentMcpSession.read_resource` and `McpRuntimePool.read_resource` do not exist yet. --- ### Task 2: Add `read_resource` to Persistent Session **Files:** - Modify: `src/wf_sources_mcp/runtime/session.py` - [ ] **Step 1: Update imports and callback types** Add `AnyUrl` import: ```python from pydantic import AnyUrl ``` Add callback type: ```python RawResourceReader = Callable[[str], Awaitable[dict[str, Any]]] ``` - [ ] **Step 2: Add callback field** In `PersistentMcpSession`, add: ```python read_resource_callback: RawResourceReader | None = None ``` - [ ] **Step 3: Add `read_resource` method** Add below `call_tool()`: ```python async def read_resource(self, uri: str) -> dict[str, Any]: """Read an MCP resource through the owner task or injected session.""" if self.read_resource_callback is not None: return await self.read_resource_callback(uri) if self.client is not None: result = await self.client.read_resource(AnyUrl(uri)) return result.model_dump(by_alias=True, mode="json", exclude_none=True) raise RuntimeError("persistent MCP session has no resource read transport") ``` - [ ] **Step 4: Run source tests** Run: ```bash uv run pytest tests/wf_sources_mcp/test_runtime.py -q ``` Expected: still fail at factory/pool routing because callbacks are not wired yet. --- ### Task 3: Route `read_resource` Through Owner Queue **Files:** - Modify: `src/wf_sources_mcp/runtime/factory.py` - [ ] **Step 1: Wire callback in `PersistentSessionFactory.create()`** Update returned `PersistentMcpSession`: ```python return PersistentMcpSession( connection=connection, auth=auth, call_callback=owner.call_tool, read_resource_callback=owner.read_resource, close_callback=owner.close, ) ``` - [ ] **Step 2: Add owner `read_resource()`** Add below `_SessionOwner.call_tool()`: ```python async def read_resource(self, uri: str) -> dict[str, Any]: """Submit a resource read through the generic owner-task operation queue.""" return await self.submit( operation="read_resource", run=lambda client: client.read_resource(uri), ) ``` - [ ] **Step 3: Run source tests** Run: ```bash uv run pytest tests/wf_sources_mcp/test_runtime.py -q ``` Expected: pool test still fails until pool method is added; direct session read should pass. --- ### Task 4: Add `read_resource` to Runtime Pool **Files:** - Modify: `src/wf_sources_mcp/runtime/pool.py` - [ ] **Step 1: Add pool method** Add below `call_tool()`: ```python async def read_resource( self, connection: McpSourceConnection, auth: AuthRecord | None, uri: str, ) -> dict[str, Any]: session = await self.get_session(connection, auth) return await session.read_resource(uri) ``` - [ ] **Step 2: Run runtime tests** Run: ```bash uv run pytest tests/wf_sources_mcp/test_runtime.py tests/wf_mcp/test_stateful_runtime.py -q ``` Expected: pass. --- ### Task 5: Verify Surface and Update Docs **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-mcp-runtime-read-resource.md` to `docs/historical/superpowers/plans/2026-06-07-mcp-runtime-read-resource.md` - [ ] **Step 1: Verify forbidden runtime methods remain absent** Run: ```bash rg -n "def (list_tools|list_resources|list_prompts|get_prompt|invoke_method|send_notification)" src/wf_sources_mcp/runtime ``` Expected: no matches. `read_resource` is now allowed. - [ ] **Step 2: Update roadmap** In `docs/current_roadmap.md`, under the MCP upstream source runtime cleanup bullets, add: ```markdown - Completed: persistent MCP runtime can now route `read_resource` through the owner-task queue and `McpSourceClient`. This is intentionally a thin wrapper over the existing source-client facade; prompt/raw method runtime operations remain separate future slices. ``` - [ ] **Step 3: Update long-lived API boundary spec** In `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md`, add after the runtime operation queue item: ```markdown 11. Complete: persistent MCP runtime can route `read_resource` through the owner-task queue and `McpSourceClient`. Runtime still does not expose `get_prompt`, raw method invocation, or notifications. ``` Renumber following item if needed. - [ ] **Step 4: Archive completed plan after implementation** Run only after all code/tests pass: ```bash git mv docs/superpowers/plans/2026-06-07-mcp-runtime-read-resource.md docs/historical/superpowers/plans/2026-06-07-mcp-runtime-read-resource.md ``` --- ### Task 6: Final Verification **Files:** - All changed files. - [ ] **Step 1: Run focused tests** Run: ```bash uv run pytest tests/wf_sources_mcp tests/wf_mcp/test_stateful_runtime.py tests/wf_mcp/test_compat_imports.py -q ``` Expected: pass. - [ ] **Step 2: Run source typecheck** Run: ```bash uv run basedpyright --level error src ``` Expected: `0 errors, 0 warnings, 0 notes`. - [ ] **Step 3: Run focused test typecheck** Run: ```bash uv run basedpyright --level error tests/wf_sources_mcp/test_runtime.py tests/wf_mcp/test_stateful_runtime.py ``` Expected: `0 errors, 0 warnings, 0 notes`. - [ ] **Step 4: Run focused lint** Run: ```bash uv run ruff check src/wf_sources_mcp/runtime tests/wf_sources_mcp/test_runtime.py tests/wf_mcp/test_stateful_runtime.py ``` Expected: `All checks passed!` - [ ] **Step 5: Confirm no forbidden runtime expansion** Run: ```bash rg -n "def (list_tools|list_resources|list_prompts|get_prompt|invoke_method|send_notification)" src/wf_sources_mcp/runtime ``` Expected: no matches. - [ ] **Step 6: Check whitespace** Run: ```bash git diff --check ``` Expected: no whitespace errors. CRLF warnings are acceptable on Windows. --- ## Self-Review - Spec coverage: The plan adds persistent resource reads through the existing generic queue and source-client facade while keeping the surface intentionally narrow. - Placeholder scan: No placeholder steps remain. - Type consistency: `read_resource` returns `dict[str, Any]` everywhere. - Hard boundary: no prompt/raw-method/notification runtime methods are added.