Files
lda-wf/docs/superpowers/research/2026-06-07-wf-mcp-runtime-source-provider-map.md
T
2026-07-30 01:18:53 +07:00

741 lines
37 KiB
Markdown

# WF MCP Runtime & Source Provider Map
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:
1. **MCP source provider runtime** (transport opening, session pooling, auth) -- belongs in `wf_sources_mcp`
2. **MCP frontend transport** (FastMCP server, proxy mounts, MCP tool registration) -- belongs in a future `wf_transport_mcp`
3. **Broker service layer** (connection catalog, discovery, source catalog, workflow runtime) -- stays in `wf_mcp` or becomes `wf_server`
4. **Compatibility shims** (re-export modules, `WfMcpService` facade) -- remains in `wf_mcp` until callers migrate
The critical blocker is `ConnectionConfig` (defined at `src/wf_mcp/broker/models.py:37`). This dataclass is consumed by `wf_sources_mcp.sdk.protocols`, `wf_sources_mcp.auth`, `wf_sources_mcp.source_registry`, `wf_sources_mcp.storage.store`, the runtime factory, the adapter, the pool, and every broker service. Until `ConnectionConfig` moves to a neutral package or is replaced by a typed protocol, extraction is blocked.
The next safe slice is **Slice 0: Define a `SourceConnection` protocol in `wf_sources_mcp`** so runtime/session code stops depending on the broker DTO. After that, the transport-opening logic in `runtime/factory.py` and `sdk/adapter.py` can merge into `wf_sources_mcp.sdk` without dragging the broker layer along.
---
## Package Responsibility Map
### `wf_sources_mcp` (source provider core)
Owns: MCP source provider identity, auth, catalog entries, storage, and the SDK adapter/executor protocols.
| Module | Responsibility | Status |
|--------|---------------|--------|
| `auth.py` | MCP auth record, env/header extraction, diagnostic helpers | Canonical. Has TYPE_CHECKING dep on `wf_mcp.broker.models.ConnectionConfig`. |
| `sdk/protocols.py` | `BackendAdapter`, `ToolExecutor`, `ToolCallResult` | Canonical. Has TYPE_CHECKING dep on `ConnectionConfig`. |
| `sdk/converters.py` | MCP tool/resource/prompt conversion | Canonical. No `wf_mcp` deps. |
| `catalog/models.py` | `CatalogSnapshot`, `dump_catalog_snapshot` | Canonical. No `wf_mcp` deps. |
| `catalog/entries.py` | `CatalogNodeEntry`, `CatalogResourceEntry`, `CatalogPromptEntry`, `DiscoveredTool`, etc. | Canonical. No `wf_mcp` deps. |
| `source_registry.py` | `McpSourceRegistryEntry`, `SourceRegistryFile`, conversion helpers | Canonical. Imports `parse_connection_id` from `wf_mcp.connections` and `RESERVED_CONNECTION_IDS` from `wf_mcp.shared.names`. TYPE_CHECKING dep on `ConnectionConfig`. |
| `storage/store.py` | `AuthStore`, `CatalogStore`, `FileAuthStore`, `FileCatalogStore` | Canonical. `FileCatalogStore._connection_path` imports `parse_connection_id` from `wf_mcp.connections` at runtime. |
### Future `wf_transport_mcp` (MCP frontend transport)
Owns: FastMCP server creation, proxy mounting, MCP tool/resource/prompt registration, admin tool registration, workflow surface tool registration.
| Module | Responsibility | Status |
|--------|---------------|--------|
| `proxy/runtime.py` | `ProxyRuntime` -- mount upstream MCP connections into FastMCP | `wf_mcp` internal. Depends on `BrokerConfig`, `ConnectionConfig`, `EventBus`, `BrokerConfigManager`. |
| `proxy/mounts.py` | `ProxyMountRegistry`, `create_proxy_mount`, `ResilientFastMCPProxy` | `wf_mcp` internal. Depends on `BrokerConfig`, `ConnectionConfig`, FastMCP. |
| `proxy/tools.py` | Proxy tool listing/filtering | `wf_mcp` internal. |
| `proxy/safe_names.py` | `SafeToolNames` transform | `wf_mcp` internal. FastMCP transform. |
| `proxy/admin.py` | Proxy admin tools | `wf_mcp` internal. |
| `server/core.py` | `create_server`, `run_server`, `create_server_client` | `wf_mcp` entrypoint. Wires broker service + proxy + workflow surface + admin surface. |
| `admin_surface/tools.py` | `register_service_admin_tools` | `wf_mcp` internal. |
| `admin_surface/handlers/*.py` | Admin tool handlers | `wf_mcp` internal. |
| `workflow_surface/tools.py` | `register_workflow_tools` | `wf_mcp` internal. |
| `workflow_surface/*.py` | Workflow tool models, handlers, lifecycle | `wf_mcp` internal. |
| `shared/names.py` | `ProxyNamespace`, `ADMIN_NAMESPACE`, `RESERVED_CONNECTION_IDS`, namespace helpers | `wf_mcp` internal. FastMCP-specific. |
| `cli.py` | CLI entrypoint | `wf_mcp` entrypoint. |
### `wf_server` / `wf_api` (server core)
Owns: Workflow API, operation context, artifact management, deployment, run lifecycle.
Already extracted. These packages consume `wf_sources_mcp` types and `wf_mcp.broker.service.WfMcpService` through `wf_api.WorkflowApi` and `wf_api.WorkflowRuntimeAdapter`.
### Legacy `wf_mcp` (broker + compatibility shims)
Owns: Connection registry, broker config, broker service coordination, events, and all compatibility re-export shims.
| Module | Responsibility | Status |
|--------|---------------|--------|
| `broker/models.py` | `ConnectionConfig`, `BrokerConfig`, `BrokerStoreRoots`, `SourceConfigOwnership` | **The core blocker.** All other packages depend on this. |
| `broker/service/core.py` | `WfMcpService` -- compatibility coordinator | Facade. Delegates to focused services. |
| `broker/service/connection_service.py` | `ConnectionService` | Focused service. Depends on `ConnectionConfig`, `SourceCatalogService`. |
| `broker/service/source_catalog.py` | `SourceCatalogService` | Focused service. Depends on `ConnectionConfig`, `McpEvent`, `NodeSpec`. |
| `broker/service/upstream_transport.py` | `UpstreamTransportService` | Focused service. Depends on `ConnectionConfig`, `BackendAdapter`, `ToolExecutor`. |
| `broker/service/workflow_runtime.py` | `WorkflowRuntimeService` | Focused service. Depends on `SourceCatalogService`. |
| `broker/service/content_access.py` | `ContentAccessService` | Focused service. |
| `broker/service/events.py` | `BrokerEventRecorder` | Focused service. Depends on `McpEvent`. |
| `broker/service/adapters.py` | `require_adapter` | Helper. |
| `broker/discovery.py` | `discover_connection_capabilities`, `specs_from_discovered_tools` | Broker logic. |
| `broker/catalog.py` | `snapshot_from_specs`, `CombinedCatalog` | Broker catalog projection. |
| `broker/config.py` | `build_service_from_config`, `load_broker_config`, `broker_config_from_workflow_config` | Config construction. |
| `connections.py` | `ConnectionRegistry`, `parse_connection_id`, `qualify_node_name` | Broker connection registry. |
| `auth.py` | Re-export shim from `wf_sources_mcp.auth` | Compatibility. |
| `models.py` | Re-export shim aggregating broker models | Compatibility. |
| `source_registry.py` | Re-export shim from `wf_sources_mcp.source_registry` | Compatibility. |
| `capabilities.py` | Re-export shim from `wf_sources_mcp.catalog.entries` | Compatibility. |
| `runtime/protocols.py` | Re-export shim from `wf_sources_mcp.sdk` | Compatibility. |
| `events/bus.py` | `EventBus`, `InMemoryEventSink` | Broker-local event fanout. |
| `events/models.py` | `McpEvent`, `make_event` | Broker event model. |
---
## Current Dependency Blockers
### Blocker 1: `ConnectionConfig` origin
**File:** `src/wf_mcp/broker/models.py:37-44`
```python
@dataclass(slots=True)
class ConnectionConfig:
id: str
server: str
account: str
enabled: bool = True
metadata: dict[str, Any] = field(default_factory=dict)
source_config_ownership: SourceConfigOwnership = "locked"
```
This DTO is imported by:
| Consumer | Import path | Import type |
|----------|------------|-------------|
| `wf_sources_mcp.auth` | `wf_mcp.broker.models.ConnectionConfig` | `TYPE_CHECKING` |
| `wf_sources_mcp.sdk.protocols` | `wf_mcp.broker.models.ConnectionConfig` | `TYPE_CHECKING` |
| `wf_sources_mcp.source_registry` | `wf_mcp.models.ConnectionConfig` | `TYPE_CHECKING` |
| `wf_sources_mcp.storage.store` | (none directly, but `FileCatalogStore._connection_path` calls `parse_connection_id` from `wf_mcp.connections`) | runtime |
| `wf_mcp.runtime.factory` | `wf_mcp.models.ConnectionConfig` | runtime |
| `wf_mcp.sdk.adapter` | `wf_mcp.models.ConnectionConfig` | runtime |
| `wf_mcp.runtime.pool` | `wf_mcp.models.ConnectionConfig` | runtime |
| `wf_mcp.runtime.session` | `wf_mcp.models.ConnectionConfig` | runtime |
| `wf_mcp.workflow.wrappers` | `wf_mcp.models.ConnectionConfig` | runtime |
| `wf_mcp.broker.discovery` | `wf_mcp.models.ConnectionConfig` | runtime |
| `wf_mcp.broker.service.*` | `wf_mcp.models.ConnectionConfig` | runtime |
| `wf_mcp.proxy.mounts` | `wf_mcp.models.ConnectionConfig` | runtime |
| `wf_mcp.proxy.runtime` | (indirectly via `BrokerConfig`) | runtime |
**Impact:** Until `ConnectionConfig` moves to a neutral package (or `wf_sources_mcp` defines its own protocol), the runtime code in `wf_sources_mcp` cannot be independent of `wf_mcp`.
### Blocker 2: `parse_connection_id` origin
**File:** `src/wf_mcp/connections.py:11-25`
Used by:
- `wf_sources_mcp.source_registry` (runtime import, line 32)
- `wf_sources_mcp.storage.store` (runtime import inside `_connection_path`, line 132)
- `wf_mcp.connections` (canonical home)
**Impact:** `wf_sources_mcp` has a runtime import dependency on `wf_mcp` for connection ID validation. This function should move to a neutral package or `wf_sources_mcp`.
### Blocker 3: `RESERVED_CONNECTION_IDS` origin
**File:** `src/wf_mcp/shared/names.py:25`
```python
RESERVED_CONNECTION_IDS = frozenset({ADMIN_NAMESPACE, "wf.mcp"})
```
Used by:
- `wf_sources_mcp.source_registry` (runtime import, line 33)
- `wf_mcp.broker.service.connection_service` (runtime import, line 14)
**Impact:** `wf_sources_mcp` imports from `wf_mcp.shared.names` which transitively imports FastMCP transforms (line 8-15 of `shared/names.py`). This creates an unwanted dependency chain.
### Blocker 4: `McpSdkAdapter` transport-opening duplication
**Files:**
- `src/wf_mcp/runtime/factory.py:43-90` (`_create_with_stack`)
- `src/wf_mcp/sdk/adapter.py:34-75` (`_session`)
Both methods contain nearly identical logic for:
- Reading `connection.metadata["transport"]` to select stdio vs streamable HTTP
- Creating `StdioServerParameters` and calling `stdio_client`
- Creating `httpx.AsyncClient` and calling `streamable_http_client`
- Creating `ClientSession` and calling `session.initialize()`
- Applying auth via `mcp_auth_env` / `mcp_auth_headers`
The difference: factory.py owns the session long-term (persistent actor pattern), while adapter.py opens/closes per call (one-shot pattern).
**Impact:** This duplication means both files must be updated together when transport handling changes. They should share a transport-opening helper.
### Blocker 5: `BrokerConfig` in proxy/runtime
**File:** `src/wf_mcp/proxy/runtime.py:73-85`
`ProxyRuntime.__init__` takes `BrokerConfig` directly and reads `config.connections`. The proxy layer is a frontend transport concern that should not depend on the broker config DTO.
**Impact:** Proxy runtime cannot move to `wf_transport_mcp` until it consumes a transport-neutral config shape.
### Blocker 6: `McpEvent` coupling
**File:** `src/wf_mcp/events/models.py`
`McpEvent` is used by:
- `UpstreamTransportService` (event_sink)
- `SourceCatalogService` (emit_event)
- `BrokerEventRecorder` (event_bus)
- `broker/catalog.py` (indirectly through event callbacks)
- `workflow/wrappers.py` (emit_event callback)
**Impact:** `McpEvent` is broker-specific. If source provider code needs to emit events, it should use a protocol or a neutral event type. Currently `wf_sources_mcp` does not import `McpEvent` directly, which is good.
---
## Detailed Findings by Worker Scope
### Worker 1: Runtime/Session Findings
#### Where is MCP session opening duplicated?
The transport-opening logic appears in two places:
1. **`runtime/factory.py:43-90`** (`PersistentSessionFactory._create_with_stack`):
- Opens stdio or streamable HTTP transport
- Creates `ClientSession`
- Calls `session.initialize()`
- Returns session owned by `AsyncExitStack`
- Used for persistent long-lived sessions (actor pattern)
2. **`sdk/adapter.py:34-75`** (`McpSdkAdapter._session`):
- Opens stdio or streamable HTTP transport
- Creates `ClientSession`
- Calls `session.initialize()`
- Yields session in async context manager
- Used for one-shot per-call sessions (discovery, admin operations)
Both import from the same MCP SDK modules:
- `mcp.client.stdio.StdioServerParameters`, `stdio_client`
- `mcp.client.streamable_http.streamable_http_client`
- `mcp.client.session.ClientSession`
- `wf_sources_mcp.auth.mcp_auth_env`, `mcp_auth_headers`
#### Which client operations are supported by one-shot adapter but not persistent runtime?
The `BackendAdapter` protocol (`wf_sources_mcp.sdk.protocols:26-88`) defines:
- `list_tools`, `list_resources`, `list_prompts`
- `get_connection_metadata`
- `read_resource`, `get_prompt`
- `invoke_method`, `send_notification`
- `call_tool`
The `PersistentMcpSession` (`runtime/session.py:19-49`) only exposes:
- `call_tool`
- `close`
The `McpRuntimePool` (`runtime/pool.py:43-96`) only exposes:
- `get_session` (returns `PersistentMcpSession`)
- `call_tool`
- `close_connection`, `close_all`
**Missing from persistent runtime:** `list_tools`, `list_resources`, `list_prompts`, `get_connection_metadata`, `read_resource`, `get_prompt`, `invoke_method`, `send_notification`.
This is intentional -- persistent sessions exist for workflow execution (tool calls only). Discovery and admin operations use one-shot adapters. However, this means the persistent runtime cannot replace the adapter for all operations.
#### What common `McpClientSession` / `McpClientSessionFactory` interface should exist?
Both factory.py and adapter.py share:
1. Transport selection logic (stdio vs streamable HTTP)
2. Auth application (env vars for stdio, headers for HTTP)
3. Session creation and initialization
A shared `open_mcp_session` helper should:
- Accept a connection descriptor (transport type, command/url, env/headers) and optional auth
- Return an initialized `ClientSession` (or yield it)
- Be used by both `PersistentSessionFactory._create_with_stack` and `McpSdkAdapter._session`
The connection descriptor should NOT be `ConnectionConfig` directly -- it should be a transport-specific DTO that `ConnectionConfig` can convert to.
#### What blocks moving this code to `wf_sources_mcp`?
1. `ConnectionConfig` dependency (Blocker 1)
2. `McpEvent` event callbacks in `PersistentSessionFactory` (the `_SessionOwner._run` method does not emit events, but the pool and factory are used by `broker/config.py` which wires events)
3. The `PersistentMcpSession` dataclass holds `connection: ConnectionConfig` and `auth: AuthRecord` -- the `AuthRecord` is already in `wf_sources_mcp`, but `ConnectionConfig` is not
**Recommendation:** Define a `SourceConnection` protocol in `wf_sources_mcp.sdk.protocols` that captures the transport fields `ConnectionConfig` exposes to runtime code. The factory/pool/session code should consume this protocol, not the concrete DTO.
### Worker 2: Broker/Upstream Findings
#### What is truly MCP-source-provider logic vs broker/catalog projection logic?
**MCP-source-provider logic** (belongs in `wf_sources_mcp`):
- Transport opening (stdio, streamable HTTP) -- currently in `factory.py` and `adapter.py`
- Auth application (env vars, headers) -- already in `wf_sources_mcp.auth`
- `BackendAdapter` protocol and `ToolExecutor` protocol -- already in `wf_sources_mcp.sdk`
- `ToolCallResult` dataclass -- already in `wf_sources_mcp.sdk`
- `DiscoveredTool`, `DiscoveredResource`, `DiscoveredPrompt` -- already in `wf_sources_mcp.catalog`
- `CatalogSnapshot` and catalog entries -- already in `wf_sources_mcp.catalog`
- Auth/catalog storage -- already in `wf_sources_mcp.storage`
- Source registry models -- already in `wf_sources_mcp.source_registry`
**Broker/catalog projection logic** (stays in `wf_mcp`):
- `UpstreamTransportService` -- wraps `BackendAdapter` with auth loading, event recording, catalog refresh orchestration
- `SourceCatalogService` -- manages `CapabilitySource` registrations, catalog hydration, source inventory
- `discover_connection_capabilities` -- orchestrates adapter calls and wraps results
- `specs_from_discovered_tools` -- wraps discovered tools into `NodeSpec` with event emission
- `snapshot_from_specs` -- builds `CatalogSnapshot` from `NodeSpec` dict
- `CombinedCatalog` -- aggregates snapshots across connections
- `ConnectionService` -- connection registry lifecycle
- `BrokerEventRecorder` -- broker event fanout
- `ConnectionConfig`, `BrokerConfig`, `BrokerStoreRoots` -- broker config DTOs
#### What should move to `wf_sources_mcp`?
1. **Transport-opening helper** (`open_mcp_session` or similar) -- extract from `factory.py` and `adapter.py`
2. **`parse_connection_id`** and **`RESERVED_CONNECTION_IDS`** -- move from `wf_mcp.connections` and `wf_mcp.shared.names`
3. **`SourceConnection` protocol** -- new, replacing `ConnectionConfig` in runtime code
#### What should stay in broker compatibility wiring?
1. `UpstreamTransportService` -- it orchestrates broker-specific concerns (events, catalog store, adapter registry)
2. `SourceCatalogService` -- it manages `CapabilitySource` which is `wf_platform`-level
3. `ConnectionService` -- it manages `ConnectionRegistry` which is broker-specific
4. `WfMcpService` -- the compatibility facade
5. All `broker/config.py` construction logic
#### Where are events/catalog/source_catalog dependencies preventing extraction?
- `UpstreamTransportService.refresh_connection_catalog` (lines 203-275) takes `source_catalog: SourceCatalogService` and `record_catalog_change_events` callback. This is broker orchestration, not source provider logic.
- `SourceCatalogService.register_specs` takes `record_catalog_change_events` callback. This is broker event wiring.
- `SourceCatalogService.spec_from_snapshot_entry` (lines 257-298) rebuilds executable `NodeSpec` from stored snapshots, routing calls through `tool_executor_for()`. This is broker hydration, not source provider logic.
None of these prevent `wf_sources_mcp` from owning transport opening -- they just cannot move with it.
### Worker 3: Config/Source/Auth Findings
#### What connection/auth/source DTOs are still too coupled to `wf_mcp`?
1. **`ConnectionConfig`** (`wf_mcp.broker.models:37-44`) -- the primary blocker. Every runtime and broker module depends on it.
2. **`BrokerConfig`** (`wf_mcp.broker.models:47-55`) -- holds `store_root`, `connections`, `store_roots`. Used by proxy/runtime, server/core, broker/config. This is broker-specific and should stay.
3. **`BrokerStoreRoots`** (`wf_mcp.broker.models:11-33`) -- filesystem roots for stores. Broker-specific.
4. **`SourceConfigOwnership`** (`wf_mcp.broker.models:7`) -- `Literal["locked", "seed"]`. Also defined in `wf_config.models:73`. The broker version should be the canonical one since it controls runtime behavior.
5. **`AuthRecord`** (`wf_sources_mcp.auth:22-25`) -- already canonical in `wf_sources_mcp`. The `wf_mcp.auth` module is a re-export shim.
#### What neutral or MCP-source-specific config object should runtime/session code consume instead of `ConnectionConfig.metadata`?
The runtime code (`factory.py`, `adapter.py`) reads these fields from `ConnectionConfig.metadata`:
| Field | Used by | Purpose |
|-------|---------|---------|
| `transport` | `factory.py:49`, `adapter.py:40` | Transport type selector (`"stdio"` or `"streamable_http"`) |
| `command` | `factory.py:56`, `adapter.py:42` | Stdio command |
| `args` | `factory.py:57`, `adapter.py:43` | Stdio arguments |
| `env` | `factory.py:51`, `adapter.py:44` | Stdio environment variables |
| `cwd` | `factory.py:59`, `adapter.py:45` | Stdio working directory |
| `url` | `factory.py:71`, `adapter.py:62` | HTTP transport URL |
A `SourceTransport` union type already exists in `wf_sources_mcp.source_registry:53-69`:
```python
class StdioSourceTransport(SourceRegistryBaseModel):
kind: Literal["stdio"] = "stdio"
command: str
args: tuple[str, ...] = ()
env: dict[str, str] = Field(default_factory=dict)
class HttpSourceTransport(SourceRegistryBaseModel):
kind: Literal["http"] = "http"
url: AnyHttpUrl
headers: dict[str, str] = Field(default_factory=dict)
```
The runtime code should consume a `SourceConnection` protocol that exposes:
- `id: str`
- `transport: StdioSourceTransport | HttpSourceTransport` (or a discriminated union)
- No `metadata: dict[str, Any]` bag
This would eliminate the `connection.metadata.get("transport", "stdio")` pattern scattered across `factory.py` and `adapter.py`.
#### What temporary dependencies remain and how should they be removed?
| Dependency | From | To | How to remove |
|-----------|------|-----|---------------|
| `parse_connection_id` | `wf_sources_mcp.source_registry:32` | `wf_mcp.connections` | Move `parse_connection_id` to `wf_sources_mcp` or a shared `wf_id` package. |
| `RESERVED_CONNECTION_IDS` | `wf_sources_mcp.source_registry:33` | `wf_mcp.shared.names` | Move constant to `wf_sources_mcp.source_registry` or shared package. Remove `wf_mcp.shared.names` import that transitively pulls FastMCP. |
| `ConnectionConfig` TYPE_CHECKING | `wf_sources_mcp.auth:18` | `wf_mcp.broker.models` | Replace with `SourceConnection` protocol. |
| `ConnectionConfig` TYPE_CHECKING | `wf_sources_mcp.sdk.protocols:16` | `wf_mcp.broker.models` | Replace with `SourceConnection` protocol. |
| `ConnectionConfig` TYPE_CHECKING | `wf_sources_mcp.source_registry:36` | `wf_mcp.models` | Replace with `SourceConnection` protocol or keep as converter-only. |
| `parse_connection_id` runtime | `wf_sources_mcp.storage.store:132` | `wf_mcp.connections` | Move function to `wf_sources_mcp`. |
### Worker 4: Frontend/Compat Findings
#### What is MCP frontend transport vs old compatibility facade?
**MCP frontend transport** (FastMCP server + proxy + tool registration):
- `server/core.py` -- creates `FastMCP` server, wires everything
- `proxy/runtime.py` -- `ProxyRuntime` mounts upstream connections as FastMCP proxies
- `proxy/mounts.py` -- `ProxyMountRegistry`, `create_proxy_mount`, `ResilientFastMCPProxy`
- `proxy/tools.py` -- proxy tool listing/filtering
- `proxy/safe_names.py` -- `SafeToolNames` transform for strict clients
- `admin_surface/tools.py` -- registers `wf.admin.*` tools on the server
- `workflow_surface/tools.py` -- registers `wf.workflow.*` tools on the server
- `shared/names.py` -- `ProxyNamespace`, `ADMIN_NAMESPACE`, namespace helpers
**Old compatibility facade** (re-export shims and `WfMcpService`):
- `auth.py` -- re-exports from `wf_sources_mcp.auth`
- `models.py` -- re-exports broker models
- `capabilities.py` -- re-exports from `wf_sources_mcp.catalog.entries`
- `source_registry.py` -- re-exports from `wf_sources_mcp.source_registry`
- `runtime/protocols.py` -- re-exports from `wf_sources_mcp.sdk`
- `broker/service/core.py` -- `WfMcpService` facade
#### What should eventually become `wf_transport_mcp`?
1. `server/core.py` -- `create_server`, `run_server`
2. `proxy/runtime.py` -- `ProxyRuntime`
3. `proxy/mounts.py` -- proxy mount logic
4. `proxy/tools.py` -- proxy tool helpers
5. `proxy/safe_names.py` -- `SafeToolNames`
6. `proxy/admin.py` -- proxy admin tools
7. `admin_surface/tools.py` -- admin tool registration
8. `admin_surface/handlers/*.py` -- admin tool handlers
9. `workflow_surface/tools.py` -- workflow tool registration
10. `workflow_surface/models.py` -- workflow tool models
11. `shared/names.py` -- namespace helpers (minus `RESERVED_CONNECTION_IDS`)
12. `cli.py` -- CLI entrypoint
#### What should remain as legacy `wf_mcp` entrypoints/shims?
1. `auth.py` -- re-export shim (keep until all callers import from `wf_sources_mcp.auth`)
2. `models.py` -- re-export shim (keep until all callers import from `wf_mcp.broker.models` directly)
3. `capabilities.py` -- re-export shim (keep until all callers import from `wf_sources_mcp.catalog`)
4. `source_registry.py` -- re-export shim (keep until all callers import from `wf_sources_mcp.source_registry`)
5. `runtime/protocols.py` -- re-export shim (keep until all callers import from `wf_sources_mcp.sdk`)
6. `runtime/__init__.py` -- re-export `McpRuntimePool`, `PersistentMcpSession`, etc. (keep for backward compat)
#### What should not be touched during upstream-source extraction?
1. `workflow_surface/*` -- workflow tools are MCP-frontend, not source-provider
2. `admin_surface/*` -- admin tools are MCP-frontend, not source-provider
3. `proxy/*` -- proxy mounting is MCP-frontend transport
4. `server/core.py` -- server creation is MCP-frontend
5. `cli.py` -- CLI entrypoint
6. `events/*` -- broker-local event system
7. `broker/service/workflow_runtime.py` -- workflow execution coordination
8. `broker/service/content_access.py` -- resource/prompt access
9. `workflow/wrappers.py` -- NodeSpec wrapping (uses `ToolExecutor` protocol, which is correct)
---
## Recommended Next Slices
Ordered smallest-safe-first.
### Slice 0: Define `SourceConnection` protocol in `wf_sources_mcp`
**Goal:** Break the TYPE_CHECKING dependency from `wf_sources_mcp.sdk.protocols` and `wf_sources_mcp.auth` on `wf_mcp.broker.models.ConnectionConfig` by defining a transport-level protocol that captures what runtime code actually needs.
**Files likely touched:**
- `src/wf_sources_mcp/sdk/protocols.py` -- add `SourceConnection` protocol, update `BackendAdapter` and `ToolExecutor` signatures
- `src/wf_sources_mcp/auth.py` -- update `auth_ref_for_connection` and `connection_auth_diagnostic` to accept protocol
- `src/wf_sources_mcp/source_registry.py` -- update TYPE_CHECKING import
- `src/wf_mcp/broker/models.py` -- make `ConnectionConfig` implement the protocol (no structural change needed, just verify compatibility)
**Tests likely needed:**
- Protocol conformance test: `ConnectionConfig` satisfies `SourceConnection`
- Verify `BackendAdapter` and `ToolExecutor` protocols still typecheck with `ConnectionConfig`
**What must NOT change:**
- `ConnectionConfig` fields/shape
- `BackendAdapter` method signatures (only the type of `connection` parameter changes)
- Any broker service code
- Any proxy/server code
**Migration/shim strategy:**
- `SourceConnection` is a new protocol, not a replacement. `ConnectionConfig` already satisfies it structurally.
- Existing `TYPE_CHECKING` imports in `wf_sources_mcp` become `SourceConnection` protocol imports.
- If any runtime code needs fields beyond the protocol (e.g., `source_config_ownership`), those stay on `ConnectionConfig` and are accessed through a cast or separate parameter.
---
### Slice 1: Move `parse_connection_id` and `RESERVED_CONNECTION_IDS` to `wf_sources_mcp`
**Goal:** Remove the runtime import dependency from `wf_sources_mcp` on `wf_mcp.connections` and `wf_mcp.shared.names`.
**Files likely touched:**
- `src/wf_sources_mcp/source_registry.py` -- replace imports, move validation logic
- `src/wf_sources_mcp/storage/store.py` -- replace `parse_connection_id` import
- `src/wf_mcp/connections.py` -- make `parse_connection_id` a re-export shim
- `src/wf_mcp/shared/names.py` -- make `RESERVED_CONNECTION_IDS` a re-export shim (or keep in `source_registry`)
- `src/wf_mcp/broker/service/connection_service.py` -- update import path
**Tests likely needed:**
- Existing tests for `parse_connection_id` should still pass
- Verify `FileCatalogStore._connection_path` still validates connection IDs
**What must NOT change:**
- Validation logic (same regex, same error messages)
- `RESERVED_CONNECTION_IDS` values
- Any broker service behavior
**Migration/shim strategy:**
- Move `CONNECTION_ID_PATTERN`, `parse_connection_id` to `wf_sources_mcp.source_registry` or a new `wf_sources_mcp.validation` module.
- `wf_mcp.connections` becomes a re-export shim: `from wf_sources_mcp.source_registry import parse_connection_id`
- `RESERVED_CONNECTION_IDS` moves to `wf_sources_mcp.source_registry` (it's already imported there).
- `wf_mcp.shared.names` keeps its own copy or re-exports.
- `wf_mcp.shared.names` can remove the FastMCP import from the top-level module (move `ProxyNamespace` and FastMCP-specific code to a separate submodule if needed).
---
### Slice 2: Extract `open_mcp_session` transport helper to `wf_sources_mcp`
**Goal:** Eliminate the transport-opening duplication between `runtime/factory.py` and `sdk/adapter.py` by extracting a shared helper.
**Files likely touched:**
- New: `src/wf_sources_mcp/sdk/transport.py` -- `open_mcp_session` async context manager
- `src/wf_mcp/runtime/factory.py` -- use `open_mcp_session` in `_create_with_stack`
- `src/wf_mcp/sdk/adapter.py` -- use `open_mcp_session` in `_session`
**Tests likely needed:**
- Unit test for `open_mcp_session` with mock transport
- Verify `PersistentSessionFactory` still creates persistent sessions correctly
- Verify `McpSdkAdapter` still creates one-shot sessions correctly
- Test error handling (unsupported transport, auth failures)
**What must NOT change:**
- `PersistentMcpSession` API
- `McpRuntimePool` behavior
- `McpSdkAdapter` method signatures
- Any broker service code
- Event emission patterns
**Migration/shim strategy:**
- `open_mcp_session` accepts a `SourceConnection` (from Slice 0) and `AuthRecord | None`.
- Returns an async context manager yielding `ClientSession`.
- Both `factory.py` and `adapter.py` call this helper instead of duplicating transport logic.
- The factory wraps it in `AsyncExitStack` for persistent ownership; the adapter uses it directly as a context manager.
---
### Slice 3: Move `PersistentSessionFactory`, `PersistentMcpSession`, `McpRuntimePool` to `wf_sources_mcp`
**Goal:** Move the persistent MCP runtime into the source provider package where it belongs.
**Files likely touched:**
- New: `src/wf_sources_mcp/runtime/__init__.py` -- package init
- New: `src/wf_sources_mcp/runtime/factory.py` -- moved from `wf_mcp/runtime/factory.py`
- New: `src/wf_sources_mcp/runtime/session.py` -- moved from `wf_mcp/runtime/session.py`
- New: `src/wf_sources_mcp/runtime/pool.py` -- moved from `wf_mcp/runtime/pool.py`
- `src/wf_mcp/runtime/__init__.py` -- becomes re-export shim
- `src/wf_mcp/runtime/factory.py` -- becomes re-export shim
- `src/wf_mcp/runtime/session.py` -- becomes re-export shim
- `src/wf_mcp/runtime/pool.py` -- becomes re-export shim
- `src/wf_mcp/broker/config.py` -- update import path
- `src/wf_mcp/broker/service/core.py` -- update import path (if any)
**Tests likely needed:**
- All existing `test_stateful_runtime.py` tests must pass unchanged
- Verify `CrashingSessionFactory` subclass still works
- Verify `McpRuntimePool` fingerprint logic still works
**What must NOT change:**
- `PersistentMcpSession` API (connection, auth, call_tool, close)
- `McpRuntimePool` API (get_session, call_tool, close_connection, close_all)
- `PersistentSessionFactory.create` signature
- `connection_runtime_fingerprint` function
- Any event emission or broker orchestration
**Migration/shim strategy:**
- `wf_mcp.runtime` becomes a re-export shim: `from wf_sources_mcp.runtime import ...`
- `wf_mcp.runtime.protocols.ToolExecutor` already re-exports from `wf_sources_mcp.sdk`
- Tests import from `wf_mcp.runtime` still work via shims
- `broker/config.py` can update to import from `wf_sources_mcp.runtime` directly
---
### Slice 4: Move `McpSdkAdapter` to `wf_sources_mcp.sdk`
**Goal:** Consolidate the one-shot MCP adapter into the source provider SDK.
**Files likely touched:**
- New: `src/wf_sources_mcp/sdk/adapter.py` -- moved from `wf_mcp/sdk/adapter.py`
- `src/wf_mcp/sdk/adapter.py` -- becomes re-export shim
- `src/wf_mcp/sdk/__init__.py` -- update re-exports
- `src/wf_mcp/broker/config.py` -- update import path
- `src/wf_mcp/server/core.py` -- update import path
**Tests likely needed:**
- Existing `test_sdk_adapter.py` tests must pass
- Verify `McpSdkAdapter` still implements `BackendAdapter`
**What must NOT change:**
- `McpSdkAdapter` method signatures
- `BackendAdapter` protocol
- Any broker service code
**Migration/shim strategy:**
- `wf_mcp.sdk.adapter.McpSdkAdapter` re-exports from `wf_sources_mcp.sdk.adapter`
- `wf_mcp.sdk.__init__` keeps exporting `McpSdkAdapter`
---
### Slice 5: Introduce `SourceConnection` dataclass in `wf_sources_mcp` (optional, future)
**Goal:** Replace the `metadata: dict[str, Any]` bag in `ConnectionConfig` with a typed transport DTO for source-provider code.
**Files likely touched:**
- `src/wf_sources_mcp/source_registry.py` -- add `SourceConnection` dataclass
- `src/wf_mcp/runtime/factory.py` -- accept `SourceConnection` instead of `ConnectionConfig`
- `src/wf_mcp/sdk/adapter.py` -- accept `SourceConnection` instead of `ConnectionConfig`
- `src/wf_mcp/runtime/pool.py` -- accept `SourceConnection` in fingerprint
- Conversion helpers in `wf_sources_mcp.source_registry`
**Tests likely needed:**
- Conversion test: `ConnectionConfig` -> `SourceConnection`
- Round-trip test for fingerprint stability
- Verify `McpRuntimePool` fingerprint changes correctly
**What must NOT change:**
- `ConnectionConfig` shape (it's still the broker DTO)
- Broker service behavior
- Proxy/server behavior
**Migration/shim strategy:**
- `SourceConnection` is a new typed DTO in `wf_sources_mcp`.
- `ConnectionConfig` gains a `to_source_connection() -> SourceConnection` method or a standalone converter.
- Runtime code (`factory.py`, `adapter.py`, `pool.py`) accepts `SourceConnection`.
- `McpRuntimePool` fingerprint uses `SourceConnection` fields.
- This slice is optional if Slices 0-4 are sufficient.
---
### Slice 6: Clean up `wf_mcp` re-export shims (final)
**Goal:** Remove all compatibility re-export shims from `wf_mcp` once all callers import from canonical packages.
**Files likely touched:**
- `src/wf_mcp/auth.py` -- delete or leave empty
- `src/wf_mcp/capabilities.py` -- delete or leave empty
- `src/wf_mcp/source_registry.py` -- delete or leave empty
- `src/wf_mcp/runtime/protocols.py` -- delete or leave empty
- `src/wf_mcp/runtime/__init__.py` -- simplify
- `src/wf_mcp/models.py` -- simplify
- `src/wf_mcp/__init__.py` -- simplify
**Tests likely needed:**
- Verify all existing imports still work (or update them)
- `test_compat_imports.py` should pass or be updated
**What must NOT change:**
- Any runtime behavior
- Any broker service behavior
**Migration/shim strategy:**
- This is the final cleanup after all other slices are complete.
- Search for all `from wf_mcp.auth import` etc. and update to canonical paths.
- Leave shims in place for one release cycle, then remove.
---
## Should `runtime/factory.py` Move Now?
**No.** A typed client/session seam must come first.
Reasons:
1. `PersistentSessionFactory._create_with_stack` reads `connection.metadata["transport"]`, `connection.metadata["command"]`, etc. These are `dict[str, Any]` bag accesses that should be replaced by typed protocol access.
2. `PersistentMcpSession` holds `connection: ConnectionConfig` directly. If factory moves to `wf_sources_mcp`, it drags `ConnectionConfig` (and therefore `wf_mcp.broker.models`) into the source provider package at runtime.
3. The `PersistentSessionFactory.create` method returns `PersistentMcpSession` which stores `connection: ConnectionConfig`. Without the protocol, this creates a circular import.
**Correct order:**
1. Slice 0: Define `SourceConnection` protocol
2. Slice 1: Move `parse_connection_id` / `RESERVED_CONNECTION_IDS`
3. Slice 2: Extract `open_mcp_session` helper
4. Slice 3: Move runtime code to `wf_sources_mcp`
5. Slice 4: Move adapter to `wf_sources_mcp.sdk`
6. Slice 5: (Optional) Typed `SourceConnection` dataclass
7. Slice 6: Clean up shims
---
## Test Coverage Summary
Existing tests relevant to the extraction:
| Test file | What it covers | Extraction impact |
|-----------|---------------|-------------------|
| `tests/wf_mcp/test_stateful_runtime.py` | `McpRuntimePool`, `PersistentMcpSession`, `PersistentSessionFactory`, `CrashingSessionFactory` | Must pass unchanged through Slices 0-4 |
| `tests/wf_mcp/test_sdk_adapter.py` | `McpSdkAdapter` one-shot operations | Must pass unchanged through Slices 0-4 |
| `tests/wf_mcp/test_compat_imports.py` | Re-export shim compatibility | Must pass through Slice 6 |
| `tests/wf_mcp/test_workflow_wrappers.py` | `wrap_discovered_tool` with `ToolExecutor` | Must pass unchanged |
| `tests/wf_mcp/test_store.py` | `FileCatalogStore._connection_path` uses `parse_connection_id` | Must pass after Slice 1 |
| `tests/wf_mcp/service/test_connection_service.py` | `ConnectionService` | Unaffected |
| `tests/wf_mcp/service/test_source_registry_admin.py` | Source registry admin tools | Unaffected |
| `tests/wf_mcp/service/test_events.py` | `BrokerEventRecorder` | Unaffected |
| `tests/wf_mcp/service/test_workflow_runtime.py` | `WorkflowRuntimeService` | Unaffected |
---
## Risk Assessment
| Risk | Severity | Mitigation |
|------|----------|------------|
| Circular import if `wf_sources_mcp` imports `ConnectionConfig` at runtime | High | Use TYPE_CHECKING + protocol pattern (Slice 0) |
| Breaking re-export shims during move | Medium | Keep shims in place, update canonical imports first |
| `McpRuntimePool` fingerprint behavioral change | Medium | Preserve exact same fingerprint computation after move |
| `FileCatalogStore` breaking after `parse_connection_id` move | Low | Move function, keep re-export, run `test_store.py` |
| FastMCP transitive import in `shared/names.py` | Low | Move `RESERVED_CONNECTION_IDS` first, then clean `shared/names.py` |