36 KiB
WfMcpService Source Catalog 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 source registry and catalog inventory responsibilities out of WfMcpService while preserving the existing service API and MCP/CLI behavior.
Architecture: Add a focused SourceCatalogService owned by WfMcpService. The new service owns capability-source registration, source inventory, planner/backend catalog projection, snapshot hydration, local docs lookup, and qualified spec lookup. WfMcpService stays as the compatibility coordinator and still owns connections, adapters, auth, upstream I/O, workflow runtime execution, event recording, and artifact/draft/run stores.
Tech Stack: Python 3.14, dataclasses, Pydantic models already in wf_platform/wf_mcp.models, pytest, ruff, basedpyright.
Scope
This is a reduction slice, not a rewrite. It should make WfMcpService thinner without changing public method names, MCP payloads, CLI behavior, or runtime execution semantics.
Move now:
- Capability source map ownership.
register_capability_source.get_catalog.get_planner_catalog.list_sources.list_source_summaries.inspect_source.list_available_specs.get_connection_snapshot.connection_statuses.list_resources.list_prompts.get_resource.get_prompt.- Local documentation resource/prompt lookup.
- Connection source hydration from stored catalog snapshots.
- Rebuilding
NodeSpecfromCatalogNodeEntry. - Qualified spec lookup.
Do not move in this slice:
- Connection registration policy.
- Adapter registry.
- Auth store methods.
- Upstream discovery I/O in
refresh_connection_catalog. - Resource/prompt upstream reads.
- Raw method/notification upstream calls.
- Workflow compile/run/resume runtime execution.
- Event bus implementation.
- Artifact/draft/run stores.
Target File Structure
-
Create
src/wf_mcp/broker/service/source_catalog.py- Owns the new
SourceCatalogService. - Imports MCP catalog/store models and
wf_platformsource models. - Accepts small callback dependencies for connection lookup, tool executor lookup, auth loading, and event emission where snapshot-hydrated specs need them.
- Contains docstrings that state this is a service-internal extraction, not a protocol-neutral API.
- Owns the new
-
Modify
src/wf_mcp/broker/service/core.py- Add
source_catalog: SourceCatalogService = field(init=False). - Keep
capability_sourcesas a property returningself.source_catalog.capability_sources. - Delegate moved public methods to
self.source_catalog. - Keep
refresh_connection_catalog,read_resource,render_prompt,compile_plan,_prepare_workflow_runtime,run_workflow_from_plan, andresume_workflow_from_planonWfMcpService.
- Add
-
Modify
src/wf_mcp/broker/service/workflow_operation_context.py- Keep adapting through
WfMcpService, but source/spec provider should read viaservice.source_catalogor the compatibility property.
- Keep adapting through
-
Modify tests under
tests/wf_mcp/service/test_catalog.py- Keep existing service-facing tests as compatibility coverage.
- Add direct
SourceCatalogServicetests for the new extracted component.
-
Modify docs:
docs/current_roadmap.mddocs/superpowers/research/2026-06-01-wf-api-extraction-map.mdif it still mentionsWfMcpServiceowning source/catalog state directly.
Task 1: Add SourceCatalogService Skeleton
Files:
-
Create:
src/wf_mcp/broker/service/source_catalog.py -
Modify:
src/wf_mcp/broker/service/core.py -
Test:
tests/wf_mcp/service/test_catalog.py -
Step 1: Write a direct component smoke test
Add this test near the source inventory tests in tests/wf_mcp/service/test_catalog.py:
from wf_mcp.broker.service.source_catalog import SourceCatalogService
def test_source_catalog_service_registers_and_lists_sources_directly() -> None:
store = FileStore(local_temp_root() / "source_catalog_direct")
def unused_tool_executor(connection: ConnectionConfig):
raise AssertionError("tool executor should not be used by source listing")
catalog = SourceCatalogService(
store=store,
connection_lookup=lambda connection_id: ConnectionConfig(
id=connection_id,
server="demo",
account="personal",
),
tool_executor_for=unused_tool_executor,
load_auth=lambda connection_id: None,
emit_event=lambda event: None,
)
catalog.register_capability_source(
CapabilitySource(
id="demo.personal",
kind="connection",
capabilities=CapabilityBuckets(),
visibility=SourceVisibility(planner=True),
)
)
payload = catalog.list_source_summaries(limit=10)
assert payload["total"] == 1
assert payload["sources"][0]["id"] == "demo.personal"
- Step 2: Run the focused test and verify it fails
Run:
uv run pytest tests/wf_mcp/service/test_catalog.py::test_source_catalog_service_registers_and_lists_sources_directly -q
Expected: import failure because wf_mcp.broker.service.source_catalog does not exist.
- Step 3: Create the skeleton component
Create src/wf_mcp/broker/service/source_catalog.py:
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any
from wf_platform import (
CapabilitySource,
page_items,
)
from ...connections import ConnectionConfig
from ...events import McpEvent
from ...models import (
AuthRecord,
)
from ...runtime import ToolExecutor
from ...storage import Store
ConnectionLookup = Callable[[str], ConnectionConfig]
ToolExecutorLookup = Callable[[ConnectionConfig], ToolExecutor]
AuthLoader = Callable[[str], AuthRecord | None]
EventEmitter = Callable[[McpEvent], None]
@dataclass(slots=True)
class SourceCatalogService:
"""Own service-local capability sources and catalog projections.
This is deliberately still MCP-broker-internal. It knows about stored MCP
catalog snapshots because hydrated workflow NodeSpecs must call back through
the broker's configured tool executor.
"""
store: Store
connection_lookup: ConnectionLookup
tool_executor_for: ToolExecutorLookup
load_auth: AuthLoader
emit_event: EventEmitter
default_catalog_max_age_seconds: int = 300
capability_sources: dict[str, CapabilitySource] = field(default_factory=dict)
def register_capability_source(self, source: CapabilitySource) -> None:
"""Register one source as canonical planner/runtime source state."""
self.capability_sources[source.id] = source
def list_source_summaries(
self,
*,
cursor: str | None = None,
limit: int = 50,
) -> dict[str, Any]:
"""Return compact paged source summaries for progressive discovery."""
summaries = [
source.as_status().model_dump(mode="json")
for source in sorted(
self.capability_sources.values(),
key=lambda source: source.id,
)
]
page = page_items(summaries, cursor=cursor, limit=limit)
return {
"sources": list(page.items),
"next_cursor": page.next_cursor,
"total": page.total,
}
Later tasks add imports as methods move. Keep imports minimal at each step because
ruff check must pass at the end of every task.
- Step 4: Run the focused test
Run:
uv run pytest tests/wf_mcp/service/test_catalog.py::test_source_catalog_service_registers_and_lists_sources_directly -q
Expected: pass.
- Step 5: Run ruff on the new file and test file
Run:
uv run ruff check src/wf_mcp/broker/service/source_catalog.py tests/wf_mcp/service/test_catalog.py
Expected: pass. Remove any imports that are still unused at this point.
Task 2: Wire SourceCatalogService Into WfMcpService Without Moving Behavior
Files:
-
Modify:
src/wf_mcp/broker/service/core.py -
Test:
tests/wf_mcp/service/test_catalog.py -
Step 1: Add a compatibility identity test
Add this test to tests/wf_mcp/service/test_catalog.py:
def test_wfmcpservice_capability_sources_proxy_source_catalog() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "source_catalog_proxy"))
assert service.capability_sources is service.source_catalog.capability_sources
assert "wf.std" in service.source_catalog.capability_sources
- Step 2: Run the test and verify it fails
Run:
uv run pytest tests/wf_mcp/service/test_catalog.py::test_wfmcpservice_capability_sources_proxy_source_catalog -q
Expected: fail because service.source_catalog does not exist.
- Step 3: Add the component field and compatibility property
In src/wf_mcp/broker/service/core.py, import:
from .source_catalog import SourceCatalogService
Change the dataclass fields:
adapters: dict[str, BackendAdapter] = field(default_factory=dict)
event_bus: EventBus = field(default_factory=EventBus)
include_builtin_specs: bool = True
artifact_store: WorkflowArtifactStore | None = None
draft_workspace_store: DraftWorkspaceStore | None = None
run_store: RunStore | None = None
tool_executor: ToolExecutor | None = None
source_catalog: SourceCatalogService = field(init=False)
Add a property inside WfMcpService:
@property
def capability_sources(self) -> dict[str, CapabilitySource]:
"""Compatibility view of source catalog state.
Source ownership is moving into SourceCatalogService. Keep this property
because workflow APIs and existing tests still consume the service facade.
"""
return self.source_catalog.capability_sources
At the top of __post_init__, before builtin source registration, create the source catalog:
self.source_catalog = SourceCatalogService(
store=self.store,
connection_lookup=self.connections.get,
tool_executor_for=self._tool_executor_for,
load_auth=self.load_auth,
emit_event=self._record_event,
default_catalog_max_age_seconds=self.default_catalog_max_age_seconds,
)
Remove the old dataclass field:
capability_sources: dict[str, CapabilitySource] = field(default_factory=dict)
- Step 4: Run focused compatibility tests
Run:
uv run pytest tests/wf_mcp/service/test_catalog.py::test_wfmcpservice_capability_sources_proxy_source_catalog tests/wf_mcp/service/test_catalog.py::test_service_installs_builtin_stdlib_specs_by_default -q
Expected: both pass.
- Step 5: Run ruff on service files
Run:
uv run ruff check src/wf_mcp/broker/service/core.py src/wf_mcp/broker/service/source_catalog.py
Expected: pass.
Task 3: Move Source Inventory and Planner Catalog Methods
Files:
-
Modify:
src/wf_mcp/broker/service/source_catalog.py -
Modify:
src/wf_mcp/broker/service/core.py -
Test:
tests/wf_mcp/service/test_catalog.py -
Step 1: Add direct component tests for inventory and planner visibility
Add this test:
def test_source_catalog_service_excludes_hidden_sources_from_planner_catalog() -> None:
def unused_tool_executor(connection: ConnectionConfig):
raise AssertionError("tool executor should not be used by planner listing")
catalog = SourceCatalogService(
store=FileStore(local_temp_root() / "source_catalog_hidden"),
connection_lookup=lambda connection_id: ConnectionConfig(
id=connection_id,
server="demo",
account="personal",
),
tool_executor_for=unused_tool_executor,
load_auth=lambda connection_id: None,
emit_event=lambda event: None,
)
visible_tool = NodeSpec(
name="visible.source.echo_tool",
input_model=echo_tool.input_model,
output_model=echo_tool.output_model,
outcomes=echo_tool.outcomes,
fn=echo_tool.fn,
description=echo_tool.description,
is_async=echo_tool.is_async,
accepts_context=echo_tool.accepts_context,
input_schema_contract=echo_tool.input_schema_contract,
output_schema_contract=echo_tool.output_schema_contract,
)
hidden_tool = NodeSpec(
name="hidden.source.echo_tool",
input_model=echo_tool.input_model,
output_model=echo_tool.output_model,
outcomes=echo_tool.outcomes,
fn=echo_tool.fn,
description=echo_tool.description,
is_async=echo_tool.is_async,
accepts_context=echo_tool.accepts_context,
input_schema_contract=echo_tool.input_schema_contract,
output_schema_contract=echo_tool.output_schema_contract,
)
catalog.register_capability_source(
CapabilitySource(
id="visible.source",
kind="system",
capabilities=CapabilityBuckets(
node_specs={"visible.source.echo_tool": visible_tool}
),
visibility=SourceVisibility(planner=True),
)
)
catalog.register_capability_source(
CapabilitySource(
id="hidden.source",
kind="system",
capabilities=CapabilityBuckets(
node_specs={"hidden.source.echo_tool": hidden_tool}
),
visibility=SourceVisibility(planner=False, admin_dashboard=False),
)
)
planner_names = {
entry.qualified_name for entry in catalog.get_planner_catalog().entries()
}
assert "visible.source.echo_tool" in planner_names
assert "hidden.source.echo_tool" not in planner_names
- Step 2: Run the direct test and verify it fails
Run:
uv run pytest tests/wf_mcp/service/test_catalog.py::test_source_catalog_service_excludes_hidden_sources_from_planner_catalog -q
Expected: fail because SourceCatalogService.get_planner_catalog does not exist.
- Step 3: Move catalog and source methods into SourceCatalogService
Move these methods from WfMcpService to SourceCatalogService with unchanged bodies except self.capability_sources now means the component field:
def get_catalog(self) -> CombinedCatalog:
snapshots: dict[str, CatalogSnapshot] = {}
for connection in self.connection_list_enabled():
snapshot = self.store.load_catalog(connection.id)
if snapshot is not None:
snapshots[connection.id] = snapshot
return CombinedCatalog(snapshots=snapshots)
The component needs connection list callbacks. Update callback types:
ConnectionList = Callable[[], list[ConnectionConfig]]
Add fields:
connection_list_enabled: ConnectionList
connection_list_all: ConnectionList
Update WfMcpService.__post_init__ construction:
connection_list_enabled=self.connections.list_enabled,
connection_list_all=self.connections.list_all,
Update every existing direct SourceCatalogService(...) construction in
tests/wf_mcp/service/test_catalog.py to include:
connection_list_enabled=lambda: [],
connection_list_all=lambda: [],
For tests that already define a concrete connection, use:
connection_list_enabled=lambda: [connection],
connection_list_all=lambda: [connection],
Move these methods exactly, adjusting connection list calls:
def get_catalog(self) -> CombinedCatalog: ...
def get_planner_catalog(self) -> CombinedCatalog: ...
def list_sources(self) -> list[dict[str, Any]]: ...
def inspect_source(self, source_id: str) -> dict[str, Any]: ...
def list_available_specs(self) -> list[CatalogNodeEntry]: ...
def get_connection_snapshot(self, connection_id: str) -> CatalogSnapshot | None: ...
def connection_statuses(self) -> list[dict[str, Any]]: ...
def list_resources(self, *, connection_id: str | None = None) -> list[CatalogResourceEntry]: ...
def list_prompts(self, *, connection_id: str | None = None) -> list[CatalogPromptEntry]: ...
def get_resource(self, qualified_name: str) -> CatalogResourceEntry: ...
def get_prompt(self, qualified_name: str) -> CatalogPromptEntry: ...
For get_connection_snapshot, use self.connection_lookup(connection_id) instead of self.connections.get(connection_id).
- Step 4: Delegate the same methods from WfMcpService
Replace each moved method body in src/wf_mcp/broker/service/core.py with a one-line delegate:
def get_planner_catalog(self) -> CombinedCatalog:
return self.source_catalog.get_planner_catalog()
Use the same pattern for every moved public method. Keep method signatures unchanged.
- Step 5: Run catalog tests
Run:
uv run pytest tests/wf_mcp/service/test_catalog.py -q
Expected: all tests in the file pass.
- Step 6: Run ruff
Run:
uv run ruff check src/wf_mcp/broker/service/core.py src/wf_mcp/broker/service/source_catalog.py tests/wf_mcp/service/test_catalog.py
Expected: pass.
Task 4: Move Connection Source Hydration and Snapshot Spec Rebuild
Files:
-
Modify:
src/wf_mcp/broker/service/source_catalog.py -
Modify:
src/wf_mcp/broker/service/core.py -
Test:
tests/wf_mcp/service/test_catalog.py -
Step 1: Add a direct hydration test
Add this test:
def test_source_catalog_hydrates_connection_source_from_snapshot_directly() -> None:
root = local_temp_root() / "source_catalog_hydrate_direct"
shutil.rmtree(root, ignore_errors=True)
first_service = WfMcpService(store=FileStore(root))
first_service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
first_service.register_adapter("demo", FakeAdapter())
asyncio.run(first_service.refresh_connection_catalog("demo.personal"))
second_service = WfMcpService(store=FileStore(root))
second_service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
specs = second_service.source_catalog.capability_sources[
"demo.personal"
].capabilities.node_specs
assert "demo.personal.echo_tool" in specs
- Step 2: Run the direct hydration test
Run:
uv run pytest tests/wf_mcp/service/test_catalog.py::test_source_catalog_hydrates_connection_source_from_snapshot_directly -q
Expected: pass before moving code, because this is still routed through WfMcpService; this is a characterization test that must stay green.
- Step 3: Move hydration helpers into SourceCatalogService
Move these methods to SourceCatalogService:
def hydrate_connection_source_from_snapshot(
self,
connection: ConnectionConfig,
) -> None: ...
def spec_from_snapshot_entry(
self,
entry: CatalogNodeEntry,
) -> NodeSpec[Any, Any]: ...
def get_qualified_spec(self, qualified_name: str) -> NodeSpec[Any, Any]: ...
Rename the public component methods without leading underscores because this is now the component API.
Inside spec_from_snapshot_entry, preserve the async tool wrapper exactly:
async def invoke_tool(payload: BaseModel) -> NodeReturn[BaseModel]:
connection = self.connection_lookup(entry.connection_id)
auth = self.load_auth(entry.connection_id)
result = await self.tool_executor_for(connection).call_tool(
connection,
auth,
entry.local_name,
payload.model_dump(exclude_unset=True),
)
return NodeReturn(
outcome=result.outcome,
output=output_model.model_validate(result.output),
)
Add the needed imports in source_catalog.py:
from pydantic import BaseModel
from wf_platform import CapabilityBuckets, SourcePermissions, SourceVisibility
from ...connections import qualify_node_name
from ...workflow.wrappers import _model_from_schema
from .specs import get_qualified_spec, qualify_spec
- Step 4: Update WfMcpService delegate call sites
In WfMcpService.register_connection, replace:
self._hydrate_connection_source_from_snapshot(connection)
with:
self.source_catalog.hydrate_connection_source_from_snapshot(connection)
In sync_connections_from_config, replace:
self._hydrate_connection_source_from_snapshot(connection)
with:
self.source_catalog.hydrate_connection_source_from_snapshot(connection)
Replace _get_qualified_spec body:
def _get_qualified_spec(self, qualified_name: str) -> NodeSpec[Any, Any]:
return self.source_catalog.get_qualified_spec(qualified_name)
Remove _hydrate_connection_source_from_snapshot and _spec_from_snapshot_entry from core.py.
- Step 5: Run hydration and runtime regression tests
Run:
uv run pytest tests/wf_mcp/service/test_catalog.py::test_service_hydrates_planner_specs_from_stored_catalog tests/wf_mcp/service/test_catalog.py::test_source_catalog_hydrates_connection_source_from_snapshot_directly -q
Expected: both pass. The first test proves hydrated specs still execute through runtime.
- Step 6: Run ruff
Run:
uv run ruff check src/wf_mcp/broker/service/core.py src/wf_mcp/broker/service/source_catalog.py tests/wf_mcp/service/test_catalog.py
Expected: pass.
Task 5: Move Spec Registration Into SourceCatalogService
Files:
-
Modify:
src/wf_mcp/broker/service/source_catalog.py -
Modify:
src/wf_mcp/broker/service/core.py -
Test:
tests/wf_mcp/service/test_catalog.py -
Step 1: Add a direct register-specs test
Add this test:
def test_source_catalog_register_specs_replaces_discovered_specs_directly() -> None:
connection = ConnectionConfig(
id="demo.personal",
server="demo",
account="personal",
)
def unused_tool_executor(connection: ConnectionConfig):
raise AssertionError("tool executor should not be used by spec registration")
catalog = SourceCatalogService(
store=FileStore(local_temp_root() / "source_catalog_register_specs"),
connection_lookup=lambda connection_id: connection,
connection_list_enabled=lambda: [connection],
connection_list_all=lambda: [connection],
tool_executor_for=unused_tool_executor,
load_auth=lambda connection_id: None,
emit_event=lambda event: None,
)
catalog.hydrate_connection_source_from_snapshot(connection)
catalog.register_specs("demo.personal", echo_tool)
specs = catalog.capability_sources["demo.personal"].capabilities.node_specs
assert set(specs) == {"demo.personal.echo_tool"}
assert catalog.store.load_catalog("demo.personal") is not None
- Step 2: Run the direct test and verify it fails
Run:
uv run pytest tests/wf_mcp/service/test_catalog.py::test_source_catalog_register_specs_replaces_discovered_specs_directly -q
Expected: fail because SourceCatalogService.register_specs does not exist.
- Step 3: Move register_specs into SourceCatalogService
Move the logic from WfMcpService.register_specs to SourceCatalogService.register_specs:
def register_specs(
self,
connection_id: str,
*specs: NodeSpec[Any, Any],
max_age_seconds: int | None = None,
emit_change_events: bool = True,
record_catalog_change_events: Callable[
[str, CatalogSnapshot, str],
None,
]
| None = None,
) -> CatalogSnapshot:
self.connection_lookup(connection_id)
qualified_specs = {
qualify_node_name(connection_id, spec.name): qualify_spec(
connection_id, spec
)
for spec in specs
}
existing_source = self.capability_sources.get(connection_id)
if existing_source is not None:
existing_source.capabilities.node_specs = qualified_specs
else:
self.register_capability_source(
CapabilitySource(
id=connection_id,
kind="connection",
capabilities=CapabilityBuckets(node_specs=qualified_specs),
enabled=self.connection_lookup(connection_id).enabled,
visibility=SourceVisibility(
planner=True,
mcp_client=True,
admin_dashboard=True,
),
permissions=SourcePermissions(calls_upstream=True),
description=(
f"Specs discovered or registered for {connection_id}."
),
)
)
snapshot = snapshot_from_specs(
connection_id,
specs=qualified_specs,
fetched_at_epoch_ms=int(time.time() * 1000),
max_age_seconds=max_age_seconds or self.default_catalog_max_age_seconds,
)
self.store.save_catalog(snapshot)
self.emit_event(
make_event(
"specs_registered",
connection_id=connection_id,
payload={"node_count": len(qualified_specs)},
)
)
if emit_change_events and record_catalog_change_events is not None:
record_catalog_change_events(connection_id, snapshot, "specs_registered")
return snapshot
Add imports:
from ...events import make_event
from ..catalog import CombinedCatalog, snapshot_from_specs
- Step 4: Delegate WfMcpService.register_specs
Replace WfMcpService.register_specs body with:
self.source_catalog.register_specs(
connection_id,
*specs,
max_age_seconds=max_age_seconds,
emit_change_events=emit_change_events,
record_catalog_change_events=lambda source_id, snapshot, reason: (
self._record_catalog_change_events(
source_id,
snapshot,
reason=reason,
)
),
)
Keep the public method signature unchanged.
- Step 5: Run spec registration tests
Run:
uv run pytest tests/wf_mcp/service/test_catalog.py::test_service_builds_namespaced_catalog tests/wf_mcp/service/test_catalog.py::test_source_catalog_register_specs_replaces_discovered_specs_directly -q
Expected: both pass.
- Step 6: Run ruff
Run:
uv run ruff check src/wf_mcp/broker/service/core.py src/wf_mcp/broker/service/source_catalog.py tests/wf_mcp/service/test_catalog.py
Expected: pass.
Task 6: Move Local Documentation Lookup
Files:
-
Modify:
src/wf_mcp/broker/service/source_catalog.py -
Modify:
src/wf_mcp/broker/service/core.py -
Test:
tests/wf_mcp/service/test_catalog.py -
Step 1: Add direct local docs lookup tests
Add this test:
def test_source_catalog_finds_local_documentation_resource_directly() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "source_local_docs"))
resource = service.source_catalog.local_documentation_resource(
"wf.docs.workflow_lifecycle"
)
assert resource is not None
assert resource.uri == "wf://docs/workflow-lifecycle.md"
If the exact docs key differs in this repository, use the actual key from service.capability_sources["wf.docs"].capabilities.resources and assert against that exact key. Do not make the test search for “any resource”; it must prove lookup by qualified name.
- Step 2: Run the direct test and verify it fails
Run:
uv run pytest tests/wf_mcp/service/test_catalog.py::test_source_catalog_finds_local_documentation_resource_directly -q
Expected: fail because local_documentation_resource does not exist, or because the guessed key is wrong. If the key is wrong, inspect service.capability_sources["wf.docs"].capabilities.resources.keys() and update the test to the actual key.
- Step 3: Move local docs helpers into SourceCatalogService
Move these methods from WfMcpService and rename them:
def local_documentation_resource(
self,
qualified_name: str,
) -> DocumentationResource | None:
"""Return a local docs resource from capability sources by qualified name."""
for source in self.capability_sources.values():
resource = source.capabilities.resources.get(qualified_name)
if isinstance(resource, DocumentationResource):
return resource
return None
def local_documentation_prompt(
self,
qualified_name: str,
) -> DocumentationPrompt | None:
"""Return a local docs prompt from capability sources by qualified name."""
for source in self.capability_sources.values():
prompt = source.capabilities.prompts.get(qualified_name)
if isinstance(prompt, DocumentationPrompt):
return prompt
return None
- Step 4: Delegate WfMcpService read/render call sites
In WfMcpService.read_resource, replace:
local_resource = self._local_documentation_resource(qualified_name)
with:
local_resource = self.source_catalog.local_documentation_resource(
qualified_name
)
In WfMcpService.render_prompt, replace:
local_prompt = self._local_documentation_prompt(qualified_name)
with:
local_prompt = self.source_catalog.local_documentation_prompt(qualified_name)
Remove _local_documentation_resource and _local_documentation_prompt from core.py.
- Step 5: Run docs/resource tests
Run:
uv run pytest tests/wf_mcp/service/test_catalog.py::test_source_catalog_finds_local_documentation_resource_directly tests/wf_mcp/test_broker_server.py -q
Expected: pass.
- Step 6: Run ruff
Run:
uv run ruff check src/wf_mcp/broker/service/core.py src/wf_mcp/broker/service/source_catalog.py tests/wf_mcp/service/test_catalog.py
Expected: pass.
Task 7: Update Workflow Operation Context and Live Checks to Use the Extracted Service
Files:
-
Modify:
src/wf_mcp/broker/service/workflow_operation_context.py -
Modify:
src/wf_mcp/broker/service/workflow_live_checks.py -
Test:
tests/wf_api/test_operation_context.py -
Test:
tests/wf_mcp/workflow_surface/test_deployments.py -
Step 1: Add a context identity test
In tests/wf_api/test_operation_context.py, add:
def test_context_uses_source_catalog_mapping() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "context_sources"))
context = context_from_service(service)
assert context.capability_sources is service.source_catalog.capability_sources
Use the existing imports/helpers in that file. If WfMcpService, FileStore, or local_temp_root are not imported there, add the same imports used by neighboring tests.
- Step 2: Run the context test
Run:
uv run pytest tests/wf_api/test_operation_context.py::test_context_uses_source_catalog_mapping -q
Expected: pass after earlier tasks.
- Step 3: Update operation context adapter
In src/wf_mcp/broker/service/workflow_operation_context.py, update:
@property
def capability_sources(self):
return self.service.source_catalog.capability_sources
def get_qualified_spec(self, qualified_name: str) -> object:
return self.service.source_catalog.get_qualified_spec(qualified_name)
In context_from_service, keep:
capability_sources=specs.capability_sources,
- Step 4: Update live checks to read source catalog explicitly
In src/wf_mcp/broker/service/workflow_live_checks.py, replace:
source = service.capability_sources.get(source_id)
with:
source = service.source_catalog.capability_sources.get(source_id)
This makes the live-check dependency on source registry explicit while still leaving connection/adapters/auth on WfMcpService.
- Step 5: Run deployment live-check tests
Run:
uv run pytest tests/wf_mcp/workflow_surface/test_deployments.py -q
Expected: pass.
- Step 6: Run ruff
Run:
uv run ruff check src/wf_mcp/broker/service/workflow_operation_context.py src/wf_mcp/broker/service/workflow_live_checks.py tests/wf_api/test_operation_context.py
Expected: pass.
Task 8: Clean Imports, Docs, and Verify Full Behavior
Files:
-
Modify:
src/wf_mcp/broker/service/core.py -
Modify:
src/wf_mcp/broker/service/source_catalog.py -
Modify:
docs/current_roadmap.md -
Modify:
docs/superpowers/research/2026-06-01-wf-api-extraction-map.mdif present and stale. -
Step 1: Remove stale imports from core.py
After all moves, src/wf_mcp/broker/service/core.py should no longer import items used only by source_catalog.py, such as:
from pydantic import BaseModel
from wf_platform import DocumentationPrompt, DocumentationResource, page_items
from ...workflow.wrappers import _model_from_schema
from ..catalog import CombinedCatalog
from .specs import get_qualified_spec, qualify_spec
Do not remove imports that are still used by refresh_connection_catalog, runtime preparation, or public method annotations.
- Step 2: Add roadmap status note
In docs/current_roadmap.md, add or update the wf_api/service extraction section with:
- `WfMcpService` is being reduced into injected implementation services. Source
registry and catalog projection now live in `SourceCatalogService`; the old
service methods remain as compatibility delegates for MCP broker callers.
- Step 3: Update extraction map if it names source ownership
If docs/superpowers/research/2026-06-01-wf-api-extraction-map.md says WfMcpService owns source/catalog state directly, change it to:
Source/catalog ownership is now split: `WfMcpService` coordinates broker runtime
state, while `SourceCatalogService` owns capability source maps, planner catalog
projection, snapshot hydration, and local docs lookup.
If the file does not contain stale source/catalog ownership wording, do not edit it.
- Step 4: Run focused source/catalog/workflow checks
Run:
uv run pytest tests/wf_mcp/service/test_catalog.py tests/wf_api/test_operation_context.py tests/wf_mcp/workflow_surface/test_deployments.py tests/wf_mcp/workflow_surface/test_runs.py -q
Expected: all selected tests pass.
- Step 5: Run full verification
Run:
uv run pytest -q
uv run ruff check src/wf_mcp/broker/service src/wf_api tests/wf_mcp/service tests/wf_api
uv run ruff format --check src/wf_mcp/broker/service src/wf_api tests/wf_mcp/service tests/wf_api docs/current_roadmap.md
uv run basedpyright --level error
Expected:
- pytest passes.
- ruff check passes.
- ruff format check passes.
- basedpyright reports
0 errors. A workspace enumeration timeout warning may still make the command exit nonzero in this repository; record the exact output if that happens.
Non-Goals and Follow-Up Slices
This plan intentionally leaves WfMcpService as the public broker coordinator. After this slice, the next reductions should be separate plans:
- Connection/runtime service extraction: move connection registry, adapter lookup, auth loading, and upstream I/O calls into a transport runtime service.
- Workflow runtime runner extraction: move
compile_plan,_prepare_workflow_runtime,run_workflow_from_plan, andresume_workflow_from_planinto a dedicated runtime implementation. - Event recorder extraction: make event emission a dependency instead of private
_record_eventcalls. - Resource/prompt API extraction: decide whether
read_resourceandrender_promptbelong in a neutral API façade or stay MCP-admin-only.
Self-Review
- Spec coverage: This plan extracts source/catalog state while preserving old
WfMcpServicemethods and explicitly defers runtime/transport moves. - Placeholder scan: No placeholder implementation tasks are left. The one docs lookup test allows correcting an exact key after inspecting actual source keys because the source key can vary with current docs registration.
- Type consistency:
SourceCatalogServiceconsistently uses callback dependencies for connection lookup, connection listing, tool executor lookup, auth loading, and event emission.WfMcpService.capability_sourcesremains a compatibility property.