fix: convert MCP tool schemas from JSON Schema to Zod for SDK v1.29.0
All checks were successful
CI / ci (push) Successful in 28s
CI / ci (pull_request) Successful in 25s
CI / e2e (push) Successful in 1m2s
CI / e2e (pull_request) Successful in 1m3s

The MCP SDK v1.29.0 changed server.tool() to require Zod schemas
(raw shapes) instead of plain JSON Schema objects. The old format
triggered "expected a Zod schema or ToolAnnotations" errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-03 20:54:20 +02:00
parent 0a40d7627f
commit 68f6647f76
5 changed files with 89 additions and 169 deletions

View File

@@ -1,3 +1,4 @@
import { z } from "zod";
import type { db as prodDb } from "../../../db/index.ts"; import type { db as prodDb } from "../../../db/index.ts";
import { import {
createCategory, createCategory,
@@ -24,24 +25,14 @@ export const categoryToolDefinitions = [
{ {
name: "list_categories", name: "list_categories",
description: "List all gear categories.", description: "List all gear categories.",
inputSchema: { inputSchema: {},
type: "object" as const,
properties: {},
},
}, },
{ {
name: "create_category", name: "create_category",
description: "Create a new gear category.", description: "Create a new gear category.",
inputSchema: { inputSchema: {
type: "object" as const, name: z.string().describe("Category name"),
properties: { icon: z.string().optional().describe("Icon name (defaults to 'package')"),
name: { type: "string", description: "Category name" },
icon: {
type: "string",
description: "Icon name (defaults to 'package')",
},
},
required: ["name"],
}, },
}, },
]; ];

View File

@@ -1,3 +1,4 @@
import { z } from "zod";
import { fetchImageFromUrl } from "../../services/image.service.ts"; import { fetchImageFromUrl } from "../../services/image.service.ts";
interface ToolResult { interface ToolResult {
@@ -20,14 +21,9 @@ export const imageToolDefinitions = [
description: description:
"Fetch an image from a URL and save it locally. Returns the filename to use with create_item or add_candidate.", "Fetch an image from a URL and save it locally. Returns the filename to use with create_item or add_candidate.",
inputSchema: { inputSchema: {
type: "object" as const, url: z
properties: { .string()
url: { .describe("URL of the image to fetch (jpeg, png, or webp)"),
type: "string",
description: "URL of the image to fetch (jpeg, png, or webp)",
},
},
required: ["url"],
}, },
}, },
]; ];

View File

@@ -1,3 +1,4 @@
import { z } from "zod";
import type { db as prodDb } from "../../../db/index.ts"; import type { db as prodDb } from "../../../db/index.ts";
import { import {
createItem, createItem,
@@ -29,24 +30,14 @@ export const itemToolDefinitions = [
description: description:
"List all items in the gear collection, optionally filtered by category.", "List all items in the gear collection, optionally filtered by category.",
inputSchema: { inputSchema: {
type: "object" as const, categoryId: z.number().optional().describe("Filter items by category ID"),
properties: {
categoryId: {
type: "number",
description: "Filter items by category ID",
},
},
}, },
}, },
{ {
name: "get_item", name: "get_item",
description: "Get a single item by its ID, including all details.", description: "Get a single item by its ID, including all details.",
inputSchema: { inputSchema: {
type: "object" as const, id: z.number().describe("The item ID"),
properties: {
id: { type: "number", description: "The item ID" },
},
required: ["id"],
}, },
}, },
{ {
@@ -54,60 +45,48 @@ export const itemToolDefinitions = [
description: description:
"Add a new item to the gear collection. Use this for items you've already decided on. For items you're still researching, use create_thread instead.", "Add a new item to the gear collection. Use this for items you've already decided on. For items you're still researching, use create_thread instead.",
inputSchema: { inputSchema: {
type: "object" as const, name: z.string().describe("Item name"),
properties: { categoryId: z.number().describe("Category ID"),
name: { type: "string", description: "Item name" }, weightGrams: z.number().optional().describe("Weight in grams"),
categoryId: { type: "number", description: "Category ID" }, priceCents: z.number().optional().describe("Price in cents"),
weightGrams: { type: "number", description: "Weight in grams" }, notes: z.string().optional().describe("Notes about the item"),
priceCents: { type: "number", description: "Price in cents" }, productUrl: z.string().optional().describe("URL to the product page"),
notes: { type: "string", description: "Notes about the item" }, imageFilename: z
productUrl: { type: "string", description: "URL to the product page" }, .string()
imageFilename: { .optional()
type: "string", .describe("Filename of an uploaded image"),
description: "Filename of an uploaded image", imageSourceUrl: z
}, .string()
imageSourceUrl: { .optional()
type: "string", .describe("Original URL the image was fetched from"),
description: "Original URL the image was fetched from",
},
},
required: ["name", "categoryId"],
}, },
}, },
{ {
name: "update_item", name: "update_item",
description: "Update an existing item's fields.", description: "Update an existing item's fields.",
inputSchema: { inputSchema: {
type: "object" as const, id: z.number().describe("The item ID to update"),
properties: { name: z.string().optional().describe("Item name"),
id: { type: "number", description: "The item ID to update" }, categoryId: z.number().optional().describe("Category ID"),
name: { type: "string", description: "Item name" }, weightGrams: z.number().optional().describe("Weight in grams"),
categoryId: { type: "number", description: "Category ID" }, priceCents: z.number().optional().describe("Price in cents"),
weightGrams: { type: "number", description: "Weight in grams" }, notes: z.string().optional().describe("Notes about the item"),
priceCents: { type: "number", description: "Price in cents" }, productUrl: z.string().optional().describe("URL to the product page"),
notes: { type: "string", description: "Notes about the item" }, imageFilename: z
productUrl: { type: "string", description: "URL to the product page" }, .string()
imageFilename: { .optional()
type: "string", .describe("Filename of an uploaded image"),
description: "Filename of an uploaded image", imageSourceUrl: z
}, .string()
imageSourceUrl: { .optional()
type: "string", .describe("Original URL the image was fetched from"),
description: "Original URL the image was fetched from",
},
},
required: ["id"],
}, },
}, },
{ {
name: "delete_item", name: "delete_item",
description: "Delete an item from the gear collection by ID.", description: "Delete an item from the gear collection by ID.",
inputSchema: { inputSchema: {
type: "object" as const, id: z.number().describe("The item ID to delete"),
properties: {
id: { type: "number", description: "The item ID to delete" },
},
required: ["id"],
}, },
}, },
]; ];

