- 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)
169 lines
4.7 KiB
TypeScript
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);
|
|
}
|
|
},
|
|
};
|
|
}
|