diagnostics_product_display_check
Identify and resolve issues causing a product to display incorrectly on the storefront. Diagnose display problems to ensure accurate product visibility.
Instructions
Check why a product may not be displaying correctly on the storefront.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| params | No | Action parameters as a JSON object |
Implementation Reference
- src/actions/diagnostics.ts:14-132 (handler)Handler for the diagnostics.product_display_check tool. Fetches product data via Magento REST API and checks status, visibility, price, website assignment, category assignment, images, stock/MSI status, and returns issues/recommended actions.
// ── Product Display Check ───────────────────────────────────────────── { name: 'diagnostics.product_display_check', description: 'Check why a product may not be displaying correctly on the storefront.', riskTier: RiskTier.Safe, requiresAuth: true, handler: async (params: Record<string, unknown>, context: ActionContext) => { const validated = DiagnosticsProductDisplaySchema.parse(params); const client = context.getClient(); const storeCode = validated.store_view_code; // Fetch product data let product: Record<string, unknown>; try { product = await client.get<Record<string, unknown>>( `/V1/products/${encodeURIComponent(validated.sku)}`, undefined, storeCode, ); } catch (err) { return { sku: validated.sku, found: false, issues: [`Product not found: ${err instanceof Error ? err.message : String(err)}`], recommended_actions: [], }; } const issues: string[] = []; const recommendedActions: string[] = []; // Check status const status = product['status']; if (status === 2 || status === '2') { issues.push('Product is DISABLED (status=2).'); recommendedActions.push('catalog.prepare_bulk_update to set status=1 (enabled)'); } // Check visibility const visibility = product['visibility']; if (visibility === 1 || visibility === '1') { issues.push('Product visibility is "Not Visible Individually" (visibility=1). It will only appear as part of a grouped/configurable product.'); recommendedActions.push('catalog.prepare_bulk_update to set visibility=4 (catalog+search)'); } // Check price const price = Number(product['price']); if (!price || price <= 0) { issues.push('Product has no price or price is 0.'); recommendedActions.push('pricing.prepare_bulk_price_update to set a valid price'); } // Check website assignment const extensionAttributes = product['extension_attributes'] as Record<string, unknown> | undefined; const websiteIds = extensionAttributes?.['website_ids'] as number[] | undefined; if (!websiteIds || websiteIds.length === 0) { issues.push('Product is not assigned to any website.'); recommendedActions.push('Assign product to appropriate website(s) via Magento Admin'); } // Check category assignment const customAttributes = product['custom_attributes'] as Array<{ attribute_code: string; value: unknown }> | undefined; const categoryIds = customAttributes?.find((a) => a.attribute_code === 'category_ids'); if (!categoryIds || !Array.isArray(categoryIds.value) || (categoryIds.value as unknown[]).length === 0) { issues.push('Product is not assigned to any category.'); recommendedActions.push('catalog.prepare_bulk_update to assign categories'); } // Check images const mediaGallery = product['media_gallery_entries'] as unknown[] | undefined; if (!mediaGallery || mediaGallery.length === 0) { issues.push('Product has no images.'); } // Check stock / MSI try { const stockItem = extensionAttributes?.['stock_item'] as Record<string, unknown> | undefined; if (stockItem) { const isInStock = stockItem['is_in_stock']; const qty = Number(stockItem['qty']); if (!isInStock) { issues.push('Product is marked as OUT OF STOCK.'); recommendedActions.push('Update stock status in Magento Admin or via inventory API'); } if (qty <= 0) { issues.push(`Product quantity is ${qty}.`); } } } catch { // Stock info may not be available } // Try MSI salable quantity try { const salableQty = await client.get<Array<{ stock_id: number; qty: number }>>( `/V1/inventory/get-product-salable-quantity/${encodeURIComponent(validated.sku)}/1`, ); if (Array.isArray(salableQty) && salableQty.length > 0) { const totalSalable = salableQty.reduce((sum, s) => sum + s.qty, 0); if (totalSalable <= 0) { issues.push(`MSI salable quantity is ${totalSalable}.`); } } } catch { // MSI may not be available } return { sku: validated.sku, found: true, product_name: product['name'], status: product['status'], visibility: product['visibility'], price: product['price'], issues: issues.length > 0 ? issues : ['No issues detected.'], recommended_actions: recommendedActions, }; }, }, - src/validation/schemas.ts:250-253 (schema)Zod input schema for diagnostics.product_display_check: requires sku (string, min 1), optional store_view_code.
export const DiagnosticsProductDisplaySchema = z.object({ sku: z.string().min(1), store_view_code: z.string().optional(), }); - src/actions/diagnostics.ts:12-78 (registration)createDiagnosticsActions() returns ActionDefinition[]; the tool is defined with name 'diagnostics.product_display_check' which is later registered in index.ts where dots are converted to underscores (becomes 'diagnostics_product_display_check').
export function createDiagnosticsActions(): ActionDefinition[] { return [ // ── Product Display Check ───────────────────────────────────────────── { name: 'diagnostics.product_display_check', description: 'Check why a product may not be displaying correctly on the storefront.', riskTier: RiskTier.Safe, requiresAuth: true, handler: async (params: Record<string, unknown>, context: ActionContext) => { const validated = DiagnosticsProductDisplaySchema.parse(params); const client = context.getClient(); const storeCode = validated.store_view_code; // Fetch product data let product: Record<string, unknown>; try { product = await client.get<Record<string, unknown>>( `/V1/products/${encodeURIComponent(validated.sku)}`, undefined, storeCode, ); } catch (err) { return { sku: validated.sku, found: false, issues: [`Product not found: ${err instanceof Error ? err.message : String(err)}`], recommended_actions: [], }; } const issues: string[] = []; const recommendedActions: string[] = []; // Check status const status = product['status']; if (status === 2 || status === '2') { issues.push('Product is DISABLED (status=2).'); recommendedActions.push('catalog.prepare_bulk_update to set status=1 (enabled)'); } // Check visibility const visibility = product['visibility']; if (visibility === 1 || visibility === '1') { issues.push('Product visibility is "Not Visible Individually" (visibility=1). It will only appear as part of a grouped/configurable product.'); recommendedActions.push('catalog.prepare_bulk_update to set visibility=4 (catalog+search)'); } // Check price const price = Number(product['price']); if (!price || price <= 0) { issues.push('Product has no price or price is 0.'); recommendedActions.push('pricing.prepare_bulk_price_update to set a valid price'); } // Check website assignment const extensionAttributes = product['extension_attributes'] as Record<string, unknown> | undefined; const websiteIds = extensionAttributes?.['website_ids'] as number[] | undefined; if (!websiteIds || websiteIds.length === 0) { issues.push('Product is not assigned to any website.'); recommendedActions.push('Assign product to appropriate website(s) via Magento Admin'); } // Check category assignment const customAttributes = product['custom_attributes'] as Array<{ attribute_code: string; value: unknown }> | undefined; const categoryIds = customAttributes?.find((a) => a.attribute_code === 'category_ids'); if (!categoryIds || !Array.isArray(categoryIds.value) || (categoryIds.value as unknown[]).length === 0) { issues.push('Product is not assigned to any category.'); - src/index.ts:76-159 (registration)In index.ts, all actions including diagnostics actions are iterated and registered as MCP tools. The name 'diagnostics.product_display_check' is converted to 'diagnostics_product_display_check' via action.name.replace(/\./g, '_').
for (const action of allActions) { // Convert dots to underscores for MCP tool names (e.g. "auth.login" -> "auth_login") const toolName = action.name.replace(/\./g, '_'); mcpServer.tool( toolName, action.description, { params: z.record(z.unknown()).optional().describe('Action parameters as a JSON object') }, async (args) => { const params = (args.params || {}) as Record<string, unknown>; // Check authentication if (action.requiresAuth) { const token = sessionStore.getToken(sessionId); if (!token) { return { content: [{ type: 'text' as const, text: JSON.stringify({ error: { code: 'NOT_AUTHENTICATED', message: 'Not authenticated. Call auth_login first.' } }, null, 2) }], isError: true, }; } } // Build action context const context: ActionContext = { sessionId, getToken: () => sessionStore.getToken(sessionId), getBaseUrl: () => sessionStore.getBaseUrl(sessionId), getDefaultScope: () => sessionStore.getDefaultScope(sessionId), getOAuthCredentials: () => sessionStore.getOAuthCredentials(sessionId), getClient: () => { const baseUrl = sessionStore.getBaseUrl(sessionId); const token = sessionStore.getToken(sessionId); if (!baseUrl) throw new Error('No active session'); const client = new MagentoRestClient(baseUrl, token); const oauth = sessionStore.getOAuthCredentials(sessionId); if (oauth) client.setOAuth(oauth); return client; }, username: sessionStore.getUsername(sessionId), }; try { const result = await action.handler(params, context); // Audit log const auditRecord: AuditRecord = { timestamp: new Date().toISOString(), username: context.username, action: action.name, scope: context.getDefaultScope(), params, result_summary: summarizeResult(result), plan_id: (params['plan_id'] as string) || null, reason: (params['reason'] as string) || null, }; auditLogger.log(auditRecord); return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], }; } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); // Audit the error const auditRecord: AuditRecord = { timestamp: new Date().toISOString(), username: context.username, action: action.name, scope: null, params, result_summary: `ERROR: ${errorMessage}`, plan_id: null, reason: null, }; auditLogger.log(auditRecord); return { content: [{ type: 'text' as const, text: JSON.stringify({ error: errorMessage }, null, 2) }], isError: true, }; } }, ); } - src/protocol/types.ts:62-73 (helper)ActionDefinition interface defining the structure of a tool action (name, description, riskTier, requiresAuth, handler).
export interface ActionDefinition { name: string; description: string; riskTier: RiskTier; requiresAuth: boolean; inputSchema?: Record<string, unknown>; handler: (params: Record<string, unknown>, context: ActionContext) => Promise<unknown>; } // ── Action Context (passed to every handler) ──────────────────────────────── export interface ActionContext {