View File

@@ -1,3 +1,4 @@
import { z } from "zod";
import type { db as prodDb } from "../../../db/index.ts"; import type { db as prodDb } from "../../../db/index.ts";
import { import {
createSetup, createSetup,
@@ -28,31 +29,20 @@ export const setupToolDefinitions = [
name: "list_setups", name: "list_setups",
description: description:
"List all gear setups with item counts and weight/cost totals.", "List all gear setups with item counts and weight/cost totals.",
inputSchema: { inputSchema: {},
type: "object" as const,
properties: {},
},
}, },
{ {
name: "get_setup", name: "get_setup",
description: "Get a setup with all its items and details.", description: "Get a setup with all its items and details.",
inputSchema: { inputSchema: {
type: "object" as const, id: z.number().describe("Setup ID"),
properties: {
id: { type: "number", description: "Setup ID" },
},
required: ["id"],
}, },
}, },
{ {
name: "create_setup", name: "create_setup",
description: "Create a new gear setup (e.g. 'Bikepacking weekend').", description: "Create a new gear setup (e.g. 'Bikepacking weekend').",
inputSchema: { inputSchema: {
type: "object" as const, name: z.string().describe("Setup name"),
properties: {
name: { type: "string", description: "Setup name" },
},
required: ["name"],
}, },
}, },
{ {
@@ -60,17 +50,12 @@ export const setupToolDefinitions = [
description: description:
"Update a setup's name and/or replace its item list. Pass itemIds to set exactly which items belong to this setup.", "Update a setup's name and/or replace its item list. Pass itemIds to set exactly which items belong to this setup.",
inputSchema: { inputSchema: {
type: "object" as const, id: z.number().describe("Setup ID"),
properties: { name: z.string().optional().describe("New setup name"),
id: { type: "number", description: "Setup ID" }, itemIds: z
name: { type: "string", description: "New setup name" }, .array(z.number())
itemIds: { .optional()
type: "array", .describe("Array of item IDs to include in the setup"),
items: { type: "number" },
description: "Array of item IDs to include in the setup",
},
},
required: ["id"],
}, },
}, },
]; ];

View File

