Files
lda-wf/docs/historical/superpowers/plans/2026-06-07-mcp-runtime-read-resource.md
T

409 lines
12 KiB
Markdown

# 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.