Files
lda-wf/docs/historical/superpowers/plans/2026-05-09-capability-sources.md
T

798 lines
23 KiB
Markdown

# 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`: store `capability_sources`, derive node-spec resolution from them, keep `spec_sources` and `specs_by_connection` as compatibility views if needed.
- Modify `src/wf_mcp/broker/service/builtins.py`: register `wf.std` stdlib specs and `wf.mcp` runtime specs through capability sources.
- Modify `src/wf_mcp/broker/tools.py`: project broker MCP tools from `wf.admin` source definitions.
- Create `src/wf_mcp/broker/admin_capabilities.py`: one reusable definition of broker/admin tool capabilities.
- Modify `src/wf_mcp/transparent_proxy/admin.py` and `src/wf_mcp/transparent_proxy/runtime.py`: project proxy admin tools from the same `wf.admin` capability definitions when admin MCP exposure is enabled.
- Modify `src/wf_mcp/shared/names.py`: move admin namespace toward `wf.admin` and use `LdaNamespace` where dotted names should be preserved.
- Modify `tests/wf_mcp/test_service.py`, `tests/wf_mcp/test_broker_server.py`, and `tests/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`:
```python
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:
```powershell
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`:
```python
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 `SpecSource` as compatibility wrapper**
Modify `src/wf_mcp/broker/service/sources.py` so existing code can still import `SpecSource` while new code can move to `CapabilitySource`:
```python
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:
```powershell
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:
```python
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:
```powershell
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:
```python
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:
```python
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:
```powershell
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.std` inventory test**
Add:
```python
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:
```powershell
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`:
```python
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:
```powershell
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:
```python
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:
```powershell
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`:
```python
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:
```python
from ..admin_capabilities import admin_source
...
self.register_capability_source(admin_source())
```
- [ ] **Step 5: Run focused tests**
Run:
```powershell
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`:
```python
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:
```powershell
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:
```text
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`:
```python
# 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:
```powershell
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`:
```python
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:
```powershell
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`:
```python
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`:
```python
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:
```python
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:
```python
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:
```powershell
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:
```markdown
## 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:
```powershell
uv run --with pytest pytest -q
```
Expected: all tests pass, live-only tests may skip when env is absent.
- [ ] **Step 3: Run ruff**
Run:
```powershell
uv run ruff check src tests examples main.py
```
Expected: `All checks passed!`
- [ ] **Step 4: Run basedpyright errors**
Run:
```powershell
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.std` migration is covered by Task 3.
- `wf.admin` privileged 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 keeps `wf.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`, and
`SourcePermissions` are introduced once and reused consistently.
- Compatibility views are named `spec_sources` and `specs_by_connection`.