From 87cdcf28e0dd36cae843b412ae06f8c515a396e1 Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 27 Aug 2026 11:10:37 +0200 Subject: [PATCH] fix(mcp): return valid string for void tool results (#6703) --- .../src/app/api/mcp/[transport]/route.ts | 3 ++- .../src/app/api/mcp/_serialize-result.spec.ts | 23 +++++++++++++++++++ .../src/app/api/mcp/_serialize-result.ts | 3 +++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 apps/nextjs/src/app/api/mcp/_serialize-result.spec.ts create mode 100644 apps/nextjs/src/app/api/mcp/_serialize-result.ts diff --git a/apps/nextjs/src/app/api/mcp/[transport]/route.ts b/apps/nextjs/src/app/api/mcp/[transport]/route.ts index 364fe90c5..f6a303a86 100644 --- a/apps/nextjs/src/app/api/mcp/[transport]/route.ts +++ b/apps/nextjs/src/app/api/mcp/[transport]/route.ts @@ -14,6 +14,7 @@ import { db } from "@homarr/db"; import { getPackageVersion } from "~/versions/package-reader"; import { extractMcpTools } from "../_extract-tools"; +import { serializeMcpToolResult } from "../_serialize-result"; const logger = createLogger({ module: "mcpRoute" }); @@ -203,7 +204,7 @@ const mcpHandler = createMcpHandler( const input = args && Object.keys(args).length > 0 ? args : undefined; const result = await procedure(input); return { - content: [{ type: "text" as const, text: JSON.stringify(result) }], + content: [{ type: "text" as const, text: serializeMcpToolResult(result) }], }; } catch (error) { const message = sanitizeErrorMessage(error); diff --git a/apps/nextjs/src/app/api/mcp/_serialize-result.spec.ts b/apps/nextjs/src/app/api/mcp/_serialize-result.spec.ts new file mode 100644 index 000000000..8ee8df795 --- /dev/null +++ b/apps/nextjs/src/app/api/mcp/_serialize-result.spec.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; + +import { serializeMcpToolResult } from "./_serialize-result"; + +describe("serializeMcpToolResult", () => { + it("serializes objects as JSON strings", () => { + expect(serializeMcpToolResult({ boardId: "1" })).toBe('{"boardId":"1"}'); + }); + + it("serializes primitive values", () => { + expect(serializeMcpToolResult("hello")).toBe('"hello"'); + expect(serializeMcpToolResult(42)).toBe("42"); + expect(serializeMcpToolResult(true)).toBe("true"); + }); + + it("serializes null as the string null", () => { + expect(serializeMcpToolResult(null)).toBe("null"); + }); + + it("serializes void results as the string null so MCP content text is always a string", () => { + expect(serializeMcpToolResult(undefined)).toBe("null"); + }); +}); diff --git a/apps/nextjs/src/app/api/mcp/_serialize-result.ts b/apps/nextjs/src/app/api/mcp/_serialize-result.ts new file mode 100644 index 000000000..f49492062 --- /dev/null +++ b/apps/nextjs/src/app/api/mcp/_serialize-result.ts @@ -0,0 +1,3 @@ +export function serializeMcpToolResult(result: unknown): string { + return JSON.stringify(result) ?? "null"; +}