Files
lda-wf/docs/historical/superpowers/plans/2026-06-04-mcp-backed-workflow-server.md
T

18 KiB

MCP-Backed Workflow Server Construction 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: Build a concrete MCP-backed WorkflowServer composition so JSON-RPC/CLI can target a long-lived server with real MCP broker sources, admin data, and source-registry mutation.

Architecture: Keep wf_server transport-neutral and MCP-free. Add the MCP-specific constructor in wf_mcp, adapting an existing WfMcpService into the neutral WorkflowServer dataclass using context_from_service, WorkflowApi, WorkflowAdminApi, WorkflowSourceAdminApi, and WorkflowSourceRegistryApi. This proves remote RPC can use MCP-backed source registry/admin surfaces without making wf_server depend on WfMcpService.

Tech Stack: Python 3.14, dataclasses, Pydantic v2 models, wf_api, wf_server, wf_mcp broker services, wf_transport_rpc_http, pytest-asyncio, ruff, basedpyright.


Current Context

Implemented pieces:

  • wf_server.context.WorkflowServer is the neutral process-host shape.
  • wf_server.build_local_static_workflow_server() builds a local/static server and intentionally leaves source_registry_admin=None.
  • wf_mcp.broker.config.build_service_from_config(config) builds a WfMcpService with workflow stores, MCP adapters, source-registry startup merge, and configured connections.
  • wf_mcp.broker.service.workflow_operation_context.context_from_service(service) adapts WfMcpService into a neutral WorkflowOperationContext.
  • wf_mcp.broker.service.source_registry_admin.SourceRegistryAdminProvider provides desired-registry reads/mutations over FileSourceRegistryStore.
  • wf_transport_rpc_http.create_rpc_app(server) registers workflow, source, source-registry, and admin JSON-RPC methods over any WorkflowServer.

Boundary rule:

  • wf_server must not import wf_mcp.
  • The MCP-backed constructor belongs in wf_mcp and returns a wf_server.WorkflowServer.

Out of scope:

  • No new HTTP server process CLI.
  • No hot reload/live remount after registry mutation.
  • No auth redesign.
  • No WebSocket/MCP transport sibling.
  • No persisted run/resume process-restart implementation.

File Structure

  • Create src/wf_mcp/broker/server.py
    • MCP-specific adapter constructors returning WorkflowServer.
    • Wires WorkflowApi, source admin, admin, and desired source registry admin.
    • Keeps the dependency direction wf_mcp -> wf_server, not wf_server -> wf_mcp.
  • Modify src/wf_mcp/broker/__init__.py
    • Re-export the constructor for callers/tests.
  • Modify src/wf_server/__init__.py
    • No MCP import. Only add exports if current WorkflowServerConfig is not exported and tests need it.
  • Test tests/wf_mcp/test_mcp_workflow_server.py
    • Direct construction tests for the MCP-backed server adapter.
  • Test tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py
    • RPC tests proving source-registry read/mutation and admin connections work against the MCP-backed server.
  • Modify docs/current_roadmap.md
    • Mark concrete MCP-backed WorkflowServer construction complete.
  • Modify docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md
    • Add implementation status for this slice.

Task 1: Add MCP-Backed Server Adapter

Files:

  • Create: src/wf_mcp/broker/server.py

  • Modify: src/wf_mcp/broker/__init__.py

  • Test: tests/wf_mcp/test_mcp_workflow_server.py

  • Step 1: Write direct construction tests

Create tests/wf_mcp/test_mcp_workflow_server.py:

from __future__ import annotations

import ast

from wf_mcp.broker.config import build_service_from_config
from wf_mcp.broker.server import (
    build_workflow_server_from_config,
    workflow_server_from_service,
)
from wf_mcp.models import BrokerConfig, ConnectionConfig
from wf_mcp.source_registry import (
    FileSourceRegistryStore,
    McpSourceRegistryEntry,
    SourceRegistryFile,
)
from wf_server import WorkflowServer


def _registry_entry(source_id: str) -> McpSourceRegistryEntry:
    return McpSourceRegistryEntry.model_validate(
        {
            "id": source_id,
            "kind": "mcp",
            "enabled": True,
            "provider": "demo",
            "account": "registry",
            "transport": {"kind": "stdio", "command": "demo-server"},
        }
    )


