34 KiB
wf_api Extraction Map
Executive Summary
WorkflowSurfaceHandlers (src/wf_mcp/workflow_surface/handlers.py:104) is a 1,130-line application-service class that owns all workflow-facing operations: capability discovery, artifact CRUD, draft workspace management, deployment validation, and durable run lifecycle. It currently depends on WfMcpService (src/wf_mcp/broker/service/core.py:79) via a single self.service field, but only uses a narrow slice of that 960-line god object.
The extraction seam is clean: WorkflowSurfaceHandlers touches ~6 distinct capabilities of WfMcpService (artifact store, draft store, run store, capability sources, workflow execution, event bus). None of these require MCP transport, connection adapters, or proxy machinery. The class is already used protocol-neutrally by wf_cli via CliContext (src/wf_cli/context.py:14).
Recommended first slice: Option B — introduce a WorkflowApi facade backed by a small WorkflowApiBackend ports object, adapt WfMcpService into it, then move the facade to wf_api. This avoids the large-class move (Option A) and the premature domain split (Option C).
Public Operation Inventory
WorkflowSurfaceHandlers — Public Methods
| Method | Line | Category | Input Parameters | Return Shape | Direct Dependencies | Callers/Tests |
|---|---|---|---|---|---|---|
list_artifacts |
110 | artifacts | query, kind, cursor, limit |
{nodes, next_cursor, total} |
self.service.artifact_store, self.service.workflow_artifact_catalog_entry |
tools.py:49, artifacts.py:41, test_artifacts.py:11 |
list_capabilities |
147 | capabilities | query, source_id, cursor, limit |
{capabilities, next_cursor, total} |
self.service.capability_sources |
tools.py:91, caps.py:41, test_capabilities.py:20 |
inspect_capability |
193 | capabilities | qualified_name |
capability contract dict | self.service.capability_sources |
tools.py:132, caps.py:65, test_capabilities.py:102 |
call_capability |
213 | capabilities | qualified_name, payload, deployment_id |
{qualified_name, source_id, kind, deployment_id, outcome, output, diagnostics} |
self.service._get_qualified_spec, self.service.capability_sources, self.service.run_workflow_from_plan, self.service.artifact_store |
tools.py:144, test_capabilities.py:50, test_wrappers.py:69 |
save_artifact |
416 | artifacts | artifact dict |
{artifact_id, version, saved} |
self.service.artifact_store, self.service._record_event |
tools.py:162 |
create_artifact_from_plan |
437 | artifacts | artifact_id, version, title, plan, outcomes, kind, description, required_capabilities, source_bindings, created_from_catalog_version |
{artifact_id, version, saved} |
self.service.artifact_store, self.service._record_event |
tools.py:186, test_wrappers.py:15 |
validate_draft |
492 | drafts | draft dict |
{status, diagnostics, compiled_plan} |
(none — pure wf_artifacts) | tools.py:170, test_drafts.py:23 |
compile_draft |
498 | drafts | draft dict |
{compiled_plan, required_capabilities} |
self.service (via _required_capabilities_for_plan) |
tools.py:178 |
create_artifact_from_draft |
511 | drafts/artifacts | artifact_id, version, title, draft, outcomes, kind, description, required_capabilities, source_bindings, created_from_catalog_version |
{artifact_id, version, saved, required_logical_sources, suggested_bindings} |
self.service.artifact_store, self.service._record_event |
tools.py:229, test_drafts.py:61 |
patch_draft |
570 | drafts | draft, patch |
patched draft dict | (none — pure wf_artifacts) | tools.py:269 |
list_draft_workspaces |
583 | draft workspaces | (none) | {workspaces} |
self.service.draft_workspace_store |
tools.py:280, test_drafts.py |
create_draft_workspace |
593 | draft workspaces | workspace_id, draft, title |
workspace summary | self.service.draft_workspace_store |
tools.py:293, test_drafts.py |
get_draft_workspace |
607 | draft workspaces | workspace_id, include_draft |
workspace summary | self.service.draft_workspace_store |
tools.py:309, test_drafts.py |
delete_draft_workspace |
619 | draft workspaces | workspace_id |
{workspace_id, deleted, status} |
self.service.draft_workspace_store |
tools.py:325, test_drafts.py |
validate_draft_workspace |
627 | draft workspaces | workspace_id |
workspace summary | self.service.draft_workspace_store |
tools.py:360, test_drafts.py |
patch_draft_workspace |
641 | draft workspaces | workspace_id, revision, patch |
workspace summary | self.service.draft_workspace_store |
tools.py:341, test_drafts.py |
set_draft_name |
655 | draft workspaces | workspace_id, revision, name |
workspace summary | (delegates to patch_draft_workspace) |
tools.py:372, test_drafts.py |
set_draft_route |
668 | draft workspaces | workspace_id, revision, step_id, outcome, target |
workspace summary | (delegates to patch_draft_workspace) |
tools.py:386, test_drafts.py |
set_step_input_map |
692 | draft workspaces | workspace_id, revision, step_id, input_map |
workspace summary | (delegates to patch_draft_workspace) |
tools.py:405, test_drafts.py |
set_step_output_map |
712 | draft workspaces | workspace_id, revision, step_id, output_map |
workspace summary | (delegates to patch_draft_workspace) |
tools.py:425, test_drafts.py |
create_minimal_draft_workspace |
732 | draft workspaces | workspace_id, name, capability_name, schemas, bindings |
workspace summary | self.service.draft_workspace_store (via _draft_store), self.service._get_qualified_spec (via _outcomes_for_capability) |
tools.py:446, test_drafts.py |
create_draft_workspace_from_capability |
802 | draft workspaces | workspace_id, capability_name, optional schemas/bindings |
workspace summary + wrapper_hints + next_actions | (delegates to inspect_capability + create_minimal_draft_workspace) |
tools.py:474, test_drafts.py |
create_artifact_from_workspace |
847 | artifacts | workspace_id, artifact_id, version, title, outcomes, kind, description, required_capabilities, source_bindings, created_from_catalog_version |
save result or validation error | self.service.draft_workspace_store |
tools.py:502, test_drafts.py |
create_wrapper_from_workspace |
884 | artifacts | same as above (without kind) |
save result | (delegates to create_artifact_from_workspace with kind="wrapper") |
tools.py:536, test_drafts.py |
inspect_artifact |
917 | artifacts | artifact_id, version |
full artifact dict | self.service.artifact_store |
tools.py:563, test_artifacts.py:58 |
list_deployments |
925 | deployments | (none) | {deployments} |
self.service.artifact_store |
tools.py:577, test_deployments.py |
inspect_deployment |
935 | deployments | deployment_id |
full deployment dict | self.service.artifact_store |
tools.py:585, test_deployments.py |
save_deployment |
942 | deployments | deployment dict |
{deployment_id, artifact_id, artifact_version, saved} |
self.service.artifact_store, self.service._record_event |
tools.py:593, test_deployments.py |
delete_deployment |
965 | deployments | deployment_id |
{deployment_id, deleted} |
self.service.artifact_store, self.service._record_event |
tools.py:604, test_deployments.py |
validate_deployment |
979 | deployments | deployment_id, live_check |
{deployment_id, artifact_id, artifact_version, status, diagnostics, next_actions} |
self.service.artifact_store, self.service.capability_sources, self.service.connections, self.service.adapters, self.service.load_auth |
tools.py:617, test_deployments.py:39 |
run_deployment |
1010 | runs | deployment_id, workflow_input, trace_range |
run payload dict | self.service.artifact_store, self.service.run_workflow_from_plan, self.service.run_store |
tools.py:650, test_runs.py:40 |
resume_run |
1074 | runs | run_id, resume_payload, resume_outcome, trace_range |
run payload dict | self.service.run_store, self.service.resume_workflow_from_plan, self.service.capability_sources |
tools.py:681, test_runs.py |
inspect_run |
1154 | runs | run_id |
run payload dict | self.service.run_store |
tools.py:712, test_runs.py:78 |
read_run_trace |
1172 | runs | run_id, trace_range |
run payload dict with trace | self.service.run_store |
tools.py:722, test_runs.py:79 |
Private Methods
| Method | Line | Notes |
|---|---|---|
_wrapper_artifact_for_capability_name |
272 | helper for wrapper resolution |
_wrapper_capability_summaries |
297 | projects wrappers into capability discovery |
_wrapper_capability_detail |
340 | NodeSpec-like contract for wrappers |
_call_wrapper_artifact |
371 | executes wrapper through workflow runner |
_draft_store |
578 | accessor for self.service.draft_workspace_store |
_outcomes_for_capability |
911 | delegates to self.service._get_qualified_spec |
_run_store |
1196 | accessor for self.service.run_store |
_deployment_validation |
1202 | shared validation logic for deployment operations |
Module-Level Private Helpers
| Function | Line | Used By | Dependencies |
|---|---|---|---|
_available_sources |
1238 | _deployment_validation, resume_run, _live_source_diagnostics |
service.capability_sources |
_live_source_diagnostics |
1277 | validate_deployment |
service.connections, service.adapters, service.load_auth |
_required_live_sources |
1326 | _live_source_diagnostics |
(pure) |
_required_capabilities_for_plan |
1341 | compile_draft |
service (for _observed_node_specs) |
_required_capability_payloads |
1373 | multiple | (pure) |
_suggested_self_bindings |
1382 | create_artifact_from_draft |
(pure) |
_observed_node_specs |
1389 | create_artifact_from_plan, create_artifact_from_draft, _required_capabilities_for_plan |
service.capability_sources |
_schema_field_names |
1400 | list_capabilities |
(pure) |
_draft_input_maps |
1408 | create_minimal_draft_workspace |
(pure) |
_draft_output_map |
1436 | create_minimal_draft_workspace |
(pure) |
_draft_input_bindings_payload |
1449 | create_minimal_draft_workspace, set_step_input_map |
(pure) |
_draft_output_bindings_payload |
1463 | create_minimal_draft_workspace, set_step_output_map |
(pure) |
_graph_path_payload |
1471 | multiple | (pure) |
_local_path_payload |
1476 | multiple | (pure) |
_state_path_payload |
1480 | multiple | (pure) |
_escape_json_pointer |
1484 | draft patch helpers | (pure) |
_draft_name_from_capability |
1489 | create_draft_workspace_from_capability |
(pure) |
_source_id_for_capability |
1494 | call_capability |
(pure) |
_capability_name |
1505 | _available_sources |
(pure) |
_artifact_capability_id |
1516 | multiple | (pure) |
_raw_plan_from_artifact |
1526 | run_deployment, resume_run, _call_wrapper_artifact |
(pure) |
_plan_field |
1543 | _raw_plan_from_artifact |
(pure) |
_plan_nodes |
1553 | _required_capabilities_for_plan |
(pure) |
_run_payload |
1558 | run_deployment, resume_run, inspect_run, read_run_trace |
(pure) |
_interrupt_payload |
1608 | _run_payload |
(pure) |
_deployment_summary |
1621 | list_deployments |
(pure) |
WfMcpService Dependency Inventory
Members Accessed from WorkflowSurfaceHandlers
| Member | Type | Accessed Via | Usage Count | Category |
|---|---|---|---|---|
artifact_store |
WorkflowArtifactStore | None |
self.service.artifact_store |
~25 | artifact storage |
draft_workspace_store |
DraftWorkspaceStore | None |
self.service.draft_workspace_store |
~8 | draft storage |
run_store |
RunStore | None |
self.service.run_store |
~5 | run storage |
capability_sources |
dict[str, CapabilitySource] |
self.service.capability_sources |
~8 | source/capability inventory |
_get_qualified_spec(qualified_name) |
NodeSpec |
self.service._get_qualified_spec(...) |
3 | private helper |
_record_event(event) |
None |
self.service._record_event(...) |
7 | private helper (event bus) |
run_workflow_from_plan(...) |
RunState |
self.service.run_workflow_from_plan(...) |
3 | workflow execution |
resume_workflow_from_plan(...) |
RunState |
self.service.resume_workflow_from_plan(...) |
1 | workflow execution |
workflow_artifact_catalog_entry(artifact) |
WorkflowArtifactCatalogEntry |
self.service.workflow_artifact_catalog_entry(...) |
1 | artifact projection |
connections |
ConnectionRegistry |
self.service.connections |
1 | MCP-specific (live source check) |
adapters |
dict[str, BackendAdapter] |
self.service.adapters |
1 | MCP-specific (live source check) |
load_auth(connection_id) |
AuthRecord | None |
self.service.load_auth(...) |
1 | MCP-specific (live source check) |
Source/catalog ownership is now split: WfMcpService coordinates broker runtime
state, while SourceCatalogService owns capability source maps, planner catalog
projection, snapshot hydration, and local docs lookup.
Workflow runtime ownership is now split: WorkflowRuntimeService owns plan
compilation, dependency preparation, run, and resume. WfMcpService remains the
broker coordinator and compatibility façade.
Event recording ownership is now split: BrokerEventRecorder owns EventBus
publication, simple event construction, event history reads, and catalog-change
fanout. WfMcpService remains the coordinator and compatibility façade.
Connection registration/config reload reconciliation is now owned by
wf_mcp.broker.service.connection_service.ConnectionService. The service owns
the ConnectionRegistry; WfMcpService.connections is only a compatibility
property.
WfMcpService Members NOT Used by WorkflowSurfaceHandlers
These members of WfMcpService (src/wf_mcp/broker/service/core.py) are NOT accessed by WorkflowSurfaceHandlers:
store(MCP catalog/auth storage)default_catalog_max_age_secondsinclude_builtin_specstool_executorevent_bus(only accessed indirectly via_record_event)register_connectionsync_connections_from_configregister_adapter_tool_executor_forsave_authregister_specsget_catalogget_planner_cataloglist_sourceslist_source_summariesinspect_sourcelist_available_specsget_connection_snapshotconnection_statuseslist_resourceslist_promptsget_resourceget_promptread_resourceinvoke_methodsend_notificationrender_prompt_local_documentation_resource_local_documentation_promptrefresh_connection_catalogcompile_plan_prepare_workflow_runtimeregister_capability_source_hydrate_connection_source_from_snapshot_spec_from_snapshot_entry_record_catalog_change_events
Protocol-Neutral vs MCP-Specific Dependencies
| Dependency | Package Today | MCP-Specific? | Should Live In |
|---|---|---|---|
WorkflowArtifactStore |
wf_artifacts |
No | wf_artifacts (already there) |
DraftWorkspaceStore |
wf_artifacts |
No | wf_artifacts (already there) |
RunStore |
wf_artifacts |
No | wf_artifacts (already there) |
CapabilitySource |
wf_platform |
No | wf_platform (already there) |
EventBus / McpEvent |
wf_mcp.events |
Renamed but protocol-neutral | wf_api.events or rename to wf_platform.events |
make_event |
wf_mcp.events |
Protocol-neutral | Move to wf_api or wf_platform |
matches_query |
wf_mcp.shared.listing |
Protocol-neutral | wf_api.shared or wf_platform |
paged_list_payload |
wf_mcp.shared.listing |
Protocol-neutral | wf_api.shared or wf_platform |
page_items |
wf_platform |
No | wf_platform (already there) |
RawWorkflowPlan |
wf_mcp.models |
No | wf_api.models or wf_core |
ConnectionRegistry |
wf_mcp.connections |
Yes (MCP connection model) | stays in wf_mcp |
ConnectionConfig |
wf_mcp.models |
Yes (MCP connection model) | stays in wf_mcp |
BackendAdapter |
wf_mcp.sdk |
Yes (MCP adapter protocol) | stays in wf_mcp |
require_adapter |
wf_mcp.broker.service.adapters |
Yes (MCP adapter lookup) | stays in wf_mcp |
AuthRecord |
wf_mcp.models |
Yes (MCP auth model) | stays in wf_mcp |
Store / FileStore |
wf_mcp.storage |
Yes (MCP catalog/auth storage) | stays in wf_mcp |
WorkflowArtifact, WorkflowDeployment, etc. |
wf_artifacts |
No | wf_artifacts (already there) |
CapabilityRef, CapabilitySource |
wf_platform |
No | wf_platform (already there) |
NodeSpec, build_async_registry |
wf_authoring |
No | wf_authoring (already there) |
RuntimeContext |
wf_core |
No | wf_core (already there) |
InputBinding, OutputBinding |
wf_core.models.steps |
No | wf_core (already there) |
GraphSourcePath, LocalPath, StatePath |
wf_core.paths |
No | wf_core (already there) |
TraceRange |
wf_mcp.workflow_surface.models |
No | wf_api.models |
NextActions |
wf_mcp.workflow_surface.next_actions |
No | wf_api.next_actions |
SavedSubgraphTree |
wf_mcp.workflow_surface.saved_subgraphs |
No | wf_api.saved_subgraphs |
resolve_runtime_dependencies |
wf_mcp.workflow_surface.runtime_dependencies |
No | wf_api.runtime_dependencies |
wrapper_hints_for_capability |
wf_mcp.workflow_surface.wrapper_hints |
No | wf_api.wrapper_hints |
parse_workflow_surface_capability_id |
wf_mcp.workflow_surface.refs |
No | wf_api.refs |
run_lifecycle helpers |
wf_mcp.workflow_surface.run_lifecycle |
No | wf_api.run_lifecycle |
constants |
wf_mcp.workflow_surface.constants |
No | wf_api.constants |
create_pinned_environment |
wf_mcp.workflow_surface.run_lifecycle |
No | wf_api.run_lifecycle |
Summary Classification
Protocol-neutral (should move to wf_api):
- All
wf_artifactstypes and functions - All
wf_platformtypes - All
wf_authoringtypes - All
wf_coretypes EventBus/McpEvent/make_event(rename from "Mcp" to generic)matches_query,paged_list_payload(listing utilities)RawWorkflowPlanTraceRange,NextActions,SavedSubgraphTree,resolve_runtime_dependencies,wrapper_hints,refs,run_lifecycle,constants
MCP-specific (stays in wf_mcp):
ConnectionRegistry,ConnectionConfigBackendAdapter,require_adapterAuthRecordStore,FileStore(MCP catalog/auth persistence)BrokerConfig,build_service_from_config- All proxy/MCP transport/session code
FastMCPtool registration (register_workflow_tools)
Extraction Options
Option A: Move Class First
Move WorkflowSurfaceHandlers to wf_api.service.WorkflowApi, keep same constructor accepting WfMcpService.
Pros:
- Minimal code changes — just move the file, update imports
- Tests keep passing with import-only changes
- No new abstractions
Cons:
wf_apiwould still depend onWfMcpService(awf_mcptype)- Circular dependency risk:
wf_api→wf_mcp→wf_api(ifwf_mcptools import fromwf_api) - Doesn't actually decouple from MCP — just relocates the code
- Makes future FastAPI adapter harder because the service dependency is still monolithic
Option B: Introduce API Facade with Ports (Recommended)
Create WorkflowApi that depends on a smaller WorkflowApiBackend/ports object instead of all WfMcpService, then adapt WfMcpService into that backend.
Pros:
- Clean dependency direction:
wf_api→ ports/interfaces,wf_mcp→ adaptsWfMcpServiceinto ports wf_clican also adapt its own backend (or reuse thewf_mcpadapter)- FastAPI later becomes just another adapter over the same ports
- Testable in isolation — mock the ports
- Incremental: can introduce ports one domain at a time
Cons:
- More upfront work — need to define the port interface
- Risk of over-abstracting if ports are too fine-grained
- Need to decide what
WorkflowApiBackendactually exposes
Option C: Split Handlers by Domain First
Split capabilities/drafts/artifacts/deployments/runs into separate classes before moving packages.
Pros:
- Each domain class is smaller and easier to reason about
- Could enable partial extraction (move artifacts first, then drafts, etc.)
- Better separation of concerns regardless of extraction
Cons:
- Large refactor with many test changes
- Still coupled to
WfMcpServiceuntil ports are introduced - May create artificial boundaries — some operations span domains (e.g.,
create_artifact_from_workspacetouches both drafts and artifacts) - Doesn't solve the packaging problem by itself
Recommended First Slice
Option B: Introduce API facade with ports, starting with a minimal port surface.
Why
-
The dependency direction is the core problem.
WorkflowSurfaceHandlerscurrently reaches intoWfMcpServicefor ~6 distinct capabilities. Extracting without ports just moves the coupling. -
The port surface is small. Looking at the actual accesses, the port interface needs:
artifact_store: WorkflowArtifactStore | Nonedraft_workspace_store: DraftWorkspaceStore | Nonerun_store: RunStore | Nonecapability_sources: dict[str, CapabilitySource]get_qualified_spec(qualified_name: str) -> NodeSpecrecord_event(event: McpEvent) -> Nonerun_workflow_from_plan(plan, input, ...) -> RunStateresume_workflow_from_plan(plan, run, ...) -> RunStateworkflow_artifact_catalog_entry(artifact) -> WorkflowArtifactCatalogEntry
-
The live-source-check (
_live_source_diagnostics) is the only MCP-specific operation. It can stay inwf_mcpas an extension or be gated behind a protocol-neutralliveness_checkcallback.
First Slice Steps
- Define
WorkflowApiBackendprotocol inwf_api/backend.pywith the ~9 members above. - Create
WorkflowApiinwf_api/service.pythat takesWorkflowApiBackendinstead ofWfMcpService. - Move
WorkflowSurfaceHandlerslogic intoWorkflowApi(rename or keep as alias). - Create
WfMcpServiceBackendAdapterinwf_mcpthat adaptsWfMcpService→WorkflowApiBackend. - Update
wf_mcp.workflow_surface.toolsto createWorkflowApi(WfMcpServiceBackendAdapter(service)). - Update
wf_cli.contextto createWorkflowApi(WfMcpServiceBackendAdapter(service)). - Move protocol-neutral helpers (
matches_query,paged_list_payload,make_event, etc.) towf_apiorwf_platform. - Keep
register_workflow_toolsinwf_mcp(it's the MCP tool registration layer).
Proposed Package Shape
First Slice (extraction)
src/wf_api/
__init__.py # public API: WorkflowApi, WorkflowApiBackend
backend.py # WorkflowApiBackend protocol
service.py # WorkflowApi (renamed WorkflowSurfaceHandlers)
models.py # TraceRange, NextActions, RunPayload, etc.
refs.py # parse_workflow_surface_capability_id
constants.py # DEFAULT_CALL_STEP_ID, outcomes, etc.
next_actions.py # NextActions model
saved_subgraphs.py # SavedSubgraphTree, resolve/validate
run_lifecycle.py # create_pinned_environment, persist_stopped_run, etc.
runtime_dependencies.py # resolve_runtime_dependencies
wrapper_hints.py # wrapper_hints_for_capability
Later Slices
src/wf_api/
shared/
listing.py # matches_query, paged_list_payload (or move to wf_platform)
events.py # EventBus, make_event (rename from McpEvent)
What Stays in wf_mcp
src/wf_mcp/
workflow_surface/
tools.py # register_workflow_tools (MCP tool registration)
__init__.py # re-exports for backward compat
broker/
service/
core.py # WfMcpService (unchanged)
adapters.py # WfMcpServiceBackendAdapter (NEW)
connections.py # ConnectionRegistry
models.py # ConnectionConfig, AuthRecord, BrokerConfig, CatalogSnapshot
storage/ # Store, FileStore
sdk/ # BackendAdapter
events/ # EventBus, McpEvent (wf_mcp-specific event bus)
shared/ # names, errors, pagination (MCP-specific utilities)
proxy/ # MCP proxy mounts
server/ # MCP server construction
runtime/ # ToolExecutor
admin_surface/ # admin tools
What Stays in wf_cli
src/wf_cli/
context.py # CliContext now uses WorkflowApi instead of WorkflowSurfaceHandlers
commands/ # unchanged — they call context.handlers.X()
Test Coverage
Tests That Protect the Extraction
| Test File | Lines | Coverage Area |
|---|---|---|
tests/wf_mcp/workflow_surface/test_artifacts.py |
71 | list_artifacts, inspect_artifact, paging, filtering |
tests/wf_mcp/workflow_surface/test_capabilities.py |
206 | list_capabilities, inspect_capability, call_capability, wrapper capabilities |
tests/wf_mcp/workflow_surface/test_drafts.py |
723 | validate_draft, compile_draft, create_artifact_from_draft, draft workspaces (CRUD, patch, name, route, input/output maps), create_minimal_draft_workspace, create_draft_workspace_from_capability, create_artifact_from_workspace, create_wrapper_from_workspace |
tests/wf_mcp/workflow_surface/test_deployments.py |
282 | validate_deployment, live_check, save_deployment, delete_deployment |
tests/wf_mcp/workflow_surface/test_runs.py |
423 | run_deployment, resume_run, inspect_run, read_run_trace, interrupts, reducers, saved subgraphs |
tests/wf_mcp/workflow_surface/test_wrappers.py |
172 | create_artifact_from_plan (wrapper), call_capability (wrapper), logical refs |
tests/wf_mcp/workflow_surface/test_next_actions.py |
148 | NextActions model behavior |
tests/wf_mcp/workflow_surface/conftest.py |
380 | Shared fixtures: handlers(), artifact(), echo_artifact(), echo_draft(), etc. |
tests/wf_cli/test_context.py |
33 | load_cli_context builds service and handlers |
tests/wf_cli/test_app.py |
88 | CLI help output (smoke tests) |
tests/wf_cli/test_run_deploy.py |
218 | CLI deploy validate, run start, run inspect, run trace |
tests/wf_cli/test_discovery_lifecycle.py |
370 | CLI cap list, artifact list, draft create-from-capability, deploy save, render formats |
tests/wf_cli/test_explain.py |
174 | explain command (diagnostic code lookup) |
tests/wf_mcp/test_workflow_surface_refs.py |
— | parse_workflow_surface_capability_id |
tests/wf_mcp/test_workflow_wrapper_hints.py |
— | wrapper_hints_for_capability |
tests/wf_mcp/test_saved_subgraphs.py |
— | SavedSubgraphTree resolution and validation |
Missing Tests Needed Before Extraction
- Integration test for
WorkflowApiwith a mock backend: Verify thatWorkflowApiworks correctly when given aWorkflowApiBackendthat is NOTWfMcpService. This catches accidental coupling. - Test that
wf_cliworks with the newWorkflowApipath: The existingtest_run_deploy.pyusespatch("wf_cli.commands.runs.load_cli_context", ...)— need to verify the patching surface doesn't change unexpectedly. - Negative test for live-source-check separation: Verify that
validate_deployment(live_check=True)still works after the MCP-specific liveness probe is decoupled.
Things To Keep In wf_mcp For Now
- MCP transport/proxy/runtime/session code (
src/wf_mcp/proxy/,src/wf_mcp/server/,src/wf_mcp/runtime/) - FastMCP registration tools (
src/wf_mcp/workflow_surface/tools.py—register_workflow_tools) - Connection adapters (
src/wf_mcp/sdk/,src/wf_mcp/broker/service/adapters.py) - Connection registry (
src/wf_mcp/connections.py) - Broker server construction (
src/wf_mcp/broker/server.py,src/wf_mcp/broker/config.py) - MCP-specific auth (
src/wf_mcp/models.py:AuthRecord) - MCP catalog storage (
src/wf_mcp/storage/) - Admin surface (
src/wf_mcp/admin_surface/) - MCP event bus (
src/wf_mcp/events/) — may need a protocol-neutral fork - Discovery/refresh (
src/wf_mcp/broker/discovery.py)
Risks And Open Questions
Circular Import Risks
- Current:
wf_mcp.workflow_surface.handlersimports fromwf_mcp.broker.service(type-checking only viaTYPE_CHECKING). No circular import today. - After extraction:
wf_api.servicemust NOT import fromwf_mcp. TheWfMcpServiceBackendAdapterlives inwf_mcpand imports fromwf_api. Direction:wf_mcp→wf_api, never reverse. - Risk: The
live_source_diagnosticshelper currently importsrequire_adapterfromwf_mcp.broker.service.adapters. This must stay inwf_mcpor be passed as a callback.
Private Method Dependencies
self.service._get_qualified_spec(qualified_name)— used 3 times. This is a private method onWfMcpService. Must become a port method.self.service._record_event(event)— used 7 times. This is a private method. Must become a port method (or the event bus itself becomes a port).
Store Ownership
wf_artifactsowns workflow store protocols and file-backed implementations.wf_api.stores.WorkflowStoresgroups the artifact, draft workspace, and run stores as protocol-neutral API dependencies.- MCP config construction creates file-backed workflow stores from
BrokerConfig.store_rootand injects them intoWfMcpService. WfMcpService.__post_init__no longer creates workflow stores from its MCPStore; direct service tests must inject stores when they exercise workflow persistence.- Future HTTP/API entrypoints should construct or receive the same
WorkflowStoresbundle instead of importingwf_mcp.
Naming Confusion
McpEvent/EventBus— the name says "Mcp" but the event system is protocol-neutral. Rename toDomainEvent/EventBusor move towf_platform.WorkflowSurfaceHandlers— the name suggests MCP surface. Rename toWorkflowApi.wf_mcp.workflow_surface— the package name implies MCP. The extracted package becomeswf_api.
Tests That Monkeypatch wf_cli.commands.*.load_cli_context
tests/wf_cli/test_run_deploy.pypatcheswf_cli.commands.deployments.load_cli_context,wf_cli.commands.runs.load_cli_contexttests/wf_cli/test_discovery_lifecycle.pypatcheswf_cli.commands.caps.load_cli_context,tests/wf_cli/test_discovery_lifecycle.pypatches multiple command modules- These patches inject
_load_cli_context_with_specswhich callsload_cli_contextthenregister_specs. After extraction,load_cli_contextwill constructWorkflowApiinstead ofWorkflowSurfaceHandlers. The patches should still work as long asCliContext.handlersremains the same attribute name.
FastAPI Implications
- The
WorkflowApiBackendport pattern makes FastAPI trivial: create aFastApiBackendAdapterthat provides the same stores/sources/execution without MCP. - The MCP-specific live-source-check can be an opt-in dependency injected into the API.
RawWorkflowPlanshould move towf_api.modelsorwf_coreso FastAPI can use it without importingwf_mcp.
Other Weirdness
WorkflowSurfaceHandlers._wrapper_artifact_for_capability_nameusesparse_workflow_surface_capability_idfromwf_mcp.workflow_surface.refs. This parser understands theworkflow.<id>.v<version>naming convention. It's protocol-neutral and should move towf_api.refs._available_sources(line 1238) is a module-level function that takesWfMcpServicedirectly. It should take the port interface instead, or become a method onWorkflowApi._live_source_diagnostics(line 1277) is the only function that needs MCP adapters/connections. It should be injected as an optional callback or stay as awf_mcpextension.
Suggested Next Plan
- Define the port interface (
WorkflowApiBackend) in a newsrc/wf_api/backend.py. Start with the 9 members identified above. - Create
WorkflowApiinsrc/wf_api/service.pyby copyingWorkflowSurfaceHandlersand changingself.service: WfMcpServicetoself.backend: WorkflowApiBackend. - Create adapter in
src/wf_mcp/broker/service/adapters.py(or a new file) that wrapsWfMcpServiceintoWorkflowApiBackend. - Update consumers (
wf_mcp.workflow_surface.tools,wf_cli.context) to useWorkflowApi(WfMcpServiceBackendAdapter(service)). - Move protocol-neutral modules (
next_actions,saved_subgraphs,run_lifecycle,runtime_dependencies,wrapper_hints,refs,constants,models) towf_api. - Move listing utilities (
matches_query,paged_list_payload) towf_platformorwf_api.shared. - Rename event types from
McpEventtoDomainEvent(or create a protocol-neutral fork). - Run full test suite — all existing tests must pass with import-only changes.
- Update
CliContextto useWorkflowApiinstead ofWorkflowSurfaceHandlers. - Add integration test for
WorkflowApiwith a mock backend (notWfMcpService). - Define store ownership before extracting service construction: remove implicit workflow-store creation from
WfMcpService.__post_init__, add config/API hooks for injected stores, and update tests to pass stores explicitly.
Dependency Graph After Extraction
wf_api (new)
├── depends on: wf_artifacts, wf_platform, wf_authoring, wf_core
└── defines: WorkflowApiBackend (protocol), WorkflowApi (implementation)
wf_mcp
├── depends on: wf_api, wf_artifacts, wf_platform, wf_authoring, wf_core
├── adapts: WfMcpService → WorkflowApiBackend
├── registers: FastMCP tools via register_workflow_tools (uses WorkflowApi)
└── owns: connections, adapters, proxy, server, admin, MCP events, MCP storage
wf_cli
├── depends on: wf_api, wf_mcp (for service construction)
├── uses: WorkflowApi via CliContext
└── owns: CLI commands, formatting, I/O