22 KiB
WfMcpService Resource/Prompt Access Extraction 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: Extract the local-docs-aware resource read and prompt render logic from WfMcpService into a focused ContentAccessService while preserving existing MCP admin, CLI, and workflow behavior.
Architecture: Add ContentAccessService under wf_mcp.broker.service. It owns the "is this a local doc? if so return inline, else look up catalog entry and delegate upstream" branching that currently lives in WfMcpService.read_resource and WfMcpService.render_prompt. WfMcpService remains the compatibility coordinator; its read_resource and render_prompt become one-line delegates.
Why a new service instead of folding into UpstreamTransportService: The local-docs check depends on SourceCatalogService, not on upstream adapters. Putting it in UpstreamTransportService would add a cross-cutting catalog dependency to a transport-focused service. A thin content-access layer keeps the boundary clean.
Tech Stack: Python 3.14, dataclasses, SourceCatalogService, UpstreamTransportService, ConnectionService, pytest, ruff, basedpyright.
Scope
Move now:
WfMcpService.read_resourcebranching logic (local docs check + upstream delegation).WfMcpService.render_promptbranching logic (local docs check + upstream delegation).
Keep now:
invoke_method. Already a clean delegate toupstream; no local-docs branching. Does not belong in this service.send_notification. Same reasoning.refresh_connection_catalog. Different semantics; stays onWfMcpService/UpstreamTransportService.- All other
WfMcpServicedelegates and compatibility properties. - Event recording internals on
BrokerEventRecorder.
Do not do in this slice:
- Do not move
invoke_methodorsend_notificationintoContentAccessService. - Do not move
refresh_connection_catalog. - Do not rename
WfMcpService. - Do not change adapter implementations.
- Do not change MCP tool schemas or CLI behavior.
Target File Structure
-
Create
src/wf_mcp/broker/service/content_access.py- Owns
ContentAccessService. - Depends on
SourceCatalogService(for local docs lookup and catalog entry resolution),UpstreamTransportService(for upstream reads/renders),ConnectionService(for connection lookup), andBrokerEventRecorder(for local-docs event recording). - Contains docstrings stating this is a broker-internal service, not a protocol-neutral API.
- Owns
-
Modify
src/wf_mcp/broker/service/core.py- Add
content_access: ContentAccessService = field(init=False). - Construct it after
source_catalog,upstream,connection_service, andeventsin__post_init__. - Replace
read_resourceandrender_promptbodies with one-line delegates.
- Add
-
Add tests in
tests/wf_mcp/service/test_content_access.py. -
Update docs:
docs/superpowers/research/2026-06-02-wfmcpservice-remaining-responsibilities.mdif stale.
Task 1: Add ContentAccessService Skeleton
Files:
-
Create:
src/wf_mcp/broker/service/content_access.py -
Create:
tests/wf_mcp/service/test_content_access.py -
Step 1: Write a direct local-resource test
Create tests/wf_mcp/service/test_content_access.py:
from __future__ import annotations
import asyncio
import pytest
from wf_mcp.broker import WfMcpService
from wf_mcp.broker.service.content_access import ContentAccessService
from wf_mcp.broker.service.connection_service import ConnectionService
from wf_mcp.broker.service.events import BrokerEventRecorder
from wf_mcp.broker.service.source_catalog import SourceCatalogService
from wf_mcp.broker.service.upstream_transport import UpstreamTransportService
from wf_mcp.events import EventBus
from wf_mcp.models import ConnectionConfig
from wf_mcp.storage import FileStore
from wf_platform import (
CapabilityBuckets,
CapabilitySource,
DocumentationPrompt,
DocumentationResource,
SourceVisibility,
)
from ..test_support import FakeAdapter, local_temp_root
def _make_content_access(
*,
store_root: str = "content_access_default",
) -> tuple[ContentAccessService, BrokerEventRecorder]:
store = FileStore(local_temp_root() / store_root)
events = BrokerEventRecorder(EventBus())
connection_service = ConnectionService(events=events)
upstream = UpstreamTransportService(
store=store,
event_sink=events.record_event,
)
source_catalog = SourceCatalogService(
store=store,
connection_lookup=connection_service.get,
connection_list_enabled=connection_service.list_enabled,
connection_list_all=connection_service.list_all,
tool_executor_for=upstream.tool_executor_for,
load_auth=upstream.load_auth,
emit_event=events.record_event,
)
connection_service.bind_source_catalog(source_catalog)
_register_local_docs(source_catalog)
content_access = ContentAccessService(
source_catalog=source_catalog,
upstream=upstream,
connection_service=connection_service,
event_sink=events.record_event,
)
return content_access, events
def _register_local_docs(source_catalog: SourceCatalogService) -> None:
"""Install deterministic local docs without depending on repo Markdown files."""
source_catalog.register_capability_source(
CapabilitySource(
id="test.docs",
kind="system",
capabilities=CapabilityBuckets(
resources={
"test.docs.example": DocumentationResource(
name="test.docs.example",
uri="wf://docs/example",
title="Example Doc",
description="Test documentation resource.",
mime_type="text/markdown",
text="# Example",
)
},
prompts={
"test.docs.guide": DocumentationPrompt(
name="test.docs.guide",
title="Guide Prompt",
description="Test documentation prompt.",
text="Use the test docs.",
)
},
),
visibility=SourceVisibility(planner=True),
)
)
def test_content_access_reads_local_documentation_resource() -> None:
content_access, events = _make_content_access()
result = asyncio.run(
content_access.read_resource("test.docs.example")
)
assert result["contents"][0]["uri"] == "wf://docs/example"
assert result["contents"][0]["text"] == "# Example"
assert "resource_read_completed" in [e.kind for e in events.list_events()]
- Step 2: Run the test and verify it fails
Run:
uv run pytest tests/wf_mcp/service/test_content_access.py::test_content_access_reads_local_documentation_resource -q
Expected: import failure because content_access.py does not exist.
- Step 3: Create the skeleton service
Create src/wf_mcp/broker/service/content_access.py:
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from wf_mcp.events import McpEvent, make_event
from .connection_service import ConnectionService
from .source_catalog import SourceCatalogService
from .upstream_transport import UpstreamTransportService
EventSink = Callable[[McpEvent], None]
@dataclass(slots=True)
class ContentAccessService:
"""Own local-docs-aware resource reads and prompt renders for the broker.
This service checks SourceCatalogService for local documentation entries
before falling back to upstream transport. It is broker-internal, not a
protocol-neutral content API.
"""
source_catalog: SourceCatalogService
upstream: UpstreamTransportService
connection_service: ConnectionService
event_sink: EventSink
async def read_resource(self, qualified_name: str) -> dict[str, Any]:
local_resource = self.source_catalog.local_documentation_resource(
qualified_name
)
if local_resource is not None:
self.event_sink(
make_event(
"resource_read_completed",
capability_id=qualified_name,
payload={"uri": local_resource.uri, "source": "local"},
)
)
return {
"contents": [
{
"uri": local_resource.uri,
"mimeType": local_resource.mime_type,
"text": local_resource.text,
}
]
}
resource = self.source_catalog.get_resource(qualified_name)
connection = self.connection_service.get(resource.connection_id)
return await self.upstream.read_resource(
connection,
qualified_name,
resource.uri,
)
async def render_prompt(
self,
qualified_name: str,
*,
arguments: dict[str, str] | None = None,
) -> dict[str, Any]:
local_prompt = self.source_catalog.local_documentation_prompt(qualified_name)
if local_prompt is not None:
self.event_sink(
make_event(
"prompt_get_completed",
capability_id=qualified_name,
payload={
"argument_keys": sorted((arguments or {}).keys()),
"source": "local",
},
)
)
return {
"description": local_prompt.description,
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": local_prompt.text,
},
}
],
}
prompt = self.source_catalog.get_prompt(qualified_name)
connection = self.connection_service.get(prompt.connection_id)
return await self.upstream.render_prompt(
connection,
qualified_name,
prompt.local_name,
arguments,
)
- Step 4: Run the test
Run:
uv run pytest tests/wf_mcp/service/test_content_access.py::test_content_access_reads_local_documentation_resource -q
Expected: pass.
- Step 5: Run ruff
Run:
uv run ruff check src/wf_mcp/broker/service/content_access.py tests/wf_mcp/service/test_content_access.py
Expected: pass.
Task 2: Add Direct Upstream Resource and Prompt Tests
Files:
-
Modify:
tests/wf_mcp/service/test_content_access.py -
Step 1: Add upstream resource read test
Append to tests/wf_mcp/service/test_content_access.py:
def test_content_access_reads_upstream_resource_with_events() -> None:
service = WfMcpService(
store=FileStore(local_temp_root() / "content_upstream_resource")
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_adapter("demo", FakeAdapter())
asyncio.run(service.refresh_connection_catalog("demo.personal"))
result = asyncio.run(
service.content_access.read_resource("demo.personal.resource.welcome")
)
assert result["contents"][0]["text"] == "Welcome from the fake adapter resource."
event_kinds = [e.kind for e in service.list_events()]
assert "resource_read_started" in event_kinds
assert "resource_read_completed" in event_kinds
def test_content_access_renders_upstream_prompt_with_events() -> None:
service = WfMcpService(
store=FileStore(local_temp_root() / "content_upstream_prompt")
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_adapter("demo", FakeAdapter())
asyncio.run(service.refresh_connection_catalog("demo.personal"))
result = asyncio.run(
service.content_access.render_prompt(
"demo.personal.prompt.summarize",
arguments={"text": "hello world"},
)
)
assert "hello world" in result["messages"][0]["content"]["text"]
event_kinds = [e.kind for e in service.list_events()]
assert "prompt_get_started" in event_kinds
assert "prompt_get_completed" in event_kinds
- Step 2: Run the tests and verify they fail
Run:
uv run pytest tests/wf_mcp/service/test_content_access.py::test_content_access_reads_upstream_resource_with_events tests/wf_mcp/service/test_content_access.py::test_content_access_renders_upstream_prompt_with_events -q
Expected: fail because service.content_access does not exist on WfMcpService.
- Step 3: Wire ContentAccessService into WfMcpService
In src/wf_mcp/broker/service/core.py, import:
from .content_access import ContentAccessService
Add field:
content_access: ContentAccessService = field(init=False)
In __post_init__, after constructing upstream, source_catalog, connection_service, and events:
self.content_access = ContentAccessService(
source_catalog=self.source_catalog,
upstream=self.upstream,
connection_service=self.connection_service,
event_sink=self.events.record_event,
)
- Step 4: Run the tests
Run:
uv run pytest tests/wf_mcp/service/test_content_access.py::test_content_access_reads_upstream_resource_with_events tests/wf_mcp/service/test_content_access.py::test_content_access_renders_upstream_prompt_with_events -q
Expected: pass.
- Step 5: Run ruff
Run:
uv run ruff check src/wf_mcp/broker/service/core.py src/wf_mcp/broker/service/content_access.py tests/wf_mcp/service/test_content_access.py
Expected: pass.
Task 3: Delegate WfMcpService.read_resource and render_prompt
Files:
-
Modify:
src/wf_mcp/broker/service/core.py -
Test:
tests/wf_mcp/service/test_events.py -
Test:
tests/wf_mcp/service/test_content_access.py -
Step 1: Replace read_resource body with delegate
In src/wf_mcp/broker/service/core.py, replace:
async def read_resource(self, qualified_name: str) -> dict[str, Any]:
local_resource = self.source_catalog.local_documentation_resource(
qualified_name
)
if local_resource is not None:
self._record_event(
make_event(
"resource_read_completed",
capability_id=qualified_name,
payload={"uri": local_resource.uri, "source": "local"},
)
)
return {
"contents": [
{
"uri": local_resource.uri,
"mimeType": local_resource.mime_type,
"text": local_resource.text,
}
]
}
resource = self.get_resource(qualified_name)
connection = self.connections.get(resource.connection_id)
return await self.upstream.read_resource(
connection,
qualified_name,
resource.uri,
)
with:
async def read_resource(self, qualified_name: str) -> dict[str, Any]:
return await self.content_access.read_resource(qualified_name)
- Step 2: Replace render_prompt body with delegate
Replace:
async def render_prompt(
self,
qualified_name: str,
*,
arguments: dict[str, str] | None = None,
) -> dict[str, Any]:
local_prompt = self.source_catalog.local_documentation_prompt(qualified_name)
if local_prompt is not None:
self._record_event(
make_event(
"prompt_get_completed",
capability_id=qualified_name,
payload={
"argument_keys": sorted((arguments or {}).keys()),
"source": "local",
},
)
)
return {
"description": local_prompt.description,
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": local_prompt.text,
},
}
],
}
prompt = self.get_prompt(qualified_name)
connection = self.connections.get(prompt.connection_id)
return await self.upstream.render_prompt(
connection,
qualified_name,
prompt.local_name,
arguments,
)
with:
async def render_prompt(
self,
qualified_name: str,
*,
arguments: dict[str, str] | None = None,
) -> dict[str, Any]:
return await self.content_access.render_prompt(
qualified_name,
arguments=arguments,
)
- Step 3: Run the existing event/proxy tests
Run:
uv run pytest tests/wf_mcp/service/test_events.py::test_service_can_proxy_resource_reads_and_prompt_gets -q
Expected: pass. This proves the delegate preserves existing behavior.
- Step 4: Run full content_access tests
Run:
uv run pytest tests/wf_mcp/service/test_content_access.py -q
Expected: pass.
- Step 5: Run ruff
Run:
uv run ruff check src/wf_mcp/broker/service/core.py
Expected: pass.
Task 4: Add Local Prompt Test and Edge-Case Coverage
Files:
-
Modify:
tests/wf_mcp/service/test_content_access.py -
Step 1: Add local prompt test
Append:
def test_content_access_renders_local_documentation_prompt() -> None:
content_access, events = _make_content_access(
store_root="content_access_local_prompt"
)
result = asyncio.run(
content_access.render_prompt("test.docs.guide")
)
assert result["description"] == "Test documentation prompt."
assert result["messages"][0]["role"] == "user"
assert result["messages"][0]["content"]["text"] == "Use the test docs."
assert "prompt_get_completed" in [e.kind for e in events.list_events()]
- Step 2: Run the local prompt test
Run:
uv run pytest tests/wf_mcp/service/test_content_access.py::test_content_access_renders_local_documentation_prompt -q
Expected: pass.
- Step 3: Add missing-resource error test
Append:
def test_content_access_raises_on_unknown_resource() -> None:
content_access, _ = _make_content_access(
store_root="content_access_missing_resource"
)
with pytest.raises(KeyError):
asyncio.run(content_access.read_resource("nonexistent.resource"))
- Step 4: Run the error test
Run:
uv run pytest tests/wf_mcp/service/test_content_access.py::test_content_access_raises_on_unknown_resource -q
Expected: pass (KeyError from catalog lookup).
- Step 5: Run ruff
Run:
uv run ruff check tests/wf_mcp/service/test_content_access.py
Expected: pass.
Task 5: Clean Unused Imports and Verify
Files:
-
Modify:
src/wf_mcp/broker/service/core.py -
Modify:
docs/superpowers/research/2026-06-02-wfmcpservice-remaining-responsibilities.mdif stale. -
Step 1: Remove stale imports from core.py
After the delegate change, core.py may no longer use make_event directly (verify with grep). If make_event is still used by _record_catalog_change_events or other methods, keep it. Only remove imports that are truly unused after the extraction.
Run:
rg -n "make_event" src/wf_mcp/broker/service/core.py
If only the removed read_resource/render_prompt bodies used make_event, remove the import. Otherwise keep it.
- Step 2: Update research doc if stale
If docs/superpowers/research/2026-06-02-wfmcpservice-remaining-responsibilities.md still lists read_resource and render_prompt as "Mixed real responsibility", update their classification:
| `read_resource` | Delegates to `ContentAccessService` | Compatibility facade | Keep for admin/MCP callers; new code should call `content_access`. |
| `render_prompt` | Delegates to `ContentAccessService` | Compatibility facade | Keep for admin/MCP callers; new code should call `content_access`. |
- Step 3: Run focused verification
Run:
uv run pytest tests/wf_mcp/service/test_content_access.py tests/wf_mcp/service/test_events.py -q
Expected: all selected tests pass.
- Step 4: Run full verification
Run:
uv run pytest -q
uv run ruff check src/wf_mcp/broker/service tests/wf_mcp/service
uv run ruff format --check src/wf_mcp/broker/service tests/wf_mcp/service
uv run basedpyright --level error
Expected:
- pytest passes.
- ruff check passes.
- ruff format check passes.
- basedpyright reports
0 errors. If the known workspace enumeration warning causes a nonzero exit despite0 errors, record the exact output.
Non-Goals and Follow-Up Slices
This plan intentionally leaves these for later:
invoke_method/send_notification: Already clean delegates toupstream. No local-docs branching. Moving them would add coupling without reducing complexity.refresh_connection_catalog: Different semantics (catalog refresh, not content access). Stays onWfMcpService/UpstreamTransportService.- Connection registry extraction: Already done in
ConnectionService; do not change it in this slice. - Event recorder extraction: Already done in
BrokerEventRecorder; do not change it in this slice. workflow_artifact_catalog_entry: Not broker content access; handle in a separate small cleanup.
Self-Review
- Spec coverage: The plan extracts the local-docs-aware resource/prompt branching logic while preserving current public
WfMcpServicemethods as one-line delegates. Upstream event semantics are unchanged. - Placeholder scan: No placeholder implementation tasks are left. Local docs tests use deterministic test-only resource and prompt keys.
- Type consistency:
ContentAccessServicedepends onSourceCatalogService,UpstreamTransportService,ConnectionService, and anEventSinkcallback.WfMcpService.content_accessis exposed as a public field so callers (admin handlers, tests) can use it directly.