# Long-Lived Workflow API Boundary Date: 2026-06-03 Status: Slices 1-5 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. Related: - [wf_api architecture](../../wf_api_architecture.md) - [wf_mcp architecture](../../wf_mcp_architecture.md) - [Persisted run/resume contract](./2026-06-03-persisted-run-resume-contract.md) - [Server CLI and transport boundary](./2026-06-10-server-cli-transport-boundary.md) - [Current roadmap](../../current_roadmap.md) ## Purpose Define the boundary for a long-lived workflow API process that clients can connect to and use for workflow authoring, deployment, run, inspect, trace, and resume operations. The goal is not "HTTP for its own sake." The goal is a durable server process that can run workflows as well as the current local CLI/MCP process, while keeping workflow semantics in `wf_api` and transport/session details outside it. ## Core Decision Introduce a process-host layer before introducing HTTP details. Recommended package shape: ```text wf_server long-lived process composition required store construction source/catalog/runtime/event implementation wiring durable WorkflowOperationContext construction wf_transport_rpc_http JSON-RPC 2.0 over HTTP endpoint/controller request/response envelope translation auth/session/streaming transport policy wf_api protocol-neutral workflow application operations no HTTP imports no MCP imports wf_mcp MCP transport and upstream MCP integration ``` `wf_mcp` is intentionally treated as a combined compatibility package in this diagram, not as the desired final boundary. It currently contains two different roles: - MCP as a client transport: an MCP client connects to workflow/admin surfaces. - MCP as an upstream source provider: workflows discover and invoke external MCP servers as capability sources. Those roles should separate over time. A future `wf_transport_mcp` can expose the same `WorkflowServer` / `wf_api` surfaces to MCP clients, while a future `wf_sources_mcp` can own upstream MCP discovery, sessions, tool invocation, resource/prompt access, and FastMCP-specific provider behavior. ### MCP Source Provider Package Direction `wf_sources_mcp` is the target package for MCP-as-upstream-source code. It should own behavior required to turn external MCP servers into workflow capability sources: - MCP auth interpretation for stdio/http transports - desired MCP source registry models and conversion into runtime connection records - source catalog snapshot cache stores - upstream discovery and tool/resource/prompt invocation adapters - stateful MCP runtime/session management - FastMCP-specific compatibility behavior `wf_sources_mcp` must not own workflow lifecycle APIs, MCP frontend tool schemas, or old `wf-mcp` entrypoint behavior. Those belong to `wf_api`, transport packages, or compatibility shims. First slices should move leaf modules only and leave `wf_mcp` re-export shims: 1. Complete: MCP auth helpers and focused auth/catalog stores moved to `wf_sources_mcp`, with `wf_mcp` shims preserved. 2. Complete: MCP source registry models/conversion moved to `wf_sources_mcp.source_registry`, with `wf_mcp.source_registry` retained as a shim. 3. Complete: upstream MCP catalog/discovery DTOs moved to `wf_sources_mcp.catalog`, with `wf_mcp.capabilities` and `wf_mcp.catalog.models` retained as shims. 4. Complete: upstream SDK protocol/result types moved to `wf_sources_mcp.sdk`, with `wf_mcp.sdk` and `wf_mcp.runtime.protocols` retained as shims. 5. Complete: MCP SDK conversion helpers moved to `wf_sources_mcp.sdk.converters`, with `wf_mcp.sdk.converters` retained as a shim. 6. Complete: shared MCP session opener in `wf_sources_mcp.client`. One-shot adapter (`McpSdkAdapter`) and persistent runtime (`PersistentSessionFactory`) both use `open_mcp_session`. 7. Complete: persistent MCP runtime (`PersistentMcpSession`, `PersistentSessionFactory`, `McpRuntimePool`, `connection_runtime_fingerprint`) moved to `wf_sources_mcp.runtime`, with `wf_mcp.runtime.*` retained as compatibility shims. Runtime remains tool-call-only. Next slice is moving `McpSdkAdapter` to `wf_sources_mcp.sdk.adapter`. 8. Complete: one-shot MCP SDK adapter moved to `wf_sources_mcp.sdk.adapter`, with `wf_mcp.sdk.adapter` retained as a compatibility shim. This does not expand persistent runtime; the next design slice should unify one-shot and persistent client operation handling behind a shared source-client facade. 9. Complete: shared `McpSourceClient` facade introduced in `wf_sources_mcp.client`. `McpSdkAdapter` now delegates operation handling to this facade. Persistent runtime still exposes only `call_tool`; expanding it requires a separate owner-task request routing slice. 10. Complete: persistent MCP runtime owner now routes explicit callables through a generic operation queue with request metadata. The runtime still exposes only `call_tool`; non-tool methods require a separate public-surface slice. 11. Complete: persistent MCP runtime can route `read_resource` through the owner-task queue and `McpSourceClient`. Runtime still does not expose raw method invocation, notifications, or discovery list operations. 12. Complete: persistent MCP runtime can route `get_prompt` through the owner-task queue and `McpSourceClient`. Runtime still does not expose raw method invocation, notifications, or discovery list operations. 13. Complete: broker content access now prefers configured `StatefulMcpRuntime` for resource and prompt reads, falling back to the one-shot adapter when no stateful runtime is configured. Catalog refresh/discovery remains one-shot. 14. Complete: stateful MCP runtime protocols split into tool/resource/prompt slices. Runtime can route `list_resources` and `list_prompts` through the owner task for session-scoped listings; catalog refresh remains one-shot. 15. Complete: MCP source catalog aggregation helpers (`CombinedCatalog` and `snapshot_from_specs`) moved to `wf_sources_mcp.catalog`, with `wf_mcp.broker.catalog` retained as a compatibility shim. 16. Complete: MCP upstream capability discovery moved to `wf_sources_mcp.discovery`, with `wf_mcp.broker.discovery` retaining compatibility re-exports. `specs_from_discovered_tools` remains in `wf_mcp` until the wrapper/event seam is neutralized. 17. Complete: MCP JSON-schema-to-Pydantic model helper moved to `wf_sources_mcp.schema_models`. This removes the broker catalog hydration dependency on private `wf_mcp.workflow.wrappers` helpers; eventful tool wrapping remains in `wf_mcp` for the next seam. 18. Complete: MCP tool wrapper event emission now uses neutral `wf_sources_mcp.tool_events` DTOs; `wf_mcp.broker.discovery` adapts them to broker-local `McpEvent`. `wrap_discovered_tool` remains in `wf_mcp` until the next move slice. 19. Complete: MCP discovered-tool wrapper generation (`wrap_discovered_tool`) moved to `wf_sources_mcp.tool_wrappers`, with `wf_mcp.workflow` retained as a compatibility shim. `specs_from_discovered_tools` remains in `wf_mcp` as the broker event projection adapter. 20. Complete: neutral `specs_from_discovered_tools` moved to `wf_sources_mcp.discovery`. `wf_mcp.broker.discovery` remains as the broker adapter for legacy `ConnectionConfig` input and `McpEvent` projection. 21. Complete: upstream MCP adapter lookup (`require_adapter`) moved to `wf_sources_mcp.adapters`, with `wf_mcp.broker.service.adapters` retained as a compatibility shim. 22. Complete: MCP source ID validation and reserved source IDs are canonical in `wf_sources_mcp.ids`; legacy `wf_mcp.connections` / `wf_mcp.shared.names` remain compatibility consumers. 23. Complete: broker DTO construction removed from `wf_sources_mcp`. `wf_sources_mcp` accepts legacy-shaped inputs structurally, while `wf_mcp.source_registry` owns helpers that construct `ConnectionConfig`. 24. Complete: `McpRuntimePool` implements the full MCP operation surface (`list_tools`, resources, prompts, tools, raw methods, notifications, and local metadata). Broker upstream operations prefer the persistent runtime when configured and fall back to one-shot adapters. 25. Complete: deterministic JSON-RPC integration coverage proves MCP-backed workflow runs reuse one persistent runtime session for repeated operations against the same source. 26. Upstream transport/discovery/session services. Each slice should add import-direction tests so the new source-provider package does not depend on `wf_mcp.workflow_surface`, `wf_mcp.admin_surface`, `wf_mcp.server`, or `wf_mcp.proxy`. Temporary imports from broker model shims are acceptable only when documented in the slice plan; the direction is to replace them with protocol/DTO seams before moving runtime services. `wf_transport_rpc_http` should call `WorkflowApi` through the server composition. It should not call `WfMcpService`. HTTP is one transport adapter, not "the API." JSON-RPC over HTTP, JSON-RPC over WebSocket, and possibly MCP can become sibling transports around the same server/application boundary. Server composition should be driven by the wider neutral `wf_config` model, including source definitions; MCP broker config is a legacy/source-specific input to normalize, not a permanent peer config family. `wf_server` may initially be small. Its role is to prove that a non-MCP process can construct the same application boundary with required stores and an explicit runtime/source implementation. ## MCP UI/App Support Policy MCP UI metadata is not workflow-server capability by itself. Upstream MCP tools may expose `_meta.ui` / `ui://...` resources, but rendering those resources is a host/frontend responsibility, not a workflow runtime responsibility. Until a dedicated MCP frontend transport owns the full host behavior, do not advertise MCP Apps/UI support to clients. Supporting it requires more than passing metadata through: - serving `ui://` resources and any dependent assets from a stable origin - sandboxed iframe hosting - a JSON-RPC `postMessage` / app bridge - app-only tool visibility and calls - CSP/domain policy - widget state and teardown semantics `wf_sources_mcp` / current upstream MCP source code may preserve UI metadata as observed source metadata, but it must not imply that `wf_server` can render or host widgets. `wf_transport_mcp` may later implement a real host bridge. Raw MCP proxy/debug modes can expose upstream behavior explicitly, but that path is not the durable workflow surface. ## Why Not Wrap WfMcpService `WfMcpService` is still a compatibility facade for MCP broker concerns: - connection config/reload - upstream MCP adapter/session management - catalog refresh - content access - event recording - runtime execution - source inventory It is being decomposed into focused services, but its public shape still carries MCP process assumptions. Building HTTP on top of it would recreate the coupling that `wf_api` extraction just removed. The long-lived API should depend on the same lower-level concepts as MCP, not on the MCP facade itself. ## First Slice First slice should be lightweight and local/static. It should prove: - a long-lived process can construct a durable `WorkflowApi` - artifact, draft, and run stores are required up front - a client can connect and call workflow operations - deployment run/inspect/trace/resume semantics match local behavior - no direct dependency on `WfMcpService` Implementation status: - Slice 1 complete: `wf_server.build_local_static_workflow_server()` constructs a durable `WorkflowApi` with required file-backed stores, local `wf.std`/`wf.recipes` sources, and a local runtime runner. - Slice 2 complete: `wf_transport_rpc_http` provides JSON-RPC 2.0 over HTTP via `create_rpc_app(server)` and the `wf-rpc-server` CLI. - Slice 3 complete: `wf_cli` supports target-aware context with `--local`, `--url`, and `--timeout` overrides, and works with remote RPC targets for capability and run commands. - 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. - Slice 5 complete: `wf-rpc-server --mcp-config ` starts JSON-RPC over an MCP-backed `WorkflowServer`; `--store-root` remains local/static-only. 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. Runtime bridge complete when `wf-rpc-server --config ` can compose an MCP-backed server from neutral `server.sources[]` entries. `--mcp-config` remains a compatibility alias until existing users migrate. First slice should not include: - live upstream MCP source management - OpenAPI dynamic source registration - auth beyond a stub or disabled mode - streaming/progress - transactional database backend - multi-worker concurrency guarantees - config hot reload Acceptable first-slice source support: - broker-local workflow stdlib capabilities such as `wf.std` - saved workflow artifacts/deployments from the configured store - explicitly registered local NodeSpecs when the server process starts This is enough to prove the server can run real workflows and durable resumes without dragging upstream source lifecycle into the first implementation. ## Runtime and Store Boundary The server process should construct: ```text WorkflowStores artifact_store draft_workspace_store run_store WorkflowOperationContext artifact_store draft_workspace_store run_store events specs runtime live_sources=None or local-only checker WorkflowApi ``` The context must pass `require_workflow_stores()` before being exposed through the long-lived API. Current config exposes one default `server.store` root. The server then fans it out into workflow stores, auth records, source registry state, and catalog/cache state. That is acceptable for the first durable server path, but the boundary should not assume every persistence role always shares one backend. Future config should support optional role-specific store overrides: ```text server.store default for every missing role server.stores.workflow artifacts, deployments, drafts, runs, traces server.stores.auth auth records or secret-manager references server.stores.sources desired source registry entries server.stores.catalog source catalog/cache snapshots ``` The compatibility rule is: if a role store is absent, use `server.store`. First implementation should keep overrides filesystem-only; SQL, object storage, and secret-manager adapters are later backend implementations. The first server runtime may reuse existing implementation classes when they do not require MCP-specific behavior. If reuse would require constructing `WfMcpService`, that is the wrong dependency direction. ## Transport Contract Every transport should follow the same shape: ```text transport request -> decode/validate transport envelope -> call WorkflowApi method -> encode transport response ``` Transport adapters own: - route names, method names, or JSON-RPC method names - request parsing - response serialization - auth/session headers or connection identity - streaming/progress mechanics Transport adapters must not own: - deployment validation semantics - run/resume state transitions - trace paging semantics - wrapper hint policy - source dependency validation ## Client Contract Clients should be able to perform the same workflow lifecycle remotely that they can perform locally: ```text list/inspect capabilities create/patch/validate draft workspace save artifact save/validate deployment run deployment inspect run read bounded trace resume interrupted run delete deployment ``` Response payloads should preserve current `wf_api` semantics: - list operations stay compact - inspect operations return detail - run responses return compact status plus trace metadata - trace entries require explicit bounded range - interrupted runs expose resume next actions - blocked resume returns diagnostics without consuming input Transport field names may differ only when transport conventions require it. They must not change workflow status meanings. ## Error and Failure Semantics The long-lived server must preserve the persisted run/resume contract: - invalid deployment dependencies before start return `unrunnable` - completed, failed, and interrupted stopped states are persisted - only interrupted runs can resume - broken pinned dependencies before resume return `blocked` - dead live tools/sources fail the run; they do not become implicit interrupts First slice can ignore live source death because it does not include live upstream sources. Later slices must preserve the same no-implicit-pause rule. ## Package Boundary Rules Hard rules: - `wf_api` must not import transports, `wf_server`, or `wf_mcp`. - transport packages may import `wf_api` and `wf_server`. - `wf_server` may import `wf_api`, `wf_artifacts`, `wf_platform`, and selected reusable implementation services. - `wf_server` should not import `wf_mcp.broker.WfMcpService`. - `wf_mcp` may continue constructing `WorkflowApi` through its existing MCP-specific context adapter. - A future MCP transport mounted through `wf_server` must still keep upstream MCP source execution separate from transport request handling. - Do not add new generic server or transport code that depends on the combined `wf_mcp` facade. If it needs MCP-specific upstream behavior, isolate that as a source-provider adapter; if it needs to expose workflow operations to MCP clients, isolate that as a transport adapter. If a reusable service currently lives under `wf_mcp.broker.service` but has no MCP dependency, later slices may move or duplicate a protocol-neutral version. Do not move large service sets in the first slice. ## Later Slice Pointers ### Slice 2: JSON-RPC HTTP Transport Adapter Add the first transport package as JSON-RPC 2.0 over HTTP, likely `wf_transport_rpc_http`. This is preferred over REST for the first remote CLI/server path because CLI and agent clients want stable operation names more than resource-shaped URLs. The method names should be dotted strings, matching the mental model already used by MCP/admin tool names without inheriting MCP's dynamic tool-list behavior. Proposed initial method names: ```text workflow.capabilities.list workflow.capabilities.inspect workflow.drafts.create_from_capability workflow.drafts.patch workflow.drafts.validate workflow.artifacts.save workflow.deployments.save workflow.deployments.validate workflow.runs.start workflow.runs.inspect workflow.runs.trace workflow.runs.resume ``` This slice should expose a small method set over the existing server composition: - health/status - list/inspect capabilities - run deployment - inspect run - read bounded trace - resume run It should not implement source provider management yet. Implementation status: - `wf_transport_rpc_http.create_rpc_app(server)` exposes a fixed JSON-RPC method set over an existing `wf_server.WorkflowServer`. - `wf-rpc-server --store-root ` and `wf-rpc-server --config ` start the local/static server over `/rpc`. - Remote `wf` CLI targeting is implemented through `wf_config` and target-aware context in `wf_cli`. - Auth, streaming/progress, and live upstream MCP source management remain future work. Preferred implementation dependency: ```bash uv add fastapi-jsonrpc uvicorn ``` `fastapi-jsonrpc` is the recommended server library for this slice because it keeps JSON-RPC 2.0 dispatch, errors, and docs near FastAPI/Pydantic instead of requiring a local hand-rolled dispatcher. Client-side CLI code can use `httpx` directly for now. A typed client wrapper can be added when the method set stabilizes. Guardrails: - Do not dynamically register saved workflows as JSON-RPC methods. - Do not add a JSON-RPC equivalent of MCP `tools/list` as the primary execution surface. - Do not require client session state to run or resume workflows. - Keep durable state addressed by explicit ids: `artifact_id`, `deployment_id`, `run_id`. - Keep trace reads bounded by explicit range. - Define request/response models explicitly with Pydantic; do not pass raw arbitrary dicts through the transport layer when a stable request shape is known. ### Slice 3: CLI Remote Target Allow `wf_cli` to target either: - local process stores/runtime, current behavior - remote long-lived API server The CLI command surface should stay stable. Only context construction changes. ### Slice 4: WebSocket Transport If JSON-RPC over HTTP starts becoming awkward for streaming/progress, add a WebSocket transport sibling rather than changing `WorkflowApi`. Possible shapes: - JSON-RPC over WebSocket - future MCP transport over the same server-owned `WorkflowApi` ### Slice 5: Source Providers Add explicit source-provider interfaces for long-lived server use. Possible providers: - static local NodeSpecs - saved workflow capability sources - OpenAPI capability source catalogs - upstream MCP connection catalogs This slice should avoid making "source" mean "MCP connection." MCP is one source provider, not the source model. Future package direction: ```text wf_transport_mcp exposes WorkflowServer / wf_api operations to MCP clients wf_sources_mcp consumes upstream MCP servers as workflow capability sources wf_mcp compatibility package until old MCP entrypoints can shrink or retire ``` Current MCP-backed server status: - MCP-backed `WorkflowServer` construction is implemented. - `wf-rpc-server --mcp-config ` can serve JSON-RPC over that server. - Source registry read/mutation APIs are reachable remotely when the target exposes `source_registry_admin`. - Boundary caveat: the first `--mcp-config` hook intentionally proved the product path quickly, but it currently makes the transport CLI import `wf_mcp.broker`. That violates the original transport-package boundary and the existing import-direction guard. Treat this as a cleanup slice, not the desired final shape. The intended cleanup is to widen `wf_config` so MCP sources are configured through `server.sources[]`, with `wf_mcp.config.json` handled as compatibility input. Next implementation slices should be: 1. Wider `wf_config` source model. Add an MCP source config variant under `server.sources[]` that can express the current broker connection shape: source id, provider/account/profile, `locked` / `seed` ownership, stdio/http transport, auth reference, enabled flag, and metadata. Keep legacy `wf_mcp.config.json` parsing as a compatibility adapter into the wider config, not as the future primary shape. `McpSourceRegistryEntry` already expresses most of this shape. The `locked` / `seed` policy currently lives on legacy `ConnectionConfig.source_config_ownership`; migrate that as a neutral source ownership/config policy field, not as a connection-specific name. 2. Transport package boundary cleanup. Keep JSON-RPC method/app/client modules transport-only. After `wf_config` can describe MCP sources, `wf-rpc-server --config ...` should compose MCP-backed sources from neutral config and the `--mcp-config` path can become deprecated/legacy. Completed when `tests/wf_transport_rpc_http/test_import_direction.py` passes and the RPC transport CLI imports only `wf_config`, `wf_server`, and transport modules for server construction. 3. Legacy MCP config migration. Provide an explicit converter from old `wf_mcp.config.json` into `WorkflowConfigFile`: `store_root` maps to `server.store` (`StoreConfig` is already a discriminated union; currently only `kind: "filesystem"` exists), and each legacy connection maps to a `kind: "mcp"` source. Preserve `source_config_ownership` as the neutral `ownership` field. Normalize old HTTP-like transport metadata (`http`, `streamable-http`, `streamable_http`, `sse`) into the neutral HTTP MCP source transport while preserving compatibility metadata where needed. `sse` remains legacy/deprecated, but conversion support is intentional because FastMCP can still expose it. Completed when `wf config migrate-mcp --output ` writes a neutral config that can be used by `wf-rpc-server --config`. 4. Manual product smoke with the real CLI/server commands. Record UX/runtime gaps before broadening architecture. 5. Completed: source registry apply/reload semantics. Registry mutation updates desired persisted state, and explicit apply reconciles that state into the current server connection/source graph without mutating config files. 6. Completed: persisted resume across server restart. The MCP-backed JSON-RPC regression rebuilds a neutral-config server from the same stores, inspects an interrupted run, and resumes it from the stored checkpoint and pinned dependency environment. ### Slice 6: Auth and Tenancy Define who can read/write artifacts, deployments, runs, and auth records. Do not store upstream credentials as plain JSON in production mode. The local file store can remain a development backend. ### Slice 7: Streaming and Progress Expose long-running run progress through a protocol-native channel: - HTTP streaming/SSE/WebSocket for HTTP/RPC transports - MCP progress/tasks for MCP if practical Do not bloat `run_deployment` responses with full traces or live logs. ### Slice 8: Transactional Store Backend Add SQLite/Postgres or another transactional backend for run records, checkpoints, artifacts, deployments, and possibly catalogs. This slice should handle: - compare-and-swap resume - multi-process safety - atomic writes - retention policy ### Slice 9: Live Upstream MCP Sources Add upstream MCP source management to the long-lived server only after the local server path is proven. This needs explicit policies for: - connection lifecycle - auth records - catalog refresh - source liveness - session failure - side-effectful tool calls ## Open Questions 1. Package name: `wf_server` is the recommended process-composition package, but the exact name can change before implementation. 2. HTTP framework: likely FastAPI under `fastapi-jsonrpc`. 3. RPC framework: use `fastapi-jsonrpc` for the first JSON-RPC-over-HTTP slice unless evaluation finds a blocking issue. 4. Source-provider extraction: some reusable code currently lives in `wf_mcp.broker.service`; first slice should avoid moving it unless a small dependency-free helper is obviously needed. 5. Storage backend: first slice can use file stores; production remote API needs a transactional backend later. ## Non-Goals - Do not redesign `WorkflowApi`. - Do not move MCP upstream session management into `wf_api`. - Do not make transport routes call `WorkflowSurfaceHandlers`. - Do not require dynamic tools to represent saved workflows. - Do not implement retry/timeout policy. - Do not treat external source failure as a pause. - Do not implement fork/gather or new workflow primitives in this server slice. ## Success Criteria First implementation plan should be considered successful when: - a process-local server composition can construct `WorkflowApi` without `WfMcpService` - required stores are enforced - a connected client can run a deployment and inspect/read trace for the run - persisted run semantics match the local `WorkflowRunApi` tests - docs clearly state which source capabilities are first-slice only and which are future work