Inventory Forecast
inventory_forecastUse moving-average sales velocity to forecast stock depletion dates, providing reorder points, safety stock levels, and recommended reorder quantities to prevent stockouts.
Instructions
Predict stock depletion dates using moving-average sales velocity. Returns reorder points, safety stock levels, and suggested reorder quantities for each product.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| store_id | Yes | UUID of a connected store (returned by store_connect with action="connect" or visible in store_connect with action="list" / the store_overview resource) | |
| product_id | No | Restrict the forecast to a single product by its external_id (Shopify product ID or WooCommerce product slug). Omit to forecast every active product in the store. |
Implementation Reference
- src/index.ts:146-166 (registration)Registration of the 'inventory_forecast' tool on the MCP server using server.registerTool(), with inputSchema (store_id UUID required, product_id optional), description, and annotations.
// ── Tool: inventory_forecast ────────────────────────────────────── server.registerTool( 'inventory_forecast', { title: 'Inventory Forecast', description: 'Predict stock depletion dates using moving-average sales velocity. Returns reorder points, safety stock levels, and suggested reorder quantities for each product.', inputSchema: z.object({ store_id: z.string().uuid().describe('UUID of a connected store (returned by store_connect with action="connect" or visible in store_connect with action="list" / the store_overview resource)'), product_id: z.string().optional().describe('Restrict the forecast to a single product by its external_id (Shopify product ID or WooCommerce product slug). Omit to forecast every active product in the store.'), }), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async ({ store_id, product_id }) => { try { const reject = await ensureProOrReject(LICENSE_CONFIG, 'inventory_forecast'); if (reject) return reject; const result = await getInventoryForecast(store_id, product_id); return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }; } catch (e) { return handleToolError(e); } } ); - src/tools/inventory.ts:52-67 (handler)Handler function getInventoryForecast that validates the store, checks for existence, retrieves orders, and delegates to forecastProduct (single product) or forecastAll (all active products).
export async function getInventoryForecast(storeId: string, productId?: string): Promise<ForecastResult[]> { validateUUID(storeId, 'store'); const store = await storage.getStoreById(storeId); if (!store) throw new NotFoundError('Store', storeId); const orders = await storage.getOrders(storeId); if (productId) { const product = await storage.getProductById(productId); if (!product) throw new NotFoundError('Product', productId); return [forecastProduct(product, orders)]; } const products = await storage.getProducts(storeId); return forecastAll(products, orders); } - src/services/forecasting.ts:65-146 (helper)Helper function forecastProduct that computes moving-average sales velocity, safety stock (1.65*σ*√leadTime), reorder point, days of stock, depletion date, suggested reorder qty, and risk level for a single product.
export function forecastProduct(product: Product, orders: Order[], daysBack = 30): ForecastResult { const dailySales = computeDailySales(product.id, orders, daysBack); const quantities = dailySales.map((d) => d.quantity); const avgDaily = movingAverage(quantities, daysBack); const salesStdDev = stdDev(quantities); // Safety stock: 1.65 × σ × √leadTime (95% service level, 7-day lead time) const leadTimeDays = 7; const safetyStock = Math.ceil(1.65 * salesStdDev * Math.sqrt(leadTimeDays)); // Reorder point: (avg daily sales × lead time) + safety stock const reorderPoint = Math.ceil(avgDaily * leadTimeDays + safetyStock); // Days of stock remaining const currentStock = product.inventory_quantity; const daysOfStock = avgDaily > 0 ? currentStock / avgDaily : null; // Depletion date let depletionDate: string | null = null; if (daysOfStock !== null && Number.isFinite(daysOfStock)) { depletionDate = new Date(Date.now() + daysOfStock * MS_PER_DAY).toISOString().slice(0, 10); } // Suggested reorder quantity: 30 days of avg sales + safety stock - current stock const targetStock = Math.ceil(avgDaily * 30 + safetyStock); const suggestedReorder = Math.max(0, targetStock - currentStock); // Risk level let riskLevel: 'low' | 'medium' | 'high' | 'critical'; let detail: string; if (currentStock <= 0) { riskLevel = 'critical'; detail = 'Out of stock — immediate restock needed'; } else if (daysOfStock !== null && daysOfStock <= 3) { riskLevel = 'critical'; detail = `Only ${Math.round(daysOfStock)} day(s) of stock remaining`; } else if (daysOfStock !== null && daysOfStock <= 7) { riskLevel = 'high'; detail = `${Math.round(daysOfStock)} days of stock — below lead time threshold`; } else if (currentStock <= reorderPoint) { riskLevel = 'medium'; detail = `Stock at or below reorder point (${reorderPoint} units)`; } else if (daysOfStock !== null && daysOfStock <= 14) { riskLevel = 'medium'; detail = `${Math.round(daysOfStock)} days of stock — approaching reorder point`; } else { riskLevel = 'low'; detail = daysOfStock !== null ? `${Math.round(daysOfStock)} days of stock remaining` : 'No recent sales data — unable to forecast depletion'; } return { product_id: product.id, product_title: product.title, sku: product.sku, current_stock: currentStock, avg_daily_sales: Math.round(avgDaily * 100) / 100, days_of_stock: daysOfStock !== null ? Math.round(daysOfStock) : null, depletion_date: depletionDate, reorder_point: reorderPoint, suggested_reorder_qty: suggestedReorder, safety_stock: safetyStock, risk_level: riskLevel, detail, }; } /** * Forecast inventory for all products in a store. */ export function forecastAll(products: Product[], orders: Order[], daysBack = 30): ForecastResult[] { return products .filter((p) => p.status === 'active') .map((p) => forecastProduct(p, orders, daysBack)) .sort((a, b) => { const riskOrder = { critical: 0, high: 1, medium: 2, low: 3 }; return riskOrder[a.risk_level] - riskOrder[b.risk_level]; }); } - src/services/forecasting.ts:138-146 (helper)Helper function forecastAll that filters active products, calls forecastProduct for each, and sorts by risk level (critical first).
export function forecastAll(products: Product[], orders: Order[], daysBack = 30): ForecastResult[] { return products .filter((p) => p.status === 'active') .map((p) => forecastProduct(p, orders, daysBack)) .sort((a, b) => { const riskOrder = { critical: 0, high: 1, medium: 2, low: 3 }; return riskOrder[a.risk_level] - riskOrder[b.risk_level]; }); } - src/models/store.ts:131-146 (schema)ForecastResultSchema Zod schema defining the forecast output shape: product_id, product_title, sku, current_stock, avg_daily_sales, days_of_stock, depletion_date, reorder_point, suggested_reorder_qty, safety_stock, risk_level, and detail.
// ── Inventory Forecast ──────────────────────────────────────────── export const ForecastResultSchema = z.object({ product_id: z.string(), product_title: z.string(), sku: z.string().nullable(), current_stock: z.number().int(), avg_daily_sales: z.number(), days_of_stock: z.number().nullable(), depletion_date: z.string().nullable(), reorder_point: z.number().int(), suggested_reorder_qty: z.number().int(), safety_stock: z.number().int(), risk_level: z.enum(['low', 'medium', 'high', 'critical']), detail: z.string(), }); export type ForecastResult = z.infer<typeof ForecastResultSchema>;