docs: archive completed superpowers plans
This commit is contained in:
+709
@@ -0,0 +1,709 @@
|
||||
# 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_resource` branching logic (local docs check + upstream delegation).
|
||||
- `WfMcpService.render_prompt` branching logic (local docs check + upstream delegation).
|
||||
|
||||
Keep now:
|
||||
|
||||
- `invoke_method`. Already a clean delegate to `upstream`; no local-docs branching. Does not belong in this service.
|
||||
- `send_notification`. Same reasoning.
|
||||
- `refresh_connection_catalog`. Different semantics; stays on `WfMcpService` / `UpstreamTransportService`.
|
||||
- All other `WfMcpService` delegates and compatibility properties.
|
||||
- Event recording internals on `BrokerEventRecorder`.
|
||||
|
||||
Do not do in this slice:
|
||||
|
||||
- Do not move `invoke_method` or `send_notification` into `ContentAccessService`.
|
||||
- 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), and `BrokerEventRecorder` (for local-docs event recording).
|
||||
- Contains docstrings stating this is a broker-internal service, not a protocol-neutral API.
|
||||
|
||||
- Modify `src/wf_mcp/broker/service/core.py`
|
||||
- Add `content_access: ContentAccessService = field(init=False)`.
|
||||
- Construct it after `source_catalog`, `upstream`, `connection_service`, and `events` in `__post_init__`.
|
||||
- Replace `read_resource` and `render_prompt` bodies with one-line delegates.
|
||||
|
||||
- Add tests in `tests/wf_mcp/service/test_content_access.py`.
|
||||
|
||||
- Update docs:
|
||||
- `docs/superpowers/research/2026-06-02-wfmcpservice-remaining-responsibilities.md` if 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`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
from .content_access import ContentAccessService
|
||||
```
|
||||
|
||||
Add field:
|
||||
|
||||
```python
|
||||
content_access: ContentAccessService = field(init=False)
|
||||
```
|
||||
|
||||
In `__post_init__`, after constructing `upstream`, `source_catalog`, `connection_service`, and `events`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_mcp/service/test_content_access.py -q
|
||||
```
|
||||
|
||||
Expected: pass.
|
||||
|
||||
- [ ] **Step 5: Run ruff**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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.md` if 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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```markdown
|
||||
| `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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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 despite `0 errors`, record the exact output.
|
||||
|
||||
---
|
||||
|
||||
## Non-Goals and Follow-Up Slices
|
||||
|
||||
This plan intentionally leaves these for later:
|
||||
|
||||
1. **`invoke_method`/`send_notification`:** Already clean delegates to `upstream`. No local-docs branching. Moving them would add coupling without reducing complexity.
|
||||
2. **`refresh_connection_catalog`:** Different semantics (catalog refresh, not content access). Stays on `WfMcpService` / `UpstreamTransportService`.
|
||||
3. **Connection registry extraction:** Already done in `ConnectionService`; do not change it in this slice.
|
||||
4. **Event recorder extraction:** Already done in `BrokerEventRecorder`; do not change it in this slice.
|
||||
5. **`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 `WfMcpService` methods 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: `ContentAccessService` depends on `SourceCatalogService`, `UpstreamTransportService`, `ConnectionService`, and an `EventSink` callback. `WfMcpService.content_access` is exposed as a public field so callers (admin handlers, tests) can use it directly.
|
||||
Reference in New Issue
Block a user