From a531465626c1016ec288629cc77de01f6f83591d Mon Sep 17 00:00:00 2001 From: ajinkyajacob <82514259+ajinkyajacob@users.noreply.github.com> Date: Tue, 10 Nov 2026 23:38:11 +0530 Subject: [PATCH 1/4] fix(acp): send Command Code's MCP servers over HTTP Command Code runs without T3's MCP tools on Windows. T3 passes the stdio bridge as `self.command` (the T3 executable, `T3 Code (Beta).exe`), and Command Code spawns stdio MCP servers through a shell on Windows (`shell: true`), so cmd.exe splits the unquoted path at the space and the bridge exits with code 1: [acp] mcp t3-code (editor): error, MCP server exited with code 1 Command Code does support ACP HTTP MCP servers -- it advertises `mcpCapabilities.http: true` and connected the same endpoint with all 71 tools when offered over HTTP, including the T3 thread/PR tools. Route local Command Code installs through the HTTP transport by detecting the `cmdc`/`command-code` launcher (direct binary or `node .../command-code/ dist/index.mjs`). `mcpOverHttp` stays opt-in per agent because codex-acp and pi-acp drop injected HTTP servers; Copilot keeps its existing opt-in. Co-authored-by: CommandCodeBot --- .../src/server/adapter.ts | 9 ++- .../src/server/mcpTransport.test.ts | 64 +++++++++++++++++++ .../src/server/mcpTransport.ts | 21 ++++++ 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 packages/provider-acp-registry/src/server/mcpTransport.test.ts create mode 100644 packages/provider-acp-registry/src/server/mcpTransport.ts diff --git a/packages/provider-acp-registry/src/server/adapter.ts b/packages/provider-acp-registry/src/server/adapter.ts index 25656a9d808e1..31ef391d19c34 100644 --- a/packages/provider-acp-registry/src/server/adapter.ts +++ b/packages/provider-acp-registry/src/server/adapter.ts @@ -17,6 +17,7 @@ import { isCopilotLostSessionError, makeCopilotSubagentRouting, } from "./copilotAcp.ts"; +import { prefersHttpMcpTransport } from "./mcpTransport.ts"; import { defaultInstanceIdForDriver, ProviderDriverKind } from "@t3tools/contracts"; import { readCustomModelEntries } from "@t3tools/shared/model"; import { AcpRegistrySettings } from "../settings.ts"; @@ -221,6 +222,11 @@ export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(fu } : options; const copilotRouting = isCopilot ? makeCopilotSubagentRouting() : undefined; + // Copilot CLI 1.0.105 rejects stdio MCP servers from the client, and Command Code cannot + // start the stdio bridge on Windows (it spawns MCP commands through cmd.exe, which splits + // the unquoted "T3 Code (Beta).exe" path). Both take T3's MCP endpoint over ACP HTTP. + const useHttpMcpServers = + copilotRouting !== undefined || prefersHttpMcpTransport(options.settings); const flavor: AcpAdapterV2Flavor = { driver: ACP_REGISTRY_PROVIDER, // Cancelling a Copilot prompt also kills the background agents it started. @@ -268,8 +274,6 @@ export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(fu ? {} : { clientCapabilitiesMeta: copilotClientCapabilitiesMeta, - // Copilot CLI 1.0.105 rejects stdio MCP servers from the client. - mcpOverHttp: true, deferMcpTools: true, registerExtensions: copilotRouting.registerExtensions, normalizeSessionUpdate: copilotRouting.normalizeSessionUpdate, @@ -278,6 +282,7 @@ export const makeAcpRegistryAdapterV2 = Effect.fn("makeAcpRegistryAdapterV2")(fu // session/new reports a built-in default, not the model Copilot runs (github/copilot-cli#5363). alwaysSetModel: true, }), + mcpOverHttp: useHttpMcpServers, makeRuntime: options.makeRuntime ?? makeAcpRegistryRuntime(runtimeOptions, catalog), ...(runtimeCoordinator === undefined ? {} diff --git a/packages/provider-acp-registry/src/server/mcpTransport.test.ts b/packages/provider-acp-registry/src/server/mcpTransport.test.ts new file mode 100644 index 0000000000000..ed308c70f4b24 --- /dev/null +++ b/packages/provider-acp-registry/src/server/mcpTransport.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from "@effect/vitest"; +import * as Schema from "effect/Schema"; + +import { AcpRegistrySettings } from "../settings.ts"; +import { prefersHttpMcpTransport } from "./mcpTransport.ts"; + +const decodeSettings = Schema.decodeSync(AcpRegistrySettings); + +describe("prefersHttpMcpTransport", () => { + it("uses HTTP for the vendored Command Code launcher", () => { + expect( + prefersHttpMcpTransport( + decodeSettings({ + source: "local", + commandPath: "C:\\Users\\me\\.t3\\bin\\cmdcode\\node.exe", + commandArgs: [ + "C:\\Users\\me\\.t3\\bin\\cmdcode\\node_modules\\command-code\\dist\\index.mjs", + "acp", + ], + }), + ), + ).toBe(true); + }); + + it("uses HTTP for the global Command Code shim", () => { + expect( + prefersHttpMcpTransport( + decodeSettings({ + source: "local", + commandPath: "C:\\tools\\cmdc.cmd", + commandArgs: ["acp"], + }), + ), + ).toBe(true); + expect( + prefersHttpMcpTransport( + decodeSettings({ source: "local", commandPath: "/usr/local/bin/cmdc" }), + ), + ).toBe(true); + }); + + it("keeps the stdio bridge for other local agents", () => { + expect( + prefersHttpMcpTransport( + decodeSettings({ + source: "local", + commandPath: "C:\\tools\\node.exe", + commandArgs: ["C:\\tools\\some-other-agent\\dist\\index.mjs", "acp"], + }), + ), + ).toBe(false); + expect( + prefersHttpMcpTransport( + decodeSettings({ source: "local", commandPath: "C:\\tools\\claude-agent-acp.exe" }), + ), + ).toBe(false); + }); + + it("keeps the stdio bridge for registry agents", () => { + expect( + prefersHttpMcpTransport(decodeSettings({ source: "registry", agentId: "claude-acp" })), + ).toBe(false); + }); +}); diff --git a/packages/provider-acp-registry/src/server/mcpTransport.ts b/packages/provider-acp-registry/src/server/mcpTransport.ts new file mode 100644 index 0000000000000..d10dd40b5fbf5 --- /dev/null +++ b/packages/provider-acp-registry/src/server/mcpTransport.ts @@ -0,0 +1,21 @@ +import type { AcpRegistrySettings } from "../settings.ts"; + +const COMMAND_CODE_EXECUTABLES = + /^(?:cmdc|command-code|commandcode)(?:\.exe|\.cmd|\.bat|\.ps1)?$/iu; +const COMMAND_CODE_PATH_SEGMENT = /(?:^|[\\/])(?:command-code|cmdcode)(?:[\\/]|$)/iu; + +/** + * Agents that take T3's MCP endpoint over ACP HTTP: Command Code cannot start the stdio + * bridge on Windows, where it spawns MCP commands through cmd.exe and the unquoted T3 + * executable path splits at the space. + */ +export function prefersHttpMcpTransport(settings: AcpRegistrySettings): boolean { + if (settings.source !== "local") { + return false; + } + const executable = settings.commandPath.split(/[\\/]/u).pop() ?? ""; + return ( + COMMAND_CODE_EXECUTABLES.test(executable) || + settings.commandArgs.some((argument) => COMMAND_CODE_PATH_SEGMENT.test(argument)) + ); +} From c8c00319af835f3996e74d9867c52bf24c110a85 Mon Sep 17 00:00:00 2001 From: ajinkyajacob <82514259+ajinkyajacob@users.noreply.github.com> Date: Wed, 11 Nov 2026 00:00:01 +0530 Subject: [PATCH 2/4] test(acp): cover Command Code's HTTP MCP transport end to end Extend the transport test that #26968 added, so it proves the adapter itself sends the HTTP entry for a local Command Code install on session/new and session/load, and keeps the stdio bridge for other local agents even when they advertise HTTP. Renames the test now that it covers more than Copilot. Co-authored-by: CommandCodeBot --- .../Adapters/AcpRegistryAdapterV2.test.ts | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts index 70f45bc8a46c0..346bd760931c2 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts @@ -1259,7 +1259,7 @@ describe("AcpRegistryAdapterV2", () => { }).pipe(Effect.provide(layerTest)), ); - it.effect("gives Copilot T3's MCP server over HTTP when it advertises HTTP", () => + it.effect("gives HTTP-capable agents T3's MCP server over HTTP", () => Effect.gen(function* () { const selfInvocation = yield* resolveSelfInvocation(); const mockAgentPath = yield* (yield* Path.Path).fromFileUrl( @@ -1345,6 +1345,14 @@ describe("AcpRegistryAdapterV2", () => { }; return { new: sent("session/new"), load: sent("session/load") }; }); + const plainHttp = (authorization: string) => [ + { + type: "http", + name: "t3-code", + url: endpoint, + headers: [{ name: "Authorization", value: authorization }], + }, + ]; const http = (authorization: string) => [ { type: "http", @@ -1389,6 +1397,30 @@ describe("AcpRegistryAdapterV2", () => { new: stdio("Bearer opened-token"), load: stdio("Bearer rotated-token"), }); + // Command Code cannot start the stdio bridge on Windows: it spawns MCP commands through + // cmd.exe, which splits the unquoted T3 executable path at the space. + const commandCode = { + source: "local", + commandPath: "C:\\tools\\cmdc.cmd", + commandArgs: ["acp"], + }; + // Command Code is not Copilot: no deferred tool-exposure header. + assert.deepEqual(yield* sessionMcpServers(commandCode, { T3_ACP_MCP_HTTP: "1" }), { + new: plainHttp("Bearer opened-token"), + load: plainHttp("Bearer rotated-token"), + }); + assert.deepEqual(yield* sessionMcpServers(commandCode), { + new: stdio("Bearer opened-token"), + load: stdio("Bearer rotated-token"), + }); + // Any other local agent keeps the stdio bridge. + assert.deepEqual( + yield* sessionMcpServers( + { source: "local", commandPath: "C:\\tools\\node.exe", commandArgs: ["agent.mjs"] }, + { T3_ACP_MCP_HTTP: "1" }, + ), + { new: stdio("Bearer opened-token"), load: stdio("Bearer rotated-token") }, + ); }).pipe(Effect.provide(layerTest)), ); From 058e1fc876fa7a1358e987276c4abe8a41377cac Mon Sep 17 00:00:00 2001 From: ajinkyajacob <82514259+ajinkyajacob@users.noreply.github.com> Date: Wed, 11 Nov 2026 00:16:42 +0530 Subject: [PATCH 3/4] fix(acp): only select HTTP for launchable Command Code installs validateLocalExecutable refuses .cmd/.bat local commands on Windows, so the batch-shim branch of the matcher could never fire. Match native executables only, and narrow the argument match to a command-code entry point under node_modules or dist/, so an unrelated wrapper in a directory named command-code cannot switch transport. Co-authored-by: CommandCodeBot --- .../Adapters/AcpRegistryAdapterV2.test.ts | 2 +- .../src/server/mcpTransport.test.ts | 51 ++++++++----------- .../src/server/mcpTransport.ts | 14 +++-- 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts index 346bd760931c2..db943ff90be4a 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts @@ -1401,7 +1401,7 @@ describe("AcpRegistryAdapterV2", () => { // cmd.exe, which splits the unquoted T3 executable path at the space. const commandCode = { source: "local", - commandPath: "C:\\tools\\cmdc.cmd", + commandPath: "C:\\tools\\cmdc.exe", commandArgs: ["acp"], }; // Command Code is not Copilot: no deferred tool-exposure header. diff --git a/packages/provider-acp-registry/src/server/mcpTransport.test.ts b/packages/provider-acp-registry/src/server/mcpTransport.test.ts index ed308c70f4b24..3de01b35ba9d0 100644 --- a/packages/provider-acp-registry/src/server/mcpTransport.test.ts +++ b/packages/provider-acp-registry/src/server/mcpTransport.test.ts @@ -6,54 +6,43 @@ import { prefersHttpMcpTransport } from "./mcpTransport.ts"; const decodeSettings = Schema.decodeSync(AcpRegistrySettings); +const local = (commandPath: string, commandArgs: string[] = []) => + decodeSettings({ source: "local", commandPath, commandArgs }); + describe("prefersHttpMcpTransport", () => { it("uses HTTP for the vendored Command Code launcher", () => { expect( prefersHttpMcpTransport( - decodeSettings({ - source: "local", - commandPath: "C:\\Users\\me\\.t3\\bin\\cmdcode\\node.exe", - commandArgs: [ - "C:\\Users\\me\\.t3\\bin\\cmdcode\\node_modules\\command-code\\dist\\index.mjs", - "acp", - ], - }), - ), - ).toBe(true); - }); - - it("uses HTTP for the global Command Code shim", () => { - expect( - prefersHttpMcpTransport( - decodeSettings({ - source: "local", - commandPath: "C:\\tools\\cmdc.cmd", - commandArgs: ["acp"], - }), + local("C:\\Users\\me\\.t3\\bin\\cmdcode\\node.exe", [ + "C:\\Users\\me\\.t3\\bin\\cmdcode\\node_modules\\command-code\\dist\\index.mjs", + "acp", + ]), ), ).toBe(true); expect( prefersHttpMcpTransport( - decodeSettings({ source: "local", commandPath: "/usr/local/bin/cmdc" }), + local("C:\\tools\\node.exe", ["C:\\src\\command-code\\dist\\cli.mjs", "acp"]), ), ).toBe(true); }); + it("uses HTTP for the global Command Code executable", () => { + expect(prefersHttpMcpTransport(local("C:\\tools\\cmdc.exe", ["acp"]))).toBe(true); + expect(prefersHttpMcpTransport(local("/usr/local/bin/cmdc"))).toBe(true); + }); + + it("keeps the stdio bridge for a batch shim T3 refuses to launch", () => { + expect(prefersHttpMcpTransport(local("C:\\tools\\cmdc.cmd", ["acp"]))).toBe(false); + }); + it("keeps the stdio bridge for other local agents", () => { expect( prefersHttpMcpTransport( - decodeSettings({ - source: "local", - commandPath: "C:\\tools\\node.exe", - commandArgs: ["C:\\tools\\some-other-agent\\dist\\index.mjs", "acp"], - }), - ), - ).toBe(false); - expect( - prefersHttpMcpTransport( - decodeSettings({ source: "local", commandPath: "C:\\tools\\claude-agent-acp.exe" }), + local("C:\\tools\\node.exe", ["C:\\tools\\some-other-agent\\dist\\index.mjs", "acp"]), ), ).toBe(false); + expect(prefersHttpMcpTransport(local("C:\\tools\\claude-agent-acp.exe"))).toBe(false); + expect(prefersHttpMcpTransport(local("C:\\tools\\command-code\\runner.mjs"))).toBe(false); }); it("keeps the stdio bridge for registry agents", () => { diff --git a/packages/provider-acp-registry/src/server/mcpTransport.ts b/packages/provider-acp-registry/src/server/mcpTransport.ts index d10dd40b5fbf5..49881663a9111 100644 --- a/packages/provider-acp-registry/src/server/mcpTransport.ts +++ b/packages/provider-acp-registry/src/server/mcpTransport.ts @@ -1,8 +1,12 @@ import type { AcpRegistrySettings } from "../settings.ts"; -const COMMAND_CODE_EXECUTABLES = - /^(?:cmdc|command-code|commandcode)(?:\.exe|\.cmd|\.bat|\.ps1)?$/iu; -const COMMAND_CODE_PATH_SEGMENT = /(?:^|[\\/])(?:command-code|cmdcode)(?:[\\/]|$)/iu; +// AcpRegistrySupport.validateLocalExecutable refuses .cmd/.bat on Windows, so only native +// executables are launchable. +const COMMAND_CODE_EXECUTABLES = /^(?:cmdc|command-code|commandcode)(?:\.exe)?$/iu; +const COMMAND_CODE_ENTRY_POINTS = [ + /(?:^|[\\/])node_modules[\\/]command-code[\\/]/iu, + /(?:^|[\\/])command-code[\\/]dist[\\/](?:index|cli)\.mjs$/iu, +]; /** * Agents that take T3's MCP endpoint over ACP HTTP: Command Code cannot start the stdio @@ -16,6 +20,8 @@ export function prefersHttpMcpTransport(settings: AcpRegistrySettings): boolean const executable = settings.commandPath.split(/[\\/]/u).pop() ?? ""; return ( COMMAND_CODE_EXECUTABLES.test(executable) || - settings.commandArgs.some((argument) => COMMAND_CODE_PATH_SEGMENT.test(argument)) + settings.commandArgs.some((argument) => + COMMAND_CODE_ENTRY_POINTS.some((pattern) => pattern.test(argument)), + ) ); } From db150c9d6252b9b6865dfb0af8eba181329220e8 Mon Sep 17 00:00:00 2001 From: ajinkyajacob <82514259+ajinkyajacob@users.noreply.github.com> Date: Wed, 11 Nov 2026 08:43:17 +0530 Subject: [PATCH 4/4] test(acp): resolve Command Code configurations through the real catalog The adapter test mocks the resolver, so it cannot show that the selected transport belongs to a launchable install. Cover the three shapes the selector matches through AcpRegistryCatalog on win32: a bare `cmdc` that resolves to `cmdc.exe` (ready), the same bare name resolving to a `cmdc.cmd` shim (runner_unavailable, so no session starts and the transport never applies), and `node.exe` plus the vendored Command Code entry point (ready). Co-authored-by: CommandCodeBot --- .../src/server/AcpRegistrySupport.test.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts index d9d9d883c2c57..ba9deefaf93cc 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts @@ -23,6 +23,7 @@ import * as ChildProcess from "effect/process/ChildProcess"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; import * as AcpRegistrySupport from "./AcpRegistrySupport.ts"; +import { prefersHttpMcpTransport } from "./mcpTransport.ts"; import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost"; const registryUrl = "https://registry.test/registry.json"; @@ -336,6 +337,54 @@ describe("AcpRegistrySupport", () => { ), ); + it.effect("resolves the local Command Code configurations the HTTP selector matches", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const cacheDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-acp-command-code-", + }); + const catalog = (executable: string) => + AcpRegistrySupport.makeAcpRegistryCatalog({ + cacheDir, + toolsDir: `${cacheDir}/tools`, + registryUrl, + }).pipe( + Effect.provideService(HostProcess.Platform, "win32"), + Effect.provideService(SpawnExecutableResolution, () => executable), + ); + + const bare = decodeAcpRegistrySettings({ source: "local", commandPath: "cmdc" }); + expect(prefersHttpMcpTransport(bare)).toBe(true); + expect( + yield* (yield* catalog("C:\\bin\\cmdc.exe")).resolve(bare, "/workspace"), + ).toMatchObject({ spawn: { command: "C:\\bin\\cmdc.exe", shell: false } }); + // A bare name that PATHEXT resolves to a batch shim never launches, so the transport it + // selects cannot matter: there is no session to attach MCP servers to. + expect( + yield* (yield* catalog("C:\\bin\\cmdc.cmd")).resolve(bare, "/workspace").pipe(Effect.flip), + ).toMatchObject({ reason: "runner_unavailable" }); + + const vendored = decodeAcpRegistrySettings({ + source: "local", + commandPath: "node.exe", + commandArgs: [ + "C:\\Users\\me\\.t3\\bin\\cmdcode\\node_modules\\command-code\\dist\\index.mjs", + "acp", + ], + }); + expect(prefersHttpMcpTransport(vendored)).toBe(true); + expect( + yield* (yield* catalog("C:\\Program Files\\nodejs\\node.exe")).resolve( + vendored, + "/workspace", + ), + ).toMatchObject({ spawn: { command: "C:\\Program Files\\nodejs\\node.exe" } }); + }).pipe( + Effect.scoped, + Effect.provide(layerResolver(() => Effect.die("unexpected registry request"))), + ), + ); + it.effect("reports missing or non-executable local commands without registry fallback", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem;