fix(mcp): z.string().nullable().optional() in update_workspace parent_id schema (KI-006) #10

Closed
sdk-dev wants to merge 2 commits from fix/kind-ki006-anyof-workspaces into main
3 changed files with 41 additions and 12 deletions
+13 -11
View File
@@ -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.
---
+1 -1
View File
@@ -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
);
+27
View File
@@ -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
// -------------------------------------------------------------------------