Skip to content

[Bug]: Codex 0.160 agents get no T3 Code MCP tools: /mcp answers server/discover with HTTP 400 #16067

Open

Description

@SiavZ

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. Install Codex CLI 0.160.0.
  2. Run t3 serve on current main. Seen on 37de6cbde; apps/server/src/mcp/McpHttpServer.ts is unchanged on 1a3f7ad50.
  3. Start a Codex thread and ask the agent to call any t3-code tool, for example orchestrator_capabilities.

Expected behavior

Codex connects to the t3-code MCP server and the agent can call T3 Code's tools.

Actual behavior

Codex marks t3-code as failed at session start, so the agent has none of T3 Code's tools. The provider event log shows:

mcpServer/startupStatus/updated  name: t3-code  status: failed
MCP client for `t3-code` failed to start: MCP startup failed: handshaking with MCP server failed:
... error: unexpected server response: HTTP 400: , when send discover request

The server trace shows three POST /mcp requests from codex-mcp-client/0.160.0 with mcp-method: server/discover and mcp-protocol-version: 2026-07-28, each answered with status 400. Bearer authentication passes (McpSessionRegistry.resolve succeeds), so the request is rejected during protocol negotiation, not auth.

Likely cause: McpHttpServer.ts pins protocols: [McpProtocol.v2025_06_18]. The bundled Effect (4.0.0-rc.115) ships protocol adapters up to v2025_11_25, and McpServer.ts carries a TODO about session strategy "before v2026-07-28".

A Claude thread in the same build connects and calls the tools normally.

Impact

Major degradation or frequent failure

Version or commit

37de6cbde (the same pin is on main 1a3f7ad50)

Environment

macOS, Node 24, Codex CLI 0.160.0 (ChatGPT sign-in), server started with t3 serve

Logs or stack traces

See "Actual behavior". Server trace span: http.server POST /mcp, http.request.header.mcp-method: server/discover, http.request.header.mcp-protocol-version: 2026-07-28, http.response.status_code: 400.

Workaround

None found. Claude threads are unaffected.

Found by Claude Opus 5.5 in Jcode while running headless acceptance tests on an isolated base dir.

