feat: add MCP tool handlers, definitions, and collection resource
Wrap existing service layer with MCP-compatible tool handlers for items, categories, threads/candidates, setups, and image fetching. Add collection summary resource for overview data. All 14 MCP-specific tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
41
src/server/mcp/resources/collection.ts
Normal file
41
src/server/mcp/resources/collection.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { db as prodDb } from "@/db/index.ts";
|
||||
import { getAllCategories } from "../../services/category.service.ts";
|
||||
import { getAllItems } from "../../services/item.service.ts";
|
||||
import { getAllSetups } from "../../services/setup.service.ts";
|
||||
import { getAllThreads } from "../../services/thread.service.ts";
|
||||
import { getGlobalTotals } from "../../services/totals.service.ts";
|
||||
|
||||
type Db = typeof prodDb;
|
||||
|
||||
export function getCollectionSummary(db: Db) {
|
||||
const totals = getGlobalTotals(db);
|
||||
const categories = getAllCategories(db);
|
||||
const items = getAllItems(db);
|
||||
const setups = getAllSetups(db);
|
||||
const activeThreads = getAllThreads(db, false);
|
||||
|
||||
// Build items-by-category map
|
||||
const itemsByCategory: Record<string, number> = {};
|
||||
for (const item of items) {
|
||||
const catName = item.categoryName;
|
||||
itemsByCategory[catName] = (itemsByCategory[catName] ?? 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
overview: {
|
||||
totalItems: totals?.itemCount ?? 0,
|
||||
totalWeightGrams: totals?.totalWeight ?? 0,
|
||||
totalCostCents: totals?.totalCost ?? 0,
|
||||
categoryCount: categories.length,
|
||||
setupCount: setups.length,
|
||||
activeThreadCount: activeThreads.length,
|
||||
},
|
||||
itemsByCategory,
|
||||
activeThreads: activeThreads.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
candidateCount: t.candidateCount,
|
||||
category: t.categoryName,
|
||||
})),
|
||||
};
|
||||
}
|
||||
72
src/server/mcp/tools/categories.ts
Normal file
72
src/server/mcp/tools/categories.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { db as prodDb } from "@/db/index.ts";
|
||||
import {
|
||||
createCategory,
|
||||
getAllCategories,
|
||||
} from "../../services/category.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 categoryToolDefinitions = [
|
||||
{
|
||||
name: "list_categories",
|
||||
description: "List all gear categories.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create_category",
|
||||
description: "Create a new gear category.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
name: { type: "string", description: "Category name" },
|
||||
icon: {
|
||||
type: "string",
|
||||
description: "Icon name (defaults to 'package')",
|
||||
},
|
||||
},
|
||||
required: ["name"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function registerCategoryTools(db: Db) {
|
||||
return {
|
||||
list_categories: async (): Promise<ToolResult> => {
|
||||
try {
|
||||
const cats = getAllCategories(db);
|
||||
return textResult(cats);
|
||||
} catch (err) {
|
||||
return errorResult((err as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
create_category: async (args: {
|
||||
name: string;
|
||||
icon?: string;
|
||||
}): Promise<ToolResult> => {
|
||||
try {
|
||||
const cat = createCategory(db, args);
|
||||
return textResult(cat);
|
||||
} catch (err) {
|
||||
return errorResult((err as Error).message);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
48
src/server/mcp/tools/images.ts
Normal file
48
src/server/mcp/tools/images.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { fetchImageFromUrl } from "../../services/image.service.ts";
|
||||
|
||||
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 imageToolDefinitions = [
|
||||
{
|
||||
name: "upload_image_from_url",
|
||||
description:
|
||||
"Fetch an image from a URL and save it locally. Returns the filename to use with create_item or add_candidate.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
url: {
|
||||
type: "string",
|
||||
description: "URL of the image to fetch (jpeg, png, or webp)",
|
||||
},
|
||||
},
|
||||
required: ["url"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function registerImageTools() {
|
||||
return {
|
||||
upload_image_from_url: async (args: {
|
||||
url: string;
|
||||
}): Promise<ToolResult> => {
|
||||
try {
|
||||
const result = await fetchImageFromUrl(args.url);
|
||||
return textResult(result);
|
||||
} catch (err) {
|
||||
return errorResult((err as Error).message);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
190
src/server/mcp/tools/items.ts
Normal file
190
src/server/mcp/tools/items.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import type { db as prodDb } from "@/db/index.ts";
|
||||
import {
|
||||
createItem,
|
||||
deleteItem,
|
||||
getAllItems,
|
||||
getItemById,
|
||||
updateItem,
|
||||
} from "../../services/item.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: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
categoryId: {
|
||||
type: "number",
|
||||
description: "Filter items by category ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_item",
|
||||
description: "Get a single item by its ID, including all details.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
id: { type: "number", description: "The item ID" },
|
||||
},
|
||||
required: ["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: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
name: { type: "string", description: "Item name" },
|
||||
categoryId: { type: "number", description: "Category ID" },
|
||||
weightGrams: { type: "number", description: "Weight in grams" },
|
||||
priceCents: { type: "number", description: "Price in cents" },
|
||||
notes: { type: "string", description: "Notes about the item" },
|
||||
productUrl: { type: "string", description: "URL to the product page" },
|
||||
imageFilename: {
|
||||
type: "string",
|
||||
description: "Filename of an uploaded image",
|
||||
},
|
||||
imageSourceUrl: {
|
||||
type: "string",
|
||||
description: "Original URL the image was fetched from",
|
||||
},
|
||||
},
|
||||
required: ["name", "categoryId"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update_item",
|
||||
description: "Update an existing item's fields.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
id: { type: "number", description: "The item ID to update" },
|
||||
name: { type: "string", description: "Item name" },
|
||||
categoryId: { type: "number", description: "Category ID" },
|
||||
weightGrams: { type: "number", description: "Weight in grams" },
|
||||
priceCents: { type: "number", description: "Price in cents" },
|
||||
notes: { type: "string", description: "Notes about the item" },
|
||||
productUrl: { type: "string", description: "URL to the product page" },
|
||||
imageFilename: {
|
||||
type: "string",
|
||||
description: "Filename of an uploaded image",
|
||||
},
|
||||
imageSourceUrl: {
|
||||
type: "string",
|
||||
description: "Original URL the image was fetched from",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete_item",
|
||||
description: "Delete an item from the gear collection by ID.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
id: { type: "number", description: "The item ID to delete" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function registerItemTools(db: Db) {
|
||||
return {
|
||||
list_items: async (args: { categoryId?: number }): Promise<ToolResult> => {
|
||||
try {
|
||||
const items = getAllItems(db);
|
||||
if (args.categoryId) {
|
||||
return textResult(
|
||||
items.filter((i) => i.categoryId === args.categoryId),
|
||||
);
|
||||
}
|
||||
return textResult(items);
|
||||
} catch (err) {
|
||||
return errorResult((err as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
get_item: async (args: { id: number }): Promise<ToolResult> => {
|
||||
try {
|
||||
const item = getItemById(db, args.id);
|
||||
if (!item) return errorResult(`Item ${args.id} not found`);
|
||||
return textResult(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 = createItem(db, 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 = updateItem(db, 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 = deleteItem(db, args.id);
|
||||
if (!item) return errorResult(`Item ${args.id} not found`);
|
||||
return textResult({ deleted: true, item });
|
||||
} catch (err) {
|
||||
return errorResult((err as Error).message);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
131
src/server/mcp/tools/setups.ts
Normal file
131
src/server/mcp/tools/setups.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import type { db as prodDb } from "@/db/index.ts";
|
||||
import {
|
||||
createSetup,
|
||||
getAllSetups,
|
||||
getSetupWithItems,
|
||||
syncSetupItems,
|
||||
updateSetup,
|
||||
} from "../../services/setup.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 setupToolDefinitions = [
|
||||
{
|
||||
name: "list_setups",
|
||||
description:
|
||||
"List all gear setups with item counts and weight/cost totals.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_setup",
|
||||
description: "Get a setup with all its items and details.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
id: { type: "number", description: "Setup ID" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create_setup",
|
||||
description: "Create a new gear setup (e.g. 'Bikepacking weekend').",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
name: { type: "string", description: "Setup name" },
|
||||
},
|
||||
required: ["name"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update_setup",
|
||||
description:
|
||||
"Update a setup's name and/or replace its item list. Pass itemIds to set exactly which items belong to this setup.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
id: { type: "number", description: "Setup ID" },
|
||||
name: { type: "string", description: "New setup name" },
|
||||
itemIds: {
|
||||
type: "array",
|
||||
items: { type: "number" },
|
||||
description: "Array of item IDs to include in the setup",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function registerSetupTools(db: Db) {
|
||||
return {
|
||||
list_setups: async (): Promise<ToolResult> => {
|
||||
try {
|
||||
const setupList = getAllSetups(db);
|
||||
return textResult(setupList);
|
||||
} catch (err) {
|
||||
return errorResult((err as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
get_setup: async (args: { id: number }): Promise<ToolResult> => {
|
||||
try {
|
||||
const setup = getSetupWithItems(db, args.id);
|
||||
if (!setup) return errorResult(`Setup ${args.id} not found`);
|
||||
return textResult(setup);
|
||||
} catch (err) {
|
||||
return errorResult((err as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
create_setup: async (args: { name: string }): Promise<ToolResult> => {
|
||||
try {
|
||||
const setup = createSetup(db, args);
|
||||
return textResult(setup);
|
||||
} catch (err) {
|
||||
return errorResult((err as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
update_setup: async (args: {
|
||||
id: number;
|
||||
name?: string;
|
||||
itemIds?: number[];
|
||||
}): Promise<ToolResult> => {
|
||||
try {
|
||||
let setup = null;
|
||||
if (args.name) {
|
||||
setup = updateSetup(db, args.id, { name: args.name });
|
||||
if (!setup) return errorResult(`Setup ${args.id} not found`);
|
||||
}
|
||||
if (args.itemIds) {
|
||||
syncSetupItems(db, args.id, args.itemIds);
|
||||
}
|
||||
// Return updated setup with items
|
||||
const result = getSetupWithItems(db, args.id);
|
||||
if (!result) return errorResult(`Setup ${args.id} not found`);
|
||||
return textResult(result);
|
||||
} catch (err) {
|
||||
return errorResult((err as Error).message);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
252
src/server/mcp/tools/threads.ts
Normal file
252
src/server/mcp/tools/threads.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import type { db as prodDb } from "@/db/index.ts";
|
||||
import {
|
||||
createCandidate,
|
||||
createThread,
|
||||
deleteCandidate,
|
||||
getAllThreads,
|
||||
getThreadWithCandidates,
|
||||
resolveThread,
|
||||
updateCandidate,
|
||||
} from "../../services/thread.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 threadToolDefinitions = [
|
||||
{
|
||||
name: "list_threads",
|
||||
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.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
includeResolved: {
|
||||
type: "boolean",
|
||||
description:
|
||||
"Include resolved threads (default: false, only active threads)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_thread",
|
||||
description:
|
||||
"Get a thread with all its candidates for detailed comparison.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
id: { type: "number", description: "Thread ID" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create_thread",
|
||||
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.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
name: {
|
||||
type: "string",
|
||||
description: "Thread name (e.g. 'Handlebar bag')",
|
||||
},
|
||||
categoryId: { type: "number", description: "Category ID" },
|
||||
},
|
||||
required: ["name", "categoryId"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "resolve_thread",
|
||||
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.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
threadId: { type: "number", description: "Thread ID" },
|
||||
candidateId: {
|
||||
type: "number",
|
||||
description: "ID of the winning candidate",
|
||||
},
|
||||
},
|
||||
required: ["threadId", "candidateId"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add_candidate",
|
||||
description: "Add a candidate option to a research thread for comparison.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
threadId: { type: "number", description: "Thread ID" },
|
||||
name: { type: "string", description: "Candidate name" },
|
||||
categoryId: { type: "number", description: "Category ID" },
|
||||
weightGrams: { type: "number", description: "Weight in grams" },
|
||||
priceCents: { type: "number", description: "Price in cents" },
|
||||
notes: { type: "string", description: "Notes" },
|
||||
productUrl: { type: "string", description: "Product URL" },
|
||||
imageFilename: { type: "string", description: "Image filename" },
|
||||
pros: { type: "string", description: "Pros of this candidate" },
|
||||
cons: { type: "string", description: "Cons of this candidate" },
|
||||
},
|
||||
required: ["threadId", "name", "categoryId"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update_candidate",
|
||||
description:
|
||||
"Update a candidate's details (name, price, pros, cons, etc.).",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
id: { type: "number", description: "Candidate ID" },
|
||||
name: { type: "string", description: "Candidate name" },
|
||||
weightGrams: { type: "number", description: "Weight in grams" },
|
||||
priceCents: { type: "number", description: "Price in cents" },
|
||||
categoryId: { type: "number", description: "Category ID" },
|
||||
notes: { type: "string", description: "Notes" },
|
||||
productUrl: { type: "string", description: "Product URL" },
|
||||
imageFilename: { type: "string", description: "Image filename" },
|
||||
imageSourceUrl: { type: "string", description: "Image source URL" },
|
||||
status: {
|
||||
type: "string",
|
||||
description: "Status: researching, ordered, or arrived",
|
||||
},
|
||||
pros: { type: "string", description: "Pros" },
|
||||
cons: { type: "string", description: "Cons" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove_candidate",
|
||||
description: "Remove a candidate from a research thread.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
id: { type: "number", description: "Candidate ID to remove" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function registerThreadTools(db: Db) {
|
||||
return {
|
||||
list_threads: async (args: {
|
||||
includeResolved?: boolean;
|
||||
}): Promise<ToolResult> => {
|
||||
try {
|
||||
const threadList = getAllThreads(db, args.includeResolved ?? false);
|
||||
return textResult(threadList);
|
||||
} catch (err) {
|
||||
return errorResult((err as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
get_thread: async (args: { id: number }): Promise<ToolResult> => {
|
||||
try {
|
||||
const thread = getThreadWithCandidates(db, args.id);
|
||||
if (!thread) return errorResult(`Thread ${args.id} not found`);
|
||||
return textResult(thread);
|
||||
} catch (err) {
|
||||
return errorResult((err as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
create_thread: async (args: {
|
||||
name: string;
|
||||
categoryId: number;
|
||||
}): Promise<ToolResult> => {
|
||||
try {
|
||||
const thread = createThread(db, args);
|
||||
return textResult(thread);
|
||||
} catch (err) {
|
||||
return errorResult((err as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
resolve_thread: async (args: {
|
||||
threadId: number;
|
||||
candidateId: number;
|
||||
}): Promise<ToolResult> => {
|
||||
try {
|
||||
const result = resolveThread(db, args.threadId, args.candidateId);
|
||||
if (!result.success) {
|
||||
return errorResult(result.error ?? "Failed to resolve thread");
|
||||
}
|
||||
return textResult(result);
|
||||
} catch (err) {
|
||||
return errorResult((err as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
add_candidate: async (args: {
|
||||
threadId: number;
|
||||
name: string;
|
||||
categoryId: number;
|
||||
weightGrams?: number;
|
||||
priceCents?: number;
|
||||
notes?: string;
|
||||
productUrl?: string;
|
||||
imageFilename?: string;
|
||||
pros?: string;
|
||||
cons?: string;
|
||||
}): Promise<ToolResult> => {
|
||||
try {
|
||||
const { threadId, ...data } = args;
|
||||
const candidate = createCandidate(db, threadId, data);
|
||||
return textResult(candidate);
|
||||
} catch (err) {
|
||||
return errorResult((err as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
update_candidate: async (args: {
|
||||
id: number;
|
||||
name?: string;
|
||||
weightGrams?: number;
|
||||
priceCents?: number;
|
||||
categoryId?: number;
|
||||
notes?: string;
|
||||
productUrl?: string;
|
||||
imageFilename?: string;
|
||||
imageSourceUrl?: string;
|
||||
status?: "researching" | "ordered" | "arrived";
|
||||
pros?: string;
|
||||
cons?: string;
|
||||
}): Promise<ToolResult> => {
|
||||
try {
|
||||
const { id, ...data } = args;
|
||||
const candidate = updateCandidate(db, id, data);
|
||||
if (!candidate) return errorResult(`Candidate ${id} not found`);
|
||||
return textResult(candidate);
|
||||
} catch (err) {
|
||||
return errorResult((err as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
remove_candidate: async (args: { id: number }): Promise<ToolResult> => {
|
||||
try {
|
||||
const candidate = deleteCandidate(db, args.id);
|
||||
if (!candidate) return errorResult(`Candidate ${args.id} not found`);
|
||||
return textResult({ deleted: true, candidate });
|
||||
} catch (err) {
|
||||
return errorResult((err as Error).message);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user