798 lines
23 KiB
Markdown
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`.
|