def test_wf_server_package_stays_mcp_free() -> None:
    path = "src/wf_server/context.py"
    tree = ast.parse(open(path, encoding="utf-8").read(), filename=path)

    violations: list[str] = []
    for node in ast.walk(tree):
        if isinstance(node, ast.ImportFrom) and node.module:
            if node.module.startswith("wf_mcp"):
                violations.append(f"{node.lineno}: from {node.module} import ...")
        elif isinstance(node, ast.Import):
            for alias in node.names:
                if alias.name.startswith("wf_mcp"):
                    violations.append(f"{node.lineno}: import {alias.name}")

    assert violations == []


def test_workflow_server_from_service_wires_neutral_surfaces(tmp_path) -> None:
    config = BrokerConfig(
        store_root=tmp_path / "store",
        connections=[
            ConnectionConfig(id="demo.default", server="demo", account="default")
        ],
    )
    service = build_service_from_config(config)

    server = workflow_server_from_service(
        service,
        config=config,
        source_registry_store=FileSourceRegistryStore(config.store_root),
    )

    assert isinstance(server, WorkflowServer)
    assert server.config.store_root == config.store_root
    assert server.api.context is server.context
    assert server.source_registry_admin is not None
    assert server.admin.connections is service.connection_service
    assert server.admin.events is service.events


def test_build_workflow_server_from_config_exposes_registry_admin(tmp_path) -> None:
    config = BrokerConfig(store_root=tmp_path / "store", connections=[])
    FileSourceRegistryStore(config.store_root).save_registry(
        SourceRegistryFile(sources=[_registry_entry("demo.registry")])
    )

    server = build_workflow_server_from_config(config)

    assert server.source_registry_admin is not None
    assert "demo.registry" in server.context.specs.capability_sources

Run:

uv run pytest tests/wf_mcp/test_mcp_workflow_server.py -q

Expected: FAIL because wf_mcp.broker.server does not exist.

  • Step 2: Implement src/wf_mcp/broker/server.py

Create src/wf_mcp/broker/server.py:

from __future__ import annotations

from wf_api import (
    WorkflowAdminApi,
    WorkflowApi,
    WorkflowSourceAdminApi,
    WorkflowSourceRegistryApi,
    durable_workflow_api,
)
from wf_api.stores import WorkflowStores
from wf_server import WorkflowServer, WorkflowServerConfig

from .config import build_service_from_config
from .service import WfMcpService
from .service.source_registry_admin import SourceRegistryAdminProvider
from .service.workflow_operation_context import context_from_service
from ..models import BrokerConfig
from ..source_registry import FileSourceRegistryStore, SourceRegistryStore


def workflow_server_from_service(
    service: WfMcpService,
    *,
    config: BrokerConfig,
    source_registry_store: SourceRegistryStore,
) -> WorkflowServer:
    """Adapt an MCP broker service into the neutral WorkflowServer shape.

    This is intentionally in wf_mcp, not wf_server: MCP owns upstream source
    management, while wf_server stays transport-neutral and MCP-free.
    """
    context = context_from_service(service)
    api: WorkflowApi = durable_workflow_api(context)
    source_admin = WorkflowSourceAdminApi(context)
    admin = WorkflowAdminApi(
        connections=service.connection_service,
        events=service.events,
    )
    source_registry_admin = WorkflowSourceRegistryApi(
        provider=SourceRegistryAdminProvider(
            source_registry_store=source_registry_store,
            config_connections=config.connections,
        ),
        mutation_provider=SourceRegistryAdminProvider(
            source_registry_store=source_registry_store,
            config_connections=config.connections,
        ),
    )
    stores = WorkflowStores(
        artifact_store=service.artifact_store,
        draft_workspace_store=service.draft_workspace_store,
        run_store=service.run_store,
    )
    return WorkflowServer(
        config=WorkflowServerConfig(store_root=config.store_root),
        stores=stores,
        context=context,
        api=api,
        source_admin=source_admin,
        admin=admin,
        events=service.events,
        source_registry_admin=source_registry_admin,
    )


def build_workflow_server_from_config(config: BrokerConfig) -> WorkflowServer:
    """Build a neutral WorkflowServer backed by MCP broker runtime services."""
    service = build_service_from_config(config)
    return workflow_server_from_service(
        service,
        config=config,
        source_registry_store=FileSourceRegistryStore(config.store_root),
    )


