chore: clean up extraction hygiene docs and guards

This commit is contained in:
lda
2026-06-08 17:27:43 +07:00 Verified
parent be417e2a44
commit e1a74fdd4a
14 changed files with 998 additions and 248 deletions
@@ -2,6 +2,12 @@
Date: 2026-06-07 Date: 2026-06-07
> **Historical note (2026-06-08):** This research snapshot predates the
> `wf_sources_mcp` extraction slices completed on 2026-06-08. Several blockers
> called out below have since moved: source ID helpers live in
> `wf_sources_mcp.ids`, MCP runtime/SDK/client code lives in `wf_sources_mcp`,
> and broker DTO construction helpers live in `wf_mcp.source_registry`.
## Executive Summary ## Executive Summary
`wf_mcp` is a monolith containing four distinct responsibilities that should become separate packages: `wf_mcp` is a monolith containing four distinct responsibilities that should become separate packages:
@@ -0,0 +1,202 @@
# Compatibility Shim Retirement Map
**Date:** 2026-06-08
**Scope:** `wf_mcp``wf_sources_mcp` re-export shims, `wf_mcp``wf_api` extraction shims
**Goal:** Decide what can stay indefinitely, what can be deprecated, and what has no callers.
---
## Shim Module Inventory
### 1. Pure Re-export Shims (wf_sources_mcp → wf_mcp)
These modules exist **solely** to re-export canonical symbols from `wf_sources_mcp`. No additional logic.
| Shim Module | Canonical Module | Re-exported Symbols | Prod Callers | Test Callers |
|---|---|---|---|---|
| `wf_mcp.auth` | `wf_sources_mcp.auth` | `AuthRecord`, `auth_missing_diagnostic`, `auth_ref_for_connection`, `connection_auth_diagnostic`, `mcp_auth_env`, `mcp_auth_from_neutral`, `mcp_auth_headers`, `neutral_auth_from_mcp` | `wf_mcp.models`, `wf_mcp.broker.discovery`, `wf_mcp.broker.service.source_registry_admin` | `test_auth.py`, `test_compat_imports.py`, `service/test_events.py`, `service/test_upstream_transport.py`, `workflow_surface/conftest.py`, `test_stateful_runtime.py`, `test_support.py`, `service/conftest.py`, `test_workflow_wrappers.py`, `test_broker_server.py`, `service/test_auth_admin.py` |
| `wf_mcp.capabilities` | `wf_sources_mcp.catalog.entries` | `CatalogNodeEntry`, `CatalogPromptEntry`, `CatalogResourceEntry`, `DiscoveredPrompt`, `DiscoveredResource`, `DiscoveredTool` | `wf_mcp.broker.discovery` (DiscoveredTool), `wf_mcp.broker.service.core` (via wf_sources_mcp directly) | `test_compat_imports.py`, `service/conftest.py`, `test_stateful_runtime.py`, `test_support.py`, `test_sdk_adapter.py`, `test_workflow_wrappers.py`, `service/test_event_recorder.py`, `workflow_surface/conftest.py`, `workflow_surface/test_deployments.py`, `test_deployment_api.py` |
| `wf_mcp.catalog.__init__` | `wf_sources_mcp.catalog` (via `.models`) | `CatalogSnapshot`, `dump_catalog_snapshot` | `wf_mcp.broker.discovery` (DiscoveredTool via wf_sources_mcp directly) | `test_compat_imports.py` |
| `wf_mcp.catalog.models` | `wf_sources_mcp.catalog.models` | `CatalogSnapshot`, `dump_catalog_snapshot` | `wf_mcp.models` | `test_compat_imports.py`, `test_workflow_config_bridge.py` |
| `wf_mcp.sdk.__init__` | `wf_sources_mcp.sdk` | `BackendAdapter`, `McpSdkAdapter`, `PromptRuntime`, `ResourceRuntime`, `StatefulMcpRuntime`, `ToolCallResult`, `ToolRuntime` | `wf_mcp.broker.config`, `wf_mcp.broker.server`, `wf_mcp.server.core` (all via wf_sources_mcp directly) | `test_compat_imports.py`, `test_sdk_adapter.py`, `test_deployment_api.py`, `service/conftest.py`, `service/test_events.py`, `test_stateful_runtime.py`, `test_workflow_wrappers.py`, `workflow_surface/conftest.py`, `workflow_surface/test_deployments.py` |
| `wf_mcp.sdk.base` | `wf_sources_mcp.sdk` | `BackendAdapter`, `PromptRuntime`, `ResourceRuntime`, `StatefulMcpRuntime`, `ToolCallResult`, `ToolRuntime` | (none - only test_compat_imports) | `test_compat_imports.py` |
| `wf_mcp.sdk.adapter` | `wf_sources_mcp.sdk.adapter` | `McpSdkAdapter` | `wf_mcp.broker.server`, `wf_mcp.server.core` (via wf_sources_mcp directly) | `test_compat_imports.py`, `test_sdk_adapter.py` |
| `wf_mcp.sdk.converters` | `wf_sources_mcp.sdk.converters` | `prompt_to_discovered`, `resource_to_discovered`, `tool_result_to_call_result`, `tool_to_discovered`, `workflow_output_schema_from_mcp_tool_schema` | (none - only test_compat_imports) | `test_compat_imports.py`, `test_sdk_converters.py` |
| `wf_mcp.storage.__init__` | `wf_sources_mcp.storage` | `AuthStore`, `CatalogStore`, `FileAuthStore`, `FileCatalogStore`, `FileStore`, `Store` | `wf_mcp.broker.config`, `wf_mcp.broker.service.core`, `wf_mcp.broker.service.auth_admin`, `wf_mcp.broker.service.upstream_transport` (all via wf_sources_mcp directly) | `test_compat_imports.py`, `test_admin_auth_rpc.py`, `test_mcp_backed_server_rpc.py`, `test_auth.py`, `test_store.py`, `test_mcp_workflow_server.py`, `service/conftest.py`, `service/test_upstream_transport.py`, `service/test_connection_service.py`, `service/test_catalog.py`, `service/test_events.py`, `service/test_auth_admin.py`, `workflow_surface/conftest.py`, `workflow_surface/test_runs.py`, `workflow_surface/test_wrappers.py` |
| `wf_mcp.storage.store` | `wf_sources_mcp.storage.store` | `AuthStore`, `CatalogStore`, `FileAuthStore`, `FileCatalogStore`, `FileStore`, `Store` | `wf_mcp.broker.service.auth_admin` (AuthStore via wf_sources_mcp directly) | `test_compat_imports.py` |
| `wf_mcp.runtime.factory` | `wf_sources_mcp.runtime.factory` | `PersistentSessionFactory` | `wf_mcp.broker.config` (via wf_sources_mcp directly) | `test_compat_imports.py`, `test_stateful_runtime.py` |
| `wf_mcp.runtime.pool` | `wf_sources_mcp.runtime.pool` | `McpRuntimePool`, `SessionFactory`, `connection_runtime_fingerprint` | `wf_mcp.broker.config` (via wf_sources_mcp directly) | `test_compat_imports.py`, `test_stateful_runtime.py` |
| `wf_mcp.runtime.session` | `wf_sources_mcp.runtime.session` | `PersistentMcpSession`, `RawToolCaller` | (none - only test_compat_imports) | `test_compat_imports.py`, `test_stateful_runtime.py` |
| `wf_mcp.runtime.protocols` | `wf_sources_mcp.sdk` | `ToolExecutor` | (none - only test_compat_imports) | `test_compat_imports.py` |
| `wf_mcp.broker.catalog` | `wf_sources_mcp.catalog` | `CombinedCatalog`, `snapshot_from_specs` | `wf_mcp.broker.__init__` (re-exports) | `test_compat_imports.py` |
| `wf_mcp.broker.service.adapters` | `wf_sources_mcp.adapters` | `AdapterLookupRef`, `LegacyAdapterRef`, `SourceAdapterRef`, `require_adapter` | `wf_mcp.broker.service.upstream_transport` (via wf_sources_mcp directly) | `test_compat_imports.py` |
| `wf_mcp.workflow.wrappers` | `wf_sources_mcp.tool_wrappers` (+ `schema_models`) | `wrap_discovered_tool`, `_model_from_schema` | `wf_mcp.workflow.__init__` (re-exports) | `test_compat_imports.py`, `test_workflow_wrappers.py`, `test_stateful_runtime.py` |
| `wf_mcp.workflow.__init__` | `wf_mcp.workflow.wrappers` | `wrap_discovered_tool` | (none outside wf_mcp) | `test_compat_imports.py`, `test_stateful_runtime.py`, `test_workflow_wrappers.py` |
### 2. Shim + Local Logic (wf_sources_mcp → wf_mcp)
These modules re-export from canonical but also contain **additional local code** that must be preserved or migrated.
| Shim Module | Canonical Module | Local Code | Prod Callers | Test Callers |
|---|---|---|---|---|
| `wf_mcp.connections` | `wf_sources_mcp.ids` | `ConnectionRegistry` class, `qualify_node_name` function | `wf_mcp.broker.service.core`, `wf_mcp.broker.service.connection_service`, `wf_mcp.broker.service.source_registry_admin` | `test_compat_imports.py`, `test_store.py`, `service/test_upstream_transport.py`, `service/test_connection_service.py`, `service/test_source_registry_admin.py` |
| `wf_mcp.source_registry` | `wf_sources_mcp.source_registry` | `registry_entry_to_connection_config`, `workflow_mcp_source_to_connection_config` | `wf_mcp.broker.config`, `wf_mcp.broker.service.connection_service` | `test_compat_imports.py`, `test_mcp_workflow_server.py`, `test_broker_server.py`, `service/test_connection_service.py`, `service/test_source_registry_admin.py`, `test_source_registry.py`, `server/test_docs.py` |
| `wf_mcp.runtime.__init__` | `wf_sources_mcp.runtime` (+ local `ToolExecutor`) | Re-exports `ToolExecutor` from `wf_sources_mcp.sdk` alongside runtime types | `wf_mcp.broker.service.core`, `wf_mcp.broker.service.upstream_transport` (via wf_sources_mcp directly) | `test_compat_imports.py`, `service/test_connection_service.py`, `service/test_events.py`, `service/test_source_registry_admin.py`, `test_workflow_wrappers.py` |
| `wf_mcp.models` | `wf_mcp.auth` + `wf_mcp.broker.models` + `wf_api.models` + `wf_sources_mcp.catalog.models` | Aggregator shim: re-exports `AuthRecord`, `BrokerConfig`, `BrokerStoreRoots`, `ConnectionConfig`, `SourceConfigOwnership`, `RawWorkflowPlan`, `CatalogSnapshot`, `dump_catalog_snapshot` | (none outside wf_mcp) | **Heavy**: 50+ test files import from this module |
### 3. wf_api Extraction Shims (wf_api → wf_mcp.workflow_surface)
Code extracted from `wf_mcp.workflow_surface` to `wf_api`. These shims preserve backward compatibility.
| Shim Module | Canonical Module | Re-exported Symbols | Prod Callers | Test Callers |
|---|---|---|---|---|
| `wf_mcp.workflow_surface.run_lifecycle` | `wf_api.run_lifecycle` | `create_pinned_environment`, `has_blocking_diagnostics`, `load_stored_run`, `mark_resume_blocked`, `persist_stopped_run`, `restore_interrupted_run`, `validate_pinned_resume_environment` | (none outside wf_mcp) | `test_run_lifecycle_extraction.py` |
| `wf_mcp.workflow_surface.saved_subgraphs` | `wf_api.saved_subgraphs` | `SavedSubgraphTree`, `direct_wrapper_interrupt_diagnostic`, `prepare_saved_subgraphs`, `resolve_saved_subgraph_tree`, `saved_subgraph_tree_from_snapshots`, `validate_saved_subgraph_tree` | (none outside wf_mcp) | `test_saved_subgraphs_extraction.py` |
| `wf_mcp.workflow_surface.runtime_dependencies` | `wf_api.runtime_dependencies` | `RuntimeDependencies`, `resolve_runtime_dependencies` | (none outside wf_mcp) | `test_runtime_dependencies_extraction.py` |
| `wf_mcp.workflow_surface.next_actions` | `wf_api.next_actions` | `NextActionPatchExample`, `NextActions`, `NextActionTool` | (none outside wf_mcp) | `test_next_actions.py` (line 164) |
---
## Analysis: Which Shims Are Still Needed for Public Compatibility
### Shims with production callers INSIDE wf_mcp (still needed internally)
These shims are imported by other `wf_mcp` modules. They are "needed" but the callers could switch to canonical.
| Shim | Internal Callers (within wf_mcp) |
|---|---|
| `wf_mcp.auth` | `wf_mcp.models`, `wf_mcp.broker.discovery`, `wf_mcp.broker.service.source_registry_admin` |
| `wf_mcp.connections` | `wf_mcp.broker.service.core`, `wf_mcp.broker.service.connection_service`, `wf_mcp.broker.service.source_registry_admin` |
| `wf_mcp.source_registry` | `wf_mcp.broker.config`, `wf_mcp.broker.service.connection_service` |
| `wf_mcp.models` | `wf_mcp.broker.discovery`, `wf_mcp.broker.service.core`, `wf_mcp.broker.service.source_catalog`, `wf_mcp.broker.service.upstream_transport`, `wf_mcp.broker.service.connection_service`, `wf_mcp.broker.service.source_registry_admin` |
| `wf_mcp.runtime.__init__` | (only test callers) |
| `wf_mcp.workflow.wrappers` | `wf_mcp.workflow.__init__` |
### Shims with NO production callers (outside wf_mcp and test_compat_imports)
These shims exist solely for the test_compat_imports.py regression suite. They can be deprecated.
| Shim | Notes |
|---|---|
| `wf_mcp.sdk.base` | Only caller is `test_compat_imports.py` |
| `wf_mcp.sdk.converters` | Only caller is `test_compat_imports.py` + `test_sdk_converters.py` |
| `wf_mcp.runtime.protocols` | Only caller is `test_compat_imports.py` |
| `wf_mcp.runtime.session` | Only caller is `test_compat_imports.py` |
| `wf_mcp.storage.store` | Only caller is `test_compat_imports.py` |
| `wf_mcp.broker.service.adapters` | Only caller is `test_compat_imports.py` |
---
## Which Shims Are Only Used by Tests
| Shim | Test-Only Callers |
|---|---|
| `wf_mcp.sdk.base` | `test_compat_imports.py` |
| `wf_mcp.sdk.converters` | `test_compat_imports.py`, `test_sdk_converters.py` |
| `wf_mcp.runtime.protocols` | `test_compat_imports.py` |
| `wf_mcp.runtime.session` | `test_compat_imports.py`, `test_stateful_runtime.py` |
| `wf_mcp.storage.store` | `test_compat_imports.py` |
| `wf_mcp.broker.service.adapters` | `test_compat_imports.py` |
| `wf_mcp.workflow_surface.run_lifecycle` | `test_run_lifecycle_extraction.py` |
| `wf_mcp.workflow_surface.saved_subgraphs` | `test_saved_subgraphs_extraction.py` |
| `wf_mcp.workflow_surface.runtime_dependencies` | `test_runtime_dependencies_extraction.py` |
| `wf_mcp.workflow_surface.next_actions` | `test_next_actions.py` (line 164) |
---
## Which Imports in Production Should Be Switched to Canonical Paths
These are `wf_mcp` modules (not shims) that import from `wf_sources_mcp` directly but could instead import through the shim or vice versa. The inconsistency means some internal callers use the shim while the "real" code uses canonical. This is fine architecturally but creates confusion.
### Production imports currently going through shims (should switch to canonical):
| File | Current Import | Should Import From |
|---|---|---|
| `wf_mcp.models` | `from wf_mcp.auth import AuthRecord` | `from wf_sources_mcp.auth import AuthRecord` |
| `wf_mcp.broker.discovery` | `from ..auth import AuthRecord` | `from wf_sources_mcp.auth import AuthRecord` |
| `wf_mcp.broker.service.source_registry_admin` | `from ...auth import AuthRecord, connection_auth_diagnostic` | `from wf_sources_mcp.auth import AuthRecord, connection_auth_diagnostic` |
| `wf_mcp.broker.service.source_catalog` | `from ...connections import ConnectionConfig, qualify_node_name` | Keep as-is (ConnectionConfig is broker-specific) |
| `wf_mcp.broker.service.core` | `from ...connections import ConnectionRegistry` | Keep as-is (ConnectionRegistry is broker-specific) |
| `wf_mcp.broker.service.connection_service` | `from wf_mcp.source_registry import connection_config_to_registry_entry, registry_entry_to_connection_config` | Keep as-is (these are broker-specific conversion helpers) |
| `wf_mcp.broker.service.connection_service` | `from wf_sources_mcp.source_registry import SourceRegistryFile, SourceRegistryStore` | Already canonical |
| `wf_mcp.broker.service.events` | `from wf_sources_mcp.catalog.models import CatalogSnapshot` | Already canonical |
| `wf_mcp.broker.config` | `from wf_sources_mcp.runtime import ...` | Already canonical |
| `wf_mcp.broker.config` | `from wf_sources_mcp.sdk import McpSdkAdapter` | Already canonical |
| `wf_mcp.broker.server` | `from wf_sources_mcp.sdk import McpSdkAdapter` | Already canonical |
| `wf_mcp.broker.server` | `from wf_sources_mcp.source_registry import FileSourceRegistryStore, SourceRegistryStore` | Already canonical |
| `wf_mcp.server.core` | `from wf_sources_mcp.sdk import McpSdkAdapter` | Already canonical |
| `wf_mcp.server.core` | `from wf_sources_mcp.source_registry import FileSourceRegistryStore` | Already canonical |
### Production imports that are already canonical (no change needed):
- `wf_mcp.broker.config``wf_sources_mcp.runtime`, `wf_sources_mcp.sdk`, `wf_sources_mcp.source_registry`, `wf_sources_mcp.storage`
- `wf_mcp.broker.server``wf_sources_mcp.sdk`, `wf_sources_mcp.source_registry`
- `wf_mcp.broker.service.core``wf_sources_mcp.auth`, `wf_sources_mcp.catalog`, `wf_sources_mcp.sdk`, `wf_sources_mcp.source_registry`, `wf_sources_mcp.storage`
- `wf_mcp.broker.service.source_catalog``wf_sources_mcp.auth`, `wf_sources_mcp.catalog`, `wf_sources_mcp.connections`, `wf_sources_mcp.schema_models`, `wf_sources_mcp.sdk`, `wf_sources_mcp.storage`
- `wf_mcp.broker.service.upstream_transport``wf_sources_mcp.adapters`, `wf_sources_mcp.auth`, `wf_sources_mcp.catalog`, `wf_sources_mcp.connections`, `wf_sources_mcp.discovery`, `wf_sources_mcp.sdk`, `wf_sources_mcp.storage`
- `wf_mcp.broker.service.auth_admin``wf_sources_mcp.storage`
- `wf_mcp.broker.service.source_registry_admin``wf_sources_mcp.connections`, `wf_sources_mcp.source_registry`
- `wf_mcp.broker.discovery``wf_sources_mcp.catalog`, `wf_sources_mcp.connections`, `wf_sources_mcp.discovery`, `wf_sources_mcp.sdk`, `wf_sources_mcp.tool_events`
- `wf_mcp.shared.names``wf_sources_mcp.ids` (one constant)
- `wf_mcp.workflow.wrappers``wf_sources_mcp.schema_models`, `wf_sources_mcp.tool_wrappers`
---
## Recommended Next Cleanup Slice
### Slice 1: Switch internal callers to canonical imports (low risk)
**Goal:** Remove internal `wf_mcp` code that imports through shims when canonical is available.
1. `wf_mcp.models`: Change `from wf_mcp.auth import AuthRecord``from wf_sources_mcp.auth import AuthRecord`
2. `wf_mcp.broker.discovery`: Change `from ..auth import AuthRecord``from wf_sources_mcp.auth import AuthRecord`
3. `wf_mcp.broker.service.source_registry_admin`: Change `from ...auth import AuthRecord, connection_auth_diagnostic``from wf_sources_mcp.auth import AuthRecord, connection_auth_diagnostic`
These three changes remove the internal dependency chain where `wf_mcp.broker.service.*` imports through `wf_mcp.auth` (shim) instead of directly from `wf_sources_mcp.auth` (canonical).
### Slice 2: Deprecate test-only shims
**Goal:** Mark shims that have NO production callers (only test_compat_imports) as deprecated.
Candidates:
- `wf_mcp.sdk.base`
- `wf_mcp.sdk.converters`
- `wf_mcp.runtime.protocols`
- `wf_mcp.runtime.session`
- `wf_mcp.storage.store`
- `wf_mcp.broker.service.adapters`
These can be marked with a `# DEPRECATED: import from wf_sources_mcp directly` comment and removed once test_compat_imports.py is updated.
### Slice 3: Deprecate wf_api extraction shims
**Goal:** Mark `wf_mcp.workflow_surface.{run_lifecycle, saved_subgraphs, runtime_dependencies, next_actions}` as deprecated.
These are already extraction-complete shims. The test files (`test_*_extraction.py`) verify they point to canonical. They can be removed once no external callers remain.
### Slice 4: Consolidate `wf_mcp.models` (high impact, medium risk)
**Goal:** `wf_mcp.models` is the highest-traffic shim (50+ test files). It aggregates `AuthRecord`, `BrokerConfig`, `ConnectionConfig`, `CatalogSnapshot`, `RawWorkflowPlan`, etc.
- `BrokerConfig`, `ConnectionConfig`, `BrokerStoreRoots`, `SourceConfigOwnership` are broker-specific DTOs — they should stay in `wf_mcp.broker.models` (already canonical).
- `AuthRecord` comes from `wf_sources_mcp.auth` via `wf_mcp.auth` shim.
- `CatalogSnapshot`, `dump_catalog_snapshot` come from `wf_sources_mcp.catalog.models` via `wf_mcp.catalog.models` shim.
- `RawWorkflowPlan` comes from `wf_api.models`.
**Recommended:** Keep `wf_mcp.models` as a convenience aggregator for tests, but update the imports within it to use canonical paths. New production code should import from `wf_mcp.broker.models` or `wf_sources_mcp.*` directly.
---
## Summary Table
| Category | Count | Action |
|---|---|---|
| Pure re-export shims (wf_sources_mcp) | 17 modules | Deprecate test-only ones (6); keep rest for now |
| Shim + local logic (wf_sources_mcp) | 4 modules | Keep (local code is broker-specific) |
| wf_api extraction shims | 4 modules | Deprecate (extraction complete) |
| Non-shim modules (own implementation) | ~30+ modules | No action needed |
**Total shim modules:** 21
**Shims with zero production callers:** 6 (+ 4 wf_api extraction shims)
**Shims with internal-only callers:** 11
**Shims with external (non-wf_mcp) callers:** 0 (all external callers are test files)
@@ -0,0 +1,385 @@
# Extraction Hygiene Audit
Date: 2026-06-08
Auditor: opencode (mimo-v2.5-free)
Scope: `src/wf_sources_mcp/**`, `src/wf_mcp/**` compatibility shims,
`docs/current_roadmap.md`, `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md`,
`docs/superpowers/research/2026-06-07-wf-mcp-runtime-source-provider-map.md`
---
## Summary
The `wf_sources_mcp` extraction is in good shape. The package is now **fully
self-contained** -- it imports nothing from `wf_mcp` at runtime or
`TYPE_CHECKING`. The dependency direction is exclusively `wf_mcp` -> `wf_sources_mcp`.
Key achievements since the 2026-06-07 research map:
- `parse_connection_id` and `RESERVED_CONNECTION_IDS` are canonical in
`wf_sources_mcp.ids` (no more `wf_mcp.connections` or `wf_mcp.shared.names`
dependency).
- `McpSourceConnection` replaces `ConnectionConfig` in all `wf_sources_mcp`
runtime code.
- `open_mcp_session` is canonical in `wf_sources_mcp.client.transport`.
- Runtime, SDK adapter, catalog, discovery, schema models, tool events, tool
wrappers, and adapters are all canonical in `wf_sources_mcp`.
The remaining items are stale docs/comments, minor shim hygiene, and
future-work boundary decisions. Nothing is a correctness issue today.
---
## Findings
### F1: `wf_mcp.sdk.base.py` docstring is stale
**File:** `src/wf_mcp/sdk/base.py:1-4`
**Content:** `"""Compatibility shim for MCP upstream SDK protocol/result types.\nCanonical implementation lives in \`wf_sources_mcp.sdk\`."""`
**Status:** Stale. The docstring says canonical lives in `wf_sources_mcp.sdk`, which is
correct, but `wf_mcp.sdk.base` is not referenced by any other code. It is a
dead re-export module.
**Category:** hygiene
**Fix:** Safe to delete now. No code imports from `wf_mcp.sdk.base`.
---
### F2: `wf_mcp.storage` shim has stale docstring
**File:** `src/wf_mcp/storage/__init__.py` (re-exports from `wf_sources_mcp.storage`)
**Content:** No docstring present. The `__init__.py` just re-exports.
**Category:** docs-only
**Fix:** Defer. No functional issue. The shim is correct.
---
### F3: `wf_mcp.workflow_surface/constants.py` docstring says "during extraction"
**File:** `src/wf_mcp/workflow_surface/constants.py:1-7`
**Content:** `"""Compatibility shim for workflow API constants.\nNew code should import these literals from \`wf_api.constants\`. This module stays\nso older MCP workflow-surface imports keep working during extraction."""`
**Status:** Stale. The extraction of `wf_api.constants` is complete. The phrase
"during extraction" implies the shim is temporary. In fact, the shim is
intentional long-lived compatibility infrastructure until MCP callers migrate.
**Category:** docs-only
**Fix:** Safe to fix now. Replace "during extraction" with "until callers migrate".
---
### F4: `wf_mcp.workflow_surface/refs.py` docstring says "during extraction"
**File:** `src/wf_mcp/workflow_surface/refs.py:1-6`
**Content:** Same pattern as F3.
**Category:** docs-only
**Fix:** Safe to fix now.
---
### F5: `wf_mcp.workflow_surface/next_actions.py` docstring says "during extraction"
**File:** `src/wf_mcp/workflow_surface/next_actions.py:1-6`
**Content:** Same pattern as F3.
**Category:** docs-only
**Fix:** Safe to fix now.
---
### F6: `wf_mcp.workflow_surface/wrapper_hints.py` docstring says "during extraction"
**File:** `src/wf_mcp/workflow_surface/wrapper_hints.py:1-6`
**Content:** Same pattern as F3.
**Category:** docs-only
**Fix:** Safe to fix now.
---
### F7: `wf_mcp.workflow_surface/runtime_dependencies.py` docstring says "during extraction"
**File:** `src/wf_mcp/workflow_surface/runtime_dependencies.py:1-6`
**Content:** Same pattern as F3.
**Category:** docs-only
**Fix:** Safe to fix now.
---
### F8: `wf_mcp.workflow_surface/saved_subgraphs.py` docstring says "during extraction"
**File:** `src/wf_mcp/workflow_surface/saved_subgraphs.py:1-6`
**Content:** `"""Compatibility shim -- canonical implementation moved to wf_api.saved_subgraphs.\nThis module re-exports every public symbol so that existing\n\`\`from wf_mcp.workflow_surface.saved_subgraphs import ...\`\` continues to work\nwithout changes. New code should import from \`\`wf_api.saved_subgraphs\`\`\ndirectly."""`
**Status:** This one is actually correct and well-worded. No action needed.
**Category:** docs-only
**Fix:** None needed.
---
### F9: `wf_mcp.workflow_surface/run_lifecycle.py` docstring is correct
**File:** `src/wf_mcp/workflow_surface/run_lifecycle.py:1-6`
**Content:** Same correct pattern as F8.
**Category:** docs-only
**Fix:** None needed.
---
### F10: `docs/superpowers/research/2026-06-07-wf-mcp-runtime-source-provider-map.md` has stale blocker claims
**File:** `docs/superpowers/research/2026-06-07-wf-mcp-runtime-source-provider-map.md`
**Line 115-118:** Claims `wf_mcp.runtime.factory`, `wf_mcp.runtime.pool`,
`wf_mcp.runtime.session` import `wf_mcp.models.ConnectionConfig` at runtime.
**Status:** These are now compatibility shims re-exporting from `wf_sources_mcp.runtime`.
The runtime modules in `wf_mcp.runtime.*` no longer contain original code -- they
are pure re-exports. The runtime code is now canonical in `wf_sources_mcp.runtime`.
**Line 116:** Claims `wf_mcp.sdk.adapter` imports `wf_mcp.models.ConnectionConfig` at
runtime. **Status:** Now a pure re-export shim from `wf_sources_mcp.sdk.adapter`.
**Category:** docs-only (historical research doc)
**Fix:** Defer. This is a historical snapshot. Add a note at the top saying the
analysis is from 2026-06-07 and some blockers have since been resolved.
---
### F11: `docs/superpowers/research/2026-06-07-wf-mcp-runtime-source-provider-map.md` claims `parse_connection_id` still lives in `wf_mcp.connections`
**File:** `docs/superpowers/research/2026-06-07-wf-mcp-runtime-source-provider-map.md`
**Lines 290-310 (Blocker 2):** States `parse_connection_id` lives in
`wf_mcp.connections` and `RESERVED_CONNECTION_IDS` in `wf_mcp.shared.names`.
**Status:** Both are now canonical in `wf_sources_mcp.ids`. The `wf_mcp.connections`
module imports from `wf_sources_mcp.ids` and is itself a compatibility shim.
**Category:** docs-only (stale claim)
**Fix:** Defer. Historical doc.
---
### F12: `wf_mcp.workflow/wrappers.py` has stale `_model_from_schema` alias
**File:** `src/wf_mcp/workflow/wrappers.py:10`
**Content:** `_model_from_schema = model_from_schema # TODO: remove when callers migrate`
**Status:** This is a compatibility alias for callers that still import
`wf_mcp.workflow.wrappers._model_from_schema`. The TODO is stale if all callers
have migrated. No grep matches for `_model_from_schema` outside this file, so
the alias may be dead.
**Category:** hygiene
**Fix:** Safe to check and potentially remove. Needs caller search confirmation.
---
### F13: `wf_mcp.models` re-exports `CatalogSnapshot` and `dump_catalog_snapshot`
**File:** `src/wf_mcp/models.py:11`
**Content:** `from wf_sources_mcp.catalog.models import CatalogSnapshot, dump_catalog_snapshot`
**Status:** This re-export means `wf_mcp.models.CatalogSnapshot` still works.
However, `wf_sources_mcp.catalog.models` is the canonical location and
`wf_mcp.catalog.models` is the primary shim. Having both `wf_mcp.models` and
`wf_mcp.catalog.models` re-export the same types is redundant but not harmful.
**Category:** hygiene
**Fix:** Defer. Remove from `wf_mcp.models` when no callers remain.
---
### F14: `current_roadmap.md` says "specs_from_discovered_tools remains in wf_mcp"
**File:** `docs/current_roadmap.md`
**Lines ~339-346:** Multiple roadmap entries state `specs_from_discovered_tools`
remains in `wf_mcp`. This is accurate -- `wf_mcp.broker.discovery` still contains
the broker compatibility adapter that wraps the canonical
`wf_sources_mcp.discovery.specs_from_discovered_tools`. The `wf_mcp` version
converts `ConnectionConfig` to `McpSourceConnection` and projects events.
**Category:** docs-only (accurate but could be clearer)
**Fix:** Defer. The wording is technically correct; the canonical version is in
`wf_sources_mcp.discovery` and `wf_mcp.broker.discovery` is the broker adapter.
---
### F15: `current_roadmap.md` says "wf_mcp.broker.catalog retained as a compatibility shim"
**File:** `docs/current_roadmap.md`
**Lines ~333-334:** States `wf_mcp.broker.catalog` is retained as a shim.
**Status:** Correct. `src/wf_mcp/broker/catalog.py` re-exports
`CombinedCatalog` and `snapshot_from_specs` from `wf_sources_mcp.catalog`.
**Category:** docs-only (accurate)
**Fix:** None needed.
---
### F16: Historical plan `2026-06-07-mcp-client-session-opener.md` references old paths
**File:** `docs/historical/superpowers/plans/2026-06-07-mcp-client-session-opener.md:7`
**Content:** References `wf_mcp.sdk.adapter.McpSdkAdapter` and
`wf_mcp.runtime.factory.PersistentSessionFactory` as needing the shared opener.
**Status:** Now completed. Both `wf_mcp.sdk.adapter` and `wf_mcp.runtime.factory`
are pure re-export shims; the canonical code uses `open_mcp_session` from
`wf_sources_mcp.client.transport`.
**Category:** docs-only (historical plan, completed)
**Fix:** Defer. Historical plans are snapshots of the state at planning time.
---
### F17: Historical plan `2026-06-05-legacy-mcp-config-migration.md` references old paths
**File:** `docs/historical/superpowers/plans/2026-06-05-legacy-mcp-config-migration.md:7`
**Content:** References `wf_mcp.sdk.adapter` and `wf_mcp.runtime.factory` as
needing the flat connection metadata shape.
**Status:** Completed. The migration path works through `wf_config` ->
`workflow_mcp_source_to_connection_config` in `wf_mcp.source_registry`.
**Category:** docs-only (historical plan, completed)
**Fix:** Defer.
---
### F18: `wf_mcp.broker.discovery` imports `specs_from_discovered_tools` from `wf_sources_mcp`
**File:** `src/wf_mcp/broker/discovery.py:13-14`
**Content:** `from wf_sources_mcp.discovery import (\n specs_from_discovered_tools as source_specs_from_discovered_tools,\n)`
**Status:** This is the correct broker adapter pattern. The broker version wraps
the canonical version with `ConnectionConfig` -> `McpSourceConnection` conversion
and event projection. No issue.
**Category:** docs-only (no issue)
**Fix:** None needed.
---
### F19: `wf_mcp.workflow_surface.handlers.py` docstring says "while the MCP surface is migrated"
**File:** `src/wf_mcp/workflow_surface/handlers.py:13-17`
**Content:** `"""Compatibility wrapper for old wf_mcp.workflow_surface imports.\n\nNew code should construct \`WorkflowApi(context_from_service(service))\`\ndirectly. This shim keeps tests and legacy broker artifact tools working\nwhile the MCP surface is migrated."""`
**Status:** Slightly stale. The `WorkflowSurfaceHandlers` shim is now the
documented compatibility path. The phrase "while the MCP surface is migrated"
implies the migration is in progress. It may be more accurate to say "for
legacy callers" instead.
**Category:** docs-only
**Fix:** Safe to fix now.
---
### F20: `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md` references historical blocker state
**File:** `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md`
**Lines 134-146:** States `specs_from_discovered_tools` remains in `wf_mcp`
"until the event/wrapper seam is neutralized." The seam has since been neutralized
(slices 18-20 in the spec). The spec lists these as completed, but the phrasing
in earlier slices ("remains in wf_mcp until...") could mislead readers who only
scan the early items.
**Category:** docs-only (spec is internally consistent when read fully)
**Fix:** Defer. The spec is internally consistent; each slice says "Complete."
---
### F21: `docs/superpowers/research/2026-06-07-wf-mcp-runtime-source-provider-map.md` has stale "Next Slices" section
**File:** `docs/superpowers/research/2026-06-07-wf-mcp-runtime-source-provider-map.md`
**Lines 549-650 (Slice 0-6):** The recommended next slices are now mostly completed:
- Slice 0 (SourceConnection protocol): Completed via `McpSourceConnection`
- Slice 1 (parse_connection_id move): Completed to `wf_sources_mcp.ids`
- Slice 2 (open_mcp_session): Completed in `wf_sources_mcp.client.transport`
- Slice 3 (runtime move): Completed to `wf_sources_mcp.runtime`
- Slice 4 (adapter move): Completed to `wf_sources_mcp.sdk.adapter`
The research doc does not have completion markers, unlike the spec which does.
**Category:** docs-only (stale research artifact)
**Fix:** Defer. Historical research. Could add a note at the top.
---
### F22: `wf_mcp.broker.service.core.py` imports `SourceRegistryStore` from `wf_sources_mcp`
**File:** `src/wf_mcp/broker/service/core.py:26`
**Content:** `from wf_sources_mcp.source_registry import SourceRegistryStore`
**Status:** This is correct broker-consumes-source-provider direction. The broker
service needs `SourceRegistryStore` for `sync_connections_from_config`. No issue.
**Category:** docs-only (no issue)
**Fix:** None needed.
---
### F23: `wf_mcp.shared.names.py` still imports FastMCP at top level
**File:** `src/wf_mcp/shared/names.py:8-15`
**Content:** Imports `fastmcp.server.transforms` and `fastmcp.utilities.versions` at
module level for `ProxyNamespace`, `LdaNamespace`, and `ProxyToolName`.
**Status:** This is intentional -- `shared/names.py` is MCP frontend transport code
that belongs in a future `wf_transport_mcp`. The `RESERVED_CONNECTION_IDS` constant
has moved to `wf_sources_mcp.ids`, and `wf_mcp.shared.names` re-exports it.
The FastMCP import is only needed for the proxy namespace classes, which are
MCP-frontend-specific. No issue with extraction hygiene, but this is a
dependency boundary concern for future `wf_transport_mcp` extraction.
**Category:** future-work
**Fix:** Defer. Part of `wf_transport_mcp` extraction.
---
### F24: `wf_mcp.broker.service.source_catalog.py` docstring says "MCP-broker-internal"
**File:** `src/wf_mcp/broker/service/source_catalog.py:55-58`
**Content:** `"""Own service-local capability sources and catalog projections.\n\nThis is deliberately still MCP-broker-internal. It knows about stored MCP\ncatalog snapshots because hydrated workflow NodeSpecs must call back through\nthe broker's configured tool executor."""`
**Status:** Accurate. The source catalog service is broker-internal by design.
It manages `CapabilitySource` registrations and catalog hydration. The word
"still" implies future change, which is correct (eventually this may move
behind a protocol), but the docstring is honest about current state.
**Category:** docs-only (accurate)
**Fix:** None needed.
---
### F25: `wf_mcp.broker.service.upstream_transport.py` docstring says "not protocol-neutral"
**File:** `src/wf_mcp/broker/service/upstream_transport.py:48-50`
**Content:** `"""Own upstream MCP adapter/auth operations for the broker service.\n\nThis is not protocol-neutral. It is the MCP transport implementation used by\nadmin calls, discovery, generated workflow NodeSpecs, and live source checks."""`
**Status:** Accurate. This is broker-specific MCP transport orchestration.
**Category:** docs-only (accurate)
**Fix:** None needed.
---
## Stale Claims Summary
| Claim | Location | Status |
|-------|----------|--------|
| "parse_connection_id lives in wf_mcp.connections" | research doc | **Stale.** Canonical in `wf_sources_mcp.ids`. |
| "RESERVED_CONNECTION_IDS lives in wf_mcp.shared.names" | research doc | **Stale.** Canonical in `wf_sources_mcp.ids`. |
| "wf_mcp.runtime.factory imports ConnectionConfig at runtime" | research doc | **Stale.** Now a re-export shim. |
| "wf_mcp.sdk.adapter imports ConnectionConfig at runtime" | research doc | **Stale.** Now a re-export shim. |
| "during extraction" in workflow_surface shim docstrings | 5 files | **Stale.** Extraction is complete; shims are long-lived. |
## Items Safe to Fix Now
1. **F1:** Delete `src/wf_mcp/sdk/base.py` (dead re-export module)
2. **F3-F7:** Replace "during extraction" with "until callers migrate" in 5 workflow_surface shim docstrings
3. **F12:** Check and potentially remove `_model_from_schema` alias in `wf_mcp.workflow.wrappers`
4. **F19:** Update `WorkflowSurfaceHandlers` docstring to say "for legacy callers"
## Items to Defer
1. **F10, F11, F21:** Historical research doc is a snapshot; add a note at top
2. **F13:** Remove `CatalogSnapshot` re-export from `wf_mcp.models` when callers migrate
3. **F23:** FastMCP import in `shared/names.py` is part of future `wf_transport_mcp` extraction
4. All completed-plan references in historical docs
## Correctness Issues
None found. All compatibility shims correctly re-export from canonical locations.
The dependency direction is clean: `wf_sources_mcp` imports nothing from `wf_mcp`.
@@ -0,0 +1,271 @@
# Stateful Runtime E2E Gap Audit
**Date:** 2026-06-08
**Auditor:** opencode
**Goal:** Design smallest e2e test proving JSON-RPC/CLI-backed workflow server uses shared `McpRuntimePool` instead of one-shot `McpSdkAdapter` sessions.
---
## 1. Current Call Chain for Tool/Resource/Prompt Operations
### Tool Calls (Workflow Node Execution)
```
JSON-RPC request
→ RpcWorkflowApiClient.call_capability()
→ WorkflowServer.api.call_capability()
→ SourceCatalogService.get_qualified_spec(qualified_name)
→ spec.fn(payload) # NodeSpec function
→ tool_executor_for(connection).call_tool(...)
→ UpstreamTransportService.tool_executor_for()
→ self.tool_executor (McpRuntimePool) or require_adapter()
→ McpRuntimePool.call_tool()
→ get_session() # fingerprint-based reuse
→ PersistentMcpSession.call_tool()
→ _SessionOwner.submit() # owner-task queue
```
**Key files:**
- `src/wf_transport_rpc_http/methods_capabilities.py` - RPC entry point
- `src/wf_mcp/broker/service/workflow_operation_context.py:78-86` - context_from_service
- `src/wf_mcp/broker/service/source_catalog.py:257-298` - spec_from_snapshot_entry (hydrated specs)
- `src/wf_mcp/broker/service/upstream_transport.py:86-95` - tool_executor_for()
- `src/wf_sources_mcp/runtime/pool.py:50-55` - McpRuntimePool.call_tool()
- `src/wf_sources_mcp/runtime/pool.py:36-48` - McpRuntimePool.get_session()
- `src/wf_sources_mcp/runtime/factory.py:82-160` - _SessionOwner (owner-task queue)
### Resource Reads
```
ContentAccessService.read_resource(qualified_name)
→ SourceCatalogService.get_resource(qualified_name)
→ ConnectionService.get(connection_id)
→ UpstreamTransportService.read_resource(connection, qualified_name, uri)
→ self.stateful_runtime.read_resource(...) # preferred
→ adapter.read_resource(...) # fallback
```
**Key files:**
- `src/wf_mcp/broker/service/content_access.py:30-52` - read_resource()
- `src/wf_mcp/broker/service/upstream_transport.py:97-126` - read_resource()
### Prompt Renders
```
ContentAccessService.render_prompt(qualified_name, arguments)
→ SourceCatalogService.get_prompt(qualified_name)
→ ConnectionService.get(connection_id)
→ UpstreamTransportService.render_prompt(connection, qualified_name, local_name, arguments)
→ self.stateful_runtime.get_prompt(...) # preferred
→ adapter.get_prompt(...) # fallback
```
**Key files:**
- `src/wf_mcp/broker/service/content_access.py:54-82` - render_prompt()
- `src/wf_mcp/broker/service/upstream_transport.py:128-163` - render_prompt()
---
## 2. Where Stateful Runtime Is Used vs One-Shot Adapter Fallback
### Stateful Runtime Usage (McpRuntimePool)
| Operation | Where | Condition |
|-----------|-------|-----------|
| Tool calls (workflow nodes) | `source_catalog.py:279` via `tool_executor_for()` | `tool_executor is not None` |
| Resource reads | `upstream_transport.py:117-121` | `stateful_runtime is not None` |
| Prompt renders | `upstream_transport.py:151-157` | `stateful_runtime is not None` |
| Catalog refresh specs | `upstream_transport.py:258` via `tool_executor_for()` | `tool_executor is not None` |
### One-Shot Adapter Fallback (McpSdkAdapter)
| Operation | Where | Condition |
|-----------|-------|-----------|
| Discovery (list_tools/resources/prompts) | `upstream_transport.py:256-258` | Always uses `require_adapter()` |
| Live source checks | `upstream_transport.py:292-298` | Always uses `require_adapter()` |
| Raw method invocation | `upstream_transport.py:165-178` | Always uses `require_adapter()` |
| Notifications | `upstream_transport.py:180-197` | Always uses `require_adapter()` |
| Resource reads (no pool) | `upstream_transport.py:123-124` | `stateful_runtime is None` |
| Prompt renders (no pool) | `upstream_transport.py:159-160` | `stateful_runtime is None` |
### Configuration Wiring
```python
# src/wf_mcp/broker/config.py:182-186
service = WfMcpService(
...
tool_executor=runtime_pool, # McpRuntimePool
stateful_runtime=runtime_pool, # McpRuntimePool (same instance)
)
```
---
## 3. Smallest Reliable Test Design
### Test Objective
Prove that two sequential `call_capability` RPC requests through the JSON-RPC server share the same underlying MCP session, rather than opening a new session per request.
### Test Implementation
```python
# tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py
async def test_rpc_workflow_shares_mcp_runtime_session_across_tool_calls(tmp_path) -> None:
"""Prove JSON-RPC-backed workflow server uses shared McpRuntimePool."""
# 1. Build server from config (wires McpRuntimePool)
config = BrokerConfig(
store_root=tmp_path / "store",
connections=[
ConnectionConfig(
id="fixture.personal",
server="fixture",
account="personal",
metadata={
"transport": "stdio",
"command": sys.executable,
"args": [fixture_server_path()],
},
)
],
)
server = build_workflow_server_from_config(config)
app = create_rpc_app(server)
# 2. Track session creation via pool's internal state
pool = server.context.specs # Access through context
# ... or access via service.upstream.tool_executor
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app),
base_url="http://test",
) as http_client:
client = RpcWorkflowApiClient(
url="http://test/rpc",
http_client=http_client,
)
# 3. First tool call - creates session
first = await client.call_capability(
qualified_name="fixture.personal.echo_tool",
payload={"text": "one"},
)
# 4. Second tool call - should reuse session
second = await client.call_capability(
qualified_name="fixture.personal.echo_tool",
payload={"text": "two"},
)
# 5. Verify same session was used (pool has exactly 1 session)
assert first["outcome"] == "ok"
assert second["outcome"] == "ok"
assert len(pool._sessions) == 1 # Key assertion
```
### Alternative: Session Counter Approach
If pool internals aren't accessible, use a recording factory:
```python
async def test_rpc_server_uses_shared_runtime_pool(tmp_path) -> None:
session_creations = 0
class CountingSessionFactory:
async def create(self, connection, auth):
nonlocal session_creations
session_creations += 1
# ... create real session
# Inject counting factory into pool
# ... then verify session_creations == 1 after multiple tool calls
```
---
## 4. Blockers and Fake/Test Hooks Needed
### Blockers
1. **Pool internals not directly accessible from RPC test**
- `WorkflowServer` wraps `WfMcpService` via `context_from_service()`
- Pool is buried in `service.upstream.tool_executor`
- Need to extract pool reference for assertion
2. **Existing test is close but not JSON-RPC**
- `test_server_reuses_real_upstream_session_across_workflow_requests()` in `tests/wf_mcp/server/test_config.py:121-147` tests via MCP client, not JSON-RPC
- Needs adaptation for RPC transport
### Fake/Test Hooks Needed
1. **Session creation counter** (preferred)
- Add optional `on_session_created` callback to `McpRuntimePool`
- Or expose `_sessions` for test inspection
2. **Recording factory** (alternative)
- Create `RecordingSessionFactory` that counts invocations
- Inject into pool during test setup
3. **Pool reference accessor**
- Add `get_pool()` method to `WfMcpService` or expose via property
- Or access via `service.upstream.tool_executor`
---
## 5. Exact Files/Tests Likely to Change
### Files to Modify
| File | Change |
|------|--------|
| `tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py` | Add new e2e test |
| `src/wf_sources_mcp/runtime/pool.py` | Add optional session counter (minimal) |
### Tests to Add/Modify
| Test | Status | Purpose |
|------|--------|---------|
| `test_rpc_workflow_shares_mcp_runtime_session_across_tool_calls` | **NEW** | Prove JSON-RPC uses shared pool |
| `test_server_reuses_real_upstream_session_across_workflow_requests` | Existing | Already tests MCP client path |
### Files to Inspect (No Changes)
- `src/wf_mcp/broker/config.py:182-186` - Pool wiring (already correct)
- `src/wf_mcp/broker/service/upstream_transport.py:86-95` - tool_executor_for (already routes to pool)
- `src/wf_sources_mcp/runtime/pool.py:36-48` - get_session (fingerprint-based reuse)
- `src/wf_mcp/broker/service/source_catalog.py:279` - Hydrated spec tool call routing
---
## 6. Verification Commands
```bash
# Run existing related tests
uv run pytest tests/wf_mcp/server/test_config.py::test_server_reuses_real_upstream_session_across_workflow_requests -q
uv run pytest tests/wf_mcp/test_stateful_runtime.py -q
uv run pytest tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py -q
# After adding new test
uv run pytest tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py::test_rpc_workflow_shares_mcp_runtime_session_across_tool_calls -q
# Type checking
uv run basedpyright --level error
uv run ruff check
uv run ruff format
```
---
## 7. Summary
**Current State:** The wiring is correct. `McpRuntimePool` is configured as both `tool_executor` and `stateful_runtime` in config-built services. The pool's `get_session()` method reuses sessions based on connection fingerprint.
**Gap:** No JSON-RPC-specific test proves this reuse. Existing tests cover MCP client path and unit-level pool behavior, but not the full RPC transport stack.
**Smallest Fix:** Add one e2e test in `test_mcp_backed_server_rpc.py` that:
1. Builds server from config
2. Makes two `call_capability` RPC requests
3. Asserts pool has exactly 1 session (not 2)
**Estimated Effort:** ~30 lines of test code + optional pool counter hook.
+1 -1
View File
@@ -4,6 +4,7 @@ from collections.abc import Callable
from typing import Any from typing import Any
from wf_authoring import NodeSpec from wf_authoring import NodeSpec
from wf_sources_mcp.auth import AuthRecord
from wf_sources_mcp.catalog import DiscoveredTool from wf_sources_mcp.catalog import DiscoveredTool
from wf_sources_mcp.connections import mcp_source_connection_from_connection_config from wf_sources_mcp.connections import mcp_source_connection_from_connection_config
from wf_sources_mcp.discovery import ( from wf_sources_mcp.discovery import (
@@ -16,7 +17,6 @@ from wf_sources_mcp.discovery import (
from wf_sources_mcp.sdk import ToolExecutor from wf_sources_mcp.sdk import ToolExecutor
from wf_sources_mcp.tool_events import ToolWrapperEvent from wf_sources_mcp.tool_events import ToolWrapperEvent
from ..auth import AuthRecord
from ..models import ConnectionConfig from ..models import ConnectionConfig
from .events import McpEvent, make_event from .events import McpEvent, make_event
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
from typing import Any from typing import Any
from wf_api.source_registry_admin import WorkflowSourceRegistryMutationProvider from wf_api.source_registry_admin import WorkflowSourceRegistryMutationProvider
from wf_sources_mcp.auth import AuthRecord, connection_auth_diagnostic
from wf_sources_mcp.connections import mcp_source_connection_from_connection_config from wf_sources_mcp.connections import mcp_source_connection_from_connection_config
from wf_sources_mcp.source_registry import ( from wf_sources_mcp.source_registry import (
McpSourceRegistryEntry, McpSourceRegistryEntry,
@@ -12,7 +13,6 @@ from wf_sources_mcp.source_registry import (
SourceRegistryStore, SourceRegistryStore,
) )
from ...auth import AuthRecord, connection_auth_diagnostic
from ...models import BrokerConfig, ConnectionConfig from ...models import BrokerConfig, ConnectionConfig
from .connection_service import ConnectionService from .connection_service import ConnectionService
+1 -1
View File
@@ -1,13 +1,13 @@
from __future__ import annotations from __future__ import annotations
from wf_api.models import RawWorkflowPlan from wf_api.models import RawWorkflowPlan
from wf_mcp.auth import AuthRecord
from wf_mcp.broker.models import ( from wf_mcp.broker.models import (
BrokerConfig, BrokerConfig,
BrokerStoreRoots, BrokerStoreRoots,
ConnectionConfig, ConnectionConfig,
SourceConfigOwnership, SourceConfigOwnership,
) )
from wf_sources_mcp.auth import AuthRecord
from wf_sources_mcp.catalog.models import CatalogSnapshot, dump_catalog_snapshot from wf_sources_mcp.catalog.models import CatalogSnapshot, dump_catalog_snapshot
__all__ = [ __all__ = [
+1 -1
View File
@@ -1,7 +1,7 @@
"""Compatibility shim for workflow API constants. """Compatibility shim for workflow API constants.
New code should import these literals from `wf_api.constants`. This module stays New code should import these literals from `wf_api.constants`. This module stays
so older MCP workflow-surface imports keep working during extraction. so older MCP workflow-surface imports keep working until callers migrate.
""" """
from wf_api.constants import ( from wf_api.constants import (
+1 -1
View File
@@ -15,7 +15,7 @@ class WorkflowSurfaceHandlers(WorkflowApi):
New code should construct `WorkflowApi(context_from_service(service))` New code should construct `WorkflowApi(context_from_service(service))`
directly. This shim keeps tests and legacy broker artifact tools working directly. This shim keeps tests and legacy broker artifact tools working
while the MCP surface is migrated. for legacy callers.
""" """
def __init__(self, service: WfMcpService) -> None: def __init__(self, service: WfMcpService) -> None:
+1 -1
View File
@@ -1,7 +1,7 @@
"""Compatibility shim for workflow API next-action guidance. """Compatibility shim for workflow API next-action guidance.
New code should import from `wf_api.next_actions`. This module stays so older New code should import from `wf_api.next_actions`. This module stays so older
MCP workflow-surface imports keep working during extraction. MCP workflow-surface imports keep working until callers migrate.
""" """
from wf_api.next_actions import NextActionPatchExample, NextActions, NextActionTool from wf_api.next_actions import NextActionPatchExample, NextActions, NextActionTool
+1 -1
View File
@@ -1,7 +1,7 @@
"""Compatibility shim for workflow API capability refs. """Compatibility shim for workflow API capability refs.
New code should import from `wf_api.refs`. This module stays so older MCP New code should import from `wf_api.refs`. This module stays so older MCP
workflow-surface imports keep working during extraction. workflow-surface imports keep working until callers migrate.
""" """
from wf_api.refs import ( from wf_api.refs import (
@@ -1,7 +1,7 @@
"""Compatibility shim for workflow runtime dependency resolution. """Compatibility shim for workflow runtime dependency resolution.
New code should import from `wf_api.runtime_dependencies`. This module stays so New code should import from `wf_api.runtime_dependencies`. This module stays so
older MCP workflow-surface imports keep working during extraction. older MCP workflow-surface imports keep working until callers migrate.
""" """
from __future__ import annotations from __future__ import annotations
+1 -1
View File
@@ -1,7 +1,7 @@
"""Compatibility shim for workflow API wrapper authoring hints. """Compatibility shim for workflow API wrapper authoring hints.
New code should import from `wf_api.wrapper_hints`. This module stays so older New code should import from `wf_api.wrapper_hints`. This module stays so older
MCP workflow-surface imports keep working during extraction. MCP workflow-surface imports keep working until callers migrate.
""" """
from wf_api.wrapper_hints import ( from wf_api.wrapper_hints import (
@@ -1,295 +1,181 @@
from __future__ import annotations from __future__ import annotations
import ast import ast
from collections.abc import Iterable
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
# Temporary low-level wf_mcp imports are allowed for connection id parsing, # Exact temporary exceptions to the wf_sources_mcp -> wf_mcp import boundary.
# reserved names, and broker DTO conversion. Catalog DTOs should now be local # Keep this empty unless a compatibility seam truly cannot live on the wf_mcp
# to wf_sources_mcp. Frontend/proxy/workflow-surface imports are forbidden # side. Values must name the removal condition, not just restate the exception.
# because wf_sources_mcp is upstream-source code. ALLOWED_WF_MCP_IMPORTS: dict[tuple[str, str], str] = {}
FORBIDDEN_WF_MCP_PREFIXES = (
WF_SOURCES_MCP_ROOT = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
@dataclass(frozen=True, slots=True)
class ImportViolation:
module: str
line: int
imported: str
statement: str
def format(self) -> str:
return f"{self.module}:{self.line}: {self.statement}"
def _module_name(py_file: Path) -> str:
rel = py_file.relative_to(WF_SOURCES_MCP_ROOT.parent)
return str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
def _is_allowed(module: str, imported: str) -> bool:
return (module, imported) in ALLOWED_WF_MCP_IMPORTS
def _collect_forbidden_imports(
*,
forbidden_exact: Iterable[str] = (),
forbidden_prefixes: Iterable[str] = (),
) -> list[ImportViolation]:
exact = set(forbidden_exact)
prefixes = tuple(forbidden_prefixes)
violations: list[ImportViolation] = []
def is_forbidden(imported: str) -> bool:
return imported in exact or imported.startswith(prefixes)
for py_file in sorted(WF_SOURCES_MCP_ROOT.rglob("*.py")):
module = _module_name(py_file)
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module is not None:
imported = node.module
if is_forbidden(imported) and not _is_allowed(module, imported):
violations.append(
ImportViolation(
module=module,
line=node.lineno,
imported=imported,
statement=f"from {imported} import ...",
)
)
elif isinstance(node, ast.Import):
for alias in node.names:
imported = alias.name
if is_forbidden(imported) and not _is_allowed(module, imported):
violations.append(
ImportViolation(
module=module,
line=node.lineno,
imported=imported,
statement=f"import {imported}",
)
)
return violations
def _assert_no_forbidden_imports(
message: str,
*,
forbidden_exact: Iterable[str] = (),
forbidden_prefixes: Iterable[str] = (),
) -> None:
violations = _collect_forbidden_imports(
forbidden_exact=forbidden_exact,
forbidden_prefixes=forbidden_prefixes,
)
assert violations == [], (
message
+ "\n"
+ "\n".join(f" {violation.format()}" for violation in violations)
)
def test_wf_sources_mcp_does_not_import_frontend_mcp_modules() -> None:
_assert_no_forbidden_imports(
"wf_sources_mcp imports frontend/proxy MCP modules:",
forbidden_prefixes=(
"wf_mcp.admin_surface", "wf_mcp.admin_surface",
"wf_mcp.workflow_surface", "wf_mcp.workflow_surface",
"wf_mcp.server", "wf_mcp.server",
"wf_mcp.proxy", "wf_mcp.proxy",
"wf_mcp.cli", "wf_mcp.cli",
) ),
def test_wf_sources_mcp_does_not_import_frontend_mcp_modules() -> None:
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
violations: list[str] = []
for py_file in sorted(root.rglob("*.py")):
rel = py_file.relative_to(root.parent)
module = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module is not None:
if node.module.startswith(FORBIDDEN_WF_MCP_PREFIXES):
violations.append(
f"{module}:{node.lineno}: from {node.module} import ..."
)
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name.startswith(FORBIDDEN_WF_MCP_PREFIXES):
violations.append(
f"{module}:{node.lineno}: import {alias.name}"
)
assert violations == [], (
"wf_sources_mcp imports frontend/proxy MCP modules:\n"
+ "\n".join(f" {violation}" for violation in violations)
) )
def test_wf_sources_mcp_does_not_import_wf_mcp_catalog_dtos() -> None: def test_wf_sources_mcp_does_not_import_wf_mcp_catalog_dtos() -> None:
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp" _assert_no_forbidden_imports(
forbidden = { "wf_sources_mcp still imports old wf_mcp catalog DTO modules:",
forbidden_exact={
"wf_mcp.broker.catalog", "wf_mcp.broker.catalog",
"wf_mcp.capabilities", "wf_mcp.capabilities",
"wf_mcp.catalog", "wf_mcp.catalog",
"wf_mcp.catalog.models", "wf_mcp.catalog.models",
} },
violations: list[str] = []
for py_file in sorted(root.rglob("*.py")):
rel = py_file.relative_to(root.parent)
module = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
violations.append(
f"{module}:{node.lineno}: from {node.module} import ..."
)
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name in forbidden:
violations.append(
f"{module}:{node.lineno}: import {alias.name}"
)
assert violations == [], (
"wf_sources_mcp still imports old wf_mcp catalog DTO modules:\n"
+ "\n".join(f" {violation}" for violation in violations)
) )
def test_wf_sources_mcp_does_not_import_old_sdk_protocol_modules() -> None: def test_wf_sources_mcp_does_not_import_old_sdk_protocol_modules() -> None:
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp" _assert_no_forbidden_imports(
forbidden = { "wf_sources_mcp still imports old wf_mcp SDK/runtime protocol modules:",
forbidden_exact={
"wf_mcp.sdk", "wf_mcp.sdk",
"wf_mcp.sdk.adapter", "wf_mcp.sdk.adapter",
"wf_mcp.sdk.base", "wf_mcp.sdk.base",
"wf_mcp.runtime", "wf_mcp.runtime",
"wf_mcp.runtime.protocols", "wf_mcp.runtime.protocols",
} },
violations: list[str] = []
for py_file in sorted(root.rglob("*.py")):
rel = py_file.relative_to(root.parent)
module = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
violations.append(
f"{module}:{node.lineno}: from {node.module} import ..."
)
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name in forbidden:
violations.append(
f"{module}:{node.lineno}: import {alias.name}"
)
assert violations == [], (
"wf_sources_mcp still imports old wf_mcp SDK/runtime protocol modules:\n"
+ "\n".join(f" {violation}" for violation in violations)
) )
def test_wf_sources_mcp_does_not_import_old_sdk_converter_module() -> None: def test_wf_sources_mcp_does_not_import_old_sdk_converter_module() -> None:
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp" _assert_no_forbidden_imports(
forbidden = { "wf_sources_mcp still imports old wf_mcp SDK converter module:",
"wf_mcp.sdk.converters", forbidden_exact={"wf_mcp.sdk.converters"},
}
violations: list[str] = []
for py_file in sorted(root.rglob("*.py")):
rel = py_file.relative_to(root.parent)
module = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
violations.append(
f"{module}:{node.lineno}: from {node.module} import ..."
)
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name in forbidden:
violations.append(
f"{module}:{node.lineno}: import {alias.name}"
)
assert violations == [], (
"wf_sources_mcp still imports old wf_mcp SDK converter module:\n"
+ "\n".join(f" {violation}" for violation in violations)
) )
def test_wf_sources_mcp_does_not_import_old_broker_discovery_module() -> None: def test_wf_sources_mcp_does_not_import_old_broker_discovery_module() -> None:
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp" _assert_no_forbidden_imports(
forbidden = {"wf_mcp.broker.discovery"} "wf_sources_mcp still imports old wf_mcp broker discovery module:",
violations: list[str] = [] forbidden_exact={"wf_mcp.broker.discovery"},
for py_file in sorted(root.rglob("*.py")):
rel = py_file.relative_to(root.parent)
module = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
violations.append(
f"{module}:{node.lineno}: from {node.module} import ..."
)
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name in forbidden:
violations.append(
f"{module}:{node.lineno}: import {alias.name}"
)
assert violations == [], (
"wf_sources_mcp still imports old wf_mcp broker discovery module:\n"
+ "\n".join(f" {violation}" for violation in violations)
) )
def test_wf_sources_mcp_does_not_import_old_workflow_wrapper_module() -> None: def test_wf_sources_mcp_does_not_import_old_workflow_wrapper_module() -> None:
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp" _assert_no_forbidden_imports(
forbidden = {"wf_mcp.workflow", "wf_mcp.workflow.wrappers"} "wf_sources_mcp still imports old wf_mcp workflow wrapper module:",
violations: list[str] = [] forbidden_exact={"wf_mcp.workflow", "wf_mcp.workflow.wrappers"},
for py_file in sorted(root.rglob("*.py")):
rel = py_file.relative_to(root.parent)
module = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
violations.append(
f"{module}:{node.lineno}: from {node.module} import ..."
)
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name in forbidden:
violations.append(
f"{module}:{node.lineno}: import {alias.name}"
)
assert violations == [], (
"wf_sources_mcp still imports old wf_mcp workflow wrapper module:\n"
+ "\n".join(f" {violation}" for violation in violations)
) )
def test_wf_sources_mcp_does_not_import_old_broker_event_modules() -> None: def test_wf_sources_mcp_does_not_import_old_broker_event_modules() -> None:
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp" _assert_no_forbidden_imports(
forbidden = {"wf_mcp.events", "wf_mcp.broker.events"} "wf_sources_mcp still imports old wf_mcp broker event modules:",
violations: list[str] = [] forbidden_exact={"wf_mcp.events", "wf_mcp.broker.events"},
for py_file in sorted(root.rglob("*.py")):
rel = py_file.relative_to(root.parent)
module = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
violations.append(
f"{module}:{node.lineno}: from {node.module} import ..."
)
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name in forbidden:
violations.append(
f"{module}:{node.lineno}: import {alias.name}"
)
assert violations == [], (
"wf_sources_mcp still imports old wf_mcp broker event modules:\n"
+ "\n".join(f" {violation}" for violation in violations)
) )
def test_wf_sources_mcp_does_not_import_old_broker_service_adapter_module() -> None: def test_wf_sources_mcp_does_not_import_old_broker_service_adapter_module() -> None:
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp" _assert_no_forbidden_imports(
forbidden = {"wf_mcp.broker.service.adapters"} "wf_sources_mcp still imports old wf_mcp broker service adapter module:",
violations: list[str] = [] forbidden_exact={"wf_mcp.broker.service.adapters"},
for py_file in sorted(root.rglob("*.py")):
rel = py_file.relative_to(root.parent)
module = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
violations.append(
f"{module}:{node.lineno}: from {node.module} import ..."
)
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name in forbidden:
violations.append(
f"{module}:{node.lineno}: import {alias.name}"
)
assert violations == [], (
"wf_sources_mcp still imports old wf_mcp broker service adapter module:\n"
+ "\n".join(f" {violation}" for violation in violations)
) )
def test_wf_sources_mcp_does_not_import_old_wf_mcp_id_modules() -> None: def test_wf_sources_mcp_does_not_import_old_wf_mcp_id_modules() -> None:
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp" _assert_no_forbidden_imports(
forbidden = {"wf_mcp.connections", "wf_mcp.shared.names"} "wf_sources_mcp still imports old wf_mcp source ID modules:",
violations: list[str] = [] forbidden_exact={"wf_mcp.connections", "wf_mcp.shared.names"},
for py_file in sorted(root.rglob("*.py")):
rel = py_file.relative_to(root.parent)
module = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
violations.append(
f"{module}:{node.lineno}: from {node.module} import ..."
)
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name in forbidden:
violations.append(
f"{module}:{node.lineno}: import {alias.name}"
)
assert violations == [], (
"wf_sources_mcp still imports old wf_mcp source ID modules:\n"
+ "\n".join(f" {violation}" for violation in violations)
) )
def test_wf_sources_mcp_does_not_import_wf_mcp_broker_dtos() -> None: def test_wf_sources_mcp_does_not_import_wf_mcp_broker_dtos() -> None:
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp" _assert_no_forbidden_imports(
forbidden = {"wf_mcp.models", "wf_mcp.broker.models"} "wf_sources_mcp still imports wf_mcp broker DTO modules:",
violations: list[str] = [] forbidden_exact={"wf_mcp.models", "wf_mcp.broker.models"},
for py_file in sorted(root.rglob("*.py")):
rel = py_file.relative_to(root.parent)
module = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
violations.append(f"{module}:{node.lineno}: from {node.module} import ...")
elif isinstance(node, ast.Import):
for alias in node.names:
if alias.name in forbidden:
violations.append(f"{module}:{node.lineno}: import {alias.name}")
assert violations == [], (
"wf_sources_mcp still imports wf_mcp broker DTO modules:\n"
+ "\n".join(f" {violation}" for violation in violations)
) )