# perf\(acp\): MCP calls over ACP no longer wait behind a long call · PR \#23834 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/23834)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: open

Source repository: Unavailable fork

Source branch: r13/b184-let-independent-mcp-over-acp-calls-run-c

Target branch: main

Head commit: 1a42091e68cca5b763228d83448cdcbf1b75e49b

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/23834)

[Commits](https://git.cafe/pingdotgg/t3code/pull/23834/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/23834/diffs?format=markdown)

## Description

---

## Problem

When an ACP agent reaches T3's tools through MCP-over-ACP (`mcp/connect`, `mcp/message`), every message on a connection went through one lock in `mcpOverAcpBridge.ts`. The lock covered the whole exchange: the HTTP POST and the full response read. So while one call was pending, such as a `t3_thread_wait` that can take minutes, every other call on that connection waited, even a `tools/list` the server could answer at once.

Nothing else in the path is serial. The ACP client's `RpcServer` and T3's MCP HTTP endpoint (`McpServer.layerHttp`) both run requests with unbounded concurrency, so the bridge lock was the only thing ordering them.

## Change

`packages/provider-acp/src/server/mcpOverAcpBridge.ts`: `send` takes the connection lock only until `initialize` has negotiated a protocol version. After that, messages go straight to `fetch` and run concurrently.

- The handshake stays ordered. A message sent before the protocol version is known waits for the lock, as it does today, so nothing is sent ahead of `initialize` with missing session headers.
- After the handshake, the session id and protocol version do not change. A response either carries the same session id or none, which keeps the old one, so there is no shared state left to protect.
- Disconnect is unchanged. It never took the lock. It removes the connection and sends `DELETE`, and the server interrupts that session's in-flight requests. A late response only updates the removed connection object, so it cannot bring the session back.

Request ids still come from the per-connection counter, and each POST is matched to its own response, as before.

## Scope and approval

No issue was filed. I think this fits CONTRIBUTING's exception for a very small, focused fix: a short call waiting minutes behind \`t3_thread_wait\` is a clear defect, and the fix narrows one lock in one function, with no protocol, setting or UI change. I searched open and closed PRs for this change and found none.

## Verification

New test in `packages/provider-acp/src/server/mcpOverAcpBridge.test.ts`: "answers a call while a long call on the same connection is pending". It initializes a connection, starts a `tools/call` whose response never arrives until the test releases it, waits until that request reached `fetch`, then sends `tools/list` and expects its result. Then it releases the first call and checks its result too. It uses deferred promises, no timers.

- With main's bridge, `tools/list` never starts, and the test fails on vitest's 60 s timeout. The other three tests pass.
- On this branch, all four tests pass.

![New test on main and on this branch](https://gist.githubusercontent.com/only21mil/6f46652477cbcbd9972f00488169fd5c/raw/397d9fb3bee4635bf28cd4419c8a166079f625a2/tests.png)

For a timing check, I ran a temporary, uncommitted script that drives the real bridge with real `fetch` against a local HTTP server. Its `tools/call` answers after 2,000 ms, and `tools/list` is sent once that call has reached the server. With main's bridge, `tools/list` took 2,009 ms, the full length of the other call. On this branch it took 1 ms. The slow call took about 2,008 ms in both runs.

![Timing on main and on this branch](https://gist.githubusercontent.com/only21mil/6f46652477cbcbd9972f00488169fd5c/raw/397d9fb3bee4635bf28cd4419c8a166079f625a2/probe.png)

Also ran: `vp lint` and `vp fmt --check` on the two changed files, and `tsc-rs --noEmit` in `packages/provider-acp`. The branch test run and both timing runs were on Linux x86_64. The main-side test run was on macOS arm64.

This is server-only. Agents get tool results sooner, and nothing a screen shows changes, so there are no UI screenshots.

Not checked:

- A live ACP agent that sends concurrent MCP calls on one connection. The repo's mock agent advertises MCP-over-ACP support but never calls it, and I had no provider credentials. The test and the timing script drive the bridge directly.
- Agents that send several messages before `initialize` finishes. They still queue one at a time, as they do today.

Prior PRs: none attempted this. [#14420](https://github.com/pingdotgg/t3code/pull/14420) by @kiiwee changes how `disconnect` discards failed response bodies in this bridge, a different fix in a different function. [#14432](https://github.com/pingdotgg/t3code/pull/14432) by @kiiwee, closed, made the 16-connection cap atomic across concurrent `connect` calls. This PR does not touch `connect`.

Production fix, tests, PR description and images written with Claude Opus 5.5 in T3 Code Nightly.

🤖 Generated with Claude Opus 5.5 in T3 Code Nightly

