feat: serve mcp backed rpc server
This commit is contained in:
@@ -1,546 +0,0 @@
|
||||
# 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.WorkflowServer` is the neutral process-host shape.
|
||||
- `wf_server.build_local_static_workflow_server()` builds a local/static server and intentionally leaves `source_registry_admin=None`.
|
||||
- `wf_mcp.broker.config.build_service_from_config(config)` builds a `WfMcpService` with workflow stores, MCP adapters, source-registry startup merge, and configured connections.
|
||||
- `wf_mcp.broker.service.workflow_operation_context.context_from_service(service)` adapts `WfMcpService` into a neutral `WorkflowOperationContext`.
|
||||
- `wf_mcp.broker.service.source_registry_admin.SourceRegistryAdminProvider` provides desired-registry reads/mutations over `FileSourceRegistryStore`.
|
||||
- `wf_transport_rpc_http.create_rpc_app(server)` registers workflow, source, source-registry, and admin JSON-RPC methods over any `WorkflowServer`.
|
||||
|
||||
Boundary rule:
|
||||
|
||||
- `wf_server` must not import `wf_mcp`.
|
||||
- The MCP-backed constructor belongs in `wf_mcp` and returns a `wf_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`, not `wf_server -> wf_mcp`.
|
||||
- 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 `WorkflowServerConfig` is not exported and tests need it.
|
||||
- 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 `WorkflowServer` construction complete.
|
||||
- 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`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_mcp/test_mcp_workflow_server.py -q
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py -q
|
||||
```
|
||||
|
||||
Expected: PASS after Task 1.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```markdown
|
||||
- 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:
|
||||
|
||||
```markdown
|
||||
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:
|
||||
|
||||
```markdown
|
||||
- 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:
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
rg -n "wf_mcp" src/wf_server
|
||||
```
|
||||
|
||||
Expected: no matches.
|
||||
|
||||
- [ ] **Step 4: Final report**
|
||||
|
||||
Report:
|
||||
|
||||
- files created/modified
|
||||
- verification output
|
||||
- whether `wf_server` stayed 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 `WorkflowServer` construction remains future work".
|
||||
- Dependency direction: constructor lives in `wf_mcp`; `wf_server` remains 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.events` is currently typed as `InMemoryWorkflowEventRecorder`; if basedpyright rejects assigning `BrokerEventRecorder`, change that field type to `WorkflowEventRecorder` in `src/wf_server/context.py` and update no behavior. This is a type-only broadening and should be documented in the implementation report.
|
||||
@@ -0,0 +1,435 @@
|
||||
# RPC Server MCP Config Hookup 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:** Let `wf-rpc-server` serve a real MCP-backed `WorkflowServer` from an MCP broker config file, so `wf --url ...` can target a long-lived server with MCP sources and source-registry/admin surfaces.
|
||||
|
||||
**Architecture:** Keep JSON-RPC transport code as the place that selects a server composition for the process. `--mcp-config` loads legacy MCP broker config and calls `wf_mcp.broker.build_workflow_server_from_config`; existing `--store-root` / neutral `--config` behavior continues to build the local/static server. The transport still receives only a neutral `WorkflowServer` and calls `create_rpc_app(server)`.
|
||||
|
||||
**Tech Stack:** Python 3.14, Typer, `wf_transport_rpc_http`, `wf_mcp.broker`, `wf_server`, pytest, ruff, basedpyright.
|
||||
|
||||
---
|
||||
|
||||
## Current Context
|
||||
|
||||
Implemented before this plan:
|
||||
|
||||
- `wf_mcp.broker.server.build_workflow_server_from_config(config)` returns a neutral `WorkflowServer`.
|
||||
- `wf_transport_rpc_http.cli.serve()` currently always calls `build_local_static_workflow_server(...)`.
|
||||
- Existing CLI tests in `tests/wf_transport_rpc_http/test_cli.py` monkeypatch server construction, `create_rpc_app`, and `uvicorn.run`; use that pattern.
|
||||
|
||||
Out of scope:
|
||||
|
||||
- No new neutral config schema for MCP source transports.
|
||||
- No server hot reload.
|
||||
- No process manager/daemon work.
|
||||
- No auth redesign.
|
||||
- No real socket startup in tests.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify `src/wf_transport_rpc_http/cli.py`
|
||||
- Add `--mcp-config`.
|
||||
- Select MCP-backed server when `--mcp-config` is supplied.
|
||||
- Keep local/static path unchanged for existing config/store-root flows.
|
||||
- Modify `tests/wf_transport_rpc_http/test_cli.py`
|
||||
- Add help assertion for `--mcp-config`.
|
||||
- Add server-selection tests.
|
||||
- Modify `docs/wf_cli.md`
|
||||
- Document local/static server and MCP-backed server startup examples.
|
||||
- Modify `docs/current_roadmap.md`
|
||||
- Mark RPC server CLI MCP config hookup complete.
|
||||
- Modify `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md`
|
||||
- Record Slice 5 status.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add MCP Config Server Selection to RPC CLI
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/wf_transport_rpc_http/cli.py`
|
||||
- Modify: `tests/wf_transport_rpc_http/test_cli.py`
|
||||
|
||||
- [ ] **Step 1: Add failing help and selection tests**
|
||||
|
||||
In `tests/wf_transport_rpc_http/test_cli.py`, update `test_rpc_server_cli_help_mentions_store_root`:
|
||||
|
||||
```python
|
||||
def test_rpc_server_cli_help_mentions_store_root() -> None:
|
||||
result = CliRunner().invoke(app, ["--help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "--store-root" in result.output
|
||||
assert "--mcp-config" in result.output
|
||||
assert "--host" in result.output
|
||||
assert "--port" in result.output
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```python
|
||||
def test_rpc_server_cli_uses_mcp_config_server(monkeypatch, tmp_path) -> None:
|
||||
config_path = tmp_path / "wf_mcp.config.json"
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"store_root": str(tmp_path / "store"),
|
||||
"connections": [],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def fake_load_broker_config(path):
|
||||
captured["mcp_config_path"] = path
|
||||
return "broker-config"
|
||||
|
||||
def fake_build_mcp_server(config):
|
||||
captured["mcp_config"] = config
|
||||
return object()
|
||||
|
||||
def fake_create_rpc_app(server, *, rpc_path="/rpc"):
|
||||
captured["server"] = server
|
||||
captured["rpc_path"] = rpc_path
|
||||
return object()
|
||||
|
||||
def fake_uvicorn_run(app_obj, *, host, port, access_log):
|
||||
captured["app"] = app_obj
|
||||
captured["host"] = host
|
||||
captured["port"] = port
|
||||
captured["access_log"] = access_log
|
||||
|
||||
monkeypatch.setattr("wf_transport_rpc_http.cli.load_broker_config", fake_load_broker_config)
|
||||
monkeypatch.setattr(
|
||||
"wf_transport_rpc_http.cli.build_workflow_server_from_config",
|
||||
fake_build_mcp_server,
|
||||
)
|
||||
monkeypatch.setattr("wf_transport_rpc_http.cli.create_rpc_app", fake_create_rpc_app)
|
||||
monkeypatch.setattr("wf_transport_rpc_http.cli.uvicorn.run", fake_uvicorn_run)
|
||||
|
||||
result = CliRunner().invoke(
|
||||
app,
|
||||
[
|
||||
"--mcp-config",
|
||||
str(config_path),
|
||||
"--host",
|
||||
"127.0.0.9",
|
||||
"--port",
|
||||
"9988",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert captured["mcp_config_path"] == config_path
|
||||
assert captured["mcp_config"] == "broker-config"
|
||||
assert captured["server"] is not None
|
||||
assert captured["rpc_path"] == "/rpc"
|
||||
assert captured["host"] == "127.0.0.9"
|
||||
assert captured["port"] == 9988
|
||||
assert captured["access_log"] is False
|
||||
```
|
||||
|
||||
Add conflict test:
|
||||
|
||||
```python
|
||||
def test_rpc_server_cli_rejects_mcp_config_with_store_root(tmp_path) -> None:
|
||||
config_path = tmp_path / "wf_mcp.config.json"
|
||||
config_path.write_text(
|
||||
json.dumps({"store_root": str(tmp_path / "store"), "connections": []}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = CliRunner().invoke(
|
||||
app,
|
||||
[
|
||||
"--mcp-config",
|
||||
str(config_path),
|
||||
"--store-root",
|
||||
str(tmp_path / "other"),
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "--mcp-config cannot be combined with --store-root" in result.output
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_transport_rpc_http/test_cli.py -q
|
||||
```
|
||||
|
||||
Expected: FAIL because `--mcp-config` is not implemented.
|
||||
|
||||
- [ ] **Step 2: Implement `--mcp-config` option**
|
||||
|
||||
In `src/wf_transport_rpc_http/cli.py`, import:
|
||||
|
||||
```python
|
||||
from wf_mcp.broker import build_workflow_server_from_config, load_broker_config
|
||||
```
|
||||
|
||||
Add option to `serve(...)`:
|
||||
|
||||
```python
|
||||
mcp_config: Path | None = typer.Option(
|
||||
None,
|
||||
"--mcp-config",
|
||||
help="Path to MCP broker config JSON for MCP-backed workflow server.",
|
||||
),
|
||||
```
|
||||
|
||||
Update docstring:
|
||||
|
||||
```python
|
||||
"""Serve WorkflowApi over JSON-RPC HTTP."""
|
||||
```
|
||||
|
||||
Before resolving local/static store root, add validation:
|
||||
|
||||
```python
|
||||
if mcp_config is not None and store_root is not None:
|
||||
raise typer.BadParameter("--mcp-config cannot be combined with --store-root")
|
||||
```
|
||||
|
||||
If `mcp_config` is supplied, build MCP server:
|
||||
|
||||
```python
|
||||
server = None
|
||||
if mcp_config is not None:
|
||||
broker_config = load_broker_config(mcp_config)
|
||||
server = build_workflow_server_from_config(broker_config)
|
||||
```
|
||||
|
||||
Then keep existing neutral `--config` parsing for host/port/path. When selecting
|
||||
the final server, only call `build_local_static_workflow_server(...)` if
|
||||
`server is None`:
|
||||
|
||||
```python
|
||||
if server is None:
|
||||
if resolved_store_root is None:
|
||||
raise typer.BadParameter(
|
||||
"--store-root is required when --config is not supplied"
|
||||
)
|
||||
server = build_local_static_workflow_server(resolved_store_root)
|
||||
```
|
||||
|
||||
Important:
|
||||
|
||||
- `--config` may still be used with `--mcp-config` for transport host/port/path.
|
||||
- `--mcp-config` owns the workflow server/store/source side.
|
||||
- `--store-root` is local/static-only and must conflict with `--mcp-config`.
|
||||
|
||||
- [ ] **Step 3: Run CLI tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_transport_rpc_http/test_cli.py -q
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/wf_transport_rpc_http/cli.py tests/wf_transport_rpc_http/test_cli.py
|
||||
git commit -m "feat: serve mcp backed rpc server"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Prove MCP Config Server Supports Registry RPC Through CLI Path
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/wf_transport_rpc_http/test_cli.py`
|
||||
|
||||
- [ ] **Step 1: Add integration-style construction test**
|
||||
|
||||
Add this test to `tests/wf_transport_rpc_http/test_cli.py`:
|
||||
|
||||
```python
|
||||
def test_rpc_server_cli_mcp_config_builds_registry_capable_server(monkeypatch, tmp_path) -> None:
|
||||
config_path = tmp_path / "wf_mcp.config.json"
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"store_root": str(tmp_path / "store"),
|
||||
"connections": [],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def fake_create_rpc_app(server, *, rpc_path="/rpc"):
|
||||
captured["source_registry_admin"] = server.source_registry_admin
|
||||
captured["rpc_path"] = rpc_path
|
||||
return object()
|
||||
|
||||
def fake_uvicorn_run(app_obj, *, host, port, access_log):
|
||||
captured["host"] = host
|
||||
captured["port"] = port
|
||||
|
||||
monkeypatch.setattr("wf_transport_rpc_http.cli.create_rpc_app", fake_create_rpc_app)
|
||||
monkeypatch.setattr("wf_transport_rpc_http.cli.uvicorn.run", fake_uvicorn_run)
|
||||
|
||||
result = CliRunner().invoke(app, ["--mcp-config", str(config_path)])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert captured["source_registry_admin"] is not None
|
||||
assert captured["rpc_path"] == "/rpc"
|
||||
assert captured["host"] == "127.0.0.1"
|
||||
assert captured["port"] == 8765
|
||||
```
|
||||
|
||||
This test uses the real `load_broker_config()` and
|
||||
`build_workflow_server_from_config()` but still avoids starting uvicorn.
|
||||
|
||||
- [ ] **Step 2: Run CLI tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_transport_rpc_http/test_cli.py -q
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/wf_transport_rpc_http/test_cli.py
|
||||
git commit -m "test: cover mcp config rpc server path"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Update User-Facing Docs and Roadmap
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/wf_cli.md`
|
||||
- Modify: `docs/current_roadmap.md`
|
||||
- Modify: `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md`
|
||||
|
||||
- [ ] **Step 1: Add RPC server startup docs**
|
||||
|
||||
In `docs/wf_cli.md`, after the opening config paragraph, add:
|
||||
|
||||
```markdown
|
||||
## Remote Server
|
||||
|
||||
Start a local/static JSON-RPC workflow server:
|
||||
|
||||
```bash
|
||||
wf-rpc-server --store-root .wf_store --host 127.0.0.1 --port 8765
|
||||
```
|
||||
|
||||
Start a JSON-RPC server backed by MCP broker config and MCP-capable sources:
|
||||
|
||||
```bash
|
||||
wf-rpc-server --mcp-config wf_mcp.config.json --host 127.0.0.1 --port 8765
|
||||
```
|
||||
|
||||
Then point `wf` at it:
|
||||
|
||||
```bash
|
||||
wf --url http://127.0.0.1:8765/rpc cap list
|
||||
wf --url http://127.0.0.1:8765/rpc admin registry list
|
||||
```
|
||||
|
||||
`--mcp-config` owns the server's workflow stores, MCP connections, and source
|
||||
registry. `--store-root` is for the local/static server path and cannot be
|
||||
combined with `--mcp-config`.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update roadmap**
|
||||
|
||||
In `docs/current_roadmap.md`, under **Durable API service shape**, add:
|
||||
|
||||
```markdown
|
||||
- Completed: `wf-rpc-server --mcp-config wf_mcp.config.json` starts the
|
||||
JSON-RPC transport over an MCP-backed `WorkflowServer`, making the remote
|
||||
CLI path usable with MCP sources and desired source registry operations.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update long-lived API spec**
|
||||
|
||||
In `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md`,
|
||||
update status from "Slices 1-4 implemented" to "Slices 1-5 implemented", and
|
||||
add under implementation status:
|
||||
|
||||
```markdown
|
||||
- Slice 5 complete: `wf-rpc-server --mcp-config <path>` starts JSON-RPC over an
|
||||
MCP-backed `WorkflowServer`; `--store-root` remains local/static-only.
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run docs grep**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
rg -n "wf-rpc-server --mcp-config|Slices 1-5 implemented|--store-root.*--mcp-config" docs/wf_cli.md docs/current_roadmap.md docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md
|
||||
```
|
||||
|
||||
Expected: all three docs mention the new server path.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/wf_cli.md docs/current_roadmap.md docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md
|
||||
git commit -m "docs: document mcp backed rpc server"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Final Verification
|
||||
|
||||
**Files:**
|
||||
- Verify only.
|
||||
|
||||
- [ ] **Step 1: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/wf_transport_rpc_http/test_cli.py tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py tests/wf_mcp/test_mcp_workflow_server.py -q
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 2: Run lint and type checks**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run ruff check src/wf_transport_rpc_http/cli.py tests/wf_transport_rpc_http/test_cli.py
|
||||
uv run basedpyright --level error src/wf_transport_rpc_http/cli.py tests/wf_transport_rpc_http/test_cli.py
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: all commands exit 0. CRLF warnings from Git are acceptable; whitespace
|
||||
errors are not.
|
||||
|
||||
- [ ] **Step 3: Final report**
|
||||
|
||||
Report:
|
||||
|
||||
- changed files
|
||||
- verification output
|
||||
- exact command a user can run for MCP-backed RPC server
|
||||
- any deviations from this plan
|
||||
|
||||
Do not run the full suite unless the focused verification is green.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- Product fit: this is the smallest visible hook after MCP-backed `WorkflowServer` construction.
|
||||
- Boundary: `wf_transport_rpc_http.cli` imports `wf_mcp.broker` only for process startup selection; JSON-RPC app/method modules still take a neutral `WorkflowServer`.
|
||||
- Config semantics: `--mcp-config` and `--store-root` conflict because they own different server composition paths.
|
||||
- Testing: no real socket startup; tests monkeypatch `uvicorn.run`.
|
||||
Reference in New Issue
Block a user