chore: clean up extraction hygiene docs and guards
This commit is contained in:
@@ -2,6 +2,12 @@
|
||||
|
||||
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
|
||||
|
||||
`wf_mcp` is a monolith containing four distinct responsibilities that should become separate packages:
|
||||
|
||||
+202
@@ -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.
|
||||
@@ -4,6 +4,7 @@ from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from wf_authoring import NodeSpec
|
||||
from wf_sources_mcp.auth import AuthRecord
|
||||
from wf_sources_mcp.catalog import DiscoveredTool
|
||||
from wf_sources_mcp.connections import mcp_source_connection_from_connection_config
|
||||
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.tool_events import ToolWrapperEvent
|
||||
|
||||
from ..auth import AuthRecord
|
||||
from ..models import ConnectionConfig
|
||||
from .events import McpEvent, make_event
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
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.source_registry import (
|
||||
McpSourceRegistryEntry,
|
||||
@@ -12,7 +13,6 @@ from wf_sources_mcp.source_registry import (
|
||||
SourceRegistryStore,
|
||||
)
|
||||
|
||||
from ...auth import AuthRecord, connection_auth_diagnostic
|
||||
from ...models import BrokerConfig, ConnectionConfig
|
||||
from .connection_service import ConnectionService
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from wf_api.models import RawWorkflowPlan
|
||||
from wf_mcp.auth import AuthRecord
|
||||
from wf_mcp.broker.models import (
|
||||
BrokerConfig,
|
||||
BrokerStoreRoots,
|
||||
ConnectionConfig,
|
||||
SourceConfigOwnership,
|
||||
)
|
||||
from wf_sources_mcp.auth import AuthRecord
|
||||
from wf_sources_mcp.catalog.models import CatalogSnapshot, dump_catalog_snapshot
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Compatibility shim for workflow API constants.
|
||||
|
||||
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 (
|
||||
|
||||
@@ -15,7 +15,7 @@ class WorkflowSurfaceHandlers(WorkflowApi):
|
||||
|
||||
New code should construct `WorkflowApi(context_from_service(service))`
|
||||
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:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Compatibility shim for workflow API next-action guidance.
|
||||
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Compatibility shim for workflow API capability refs.
|
||||
|
||||
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 (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Compatibility shim for workflow runtime dependency resolution.
|
||||
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Compatibility shim for workflow API wrapper authoring hints.
|
||||
|
||||
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 (
|
||||
|
||||
@@ -1,295 +1,181 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
# Temporary low-level wf_mcp imports are allowed for connection id parsing,
|
||||
# reserved names, and broker DTO conversion. Catalog DTOs should now be local
|
||||
# to wf_sources_mcp. Frontend/proxy/workflow-surface imports are forbidden
|
||||
# because wf_sources_mcp is upstream-source code.
|
||||
FORBIDDEN_WF_MCP_PREFIXES = (
|
||||
"wf_mcp.admin_surface",
|
||||
"wf_mcp.workflow_surface",
|
||||
"wf_mcp.server",
|
||||
"wf_mcp.proxy",
|
||||
"wf_mcp.cli",
|
||||
)
|
||||
# Exact temporary exceptions to the wf_sources_mcp -> wf_mcp import boundary.
|
||||
# Keep this empty unless a compatibility seam truly cannot live on the wf_mcp
|
||||
# side. Values must name the removal condition, not just restate the exception.
|
||||
ALLOWED_WF_MCP_IMPORTS: dict[tuple[str, str], str] = {}
|
||||
|
||||
WF_SOURCES_MCP_ROOT = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
|
||||
|
||||
|
||||
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] = []
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ImportViolation:
|
||||
module: str
|
||||
line: int
|
||||
imported: str
|
||||
statement: str
|
||||
|
||||
for py_file in sorted(root.rglob("*.py")):
|
||||
rel = py_file.relative_to(root.parent)
|
||||
module = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
|
||||
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:
|
||||
if node.module.startswith(FORBIDDEN_WF_MCP_PREFIXES):
|
||||
imported = node.module
|
||||
if is_forbidden(imported) and not _is_allowed(module, imported):
|
||||
violations.append(
|
||||
f"{module}:{node.lineno}: from {node.module} import ..."
|
||||
ImportViolation(
|
||||
module=module,
|
||||
line=node.lineno,
|
||||
imported=imported,
|
||||
statement=f"from {imported} import ...",
|
||||
)
|
||||
)
|
||||
elif isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if alias.name.startswith(FORBIDDEN_WF_MCP_PREFIXES):
|
||||
imported = alias.name
|
||||
if is_forbidden(imported) and not _is_allowed(module, imported):
|
||||
violations.append(
|
||||
f"{module}:{node.lineno}: import {alias.name}"
|
||||
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 == [], (
|
||||
"wf_sources_mcp imports frontend/proxy MCP modules:\n"
|
||||
+ "\n".join(f" {violation}" for violation in 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.workflow_surface",
|
||||
"wf_mcp.server",
|
||||
"wf_mcp.proxy",
|
||||
"wf_mcp.cli",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_wf_sources_mcp_does_not_import_wf_mcp_catalog_dtos() -> None:
|
||||
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
|
||||
forbidden = {
|
||||
"wf_mcp.broker.catalog",
|
||||
"wf_mcp.capabilities",
|
||||
"wf_mcp.catalog",
|
||||
"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)
|
||||
_assert_no_forbidden_imports(
|
||||
"wf_sources_mcp still imports old wf_mcp catalog DTO modules:",
|
||||
forbidden_exact={
|
||||
"wf_mcp.broker.catalog",
|
||||
"wf_mcp.capabilities",
|
||||
"wf_mcp.catalog",
|
||||
"wf_mcp.catalog.models",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_wf_sources_mcp_does_not_import_old_sdk_protocol_modules() -> None:
|
||||
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
|
||||
forbidden = {
|
||||
"wf_mcp.sdk",
|
||||
"wf_mcp.sdk.adapter",
|
||||
"wf_mcp.sdk.base",
|
||||
"wf_mcp.runtime",
|
||||
"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)
|
||||
_assert_no_forbidden_imports(
|
||||
"wf_sources_mcp still imports old wf_mcp SDK/runtime protocol modules:",
|
||||
forbidden_exact={
|
||||
"wf_mcp.sdk",
|
||||
"wf_mcp.sdk.adapter",
|
||||
"wf_mcp.sdk.base",
|
||||
"wf_mcp.runtime",
|
||||
"wf_mcp.runtime.protocols",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_wf_sources_mcp_does_not_import_old_sdk_converter_module() -> None:
|
||||
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
|
||||
forbidden = {
|
||||
"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)
|
||||
_assert_no_forbidden_imports(
|
||||
"wf_sources_mcp still imports old wf_mcp SDK converter module:",
|
||||
forbidden_exact={"wf_mcp.sdk.converters"},
|
||||
)
|
||||
|
||||
|
||||
def test_wf_sources_mcp_does_not_import_old_broker_discovery_module() -> None:
|
||||
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
|
||||
forbidden = {"wf_mcp.broker.discovery"}
|
||||
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 broker discovery module:\n"
|
||||
+ "\n".join(f" {violation}" for violation in violations)
|
||||
_assert_no_forbidden_imports(
|
||||
"wf_sources_mcp still imports old wf_mcp broker discovery module:",
|
||||
forbidden_exact={"wf_mcp.broker.discovery"},
|
||||
)
|
||||
|
||||
|
||||
def test_wf_sources_mcp_does_not_import_old_workflow_wrapper_module() -> None:
|
||||
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
|
||||
forbidden = {"wf_mcp.workflow", "wf_mcp.workflow.wrappers"}
|
||||
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 workflow wrapper module:\n"
|
||||
+ "\n".join(f" {violation}" for violation in violations)
|
||||
_assert_no_forbidden_imports(
|
||||
"wf_sources_mcp still imports old wf_mcp workflow wrapper module:",
|
||||
forbidden_exact={"wf_mcp.workflow", "wf_mcp.workflow.wrappers"},
|
||||
)
|
||||
|
||||
|
||||
def test_wf_sources_mcp_does_not_import_old_broker_event_modules() -> None:
|
||||
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
|
||||
forbidden = {"wf_mcp.events", "wf_mcp.broker.events"}
|
||||
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 broker event modules:\n"
|
||||
+ "\n".join(f" {violation}" for violation in violations)
|
||||
_assert_no_forbidden_imports(
|
||||
"wf_sources_mcp still imports old wf_mcp broker event modules:",
|
||||
forbidden_exact={"wf_mcp.events", "wf_mcp.broker.events"},
|
||||
)
|
||||
|
||||
|
||||
def test_wf_sources_mcp_does_not_import_old_broker_service_adapter_module() -> None:
|
||||
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
|
||||
forbidden = {"wf_mcp.broker.service.adapters"}
|
||||
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 broker service adapter module:\n"
|
||||
+ "\n".join(f" {violation}" for violation in violations)
|
||||
_assert_no_forbidden_imports(
|
||||
"wf_sources_mcp still imports old wf_mcp broker service adapter module:",
|
||||
forbidden_exact={"wf_mcp.broker.service.adapters"},
|
||||
)
|
||||
|
||||
|
||||
def test_wf_sources_mcp_does_not_import_old_wf_mcp_id_modules() -> None:
|
||||
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
|
||||
forbidden = {"wf_mcp.connections", "wf_mcp.shared.names"}
|
||||
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 source ID modules:\n"
|
||||
+ "\n".join(f" {violation}" for violation in violations)
|
||||
_assert_no_forbidden_imports(
|
||||
"wf_sources_mcp still imports old wf_mcp source ID modules:",
|
||||
forbidden_exact={"wf_mcp.connections", "wf_mcp.shared.names"},
|
||||
)
|
||||
|
||||
|
||||
def test_wf_sources_mcp_does_not_import_wf_mcp_broker_dtos() -> None:
|
||||
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
|
||||
forbidden = {"wf_mcp.models", "wf_mcp.broker.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 wf_mcp broker DTO modules:\n"
|
||||
+ "\n".join(f" {violation}" for violation in violations)
|
||||
_assert_no_forbidden_imports(
|
||||
"wf_sources_mcp still imports wf_mcp broker DTO modules:",
|
||||
forbidden_exact={"wf_mcp.models", "wf_mcp.broker.models"},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user