diff --git a/packages/provider-pi/src/server/mcpBridge.testkit.ts b/packages/provider-pi/src/server/mcpBridge.testkit.ts index 2b9b4701e367..c6cb690355de 100644 --- a/packages/provider-pi/src/server/mcpBridge.testkit.ts +++ b/packages/provider-pi/src/server/mcpBridge.testkit.ts @@ -44,6 +44,7 @@ export async function loadMcpBridge( readonly toolSearchDisabled?: boolean; readonly allowsTool?: (name: string) => boolean; readonly runtimeMode?: string; + readonly inputSchema?: Record; } = {}, ) { const handlers = new Map(); @@ -72,7 +73,7 @@ export async function loadMcpBridge( { name: "preview_snapshot", description: "Inspect the collaborative browser." }, ].map((tool) => ({ ...tool, - inputSchema: { type: "object", properties: {} }, + inputSchema: options.inputSchema ?? { type: "object", properties: {} }, outputSchema: { type: "object", properties: { count: { type: "number" } } }, })); const source = NodeModule.stripTypeScriptTypes( diff --git a/packages/provider-pi/src/server/mcpExtensionSource.test.ts b/packages/provider-pi/src/server/mcpExtensionSource.test.ts index 1205c7573ea4..7752db86d621 100644 --- a/packages/provider-pi/src/server/mcpExtensionSource.test.ts +++ b/packages/provider-pi/src/server/mcpExtensionSource.test.ts @@ -286,6 +286,32 @@ describe("Pi MCP tool exposure", () => { ); }); +describe("Pi MCP tool schemas", () => { + it.each([{ type: "object" }, {}])( + "declares empty properties on object schemas that omit them: %j", + async (inputSchema) => { + const bridge = await loadMcpBridge({ inputSchema }); + assert.isAbove(bridge.tools.length, 0); + for (const tool of bridge.tools) { + assert.deepEqual(tool.parameters, { type: "object", properties: {} }); + } + }, + ); + + it("keeps declared properties and other schema keywords", async () => { + const inputSchema = { + type: "object", + properties: { depth: { type: "number" } }, + required: ["depth"], + additionalProperties: false, + }; + const bridge = await loadMcpBridge({ inputSchema }); + for (const tool of bridge.tools) { + assert.deepEqual(tool.parameters, inputSchema); + } + }); +}); + describe("Pi tool discovery permissions", () => { it.each(["approval-required", "auto-accept-edits", "auto"])( "allows annotated T3 reads in %s while gating mutations and replacements", diff --git a/packages/provider-pi/src/server/mcpExtensionSource.ts b/packages/provider-pi/src/server/mcpExtensionSource.ts index 5e473c700d0c..8747d1d7b131 100644 --- a/packages/provider-pi/src/server/mcpExtensionSource.ts +++ b/packages/provider-pi/src/server/mcpExtensionSource.ts @@ -91,7 +91,13 @@ function parseSseOrJson(body: string, contentType: string): JsonRpcResponse { function jsonSchemaToTypebox(schema: Record | undefined) { const unsafe = (Type as { Unsafe?: (value: unknown) => unknown }).Unsafe; if (typeof unsafe === "function" && schema !== undefined) { - return unsafe(schema); + // Zero-argument MCP tools may omit "properties", which some OpenAI-compatible + // servers reject. Declare it, as Pi does for its own MCP tools. + return unsafe({ + ...schema, + type: schema.type ?? "object", + ...(schema.properties === undefined ? { properties: {} } : {}), + }); } return Type.Object({}, { additionalProperties: true }); }