Files
lda-wf/docs/historical/superpowers/plans/2026-06-03-source-registry-store-slice-1.md
T

12 KiB

Source Registry Store Slice 1 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 validated source registry models and a filesystem store for desired server-owned source configuration, without wiring it into startup or mutation commands yet.

Architecture: The new registry is desired configuration state and stays separate from existing auth/catalog storage. Models live in wf_mcp for this first slice because current connection/source registry semantics are still MCP-provider-specific; the file-store interface is small enough to move later. Runtime merge, RPC/CLI mutation, and config reconciliation are explicitly deferred.

Tech Stack: Python 3.14, Pydantic v2, existing wf_mcp.connections.parse_connection_id, existing RESERVED_CONNECTION_IDS, pytest, ruff, basedpyright.


Scope

In scope:

  • Typed SourceRegistryFile model.
  • Typed McpSourceRegistryEntry model.
  • Typed StdioSourceTransport and HttpSourceTransport models.
  • Duplicate id validation.
  • Reserved id validation.
  • ID validation using existing connection id rules.
  • SourceRegistryStore protocol.
  • FileSourceRegistryStore using <store_root>/source_registry.json.
  • Atomic filesystem writes.
  • Tests for load missing file, save/load round trip, validation errors, and path.

Out of scope:

  • Startup merge with config.
  • Runtime hydration from registry.
  • Mutating API/CLI/RPC commands.
  • Auth record changes.
  • Catalog deletion or cleanup.
  • SQL/remote stores.

File Structure

  • Create src/wf_mcp/source_registry.py

    • Pydantic models and validation.
    • Protocol and file store.
    • No dependency on WfMcpService.
  • Modify src/wf_mcp/storage/__init__.py

    • No change in this slice unless the implementor decides a store export is needed.
    • Prefer exporting from wf_mcp.source_registry, not overloading wf_mcp.storage.
  • Create tests/wf_mcp/test_source_registry.py

    • Direct model/store tests.
  • Modify docs/current_roadmap.md

    • Mark Slice 1 complete after implementation.

Task 1: Add failing source registry model tests

Files:

  • Create: tests/wf_mcp/test_source_registry.py

  • Step 1: Write model validation tests

Create tests/wf_mcp/test_source_registry.py:

from __future__ import annotations

import pytest

from wf_mcp.source_registry import (
    HttpSourceTransport,
    McpSourceRegistryEntry,
    SourceRegistryFile,
    StdioSourceTransport,
)


def _entry(source_id: str = "github.work") -> McpSourceRegistryEntry:
    return McpSourceRegistryEntry(
        id=source_id,
        provider="github",
        account="work",
        transport=StdioSourceTransport(
            command="npx",
            args=("-y", "@modelcontextprotocol/server-github"),
            env={"GITHUB_TOKEN": "${GITHUB_TOKEN}"},
        ),
        auth_ref=source_id,
        metadata={"purpose": "tests"},
    )


def test_source_registry_entry_keeps_identity_and_transport_structural() -> None:
    entry = _entry()

    assert entry.id == "github.work"
    assert entry.provider == "github"
    assert entry.account == "work"
    assert entry.profile is None
    assert entry.transport.kind == "stdio"
    assert entry.transport.command == "npx"
    assert entry.auth_ref == "github.work"


def test_source_registry_accepts_http_transport() -> None:
    entry = McpSourceRegistryEntry(
        id="github.http",
        provider="github",
        account="work",
        transport=HttpSourceTransport(url="https://example.test/mcp"),
    )

    assert entry.transport.kind == "http"
    assert str(entry.transport.url) == "https://example.test/mcp"


def test_source_registry_rejects_duplicate_ids() -> None:
    with pytest.raises(ValueError, match="duplicate source id 'github.work'"):
        SourceRegistryFile(sources=[_entry("github.work"), _entry("github.work")])


def test_source_registry_rejects_reserved_ids() -> None:
    with pytest.raises(ValueError, match="reserved"):
        _entry("wf.admin")


