From bf4bcfadde226d6b636b7fee20a964f5d4f957a1 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sat, 11 Apr 2026 13:26:24 -0500 Subject: [PATCH] fix(claude-code): harden MCP elicitation schema handling --- packages/mcp-server/src/server.ts | 3 -- .../claude-code-cli/stream-adapter.ts | 19 +++++++---- .../tests/stream-adapter.test.ts | 33 +++++++++++++++++++ 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 1db1e6254..d619ff0f6 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -547,9 +547,6 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{ type: 'string', title: item.key, description: descParts.join('\n'), - format: 'password', - writeOnly: true, - 'x-sensitive': true, }; // Don't mark as required — empty string = skip } diff --git a/src/resources/extensions/claude-code-cli/stream-adapter.ts b/src/resources/extensions/claude-code-cli/stream-adapter.ts index a1d08e7b4..a6efa439a 100644 --- a/src/resources/extensions/claude-code-cli/stream-adapter.ts +++ b/src/resources/extensions/claude-code-cli/stream-adapter.ts @@ -313,8 +313,15 @@ export function parseTextInputElicitation( request: Pick, ): ParsedTextInputField[] | null { if (request.mode && request.mode !== "form") return null; - const properties = request.requestedSchema?.properties; - if (!properties || typeof properties !== "object") return null; + const schema = request.requestedSchema as + | ({ properties?: Record; keys?: Record } & Record) + | undefined; + const fieldsSource = schema?.properties && typeof schema.properties === "object" + ? schema.properties + : schema?.keys && typeof schema.keys === "object" + ? schema.keys + : undefined; + if (!fieldsSource) return null; const requiredSet = new Set( Array.isArray(request.requestedSchema?.required) @@ -323,10 +330,10 @@ export function parseTextInputElicitation( ); const fields: ParsedTextInputField[] = []; - for (const [fieldId, field] of Object.entries(properties)) { - if (!field || typeof field !== "object") return null; - if (field.type !== "string") return null; - if (Array.isArray(field.oneOf) && field.oneOf.length > 0) return null; + for (const [fieldId, field] of Object.entries(fieldsSource)) { + if (!field || typeof field !== "object") continue; + if (field.type !== "string") continue; + if (Array.isArray(field.oneOf) && field.oneOf.length > 0) continue; fields.push({ id: fieldId, diff --git a/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts b/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts index d0b7bed10..082b40da2 100644 --- a/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +++ b/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts @@ -557,6 +557,39 @@ describe("stream-adapter — MCP elicitation bridge", () => { ]); }); + test("parseTextInputElicitation accepts legacy keys schema and skips unsupported fields", () => { + const request = { + serverName: "gsd-workflow", + message: "Enter secure values", + mode: "form" as const, + requestedSchema: { + type: "object" as const, + keys: { + API_TOKEN: { + type: "string", + title: "API_TOKEN", + description: "Leave empty to skip.", + }, + META: { + type: "object", + title: "metadata", + }, + }, + }, + }; + + const parsed = parseTextInputElicitation(request as any); + assert.deepEqual(parsed, [ + { + id: "API_TOKEN", + title: "API_TOKEN", + description: "Leave empty to skip.", + required: false, + secure: true, + }, + ]); + }); + test("createClaudeCodeElicitationHandler collects secure_env_collect fields through input dialogs", async () => { const secureRequest = { serverName: "gsd-workflow",