From 0993a91ed815483e46c1c2678f3433b3126eeb4a Mon Sep 17 00:00:00 2001 From: thesiti92 Date: Fri, 25 Sep 2026 19:11:05 -0400 Subject: [PATCH 1/2] Publish review_upload as one object so Anthropic accepts it OpenCode forwards MCP input schemas to the Anthropic API unchanged, and the API rejects oneOf/anyOf/allOf at the top level of a tool input_schema. The upload tool published its discriminated union verbatim, so enabling Whiteboard broke every request. Claude Code flattens such schemas itself, which hid the problem there. Publish upload as one object with kind as an enum and the kind-specific fields optional; the host still validates the union. Agent-Session: 0e2e21cf-19cf-4e82-b801-11a38760f54a Agent-Session: ffd15b4f-a561-403e-bf42-e841bd247f58 --- .../review/src/review-api/agent-client.test.ts | 6 ++++++ packages/review/src/review-api/authoring-tools.ts | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/review/src/review-api/agent-client.test.ts b/packages/review/src/review-api/agent-client.test.ts index 2225a634c..9a791c65b 100644 --- a/packages/review/src/review-api/agent-client.test.ts +++ b/packages/review/src/review-api/agent-client.test.ts @@ -207,6 +207,12 @@ it("serves MCP framing without stdout diagnostics and returns host errors as too }); const list = await request(2, "tools/list", {}); ListToolsResultSchema.parse(list.result); + // The Anthropic API rejects these at the top level of a tool input schema. + expect( + list.result.tools.filter((tool: AuthoringTool) => + ["anyOf", "oneOf", "allOf"].some((key) => key in tool.inputSchema), + ), + ).toEqual([]); expect( list.result.tools.find( (tool: AuthoringTool) => tool.name === "session_edit", diff --git a/packages/review/src/review-api/authoring-tools.ts b/packages/review/src/review-api/authoring-tools.ts index 50880d34f..df0468b16 100644 --- a/packages/review/src/review-api/authoring-tools.ts +++ b/packages/review/src/review-api/authoring-tools.ts @@ -17,6 +17,16 @@ export function authoringTools( const review = { reviewId: id }; const version = z.number().int().nonnegative().optional(); + // Anthropic rejects a top-level union, so publish one object; the host validates the union. + const uploadInput = z.strictObject({ + ...Object.assign( + {}, + ...uploadSchema.options.map((option) => option.partial().shape), + ), + ...uploadSchema.options[0].pick({ id: true, repositoryId: true }).shape, + kind: z.enum(uploadSchema.options.map((option) => option.shape.kind.value)), + }); + const read = (name: keyof typeof readQuerySchemas) => z.strictObject({ ...review, ...readQuerySchemas[name].shape }); @@ -162,8 +172,8 @@ export function authoringTools( ), tool( "upload", - "Retain an image, trace or software map for use in a review. Reusing an upload ID requires identical content; rejected uploads are not saved.", - uploadSchema, + 'Retain an image, trace or software map for use in a review. kind:"image" takes base64; kind:"trace" takes trace; kind:"map" takes pins, side and model. Reusing an upload ID requires identical content; rejected uploads are not saved.', + uploadInput, "POST", "/resources", ), From 0ed60edea07871b86ef7edd3bdcaade073004525 Mon Sep 17 00:00:00 2001 From: thesiti92 Date: Fri, 25 Sep 2026 20:18:42 -0400 Subject: [PATCH 2/2] Build the flat upload schema from its three variants Agent-Session: 0e2e21cf-19cf-4e82-b801-11a38760f54a Agent-Session: ffd15b4f-a561-403e-bf42-e841bd247f58 --- .../review/src/review-api/authoring-tools.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/review/src/review-api/authoring-tools.ts b/packages/review/src/review-api/authoring-tools.ts index df0468b16..eb4c208eb 100644 --- a/packages/review/src/review-api/authoring-tools.ts +++ b/packages/review/src/review-api/authoring-tools.ts @@ -18,14 +18,17 @@ export function authoringTools( const version = z.number().int().nonnegative().optional(); // Anthropic rejects a top-level union, so publish one object; the host validates the union. - const uploadInput = z.strictObject({ - ...Object.assign( - {}, - ...uploadSchema.options.map((option) => option.partial().shape), - ), - ...uploadSchema.options[0].pick({ id: true, repositoryId: true }).shape, - kind: z.enum(uploadSchema.options.map((option) => option.shape.kind.value)), - }); + const [image, trace, map] = uploadSchema.options; + + const uploadInput = z + .strictObject({ + ...image.shape, + ...trace.shape, + ...map.shape, + kind: z.enum(["image", "trace", "map"]), + }) + .partial() + .required({ id: true, repositoryId: true, kind: true }); const read = (name: keyof typeof readQuerySchemas) => z.strictObject({ ...review, ...readQuerySchemas[name].shape });