Skip to content
Source
Sign inSign up

[Bug]: ACP v2 sessions receive stdio MCP servers without advertising stdio support #35830

Send to my agent
Opencoygeek(GitHub profile, opens in a new tab) opened this issue 0 comments

Summary

An ACP v2 agent that does not advertise stdio MCP support can fail to start a thread because T3 still sends it a stdio server declaration. T3 should choose a transport the agent advertised and omit an unsupported declaration. A deterministic fake-peer run reproduced failed session creation, while the same peer advertising stdio support started successfully.

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.
  • Removed credentials, tokens, private paths, and sensitive data.

Area

apps/server

Steps to reproduce

  1. Use a clean checkout of the stated commit with its locked dependencies installed.
  2. Run the script below against that checkout. It calls the production runtime and creates a local newline-delimited JSON-RPC peer.
  3. The control advertises ACP v2 stdio support and accepts session/new; the failing input omits that capability and rejects a supplied stdio declaration.

The defect reproduced in one recorded run; the nearby advertised-stdio control succeeded in that same run.

Save this as repro-mcp.mjs outside the checkout and run node repro-mcp.mjs /path/to/t3code. It requires Node with TypeScript stripping support and Git. It prints their versions and the target revision, creates its peer and isolated HOME, TMPDIR, T3CODE_HOME and working directory under a temporary directory, removes them on exit, and leaves the checkout unchanged. Exit 0 means the unsupported declaration is absent and setup succeeds, 1 means the defect reproduced, and 2 means the setup or control failed.

Reproduction script: repro-mcp.mjs
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { resolve, join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { createRequire } from 'node:module';
import { execFileSync } from 'node:child_process';

const repo = resolve(process.argv[2]);
const scratch = mkdtempSync(join(tmpdir(), 'acp-mcp-repro-'));
let code = 2;
try {
  for (const dir of ['home', 'tmp', 'state', 'cwd']) mkdirSync(join(scratch, dir));
  Object.assign(process.env, { HOME: join(scratch, 'home'), TMPDIR: join(scratch, 'tmp'), T3CODE_HOME: join(scratch, 'state') });
  process.chdir(join(scratch, 'cwd'));
  const require = createRequire(join(repo, 'apps/server/package.json'));
  const load = (name) => import(pathToFileURL(require.resolve(name)).href);
  const Effect = await load('effect/Effect');
  const Exit = await load('effect/Exit');
  const Layer = await load('effect/Layer');
  const NodeServices = await load('@effect/platform-node/NodeServices');
  const NodeCrypto = await load('@effect/platform-node/NodeCrypto');
  const Runtime = await import(pathToFileURL(join(repo, 'packages/provider-acp/src/server/AcpSessionRuntime.ts')).href);
  console.log('Node ' + process.version);
  console.log(execFileSync('git', ['--version'], { encoding: 'utf8' }).trim());
  console.log('Revision ' + execFileSync('git', ['-C', repo, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim());
  console.log('Effect ' + JSON.parse(readFileSync(require.resolve('effect/package.json'))).version);
  const peer = join(scratch, 'peer.mjs');
  writeFileSync(peer, `import { createInterface } from 'node:readline';
const supports = process.argv[2] === 'supported';
for await (const line of createInterface({input:process.stdin})) {
 const r=JSON.parse(line); if(r.id===undefined)continue;
 let result={},error;
 if(r.method==='initialize') result={protocolVersion:2,capabilities:{session:{mcp:supports?{stdio:{}}:{}}},info:{name:'fixture',version:'1'}};
 else if(r.method==='session/new') {
  if(!supports && r.params.mcpServers?.some(s=>s.type==='stdio')) error={code:-32602,message:'Stdio MCP transport not advertised'};
  else result={sessionId:'fixture-session'};
 }
 else error={code:-32601,message:'Unsupported fixture method'};
 process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:r.id,...(error?{error}:{result})})+'\\n');
}
`);
  const run = async (supports) => {
    let sent;
    const exit = await Effect.runPromiseExit(Effect.gen(function* () {
      const runtime = yield* Runtime.make({
        spawn: { command: process.execPath, args: [peer, supports ? 'supported' : 'unsupported'], extendEnv: false, env: { HOME: process.env.HOME, TMPDIR: process.env.TMPDIR, T3CODE_HOME: process.env.T3CODE_HOME } },
        cwd: join(scratch, 'cwd'), clientInfo: { name: 'fixture-client', version: '1' },
        mcpServers: [{ type: 'stdio', name: 't3-code', command: process.execPath, args: [] }],
        requestLogger: event => Effect.sync(() => { if(event.method==='session/new' && event.status==='started') sent=event.payload; }),
      });
      return yield* runtime.start();
    }).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))));
    console.log(JSON.stringify({ advertisedStdio: supports, sentServers: sent?.mcpServers, outcome: exit._tag }));
    return { exit, sent };
  };
  const control = await run(true);
  if (!Exit.isSuccess(control.exit)) throw new Error('Control failed');
  const failing = await run(false);
  if (Exit.isSuccess(failing.exit) && (!failing.sent?.mcpServers?.length)) code = 0;
  else if (Exit.isFailure(failing.exit) && failing.sent?.mcpServers?.some(s=>s.type==='stdio')) code = 1;
} catch (error) { console.log('Setup error: ' + error.message); }
finally { rmSync(scratch, {recursive:true,force:true}); }
console.log(code === 0 ? 'RESULT: expected behavior' : code === 1 ? 'RESULT: defect reproduced' : 'RESULT: inconclusive');
process.exitCode = code;

