23 KiB
Capability Sources 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: Replace the ad hoc broker/proxy/admin/stdlib split with a source registry that can project workflow node specs, MCP tools, prompts, and resources to the right surfaces.
Architecture: Add CapabilitySource as the source registry model, keep SpecSource behavior as a compatibility projection while the rest of the system migrates, then move stdlib and admin capabilities into explicit wf.std, wf.mcp, and wf.admin sources. Broker/proxy servers should project capabilities from sources instead of defining separate duplicate admin tools.
Tech Stack: Python 3.14, dataclasses, Pydantic models already used by node specs, FastMCP/MCP server decorators, pytest, ruff, basedpyright.
File Structure
- Create
src/wf_mcp/broker/service/capability_sources.py: canonical source model, visibility flags, permission flags, capability buckets, projection helpers. - Modify
src/wf_mcp/broker/service/sources.py: either delegate to the new model or become a small compatibility import. - Modify
src/wf_mcp/broker/service/core.py: storecapability_sources, derive node-spec resolution from them, keepspec_sourcesandspecs_by_connectionas compatibility views if needed. - Modify
src/wf_mcp/broker/service/builtins.py: registerwf.stdstdlib specs andwf.mcpruntime specs through capability sources. - Modify
src/wf_mcp/broker/tools.py: project broker MCP tools fromwf.adminsource definitions. - Create
src/wf_mcp/broker/admin_capabilities.py: one reusable definition of broker/admin tool capabilities. - Modify
src/wf_mcp/transparent_proxy/admin.pyandsrc/wf_mcp/transparent_proxy/runtime.py: project proxy admin tools from the samewf.admincapability definitions when admin MCP exposure is enabled. - Modify
src/wf_mcp/shared/names.py: move admin namespace towardwf.adminand useLdaNamespacewhere dotted names should be preserved. - Modify
tests/wf_mcp/test_service.py,tests/wf_mcp/test_broker_server.py, andtests/wf_mcp/test_transparent_proxy.py: prove projections and defaults. - Modify
docs/wf_mcp_capability_sources.md: update once implementation names are final.
Task 1: Add Canonical Capability Source Model
Files:
-
Create:
src/wf_mcp/broker/service/capability_sources.py -
Modify:
src/wf_mcp/broker/service/sources.py -
Test:
tests/wf_mcp/test_service.py -
Step 1: Write failing tests for source visibility and buckets
Add tests in tests/wf_mcp/test_service.py:
def test_service_sources_have_visibility_and_capability_buckets() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "source_shape_store"))
std_source = service.capability_sources["wf.std"]
mcp_source = service.capability_sources["wf.mcp"]
assert std_source.id == "wf.std"
assert std_source.kind == "system"
assert std_source.visibility.planner is True
assert std_source.visibility.mcp_client is True
assert std_source.visibility.admin_dashboard is True
assert "wf.std.runtime_error" in std_source.capabilities.node_specs
assert std_source.capabilities.tools == {}
assert mcp_source.id == "wf.mcp"
assert mcp_source.visibility.planner is True
assert mcp_source.permissions.calls_upstream is True
assert "wf.mcp.call_tool" in mcp_source.capabilities.node_specs
- Step 2: Run the focused failing test
Run:
uv run --with pytest pytest tests\wf_mcp\test_service.py::test_service_sources_have_visibility_and_capability_buckets -q
Expected: fail because capability_sources does not exist.
- Step 3: Add the source model
Create src/wf_mcp/broker/service/capability_sources.py:
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Literal
from wf_authoring import NodeSpec
SourceKind = Literal["system", "connection"]
@dataclass(frozen=True, slots=True)
class SourceVisibility:
planner: bool = False
mcp_client: bool = False
admin_dashboard: bool = True
@dataclass(frozen=True, slots=True)
class SourcePermissions:
safe_for_workflow: bool = False
calls_upstream: bool = False
mutates_config: bool = False
mutates_auth: bool = False
@dataclass(slots=True)
class CapabilityBuckets:
tools: dict[str, Any] = field(default_factory=dict)
node_specs: dict[str, NodeSpec[Any, Any]] = field(default_factory=dict)
prompts: dict[str, Any] = field(default_factory=dict)
resources: dict[str, Any] = field(default_factory=dict)
@dataclass(slots=True)
class CapabilitySource:
id: str
kind: SourceKind
capabilities: CapabilityBuckets = field(default_factory=CapabilityBuckets)
enabled: bool = True
visibility: SourceVisibility = field(default_factory=SourceVisibility)
permissions: SourcePermissions = field(default_factory=SourcePermissions)
description: str | None = None
def as_status(self) -> dict[str, Any]:
return {
"id": self.id,
"kind": self.kind,
"enabled": self.enabled,
"visibility": {
"planner": self.visibility.planner,
"mcp_client": self.visibility.mcp_client,
"admin_dashboard": self.visibility.admin_dashboard,
},
"permissions": {
"safe_for_workflow": self.permissions.safe_for_workflow,
"calls_upstream": self.permissions.calls_upstream,
"mutates_config": self.permissions.mutates_config,
"mutates_auth": self.permissions.mutates_auth,
},
"description": self.description,
"tool_count": len(self.capabilities.tools),
"node_spec_count": len(self.capabilities.node_specs),
"prompt_count": len(self.capabilities.prompts),
"resource_count": len(self.capabilities.resources),
}
- Step 4: Keep
SpecSourceas compatibility wrapper
Modify src/wf_mcp/broker/service/sources.py so existing code can still import SpecSource while new code can move to CapabilitySource:
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from wf_authoring import NodeSpec
from .capability_sources import (
CapabilityBuckets,
CapabilitySource,
SourceKind,
SourcePermissions,
SourceVisibility,
)
@dataclass(slots=True)
class SpecSource:
id: str
kind: SourceKind
specs: dict[str, NodeSpec[Any, Any]] = field(default_factory=dict)
visible: bool = True
description: str | None = None
def as_capability_source(self) -> CapabilitySource:
return CapabilitySource(
id=self.id,
kind=self.kind,
capabilities=CapabilityBuckets(node_specs=self.specs),
visibility=SourceVisibility(
planner=self.visible,
mcp_client=False,
admin_dashboard=True,
),
permissions=SourcePermissions(safe_for_workflow=self.kind == "system"),
description=self.description,
)
def as_status(self) -> dict[str, Any]:
return self.as_capability_source().as_status()
- Step 5: Run focused test
Run:
uv run --with pytest pytest tests\wf_mcp\test_service.py::test_service_sources_have_visibility_and_capability_buckets -q
Expected: fail until service stores capability_sources; pass after Task 2.
Task 2: Make WfMcpService Store Capability Sources
Files:
-
Modify:
src/wf_mcp/broker/service/core.py -
Modify:
src/wf_mcp/broker/service/specs.py -
Test:
tests/wf_mcp/test_service.py -
Step 1: Add failing service projection tests
Add:
def test_service_spec_views_are_derived_from_capability_sources() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "source_view_store"))
assert "wf.std" in service.capability_sources
assert "wf.std" in service.spec_sources
assert "wf.std" in service.specs_by_connection
assert (
service.specs_by_connection["wf.std"]["wf.std.runtime_error"]
is service.capability_sources["wf.std"].capabilities.node_specs[
"wf.std.runtime_error"
]
)
- Step 2: Run focused test
Run:
uv run --with pytest pytest tests\wf_mcp\test_service.py::test_service_spec_views_are_derived_from_capability_sources -q
Expected: fail because compatibility views are not derived from capability sources.
- Step 3: Update service fields and registration
In src/wf_mcp/broker/service/core.py, replace the stored spec_sources field with canonical capability storage and add derived views:
capability_sources: dict[str, CapabilitySource] = field(default_factory=dict)
@property
def spec_sources(self) -> dict[str, SpecSource]:
return {
source.id: SpecSource(
id=source.id,
kind=source.kind,
specs=source.capabilities.node_specs,
visible=source.enabled and source.visibility.planner,
description=source.description,
)
for source in self.capability_sources.values()
if source.capabilities.node_specs
}
@property
def specs_by_connection(self) -> dict[str, dict[str, NodeSpec[Any, Any]]]:
return {
source.id: source.capabilities.node_specs
for source in self.capability_sources.values()
if source.capabilities.node_specs
}
def register_capability_source(self, source: CapabilitySource) -> None:
self.capability_sources[source.id] = source
def register_spec_source(self, source: SpecSource) -> None:
self.register_capability_source(source.as_capability_source())
- Step 4: Update spec resolution
In src/wf_mcp/broker/service/specs.py, make get_qualified_spec read capability sources:
from collections.abc import Mapping
from .capability_sources import CapabilitySource
def get_qualified_spec(
sources: Mapping[str, CapabilitySource],
qualified_name: str,
) -> NodeSpec[Any, Any]:
source_id, _ = qualified_name.rsplit(".", 1)
source = sources.get(source_id)
if (
source is None
or not source.enabled
or qualified_name not in source.capabilities.node_specs
):
raise KeyError(f"unknown qualified node {qualified_name!r}")
return source.capabilities.node_specs[qualified_name]
- Step 5: Run focused service tests
Run:
uv run --with pytest pytest tests\wf_mcp\test_service.py -q
Expected: pass.
Task 3: Move All Authoring Ops Into wf.std
Files:
-
Modify:
src/wf_mcp/broker/service/builtins.py -
Test:
tests/wf_mcp/test_service.py -
Step 1: Add failing
wf.stdinventory test
Add:
def test_wf_std_source_contains_authoring_ops() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "stdlib_source_store"))
specs = service.capability_sources["wf.std"].capabilities.node_specs
expected = {
"wf.std.coalesce",
"wf.std.default_if_none",
"wf.std.constant",
"wf.std.pick_key",
"wf.std.truthy",
"wf.std.runtime_error",
"wf.std.first_item",
"wf.std.first_item_or_none",
"wf.std.first_item_maybe",
"wf.std.last_item",
"wf.std.last_item_or_none",
"wf.std.length",
"wf.std.is_empty",
}
assert expected <= set(specs)
- Step 2: Run focused test
Run:
uv run --with pytest pytest tests\wf_mcp\test_service.py::test_wf_std_source_contains_authoring_ops -q
Expected: fail because only runtime_error is currently registered.
- Step 3: Register stdlib ops
In src/wf_mcp/broker/service/builtins.py, import wf_authoring.ops symbols and qualify each one under wf.std:
from wf_authoring import (
coalesce,
constant,
default_if_none,
first_item,
first_item_maybe,
first_item_or_none,
is_empty,
last_item,
last_item_or_none,
length,
pick_key,
runtime_error,
truthy,
)
def builtin_specs() -> dict[str, NodeSpec[Any, Any]]:
specs = [
coalesce,
default_if_none,
constant,
pick_key,
truthy,
runtime_error,
first_item,
first_item_or_none,
first_item_maybe,
last_item,
last_item_or_none,
length,
is_empty,
]
qualified_specs = [
qualify_spec(BUILTIN_CONNECTION_ID, _strip_authoring_prefix(spec))
for spec in specs
]
return {spec.name: spec for spec in qualified_specs}
def _strip_authoring_prefix(spec: NodeSpec[Any, Any]) -> NodeSpec[Any, Any]:
name = spec.name.removeprefix("authoring.")
return NodeSpec(
name=name,
input_model=spec.input_model,
output_model=spec.output_model,
outcomes=spec.outcomes,
fn=spec.fn,
description=spec.description,
is_async=spec.is_async,
accepts_context=spec.accepts_context,
input_schema_contract=spec.input_schema_contract,
output_schema_contract=spec.output_schema_contract,
)
- Step 4: Run service tests
Run:
uv run --with pytest pytest tests\wf_mcp\test_service.py -q
Expected: pass.
Task 4: Add wf.admin Source Without MCP Exposure
Files:
-
Create:
src/wf_mcp/broker/admin_capabilities.py -
Modify:
src/wf_mcp/broker/service/core.py -
Test:
tests/wf_mcp/test_service.py -
Step 1: Add failing admin source test
Add:
def test_wf_admin_source_exists_but_is_not_planner_visible() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "admin_source_store"))
source = service.capability_sources["wf.admin"]
assert source.kind == "system"
assert source.visibility.planner is False
assert source.visibility.mcp_client is False
assert source.visibility.admin_dashboard is True
assert source.permissions.mutates_config is True
assert "wf.admin.list_sources" in source.capabilities.tools
assert "wf.admin.disable_source" in source.capabilities.tools
assert "wf.admin" not in service.get_planner_catalog().snapshots
- Step 2: Run focused test
Run:
uv run --with pytest pytest tests\wf_mcp\test_service.py::test_wf_admin_source_exists_but_is_not_planner_visible -q
Expected: fail because wf.admin does not exist.
- Step 3: Define admin capability objects
Create src/wf_mcp/broker/admin_capabilities.py:
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Awaitable, Callable
from .service.capability_sources import (
CapabilityBuckets,
CapabilitySource,
SourcePermissions,
SourceVisibility,
)
ADMIN_SOURCE_ID = "wf.admin"
@dataclass(frozen=True, slots=True)
class AdminTool:
name: str
description: str
handler_name: str
mutates_config: bool = False
mutates_auth: bool = False
def admin_source() -> CapabilitySource:
tools: dict[str, AdminTool] = {
"wf.admin.list_sources": AdminTool(
name="wf.admin.list_sources",
description="List broker capability sources.",
handler_name="list_sources",
),
"wf.admin.disable_source": AdminTool(
name="wf.admin.disable_source",
description="Disable a capability source.",
handler_name="disable_source",
mutates_config=True,
),
"wf.admin.enable_source": AdminTool(
name="wf.admin.enable_source",
description="Enable a capability source.",
handler_name="enable_source",
mutates_config=True,
),
}
return CapabilitySource(
id=ADMIN_SOURCE_ID,
kind="system",
capabilities=CapabilityBuckets(tools=tools),
visibility=SourceVisibility(
planner=False,
mcp_client=False,
admin_dashboard=True,
),
permissions=SourcePermissions(
safe_for_workflow=False,
calls_upstream=False,
mutates_config=True,
mutates_auth=True,
),
description="Privileged broker administration capabilities.",
)
- Step 4: Install admin source
In WfMcpService.__post_init__, register admin_source() after builtin sources:
from ..admin_capabilities import admin_source
...
self.register_capability_source(admin_source())
- Step 5: Run focused tests
Run:
uv run --with pytest pytest tests\wf_mcp\test_service.py -q
Expected: pass.
Task 5: Project Broker Admin Tools From wf.admin
Files:
-
Modify:
src/wf_mcp/broker/tools.py -
Test:
tests/wf_mcp/test_broker_server.py -
Step 1: Add test proving default broker exposes current public tools but has source-backed metadata
Add to tests/wf_mcp/test_broker_server.py:
def test_broker_admin_tools_are_backed_by_wf_admin_source() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "broker_admin_source"))
server = create_broker_server(service)
tools = asyncio.run(server.list_tools())
tool_names = {tool.name for tool in tools}
assert "list_spec_sources" in tool_names
assert "get_planner_catalog" in tool_names
assert "wf.admin.list_sources" in service.capability_sources[
"wf.admin"
].capabilities.tools
- Step 2: Run focused broker test
Run:
uv run --with pytest pytest tests\wf_mcp\test_broker_server.py::test_broker_admin_tools_are_backed_by_wf_admin_source -q
Expected: pass after Task 4; fail before Task 4.
- Step 3: Keep current tool names as compatibility exports
Do not rename public broker MCP tools in this task. Keep:
list_connections
get_connection_statuses
refresh_connection_catalog
get_catalog
get_planner_catalog
list_spec_sources
read_broker_resource
render_broker_prompt
invoke_broker_method
call_broker_tool
get_broker_events
Add comments in src/wf_mcp/broker/tools.py:
# These MCP tool names are compatibility exports. Their capability metadata
# belongs to the wf.admin source; future admin-enabled servers can project
# dotted wf.admin.* names from that source.
- Step 4: Run focused broker tests
Run:
uv run --with pytest pytest tests\wf_mcp\test_broker_server.py -q
Expected: pass.
Task 6: Normalize Transparent Proxy Admin Naming Strategy
Files:
-
Modify:
src/wf_mcp/shared/names.py -
Modify:
src/wf_mcp/transparent_proxy/runtime.py -
Test:
tests/wf_mcp/test_names.py -
Test:
tests/wf_mcp/test_transparent_proxy.py -
Step 1: Add naming tests for admin namespace
Add to tests/wf_mcp/test_names.py:
def test_admin_namespace_is_distinct_from_wf_mcp_runtime_source() -> None:
assert ADMIN_NAMESPACE == "wf.admin"
assert is_admin_tool_name("wf.admin.list_connections") is True
assert is_admin_tool_name("wf.mcp.call_tool") is False
- Step 2: Run focused naming test
Run:
uv run --with pytest pytest tests\wf_mcp\test_names.py::test_admin_namespace_is_distinct_from_wf_mcp_runtime_source -q
Expected: fail because ADMIN_NAMESPACE is currently wf.mcp.
- Step 3: Update namespace constants
In src/wf_mcp/shared/names.py:
ADMIN_NAMESPACE = "wf.admin"
def is_admin_tool_name(proxy_name: str) -> bool:
return proxy_name.startswith(f"{ADMIN_NAMESPACE}.") or proxy_name.startswith(
f"{ADMIN_NAMESPACE}_"
)
Use LdaNamespace(ADMIN_NAMESPACE) in src/wf_mcp/transparent_proxy/runtime.py:
from ..shared.names import ADMIN_NAMESPACE, LdaNamespace
...
admin.add_transform(LdaNamespace(ADMIN_NAMESPACE))
- Step 4: Update transparent proxy tests
In tests/wf_mcp/test_transparent_proxy.py, update expected admin names from wf.mcp_* to dotted wf.admin.* if LdaNamespace preserves dotted names:
assert "wf.admin.list_connections" in names
assert "wf.admin.get_connection_statuses" in names
assert "wf.admin.list_proxy_tools" in names
assert "wf.admin.get_proxy_tool" in names
Call tools by the new names:
connections_result = await client.call_tool("wf.admin.list_connections")
proxy_tools_result = await client.call_tool("wf.admin.list_proxy_tools")
- Step 5: Run transparent proxy tests
Run:
uv run --with pytest pytest tests\wf_mcp\test_names.py tests\wf_mcp\test_transparent_proxy.py -q
Expected: pass if LdaNamespace preserves dotted names. If FastMCP still emits underscore names, keep wf.admin_* as compatibility and document that dotted projection needs a deeper transform.
Task 7: Update Docs And Full Verification
Files:
-
Modify:
docs/wf_mcp_capability_sources.md -
Modify:
docs/wf_mcp_architecture.md -
Test: full verification commands
-
Step 1: Update docs with implemented names
In docs/wf_mcp_capability_sources.md, update the migration section to mark implemented pieces:
## Implemented Shape
- `CapabilitySource` owns source metadata and capability buckets.
- `wf.std` owns workflow stdlib node specs.
- `wf.mcp` owns workflow MCP runtime node specs.
- `wf.admin` owns privileged admin capability metadata.
- Broker and proxy MCP tool projection remains compatibility-first while the
admin projection stabilizes.
- Step 2: Run full pytest
Run:
uv run --with pytest pytest -q
Expected: all tests pass, live-only tests may skip when env is absent.
- Step 3: Run ruff
Run:
uv run ruff check src tests examples main.py
Expected: All checks passed!
- Step 4: Run basedpyright errors
Run:
uv run basedpyright src tests examples main.py --level error
Expected: 0 errors.
Self-Review
Spec coverage:
- Capability source model is covered by Tasks 1 and 2.
wf.stdmigration is covered by Task 3.wf.adminprivileged source is covered by Task 4.- Broker projection is covered by Task 5.
- Transparent proxy naming and admin namespace separation is covered by Task 6.
- Documentation and verification are covered by Task 7.
Known deliberate scope limits:
- Task 5 keeps current broker MCP public tool names as compatibility exports.
- Task 6 attempts dotted transparent-proxy admin names via
LdaNamespace; if FastMCP still forces underscore naming, this plan keepswf.admin_*compatibility and defers deeper transform work. - Source enable/disable runtime behavior is not implemented in this plan beyond introducing
enabled; it should be the next plan after the registry shape lands.
Placeholder scan:
- No
TBD,TODO, or unspecified implementation steps remain. - Each task includes exact paths, test names, commands, and expected outcomes.
Type consistency:
CapabilitySource,CapabilityBuckets,SourceVisibility, andSourcePermissionsare introduced once and reused consistently.- Compatibility views are named
spec_sourcesandspecs_by_connection.