From 1c9673db6ee462187431d9e3b289a24f07feee04 Mon Sep 17 00:00:00 2001 From: Molecule AI SDK-Dev Date: Wed, 13 May 2026 07:07:54 +0000 Subject: [PATCH 1/2] fix(mcp): z.string().nullable().optional() in update_workspace parent_id schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes KI-006: zod-to-json-schema with strictUnions: true produces `anyOf` for z.string().optional().nullable(), which causes INVALID_ARGUMENTS on valid null/undefined inputs in some MCP hosts. Safe order is z.string().nullable().optional() — semantically equivalent, no anyOf. Also adds regression test for the update_workspace schema in the KI-006 anyOf-free test suite. Co-Authored-By: Claude Opus 4.7 --- src/tools/workspaces.ts | 2 +- tests/__tests__/plugins-schema.test.ts | 27 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/tools/workspaces.ts b/src/tools/workspaces.ts index ee65309..b57f7f6 100644 --- a/src/tools/workspaces.ts +++ b/src/tools/workspaces.ts @@ -119,7 +119,7 @@ export function registerWorkspaceTools(srv: McpServer) { name: z.string().optional(), role: z.string().optional(), tier: z.number().optional(), - parent_id: z.string().optional().nullable().describe("Set parent for nesting, null to un-nest"), + parent_id: z.string().nullable().optional().describe("Set parent for nesting, null to un-nest"), }, handleUpdateWorkspace ); diff --git a/tests/__tests__/plugins-schema.test.ts b/tests/__tests__/plugins-schema.test.ts index edc453d..50ffa47 100644 --- a/tests/__tests__/plugins-schema.test.ts +++ b/tests/__tests__/plugins-schema.test.ts @@ -54,6 +54,24 @@ describe("KI-006: plugin tool schemas are anyOf-free", () => { }), } as const; + // ------------------------------------------------------------------------- + // Schema fixtures — mirrors src/tools/workspaces.ts + // ------------------------------------------------------------------------- + + const workspaceSchemas = { + update_workspace: z.object({ + workspace_id: z.string(), + name: z.string().optional(), + role: z.string().optional(), + tier: z.number().optional(), + // NOTE: nullable must come BEFORE optional in the Zod chain. + // z.string().optional().nullable() → zod-to-json-schema produces `anyOf`, + // which causes INVALID_ARGUMENTS on valid null/undefined inputs in some + // MCP hosts. The safe order is z.string().nullable().optional(). + parent_id: z.string().nullable().optional().describe("Set parent for nesting, null to un-nest"), + }), + } as const; + // ------------------------------------------------------------------------- // Tests // ------------------------------------------------------------------------- @@ -67,6 +85,15 @@ describe("KI-006: plugin tool schemas are anyOf-free", () => { }); } + for (const [tool, schema] of Object.entries(workspaceSchemas)) { + describe(tool, () => { + const json = zodToJsonSchema(schema, { strictUnions: true }); + it("has no anyOf", () => { + expect(hasAnyOf(json)).toBe(false); + }); + }); + } + // ------------------------------------------------------------------------- // Control: document the optional().nullable() zod-to-json-schema quirk // ------------------------------------------------------------------------- -- 2.52.0 From 71e4b5a640dd610a8c9524a961a4959923060d49 Mon Sep 17 00:00:00 2001 From: Molecule AI SDK-Dev Date: Wed, 13 May 2026 07:22:59 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs(mcp):=20fix=20KI-006=20resolution=20?= =?UTF-8?q?=E2=80=94=20fix=20direction=20was=20reversed=20in=20known-issue?= =?UTF-8?q?s.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fix was: optional().nullable() → nullable().optional() (not the reverse). Clarify that plugins.ts was already safe (PR #5), workspaces.ts fix is PR #10. Co-Authored-By: Claude Opus 4.7 --- known-issues.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/known-issues.md b/known-issues.md index 5958fbe..e6c4a87 100644 --- a/known-issues.md +++ b/known-issues.md @@ -158,22 +158,24 @@ convention in CLAUDE.md. ## KI-006 — `anyOf` schemas cause `INVALID_ARGUMENTS` on valid inputs **File:** `src/tools/plugins.ts`, `src/tools/workspaces.ts` -**Status:** Resolved (PR: `fix/kind-ki006-anyof` #5) +**Status:** Resolved +**Resolved in:** PR `fix/kind-ki006-anyof` (#5, plugins.ts) and PR #10 (workspaces.ts) **Severity:** Medium ### Resolution -The root cause was `z.string().optional().nullable()` (zod chain order) in the -`update_workspace` tool's `parent_id` schema. `zod-to-json-schema` with -`strictUnions: true` produces `anyOf` for the `optional().nullable()` chain, but -`nullable().optional()` produces a clean `type: ["string","null"]` with no `anyOf`. +The root cause is `z.string().optional().nullable()` (zod chain order). +`zod-to-json-schema` with `strictUnions: true` produces `anyOf` for the +`optional().nullable()` chain, but `nullable().optional()` produces a clean +`type: ["string","null"]` with no `anyOf`. -Fix: changed `z.string().nullable().optional()` → `z.string().optional().nullable()` -in `src/tools/workspaces.ts:122`. Semantically equivalent (string | null | undefined), -no runtime behaviour change. +- **plugins.ts:** already used safe `nullable().optional()` order (PR #5). +- **workspaces.ts:** `parent_id: z.string().optional().nullable()` → `z.string().nullable().optional()`. + Semantically equivalent (string | null | undefined); no runtime behaviour change. -Regression guard added in `tests/__tests__/plugins-schema.test.ts`: mirrors all 6 -plugin tool schemas and asserts no `anyOf` in JSON Schema output. Includes a control -test documenting the known `optional().nullable()` zod-to-json-schema quirk. +Regression guard in `tests/__tests__/plugins-schema.test.ts`: mirrors all 6 +plugin tool schemas and the `update_workspace` workspace schema; asserts no +`anyOf` in JSON Schema output. Includes a control test documenting the +`optional().nullable()` → `anyOf` quirk. --- -- 2.52.0