From c355ff43c058290a55efcd5c01a9da1b46926af9 Mon Sep 17 00:00:00 2001 From: Florent <42492541+florentmsl@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:10:46 +0200 Subject: [PATCH 1/2] fix(server): avoid duplicate JSON in ACP terminal results --- apps/server/src/mcp/AcpMcpStdioBridge.test.ts | 51 ++++++++++++++++++- apps/server/src/mcp/AcpMcpStdioBridge.ts | 31 ++++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/apps/server/src/mcp/AcpMcpStdioBridge.test.ts b/apps/server/src/mcp/AcpMcpStdioBridge.test.ts index 0368c89714208..cc55aca6e3fc5 100644 --- a/apps/server/src/mcp/AcpMcpStdioBridge.test.ts +++ b/apps/server/src/mcp/AcpMcpStdioBridge.test.ts @@ -4,8 +4,9 @@ import * as NodeStream from "node:stream"; import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; +import { vi } from "vite-plus/test"; -import { runAcpMcpStdioBridge } from "./AcpMcpStdioBridge.ts"; +import { runAcpMcpCliFastPath, runAcpMcpStdioBridge } from "./AcpMcpStdioBridge.ts"; function makeHarness(responder: (request: Request) => Promise | Response) { const input = new NodeStream.PassThrough(); @@ -30,6 +31,54 @@ function makeHarness(responder: (request: Request) => Promise | Respon } describe("AcpMcpStdioBridge", () => { + it("prints structured results once in the terminal while retaining other content and errors", async () => { + const originalEnvironment = process.env; + using _environment = { + [Symbol.dispose]: () => { + process.env = originalEnvironment; + }, + }; + process.env = { + ...originalEnvironment, + T3_ACP_MCP_ENDPOINT: "http://127.0.0.1:1/mcp", + T3_ACP_MCP_AUTHORIZATION: "Bearer bridge-test", + }; + const structuredContent = { taskId: "task-1", status: "failed" }; + const explanation = { type: "text", text: "The command failed." }; + const image = { type: "image", data: "image-data", mimeType: "image/png" }; + const result = { + structuredContent, + content: [{ type: "text", text: JSON.stringify(structuredContent) }, explanation, image], + isError: true, + }; + using fetch = vi.spyOn(globalThis, "fetch").mockImplementation(async (_url, init) => { + if (init?.method === "DELETE") return new Response(null, { status: 204 }); + const message = JSON.parse(String(init?.body)) as { id?: string; method: string }; + if (message.method === "notifications/initialized") { + return new Response(null, { status: 202 }); + } + return Response.json( + { + jsonrpc: "2.0", + id: message.id, + result: message.method === "initialize" ? { protocolVersion: "2025-06-18" } : result, + }, + { headers: { "mcp-session-id": "session-42" } }, + ); + }); + using output = vi.spyOn(process.stdout, "write").mockReturnValue(true); + + await runAcpMcpCliFastPath("acp-mcp-call", ["task_status", '{"taskId":"task-1"}']); + + expect(output).toHaveBeenCalledExactlyOnceWith( + `${JSON.stringify({ ...result, content: [explanation, image] })}\n`, + ); + expect(fetch).toHaveBeenLastCalledWith( + "http://127.0.0.1:1/mcp", + expect.objectContaining({ method: "DELETE" }), + ); + }); + it.effect("forwards requests and replays the session id and protocol version", () => Effect.gen(function* () { const { input, written, requests, bridge } = makeHarness((request) => { diff --git a/apps/server/src/mcp/AcpMcpStdioBridge.ts b/apps/server/src/mcp/AcpMcpStdioBridge.ts index 2db36ce5abc28..98bc3868e5f53 100644 --- a/apps/server/src/mcp/AcpMcpStdioBridge.ts +++ b/apps/server/src/mcp/AcpMcpStdioBridge.ts @@ -378,5 +378,34 @@ export async function runAcpMcpCliFastPath( arguments: parsed as Record, }), ); - process.stdout.write(`${JSON.stringify(result)}\n`); + // Terminal callers see the whole envelope; native MCP clients choose a field. + // Keep distinct content, but avoid printing the JSON fallback a second time. + if ( + typeof result === "object" && + result !== null && + "structuredContent" in result && + result.structuredContent !== undefined && + "content" in result && + Array.isArray(result.content) + ) { + const encoded = JSON.stringify(result.structuredContent); + process.stdout.write( + `${JSON.stringify({ + ...result, + content: result.content.filter( + (part: unknown) => + !( + typeof part === "object" && + part !== null && + "type" in part && + part.type === "text" && + "text" in part && + part.text === encoded + ), + ), + })}\n`, + ); + } else { + process.stdout.write(`${JSON.stringify(result)}\n`); + } } From 9ee5ca8d9e3bf4702841263ec123ac616fa02f26 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 05:51:54 +0000 Subject: [PATCH 2/2] refactor(server): print ACP terminal results in one place --- apps/server/src/mcp/AcpMcpStdioBridge.ts | 38 +++++++++++------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/apps/server/src/mcp/AcpMcpStdioBridge.ts b/apps/server/src/mcp/AcpMcpStdioBridge.ts index 1424a67b3b3eb..3f5ef1006c465 100644 --- a/apps/server/src/mcp/AcpMcpStdioBridge.ts +++ b/apps/server/src/mcp/AcpMcpStdioBridge.ts @@ -361,7 +361,7 @@ export async function runAcpMcpCliFastPath( process.exitCode = 2; return; } - const result = await Effect.runPromise( + let result = await Effect.runPromise( callAcpMcpTool({ endpoint, authorization, @@ -369,8 +369,7 @@ export async function runAcpMcpCliFastPath( arguments: parsed as Record, }), ); - // Terminal callers see the whole envelope; native MCP clients choose a field. - // Keep distinct content, but avoid printing the JSON fallback a second time. + // Terminal callers see both fields, unlike native MCP clients that choose one. if ( typeof result === "object" && result !== null && @@ -380,23 +379,20 @@ export async function runAcpMcpCliFastPath( Array.isArray(result.content) ) { const encoded = JSON.stringify(result.structuredContent); - process.stdout.write( - `${JSON.stringify({ - ...result, - content: result.content.filter( - (part: unknown) => - !( - typeof part === "object" && - part !== null && - "type" in part && - part.type === "text" && - "text" in part && - part.text === encoded - ), - ), - })}\n`, - ); - } else { - process.stdout.write(`${JSON.stringify(result)}\n`); + result = { + ...result, + content: result.content.filter( + (part: unknown) => + !( + typeof part === "object" && + part !== null && + "type" in part && + part.type === "text" && + "text" in part && + part.text === encoded + ), + ), + }; } + process.stdout.write(`${JSON.stringify(result)}\n`); }