12 KiB
MCP Runtime Get Prompt 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 get_prompt() by routing it through the existing owner-task operation queue and McpSourceClient.
Architecture: This is the second safe non-tool runtime operation after read_resource. It stays a thin wrapper over McpSourceClient.get_prompt() and returns serialized dict[str, Any]. Raw method invocation, notifications, and discovery lists remain separate future decisions.
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.get_prompt(prompt_name, arguments=None). - Implement through
submit(operation="get_prompt", run=lambda client: client.get_prompt(prompt_name, arguments)).
- Add
- Modify
src/wf_sources_mcp/runtime/session.py- Add
RawPromptGettercallback type. - Add
get_prompt_callbackfield. - Add
get_prompt(prompt_name, arguments=None)method. - Keep fallback
client.get_prompt()serialization for injected/fake sessions.
- Add
- Modify
src/wf_sources_mcp/runtime/pool.py- Add
McpRuntimePool.get_prompt(connection, auth, prompt_name, arguments=None).
- Add
- Modify tests:
tests/wf_sources_mcp/test_runtime.pytests/wf_mcp/test_stateful_runtime.pyonly if type compatibility requires it.
- Update docs:
docs/current_roadmap.mddocs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md
Hard Boundaries
- Do not add persistent runtime
invoke_method,send_notification, or discovery list methods. - Do not expose raw
ClientSessionoutside the owner task. - Do not return raw MCP SDK result models from
get_prompt; return serializeddict[str, Any]. - Do not alter
McpSourceClient.get_prompt()behavior. - Do not dispatch by string;
operation="get_prompt"is metadata only.
Task 1: Add Persistent Prompt Tests
Files:
-
Modify:
tests/wf_sources_mcp/test_runtime.py -
Step 1: Extend fake client with prompt gets
In tests/wf_sources_mcp/test_runtime.py, update _FakeFactory._create_with_stack()'s _FakeClient class to include:
async def get_prompt(
self,
prompt_name: str,
arguments: dict[str, str] | None = None,
):
return type(
"GetPromptResult",
(),
{
"model_dump": lambda _self, **_kwargs: {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": f"{prompt_name}:{arguments or {}}",
},
}
]
}
},
)()
- Step 2: Add error-path test
Append to tests/wf_sources_mcp/test_runtime.py:
@pytest.mark.asyncio
async def test_persistent_session_raises_without_prompt_transport() -> None:
session = PersistentMcpSession(connection=_connection(), auth=None)
with pytest.raises(RuntimeError, match="no prompt transport"):
await session.get_prompt("prompt.summarize")
- Step 3: Add session-level persistent prompt test
Append:
@pytest.mark.asyncio
async def test_persistent_session_factory_routes_prompts_through_owner() -> None:
factory = _FakeFactory()
connection = _connection()
session = await factory.create(connection, None)
await session.call_tool("echo", {"text": "one"})
await session.read_resource("fixture://docs/welcome")
prompt_payload = await session.get_prompt(
"prompt.summarize",
{"text": "hello"},
)
await session.close()
assert factory.created_connections == [connection]
assert factory.calls == [("echo", {"text": "one"})]
assert prompt_payload["messages"][0]["content"]["text"] == (
"prompt.summarize:{'text': 'hello'}"
)
- Step 4: Add pool-level persistent prompt test
Append:
@pytest.mark.asyncio
async def test_runtime_pool_reuses_session_for_tool_resource_and_prompt() -> 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",
)
prompt_payload = await pool.get_prompt(
connection,
None,
"prompt.summarize",
{"text": "hello"},
)
await pool.close_all()
assert tool_result.output == {"echoed": "one"}
assert resource_payload["contents"][0]["text"] == "resource text"
assert prompt_payload["messages"][0]["content"]["text"] == (
"prompt.summarize:{'text': 'hello'}"
)
assert factory.created_connections == [connection]
- Step 5: Update public surface test
Update test_persistent_session_public_runtime_exposes_only_tool_and_resource_read to allow get_prompt and still forbid the rest:
def test_persistent_session_public_runtime_exposes_safe_read_operations() -> 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" in public_operations
assert "invoke_method" not in public_operations
assert "send_notification" not in public_operations
- Step 6: Run tests and confirm failure
Run:
uv run pytest tests/wf_sources_mcp/test_runtime.py -q
Expected: fail because PersistentMcpSession.get_prompt and McpRuntimePool.get_prompt do not exist yet.
Task 2: Add get_prompt to Persistent Session
Files:
-
Modify:
src/wf_sources_mcp/runtime/session.py -
Step 1: Add callback type
Add near existing callback types:
RawPromptGetter = Callable[
[str, dict[str, str] | None],
Awaitable[dict[str, Any]],
]
- Step 2: Add callback field
In PersistentMcpSession, add:
get_prompt_callback: RawPromptGetter | None = None
- Step 3: Add
get_promptmethod
Add below read_resource():
async def get_prompt(
self,
prompt_name: str,
arguments: dict[str, str] | None = None,
) -> dict[str, Any]:
"""Get an MCP prompt through the owner task or injected session."""
if self.get_prompt_callback is not None:
return await self.get_prompt_callback(prompt_name, arguments)
if self.client is not None:
result = await self.client.get_prompt(prompt_name, arguments)
return result.model_dump(by_alias=True, mode="json", exclude_none=True)
raise RuntimeError("persistent MCP session has no prompt transport")
- Step 4: Run source tests
Run:
uv run pytest tests/wf_sources_mcp/test_runtime.py -q
Expected: still fail at factory/pool routing until callbacks are wired.
Task 3: Route get_prompt Through Owner Queue
Files:
-
Modify:
src/wf_sources_mcp/runtime/factory.py -
Step 1: Wire callback in
PersistentSessionFactory.create()
Update returned PersistentMcpSession:
return PersistentMcpSession(
connection=connection,
auth=auth,
call_callback=owner.call_tool,
read_resource_callback=owner.read_resource,
get_prompt_callback=owner.get_prompt,
close_callback=owner.close,
)
- Step 2: Add owner
get_prompt()
Add below _SessionOwner.read_resource():
async def get_prompt(
self,
prompt_name: str,
arguments: dict[str, str] | None = None,
) -> dict[str, Any]:
"""Submit a prompt get through the generic owner-task operation queue."""
return await self.submit(
operation="get_prompt",
run=lambda client: client.get_prompt(prompt_name, arguments),
)
- Step 3: Run source tests
Run:
uv run pytest tests/wf_sources_mcp/test_runtime.py -q
Expected: pool test still fails until pool method is added; direct session prompt should pass.
Task 4: Add get_prompt to Runtime Pool
Files:
-
Modify:
src/wf_sources_mcp/runtime/pool.py -
Step 1: Add pool method
Add below read_resource():
async def get_prompt(
self,
connection: McpSourceConnection,
auth: AuthRecord | None,
prompt_name: str,
arguments: dict[str, str] | None = None,
) -> dict[str, Any]:
session = await self.get_session(connection, auth)
return await session.get_prompt(prompt_name, arguments)
- Step 2: Run runtime tests
Run:
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-get-prompt.mdtodocs/historical/superpowers/plans/2026-06-07-mcp-runtime-get-prompt.md -
Step 1: Verify forbidden runtime methods remain absent
Run:
rg -n "def (list_tools|list_resources|list_prompts|invoke_method|send_notification)" src/wf_sources_mcp/runtime
Expected: no matches. read_resource and get_prompt are now allowed.
- Step 2: Update roadmap
In docs/current_roadmap.md, under the MCP upstream source runtime cleanup bullets, add:
- Completed: persistent MCP runtime can now route `get_prompt` through
the owner-task queue and `McpSourceClient`. This keeps prompt reads
stateful without adding raw method invocation or notification support.
- Step 3: Update long-lived API boundary spec
In docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md, add after the read_resource runtime item:
12. Complete: persistent MCP runtime can route `get_prompt` through the
owner-task queue and `McpSourceClient`. Runtime still does not expose raw
method invocation, notifications, or discovery list operations.
Renumber following item if needed.
- Step 4: Archive completed plan after implementation
Run only after all code/tests pass:
git mv docs/superpowers/plans/2026-06-07-mcp-runtime-get-prompt.md docs/historical/superpowers/plans/2026-06-07-mcp-runtime-get-prompt.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/test_stateful_runtime.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_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:
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:
rg -n "def (list_tools|list_resources|list_prompts|invoke_method|send_notification)" src/wf_sources_mcp/runtime
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 adds persistent prompt gets through the existing generic queue and source-client facade while keeping the surface narrow.
- Placeholder scan: No placeholder steps remain.
- Type consistency:
get_promptreturnsdict[str, Any]everywhere and acceptsdict[str, str] | Nonearguments. - Hard boundary: no raw method, notification, or discovery-list runtime methods are added.