Files
GearBox/src/server/mcp/tools/items.ts
Jean-Luc Makiola f5d79072f2 feat(17-02): wire storage service into all routes and MCP tools, remove static /uploads/*
- Replace unlink() with deleteImage() in items and threads routes
- Add withImageUrl/withImageUrls to item, thread, setup GET responses
- Enrich MCP tool responses with presigned image URLs
- Remove /uploads/* static file serving from server index
- Update MCP image tool description (local -> storage)
2026-04-05 12:22:41 +02:00

169 lines
4.7 KiB
TypeScript

import { z } from "zod";
import type { db as prodDb } from "../../../db/index.ts";
import {
createItem,
deleteItem,
getAllItems,
getItemById,
updateItem,
} from "../../services/item.service.ts";
import { withImageUrl, withImageUrls } from "../../services/storage.service.ts";
type Db = typeof prodDb;
interface ToolResult {
content: Array<{ type: "text"; text: string }>;
}
function textResult(data: unknown): ToolResult {
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
}
function errorResult(message: string): ToolResult {
return {
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
};
}
export const itemToolDefinitions = [
{
name: "list_items",
description:
"List all items in the gear collection, optionally filtered by category.",
inputSchema: {
categoryId: z.number().optional().describe("Filter items by category ID"),
},
},
{
name: "get_item",
description: "Get a single item by its ID, including all details.",
inputSchema: {
id: z.number().describe("The item ID"),
},
},
{
name: "create_item",
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.",
inputSchema: {
name: z.string().describe("Item name"),
categoryId: z.number().describe("Category ID"),
weightGrams: z.number().optional().describe("Weight in grams"),
priceCents: z.number().optional().describe("Price in cents"),
notes: z.string().optional().describe("Notes about the item"),
productUrl: z.string().optional().describe("URL to the product page"),
imageFilename: z
.string()
.optional()
.describe("Filename of an uploaded image"),
imageSourceUrl: z
.string()
.optional()
.describe("Original URL the image was fetched from"),
},
},
{
name: "update_item",
description: "Update an existing item's fields.",
inputSchema: {
id: z.number().describe("The item ID to update"),
name: z.string().optional().describe("Item name"),
categoryId: z.number().optional().describe("Category ID"),
weightGrams: z.number().optional().describe("Weight in grams"),
priceCents: z.number().optional().describe("Price in cents"),
notes: z.string().optional().describe("Notes about the item"),
productUrl: z.string().optional().describe("URL to the product page"),
imageFilename: z
.string()
.optional()
.describe("Filename of an uploaded image"),
imageSourceUrl: z
.string()
.optional()
.describe("Original URL the image was fetched from"),
},
},
{
name: "delete_item",
description: "Delete an item from the gear collection by ID.",
inputSchema: {
id: z.number().describe("The item ID to delete"),
},
},
];
export function registerItemTools(db: Db, userId: number) {
return {
list_items: async (args: { categoryId?: number }): Promise<ToolResult> => {
try {
let items = await getAllItems(db, userId);
if (args.categoryId) {
items = items.filter((i) => i.categoryId === args.categoryId);
}
return textResult(await withImageUrls(items));
} catch (err) {
return errorResult((err as Error).message);
}
},
get_item: async (args: { id: number }): Promise<ToolResult> => {
try {
const item = await getItemById(db, userId, args.id);
if (!item) return errorResult(`Item ${args.id} not found`);
return textResult(await withImageUrl(item));
} catch (err) {
return errorResult((err as Error).message);
}
},
create_item: async (args: {
name: string;
categoryId: number;
weightGrams?: number;
priceCents?: number;
notes?: string;
productUrl?: string;
imageFilename?: string;
imageSourceUrl?: string;
}): Promise<ToolResult> => {
try {
const item = await createItem(db, userId, args);
return textResult(item);
} catch (err) {
return errorResult((err as Error).message);
}
},
update_item: async (args: {
id: number;
name?: string;
categoryId?: number;
weightGrams?: number;
priceCents?: number;
notes?: string;
productUrl?: string;
imageFilename?: string;
imageSourceUrl?: string;
}): Promise<ToolResult> => {
try {
const { id, ...data } = args;
const item = await updateItem(db, userId, id, data);
if (!item) return errorResult(`Item ${id} not found`);
return textResult(item);
} catch (err) {
return errorResult((err as Error).message);
}
},
delete_item: async (args: { id: number }): Promise<ToolResult> => {
try {
const item = await deleteItem(db, userId, args.id);
if (!item) return errorResult(`Item ${args.id} not found`);
return textResult({ deleted: true, item });
} catch (err) {
return errorResult((err as Error).message);
}
},
};
}