refactor: move server startup CLI to wf_server
This commit is contained in:
@@ -81,11 +81,9 @@ auth admin are implemented. The next work is polish, not new broad surfaces.
|
||||
- New source families should follow the generic runtime source lifecycle rather
|
||||
than being forced through MCP `ConnectionConfig`:
|
||||
[`runtime source lifecycle`](superpowers/specs/2026-06-09-runtime-source-lifecycle.md).
|
||||
- Server startup policy should move out of the JSON-RPC transport package and
|
||||
into `wf_server` before adding more hosted transports:
|
||||
- Completed: server startup policy moved to `wf_server.cli`; JSON-RPC HTTP
|
||||
remains in `wf_transport_rpc_http`:
|
||||
[`server CLI and transport boundary`](superpowers/specs/2026-06-10-server-cli-transport-boundary.md).
|
||||
Active implementation plan:
|
||||
[`server CLI ownership move`](superpowers/plans/2026-06-10-server-cli-ownership-move.md).
|
||||
- Deferred auth work: OAuth/OIDC, production secret manager integration,
|
||||
encrypted-at-rest file format, and provider-specific display models.
|
||||
- Active specs:
|
||||
|
||||
+4
-5
@@ -11,8 +11,8 @@ paths should go through `wf_server` plus transport/source packages.
|
||||
| `wf_core` | Deterministic workflow kernel: models, validation, runtime, run state, traces, interrupts, foreach, and path/state operations. | Runtime users, `wf_authoring`, workflow adapters. |
|
||||
| `wf_authoring` | Ergonomic workflow construction: `@node`, `NodeSpec`, builder DSL, conditions, path helpers, reusable ops, subgraph nodes. | Humans, tests, future LLM workflow builders. |
|
||||
| `wf_api` | Workflow application surface over core/artifacts/platform: capabilities, drafts, artifacts, deployments, runs, and source/admin surfaces. | `wf_cli`, `wf_server`, JSON-RPC clients, future transports. |
|
||||
| `wf_server` | Durable server composition boundary around `WorkflowApi` plus optional admin/source-registry surfaces. Target home for server startup CLI/policy. | Transport packages and server startup code. |
|
||||
| `wf_transport_rpc_http` | JSON-RPC-over-HTTP app/client. The current `wf-rpc-server` CLI is slated to move to `wf_server` with a compatibility shim. | Remote `wf` clients and local server smoke tests. |
|
||||
| `wf_server` | Durable server composition boundary around `WorkflowApi` plus optional admin/source-registry surfaces. Owns the `wf-rpc-server` startup CLI/policy. | Transport packages and server startup code. |
|
||||
| `wf_transport_rpc_http` | JSON-RPC-over-HTTP app/client and compatibility CLI shim. | Remote `wf` clients and local server smoke tests. |
|
||||
| `wf_sources_mcp` | MCP-as-upstream-source implementation: ids, registry DTOs, auth/catalog stores, discovery, SDK client/facade, runtime pool, wrappers. | `wf_server`, broker glue, MCP source tests. |
|
||||
| `wf_mcp` | MCP frontend/compatibility package: legacy `wf-mcp` entrypoints, broker glue, proxy/admin tools, and shims while extraction continues. | Compatibility callers and MCP transport work. |
|
||||
| `wf_cli` | Command-line frontend over local or remote workflow APIs. | Humans, scripts, agent skills. |
|
||||
@@ -39,9 +39,8 @@ paths should go through `wf_server` plus transport/source packages.
|
||||
upstream tools/resources/prompts.
|
||||
- `wf_mcp`: MCP-specific frontend and compatibility package.
|
||||
- `wf-mcp`: legacy/special-purpose MCP script from `pyproject.toml`.
|
||||
- `wf-rpc-server`: preferred durable workflow server script for CLI/API clients;
|
||||
currently implemented in `wf_transport_rpc_http.cli`, but the target owner is
|
||||
`wf_server.cli`.
|
||||
- `wf-rpc-server`: preferred durable workflow server script for CLI/API clients,
|
||||
implemented by `wf_server.cli`.
|
||||
- `wf_mcp.broker.WfMcpService.get_catalog()`: backend MCP catalog snapshots.
|
||||
- `wf_mcp.broker.WfMcpService.get_planner_catalog()`: backend snapshots plus
|
||||
broker-local workflow sources such as `wf.std` and `wf.mcp`.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Date: 2026-06-10
|
||||
|
||||
Status: design direction; implementation not started
|
||||
Status: first move slice implemented
|
||||
|
||||
Related:
|
||||
|
||||
@@ -90,6 +90,16 @@ wf-rpc-server = "wf_server.cli:main"
|
||||
Keep `wf_transport_rpc_http.cli` temporarily so tests and imports do not break
|
||||
immediately.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
First move slice complete:
|
||||
|
||||
- `wf-rpc-server` script entrypoint points at `wf_server.cli:main`.
|
||||
- `wf_server.cli` owns startup config parsing and server composition.
|
||||
- `wf_transport_rpc_http.cli` remains as a compatibility shim.
|
||||
- `wf_transport_rpc_http.create_rpc_app(server)` remains the JSON-RPC HTTP
|
||||
transport adapter.
|
||||
|
||||
## Why Not Move `create_rpc_app`
|
||||
|
||||
`create_rpc_app(server)` is transport code:
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ dependencies = [
|
||||
[project.scripts]
|
||||
wf = "wf_cli.app:main"
|
||||
wf-mcp = "wf_mcp.cli:main"
|
||||
wf-rpc-server = "wf_transport_rpc_http.cli:main"
|
||||
wf-rpc-server = "wf_server.cli:main"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
import uvicorn
|
||||
|
||||
from wf_config import (
|
||||
FilesystemStoreConfig,
|
||||
RpcHttpTransportConfig,
|
||||
load_workflow_config,
|
||||
)
|
||||
from wf_server.config import (
|
||||
build_workflow_server_from_legacy_mcp_config,
|
||||
build_workflow_server_from_workflow_config,
|
||||
)
|
||||
from wf_server.context import build_local_static_workflow_server
|
||||
from wf_transport_rpc_http import create_rpc_app
|
||||
|
||||
app = typer.Typer(add_completion=False)
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def serve(
|
||||
config: Path | None = typer.Option(
|
||||
None,
|
||||
"--config",
|
||||
help="Path to neutral workflow config JSON.",
|
||||
),
|
||||
store_root: Path | None = typer.Option(
|
||||
None,
|
||||
"--store-root",
|
||||
help="Override filesystem workflow store root.",
|
||||
),
|
||||
mcp_config: Path | None = typer.Option(
|
||||
None,
|
||||
"--mcp-config",
|
||||
help="Path to MCP broker config JSON for MCP-backed workflow server.",
|
||||
),
|
||||
host: str | None = typer.Option(
|
||||
None,
|
||||
"--host",
|
||||
help="Override RPC bind host; defaults to config or 127.0.0.1.",
|
||||
),
|
||||
port: int | None = typer.Option(
|
||||
None,
|
||||
"--port",
|
||||
min=1,
|
||||
max=65535,
|
||||
help="Override RPC bind port; defaults to config or 8765.",
|
||||
),
|
||||
) -> None:
|
||||
"""Serve WorkflowApi over JSON-RPC HTTP."""
|
||||
if mcp_config is not None and store_root is not None:
|
||||
raise typer.BadParameter("--mcp-config cannot be combined with --store-root")
|
||||
|
||||
resolved_store_root = store_root
|
||||
resolved_host = host
|
||||
resolved_port = port
|
||||
resolved_rpc_path = "/rpc"
|
||||
|
||||
server = None
|
||||
if mcp_config is not None:
|
||||
server = build_workflow_server_from_legacy_mcp_config(mcp_config)
|
||||
|
||||
if config is not None:
|
||||
workflow_config = load_workflow_config(config)
|
||||
store = workflow_config.server.workflow_store
|
||||
if server is None and store_root is None:
|
||||
server = build_workflow_server_from_workflow_config(workflow_config)
|
||||
elif server is None:
|
||||
has_mcp_sources = any(
|
||||
getattr(source, "kind", None) == "mcp"
|
||||
for source in workflow_config.server.sources
|
||||
)
|
||||
if has_mcp_sources:
|
||||
raise typer.BadParameter(
|
||||
"--store-root cannot override MCP-source config"
|
||||
)
|
||||
if not isinstance(store, FilesystemStoreConfig):
|
||||
raise typer.BadParameter(
|
||||
"wf-rpc-server currently requires filesystem store"
|
||||
)
|
||||
resolved_store_root = resolved_store_root or store.root
|
||||
rpc_transport = next(
|
||||
(
|
||||
transport
|
||||
for transport in workflow_config.server.transports
|
||||
if isinstance(transport, RpcHttpTransportConfig)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if rpc_transport is not None:
|
||||
resolved_host = host or rpc_transport.host
|
||||
resolved_port = port or rpc_transport.port
|
||||
resolved_rpc_path = rpc_transport.path
|
||||
|
||||
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)
|
||||
|
||||
rpc_app = create_rpc_app(server, rpc_path=resolved_rpc_path)
|
||||
uvicorn.run(
|
||||
rpc_app,
|
||||
host=resolved_host or "127.0.0.1",
|
||||
port=resolved_port or 8765,
|
||||
access_log=False,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
app()
|
||||
@@ -1,116 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from wf_server.cli import app, main
|
||||
|
||||
import typer
|
||||
import uvicorn
|
||||
|
||||
from wf_config import (
|
||||
FilesystemStoreConfig,
|
||||
RpcHttpTransportConfig,
|
||||
load_workflow_config,
|
||||
)
|
||||
from wf_server.config import (
|
||||
build_workflow_server_from_legacy_mcp_config,
|
||||
build_workflow_server_from_workflow_config,
|
||||
)
|
||||
from wf_server.context import build_local_static_workflow_server
|
||||
|
||||
from .app import create_rpc_app
|
||||
|
||||
app = typer.Typer(add_completion=False)
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def serve(
|
||||
config: Path | None = typer.Option(
|
||||
None,
|
||||
"--config",
|
||||
help="Path to neutral workflow config JSON.",
|
||||
),
|
||||
store_root: Path | None = typer.Option(
|
||||
None,
|
||||
"--store-root",
|
||||
help="Override filesystem workflow store root.",
|
||||
),
|
||||
mcp_config: Path | None = typer.Option(
|
||||
None,
|
||||
"--mcp-config",
|
||||
help="Path to MCP broker config JSON for MCP-backed workflow server.",
|
||||
),
|
||||
host: str | None = typer.Option(
|
||||
None,
|
||||
"--host",
|
||||
help="Override RPC bind host; defaults to config or 127.0.0.1.",
|
||||
),
|
||||
port: int | None = typer.Option(
|
||||
None,
|
||||
"--port",
|
||||
min=1,
|
||||
max=65535,
|
||||
help="Override RPC bind port; defaults to config or 8765.",
|
||||
),
|
||||
) -> None:
|
||||
"""Serve WorkflowApi over JSON-RPC HTTP."""
|
||||
if mcp_config is not None and store_root is not None:
|
||||
raise typer.BadParameter("--mcp-config cannot be combined with --store-root")
|
||||
|
||||
resolved_store_root = store_root
|
||||
resolved_host = host
|
||||
resolved_port = port
|
||||
resolved_rpc_path = "/rpc"
|
||||
|
||||
server = None
|
||||
if mcp_config is not None:
|
||||
server = build_workflow_server_from_legacy_mcp_config(mcp_config)
|
||||
|
||||
if config is not None:
|
||||
workflow_config = load_workflow_config(config)
|
||||
store = workflow_config.server.workflow_store
|
||||
if server is None and store_root is None:
|
||||
server = build_workflow_server_from_workflow_config(workflow_config)
|
||||
elif server is None:
|
||||
has_mcp_sources = any(
|
||||
getattr(source, "kind", None) == "mcp"
|
||||
for source in workflow_config.server.sources
|
||||
)
|
||||
if has_mcp_sources:
|
||||
raise typer.BadParameter(
|
||||
"--store-root cannot override MCP-source config"
|
||||
)
|
||||
if not isinstance(store, FilesystemStoreConfig):
|
||||
raise typer.BadParameter(
|
||||
"wf-rpc-server currently requires filesystem store"
|
||||
)
|
||||
resolved_store_root = resolved_store_root or store.root
|
||||
rpc_transport = next(
|
||||
(
|
||||
transport
|
||||
for transport in workflow_config.server.transports
|
||||
if isinstance(transport, RpcHttpTransportConfig)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if rpc_transport is not None:
|
||||
resolved_host = host or rpc_transport.host
|
||||
resolved_port = port or rpc_transport.port
|
||||
resolved_rpc_path = rpc_transport.path
|
||||
|
||||
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)
|
||||
|
||||
rpc_app = create_rpc_app(server, rpc_path=resolved_rpc_path)
|
||||
uvicorn.run(
|
||||
rpc_app,
|
||||
host=resolved_host or "127.0.0.1",
|
||||
port=resolved_port or 8765,
|
||||
access_log=False,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
app()
|
||||
__all__ = ["app", "main"]
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from wf_transport_rpc_http.cli import app
|
||||
from wf_server.cli import app
|
||||
|
||||
|
||||
def test_rpc_server_cli_help_mentions_store_root() -> None:
|
||||
@@ -86,11 +86,11 @@ def test_rpc_server_cli_uses_configured_store_and_transport(
|
||||
captured["access_log"] = access_log
|
||||
|
||||
monkeypatch.setattr(
|
||||
"wf_transport_rpc_http.cli.build_workflow_server_from_workflow_config",
|
||||
"wf_server.cli.build_workflow_server_from_workflow_config",
|
||||
fake_build_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)
|
||||
monkeypatch.setattr("wf_server.cli.create_rpc_app", fake_create_rpc_app)
|
||||
monkeypatch.setattr("wf_server.cli.uvicorn.run", fake_uvicorn_run)
|
||||
|
||||
result = CliRunner().invoke(app, ["--config", str(config_path)])
|
||||
|
||||
@@ -131,11 +131,11 @@ def test_rpc_server_cli_uses_mcp_config_server(monkeypatch, tmp_path) -> None:
|
||||
captured["access_log"] = access_log
|
||||
|
||||
monkeypatch.setattr(
|
||||
"wf_transport_rpc_http.cli.build_workflow_server_from_legacy_mcp_config",
|
||||
"wf_server.cli.build_workflow_server_from_legacy_mcp_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)
|
||||
monkeypatch.setattr("wf_server.cli.create_rpc_app", fake_create_rpc_app)
|
||||
monkeypatch.setattr("wf_server.cli.uvicorn.run", fake_uvicorn_run)
|
||||
|
||||
result = CliRunner().invoke(
|
||||
app,
|
||||
@@ -203,8 +203,8 @@ def test_rpc_server_cli_mcp_config_builds_registry_capable_server(
|
||||
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)
|
||||
monkeypatch.setattr("wf_server.cli.create_rpc_app", fake_create_rpc_app)
|
||||
monkeypatch.setattr("wf_server.cli.uvicorn.run", fake_uvicorn_run)
|
||||
|
||||
result = CliRunner().invoke(app, ["--mcp-config", str(config_path)])
|
||||
|
||||
@@ -260,8 +260,8 @@ def test_rpc_server_cli_mcp_config_with_config_uses_transport_settings(
|
||||
captured["port"] = port
|
||||
captured["access_log"] = access_log
|
||||
|
||||
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)
|
||||
monkeypatch.setattr("wf_server.cli.create_rpc_app", fake_create_rpc_app)
|
||||
monkeypatch.setattr("wf_server.cli.uvicorn.run", fake_uvicorn_run)
|
||||
|
||||
result = CliRunner().invoke(
|
||||
app,
|
||||
@@ -304,11 +304,11 @@ def test_rpc_server_cli_config_with_mcp_source_uses_mcp_builder(
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"wf_transport_rpc_http.cli.build_workflow_server_from_workflow_config",
|
||||
"wf_server.cli.build_workflow_server_from_workflow_config",
|
||||
fake_build_from_workflow_config,
|
||||
)
|
||||
monkeypatch.setattr("wf_transport_rpc_http.cli.create_rpc_app", fake_create_rpc_app)
|
||||
monkeypatch.setattr("wf_transport_rpc_http.cli.uvicorn.run", fake_run)
|
||||
monkeypatch.setattr("wf_server.cli.create_rpc_app", fake_create_rpc_app)
|
||||
monkeypatch.setattr("wf_server.cli.uvicorn.run", fake_run)
|
||||
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text(
|
||||
@@ -333,10 +333,6 @@ def test_rpc_server_cli_config_with_mcp_source_uses_mcp_builder(
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from wf_transport_rpc_http.cli import app
|
||||
|
||||
result = CliRunner().invoke(app, ["--config", str(config_path)])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
@@ -413,11 +409,11 @@ def test_rpc_server_cli_config_uses_workflow_store_override(
|
||||
captured["app"] = app_obj
|
||||
|
||||
monkeypatch.setattr(
|
||||
"wf_transport_rpc_http.cli.build_workflow_server_from_workflow_config",
|
||||
"wf_server.cli.build_workflow_server_from_workflow_config",
|
||||
fake_build_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)
|
||||
monkeypatch.setattr("wf_server.cli.create_rpc_app", fake_create_rpc_app)
|
||||
monkeypatch.setattr("wf_server.cli.uvicorn.run", fake_uvicorn_run)
|
||||
|
||||
result = CliRunner().invoke(app, ["--config", str(config_path)])
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def test_rpc_server_cli_compat_import_matches_server_cli() -> None:
|
||||
from wf_server import cli as server_cli
|
||||
from wf_transport_rpc_http import cli as transport_cli
|
||||
|
||||
assert transport_cli.app is server_cli.app
|
||||
assert transport_cli.main is server_cli.main
|
||||
Reference in New Issue
Block a user