docs: plan source registry startup merge

This commit is contained in:
lda
2026-06-03 23:58:01 +07:00 Verified
parent 332529cf89
commit 4e8726f36d
4 changed files with 380 additions and 5 deletions
+5 -3
View File
@@ -125,9 +125,11 @@ implementation state.
commands are still deferred. commands are still deferred.
- Next source registry slices are planned in - Next source registry slices are planned in
[2026-06-03 source registry next slices](./superpowers/plans/2026-06-03-source-registry-next-slices.md): [2026-06-03 source registry next slices](./superpowers/plans/2026-06-03-source-registry-next-slices.md):
first split generic registry mechanics into `wf_api`, then add MCP entry generic registry mechanics now live in `wf_api`, MCP entry conversion is
conversion, startup merge, desired-registry admin reads, and finally safe explicit and tested, and the next executable slice is
mutation commands. [startup merge](./superpowers/plans/2026-06-03-source-registry-startup-merge.md).
Desired-registry admin reads and safe mutation commands remain later
slices.
- Longer term: make the MCP frontend an adapter over these neutral workflow, - Longer term: make the MCP frontend an adapter over these neutral workflow,
source-admin, and config-admin surfaces so the old `wf_mcp` server entry source-admin, and config-admin surfaces so the old `wf_mcp` server entry
point can shrink or retire. point can shrink or retire.
@@ -29,11 +29,17 @@ This is useful and tested, but still MCP-shaped:
- source entry type is `McpSourceRegistryEntry` - source entry type is `McpSourceRegistryEntry`
- transport definitions are MCP transports - transport definitions are MCP transports
That is acceptable for Slice 1, but should be resolved before startup merge and mutation commands make the shape more permanent. Slice 2A then moved generic registry mechanics to `wf_api.source_registry`,
while `wf_mcp.source_registry` kept MCP-specific entries and transports. Slice
2B added `registry_entry_to_connection_config()`.
The next executable slice is startup merge:
`docs/superpowers/plans/2026-06-03-source-registry-startup-merge.md`.
## Slice Order ## Slice Order
1. **Slice 2A: Generic Registry Mechanics** 1. **Slice 2A: Generic Registry Mechanics**
- **Status: complete.**
- Move generic validation/store/file mechanics to `wf_api`. - Move generic validation/store/file mechanics to `wf_api`.
- Keep MCP entry/transport validation in `wf_mcp`. - Keep MCP entry/transport validation in `wf_mcp`.
- Prefer boring helpers over deep Pydantic generics where that keeps the - Prefer boring helpers over deep Pydantic generics where that keeps the
@@ -41,11 +47,13 @@ That is acceptable for Slice 1, but should be resolved before startup merge and
- Do not change runtime behavior. - Do not change runtime behavior.
2. **Slice 2B: MCP Entry Conversion** 2. **Slice 2B: MCP Entry Conversion**
- **Status: complete.**
- Add conversion helpers between `McpSourceRegistryEntry` and - Add conversion helpers between `McpSourceRegistryEntry` and
`ConnectionConfig`. `ConnectionConfig`.
- Keep config merge out of scope. - Keep config merge out of scope.
3. **Slice 3: Startup Merge** 3. **Slice 3: Startup Merge**
- **Status: planned.**
- Load registry at broker/server startup. - Load registry at broker/server startup.
- Merge config and registry with config precedence. - Merge config and registry with config precedence.
- Emit events/diagnostics for shadowed registry entries. - Emit events/diagnostics for shadowed registry entries.
@@ -216,6 +224,9 @@ Keep MCP tests in `tests/wf_mcp/test_source_registry.py`:
Add explicit conversion helpers so startup merge can convert registry entries Add explicit conversion helpers so startup merge can convert registry entries
into broker connection configs without duplicating field logic. into broker connection configs without duplicating field logic.
Status: complete. The implemented helper preserves entry metadata, `auth_ref`,
profile, transport details, and a `source_registry` marker.
### New Helpers ### New Helpers
In `src/wf_mcp/source_registry.py`: In `src/wf_mcp/source_registry.py`:
@@ -230,10 +241,11 @@ def registry_entry_to_connection_config(
account=entry.account, account=entry.account,
enabled=entry.enabled, enabled=entry.enabled,
metadata={ metadata={
**entry.metadata,
"auth_ref": entry.auth_ref,
"profile": entry.profile, "profile": entry.profile,
"transport": entry.transport.model_dump(mode="json"), "transport": entry.transport.model_dump(mode="json"),
"source_registry": True, "source_registry": True,
**entry.metadata,
}, },
) )
``` ```
@@ -0,0 +1,351 @@
# Source Registry Startup Merge 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:** Load persisted dynamic MCP source registry entries during broker/server construction and merge them with config-defined connections without changing behavior when the registry file is absent.
**Architecture:** Keep parsing/loading config separate from runtime hydration. `wf_mcp.source_registry` owns MCP registry entries and conversion to `ConnectionConfig`; `ConnectionService` owns connection reconciliation and source-catalog hydration; `build_service_from_config()` wires the default file store from `BrokerConfig.store_root`.
**Tech Stack:** Python 3.14, dataclasses, Pydantic v2, `wf_mcp.source_registry`, `ConnectionService`, `WfMcpService`, `build_service_from_config`, pytest, ruff, basedpyright.
---
## Preconditions
Completed before this plan:
- Generic registry mechanics exist in `wf_api.source_registry`.
- MCP-specific registry models remain in `wf_mcp.source_registry`.
- `registry_entry_to_connection_config()` exists and is tested.
- No startup/runtime merge uses the registry yet.
## Merge Rules
1. Config-defined connections win over registry entries with the same id.
2. Registry entries fill ids not present in config.
3. Reserved ids remain rejected by existing `ConnectionService` validation.
4. Shadowed registry entries must be visible through an event/diagnostic, not silently ignored.
5. Missing `source_registry.json` must preserve current config-only behavior.
Do not mutate config files. Do not delete auth/catalog files.
---
## Task 1: Add Merge Helper Tests First
- [ ] Add focused tests in `tests/wf_mcp/service/test_connection_service.py`.
- [ ] Import:
```python
from wf_mcp.source_registry import (
FileSourceRegistryStore,
McpSourceRegistryEntry,
SourceRegistryFile,
StdioSourceTransport,
)
```
- [ ] Add helper:
```python
def _registry_entry(
source_id: str = "demo.registry",
*,
enabled: bool = True,
) -> McpSourceRegistryEntry:
return McpSourceRegistryEntry(
id=source_id,
kind="mcp",
enabled=enabled,
provider="demo",
account=source_id.rsplit(".", 1)[-1],
transport=StdioSourceTransport(command="demo-server"),
)
```
- [ ] Add `test_connection_service_sync_merges_registry_entries`.
Expected behavior:
```python
store = FileSourceRegistryStore(tmp_path)
store.save_registry(SourceRegistryFile(sources=[_registry_entry()]))
service.sync_connections_from_config(
BrokerConfig(store_root=tmp_path, connections=[]),
source_registry_store=store,
)
assert [connection.id for connection in service.list_all()] == ["demo.registry"]
assert "demo.registry" in catalog.capability_sources
```
- [ ] Add `test_connection_service_sync_config_shadows_registry_entry`.
Expected behavior:
```python
store.save_registry(SourceRegistryFile(sources=[_registry_entry("demo.same")]))
service.sync_connections_from_config(
BrokerConfig(
store_root=tmp_path,
connections=[
ConnectionConfig(id="demo.same", server="demo", account="config"),
],
),
source_registry_store=store,
)
assert service.get("demo.same").account == "config"
assert service.events.list_events()[-1].kind == "source_registry_ignored_config_shadow"
assert service.events.list_events()[-1].connection_id == "demo.same"
```
- [ ] Add `test_connection_service_sync_registry_disabled_entry_hydrates_disabled_source`.
Expected behavior:
```python
store.save_registry(SourceRegistryFile(sources=[_registry_entry(enabled=False)]))
service.sync_connections_from_config(
BrokerConfig(store_root=tmp_path, connections=[]),
source_registry_store=store,
)
assert service.get("demo.registry").enabled is False
assert catalog.capability_sources["demo.registry"].enabled is False
```
- [ ] Run:
```bash
uv run pytest tests/wf_mcp/service/test_connection_service.py -q
```
Expected: new tests fail until Task 2.
---
## Task 2: Implement Registry-Aware Connection Reconciliation
- [ ] Update `src/wf_mcp/broker/service/connection_service.py`.
- [ ] Import:
```python
from ...source_registry import (
SourceRegistryStore,
registry_entry_to_connection_config,
)
```
- [ ] Change `sync_connections_from_config` signature:
```python
def sync_connections_from_config(
self,
config: BrokerConfig,
*,
source_registry_store: SourceRegistryStore | None = None,
) -> None:
```
- [ ] Build a merged connection list before existing remove/update/register logic:
```python
connections = list(config.connections)
config_ids = {connection.id for connection in connections}
if source_registry_store is not None:
registry = source_registry_store.load_registry()
for entry in registry.sources:
if entry.id in config_ids:
self.events.record_kind(
"source_registry_ignored_config_shadow",
connection_id=entry.id,
payload={
"server": entry.provider,
"account": entry.account,
"reason": "config_connection_takes_precedence",
},
)
continue
connections.append(registry_entry_to_connection_config(entry))
```
- [ ] Reuse the existing reconciliation logic against the merged `connections` list.
- [ ] Keep `BrokerConfig` immutable; do not mutate `config.connections`.
- [ ] Keep existing event behavior for registered/updated/removed connections.
- [ ] Add a short comment near the merge explaining config precedence.
- [ ] Run:
```bash
uv run pytest tests/wf_mcp/service/test_connection_service.py -q
```
Expected: all connection service tests pass.
---
## Task 3: Wire WfMcpService Facade
- [ ] Update `src/wf_mcp/broker/service/core.py`.
- [ ] Import `SourceRegistryStore` from `wf_mcp.source_registry`.
- [ ] Change facade method signature:
```python
def sync_connections_from_config(
self,
config: BrokerConfig,
*,
source_registry_store: SourceRegistryStore | None = None,
) -> None:
self.connection_service.sync_connections_from_config(
config,
source_registry_store=source_registry_store,
)
```
- [ ] Add/adjust a facade test in `tests/wf_mcp/service/test_connection_service.py` proving `WfMcpService.sync_connections_from_config(..., source_registry_store=store)` delegates and hydrates a registry connection.
---
## Task 4: Wire Build From Config
- [ ] Update `src/wf_mcp/broker/config.py`.
- [ ] Import `FileSourceRegistryStore`.
- [ ] Construct the store in `build_service_from_config(config)`:
```python
source_registry_store = FileSourceRegistryStore(config.store_root)
```
- [ ] Replace the manual connection loop with one registry-aware sync call:
```python
service.sync_connections_from_config(
config,
source_registry_store=source_registry_store,
)
for connection in service.connections.list_all():
if connection.server not in service.adapters:
service.register_adapter(connection.server, McpSdkAdapter())
```
Important:
- Preserve adapter registration for config-defined and registry-defined connections.
- Do not load the registry in `load_broker_config`; it should only parse config files.
- Do not save the registry during startup.
---
## Task 5: Add Build-Service Integration Tests
- [ ] Update `tests/wf_mcp/test_broker_server.py` or `tests/wf_mcp/server/test_config.py`.
- [ ] Add `test_build_service_from_config_loads_source_registry_entries`.
Setup:
```python
config = BrokerConfig(store_root=tmp_path, connections=[])
FileSourceRegistryStore(tmp_path).save_registry(
SourceRegistryFile(sources=[_registry_entry("fixture.registry")])
)
service = build_service_from_config(config)
```
Assertions:
```python
assert service.connections.get("fixture.registry").server == "fixture"
assert "fixture" in service.adapters
assert "fixture.registry" in service.capability_sources
```
- [ ] Add `test_build_service_from_config_config_shadows_registry`.
Assertions:
```python
assert service.connections.get("fixture.same").account == "config"
assert any(
event.kind == "source_registry_ignored_config_shadow"
and event.connection_id == "fixture.same"
for event in service.list_events()
)
```
- [ ] Add `test_build_service_from_config_absent_registry_preserves_existing_behavior` if no existing test already covers this. It can extend the current `test_build_service_from_config_registers_connections`.
---
## Task 6: Update Docs
- [ ] Update `docs/superpowers/specs/2026-06-03-store-backed-source-registry-design.md`.
Mark startup merge as complete or in-progress, depending on final status:
```md
### Slice 3: Startup Merge
Status: complete. Broker/service construction now loads `source_registry.json`,
merges config-defined connections with dynamic registry entries, preserves config
precedence, and emits `source_registry_ignored_config_shadow` for shadowed
registry entries.
```
- [ ] Update `docs/current_roadmap.md`.
Replace the source-registry note with:
```md
- Source registry startup merge is implemented: absent registry preserves
config-only behavior, registry-only entries hydrate as dynamic connections,
and config entries shadow same-id registry entries with an event.
```
- [ ] Update `docs/superpowers/plans/2026-06-03-source-registry-next-slices.md`.
Mark Slice 2A and Slice 2B complete if not already done, and mark Slice 3 complete after implementation.
---
## Task 7: Verify
- [ ] Run focused tests:
```bash
uv run pytest tests/wf_mcp/service/test_connection_service.py tests/wf_mcp/test_broker_server.py tests/wf_mcp/server/test_config.py -q
```
- [ ] Run source registry tests:
```bash
uv run pytest tests/wf_api/test_source_registry.py tests/wf_mcp/test_source_registry.py -q
```
- [ ] Run quality checks:
```bash
uv run ruff check src/wf_mcp/source_registry.py src/wf_mcp/broker/config.py src/wf_mcp/broker/service/connection_service.py src/wf_mcp/broker/service/core.py tests/wf_mcp/service/test_connection_service.py tests/wf_mcp/test_broker_server.py tests/wf_mcp/server/test_config.py
uv run basedpyright --level error src/wf_mcp/source_registry.py src/wf_mcp/broker/config.py src/wf_mcp/broker/service/connection_service.py src/wf_mcp/broker/service/core.py tests/wf_mcp/service/test_connection_service.py tests/wf_mcp/test_broker_server.py tests/wf_mcp/server/test_config.py
```
- [ ] If focused tests pass, run the full suite if time permits:
```bash
uv run pytest -q
```
---
## Acceptance Criteria
- Missing registry file keeps existing config-only behavior.
- Registry-only entries become registered connections and hydrated source catalog entries.
- Config entries shadow same-id registry entries.
- Shadowing emits `source_registry_ignored_config_shadow`.
- Disabled registry entries hydrate disabled connection/source state.
- `load_broker_config()` remains config-only parsing.
- No registry mutation commands are added in this slice.
- `wf_api` still imports no `wf_mcp`.
@@ -235,6 +235,9 @@ source ids, and concrete MCP transports all come from the MCP broker layer.
### Slice 2A: Generic Registry Mechanics ### Slice 2A: Generic Registry Mechanics
Status: complete. Generic registry mechanics live in `wf_api.source_registry`;
MCP source entries and transports remain in `wf_mcp.source_registry`.
- Move protocol-neutral registry mechanics to `wf_api.source_registry`. - Move protocol-neutral registry mechanics to `wf_api.source_registry`.
- Keep MCP source entries, MCP transports, connection-id parsing, and reserved - Keep MCP source entries, MCP transports, connection-id parsing, and reserved
id rules in `wf_mcp.source_registry`. id rules in `wf_mcp.source_registry`.
@@ -248,6 +251,10 @@ the generic mechanics and define their own entry models.
### Slice 2B: MCP Entry Conversion ### Slice 2B: MCP Entry Conversion
Status: complete. `registry_entry_to_connection_config()` converts MCP registry
entries to broker `ConnectionConfig` values while preserving metadata, `auth_ref`,
profile, transport details, enabled state, and source-registry origin.
- Add explicit conversion from `McpSourceRegistryEntry` to `ConnectionConfig`. - Add explicit conversion from `McpSourceRegistryEntry` to `ConnectionConfig`.
- Preserve structural fields such as provider, account, profile, transport, - Preserve structural fields such as provider, account, profile, transport,
enabled state, auth reference, and metadata. enabled state, auth reference, and metadata.
@@ -255,6 +262,9 @@ the generic mechanics and define their own entry models.
### Slice 3: Startup Merge ### Slice 3: Startup Merge
Implementation plan:
[2026-06-03 source registry startup merge](../plans/2026-06-03-source-registry-startup-merge.md).
- Load registry during server/broker construction. - Load registry during server/broker construction.
- Merge config + registry deterministically. - Merge config + registry deterministically.
- Emit diagnostics/events for ignored shadowed entries. - Emit diagnostics/events for ignored shadowed entries.