Files
lda-wf/docs/historical/superpowers/plans/2026-06-03-source-admin-api-surface.md
T

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_api does 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_sources already exposes the source inventory wf_api needs for read-only source listing and inspection.

Scope

In scope:

  • list_sources(cursor=None, limit=50) -> dict
  • inspect_source(source_id: str) -> dict
  • Protocol-neutral WorkflowSourceAdminSurface
  • Local implementation WorkflowSourceAdminApi
  • MCP BrokerAdminHandlers.list_sources() and .inspect_source() delegate through WorkflowSourceAdminApi
  • 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 WorkflowOperationContext and platform source models.
    • Imports no wf_mcp.
  • Modify src/wf_api/surface.py

    • Adds sibling protocol WorkflowSourceAdminSurface.
    • Does not make WorkflowApiSurface inherit it.
  • Modify src/wf_api/__init__.py

    • Exports WorkflowSourceAdminApi and WorkflowSourceAdminSurface.
  • Modify src/wf_mcp/admin_surface/handlers/broker.py

    • Construct WorkflowSourceAdminApi(context_from_service(service)).
    • Delegate list_sources and inspect_source to it.
    • Keep connection/catalog/resource/raw methods unchanged.
    • Make those two source methods async, matching the async MCP tool boundary.
  • 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

  1. Source/admin JSON-RPC transport

    • Add fixed methods such as workflow.sources.list and workflow.sources.inspect.
    • Add a dedicated RPC source-admin client or mixin, but keep the lifecycle RpcWorkflowApiClient contract clear.
  2. CLI wf source commands

    • Add wf source list and wf 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.
  3. 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.
  4. 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, and limit names are consistent across tasks.
  • Risk: BrokerAdminHandlers.list_sources / inspect_source become async. The plan updates the direct handler test and the MCP tool wrappers that call them.