__all__ = [
    "build_workflow_server_from_config",
    "workflow_server_from_service",
]

If basedpyright rejects WorkflowStores(...) because stores are optional on WfMcpService, add explicit fail-fast guards before constructing it:

if (
    service.artifact_store is None
    or service.draft_workspace_store is None
    or service.run_store is None
):
    raise ValueError("MCP-backed WorkflowServer requires workflow stores")
  • Step 3: Re-export the constructor

Modify src/wf_mcp/broker/__init__.py to export:

from .server import build_workflow_server_from_config, workflow_server_from_service

__all__ = [
    # keep existing exports here
    "build_workflow_server_from_config",
    "workflow_server_from_service",
]

Do not remove existing exports. If the file currently has no __all__, add only the imports and let existing import behavior continue.

  • Step 4: Run direct construction tests

Run:

uv run pytest tests/wf_mcp/test_mcp_workflow_server.py -q

Expected: PASS.

  • Step 5: Commit
git add src/wf_mcp/broker/server.py src/wf_mcp/broker/__init__.py tests/wf_mcp/test_mcp_workflow_server.py
git commit -m "feat: build mcp backed workflow server"

Task 2: Prove JSON-RPC Uses MCP-Backed Registry and Admin Surfaces

Files:

  • Create: tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py

  • Step 1: Write RPC integration tests

Create tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py:

from __future__ import annotations

import httpx

from wf_mcp.broker.server import build_workflow_server_from_config
from wf_mcp.models import BrokerConfig, ConnectionConfig
from wf_mcp.source_registry import (
    FileSourceRegistryStore,
    McpSourceRegistryEntry,
    SourceRegistryFile,
)
from wf_transport_rpc_http import RpcWorkflowApiClient, create_rpc_app


def _registry_entry(source_id: str, *, enabled: bool = True) -> McpSourceRegistryEntry:
    return McpSourceRegistryEntry.model_validate(
        {
            "id": source_id,
            "kind": "mcp",
            "enabled": enabled,
            "provider": "demo",
            "account": "registry",
            "transport": {"kind": "stdio", "command": "demo-server"},
        }
    )


async def _rpc(client: httpx.AsyncClient, method: str, params: dict) -> dict:
    response = await client.post(
        "/rpc",
        json={"jsonrpc": "2.0", "id": "test", "method": method, "params": params},
    )
    assert response.status_code == 200
    return response.json()


async def test_mcp_backed_rpc_lists_and_mutates_source_registry(tmp_path) -> None:
    config = BrokerConfig(store_root=tmp_path / "store", connections=[])
    FileSourceRegistryStore(config.store_root).save_registry(
        SourceRegistryFile(sources=[_registry_entry("demo.registry")])
    )
    server = build_workflow_server_from_config(config)
    app = create_rpc_app(server)
    transport = httpx.ASGITransport(app=app)

    async with httpx.AsyncClient(
        transport=transport, base_url="http://test"
    ) as http_client:
        client = RpcWorkflowApiClient(http_client)

        listed = await client.list_registry_entries(limit=10)
        disabled = await client.disable_registry_entry("demo.registry")
        inspected = await client.inspect_registry_entry("demo.registry")

    assert listed["entries"][0]["id"] == "demo.registry"
    assert disabled["entry"]["enabled"] is False
    assert inspected["entry"]["enabled"] is False


async def test_mcp_backed_rpc_reports_connections_and_events(tmp_path) -> None:
    config = BrokerConfig(
        store_root=tmp_path / "store",
        connections=[
            ConnectionConfig(
                id="demo.default",
                server="demo",
                account="default",
            )
        ],
    )
    server = build_workflow_server_from_config(config)
    app = create_rpc_app(server)
    transport = httpx.ASGITransport(app=app)

    async with httpx.AsyncClient(
        transport=transport, base_url="http://test"
    ) as http_client:
        connections = await _rpc(
            http_client, "workflow.admin.connections.list", {"limit": 20}
        )
        events = await _rpc(http_client, "workflow.admin.events.list", {"limit": 20})

    assert connections["result"]["connections"][0]["id"] == "demo.default"
    assert any(
        event["kind"] == "connection_registered"
        for event in events["result"]["events"]
    )

