refactor: move server startup CLI to wf_server

This commit is contained in:
lda
2026-06-10 22:57:11 +07:00 Verified
parent 308338cd0e
commit 5c60ff5b9c
9 changed files with 161 additions and 145 deletions
+2 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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 = [
+115
View File
@@ -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()
+2 -113
View File
@@ -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