def test_source_registry_rejects_unsafe_ids() -> None:
    with pytest.raises(ValueError, match="connection id"):
        _entry("../bad")
  • Step 2: Run tests and verify failure

Run:

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

Expected: FAIL because wf_mcp.source_registry does not exist yet.


Task 2: Implement source registry models

Files:

  • Create: src/wf_mcp/source_registry.py

  • Test: tests/wf_mcp/test_source_registry.py

  • Step 1: Add models and validators

Create src/wf_mcp/source_registry.py:

from __future__ import annotations

import json
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Annotated, Literal, Protocol

from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, field_validator, model_validator

from .connections import parse_connection_id
from .shared.names import RESERVED_CONNECTION_IDS


class SourceRegistryModel(BaseModel):
    """Base model for persisted source registry state; reject misspelled fields."""

    model_config = ConfigDict(extra="forbid")


class StdioSourceTransport(SourceRegistryModel):
    kind: Literal["stdio"] = "stdio"
    command: str = Field(min_length=1)
    args: tuple[str, ...] = ()
    env: dict[str, str] = Field(default_factory=dict)


class HttpSourceTransport(SourceRegistryModel):
    kind: Literal["http"] = "http"
    url: AnyHttpUrl
    headers: dict[str, str] = Field(default_factory=dict)


SourceTransport = Annotated[
    StdioSourceTransport | HttpSourceTransport,
    Field(discriminator="kind"),
]


class McpSourceRegistryEntry(SourceRegistryModel):
    """Desired MCP source configuration persisted by server-owned mutation."""

    id: str
    kind: Literal["mcp"] = "mcp"
    enabled: bool = True
    provider: str = Field(min_length=1)
    account: str = Field(min_length=1)
    profile: str | None = None
    transport: SourceTransport
    auth_ref: str | None = None
    metadata: dict[str, object] = Field(default_factory=dict)

    @field_validator("id")
    @classmethod
    def validate_id(cls, value: str) -> str:
        parse_connection_id(value)
        if value in RESERVED_CONNECTION_IDS:
            raise ValueError(f"source id {value!r} is reserved")
        return value


class SourceRegistryFile(SourceRegistryModel):
    version: Literal[1] = 1
    sources: list[McpSourceRegistryEntry] = Field(default_factory=list)

    @model_validator(mode="after")
    def validate_unique_source_ids(self) -> SourceRegistryFile:
        seen: set[str] = set()
        for source in self.sources:
            if source.id in seen:
                raise ValueError(f"duplicate source id {source.id!r}")
            seen.add(source.id)
        return self

    def source_map(self) -> dict[str, McpSourceRegistryEntry]:
        return {source.id: source for source in self.sources}


class SourceRegistryStore(Protocol):
    """Persistence boundary for desired server-owned source configuration."""

    def load_registry(self) -> SourceRegistryFile:
        """Return the stored registry, or an empty registry when absent."""
        ...

    def save_registry(self, registry: SourceRegistryFile) -> None:
        """Persist one validated registry atomically."""
        ...
  • Step 2: Run model tests

Run:

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

Expected: model tests PASS except store tests are not added yet.


Task 3: Add failing file store tests

Files:

  • Modify: tests/wf_mcp/test_source_registry.py

  • Step 1: Append file store tests

Append:

from pathlib import Path

from wf_mcp.source_registry import FileSourceRegistryStore


def test_file_source_registry_store_loads_empty_registry_when_missing(
    tmp_path: Path,
) -> None:
    store = FileSourceRegistryStore(tmp_path)

    registry = store.load_registry()

    assert registry.version == 1
    assert registry.sources == []
    assert store.path == tmp_path / "source_registry.json"


def test_file_source_registry_store_round_trips_registry(tmp_path: Path) -> None:
    store = FileSourceRegistryStore(tmp_path)
    registry = SourceRegistryFile(sources=[_entry("github.work")])

    store.save_registry(registry)
    loaded = store.load_registry()

    assert loaded.source_map()["github.work"].provider == "github"
    assert loaded.source_map()["github.work"].transport.kind == "stdio"


