13 KiB
wf_config MCP Source Model 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 a neutral kind: "mcp" source variant to wf_config.server.sources[] so MCP source definitions can live in the wider workflow config model.
Architecture: Keep this slice pure wf_config: no imports from wf_mcp, no server construction changes, no runtime behavior changes. The new config shape should intentionally mirror wf_mcp.source_registry.McpSourceRegistryEntry, with the legacy ConnectionConfig.source_config_ownership policy renamed to a neutral config/source field.
Tech Stack: Pydantic v2 discriminated unions, AnyHttpUrl, existing tests/wf_config/test_config_models.py.
File Structure
- Modify
src/wf_config/models.py: add transport config models,McpSourceConfig, and include it in the existingSourceConfigunion. - Modify
src/wf_config/__init__.py: export the new config types. - Modify
tests/wf_config/test_config_models.py: add model parsing/validation tests for MCP sources. - Modify
docs/current_roadmap.mdanddocs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md: mark this slice complete after implementation.
Current Context
Current neutral config has:
class StdlibSourceConfig(WorkflowConfigModel):
kind: Literal["stdlib"]
id: Literal["wf.std", "wf.recipes"]
SourceConfig = Annotated[
StdlibSourceConfig,
Field(discriminator="kind"),
]
MCP’s desired source shape already exists at src/wf_mcp/source_registry.py as McpSourceRegistryEntry, but that module imports MCP-specific validators and models. Do not import it into wf_config.
The source id rule to mirror is currently in src/wf_mcp/connections.py:
CONNECTION_ID_PATTERN = r"^[A-Za-z0-9_][A-Za-z0-9_.-]*$"
and MCP ids must look like <provider>.<account>.
Task 1: Add MCP Source Config Tests
Files:
-
Modify:
tests/wf_config/test_config_models.py -
Step 1: Add imports
Update the import block from wf_config to include the new classes that will be implemented:
from wf_config import (
FilesystemStoreConfig,
HttpSourceTransportConfig,
LocalTargetConfig,
McpSourceConfig,
RpcHttpTargetConfig,
RpcHttpTransportConfig,
StdioSourceTransportConfig,
StdlibSourceConfig,
WorkflowConfigFile,
load_workflow_config,
)
- Step 2: Add stdio MCP source parsing test
Append this test:
def test_workflow_config_parses_mcp_stdio_source() -> None:
config = WorkflowConfigFile.model_validate(
{
"version": 1,
"server": {
"sources": [
{
"kind": "mcp",
"id": "everything.default",
"enabled": True,
"provider": "everything",
"account": "default",
"profile": "dev",
"ownership": "seed",
"transport": {
"kind": "stdio",
"command": "uvx",
"args": ["mcp-server-everything"],
"env": {"DEBUG": "1"},
},
"auth_ref": "auth.everything.default",
"metadata": {"description": "Everything test server"},
}
]
},
}
)
source = config.server.sources[0]
assert isinstance(source, McpSourceConfig)
assert source.id == "everything.default"
assert source.enabled is True
assert source.provider == "everything"
assert source.account == "default"
assert source.profile == "dev"
assert source.ownership == "seed"
assert isinstance(source.transport, StdioSourceTransportConfig)
assert source.transport.command == "uvx"
assert source.transport.args == ("mcp-server-everything",)
assert source.transport.env == {"DEBUG": "1"}
assert source.auth_ref == "auth.everything.default"
assert source.metadata["description"] == "Everything test server"
- Step 3: Add HTTP MCP source parsing test
Append this test:
def test_workflow_config_parses_mcp_http_source() -> None:
config = WorkflowConfigFile.model_validate(
{
"version": 1,
"server": {
"sources": [
{
"kind": "mcp",
"id": "context7.default",
"provider": "context7",
"account": "default",
"transport": {
"kind": "http",
"url": "http://127.0.0.1:3000/mcp",
"headers": {"X-Test": "yes"},
},
}
]
},
}
)
source = config.server.sources[0]
assert isinstance(source, McpSourceConfig)
assert source.enabled is True
assert source.ownership == "locked"
assert isinstance(source.transport, HttpSourceTransportConfig)
assert str(source.transport.url) == "http://127.0.0.1:3000/mcp"
assert source.transport.headers == {"X-Test": "yes"}
- Step 4: Add validation tests
Append these tests:
def test_workflow_config_rejects_mcp_source_without_provider_account_shape() -> None:
with pytest.raises(ValidationError, match="source id must look like"):
WorkflowConfigFile.model_validate(
{
"version": 1,
"server": {
"sources": [
{
"kind": "mcp",
"id": "everything",
"provider": "everything",
"account": "default",
"transport": {"kind": "stdio", "command": "uvx"},
}
]
},
}
)
def test_workflow_config_rejects_unsafe_mcp_source_id() -> None:
with pytest.raises(ValidationError, match="source id must start"):
WorkflowConfigFile.model_validate(
{
"version": 1,
"server": {
"sources": [
{
"kind": "mcp",
"id": ".hidden.default",
"provider": "hidden",
"account": "default",
"transport": {"kind": "stdio", "command": "uvx"},
}
]
},
}
)
def test_workflow_config_rejects_duplicate_source_ids_across_kinds() -> None:
with pytest.raises(ValidationError, match="duplicate source id"):
WorkflowConfigFile.model_validate(
{
"version": 1,
"server": {
"sources": [
{"kind": "stdlib", "id": "wf.std"},
{
"kind": "mcp",
"id": "wf.std",
"provider": "wf",
"account": "std",
"transport": {"kind": "stdio", "command": "uvx"},
},
]
},
}
)
- Step 5: Run tests and verify failure
Run:
uv run pytest tests/wf_config/test_config_models.py -q
Expected: fail with import errors for McpSourceConfig, StdioSourceTransportConfig, and HttpSourceTransportConfig.
Task 2: Implement Neutral MCP Source Config Models
Files:
-
Modify:
src/wf_config/models.py -
Step 1: Add imports
Update imports:
import re
from pathlib import Path
from typing import Annotated, Literal
- Step 2: Add ownership and source id constants
Place after ServerTransportConfig:
SourceConfigOwnership = Literal["locked", "seed"]
SOURCE_ID_PATTERN = r"^[A-Za-z0-9_][A-Za-z0-9_.-]*$"
- Step 3: Add transport models
Place before StdlibSourceConfig:
class StdioSourceTransportConfig(WorkflowConfigModel):
kind: Literal["stdio"] = "stdio"
command: str = Field(min_length=1)
args: tuple[str, ...] = ()
env: dict[str, str] = Field(default_factory=dict)
class HttpSourceTransportConfig(WorkflowConfigModel):
kind: Literal["http"] = "http"
url: AnyHttpUrl
headers: dict[str, str] = Field(default_factory=dict)
SourceTransportConfig = Annotated[
StdioSourceTransportConfig | HttpSourceTransportConfig,
Field(discriminator="kind"),
]
- Step 4: Add MCP source model
Place after StdlibSourceConfig:
class McpSourceConfig(WorkflowConfigModel):
"""Neutral config shape for MCP-backed workflow capability sources.
This intentionally mirrors `wf_mcp.source_registry.McpSourceRegistryEntry`
without importing MCP modules. `ownership` carries the old
`ConnectionConfig.source_config_ownership` policy with neutral terminology.
"""
kind: Literal["mcp"] = "mcp"
id: str
enabled: bool = True
provider: str = Field(min_length=1)
account: str = Field(min_length=1)
profile: str | None = None
ownership: SourceConfigOwnership = "locked"
transport: SourceTransportConfig
auth_ref: str | None = None
metadata: dict[str, object] = Field(default_factory=dict)
@field_validator("id")
@classmethod
def validate_source_id(cls, value: str) -> str:
if not re.fullmatch(SOURCE_ID_PATTERN, value):
raise ValueError(
"source id must start with alphanumeric or underscore and contain "
"only [A-Za-z0-9_.-]"
)
if "." not in value:
raise ValueError("source id must look like '<provider>.<account>'")
provider, account = value.split(".", 1)
if not provider or not account:
raise ValueError("source id must look like '<provider>.<account>'")
return value
- Step 5: Extend SourceConfig union
Replace the existing union with:
SourceConfig = Annotated[
StdlibSourceConfig | McpSourceConfig,
Field(discriminator="kind"),
]
- Step 6: Run tests
Run:
uv run pytest tests/wf_config/test_config_models.py -q
Expected: pass.
Task 3: Export New Config Types
Files:
-
Modify:
src/wf_config/__init__.py -
Step 1: Add imports
Update the import block to include:
HttpSourceTransportConfig,
McpSourceConfig,
SourceConfigOwnership,
SourceTransportConfig,
StdioSourceTransportConfig,
- Step 2: Add
__all__entries
Add:
"HttpSourceTransportConfig",
"McpSourceConfig",
"SourceConfigOwnership",
"SourceTransportConfig",
"StdioSourceTransportConfig",
- Step 3: Run tests
Run:
uv run pytest tests/wf_config/test_config_models.py -q
Expected: pass.
Task 4: Document Slice Completion
Files:
-
Modify:
docs/current_roadmap.md -
Modify:
docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md -
Step 1: Update roadmap
In docs/current_roadmap.md, under the "Wider wf_config source model" bullet, append:
First slice complete: `wf_config.server.sources[]` now accepts
`kind: "mcp"` entries with stdio/http transport, auth reference, metadata,
enabled flag, and `locked` / `seed` ownership policy. Runtime composition
from these entries is the next slice.
- Step 2: Update spec
In docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md, under the Slice 1 text, append:
Model slice complete when `wf_config.server.sources[]` accepts `kind: "mcp"`
entries. The next slice converts those neutral source entries into MCP
broker runtime connections and server composition.
- Step 3: Run docs diff
Run:
git diff -- docs/current_roadmap.md docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md
Expected: only the completion notes above.
Task 5: Final Verification and Commit
Files:
-
All touched files from prior tasks.
-
Step 1: Run focused tests
Run:
uv run pytest tests/wf_config/test_config_models.py -q
Expected: all tests pass.
- Step 2: Run lint/type checks
Run:
uv run ruff check src/wf_config tests/wf_config
uv run basedpyright --level error src/wf_config tests/wf_config
Expected: both pass with 0 errors.
- Step 3: Commit
Run:
git add src/wf_config tests/wf_config docs/current_roadmap.md docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md
git commit -m "feat: add mcp source config model"
Self-Review Checklist
- This plan does not import
wf_mcpintowf_config. - This plan does not change runtime behavior.
- The ownership field is neutral (
ownership), while docs explain its legacy origin. - Duplicate source id validation continues to work across all source kinds.