Inventory Forecast
inventory_forecastPredict stock depletion dates using moving-average sales velocity. Get reorder points, safety stock levels, and suggested reorder quantities for each product.
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/tools/inventory.ts:52-67 (handler)Main handler function for the inventory_forecast tool. Validates the store UUID, loads store/orders/products, and delegates to forecastProduct (single product) or forecastAll (all products) from the forecasting service.
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/index.ts:146-166 (registration)Registration of the 'inventory_forecast' tool on the MCP server. Defines input schema (store_id required, product_id optional), title, description, annotations, and the async handler that checks license (ensureProOrReject) before calling getInventoryForecast.
// ── 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/models/store.ts:131-146 (schema)Type definition (ForecastResult) used by the inventory_forecast tool. Defines all output fields: 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>; - src/services/forecasting.ts:65-133 (helper)Core forecasting logic for a single product. Computes daily sales from order history, calculates moving average, standard deviation, safety stock (1.65σ × √7 for 95% service level), reorder point, depletion date, suggested reorder quantity, and assigns a risk level (critical/high/medium/low). This is the algorithm behind the forecast.
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, }; } - src/services/forecasting.ts:138-146 (helper)Batch forecast for all active products in a store. Filters active products, runs forecastProduct on each, then sorts by risk priority (critical → high → medium → low).
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]; }); }