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.WorkflowServeris the neutral process-host shape.wf_server.build_local_static_workflow_server()builds a local/static server and intentionally leavessource_registry_admin=None.wf_mcp.broker.config.build_service_from_config(config)builds aWfMcpServicewith workflow stores, MCP adapters, source-registry startup merge, and configured connections.wf_mcp.broker.service.workflow_operation_context.context_from_service(service)adaptsWfMcpServiceinto a neutralWorkflowOperationContext.wf_mcp.broker.service.source_registry_admin.SourceRegistryAdminProviderprovides desired-registry reads/mutations overFileSourceRegistryStore.wf_transport_rpc_http.create_rpc_app(server)registers workflow, source, source-registry, and admin JSON-RPC methods over anyWorkflowServer.
Boundary rule:
wf_servermust not importwf_mcp.- The MCP-backed constructor belongs in
wf_mcpand returns awf_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, notwf_server -> wf_mcp.
- MCP-specific adapter constructors returning
- 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
WorkflowServerConfigis not exported and tests need it.
- No MCP import. Only add exports if current
- 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
WorkflowServerconstruction complete.
- Mark concrete MCP-backed
- 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_serverstayed 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
WorkflowServerconstruction remains future work". - Dependency direction: constructor lives in
wf_mcp;wf_serverremains 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.eventsis currently typed asInMemoryWorkflowEventRecorder; if basedpyright rejects assigningBrokerEventRecorder, change that field type toWorkflowEventRecorderinsrc/wf_server/context.pyand update no behavior. This is a type-only broadening and should be documented in the implementation report.