25 KiB
Source Registry Apply/Reload 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 an explicit wf admin registry apply operation so persisted source registry changes can affect the running MCP-backed WorkflowServer without restarting the process.
Architecture: Keep registry add/update/enable/disable/remove as desired-state writes only. Add a separate apply operation that mirrors the old wf.admin.reload_config pattern: reconcile current config plus registry store into runtime connection/source state using ConnectionService.sync_connections_from_config(...). Expose the operation through wf_api, JSON-RPC HTTP, and CLI.
Tech Stack: Python 3.14, Pydantic v2, Typer, JSON-RPC HTTP transport, wf_api.source_registry_admin, wf_mcp.broker.service.ConnectionService, FileSourceRegistryStore, pytest, ruff, basedpyright.
File Structure
- Modify
src/wf_api/source_registry_admin.py: add an apply provider protocol andWorkflowSourceRegistryApi.apply_registry_changes(). - Modify
src/wf_api/surface.py: addapply_registry_changes()toWorkflowSourceRegistrySurface. - Modify
src/wf_mcp/broker/service/source_registry_admin.py: implement apply/reload logic for MCP-backed runtime state. - Modify
src/wf_mcp/broker/server.py: wire apply dependencies intoSourceRegistryAdminProvider. - Modify
src/wf_transport_rpc_http/models.py: add empty params model if needed. - Modify
src/wf_transport_rpc_http/methods_source_registry.py: registerworkflow.admin.source_registry.apply. - Modify
src/wf_transport_rpc_http/client_source_registry.py: add RPC client method. - Modify
src/wf_cli/commands/source_registry.py: addwf admin registry apply. - Modify tests in
tests/wf_api,tests/wf_mcp/service,tests/wf_transport_rpc_http, andtests/wf_cli. - Update
docs/current_roadmap.mdanddocs/superpowers/specs/2026-06-03-store-backed-source-registry-design.md.
Current Context
Registry mutation operations currently write desired state only:
wf admin registry add/update/enable/disable/remove
-> WorkflowSourceRegistryApi
-> SourceRegistryAdminProvider
-> FileSourceRegistryStore
Runtime source state is built by:
service.sync_connections_from_config(
config,
source_registry_store=FileSourceRegistryStore(config.store_root),
)
The old config reload path already uses the same reconciliation hook:
def sync_service(config: BrokerConfig) -> None:
service.sync_connections_from_config(
config,
source_registry_store=FileSourceRegistryStore(config.store_root),
)
This plan adds the registry equivalent of reload, but does not mutate config and does not attempt proxy/FastMCP remount semantics. It updates the neutral workflow server's runtime source/catalog state.
Apply Semantics
apply_registry_changes should:
- Load the current registry from the registry store.
- Re-run
ConnectionService.sync_connections_from_config(config, source_registry_store=store). - Ensure SDK adapters exist for any newly registered connection server.
- Return a compact summary:
{
"applied": True,
"registered": ["new.source"],
"updated": ["existing.source"],
"removed": ["old.source"],
"connection_count": 5,
"registry_entry_count": 2,
}
Rules:
- Config
lockedentries still win over same-id registry entries. - Config
seedentries still yield to existing registry entries. - Disabled registry entries should remain visible as disabled runtime sources when current reconciliation does that today; do not invent new hide/remove semantics in this slice.
- Local/static servers without registry admin still return
source_registry_unavailable. - No automatic apply after add/update/enable/disable/remove in v1.
- No upstream proxy remount, no long-lived subscription handling, no auth prompt flow in this slice.
Task 1: Add API-Layer Apply Contract
Files:
-
Modify:
src/wf_api/source_registry_admin.py -
Modify:
src/wf_api/surface.py -
Test:
tests/wf_api/test_source_registry_admin_api.py -
Step 1: Add a failing API test
Add this test to tests/wf_api/test_source_registry_admin_api.py:
class RecordingApplyProvider:
def __init__(self) -> None:
self.called = False
def apply_registry_changes(self) -> dict[str, object]:
self.called = True
return {
"applied": True,
"registered": ["demo.new"],
"updated": [],
"removed": [],
"connection_count": 1,
"registry_entry_count": 1,
}
async def test_apply_registry_changes_delegates_to_apply_provider() -> None:
read_provider = _provider([])
apply_provider = RecordingApplyProvider()
api = WorkflowSourceRegistryApi(
provider=read_provider,
apply_provider=apply_provider,
)
payload = await api.apply_registry_changes()
assert apply_provider.called is True
assert payload["applied"] is True
assert payload["registered"] == ["demo.new"]
assert payload["connection_count"] == 1
Also add:
async def test_apply_registry_changes_requires_apply_provider() -> None:
api = WorkflowSourceRegistryApi(provider=_provider([]))
with pytest.raises(TypeError, match="apply_registry_changes requires"):
await api.apply_registry_changes()
- Step 2: Run failing tests
Run:
uv run pytest tests/wf_api/test_source_registry_admin_api.py -q
Expected: fail because WorkflowSourceRegistryApi.__init__ does not accept apply_provider and apply_registry_changes does not exist.
- Step 3: Add provider protocol and API method
In src/wf_api/source_registry_admin.py, add:
@runtime_checkable
class WorkflowSourceRegistryApplyProvider(Protocol):
"""Applies desired registry state to the currently running server."""
def apply_registry_changes(self) -> Mapping[str, Any] | object: ...
Change WorkflowSourceRegistryApi.__init__ to:
def __init__(
self,
*,
provider: WorkflowSourceRegistryProvider,
mutation_provider: WorkflowSourceRegistryMutationProvider | None = None,
apply_provider: WorkflowSourceRegistryApplyProvider | None = None,
) -> None:
self._provider = provider
self._mutation_provider = mutation_provider
self._apply_provider = apply_provider
Add:
async def apply_registry_changes(self) -> dict[str, Any]:
if self._apply_provider is None:
raise TypeError("apply_registry_changes requires an apply provider")
return _payload(self._apply_provider.apply_registry_changes())
Export WorkflowSourceRegistryApplyProvider from src/wf_api/__init__.py.
- Step 4: Extend protocol surface
In src/wf_api/surface.py, add to WorkflowSourceRegistrySurface:
async def apply_registry_changes(self) -> dict[str, Any]: ...
- Step 5: Run API tests
Run:
uv run pytest tests/wf_api/test_source_registry_admin_api.py -q
Expected: pass.
Task 2: Implement MCP Runtime Apply Provider
Files:
-
Modify:
src/wf_mcp/broker/service/source_registry_admin.py -
Modify:
src/wf_mcp/broker/server.py -
Test:
tests/wf_mcp/service/test_source_registry_admin.py -
Step 1: Add failing service test
Add this helper near existing test helpers in tests/wf_mcp/service/test_source_registry_admin.py:
def _apply_provider(
tmp_path,
*,
config_connections=(),
registry_sources=(),
):
from wf_mcp.broker.service.connection_service import ConnectionService
from wf_mcp.broker.service.events import BrokerEventRecorder
from wf_mcp.broker.service.source_catalog import SourceCatalogService
from wf_mcp.models import BrokerConfig
from wf_mcp.source_registry import FileSourceRegistryStore, SourceRegistryFile
from wf_mcp.storage import FileStore
from wf_mcp.events import EventBus
events = BrokerEventRecorder(EventBus())
connection_service = ConnectionService(events=events)
source_catalog = SourceCatalogService(
store=FileStore(tmp_path),
connection_lookup=connection_service.get,
connection_list_enabled=connection_service.list_enabled,
connection_list_all=connection_service.list_all,
tool_executor_for=lambda connection: None,
load_auth=lambda connection_id: None,
emit_event=events.record_event,
)
connection_service.bind_source_catalog(source_catalog)
store = FileSourceRegistryStore(tmp_path)
store.save_registry(SourceRegistryFile(sources=list(registry_sources)))
config = BrokerConfig(store_root=tmp_path, connections=list(config_connections))
provider = SourceRegistryAdminProvider(
source_registry_store=store,
config_connections=config.connections,
connection_service=connection_service,
config=config,
ensure_adapter=lambda connection: None,
)
return provider, connection_service, source_catalog
Then add:
def test_source_registry_apply_materializes_registry_connection(tmp_path) -> None:
entry = _registry_entry("dynamic.default", provider="dynamic", account="default")
provider, connection_service, source_catalog = _apply_provider(
tmp_path,
registry_sources=[entry],
)
payload = provider.apply_registry_changes()
assert payload["applied"] is True
assert payload["registered"] == ["dynamic.default"]
assert payload["updated"] == []
assert payload["removed"] == []
assert payload["connection_count"] == 1
assert payload["registry_entry_count"] == 1
assert connection_service.get("dynamic.default").server == "dynamic"
assert source_catalog.capability_sources["dynamic.default"].enabled is True
Add a second test for removal:
def test_source_registry_apply_removes_deleted_registry_connection(tmp_path) -> None:
entry = _registry_entry("dynamic.default", provider="dynamic", account="default")
provider, connection_service, source_catalog = _apply_provider(
tmp_path,
registry_sources=[entry],
)
provider.apply_registry_changes()
provider.remove_registry_entry("dynamic.default")
payload = provider.apply_registry_changes()
assert payload["removed"] == ["dynamic.default"]
assert "dynamic.default" not in connection_service.connections.connections
assert "dynamic.default" not in source_catalog.capability_sources
- Step 2: Run failing service tests
Run:
uv run pytest tests/wf_mcp/service/test_source_registry_admin.py -q
Expected: fail because SourceRegistryAdminProvider lacks runtime apply dependencies and method.
- Step 3: Extend provider dataclass
In src/wf_mcp/broker/service/source_registry_admin.py, import:
from collections.abc import Callable
from ...models import BrokerConfig
from .connection_service import ConnectionService
Change the dataclass fields to include:
connection_service: ConnectionService | None = None
config: BrokerConfig | None = None
ensure_adapter: Callable[[ConnectionConfig], None] | None = None
- Step 4: Add apply implementation
Add this method to SourceRegistryAdminProvider:
def apply_registry_changes(self) -> dict[str, Any]:
"""Reconcile desired registry state into the live service connection graph.
This mirrors config reload reconciliation, but it only applies persisted
registry state. It does not mutate config files or remount FastMCP proxy
providers.
"""
if self.connection_service is None or self.config is None:
raise RuntimeError("source registry apply requires runtime service context")
before = {connection.id: connection for connection in self.connection_service.list_all()}
registry = self.source_registry_store.load_registry()
self.connection_service.sync_connections_from_config(
self.config,
source_registry_store=self.source_registry_store,
)
after = {connection.id: connection for connection in self.connection_service.list_all()}
if self.ensure_adapter is not None:
for connection in after.values():
self.ensure_adapter(connection)
before_ids = set(before)
after_ids = set(after)
updated = sorted(
source_id
for source_id in before_ids & after_ids
if before[source_id] != after[source_id]
)
return {
"applied": True,
"registered": sorted(after_ids - before_ids),
"updated": updated,
"removed": sorted(before_ids - after_ids),
"connection_count": len(after),
"registry_entry_count": len(registry.sources),
}
- Step 5: Wire provider in MCP-backed WorkflowServer
In src/wf_mcp/broker/server.py, inside workflow_server_from_service, replace:
registry_provider = SourceRegistryAdminProvider(
source_registry_store=source_registry_store,
config_connections=config.connections,
)
with:
def ensure_adapter(connection: ConnectionConfig) -> None:
if connection.server not in service.adapters:
service.register_adapter(connection.server, McpSdkAdapter())
registry_provider = SourceRegistryAdminProvider(
source_registry_store=source_registry_store,
config_connections=config.connections,
connection_service=service.connection_service,
config=config,
ensure_adapter=ensure_adapter,
)
Add apply_provider=registry_provider when constructing WorkflowSourceRegistryApi:
source_registry_admin = WorkflowSourceRegistryApi(
provider=registry_provider,
mutation_provider=registry_provider,
apply_provider=registry_provider,
)
Import ConnectionConfig if needed.
- Step 6: Run service tests
Run:
uv run pytest tests/wf_mcp/service/test_source_registry_admin.py tests/wf_mcp/service/test_connection_service.py -q
Expected: pass.
Task 3: Add JSON-RPC Apply Method and Client
Files:
-
Modify:
src/wf_transport_rpc_http/models.py -
Modify:
src/wf_transport_rpc_http/methods_source_registry.py -
Modify:
src/wf_transport_rpc_http/client_source_registry.py -
Test:
tests/wf_transport_rpc_http/test_source_registry_rpc.py -
Step 1: Add failing RPC tests
In tests/wf_transport_rpc_http/test_source_registry_rpc.py, add:
async def test_rpc_source_registry_apply_unavailable_on_local_static(tmp_path) -> None:
app = create_rpc_app(build_local_static_workflow_server(tmp_path / "store"))
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
) as client:
payload = await _rpc(
client,
"workflow.admin.source_registry.apply",
{},
)
assert payload["error"]["data"]["code"] == "source_registry_unavailable"
Add a fake method test:
async def test_rpc_source_registry_apply_returns_summary(tmp_path) -> None:
admin = AsyncMock()
admin.apply_registry_changes.return_value = {
"applied": True,
"registered": ["demo.new"],
"updated": [],
"removed": [],
"connection_count": 1,
"registry_entry_count": 1,
}
server = build_local_static_workflow_server(tmp_path / "store")
server.source_registry_admin = admin
app = create_rpc_app(server)
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
) as client:
payload = await _rpc(
client,
"workflow.admin.source_registry.apply",
{},
)
assert payload["result"]["applied"] is True
assert payload["result"]["registered"] == ["demo.new"]
admin.apply_registry_changes.assert_awaited_once()
Add a client method existence test near existing client tests:
async def test_rpc_client_source_registry_apply_method_exists() -> None:
calls = []
class Client(RpcSourceRegistryClientMixin):
async def _call(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
calls.append((method, params))
return {"applied": True}
payload = await Client().apply_registry_changes()
assert payload["applied"] is True
assert calls == [("workflow.admin.source_registry.apply", {})]
- Step 2: Run failing RPC tests
Run:
uv run pytest tests/wf_transport_rpc_http/test_source_registry_rpc.py -q
Expected: fail because the method/client do not exist.
- Step 3: Add params model
In src/wf_transport_rpc_http/models.py, add:
class ApplyRegistryChangesParams(RpcParamsModel):
pass
- Step 4: Register JSON-RPC method
In src/wf_transport_rpc_http/methods_source_registry.py, import ApplyRegistryChangesParams.
Add inside register_methods:
@entrypoint.method(
name="workflow.admin.source_registry.apply",
errors=[WorkflowRpcError],
)
async def workflow_admin_source_registry_apply(
params: ApplyRegistryChangesParams = RpcParams(),
) -> dict[str, Any]:
admin = _require_source_registry_admin(server, operation="apply")
try:
return await admin.apply_registry_changes()
except (ValueError, KeyError, LookupError, FileNotFoundError, RuntimeError) as exc:
raise_workflow_rpc_error(exc)
- Step 5: Add client method
In src/wf_transport_rpc_http/client_source_registry.py, add:
async def apply_registry_changes(self) -> dict[str, Any]:
return await self._call(
"workflow.admin.source_registry.apply",
{},
)
- Step 6: Run RPC tests
Run:
uv run pytest tests/wf_transport_rpc_http/test_source_registry_rpc.py -q
Expected: pass.
Task 4: Add CLI Apply Command
Files:
-
Modify:
src/wf_cli/commands/source_registry.py -
Test:
tests/wf_cli/test_source_registry.py -
Step 1: Add failing CLI test
In tests/wf_cli/test_source_registry.py, add:
def test_registry_apply_calls_surface(monkeypatch) -> None:
surface = MagicMock()
surface.apply_registry_changes.return_value = {
"applied": True,
"registered": ["demo.new"],
"updated": [],
"removed": [],
"connection_count": 1,
"registry_entry_count": 1,
}
_patch_context(monkeypatch, source_registry_admin=surface)
result = CliRunner().invoke(app, ["apply"])
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["applied"] is True
assert payload["registered"] == ["demo.new"]
Use the existing test helper names in that file. If the helper is named differently, adapt only the helper call; keep the assertion payload.
- Step 2: Run failing CLI tests
Run:
uv run pytest tests/wf_cli/test_source_registry.py -q
Expected: fail because apply command does not exist.
- Step 3: Add command
In src/wf_cli/commands/source_registry.py, add:
@app.command("apply")
def apply_registry_changes(ctx: typer.Context) -> None:
"""Apply desired registry state to the running server."""
context = load_cli_context_from_typer(ctx)
admin = _require_registry_admin(context)
payload = asyncio.run(admin.apply_registry_changes())
emit_json(payload)
- Step 4: Run CLI tests
Run:
uv run pytest tests/wf_cli/test_source_registry.py -q
Expected: pass.
Task 5: MCP-Backed RPC Integration Test
Files:
-
Modify:
tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py -
Step 1: Add integration test
Add a test that proves persisted registry changes affect live source inventory after apply:
async def test_mcp_backed_rpc_applies_source_registry_changes(tmp_path) -> None:
config = BrokerConfig(store_root=tmp_path / "store", connections=[])
server = build_workflow_server_from_config(config)
app = create_rpc_app(server)
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
) as client:
await _rpc(
client,
"workflow.admin.source_registry.add",
{
"entry": {
"kind": "mcp",
"id": "dynamic.default",
"enabled": True,
"provider": "dynamic",
"account": "default",
"transport": {
"kind": "stdio",
"command": "dynamic-server",
"args": [],
"env": {},
},
}
},
)
before = await _rpc(
client,
"workflow.sources.list",
{"limit": 50},
)
applied = await _rpc(
client,
"workflow.admin.source_registry.apply",
{},
)
after = await _rpc(
client,
"workflow.sources.list",
{"limit": 50},
)
before_ids = {source["id"] for source in before["result"]["sources"]}
after_ids = {source["id"] for source in after["result"]["sources"]}
assert "dynamic.default" not in before_ids
assert applied["result"]["registered"] == ["dynamic.default"]
assert "dynamic.default" in after_ids
If the existing helper for source list uses a different JSON-RPC method name, use the method already registered in methods_sources.py.
- Step 2: Run integration test
Run:
uv run pytest tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py -q
Expected: pass.
Task 6: Docs and Final Verification
Files:
-
Modify:
docs/current_roadmap.md -
Modify:
docs/superpowers/specs/2026-06-03-store-backed-source-registry-design.md -
Modify:
docs/wf_cli.md -
Step 1: Update roadmap
In docs/current_roadmap.md, under "Source registry apply/reload", add:
Completed: desired source registry mutations can now be applied explicitly
through `wf admin registry apply` / `workflow.admin.source_registry.apply`.
V1 apply reconciles registry state into the current server connection/source
graph; it does not auto-apply mutations, mutate config files, or remount
MCP proxy providers.
- Step 2: Update source registry spec
In docs/superpowers/specs/2026-06-03-store-backed-source-registry-design.md, add an "Apply semantics" section:
### Apply Semantics
Registry mutation commands write desired persisted state. They do not implicitly
change the running server. `apply_registry_changes` is the explicit boundary
that reconciles desired registry state with the current runtime source graph.
The apply operation mirrors config reload reconciliation by calling the same
connection/source merge logic. It preserves `locked` config shadowing and `seed`
config handoff rules. It does not mutate config files, remount public MCP proxy
providers, or handle upstream credential prompts.
- Step 3: Update CLI docs
In docs/wf_cli.md, near source registry commands, add:
After `wf admin registry add/update/enable/disable/remove`, call:
```bash
wf --url http://127.0.0.1:8765/rpc admin registry apply
Apply updates the running server's source graph from desired registry state. It is explicit in v1; registry mutations are not auto-applied.
- [ ] **Step 4: Run focused tests**
Run:
```bash
uv run pytest tests/wf_api/test_source_registry_admin_api.py tests/wf_mcp/service/test_source_registry_admin.py tests/wf_transport_rpc_http/test_source_registry_rpc.py tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py tests/wf_cli/test_source_registry.py -q
Expected: pass.
- Step 5: Run lint/type checks
Run:
uv run ruff check src/wf_api src/wf_mcp/broker src/wf_transport_rpc_http src/wf_cli tests/wf_api tests/wf_mcp tests/wf_transport_rpc_http tests/wf_cli
uv run basedpyright --level error src/wf_api src/wf_mcp/broker src/wf_transport_rpc_http src/wf_cli tests/wf_api tests/wf_mcp tests/wf_transport_rpc_http tests/wf_cli
Expected: 0 errors.
- Step 6: Commit
Run:
git add src/wf_api src/wf_mcp/broker src/wf_transport_rpc_http src/wf_cli tests/wf_api tests/wf_mcp tests/wf_transport_rpc_http tests/wf_cli docs/current_roadmap.md docs/superpowers/specs/2026-06-03-store-backed-source-registry-design.md docs/wf_cli.md
git commit -m "feat: apply source registry changes"
Self-Review Checklist
- Registry mutation methods still only persist desired state.
apply_registry_changesis the only new runtime-adoption operation.- Local/static servers still return
source_registry_unavailable. - Locked config entries still shadow same-id registry entries.
- Seed config entries still bootstrap/yield according to existing merge logic.
- No proxy/FastMCP remount behavior is introduced.
- No registry operation mutates
wf_mcp.config.jsonor neutral workflow config.