@@ -1,3 +1,4 @@
import { z } from "zod";
import type { db as prodDb } from "../../../db/index.ts"; import type { db as prodDb } from "../../../db/index.ts";
import { import {
createCandidate, createCandidate,
@@ -31,14 +32,12 @@ export const threadToolDefinitions = [
description: description:
"List research threads. Threads are the recommended way to evaluate gear purchases — each thread tracks multiple candidates for a single gear slot, making it easy to compare options before committing.", "List research threads. Threads are the recommended way to evaluate gear purchases — each thread tracks multiple candidates for a single gear slot, making it easy to compare options before committing.",
inputSchema: { inputSchema: {
type: "object" as const, includeResolved: z
properties: { .boolean()
includeResolved: { .optional()
type: "boolean", .describe(
description:
"Include resolved threads (default: false, only active threads)", "Include resolved threads (default: false, only active threads)",
}, ),
},
}, },
}, },
{ {
@@ -46,11 +45,7 @@ export const threadToolDefinitions = [
description: description:
"Get a thread with all its candidates for detailed comparison.", "Get a thread with all its candidates for detailed comparison.",
inputSchema: { inputSchema: {
type: "object" as const, id: z.number().describe("Thread ID"),
properties: {
id: { type: "number", description: "Thread ID" },
},
required: ["id"],
}, },
}, },
{ {
@@ -58,15 +53,8 @@ export const threadToolDefinitions = [
description: description:
"Start a new research thread for a gear slot. This is the preferred workflow: create a thread, add candidates with pros/cons/prices, compare them, then resolve the thread to add the winner to your collection.", "Start a new research thread for a gear slot. This is the preferred workflow: create a thread, add candidates with pros/cons/prices, compare them, then resolve the thread to add the winner to your collection.",
inputSchema: { inputSchema: {
type: "object" as const, name: z.string().describe("Thread name (e.g. 'Handlebar bag')"),
properties: { categoryId: z.number().describe("Category ID"),
name: {
type: "string",
description: "Thread name (e.g. 'Handlebar bag')",
},
categoryId: { type: "number", description: "Category ID" },
},
required: ["name", "categoryId"],
}, },
}, },
{ {
@@ -74,35 +62,24 @@ export const threadToolDefinitions = [
description: description:
"Resolve a research thread by picking the winning candidate. The winner is automatically added to the gear collection as a new item, and the thread is marked as resolved.", "Resolve a research thread by picking the winning candidate. The winner is automatically added to the gear collection as a new item, and the thread is marked as resolved.",
inputSchema: { inputSchema: {
type: "object" as const, threadId: z.number().describe("Thread ID"),
properties: { candidateId: z.number().describe("ID of the winning candidate"),
threadId: { type: "number", description: "Thread ID" },
candidateId: {
type: "number",
description: "ID of the winning candidate",
},
},
required: ["threadId", "candidateId"],
}, },
}, },
{ {
name: "add_candidate", name: "add_candidate",
description: "Add a candidate option to a research thread for comparison.", description: "Add a candidate option to a research thread for comparison.",
inputSchema: { inputSchema: {
type: "object" as const, threadId: z.number().describe("Thread ID"),
properties: { name: z.string().describe("Candidate name"),
threadId: { type: "number", description: "Thread ID" }, categoryId: z.number().describe("Category ID"),
name: { type: "string", description: "Candidate name" }, weightGrams: z.number().optional().describe("Weight in grams"),
categoryId: { type: "number", description: "Category ID" }, priceCents: z.number().optional().describe("Price in cents"),
weightGrams: { type: "number", description: "Weight in grams" }, notes: z.string().optional().describe("Notes"),
priceCents: { type: "number", description: "Price in cents" }, productUrl: z.string().optional().describe("Product URL"),
notes: { type: "string", description: "Notes" }, imageFilename: z.string().optional().describe("Image filename"),
productUrl: { type: "string", description: "Product URL" }, pros: z.string().optional().describe("Pros of this candidate"),
imageFilename: { type: "string", description: "Image filename" }, cons: z.string().optional().describe("Cons of this candidate"),
pros: { type: "string", description: "Pros of this candidate" },
cons: { type: "string", description: "Cons of this candidate" },
},
required: ["threadId", "name", "categoryId"],
}, },
}, },
{ {
@@ -110,36 +87,28 @@ export const threadToolDefinitions = [
description: description:
"Update a candidate's details (name, price, pros, cons, etc.).", "Update a candidate's details (name, price, pros, cons, etc.).",
inputSchema: { inputSchema: {
type: "object" as const, id: z.number().describe("Candidate ID"),
properties: { name: z.string().optional().describe("Candidate name"),
id: { type: "number", description: "Candidate ID" }, weightGrams: z.number().optional().describe("Weight in grams"),
name: { type: "string", description: "Candidate name" }, priceCents: z.number().optional().describe("Price in cents"),
weightGrams: { type: "number", description: "Weight in grams" }, categoryId: z.number().optional().describe("Category ID"),
priceCents: { type: "number", description: "Price in cents" }, notes: z.string().optional().describe("Notes"),
categoryId: { type: "number", description: "Category ID" }, productUrl: z.string().optional().describe("Product URL"),
notes: { type: "string", description: "Notes" }, imageFilename: z.string().optional().describe("Image filename"),
productUrl: { type: "string", description: "Product URL" }, imageSourceUrl: z.string().optional().describe("Image source URL"),
imageFilename: { type: "string", description: "Image filename" }, status: z
imageSourceUrl: { type: "string", description: "Image source URL" }, .string()
status: { .optional()
type: "string", .describe("Status: researching, ordered, or arrived"),
description: "Status: researching, ordered, or arrived", pros: z.string().optional().describe("Pros"),
}, cons: z.string().optional().describe("Cons"),
pros: { type: "string", description: "Pros" },
cons: { type: "string", description: "Cons" },
},
required: ["id"],
}, },
}, },
{ {
name: "remove_candidate", name: "remove_candidate",
description: "Remove a candidate from a research thread.", description: "Remove a candidate from a research thread.",
inputSchema: { inputSchema: {
type: "object" as const, id: z.number().describe("Candidate ID to remove"),
properties: {
id: { type: "number", description: "Candidate ID to remove" },
},
required: ["id"],
}, },
}, },
]; ];