17 KiB
Source Admin API Surface 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: Add a protocol-neutral read-only source/admin surface in wf_api and make MCP admin source tools delegate through it.
Architecture: WorkflowApiSurface stays workflow-lifecycle-only. Source/admin operations get a sibling WorkflowSourceAdminSurface plus WorkflowSourceAdminApi implementation over WorkflowOperationContext.specs.capability_sources. The MCP admin handler becomes an adapter over this neutral API while connection/raw MCP methods remain MCP-broker-owned.
Tech Stack: Python 3.14, dataclasses, Protocols, Pydantic-backed platform models, pytest, ruff, basedpyright.
Current Findings
- Source catalog internals already moved out of the old god service into
src/wf_mcp/broker/service/source_catalog.py. wf_apidoes not currently have a source/admin API module.- MCP admin source tools still flow through:
wf_mcp.admin_surface.tools
-> BrokerAdminHandlers
-> WfMcpService.list_source_summaries / inspect_source
-> SourceCatalogService
WorkflowOperationContext.specs.capability_sourcesalready exposes the source inventorywf_apineeds for read-only source listing and inspection.
Scope
In scope:
list_sources(cursor=None, limit=50) -> dictinspect_source(source_id: str) -> dict- Protocol-neutral
WorkflowSourceAdminSurface - Local implementation
WorkflowSourceAdminApi - MCP
BrokerAdminHandlers.list_sources()and.inspect_source()delegate throughWorkflowSourceAdminApi - Focused tests proving payload compatibility with existing MCP source output
Out of scope for this slice:
- Adding/removing/updating sources
- Store-backed source registry
- Connection status, catalog refresh, raw method invocation
- JSON-RPC transport methods for source admin
- CLI
wf source ...commands
Those are follow-up slices once this neutral seam exists.
File Structure
-
Create
src/wf_api/source_admin.py- Owns
WorkflowSourceAdminApi. - Uses only
WorkflowOperationContextand platform source models. - Imports no
wf_mcp.
- Owns
-
Modify
src/wf_api/surface.py- Adds sibling protocol
WorkflowSourceAdminSurface. - Does not make
WorkflowApiSurfaceinherit it.
- Adds sibling protocol
-
Modify
src/wf_api/__init__.py- Exports
WorkflowSourceAdminApiandWorkflowSourceAdminSurface.
- Exports
-
Modify
src/wf_mcp/admin_surface/handlers/broker.py- Construct
WorkflowSourceAdminApi(context_from_service(service)). - Delegate
list_sourcesandinspect_sourceto it. - Keep connection/catalog/resource/raw methods unchanged.
- Make those two source methods async, matching the async MCP tool boundary.
- Construct
-
Modify
src/wf_mcp/admin_surface/tools.py- Await async source handler methods.
-
Create
tests/wf_api/test_source_admin_api.py- Direct neutral API tests.
-
Modify
tests/wf_mcp/test_admin_surface.py- Add adapter smoke coverage for the neutral source admin delegation.
-
Modify
docs/current_roadmap.md- Mark the read-only neutral source/admin seam as completed.
Task 1: Add failing wf_api source admin tests
Files:
-
Create:
tests/wf_api/test_source_admin_api.py -
Step 1: Write direct tests
Create tests/wf_api/test_source_admin_api.py:
from __future__ import annotations
from typing import Any
import pytest
from wf_api import WorkflowSourceAdminApi
from wf_api.operation_context import WorkflowOperationContext
from wf_authoring import NodeSpec
from wf_platform import (
CapabilityBuckets,
CapabilitySource,
SourcePermissions,
SourceVisibility,
)
class DummyEvents:
def record_event(self, event: object) -> None:
pass
def record_workflow_event(
self,
event_type: str,
*,
capability_id: str,
payload: dict[str, Any],
) -> None:
pass
class DummyRuntime:
async def run_workflow_from_plan(self, *args: Any, **kwargs: Any) -> object:
raise AssertionError("source admin tests must not run workflows")
async def resume_workflow_from_plan(
self,
*args: Any,
**kwargs: Any,
) -> object:
raise AssertionError("source admin tests must not resume workflows")
class StaticSpecProvider:
def __init__(self, sources: dict[str, CapabilitySource]) -> None:
self._sources = sources
@property
def capability_sources(self) -> dict[str, CapabilitySource]:
return self._sources
def get_qualified_spec(self, qualified_name: str) -> NodeSpec[Any, Any]:
raise KeyError(f"unknown capability {qualified_name!r}")
def _api(*sources: CapabilitySource) -> WorkflowSourceAdminApi:
provider = StaticSpecProvider({source.id: source for source in sources})
return WorkflowSourceAdminApi(
WorkflowOperationContext(
artifact_store=None,
draft_workspace_store=None,
run_store=None,
events=DummyEvents(),
specs=provider,
runtime=DummyRuntime(),
live_sources=None,
)
)
def _source(source_id: str, *, enabled: bool = True) -> CapabilitySource:
return CapabilitySource(
id=source_id,
kind="connection",
enabled=enabled,
capabilities=CapabilityBuckets(),
visibility=SourceVisibility(
planner=True,
mcp_client=True,
admin_dashboard=True,
),
permissions=SourcePermissions(calls_upstream=True),
description=f"{source_id} source",
)
@pytest.mark.asyncio
async def test_source_admin_lists_compact_sources_in_id_order() -> None:
api = _api(_source("zeta.personal"), _source("alpha.personal", enabled=False))
payload = await api.list_sources(limit=10)
assert payload["total"] == 2
assert payload["next_cursor"] is None
assert [source["id"] for source in payload["sources"]] == [
"alpha.personal",
"zeta.personal",
]
assert payload["sources"][0]["enabled"] is False
assert payload["sources"][1]["description"] == "zeta.personal source"
@pytest.mark.asyncio
async def test_source_admin_pages_sources() -> None:
api = _api(_source("a"), _source("b"), _source("c"))
first = await api.list_sources(limit=2)
second = await api.list_sources(cursor=first["next_cursor"], limit=2)
assert [source["id"] for source in first["sources"]] == ["a", "b"]
assert first["next_cursor"] == "2"
assert [source["id"] for source in second["sources"]] == ["c"]
assert second["next_cursor"] is None
@pytest.mark.asyncio
async def test_source_admin_inspects_full_source_inventory() -> None:
api = _api(_source("demo.personal"))
payload = await api.inspect_source(source_id="demo.personal")
assert payload["id"] == "demo.personal"
assert payload["kind"] == "connection"
assert payload["description"] == "demo.personal source"
assert payload["visibility"]["planner"] is True
assert payload["permissions"]["calls_upstream"] is True
@pytest.mark.asyncio
async def test_source_admin_inspect_unknown_source_raises_clear_key_error() -> None:
api = _api(_source("demo.personal"))
with pytest.raises(KeyError, match="unknown source 'missing.source'"):
await api.inspect_source(source_id="missing.source")
- Step 2: Run tests and verify failure
Run:
uv run pytest tests/wf_api/test_source_admin_api.py -q
Expected: FAIL because WorkflowSourceAdminApi is not exported yet.
Task 2: Implement WorkflowSourceAdminApi
Files:
-
Create:
src/wf_api/source_admin.py -
Modify:
src/wf_api/__init__.py -
Step 1: Add source admin API
Create src/wf_api/source_admin.py:
from __future__ import annotations
from typing import Any
from wf_platform import page_items
from .operation_context import WorkflowOperationContext
class WorkflowSourceAdminApi:
"""Read-only protocol-neutral source inventory operations.
This is a sibling to WorkflowApi, not part of WorkflowApiSurface, because
source administration is server/platform management rather than workflow
lifecycle execution.
"""
def __init__(self, context: WorkflowOperationContext) -> None:
self.context = context
async def list_sources(
self,
*,
cursor: str | None = None,
limit: int = 50,
) -> dict[str, Any]:
summaries = [
source.as_status().model_dump(mode="json")
for source in sorted(
self.context.specs.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,
}
async def inspect_source(self, *, source_id: str) -> dict[str, Any]:
try:
source = self.context.specs.capability_sources[source_id]
except KeyError as exc:
raise KeyError(f"unknown source {source_id!r}") from exc
return source.as_inventory().model_dump(mode="json")
- Step 2: Export from
wf_api
Modify src/wf_api/__init__.py:
from .source_admin import WorkflowSourceAdminApi
Add "WorkflowSourceAdminApi" to __all__.
- Step 3: Run direct tests
Run:
uv run pytest tests/wf_api/test_source_admin_api.py -q
Expected: PASS.
Task 3: Add sibling surface protocol
Files:
-
Modify:
src/wf_api/surface.py -
Modify:
src/wf_api/__init__.py -
Test:
tests/wf_api/test_source_admin_api.py -
Step 1: Add protocol conformance test
Append to tests/wf_api/test_source_admin_api.py:
from wf_api import WorkflowSourceAdminSurface
def test_source_admin_api_satisfies_surface_protocol() -> None:
api: WorkflowSourceAdminSurface = _api(_source("demo.personal"))
assert api is not None
- Step 2: Add protocol
In src/wf_api/surface.py, add this class near the other surface protocols:
class WorkflowSourceAdminSurface(Protocol):
"""Read-only source/admin methods exposed by platform frontends."""
async def list_sources(
self,
*,
cursor: str | None = None,
limit: int = 50,
) -> dict[str, Any]: ...
async def inspect_source(
self,
*,
source_id: str,
) -> dict[str, Any]: ...
Add "WorkflowSourceAdminSurface" to __all__.
Do not add it as a base class of WorkflowApiSurface.
- Step 3: Export protocol
Modify src/wf_api/__init__.py:
from .surface import WorkflowSourceAdminSurface
Add "WorkflowSourceAdminSurface" to __all__.
- Step 4: Run tests and type check
Run:
uv run pytest tests/wf_api/test_source_admin_api.py tests/wf_api/test_import_direction.py -q
uv run basedpyright --level error src/wf_api tests/wf_api/test_source_admin_api.py
Expected: tests PASS, basedpyright reports 0 errors.
Task 4: Delegate MCP admin source tools through wf_api
Files:
-
Modify:
src/wf_mcp/admin_surface/handlers/broker.py -
Modify:
src/wf_mcp/admin_surface/tools.py -
Test:
tests/wf_mcp/test_admin_surface.py -
Step 1: Add adapter smoke assertion
In tests/wf_mcp/test_admin_surface.py, inside
test_broker_admin_handlers_list_connections_and_events, add:
sources = _run(handlers.list_sources(limit=100))
source_ids = {source["id"] for source in sources["sources"]}
assert "wf.std" in source_ids
assert "wf.docs" in source_ids
assert sources["total"] >= 2
This test uses the existing _run() helper.
- Step 2: Run the updated test and verify failure
Run:
uv run pytest tests/wf_mcp/test_admin_surface.py::test_broker_admin_handlers_list_connections_and_events -q
Expected before delegation: FAIL because BrokerAdminHandlers.list_sources()
is still sync and returns a dict, not an awaitable. This failure drives the async
source-handler cleanup.
- Step 3: Change handler implementation
Modify src/wf_mcp/admin_surface/handlers/broker.py:
from wf_api import WorkflowSourceAdminApi, WorkflowSourceAdminSurface
from wf_mcp.broker.service.workflow_operation_context import context_from_service
Update __init__:
def __init__(self, service: WfMcpService) -> None:
self.service = service
self.sources: WorkflowSourceAdminSurface = WorkflowSourceAdminApi(
context_from_service(service)
)
Update source methods:
async def list_sources(
self,
*,
cursor: str | None = None,
limit: int = 50,
) -> dict[str, Any]:
return await self.sources.list_sources(cursor=cursor, limit=limit)
async def inspect_source(self, source_id: str) -> dict[str, Any]:
return await self.sources.inspect_source(source_id=source_id)
Do not add an asyncio.run() bridge. The handler is called from async MCP tools,
so source methods should be async at this boundary.
- Step 4: Await source handler calls in MCP tools
In src/wf_mcp/admin_surface/tools.py, update:
return await handlers.list_sources(cursor=cursor, limit=limit)
and:
return await handlers.inspect_source(source_id)
- Step 5: Run MCP admin tests
Run:
uv run pytest tests/wf_mcp/test_admin_surface.py tests/wf_mcp/server/test_config.py tests/wf_mcp/test_broker_server.py -q
Expected: PASS.
Task 5: Documentation and verification
Files:
-
Modify:
docs/current_roadmap.md -
Modify:
docs/superpowers/specs/2026-06-03-cli-api-alignment-notes.md -
Step 1: Update roadmap
In docs/current_roadmap.md, under Durable API service shape or
CLI/API alignment, add:
- Completed: read-only source inventory now has a protocol-neutral
`WorkflowSourceAdminApi` / `WorkflowSourceAdminSurface`; MCP admin source
tools delegate through it while connection/raw MCP operations remain
broker-owned.
- Step 2: Update CLI/API notes
In docs/superpowers/specs/2026-06-03-cli-api-alignment-notes.md, under
Next Slices, replace the source/admin item with:
1. **Source/admin transport and CLI commands**
- Build JSON-RPC methods and `wf source ...` commands over
`WorkflowSourceAdminSurface`.
- Keep mutation out until the store-backed source registry is designed.
- Step 3: Run verification
Run:
uv run pytest tests/wf_api/test_source_admin_api.py tests/wf_mcp/test_admin_surface.py tests/wf_mcp/server/test_config.py tests/wf_mcp/test_broker_server.py -q
uv run ruff check src/wf_api src/wf_mcp/admin_surface tests/wf_api/test_source_admin_api.py tests/wf_mcp/test_admin_surface.py tests/wf_mcp/server/test_config.py tests/wf_mcp/test_broker_server.py
uv run ruff format --check src/wf_api src/wf_mcp/admin_surface tests/wf_api/test_source_admin_api.py tests/wf_mcp/test_admin_surface.py tests/wf_mcp/server/test_config.py tests/wf_mcp/test_broker_server.py
uv run basedpyright --level error src/wf_api src/wf_mcp/admin_surface tests/wf_api/test_source_admin_api.py tests/wf_mcp/test_admin_surface.py
Expected:
-
pytest PASS
-
ruff check PASS
-
ruff format PASS
-
basedpyright 0 errors
-
Step 4: Commit
git add src/wf_api/source_admin.py src/wf_api/surface.py src/wf_api/__init__.py src/wf_mcp/admin_surface/handlers/broker.py src/wf_mcp/admin_surface/tools.py tests/wf_api/test_source_admin_api.py tests/wf_mcp/test_admin_surface.py docs/current_roadmap.md docs/superpowers/specs/2026-06-03-cli-api-alignment-notes.md
git commit -m "feat: add source admin api surface"
Follow-Up Slices
-
Source/admin JSON-RPC transport
- Add fixed methods such as
workflow.sources.listandworkflow.sources.inspect. - Add a dedicated RPC source-admin client or mixin, but keep the lifecycle
RpcWorkflowApiClientcontract clear.
- Add fixed methods such as
-
CLI
wf sourcecommands- Add
wf source listandwf source inspect. - Use the same target-aware context pattern as workflow lifecycle commands, but the CLI context may need a second handler field for source admin.
- Add
-
Store-backed source registry
- Config remains bootstrap.
- Server-owned dynamic source changes persist through a source registry store.
- Source identity stays structural: source id, provider/account/profile, and concrete transport details are not inferred from dotted display names.
-
Mutable source admin
- Add source create/update/delete only after store persistence and validation rules exist.
- Enforce duplicate id behavior and liveness/validation diagnostics before a source can become runnable.
Self-Review
- Spec coverage: read-only source list/inspect, neutral surface, MCP adapter, and docs are covered.
- Placeholder scan: no TBD/TODO placeholders.
- Type consistency:
WorkflowSourceAdminApi,WorkflowSourceAdminSurface,source_id,cursor, andlimitnames are consistent across tasks. - Risk:
BrokerAdminHandlers.list_sources/inspect_sourcebecome async. The plan updates the direct handler test and the MCP tool wrappers that call them.