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..eb4c208eb 100644 --- a/packages/review/src/review-api/authoring-tools.ts +++ b/packages/review/src/review-api/authoring-tools.ts @@ -17,6 +17,19 @@ 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 [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 }); @@ -162,8 +175,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", ),