calculate_order_need
Determine MRP order requirements by analyzing inventory, forecasts, and delivery schedules to optimize stock levels and meet demand accurately.
Instructions
Calculate MRP order need based on forecast, inventory, and delivery schedule
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| analysis_date | Yes | ||
| batch_sizes | No | ||
| current_balance | Yes | ||
| delivery_schedule | Yes | ||
| forecast_periods | Yes | ||
| must_order_point | Yes | ||
| open_orders | Yes | ||
| sku_location | Yes |
Implementation Reference
- src/calculator.ts:3-116 (handler)Core handler function that executes the MRP order need calculation logic, including balance projections, demand summation, batch optimization, and validation messages.export function calculateMRP(input: MRPInput): MRPOutput { const steps: CalculationStep[] = []; const projectedBalances = []; // Step 1: Calculate Starting Balance const startingBalance = input.current_balance + input.open_orders.reduce((sum, order) => sum + order.quantity, 0); steps.push({ step: 'starting_balance', description: 'Current balance + Open orders', value: startingBalance }); // Step 2: Calculate Total Demand const totalDemand = input.forecast_periods.reduce((sum, period) => sum + period.quantity, 0); steps.push({ step: 'total_demand', description: 'Sum of all forecast periods', value: totalDemand }); // Step 3: Calculate Order Need const orderNeed = (input.must_order_point + totalDemand) - startingBalance; steps.push({ step: 'order_need', description: '(Must-order point + Total demand) - Starting balance', value: orderNeed }); // Step 4: Calculate Daily Projected Balances let currentBalance = startingBalance; let currentDate = new Date(input.analysis_date); const secondDelivery = new Date(input.delivery_schedule.second_delivery); while (currentDate < secondDelivery) { const dateStr = currentDate.toISOString().split('T')[0]; const adjustments = []; let endingBalance = currentBalance; // Check for open orders on this date const todaysOrders = input.open_orders.filter(order => order.delivery_date === dateStr ); if (todaysOrders.length > 0) { const orderSum = todaysOrders.reduce((sum, order) => sum + order.quantity, 0); adjustments.push({ type: 'RECEIPT', quantity: orderSum, reason: 'Open order delivery' }); endingBalance += orderSum; } // Apply forecast demand const forecastPeriod = input.forecast_periods.find(period => dateStr >= period.start_date && dateStr <= period.end_date ); if (forecastPeriod) { const daysInPeriod = (new Date(forecastPeriod.end_date).getTime() - new Date(forecastPeriod.start_date).getTime()) / (1000 * 60 * 60 * 24) + 1; const dailyDemand = forecastPeriod.quantity / daysInPeriod; adjustments.push({ type: 'DEMAND', quantity: -dailyDemand, reason: 'Daily forecast demand' }); endingBalance -= dailyDemand; } projectedBalances.push({ date: dateStr, starting_balance: currentBalance, adjustments, ending_balance: endingBalance }); currentBalance = endingBalance; currentDate.setDate(currentDate.getDate() + 1); } // Step 5: Apply Batch Size Optimization let finalOrderNeed = orderNeed; if (input.batch_sizes && input.batch_sizes.length > 0 && orderNeed > 0) { const batchSize = input.batch_sizes.find(size => size >= orderNeed) || input.batch_sizes[input.batch_sizes.length - 1]; finalOrderNeed = batchSize; steps.push({ step: 'batch_optimization', description: 'Rounded to nearest valid batch size', value: finalOrderNeed }); } // Prepare validation messages const messages = []; const lowestProjectedBalance = Math.min(...projectedBalances.map(pb => pb.ending_balance)); if (lowestProjectedBalance < input.must_order_point) { messages.push(`Warning: Projected balance falls below must-order point (${input.must_order_point})`); } return { order_need: finalOrderNeed, calculation_steps: steps, validation: { status: messages.length > 0 ? 'WARNING' : 'SUCCESS', messages }, projected_balances: projectedBalances };
- src/index.ts:149-230 (handler)MCP server request handler for tool calls, specifically dispatches 'calculate_order_need', handles input parsing, validation, execution via calculateMRP, and response formatting.this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name !== 'calculate_order_need') { return { content: [{ type: 'text', text: `Unknown tool: ${request.params.name}`, }], isError: true, }; } try { // Type assertion with validation const rawInput = request.params.arguments as Record<string, unknown>; const deliverySchedule = rawInput.delivery_schedule as Record<string, unknown>; const input: MRPInput = { sku_location: rawInput.sku_location as { sku: string; location: string }, analysis_date: String(rawInput.analysis_date), must_order_point: Number(rawInput.must_order_point), current_balance: Number(rawInput.current_balance), open_orders: (rawInput.open_orders as Array<any>).map(order => ({ quantity: Number(order.quantity), delivery_date: String(order.delivery_date) })), delivery_schedule: { first_delivery: String(deliverySchedule.first_delivery), second_delivery: String(deliverySchedule.second_delivery), lead_time: Number(deliverySchedule.lead_time) }, forecast_periods: (rawInput.forecast_periods as Array<any>).map(period => ({ start_date: String(period.start_date), end_date: String(period.end_date), quantity: Number(period.quantity) })), batch_sizes: rawInput.batch_sizes ? (rawInput.batch_sizes as number[]) : undefined }; // Validate input const validationResult = validateMRPInput(input); if (!validationResult.isValid) { return { content: [{ type: 'text', text: `Validation failed:\n${validationResult.messages.map(m => `${m.type}: ${m.message}`).join('\n')}`, }], isError: true, }; } // Calculate MRP const result = calculateMRP(input); // Format response const response = { order_need: result.order_need, calculation_steps: result.calculation_steps.map(step => `${step.step}: ${step.description} = ${step.value}` ).join('\n'), projected_balances: result.projected_balances.map(pb => `${pb.date}: ${pb.starting_balance} → ${pb.ending_balance}` ).join('\n'), validation: result.validation.messages.join('\n') }; return { content: [{ type: 'text', text: JSON.stringify(response, null, 2), }], }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [{ type: 'text', text: `Error calculating MRP: ${errorMessage}`, }], isError: true, }; } });
- src/index.ts:21-74 (registration)Registration of the 'calculate_order_need' tool in MCP server capabilities, defining name, description, and input schema.calculate_order_need: { name: 'calculate_order_need', description: 'Calculate MRP order need based on forecast, inventory, and delivery schedule', inputSchema: { type: 'object', properties: { sku_location: { type: 'object', properties: { sku: { type: 'string' }, location: { type: 'string' } }, required: ['sku', 'location'] }, analysis_date: { type: 'string' }, must_order_point: { type: 'number' }, current_balance: { type: 'number' }, open_orders: { type: 'array', items: { type: 'object', properties: { quantity: { type: 'number' }, delivery_date: { type: 'string' } } } }, delivery_schedule: { type: 'object', properties: { first_delivery: { type: 'string' }, second_delivery: { type: 'string' }, lead_time: { type: 'number' } } }, forecast_periods: { type: 'array', items: { type: 'object', properties: { start_date: { type: 'string' }, end_date: { type: 'string' }, quantity: { type: 'number' } } } }, batch_sizes: { type: 'array', items: { type: 'number' } } }, required: ['sku_location', 'analysis_date', 'must_order_point', 'current_balance', 'open_orders', 'delivery_schedule', 'forecast_periods'] } }
- src/types.ts:1-24 (schema)TypeScript interface defining the input structure (MRPInput) for the calculate_order_need tool, matching the JSON schema.export interface MRPInput { sku_location: { sku: string; location: string; }; analysis_date: string; // ISO 8601 must_order_point: number; current_balance: number; open_orders: Array<{ quantity: number; delivery_date: string; // ISO 8601 }>; delivery_schedule: { first_delivery: string; // ISO 8601 second_delivery: string; // ISO 8601 lead_time: number; }; forecast_periods: Array<{ start_date: string; // ISO 8601 end_date: string; // ISO 8601 quantity: number; }>; batch_sizes?: number[]; // Optional }
- src/validator.ts:3-112 (helper)Helper function to validate the input parameters for the tool, checking dates, numbers, required fields, and producing error/warning messages.export function validateMRPInput(input: MRPInput): ValidationResult { const messages: ValidationMessage[] = []; // Required fields validation if (!input.sku_location?.sku || !input.sku_location?.location) { messages.push({ type: 'ERROR', message: 'SKU and location are required' }); } // Numeric validations if (input.must_order_point < 0) { messages.push({ type: 'ERROR', message: 'Must-order point cannot be negative' }); } if (input.current_balance < 0) { messages.push({ type: 'WARNING', message: 'Current balance is negative' }); } // Date validations try { const analysisDate = new Date(input.analysis_date); const firstDelivery = new Date(input.delivery_schedule.first_delivery); const secondDelivery = new Date(input.delivery_schedule.second_delivery); if (firstDelivery <= analysisDate) { messages.push({ type: 'ERROR', message: 'First delivery date must be after analysis date' }); } if (secondDelivery <= firstDelivery) { messages.push({ type: 'ERROR', message: 'Second delivery date must be after first delivery date' }); } } catch (e) { messages.push({ type: 'ERROR', message: 'Invalid date format. Use ISO 8601 format (YYYY-MM-DD)' }); } // Forecast period validations if (!input.forecast_periods || input.forecast_periods.length === 0) { messages.push({ type: 'ERROR', message: 'At least one forecast period is required' }); } else { input.forecast_periods.forEach((period, index) => { try { const startDate = new Date(period.start_date); const endDate = new Date(period.end_date); if (endDate <= startDate) { messages.push({ type: 'ERROR', message: `Forecast period ${index + 1}: End date must be after start date` }); } if (period.quantity < 0) { messages.push({ type: 'ERROR', message: `Forecast period ${index + 1}: Quantity cannot be negative` }); } } catch (e) { messages.push({ type: 'ERROR', message: `Forecast period ${index + 1}: Invalid date format` }); } }); } // Batch size validations if (input.batch_sizes) { if (!input.batch_sizes.every(size => size > 0)) { messages.push({ type: 'ERROR', message: 'Batch sizes must be positive numbers' }); } if (!input.batch_sizes.every((size, i, arr) => i === 0 || size > arr[i - 1])) { messages.push({ type: 'ERROR', message: 'Batch sizes must be in ascending order' }); } } const hasErrors = messages.some(msg => msg.type === 'ERROR'); return { isValid: !hasErrors, messages }; }