Activity

  1. abdtriedcoding commented on Oct 8, 2026

    @abdtriedcoding

    Claude Code hits this too, not only Codex.

    Version: T3 Code 0.0.45 on Windows, Claude Code 2.1.294 (agent-sdk 0.3.276).

    At session start the t3-code MCP server shows status: failed, with the error InvalidHTTPResponse fetching http://127.0.0.1:3773. The agent gets no T3 tools, so link_pull_request is unavailable.

    The server trace shows the same sequence as the Codex report:

    1. POST /mcp with mcp-method: server/discover and mcp-protocol-version: 2026-07-28 returns 400.
    2. About 1.6 seconds later, a plain initialize returns 200 (@effect/mcp/2025-06-18/initialize).
    3. No tools/list follows, and the client marks the server failed.

    Bearer auth passes in both requests. Restarting the app does not help.

    The issue says Claude threads are unaffected. That holds only for older Claude Code builds. Builds that send server/discover fail the same way.

  2. borzaka commented on Oct 25, 2026

    @borzaka

    Confirming the same failure signature on Windows with Claude Code (T3 Code 0.1.2), with server-trace details, a note on how Claude Code reacts to the 400, and an HTTP framing observation I could not fully attribute (see caveats).

    Environment

    • T3 Code 0.1.2 desktop app (server-runtime.json: serverVersion: 0.1.2, listening on 127.0.0.1:3773)
    • Windows 11 Pro 10.0.26200
    • Provider claudeAgent, Claude Code 2.1.310 (agent-sdk 0.3.276)
    • A local security product on this machine re-signs HTTPS traffic (TLS interception). This matters for the caveats below.

    Symptom

    At session start the SDK init message reports the server as failed, while other MCP servers in the same session connect:

    "mcp_servers":[
      {"name":"plugin:context7:context7","status":"failed","source":"plugin"},
      {"name":"context7","status":"connected","source":"user"},
      {"name":"claude.ai Claude Docs","status":"connected","source":"claudeai"},
      {"name":"t3-code","status":"failed","source":"dynamic"}
    ]

    Error: InvalidHTTPResponse fetching http://127.0.0.1:3773. The thread has none of the T3 tools (orchestrator_capabilities, delegate_task, link_pull_request, ...).

    It is not intermittent: in my local provider event logs, t3-code is failed in 31 of 32 Claude threads since 2026-10-10 (the remaining one is stuck at starting), never connected. Restarting the desktop app (new server process, still 0.1.2) does not help, and a new thread started after the restart fails the same way.

    Registered vs. not added: the init event lists t3-code with "source":"dynamic","status":"failed", so the server is registered (injected by T3 at runtime) and fails at connect time; it is not missing from the thread. claude mcp list does not show it, since dynamic servers are not in the config files. The same command in a plain CLI session shows plugin:context7:context7 failing with InvalidHTTPResponse while context7 connects, so this error also appears for a remote HTTP server outside T3.

    Server trace: POST /mcp 400 then 200 on every session start

    server.trace.ndjson has 8 session starts from claude-code/2.1.310 (sdk-ts, agent-sdk/0.3.276). Each is a POST /mcp answered with 400, followed 25 ms to 1.6 s later by a POST /mcp answered with 200, after which the client marks t3-code as failed:

    2026-10-25T15:30:28.304Z  POST /mcp  400   2.8ms
    2026-10-25T15:30:29.915Z  POST /mcp  200   9.8ms
    2026-10-25T15:48:40.876Z  POST /mcp  400   1.3ms   <- thread started after the app restart
    2026-10-25T15:48:40.899Z  POST /mcp  200   1.3ms
    

    McpHttpServer.authenticateRequest succeeds, so the 400 is not an auth failure. The 0.1.2 trace does not record the mcp-method / mcp-protocol-version request headers or the response body, so I can't show which header triggered the 400.

    Correction to my earlier reading: I first took this 400 for the cause. Claude Code's own debug log (from a different MCP server in a plain CLI session; T3-injected servers don't appear there) shows that it probes the modern protocol first and then falls back on purpose: version negotiation probe failed on the http transport; reconnecting pinned legacy. So the 400 is probably expected from a server that pins 2025-06-18, and the failure for Claude Code happens after the legacy fallback. That would make the server/discover 400 the cause for clients that don't fall back (Codex, per the original report), but not necessarily for Claude Code.

    Observation: malformed chunked responses from /mcp

    I probed the endpoint without a token. Both POST /mcp and GET /.well-known/oauth-protected-resource/mcp send transfer-encoding: chunked, but the body is not chunk-framed (the JSON follows the headers directly, with no hex chunk-size line). A plain TCP client confirms the raw bytes:

    $ curl -i -X POST http://127.0.0.1:3773/mcp -H 'Content-Type: application/json' \
        -H 'Accept: application/json, text/event-stream' -d '{"jsonrpc":"2.0","id":1,"method":"initialize"}'
    HTTP/1.1 401 Unauthorized
    transfer-encoding: chunked
    www-authenticate: Bearer resource_metadata="http://127.0.0.1:3773/.well-known/oauth-protected-resource/mcp"
    content-type: application/json
    ...
    
    {"error":"invalid_mcp_credential","message":"A valid T3 Code MCP credential is required."}
    curl: (56) chunk hex-length char not a hex digit: 0x7b
    

    GET / is fine (it uses content-length). Node's fetch (undici) also rejects these responses ("Invalid character in chunk size"), PowerShell 5.1 fails with "unexpected error occurred on a receive". A strict client such as Claude Code's would report this as InvalidHTTPResponse, which is the error I see.

    Caveats (I could not close these)

    • Possible local interference. The same pattern (transfer-encoding: chunked plus an unframed JSON body) also appears on https://mcp.context7.com/.well-known/oauth-protected-resource/mcp on this machine, where the TLS-intercepting security product is in the path. Controls that were fine: my own Node servers on six loopback ports (JSON with Content-Length, and JSON with explicit/automatic chunking), and JSON from api.github.com and registry.npmjs.org. I tried to turn the product's web protection off, but its interception stayed active (verified through the certificate issuer), so this test was inconclusive. I can't say whether the server itself or something on the path produces the unframed body.
    • I did not test with a valid bearer token (I didn't want to extract the session credential), so I can't say whether the authenticated initialize response is affected too.

    What would help

    • Log mcp-method, mcp-protocol-version and the response body in the /mcp request span.
    • Check whether the response to an authenticated initialize is chunk-framed. If it is not, Claude Code would fail right after the fallback exactly as observed.

    Happy to run further diagnostics on Windows.

    Diagnostics gathered with the help of Claude Code.

  3. desidia-cyber commented on Nov 3, 2026

    @desidia-cyber

    Correction (later the same day): on my machine this was Avast, not T3. The conclusions struck through below are wrong.

    • About 15 minutes later, the same T3 server process returned the same /mcp 401 correctly: content-length: 90 and an intact body. Bun and Node fetch both read it fine. A second, isolated copy of the server built from the same server.asar also answered correctly. In the bad responses, content-length had been removed and transfer-encoding: chunked inserted as the first header without re-framing the body. The T3 server writes content-length, so something between the server and the client rewrote it intermittently.
    • The fix was adding Avast exceptions (Settings → General → Exceptions) for the URL http://127.0.0.1:3773/* and for claude.exe. Claude Code sessions in T3 now load the t3-code MCP tools again.
    • This also accounts for the earlier caveat about a TLS-intercepting security product, and for the intermittent pattern in my timeline. DeepSeek probably kept working because its MCP client runs in a different process that the AV filters differently.
    • The POST /api/nonexistent 404 row in the table below was a false positive from my checker. It's a correctly framed empty chunked body (0\r\n\r\n).
    • The /mcp 400 on Claude Code's first request (the version-negotiation probe) is still there, but it doesn't break anything once responses reach the client intact.

    For anyone else hitting InvalidHTTPResponse fetching http://127.0.0.1:3773 on Windows: check for an antivirus or web shield that filters HTTP (Avast, AVG and similar), and add an exception for 127.0.0.1:3773.


    Still reproducing on T3 Code 0.1.7-nightly.20261103.4119 (Windows 10 LTSC 2021) with Claude Code 2.1.317 (agent-sdk 0.3.284). Adding a control test that addresses the "possible local interference" caveat above, plus a regression timeline.

    Same signature

    • Claude Code MCP log: Initializing HTTP transport to http://127.0.0.1:3773/mcp → HTTP Connection failed after 1364ms: InvalidHTTPResponse fetching "http://127.0.0.1:3773/mcp".
    • server.trace.ndjson: every Claude session start is POST /mcp 400 followed by POST /mcp 200 (@effect/mcp/2025-06-18/initialize), then no tools/list.
    • DeepSeek via ACP (user agent node) in the same app, at the same time, works fine: initialize 200 → 202 → tools/list → tools/call all succeed. It never gets the 400.

    Malformed chunked responses: control test suggests it's the T3 server (see correction: local AV)

    Raw socket dump of an unauthenticated POST /mcp:

    HTTP/1.1 401 Unauthorized
    transfer-encoding: chunked
    cache-control: no-store
    www-authenticate: Bearer resource_metadata="http://127.0.0.1:3773/.well-known/oauth-protected-resource/mcp"
    content-type: application/json
    vary: Accept-Encoding
    access-control-allow-origin: *
    Connection: close
    
    {"error":"invalid_mcp_credential","message":"A valid T3 Code MCP credential is required."}
    

    No chunk-size line and no terminating 0 chunk. Per route:

    Request Status transfer-encoding: chunked Body chunk-framed
    POST /mcp (no auth / bad bearer) 401 yes no
    GET /mcp, DELETE /mcp 401 yes no
    GET /.well-known/oauth-protected-resource/mcp 200 yes no
    POST /api/nonexistent 404 yes no yes (empty body; checker false positive)
    GET /, GET /api/auth/session 200 no (content-length) n/a

    Control: a plain node:http server on loopback (127.0.0.1:3799) doing writeHead(401, json) + write + end, read by the same raw-socket client, comes back properly framed (d\r\n{"error":"x"}\r\n0\r\n\r\n). So the loopback path on this machine doesn't strip chunk framing. The unframed bodies come from the T3 server process. That control only showed the AV wasn't rewriting traffic on that port at that moment. (Caveat: Avast Antivirus is installed here, and I can't rule out per-process filtering completely, but this control covers the same client and the same loopback and it turned out to be the cause.)

    How clients react to these responses:

    • Bun 1.3.9 fetch (Claude Code's runtime): InvalidHTTPResponse, the exact error Claude Code reports.
    • Node fetch (undici): TypeError: terminated.

    Working theory, consistent with the note above on the version-negotiation probe: Claude Code's first /mcp request (the modern-protocol probe) gets a 400 whose body is mis-framed the same way. Bun's fetch throws InvalidHTTPResponse instead of returning a 400, so the client never falls back cleanly. I couldn't test authenticated responses (no token available).

    Regression timeline (from Claude Code's mcp-logs-t3-code/*.jsonl)

    • 2026-10-24 → 10-31 16:00 UTC: connected every time (Claude Code 2.1.312 / 2.1.314).
    • 2026-10-31 16:39 → 11-02 11:12: intermittent; same InvalidHTTPResponse on failures (2.1.314 / 2.1.316).
    • 2026-11-02 15:00 → now: fails every time (2.1.316 / 2.1.317).

    The same Claude Code builds both connected and failed, so the Claude Code version doesn't look like the trigger. I'm on the Nightly channel, but my rotated server logs no longer show which T3 builds were running at those times.

    Repro (no token needed)

    // bun repro.mjs → InvalidHTTPResponse;  node repro.mjs → TypeError: terminated
    const r = await fetch('http://127.0.0.1:3773/mcp', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream' },
      body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize',
        params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'probe', version: '0' } } }),
    });
    console.log(r.status, await r.text());

    Expected: 401 with a readable JSON body. Actual: transport-level error, because the header says chunked and the body isn't.

    Diagnostics gathered with the help of Claude Code.

  4. desidia-cyber commented on Nov 3, 2026

    @desidia-cyber

    Correction to my comment above (now edited): on my machine the malformed transfer-encoding: chunked responses came from Avast rewriting loopback HTTP, not from the T3 server. The same server process later answered the same request correctly with content-length. Adding Avast exceptions for http://127.0.0.1:3773/* and claude.exe fixed Claude Code's InvalidHTTPResponse, and the T3 MCP tools load again. If you hit this on Windows, check for an HTTP-filtering antivirus or web shield first.

  5. borzaka commented on Nov 7, 2026

    @borzaka

    Follow-up: resolved on my machine. The cause was local security software (Norton Safe Web) rewriting loopback HTTP, not the T3 server. This resolves the caveats in my earlier comment and matches the Avast finding in desidia-cyber's comment. I withdraw my earlier suspicion that the T3 server itself sends unframed chunked responses: it does not.

    Environment

    • T3 Code 0.1.2 and 0.1.7 (same failure on both), 127.0.0.1:3773
    • Windows 11 Pro 10.0.26200, Claude Code 2.1.310 to 2.1.317
    • Norton 360 for Gamers 26.10 (Avast-engine processes aswEngSrv, afwServ, aswidsagent run alongside it)

    Root cause: Norton Safe Web

    With Safe Web active, responses from 127.0.0.1:3773 are rewritten: the content-length header is removed and transfer-encoding: chunked is inserted as the first header, but the body is not re-framed into chunks. Strict HTTP clients reject this (Claude Code's runtime reports InvalidHTTPResponse, Node's fetch reports Invalid character in chunk size, curl reports chunk hex-length char not a hex digit).

    Raw bytes of GET /.well-known/oauth-protected-resource/mcp on a plain TCP socket:

    # Safe Web active (broken)                      # Safe Web off, or exception added (correct)
    HTTP/1.1 200 OK                                 HTTP/1.1 200 OK
    transfer-encoding: chunked                      content-type: application/json
    content-type: application/json                  content-length: 220
    ...                                             ...
    {"resource":"http://127.0.0.1:3773/mcp",...     {"resource":"http://127.0.0.1:3773/mcp",...
    

    The same holds for the 401 on POST /mcp (content-length: 90 when intact). So the T3 server sends proper content-length responses.

    A/B results

    Each row was measured with the well-known endpoint, POST /mcp without a token, and an actual t3-code tool call.

    Safe Web main switch Safe Web sub-switches Result
    on all on broken
    on all off (HTTPS scanning, Uncommon application scanning, QUIC/HTTP3, WebSocket protocol, DNS/DoH, botnets) still broken
    off n/a works (repeated twice)
    on all on, plus exception http://127.0.0.1/* works

    None of the sub-switches affects the loopback rewriting, tested one at a time and cumulatively. Only the main switch, or an exception, fixes it. The Norton firewall and Auto-Protect are unrelated; turning them off changed nothing.

    A separate effect: HTTPS scanning re-signs TLS certificates (issuer Norton Web/Mail Shield) and produced the same malformed framing for https://mcp.context7.com/.well-known/oauth-protected-resource/mcp. That is why plugin:context7:context7 (anonymous, so it goes through OAuth discovery) failed with InvalidHTTPResponse while the header-authenticated context7 entry connected. Turning HTTPS scanning off fixed the plugin (claude mcp list shows all three servers connected).

    Fix

    Add an exception for http://127.0.0.1/* in Norton's Safe Web settings (the T3 port is 3773, but the wildcard also survives a port change). If you use the context7 plugin, also turn HTTPS scanning off or add an exception for https://mcp.context7.com.

    Image Image

    On the earlier POST /mcp 400

    Claude Code's debug log shows it probes the modern protocol first and then falls back on purpose (version negotiation probe failed on the http transport; reconnecting pinned legacy), so the 400 is expected from a server that pins 2025-06-18 and is not what broke Claude Code here. The failure came after the fallback, from the rewritten responses above. For clients that do not fall back (Codex, per the original report) the server/discover 400 is still a real problem, and PR #28683 addresses it.

    Suggestions

    • A short troubleshooting note for Windows: InvalidHTTPResponse fetching http://127.0.0.1:3773 with web-shield antivirus software (Norton, Avast, AVG) should be solved with an exception for http://127.0.0.1/*.
    • If it is cheap, a clearer error or health hint when the t3-code MCP server fails with InvalidHTTPResponse on loopback would save users a long investigation.

    Thanks to desidia-cyber for the Avast lead that made this findable.

    Diagnostics gathered with the help of Claude Code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions