37 KiB
WF MCP Runtime & Source Provider Map
Date: 2026-06-07
Historical note (2026-06-08): This research snapshot predates the
wf_sources_mcpextraction slices completed on 2026-06-08. Several blockers called out below have since moved: source ID helpers live inwf_sources_mcp.ids, MCP runtime/SDK/client code lives inwf_sources_mcp, and broker DTO construction helpers live inwf_mcp.source_registry.
Executive Summary
wf_mcp is a monolith containing four distinct responsibilities that should become separate packages:
- MCP source provider runtime (transport opening, session pooling, auth) -- belongs in
wf_sources_mcp - MCP frontend transport (FastMCP server, proxy mounts, MCP tool registration) -- belongs in a future
wf_transport_mcp - Broker service layer (connection catalog, discovery, source catalog, workflow runtime) -- stays in
wf_mcpor becomeswf_server - Compatibility shims (re-export modules,
WfMcpServicefacade) -- remains inwf_mcpuntil 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
@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
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
StdioServerParametersand callingstdio_client - Creating
httpx.AsyncClientand callingstreamable_http_client - Creating
ClientSessionand callingsession.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:
-
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)
-
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_clientmcp.client.streamable_http.streamable_http_clientmcp.client.session.ClientSessionwf_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_promptsget_connection_metadataread_resource,get_promptinvoke_method,send_notificationcall_tool
The PersistentMcpSession (runtime/session.py:19-49) only exposes:
call_toolclose
The McpRuntimePool (runtime/pool.py:43-96) only exposes:
get_session(returnsPersistentMcpSession)call_toolclose_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:
- Transport selection logic (stdio vs streamable HTTP)
- Auth application (env vars for stdio, headers for HTTP)
- 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_stackandMcpSdkAdapter._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?
ConnectionConfigdependency (Blocker 1)McpEventevent callbacks inPersistentSessionFactory(the_SessionOwner._runmethod does not emit events, but the pool and factory are used bybroker/config.pywhich wires events)- The
PersistentMcpSessiondataclass holdsconnection: ConnectionConfigandauth: AuthRecord-- theAuthRecordis already inwf_sources_mcp, butConnectionConfigis 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.pyandadapter.py - Auth application (env vars, headers) -- already in
wf_sources_mcp.auth BackendAdapterprotocol andToolExecutorprotocol -- already inwf_sources_mcp.sdkToolCallResultdataclass -- already inwf_sources_mcp.sdkDiscoveredTool,DiscoveredResource,DiscoveredPrompt-- already inwf_sources_mcp.catalogCatalogSnapshotand catalog entries -- already inwf_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-- wrapsBackendAdapterwith auth loading, event recording, catalog refresh orchestrationSourceCatalogService-- managesCapabilitySourceregistrations, catalog hydration, source inventorydiscover_connection_capabilities-- orchestrates adapter calls and wraps resultsspecs_from_discovered_tools-- wraps discovered tools intoNodeSpecwith event emissionsnapshot_from_specs-- buildsCatalogSnapshotfromNodeSpecdictCombinedCatalog-- aggregates snapshots across connectionsConnectionService-- connection registry lifecycleBrokerEventRecorder-- broker event fanoutConnectionConfig,BrokerConfig,BrokerStoreRoots-- broker config DTOs
What should move to wf_sources_mcp?
- Transport-opening helper (
open_mcp_sessionor similar) -- extract fromfactory.pyandadapter.py parse_connection_idandRESERVED_CONNECTION_IDS-- move fromwf_mcp.connectionsandwf_mcp.shared.namesSourceConnectionprotocol -- new, replacingConnectionConfigin runtime code
What should stay in broker compatibility wiring?
UpstreamTransportService-- it orchestrates broker-specific concerns (events, catalog store, adapter registry)SourceCatalogService-- it managesCapabilitySourcewhich iswf_platform-levelConnectionService-- it managesConnectionRegistrywhich is broker-specificWfMcpService-- the compatibility facade- All
broker/config.pyconstruction logic
Where are events/catalog/source_catalog dependencies preventing extraction?
UpstreamTransportService.refresh_connection_catalog(lines 203-275) takessource_catalog: SourceCatalogServiceandrecord_catalog_change_eventscallback. This is broker orchestration, not source provider logic.SourceCatalogService.register_specstakesrecord_catalog_change_eventscallback. This is broker event wiring.SourceCatalogService.spec_from_snapshot_entry(lines 257-298) rebuilds executableNodeSpecfrom stored snapshots, routing calls throughtool_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?
-
ConnectionConfig(wf_mcp.broker.models:37-44) -- the primary blocker. Every runtime and broker module depends on it. -
BrokerConfig(wf_mcp.broker.models:47-55) -- holdsstore_root,connections,store_roots. Used by proxy/runtime, server/core, broker/config. This is broker-specific and should stay. -
BrokerStoreRoots(wf_mcp.broker.models:11-33) -- filesystem roots for stores. Broker-specific. -
SourceConfigOwnership(wf_mcp.broker.models:7) --Literal["locked", "seed"]. Also defined inwf_config.models:73. The broker version should be the canonical one since it controls runtime behavior. -
AuthRecord(wf_sources_mcp.auth:22-25) -- already canonical inwf_sources_mcp. Thewf_mcp.authmodule 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:
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: strtransport: 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-- createsFastMCPserver, wires everythingproxy/runtime.py--ProxyRuntimemounts upstream connections as FastMCP proxiesproxy/mounts.py--ProxyMountRegistry,create_proxy_mount,ResilientFastMCPProxyproxy/tools.py-- proxy tool listing/filteringproxy/safe_names.py--SafeToolNamestransform for strict clientsadmin_surface/tools.py-- registerswf.admin.*tools on the serverworkflow_surface/tools.py-- registerswf.workflow.*tools on the servershared/names.py--ProxyNamespace,ADMIN_NAMESPACE, namespace helpers
Old compatibility facade (re-export shims and WfMcpService):
auth.py-- re-exports fromwf_sources_mcp.authmodels.py-- re-exports broker modelscapabilities.py-- re-exports fromwf_sources_mcp.catalog.entriessource_registry.py-- re-exports fromwf_sources_mcp.source_registryruntime/protocols.py-- re-exports fromwf_sources_mcp.sdkbroker/service/core.py--WfMcpServicefacade
What should eventually become wf_transport_mcp?
server/core.py--create_server,run_serverproxy/runtime.py--ProxyRuntimeproxy/mounts.py-- proxy mount logicproxy/tools.py-- proxy tool helpersproxy/safe_names.py--SafeToolNamesproxy/admin.py-- proxy admin toolsadmin_surface/tools.py-- admin tool registrationadmin_surface/handlers/*.py-- admin tool handlersworkflow_surface/tools.py-- workflow tool registrationworkflow_surface/models.py-- workflow tool modelsshared/names.py-- namespace helpers (minusRESERVED_CONNECTION_IDS)cli.py-- CLI entrypoint
What should remain as legacy wf_mcp entrypoints/shims?
auth.py-- re-export shim (keep until all callers import fromwf_sources_mcp.auth)models.py-- re-export shim (keep until all callers import fromwf_mcp.broker.modelsdirectly)capabilities.py-- re-export shim (keep until all callers import fromwf_sources_mcp.catalog)source_registry.py-- re-export shim (keep until all callers import fromwf_sources_mcp.source_registry)runtime/protocols.py-- re-export shim (keep until all callers import fromwf_sources_mcp.sdk)runtime/__init__.py-- re-exportMcpRuntimePool,PersistentMcpSession, etc. (keep for backward compat)
What should not be touched during upstream-source extraction?
workflow_surface/*-- workflow tools are MCP-frontend, not source-provideradmin_surface/*-- admin tools are MCP-frontend, not source-providerproxy/*-- proxy mounting is MCP-frontend transportserver/core.py-- server creation is MCP-frontendcli.py-- CLI entrypointevents/*-- broker-local event systembroker/service/workflow_runtime.py-- workflow execution coordinationbroker/service/content_access.py-- resource/prompt accessworkflow/wrappers.py-- NodeSpec wrapping (usesToolExecutorprotocol, 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-- addSourceConnectionprotocol, updateBackendAdapterandToolExecutorsignaturessrc/wf_sources_mcp/auth.py-- updateauth_ref_for_connectionandconnection_auth_diagnosticto accept protocolsrc/wf_sources_mcp/source_registry.py-- update TYPE_CHECKING importsrc/wf_mcp/broker/models.py-- makeConnectionConfigimplement the protocol (no structural change needed, just verify compatibility)
Tests likely needed:
- Protocol conformance test:
ConnectionConfigsatisfiesSourceConnection - Verify
BackendAdapterandToolExecutorprotocols still typecheck withConnectionConfig
What must NOT change:
ConnectionConfigfields/shapeBackendAdaptermethod signatures (only the type ofconnectionparameter changes)- Any broker service code
- Any proxy/server code
Migration/shim strategy:
SourceConnectionis a new protocol, not a replacement.ConnectionConfigalready satisfies it structurally.- Existing
TYPE_CHECKINGimports inwf_sources_mcpbecomeSourceConnectionprotocol imports. - If any runtime code needs fields beyond the protocol (e.g.,
source_config_ownership), those stay onConnectionConfigand 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 logicsrc/wf_sources_mcp/storage/store.py-- replaceparse_connection_idimportsrc/wf_mcp/connections.py-- makeparse_connection_ida re-export shimsrc/wf_mcp/shared/names.py-- makeRESERVED_CONNECTION_IDSa re-export shim (or keep insource_registry)src/wf_mcp/broker/service/connection_service.py-- update import path
Tests likely needed:
- Existing tests for
parse_connection_idshould still pass - Verify
FileCatalogStore._connection_pathstill validates connection IDs
What must NOT change:
- Validation logic (same regex, same error messages)
RESERVED_CONNECTION_IDSvalues- Any broker service behavior
Migration/shim strategy:
- Move
CONNECTION_ID_PATTERN,parse_connection_idtowf_sources_mcp.source_registryor a newwf_sources_mcp.validationmodule. wf_mcp.connectionsbecomes a re-export shim:from wf_sources_mcp.source_registry import parse_connection_idRESERVED_CONNECTION_IDSmoves towf_sources_mcp.source_registry(it's already imported there).wf_mcp.shared.nameskeeps its own copy or re-exports.wf_mcp.shared.namescan remove the FastMCP import from the top-level module (moveProxyNamespaceand 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_sessionasync context manager src/wf_mcp/runtime/factory.py-- useopen_mcp_sessionin_create_with_stacksrc/wf_mcp/sdk/adapter.py-- useopen_mcp_sessionin_session
Tests likely needed:
- Unit test for
open_mcp_sessionwith mock transport - Verify
PersistentSessionFactorystill creates persistent sessions correctly - Verify
McpSdkAdapterstill creates one-shot sessions correctly - Test error handling (unsupported transport, auth failures)
What must NOT change:
PersistentMcpSessionAPIMcpRuntimePoolbehaviorMcpSdkAdaptermethod signatures- Any broker service code
- Event emission patterns
Migration/shim strategy:
open_mcp_sessionaccepts aSourceConnection(from Slice 0) andAuthRecord | None.- Returns an async context manager yielding
ClientSession. - Both
factory.pyandadapter.pycall this helper instead of duplicating transport logic. - The factory wraps it in
AsyncExitStackfor 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 fromwf_mcp/runtime/factory.py - New:
src/wf_sources_mcp/runtime/session.py-- moved fromwf_mcp/runtime/session.py - New:
src/wf_sources_mcp/runtime/pool.py-- moved fromwf_mcp/runtime/pool.py src/wf_mcp/runtime/__init__.py-- becomes re-export shimsrc/wf_mcp/runtime/factory.py-- becomes re-export shimsrc/wf_mcp/runtime/session.py-- becomes re-export shimsrc/wf_mcp/runtime/pool.py-- becomes re-export shimsrc/wf_mcp/broker/config.py-- update import pathsrc/wf_mcp/broker/service/core.py-- update import path (if any)
Tests likely needed:
- All existing
test_stateful_runtime.pytests must pass unchanged - Verify
CrashingSessionFactorysubclass still works - Verify
McpRuntimePoolfingerprint logic still works
What must NOT change:
PersistentMcpSessionAPI (connection, auth, call_tool, close)McpRuntimePoolAPI (get_session, call_tool, close_connection, close_all)PersistentSessionFactory.createsignatureconnection_runtime_fingerprintfunction- Any event emission or broker orchestration
Migration/shim strategy:
wf_mcp.runtimebecomes a re-export shim:from wf_sources_mcp.runtime import ...wf_mcp.runtime.protocols.ToolExecutoralready re-exports fromwf_sources_mcp.sdk- Tests import from
wf_mcp.runtimestill work via shims broker/config.pycan update to import fromwf_sources_mcp.runtimedirectly
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 fromwf_mcp/sdk/adapter.py src/wf_mcp/sdk/adapter.py-- becomes re-export shimsrc/wf_mcp/sdk/__init__.py-- update re-exportssrc/wf_mcp/broker/config.py-- update import pathsrc/wf_mcp/server/core.py-- update import path
Tests likely needed:
- Existing
test_sdk_adapter.pytests must pass - Verify
McpSdkAdapterstill implementsBackendAdapter
What must NOT change:
McpSdkAdaptermethod signaturesBackendAdapterprotocol- Any broker service code
Migration/shim strategy:
wf_mcp.sdk.adapter.McpSdkAdapterre-exports fromwf_sources_mcp.sdk.adapterwf_mcp.sdk.__init__keeps exportingMcpSdkAdapter
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-- addSourceConnectiondataclasssrc/wf_mcp/runtime/factory.py-- acceptSourceConnectioninstead ofConnectionConfigsrc/wf_mcp/sdk/adapter.py-- acceptSourceConnectioninstead ofConnectionConfigsrc/wf_mcp/runtime/pool.py-- acceptSourceConnectionin fingerprint- Conversion helpers in
wf_sources_mcp.source_registry
Tests likely needed:
- Conversion test:
ConnectionConfig->SourceConnection - Round-trip test for fingerprint stability
- Verify
McpRuntimePoolfingerprint changes correctly
What must NOT change:
ConnectionConfigshape (it's still the broker DTO)- Broker service behavior
- Proxy/server behavior
Migration/shim strategy:
SourceConnectionis a new typed DTO inwf_sources_mcp.ConnectionConfiggains ato_source_connection() -> SourceConnectionmethod or a standalone converter.- Runtime code (
factory.py,adapter.py,pool.py) acceptsSourceConnection. McpRuntimePoolfingerprint usesSourceConnectionfields.- 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 emptysrc/wf_mcp/capabilities.py-- delete or leave emptysrc/wf_mcp/source_registry.py-- delete or leave emptysrc/wf_mcp/runtime/protocols.py-- delete or leave emptysrc/wf_mcp/runtime/__init__.py-- simplifysrc/wf_mcp/models.py-- simplifysrc/wf_mcp/__init__.py-- simplify
Tests likely needed:
- Verify all existing imports still work (or update them)
test_compat_imports.pyshould 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 importetc. 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:
PersistentSessionFactory._create_with_stackreadsconnection.metadata["transport"],connection.metadata["command"], etc. These aredict[str, Any]bag accesses that should be replaced by typed protocol access.PersistentMcpSessionholdsconnection: ConnectionConfigdirectly. If factory moves towf_sources_mcp, it dragsConnectionConfig(and thereforewf_mcp.broker.models) into the source provider package at runtime.- The
PersistentSessionFactory.createmethod returnsPersistentMcpSessionwhich storesconnection: ConnectionConfig. Without the protocol, this creates a circular import.
Correct order:
- Slice 0: Define
SourceConnectionprotocol - Slice 1: Move
parse_connection_id/RESERVED_CONNECTION_IDS - Slice 2: Extract
open_mcp_sessionhelper - Slice 3: Move runtime code to
wf_sources_mcp - Slice 4: Move adapter to
wf_sources_mcp.sdk - Slice 5: (Optional) Typed
SourceConnectiondataclass - 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 |