Skip to main content
Glama
findmine

FindMine Shopping Stylist

Official
by findmine
tools.ts14.6 kB
/** * Tool call dispatcher for the FindMine MCP server. * Routes tool calls to their appropriate handlers with Zod validation. */ import { ZodError } from 'zod'; import { logger } from '../utils/logger.js'; import { config } from '../config.js'; import { getToolDefinitions } from '../tools/index.js'; import { safeValidateToolInput, GetStyleGuideInput, GetCompleteTheLookInput, GetVisuallySimilarInput, TrackInteractionInput, UpdateItemDetailsInput, } from '../schemas/tool-inputs.js'; import { findMineService } from '../services/findmine-service.js'; import { createProductUri, createLookUri } from '../types/mcp.js'; import { styleGuides, getOccasionAdvice, getSeasonAdvice } from '../content/style-guides.js'; /** * Helper to format Zod validation errors for MCP error responses */ function formatValidationError(error: ZodError): string { const issues = error.issues.map((issue) => { const path = issue.path.length > 0 ? `${issue.path.join('.')}: ` : ''; return `${path}${issue.message}`; }); return `Validation failed: ${issues.join('; ')}`; } /** Text content in tool responses */ interface TextContent { type: 'text'; text: string; } /** Tool response with content array */ interface ToolResult { isError?: boolean; success?: boolean; content: TextContent[]; } /** * Creates an MCP-compliant error response for tool calls * Per MCP spec, isError: true enables model self-correction */ function createErrorResponse(message: string): ToolResult { return { isError: true, content: [{ type: 'text' as const, text: message }], }; } /** * Handler for listing available tools * @returns List of available tools or error */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function handleListTools() { try { logger.error('Listing available tools'); const tools = getToolDefinitions(); logger.error(`Returning ${String(tools.length)} available tools`); return { success: true, tools, }; } catch (error) { logger.error( `Error listing tools: ${error instanceof Error ? error.message : String(error)}`, error ); return { error: { message: error instanceof Error ? error.message : 'Unknown error occurred while listing tools', code: 'TOOL_LIST_ERROR', }, }; } } /** * Handles the get_style_guide tool request */ function handleGetStyleGuide(args: GetStyleGuideInput): ToolResult { const category = args.category; const occasion = args.occasion; const fashion_season = args.fashion_season; logger.error( `Getting style guide for category: ${category}${occasion ? `, occasion: ${occasion}` : ''}${fashion_season ? `, season: ${fashion_season}` : ''}` ); // Get the appropriate style guide let styleGuideContent = styleGuides[category] || styleGuides.general; // Add occasion-specific advice if requested if (occasion) { styleGuideContent += getOccasionAdvice(occasion); } // Add seasonal advice if requested if (fashion_season) { styleGuideContent += getSeasonAdvice(fashion_season); } return { success: true, content: [ { type: 'text', text: styleGuideContent, }, ], }; } /** * Handles the get_complete_the_look tool request */ async function handleGetCompleteTheLook(args: GetCompleteTheLookInput): Promise<ToolResult> { const productId = args.product_id; const inStock = args.in_stock; const onSale = args.on_sale; const returnPdpItem = args.return_pdp_item; const colorId = args.product_color_id; const sessionId = args.session_id; const customerId = args.customer_id; const gender = args.customer_gender; const apiVersion = args.api_version; logger.error( `Getting complete the look for product ${productId}${colorId ? ` (color: ${colorId})` : ''}` ); // Get complete the look recommendations const result = await findMineService.getCompleteTheLook(productId, inStock, onSale, { colorId, sessionId, customerId, returnPdpItem, gender, apiVersion, }); // Process looks data with null safety const lookItems = result.looks .map((look) => { if (!look.id) { return null; } const lookUri = createLookUri(look.id); return { look_id: look.id, title: look.title || '', description: look.description || '', image_url: look.imageUrl || '', products: Array.isArray(look.productIds) ? look.productIds .map((pid) => { if (!pid) return null; const product = findMineService.getProduct(pid); const productUri = product ? createProductUri(product.id, product.colorId) : null; return { product_id: pid, name: product?.name || '', uri: productUri, }; }) .filter(Boolean) : [], uri: lookUri, }; }) .filter(Boolean); // Include the product if requested let productInfo = null; if (result.product) { const productUri = createProductUri(result.product.id, result.product.colorId); productInfo = { product_id: result.product.id, name: result.product.name, uri: productUri, }; } return { success: true, content: [ { type: 'text', text: JSON.stringify( { product: productInfo, looks: lookItems, total_looks: lookItems.length, }, null, 2 ), }, ], }; } /** * Handles the get_visually_similar tool request */ async function handleGetVisuallySimilar(args: GetVisuallySimilarInput): Promise<ToolResult> { const productId = args.product_id; const limit = args.limit; const offset = args.offset; const colorId = args.product_color_id; const sessionId = args.session_id; const customerId = args.customer_id; const gender = args.customer_gender; const apiVersion = args.api_version; logger.error( `Getting visually similar products for ${productId}, limit: ${String(limit)}, offset: ${String(offset)}` ); // Get visually similar products const result = await findMineService.getVisuallySimilar(productId, { colorId, sessionId, customerId, limit, offset, gender, apiVersion, }); // Process products data const productItems = result.products.map((product) => { const productUri = createProductUri(product.id, product.colorId); return { product_id: product.id, name: product.name, uri: productUri, }; }); return { success: true, content: [ { type: 'text', text: JSON.stringify( { products: productItems, total: result.total, limit, offset, source_product_id: productId, }, null, 2 ), }, ], }; } /** * Handles the track_interaction tool request */ async function handleTrackInteraction(args: TrackInteractionInput): Promise<ToolResult> { const forceEnable = args.force_enable; const eventType = args.event_type; const productId = args.product_id; // Check if tracking is enabled or forced if (!config.features.enableTracking && !forceEnable) { logger.error('Tracking is disabled and force_enable is not set'); return createErrorResponse( 'Tracking is disabled. Set FINDMINE_ENABLE_TRACKING=true or use force_enable=true to enable it.' ); } const colorId = args.product_color_id; const lookId = args.look_id; const sourceProductId = args.source_product_id; const price = args.price; const quantity = args.quantity; const sessionId = args.session_id; const customerId = args.customer_id; const apiVersion = args.api_version; logger.error(`Tracking ${eventType} event for product ${productId}`); // Track the event const result = await findMineService.trackEvent(eventType, productId, { colorId, lookId, sourceProductId, price, quantity, sessionId, customerId, apiVersion, }); return { success: true, content: [ { type: 'text', text: JSON.stringify( { success: result.success, event_id: result.event_id, event_type: eventType, product_id: productId, timestamp: new Date().toISOString(), }, null, 2 ), }, ], }; } /** * Handles the update_item_details tool request */ async function handleUpdateItemDetails(args: UpdateItemDetailsInput): Promise<ToolResult> { const forceEnable = args.force_enable; const items = args.items; // Check if item details updates are enabled or forced if (!config.features.enableItemDetailsUpdates && !forceEnable) { logger.error('Item details updates are disabled and force_enable is not set'); return createErrorResponse( 'Item details updates are disabled. Set FINDMINE_ENABLE_ITEM_UPDATES=true or use force_enable=true to enable it.' ); } const sessionId = args.session_id; const customerId = args.customer_id; const apiVersion = args.api_version; logger.error(`Updating details for ${String(items.length)} items`); // Map the items to the format expected by the service const mappedItems = items.map((item) => ({ productId: item.product_id, colorId: item.product_color_id, inStock: item.in_stock, onSale: item.on_sale, })); // Update item details const result = await findMineService.updateItemDetails(mappedItems, { sessionId, customerId, apiVersion, }); return { success: true, content: [ { type: 'text', text: JSON.stringify( { success: result.success, updated_count: result.updated_count, failed_count: result.failed_count, failures: result.failures, timestamp: new Date().toISOString(), }, null, 2 ), }, ], }; } /** * Handler for tool calls - dispatches to appropriate handler with Zod validation * @param name - Tool name * @param args - Tool arguments (raw, will be validated) * @returns Tool result */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export async function handleCallTool(name: string, args: Record<string, unknown>) { switch (name) { case 'get_style_guide': { try { const validation = safeValidateToolInput('get_style_guide', args); if (!validation.success) { logger.error(`Validation failed for get_style_guide: ${formatValidationError(validation.error)}`); return createErrorResponse(formatValidationError(validation.error)); } return handleGetStyleGuide(validation.data); } catch (error) { logger.error( `Error getting style guide: ${error instanceof Error ? error.message : String(error)}`, error ); return createErrorResponse( error instanceof Error ? error.message : 'Unknown error occurred while getting style guide' ); } } case 'get_complete_the_look': { try { const validation = safeValidateToolInput('get_complete_the_look', args); if (!validation.success) { logger.error(`Validation failed for get_complete_the_look: ${formatValidationError(validation.error)}`); return createErrorResponse(formatValidationError(validation.error)); } return await handleGetCompleteTheLook(validation.data); } catch (error) { logger.error( `Error getting complete the look: ${error instanceof Error ? error.message : String(error)}`, error ); return createErrorResponse( error instanceof Error ? error.message : 'Unknown error occurred while getting complete the look' ); } } case 'get_visually_similar': { try { const validation = safeValidateToolInput('get_visually_similar', args); if (!validation.success) { logger.error(`Validation failed for get_visually_similar: ${formatValidationError(validation.error)}`); return createErrorResponse(formatValidationError(validation.error)); } return await handleGetVisuallySimilar(validation.data); } catch (error) { logger.error( `Error getting visually similar products: ${error instanceof Error ? error.message : String(error)}`, error ); return createErrorResponse( error instanceof Error ? error.message : 'Unknown error occurred while getting visually similar products' ); } } case 'track_interaction': { try { const validation = safeValidateToolInput('track_interaction', args); if (!validation.success) { logger.error(`Validation failed for track_interaction: ${formatValidationError(validation.error)}`); return createErrorResponse(formatValidationError(validation.error)); } return await handleTrackInteraction(validation.data); } catch (error) { logger.error( `Error tracking interaction: ${error instanceof Error ? error.message : String(error)}`, error ); return createErrorResponse( error instanceof Error ? error.message : 'Unknown error occurred while tracking interaction' ); } } case 'update_item_details': { try { const validation = safeValidateToolInput('update_item_details', args); if (!validation.success) { logger.error(`Validation failed for update_item_details: ${formatValidationError(validation.error)}`); return createErrorResponse(formatValidationError(validation.error)); } return await handleUpdateItemDetails(validation.data); } catch (error) { logger.error( `Error updating item details: ${error instanceof Error ? error.message : String(error)}`, error ); return createErrorResponse( error instanceof Error ? error.message : 'Unknown error occurred while updating item details' ); } } default: logger.error(`Unknown tool requested: ${name}`); return createErrorResponse(`Unknown tool: ${name}`); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/findmine/findmine-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server