Expected behavior

Only send a stdio MCP declaration to an ACP v2 agent that advertises capabilities.session.mcp.stdio. This is already required by T3's own pinned schema-v2.0.0-alpha.3: the McpCapabilities definition defines omitted or null stdio capability as unadvertised, and the McpServer stdio variant limits its availability to agents with session.mcp.stdio. The current session-setup transport check states the same client requirement. This report relies on the pinned schema, rather than introducing a later documentation requirement. ACP v1 differs: its pinned schema requires every agent to support stdio, so an unconditional v1 fallback is outside this finding.

Actual behavior

Calling the production AcpSessionRuntime.make(...).start() with a stdio MCP server and an ACP v2 peer advertising capabilities.session.mcp: {} still sends that stdio server in session/new. The fixture rejects that unadvertised transport with JSON-RPC -32602, and the runtime fails startup. The otherwise identical peer advertising stdio: {} starts successfully. This is a runtime reproduction with a deterministic local peer, without a real provider, login, network endpoint, or paid prompt.

Impact

Blocks work completely

Impact: P1 - Valid ACP v2 deployments that omit stdio and have no selected ACP or HTTP alternative cannot open a T3 session when the normal T3 MCP declaration is present.

The failure is deterministic for the tested peer. The normal adapter constructs the stdio fallback whenever a thread has a T3 MCP session. No practical user setting that suppresses that declaration on this path was established; changing the peer to advertise a transport it cannot support is not a workaround. NOT_ENOUGH_INFO: the number of real deployed v2 agents with these capabilities was not measured; an initialization capture from those deployments would establish prevalence. The successful control establishes that the test's process launch and protocol setup work.

Version or commit

T3 Code commit 59e64eef881632a23f89c4138bcf37f23d92bf93; pinned ACP v2 schema schema-v2.0.0-alpha.3 and v1 schema schema-v1.21.0.

Environment

Node v26.12.1, Git 2.56.0, Darwin 27.2.0, arm64, repository-pinned Effect 4.1.3. No real ACP agent was launched.

Interface: API: AcpSessionRuntime.make and start; also used by the local/Registry ACP adapter's thread setup.

Earliest failure stage: Integration or transport

Evidence

Recorded script output, with the installed Node executable path replaced by [REDACTED]:

Node v26.12.1
git version 2.56.0
Revision 59e64eef881632a23f89c4138bcf37f23d92bf93
Effect 4.1.3
{"advertisedStdio":true,"sentServers":[{"type":"stdio","name":"t3-code","command":"[REDACTED]","args":[]}],"outcome":"Success"}
{"advertisedStdio":false,"sentServers":[{"type":"stdio","name":"t3-code","command":"[REDACTED]","args":[]}],"outcome":"Failure"}
RESULT: defect reproduced

packages/effect-acp/src/client.ts#L597-L602 correctly normalizes the v2 capability to stdio: false. packages/provider-acp/src/server/AcpSessionRuntime.ts#L2148-L2161 checks ACP and HTTP capabilities but returns the fallback list without checking stdio. packages/provider-acp/src/server/AcpSessionRuntime.ts#L2257-L2271 passes that list to session/new. The normal adapter fallback declaration comes from packages/provider-acp/src/server/adapter.ts#L888-L906. The fixture never launches the declared MCP command; it only validates its transport against the initialization capability.

Expected source: the linked upstream contract. Failure source: the production runtime/adapter run with the supplied fake peer. Evidence provenance: observed. Local verification: reproduced. Reproduction completeness: complete for the fake-peer case. Missing fact: live-deployment prevalence was not measured.

References

  • https://github.com/pingdotgg/t3code/issues/35320 — concerns an ACP v1 OpenClaw bridge rejecting all per-session MCP; this finding concerns explicit transport gating in the pinned ACP v2 contract.
  • https://github.com/pingdotgg/t3code/pull/35392 — proposes tolerating agents that reject per-session MCP; the v2 client must also respect its negotiated stdio capability before sending setup.

Restoration check

The script's advertised-stdio control should continue to succeed. The peer without stdio support should receive no stdio declaration and should start successfully, making the script print RESULT: expected behavior and exit 0. An absent MCP capability must not make an otherwise valid session unusable through an unsupported declaration.

  1. tanstack-source added labelsbug

  2. github-actions[bot](GitHub profile, opens in a new tab) added labelsvouch:denounced

Sign in to join this conversation.