If RpcWorkflowApiClient method names differ, inspect src/wf_transport_rpc_http/client_source_registry.py and use the exact names. Do not change client method names in this slice unless the tests reveal a real bug.

  • Step 2: Run RPC integration tests

Run:

uv run pytest tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py -q

Expected: PASS after Task 1.

  • Step 3: Commit
git add tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py
git commit -m "test: cover mcp backed workflow server rpc"

Task 3: Add Server Construction Docs Status

Files:

  • Modify: docs/current_roadmap.md

  • Modify: docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md

  • Step 1: Update current roadmap

In docs/current_roadmap.md, under Durable API service shape, replace the remaining "concrete MCP-backed WorkflowServer construction remains future work" language with:

   - Completed: MCP-backed `WorkflowServer` construction is available through
      `wf_mcp.broker.server.build_workflow_server_from_config`. JSON-RPC can now
      expose real MCP-backed workflow, source-admin, admin, and desired source
      registry surfaces without making `wf_server` import `wf_mcp`.

Keep any longer-term note about shrinking/retiring old wf_mcp server entry points; this slice does not retire them.

  • Step 2: Update long-lived API spec status

In docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md, update the status paragraph near the top to mention this slice:

Status: Slices 1-4 implemented. `wf_server` provides
`build_local_static_workflow_server`; `wf_mcp.broker.server` can adapt MCP
broker config/services into the neutral `WorkflowServer`; `wf_transport_rpc_http`
provides JSON-RPC methods and client support; `wf_cli` has target-aware context;
and `wf_config` owns neutral config models. WebSocket transport, auth,
streaming/progress, database backend, and live source hot reload remain future
work.

Under First Slice implementation status, add:

- Slice 4 complete: `wf_mcp.broker.server.build_workflow_server_from_config()`
  returns a neutral `WorkflowServer` backed by MCP broker runtime services,
  including source registry admin and platform admin surfaces.
  • Step 3: Run link/status search

Run:

rg -n "concrete MCP-backed `WorkflowServer` construction remains future work|Slices 1-3 implemented|MCP-backed `WorkflowServer`" docs/current_roadmap.md docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md

Expected:

  • No stale "remains future work" claim for MCP-backed server construction.

  • Status says Slices 1-4 implemented.

  • The new constructor path is named.

  • Step 4: Commit

git add docs/current_roadmap.md docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md
git commit -m "docs: record mcp backed workflow server"

Task 4: Final Verification

Files:

  • Verify only.

  • Step 1: Run focused test set

Run:

uv run pytest tests/wf_mcp/test_mcp_workflow_server.py tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py tests/wf_server/test_local_static_server.py tests/wf_transport_rpc_http/test_source_registry_rpc.py -q

Expected: all tests pass.

  • Step 2: Run lint/type checks

Run:

uv run ruff check src/wf_mcp/broker/server.py tests/wf_mcp/test_mcp_workflow_server.py tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py
uv run basedpyright --level error src/wf_mcp/broker/server.py tests/wf_mcp/test_mcp_workflow_server.py tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py
git diff --check

Expected: all commands exit 0. CRLF warnings from Git are acceptable; whitespace errors are not.

  • Step 3: Review package dependency direction

Run:

rg -n "wf_mcp" src/wf_server

Expected: no matches.

  • Step 4: Final report

Report:

  • files created/modified
  • verification output
  • whether wf_server stayed MCP-free
  • whether local/static source-registry unavailable behavior still passes
  • any deviations from this plan

Do not run the full suite unless focused verification is green.


Self-Review

  • Spec coverage: covers the active roadmap gap "concrete MCP-backed WorkflowServer construction remains future work".
  • Dependency direction: constructor lives in wf_mcp; wf_server remains MCP-free.
  • Scope: no hot reload, auth redesign, process CLI, or persisted resume restart behavior.
  • Testing: direct adapter tests plus JSON-RPC integration tests prove this is product-visible, not only internal wiring.
  • Risk: WorkflowServer.events is currently typed as InMemoryWorkflowEventRecorder; if basedpyright rejects assigning BrokerEventRecorder, change that field type to WorkflowEventRecorder in src/wf_server/context.py and update no behavior. This is a type-only broadening and should be documented in the implementation report.