From 5314042990bd4e237c171f57845b0b03e2dae9a6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Apr 2026 21:01:20 +0100 Subject: [PATCH] fix(gateway): normalize MCP object schemas --- src/gateway/mcp-http.schema.ts | 6 ++-- src/gateway/mcp-http.test.ts | 51 +++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/gateway/mcp-http.schema.ts b/src/gateway/mcp-http.schema.ts index 232a3bedc71..9942e9bfda4 100644 --- a/src/gateway/mcp-http.schema.ts +++ b/src/gateway/mcp-http.schema.ts @@ -70,9 +70,9 @@ export function buildMcpToolSchema(tools: McpLoopbackTool[]): McpToolSchemaEntry } if (raw.type !== "object") { raw.type = "object"; - if (!raw.properties) { - raw.properties = {}; - } + } + if (!raw.properties) { + raw.properties = {}; } return { name: tool.name, diff --git a/src/gateway/mcp-http.test.ts b/src/gateway/mcp-http.test.ts index 5670bb74bc0..0d10a1bf8c3 100644 --- a/src/gateway/mcp-http.test.ts +++ b/src/gateway/mcp-http.test.ts @@ -1,8 +1,20 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { getFreePortBlockWithPermissionFallback } from "../test-utils/ports.js"; +type MockGatewayTool = { + name: string; + description: string; + parameters: Record; + execute: (...args: unknown[]) => Promise<{ content: Array<{ type: string; text: string }> }>; +}; + +type MockGatewayScopedTools = { + agentId: string; + tools: MockGatewayTool[]; +}; + const resolveGatewayScopedToolsMock = vi.hoisted(() => - vi.fn(() => ({ + vi.fn<(...args: unknown[]) => MockGatewayScopedTools>(() => ({ agentId: "main", tools: [ { @@ -112,6 +124,43 @@ describe("mcp loopback server", () => { ); }); + it("adds empty properties for object schemas that omit properties", async () => { + resolveGatewayScopedToolsMock.mockReturnValue({ + agentId: "main", + tools: [ + { + name: "schema_probe", + description: "exercise no-argument MCP schemas", + parameters: { type: "object" }, + execute: async () => ({ + content: [{ type: "text", text: "ok" }], + }), + }, + ], + }); + server = await startMcpLoopbackServer(0); + const runtime = getActiveMcpLoopbackRuntime(); + + const response = await sendRaw({ + port: server.port, + token: runtime ? resolveMcpLoopbackBearerToken(runtime, false) : undefined, + headers: { + "content-type": "application/json", + "x-session-key": "agent:main:main", + }, + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }), + }); + const payload = (await response.json()) as { + result?: { tools?: Array<{ inputSchema?: Record }> }; + }; + + expect(response.status).toBe(200); + expect(payload.result?.tools?.[0]?.inputSchema).toEqual({ + type: "object", + properties: {}, + }); + }); + it("derives senderIsOwner from the loopback bearer token", async () => { server = await startMcpLoopbackServer(0); const activeServer = server;