You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Thread the resolved connection-descriptor (JsonElement) + a production ExecutorTargetRouter into each McpToolContextProvider (constructed in AgentChat). When the bound descriptor is non-local, feed it straight into the router → ITransportFactoryRegistry.ConnectToAsync (no string hop, no intermediate schema), opening an McpClientOverTransport; when local ({"type":"local"}), keep the in-process path (no round-trip). Add the NEW McpChannelClientTransport bridge (M2) and the production RemoteMcpHostHandler (M3), and register the handler on a production McpTransportListener in WorkspacesTransportComposition. This makes ExecutorTargetRouter a production consumer (G8) and provides the arbitrary stdio/HTTP MCP host (G9).
Reuse-first / no new schema. Routing feeds the bound connection-descriptor (the existing type-discriminated JSON: local, user-computer-profile, http, reverse-http, …) directly into the transport factory registry — there is NOExecutorDescriptor and no new execution schema. Nesting is host-OUTER, target-INNER: {"type":"user-computer-profile","entity-id":"<host>","target":{ ...inner... }} — the OUTER descriptor reaches the host, the INNER target is what runs there (the TargetedTransport seam already established by UserComputerProfileTransportFactory.cs:47-106).
Files
Phantom.Workspaces.Llm.Core/McpToolContextProvider.cs, Phantom.Workspaces.Llm.Core/AgentChat.cs, Phantom.Workspaces.Transport.Mcp/McpChannelClientTransport.cs (new — M2), Phantom.Workspaces.Transport.Mcp/RemoteMcpHostHandler.cs (new — M3), Phantom.Workspaces/Services/WorkspacesTransportComposition.cs.
Detailed design
Cohesion seam M2 — McpToolContextProvider consumes a descriptor; add the McpChannelClientTransport bridge
Today: the ctor is McpToolContextProvider(McpTool tool, ILoggerFactory? loggerFactory, ExecutorTarget executorTarget, AgentServices? services) (Phantom.Workspaces.Llm.Core/McpToolContextProvider.cs:26-37). ProvideAIContextAsync (:47-102) connects in-process via McpTransportFactory.CreateMcpTransportAsync(...) (:65) → McpClient.CreateAsync(...) (:71) and ignores the ExecutorTarget property (:43, never consumed). Core gap.
local ({"type":"local"}) → the existing in-process McpTransportFactory.CreateMcpTransportAsync → McpClient.CreateAsync path, UNCHANGED (no round-trip).
remote → ITransportFactoryRegistry.ConnectToAsync(descriptor) returns a transport; build a {"type":"mcp","connection":{...}} request from this.tool; open McpClientOverTransport(transport, mcpConnectionRequest).
NEW class McpChannelClientTransport(required deliverable of this issue). The MCP SDK client (McpClient.CreateAsync) needs an IClientTransport; a routed transport only gives us an IMessageChannel. McpChannelClientTransport is the adapter that bridges the transport IMessageChannel to IClientTransport so McpClient.CreateAsync can run over the routed channel. Without it the remote branch has no way to reach the SDK client.
Namespace:Phantom.Workspaces.Transport.Mcp.
Kind: class implementing ModelContextProtocol.Client.IClientTransport over an IMessageChannel.
Wrapped by McpClientOverTransport; local path does not use it.
RemoteMcpHostHandler(NEW/PROPOSED — M3)
Namespace:Phantom.Workspaces.Transport.Mcp (or Phantom.Workspaces.Services) Kind: class (the Func<JsonElement, IMessageChannel, CancellationToken, Task<IAsyncDisposable?>> registered on McpTransportListener) Responsibility: on the remote host, open an arbitrary stdio/HTTP MCP connection described by the inbound {"type":"mcp","connection":{...}} request and bridge it to the caller's message channel. This is the production openConnectionAsync that G9 says is missing.
Handler steps (a–e):
a. parse the connection request (tool-type-name + tool-entity-id);
b. resolve the MCP tool config on the remote host using the executor-scoped resolver from [per-component-executor] - MCP mcp-server-entity resolution scoped to the bound executor #1439 — McpServerEntityToolResourceFactory(IDataAccessLayer dataAccessLayer, IReadOnlyList<EntityName> searchPrefixes) (Phantom.Workspaces/McpServerEntityToolResourceFactory.cs:21-113) with the remote machine's prefix FIRST (machine profile → ${USER}/mcp-servers → defaults/mcp-servers);
c. launch the in-process MCP transport there (shared McpTransportFactory);
d. bridge it to the incoming IMessageChannel;
e. dispose on channel close (return the tear-down IAsyncDisposable).
Cohesion seam M3 — production McpTransportListener registration in WorkspacesTransportComposition
WorkspacesTransportComposition (ctor Phantom.Workspaces/Services/WorkspacesTransportComposition.cs:37-82) today registers ONLY ChatClientTransportListener on LocalListeners (:54-63). McpTransportListener (Func<JsonElement,IMessageChannel,CT,Task<IAsyncDisposable?>>) exists (Phantom.Workspaces.Transport/Mcp/McpTransportListener.cs:9-27) but new McpTransportListener(...) appears only in tests today. Confirms G9.
This issue adds a PRODUCTIONMcpTransportListener registration on LocalListeners, wired to RemoteMcpHostHandler.OpenAsync, next to the existing ChatClientTransportListener registration.
The remote host handler (step b) CONSUMES #1439's scoped resolver — McpServerEntityToolResourceFactory with the bound executor's machine prefix FIRST. So #1438 depends on #1439's resolver semantics even though #1439 is currently listed as depending on #1438 for the transport plumbing. This is a two-way touchpoint, not a dependency-direction change: the transport/host shell (McpTransportListener registration + RemoteMcpHostHandler + McpChannelClientTransport) lands in #1438; the scoped search-prefix wiring (remote machine prefix first) lands in #1439; they meet at the handler. The dependency edges stay as-is (#1439 depends-on #1438); both issues document this seam.
ExecutorTargetRouter(EXISTING — becomes production consumer)
No shape change; this feature adds its first production new ExecutorTargetRouter(...) in the session build path and threads it into McpToolContextProvider (closing G8).
Modified files (applicable):
Phantom.Workspaces.Llm.Core/McpToolContextProvider.cs — accept a resolved connection-descriptor + router; branch local (in-process) vs. remote (router → McpChannelClientTransport → McpClientOverTransport).
Phantom.Workspaces.Llm.Core/AgentChat.cs — construct each McpToolContextProvider with its bound connection-descriptor (RuntimeContextProviderRegistration.ConnectionDescriptor, AgentChat.cs:2965-2969, constructed :2389-2415) and the production ExecutorTargetRouter.
Phantom.Workspaces/Services/WorkspacesTransportComposition.cs — register the production RemoteMcpHostHandler on LocalListeners via McpTransportListener (:54-63 currently only ChatClientTransportListener).
Verified starting state (evidence):
McpToolContextProvider connects in-process via McpTransportFactory.CreateMcpTransportAsync (McpToolContextProvider.cs:65) → McpClient.CreateAsync (:71). Its ExecutorTarget property (:43) is never consumed. Core gap.
ExecutorTargetRouter maps target → client-instance via ExecutorTopology, builds a descriptor via ExecutionTargetResolver, and connects via ITransportFactoryRegistry (Phantom.Workspaces.Llm.Core/Transport/ExecutorTargetRouter.cs:17-49). It has no production consumer.
UserComputerProfileTransportFactory establishes the host-outer/target-inner recursive-resolve pattern reused here (:47-106). Existing descriptor types: local, user-computer-profile, http, reverse-http.
McpTransportListener accepts {"type":"mcp","connection":{...}} and delegates to a registered openConnectionAsync callback, wrapping the result in McpServerSession (McpTransportListener.cs:9-27). No production openConnectionAsync is registered — WorkspacesTransportComposition registers only a ChatClientTransportListener (WorkspacesTransportComposition.cs:54-63). Confirms G9.
AgentChat tags tools via Core.Transport.ExecutorTargetResolver.ForTool(tool) and constructs each McpToolContextProvider with that target (AgentChat.cs:2403-2415) — but nothing routes them remotely per tag.
Data flow (applicable parts):
Build step 5: AgentChat constructs each McpToolContextProvider with its bound connection-descriptor and the production ExecutorTargetRouter (built from ExecutorBindings.ToTopology() and the ITransportFactoryRegistry). DeferredTrustedExecutorSelector.SetTopology is set from the same topology so CustomTool (gui-local) routing is unchanged.
Runtime step 7: McpToolContextProvider.ProvideAIContextAsync runs lazily. If its bound descriptor is {"type":"local"}, it connects in-process exactly as today. Otherwise it feeds the connection-descriptor into the router → ITransportFactoryRegistry.ConnectToAsync, then opens McpClientOverTransport over the channel via McpChannelClientTransport (M2).
Runtime step 8: On the remote host, the inbound {"type":"mcp","connection":{...}} channel is served by the production RemoteMcpHostHandler registered on McpTransportListener (M3), which resolves the tool config via the [per-component-executor] - MCP mcp-server-entity resolution scoped to the bound executor #1439 scoped resolver (machine prefix first), opens the described stdio/HTTP MCP server locally, and bridges it back.
Runtime step 9: Tool listing / calls flow over the channel; results and tool-errors round-trip.
ProvideAIContext_BoundRemote_BridgesChannelViaMcpChannelClientTransport — (M2) the remote branch reaches the MCP SDK client through McpChannelClientTransport (IClientTransport over IMessageChannel).
Composition_RegistersProductionMcpTransportListener — (M3) the production composition registers an McpTransportListener (wired to RemoteMcpHostHandler) alongside ChatClientTransportListener.
Testing strategy — Transport scenario tests (integration, hermetic): Mirror Scenario2_GuiLocalToolRoutingTests (in-process TransportRegistry machines reached over LocalTransport; routing via ExecutorTargetRouter). A tool bound local connects in-process — assert no transport round-trip. A tool bound to a remote executor routes over transport (bound descriptor fed straight into the factory registry; channel bridged via McpChannelClientTransport). The remote host opens both a stdio and an HTTP MCP connection via the production openConnectionAsync handler, resolving the tool config with the machine prefix first. An end-to-end tool call round-trips a result, and a failing tool call round-trips a tool-error.
Testing strategy — McpToolContextProvider behaviour: Bound-local uses the in-process factory; bound-remote uses the router + McpChannelClientTransport; ExecutorTargetRouter is exercised as a production consumer (closing G8). → McpToolContextProviderRoutingTests.
Testing strategy — Remote MCP host handler:OpenAsync hosts a stdio server and an HTTP server; resolves the tool config with the bound machine prefix first (#1439 touchpoint); an unrecognised connection descriptor returns null (so McpTransportListener declines it); disposes on channel close. → RemoteMcpHostHandlerTests.
Considered / Background
Re-using ChatClientTransportListener (which remotes a whole AgentChat) to host per-tool MCP was rejected — it cannot express "one arbitrary stdio/HTTP MCP server hosted remotely"; the dedicated McpTransportListener + RemoteMcpHostHandler seam is required (G9). Passing the SDK client an in-process transport for the remote branch was likewise impossible without the McpChannelClientTransport adapter, since the router yields only an IMessageChannel (M2).
Dependencies
Depends on: #1435 — PhantomMcpTool.Executor field
Depends on: #1436 — Executor-resource resolver (supplies the bound connection-descriptor + populates RuntimeContextProviderRegistration.ConnectionDescriptor — M1)
Depends on: #1437 — Explicit session executor + executor-bindings persistence + resume
Touchpoint with #1439 — the remote host handler (step b) CONSUMES #1439's McpServerEntityToolResourceFactory scoped resolver (machine prefix first). Documented in both issues; #1439 remains depends-on #1438 for the transport/host shell.
Per-tool MCP execution over transport + production remote MCP host
Part of design: #1432 — per-component-executor-binding
Design document: https://github.com/JoshuaRowePhantom/Phantom.Workspaces/blob/design/docs/design/per-component-executor-binding.md
Scope
Thread the resolved connection-descriptor (
JsonElement) + a productionExecutorTargetRouterinto eachMcpToolContextProvider(constructed inAgentChat). When the bound descriptor is non-local, feed it straight into the router →ITransportFactoryRegistry.ConnectToAsync(no string hop, no intermediate schema), opening anMcpClientOverTransport; when local ({"type":"local"}), keep the in-process path (no round-trip). Add the NEWMcpChannelClientTransportbridge (M2) and the productionRemoteMcpHostHandler(M3), and register the handler on a productionMcpTransportListenerinWorkspacesTransportComposition. This makesExecutorTargetRoutera production consumer (G8) and provides the arbitrary stdio/HTTP MCP host (G9).Reuse-first / no new schema. Routing feeds the bound connection-descriptor (the existing
type-discriminated JSON:local,user-computer-profile,http,reverse-http, …) directly into the transport factory registry — there is NOExecutorDescriptorand no new execution schema. Nesting is host-OUTER, target-INNER:{"type":"user-computer-profile","entity-id":"<host>","target":{ ...inner... }}— the OUTER descriptor reaches the host, the INNERtargetis what runs there (theTargetedTransportseam already established byUserComputerProfileTransportFactory.cs:47-106).Files
Phantom.Workspaces.Llm.Core/McpToolContextProvider.cs,Phantom.Workspaces.Llm.Core/AgentChat.cs,Phantom.Workspaces.Transport.Mcp/McpChannelClientTransport.cs(new — M2),Phantom.Workspaces.Transport.Mcp/RemoteMcpHostHandler.cs(new — M3),Phantom.Workspaces/Services/WorkspacesTransportComposition.cs.Detailed design
Cohesion seam M2 —
McpToolContextProviderconsumes a descriptor; add theMcpChannelClientTransportbridgeToday: the ctor is
McpToolContextProvider(McpTool tool, ILoggerFactory? loggerFactory, ExecutorTarget executorTarget, AgentServices? services)(Phantom.Workspaces.Llm.Core/McpToolContextProvider.cs:26-37).ProvideAIContextAsync(:47-102) connects in-process viaMcpTransportFactory.CreateMcpTransportAsync(...)(:65) →McpClient.CreateAsync(...)(:71) and ignores theExecutorTargetproperty (:43, never consumed). Core gap.Change:
JsonElement boundExecutorand an optionalExecutorTargetRouter router(both additive; default{"type":"local"}+ null preserve today's in-process behaviour). The bound descriptor is the one resolved viaExecutorBindings.ResolveComponent(tool.Executor)in [per-component-executor] - Executor-resource resolver #1436/[per-component-executor] - Explicit session executor +executor-bindingspersistence + resume #1437 (also stored onRuntimeContextProviderRegistration.ConnectionDescriptor— M1, see [per-component-executor] - Executor-resource resolver #1436).ProvideAIContextAsyncbranches on the descriptor:{"type":"local"}) → the existing in-processMcpTransportFactory.CreateMcpTransportAsync→McpClient.CreateAsyncpath, UNCHANGED (no round-trip).ITransportFactoryRegistry.ConnectToAsync(descriptor)returns a transport; build a{"type":"mcp","connection":{...}}request fromthis.tool; openMcpClientOverTransport(transport, mcpConnectionRequest).NEW class
McpChannelClientTransport(required deliverable of this issue). The MCP SDK client (McpClient.CreateAsync) needs anIClientTransport; a routed transport only gives us anIMessageChannel.McpChannelClientTransportis the adapter that bridges the transportIMessageChanneltoIClientTransportsoMcpClient.CreateAsynccan run over the routed channel. Without it the remote branch has no way to reach the SDK client.Phantom.Workspaces.Transport.Mcp.ModelContextProtocol.Client.IClientTransportover anIMessageChannel.McpClientOverTransport; local path does not use it.RemoteMcpHostHandler(NEW/PROPOSED — M3)Namespace:
Phantom.Workspaces.Transport.Mcp(orPhantom.Workspaces.Services)Kind: class (the
Func<JsonElement, IMessageChannel, CancellationToken, Task<IAsyncDisposable?>>registered onMcpTransportListener)Responsibility: on the remote host, open an arbitrary stdio/HTTP MCP connection described by the inbound
{"type":"mcp","connection":{...}}request and bridge it to the caller's message channel. This is the productionopenConnectionAsyncthat G9 says is missing.Handler steps (a–e):
tool-type-name+tool-entity-id);mcp-server-entityresolution scoped to the bound executor #1439 —McpServerEntityToolResourceFactory(IDataAccessLayer dataAccessLayer, IReadOnlyList<EntityName> searchPrefixes)(Phantom.Workspaces/McpServerEntityToolResourceFactory.cs:21-113) with the remote machine's prefix FIRST (machine profile →${USER}/mcp-servers→defaults/mcp-servers);McpTransportFactory);IMessageChannel;IAsyncDisposable).Member:
Task<IAsyncDisposable?> OpenAsync(JsonElement request, IMessageChannel channel, CancellationToken ct).Cohesion seam M3 — production
McpTransportListenerregistration inWorkspacesTransportCompositionWorkspacesTransportComposition(ctorPhantom.Workspaces/Services/WorkspacesTransportComposition.cs:37-82) today registers ONLYChatClientTransportListeneronLocalListeners(:54-63).McpTransportListener(Func<JsonElement,IMessageChannel,CT,Task<IAsyncDisposable?>>) exists (Phantom.Workspaces.Transport/Mcp/McpTransportListener.cs:9-27) butnew McpTransportListener(...)appears only in tests today. Confirms G9.This issue adds a PRODUCTION
McpTransportListenerregistration onLocalListeners, wired toRemoteMcpHostHandler.OpenAsync, next to the existingChatClientTransportListenerregistration.#1438 ↔ #1439 touchpoint (make explicit)
The remote host handler (step b) CONSUMES #1439's scoped resolver —
McpServerEntityToolResourceFactorywith the bound executor's machine prefix FIRST. So #1438 depends on #1439's resolver semantics even though #1439 is currently listed as depending on #1438 for the transport plumbing. This is a two-way touchpoint, not a dependency-direction change: the transport/host shell (McpTransportListenerregistration +RemoteMcpHostHandler+McpChannelClientTransport) lands in #1438; the scoped search-prefix wiring (remote machine prefix first) lands in #1439; they meet at the handler. The dependency edges stay as-is (#1439 depends-on #1438); both issues document this seam.ExecutorTargetRouter(EXISTING — becomes production consumer)No shape change; this feature adds its first production
new ExecutorTargetRouter(...)in the session build path and threads it intoMcpToolContextProvider(closing G8).Modified files (applicable):
Phantom.Workspaces.Llm.Core/McpToolContextProvider.cs— accept a resolved connection-descriptor + router; branch local (in-process) vs. remote (router →McpChannelClientTransport→McpClientOverTransport).Phantom.Workspaces.Llm.Core/AgentChat.cs— construct eachMcpToolContextProviderwith its bound connection-descriptor (RuntimeContextProviderRegistration.ConnectionDescriptor,AgentChat.cs:2965-2969, constructed:2389-2415) and the productionExecutorTargetRouter.Phantom.Workspaces/Services/WorkspacesTransportComposition.cs— register the productionRemoteMcpHostHandleronLocalListenersviaMcpTransportListener(:54-63currently onlyChatClientTransportListener).Verified starting state (evidence):
McpToolContextProviderconnects in-process viaMcpTransportFactory.CreateMcpTransportAsync(McpToolContextProvider.cs:65) →McpClient.CreateAsync(:71). ItsExecutorTargetproperty (:43) is never consumed. Core gap.ExecutorTargetRoutermaps target → client-instance viaExecutorTopology, builds a descriptor viaExecutionTargetResolver, and connects viaITransportFactoryRegistry(Phantom.Workspaces.Llm.Core/Transport/ExecutorTargetRouter.cs:17-49). It has no production consumer.UserComputerProfileTransportFactoryestablishes the host-outer/target-inner recursive-resolve pattern reused here (:47-106). Existing descriptortypes:local,user-computer-profile,http,reverse-http.McpTransportListeneraccepts{"type":"mcp","connection":{...}}and delegates to a registeredopenConnectionAsynccallback, wrapping the result inMcpServerSession(McpTransportListener.cs:9-27). No productionopenConnectionAsyncis registered —WorkspacesTransportCompositionregisters only aChatClientTransportListener(WorkspacesTransportComposition.cs:54-63). Confirms G9.AgentChattags tools viaCore.Transport.ExecutorTargetResolver.ForTool(tool)and constructs eachMcpToolContextProviderwith that target (AgentChat.cs:2403-2415) — but nothing routes them remotely per tag.Data flow (applicable parts):
AgentChatconstructs eachMcpToolContextProviderwith its bound connection-descriptor and the productionExecutorTargetRouter(built fromExecutorBindings.ToTopology()and theITransportFactoryRegistry).DeferredTrustedExecutorSelector.SetTopologyis set from the same topology soCustomTool(gui-local) routing is unchanged.McpToolContextProvider.ProvideAIContextAsyncruns lazily. If its bound descriptor is{"type":"local"}, it connects in-process exactly as today. Otherwise it feeds the connection-descriptor into the router →ITransportFactoryRegistry.ConnectToAsync, then opensMcpClientOverTransportover the channel viaMcpChannelClientTransport(M2).{"type":"mcp","connection":{...}}channel is served by the productionRemoteMcpHostHandlerregistered onMcpTransportListener(M3), which resolves the tool config via the [per-component-executor] - MCPmcp-server-entityresolution scoped to the bound executor #1439 scoped resolver (machine prefix first), opens the described stdio/HTTP MCP server locally, and bridges it back.tool-errors round-trip.Tests
McpToolContextProviderRoutingTests(Phantom.Workspaces.Llm.Core.Tests)ProvideAIContext_BoundLocal_UsesInProcessFactory_NoRoundTripProvideAIContext_BoundRemote_ConnectsViaRouterProvideAIContext_BoundRemote_ExecutorTargetRouterExercisedAsProductionConsumerProvideAIContext_BoundRemote_BridgesChannelViaMcpChannelClientTransport— (M2) the remote branch reaches the MCP SDK client throughMcpChannelClientTransport(IClientTransportoverIMessageChannel).McpChannelClientTransportTests(Phantom.Workspaces.Transport.Tests/Mcp)Send_Receive_PumpsMessagesOverChannel— (M2) the adapter round-trips MCP JSON-RPC frames over an in-processIMessageChannel.Dispose_ClosesUnderlyingChannelRemoteMcpHostHandlerTests(Phantom.Workspaces.Transport.Tests/Mcp)OpenAsync_StdioConnection_HostsServerOpenAsync_HttpConnection_HostsServerOpenAsync_UnknownConnection_ReturnsNullOpenAsync_ResolvesToolConfig_WithMachinePrefixFirst— (M3 / [per-component-executor] - MCPmcp-server-entityresolution scoped to the bound executor #1439 touchpoint) the handler resolves the MCP tool config viaMcpServerEntityToolResourceFactorywith the remote machine's prefix ahead of${USER}anddefaults.OpenAsync_DisposesOnChannelCloseWorkspacesTransportCompositionTests(Phantom.Workspaces.Tests)Composition_RegistersProductionMcpTransportListener— (M3) the production composition registers anMcpTransportListener(wired toRemoteMcpHostHandler) alongsideChatClientTransportListener.Scenario3_PerMcpServerRoutingTests(Phantom.Workspaces.Transport.Tests/Scenarios)Mirrors
Scenario2_GuiLocalToolRoutingTests.Scenario3_LocalBoundMcpServer_ConnectsInProcess_NoTransportRoundTripScenario3_RemoteBoundMcpServer_RoutesOverTransportScenario3_RemoteHost_OpensStdioMcpConnection_ViaProductionHandlerScenario3_RemoteHost_OpensHttpMcpConnection_ViaProductionHandlerScenario3_RemoteBoundMcpServer_ToolCall_RoundTripsResultScenario3_RemoteBoundMcpServer_ToolError_RoundTripsErrorTesting strategy — Transport scenario tests (integration, hermetic): Mirror
Scenario2_GuiLocalToolRoutingTests(in-processTransportRegistrymachines reached overLocalTransport; routing viaExecutorTargetRouter). A tool boundlocalconnects in-process — assert no transport round-trip. A tool bound to a remote executor routes over transport (bound descriptor fed straight into the factory registry; channel bridged viaMcpChannelClientTransport). The remote host opens both a stdio and an HTTP MCP connection via the productionopenConnectionAsynchandler, resolving the tool config with the machine prefix first. An end-to-end tool call round-trips a result, and a failing tool call round-trips atool-error.Testing strategy —
McpToolContextProviderbehaviour: Bound-local uses the in-process factory; bound-remote uses the router +McpChannelClientTransport;ExecutorTargetRouteris exercised as a production consumer (closing G8). →McpToolContextProviderRoutingTests.Testing strategy — Remote MCP host handler:
OpenAsynchosts a stdio server and an HTTP server; resolves the tool config with the bound machine prefix first (#1439 touchpoint); an unrecognised connection descriptor returns null (soMcpTransportListenerdeclines it); disposes on channel close. →RemoteMcpHostHandlerTests.Considered / Background
Re-using
ChatClientTransportListener(which remotes a whole AgentChat) to host per-tool MCP was rejected — it cannot express "one arbitrary stdio/HTTP MCP server hosted remotely"; the dedicatedMcpTransportListener+RemoteMcpHostHandlerseam is required (G9). Passing the SDK client an in-process transport for the remote branch was likewise impossible without theMcpChannelClientTransportadapter, since the router yields only anIMessageChannel(M2).Dependencies
Depends on: #1435 —
PhantomMcpTool.ExecutorfieldDepends on: #1436 — Executor-resource resolver (supplies the bound connection-descriptor + populates
RuntimeContextProviderRegistration.ConnectionDescriptor— M1)Depends on: #1437 — Explicit session executor +
executor-bindingspersistence + resumeTouchpoint with #1439 — the remote host handler (step b) CONSUMES #1439's
McpServerEntityToolResourceFactoryscoped resolver (machine prefix first). Documented in both issues; #1439 remains depends-on #1438 for the transport/host shell.