def test_file_source_registry_store_validates_loaded_registry(tmp_path: Path) -> None:
    store = FileSourceRegistryStore(tmp_path)
    store.path.write_text(
        '{"version": 1, "sources": [{"id": "wf.admin", "provider": "wf", '
        '"account": "admin", "transport": {"kind": "stdio", "command": "x"}}]}',
        encoding="utf-8",
    )

    with pytest.raises(ValueError, match="reserved"):
        store.load_registry()
  • Step 2: Run tests and verify failure

Run:

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

Expected: FAIL because FileSourceRegistryStore does not exist yet.


Task 4: Implement file store

Files:

  • Modify: src/wf_mcp/source_registry.py

  • Test: tests/wf_mcp/test_source_registry.py

  • Step 1: Add file store implementation

Append to src/wf_mcp/source_registry.py:

class FileSourceRegistryStore:
    """Filesystem implementation for desired source registry state."""

    def __init__(self, root: Path) -> None:
        self.root = root
        self.root.mkdir(parents=True, exist_ok=True)

    @property
    def path(self) -> Path:
        return self.root / "source_registry.json"

    def load_registry(self) -> SourceRegistryFile:
        if not self.path.exists():
            return SourceRegistryFile()
        data = json.loads(self.path.read_text(encoding="utf-8"))
        return SourceRegistryFile.model_validate(data)

    def save_registry(self, registry: SourceRegistryFile) -> None:
        # Validate again at the store boundary so callers cannot persist stale or
        # partially constructed model-like objects after mutation.
        validated = SourceRegistryFile.model_validate(
            registry.model_dump(mode="json")
        )
        payload = json.dumps(validated.model_dump(mode="json"), indent=2)
        tmp_path = self.path.with_name(f"{self.path.name}.tmp")
        tmp_path.write_text(payload, encoding="utf-8")
        tmp_path.replace(self.path)
  • Step 2: Add __all__

At the bottom of src/wf_mcp/source_registry.py, add:

__all__ = [
    "FileSourceRegistryStore",
    "HttpSourceTransport",
    "McpSourceRegistryEntry",
    "SourceRegistryFile",
    "SourceRegistryStore",
    "SourceTransport",
    "StdioSourceTransport",
]
  • Step 3: Run tests

Run:

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

Expected: PASS.


Task 5: Documentation and verification

Files:

  • Modify: docs/current_roadmap.md

  • Step 1: Update roadmap

In docs/current_roadmap.md, near the store-backed source registry note, add:

- First source registry implementation slice complete: validated registry
  models plus `FileSourceRegistryStore` exist, but startup merge and mutation
  commands are still deferred.
  • Step 2: Run verification

Run:

uv run pytest tests/wf_mcp/test_source_registry.py -q
uv run ruff check src/wf_mcp/source_registry.py tests/wf_mcp/test_source_registry.py
uv run ruff format --check src/wf_mcp/source_registry.py tests/wf_mcp/test_source_registry.py
uv run basedpyright --level error src/wf_mcp/source_registry.py tests/wf_mcp/test_source_registry.py

Expected:

  • pytest PASS

  • ruff check PASS

  • ruff format PASS

  • basedpyright 0 errors

  • Step 3: Commit

git add src/wf_mcp/source_registry.py tests/wf_mcp/test_source_registry.py docs/current_roadmap.md
git commit -m "feat: add source registry file store"

Self-Review

  • Spec coverage: Slice 1 only is covered: models, validation, file store, tests, docs.
  • Placeholder scan: no TBD/TODO placeholders.
  • Type consistency: SourceRegistryFile, McpSourceRegistryEntry, SourceRegistryStore, and FileSourceRegistryStore names are consistent.
  • Deferred work is explicit: startup merge, runtime hydration, RPC/CLI mutation, and auth/catalog cleanup are not part of this slice.