Skip to main content
Glama

solve_employee_shift_scheduling

Assign employees to shifts optimally by balancing coverage needs with constraints and preferences for efficient workforce scheduling.

Instructions

Solve Employee Shift Scheduling to assign employees to shifts optimally.

Args: employees: List of employee names shifts: List of shift dictionaries with time and requirements days: Number of days to schedule employee_constraints: Optional constraints and preferences per employee time_limit_seconds: Maximum solving time in seconds (default: 30.0) Returns: Optimization result with employee schedules and coverage statistics

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
employeesYes
shiftsYes
daysYes
employee_constraintsNo
time_limit_secondsNo

Implementation Reference

  • The decorated MCP tool handler that accepts parameters, constructs input data, invokes the shift scheduling solver, and returns the result.
    @mcp.tool() def solve_employee_shift_scheduling( employees: list[str], shifts: list[dict[str, Any]], days: int, employee_constraints: dict[str, dict[str, Any]] | None = None, time_limit_seconds: float = 30.0, ) -> dict[str, Any]: """Solve Employee Shift Scheduling to assign employees to shifts optimally. Args: employees: List of employee names shifts: List of shift dictionaries with time and requirements days: Number of days to schedule employee_constraints: Optional constraints and preferences per employee time_limit_seconds: Maximum solving time in seconds (default: 30.0) Returns: Optimization result with employee schedules and coverage statistics """ input_data = { "employees": employees, "shifts": shifts, "days": days, "employee_constraints": employee_constraints or {}, "time_limit_seconds": time_limit_seconds, } result = solve_shift_scheduling(input_data) result_dict: dict[str, Any] = result.model_dump() return result_dict
  • Pydantic model defining and validating the input structure for the shift scheduling solver.
    class ShiftSchedulingInput(BaseModel): """Input schema for Shift Scheduling.""" employees: list[str] shifts: list[Shift] days: int = Field(ge=1) employee_constraints: dict[str, EmployeeConstraints] = Field(default_factory=dict) time_limit_seconds: float = Field(default=30.0, ge=0) @field_validator("employees") @classmethod def validate_employees(cls, v: list[str]) -> list[str]: if not v: raise ValueError("Must have at least one employee") return v @field_validator("shifts") @classmethod def validate_shifts(cls, v: list[Shift]) -> list[Shift]: if not v: raise ValueError("Must have at least one shift") return v
  • Invocation of register_scheduling_tools during MCP server setup, which defines and registers the solve_employee_shift_scheduling tool.
    register_scheduling_tools(mcp)
  • Core solver function implementing the OR-Tools CP-SAT model for employee shift assignment optimization, handling constraints and objectives.
    @with_resource_limits(timeout_seconds=90.0, estimated_memory_mb=120.0) def solve_shift_scheduling(input_data: dict[str, Any]) -> OptimizationResult: """Solve Shift Scheduling Problem using OR-Tools CP-SAT. Args: input_data: Shift scheduling problem specification Returns: OptimizationResult with employee shift assignments """ if not ORTOOLS_AVAILABLE: return OptimizationResult( status=OptimizationStatus.ERROR, objective_value=None, variables={}, execution_time=0.0, error_message="OR-Tools is not available. Please install it with 'pip install ortools'", ) start_time = time.time() try: # Parse and validate input scheduling_input = ShiftSchedulingInput(**input_data) employees = scheduling_input.employees shifts = scheduling_input.shifts days = scheduling_input.days employee_constraints = scheduling_input.employee_constraints # Create CP-SAT model model = cp_model.CpModel() # Variables: assignment[employee][shift][day] = 1 if employee works shift on day assignments: dict[int, dict[int, dict[int, Any]]] = {} for emp_idx, employee in enumerate(employees): assignments[emp_idx] = {} for shift_idx, shift in enumerate(shifts): assignments[emp_idx][shift_idx] = {} for day in range(days): var_name = f"assign_{employee}_{shift.name}_{day}" assignments[emp_idx][shift_idx][day] = model.NewBoolVar(var_name) # Constraint: Each shift must have required staff each day for shift_idx, shift in enumerate(shifts): for day in range(days): model.Add( sum(assignments[emp_idx][shift_idx][day] for emp_idx in range(len(employees))) >= shift.required_staff ) # Employee constraints for emp_idx, employee in enumerate(employees): emp_constraints = employee_constraints.get(employee, EmployeeConstraints()) # Max/min shifts per week if emp_constraints.max_shifts_per_week is not None: total_shifts = sum( assignments[emp_idx][shift_idx][day] for shift_idx in range(len(shifts)) for day in range(days) ) model.Add(total_shifts <= emp_constraints.max_shifts_per_week) if emp_constraints.min_shifts_per_week is not None: total_shifts = sum( assignments[emp_idx][shift_idx][day] for shift_idx in range(len(shifts)) for day in range(days) ) model.Add(total_shifts >= emp_constraints.min_shifts_per_week) # Unavailable shifts for shift_name in emp_constraints.unavailable_shifts: for shift_idx, shift in enumerate(shifts): if shift.name == shift_name: for day in range(days): model.Add(assignments[emp_idx][shift_idx][day] == 0) # Skills requirements for shift_idx, shift in enumerate(shifts): if shift.skills_required: has_required_skills = all( skill in emp_constraints.skills for skill in shift.skills_required ) if not has_required_skills: for day in range(days): model.Add(assignments[emp_idx][shift_idx][day] == 0) # No overlapping shifts on same day for day in range(days): overlapping_shifts = [] for shift_idx, _shift in enumerate(shifts): overlapping_shifts.append(assignments[emp_idx][shift_idx][day]) model.Add(sum(overlapping_shifts) <= 1) # Max consecutive shifts if emp_constraints.max_consecutive_shifts is not None: for start_day in range(days - emp_constraints.max_consecutive_shifts): consecutive_vars = [] for day in range( start_day, start_day + emp_constraints.max_consecutive_shifts + 1, ): day_working = model.NewBoolVar(f"working_{employee}_{day}") model.Add( day_working == sum( assignments[emp_idx][shift_idx][day] for shift_idx in range(len(shifts)) ) ) consecutive_vars.append(day_working) model.Add(sum(consecutive_vars) <= emp_constraints.max_consecutive_shifts) # Objective: Minimize total assignments (prefer fewer shifts) and maximize preferences total_assignments = sum( assignments[emp_idx][shift_idx][day] for emp_idx in range(len(employees)) for shift_idx in range(len(shifts)) for day in range(days) ) # Add preference bonus preference_bonus = 0 for emp_idx, employee in enumerate(employees): emp_constraints = employee_constraints.get(employee, EmployeeConstraints()) for shift_name in emp_constraints.preferred_shifts: for shift_idx, shift in enumerate(shifts): if shift.name == shift_name: preference_bonus += sum( assignments[emp_idx][shift_idx][day] for day in range(days) ) # Minimize negative preference (maximize preference) model.Minimize(total_assignments - preference_bonus) # Solve solver = cp_model.CpSolver() solver.parameters.max_time_in_seconds = scheduling_input.time_limit_seconds status = solver.Solve(model) if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: # type: ignore[comparison-overlap,unused-ignore] # Extract solution schedule = [] total_cost = 0 for emp_idx, employee in enumerate(employees): employee_schedule = [] for day in range(days): day_shifts = [] for shift_idx, shift in enumerate(shifts): if solver.Value(assignments[emp_idx][shift_idx][day]): day_shifts.append( { "shift_name": shift.name, "start": shift.start, "end": shift.end, "skills_required": shift.skills_required, } ) total_cost += 1 employee_schedule.append({"day": day, "shifts": day_shifts}) schedule.append({"employee": employee, "schedule": employee_schedule}) execution_time = time.time() - start_time return OptimizationResult( status=OptimizationStatus.OPTIMAL if status == cp_model.OPTIMAL # type: ignore[comparison-overlap,unused-ignore] else OptimizationStatus.FEASIBLE, objective_value=float(total_cost), variables={ "schedule": schedule, "total_assignments": total_cost, "num_employees": len(employees), "num_shifts": len(shifts), "num_days": days, }, execution_time=execution_time, solver_info={ "solver_name": "OR-Tools CP-SAT", "status": solver.StatusName(status), }, ) else: status_name = solver.StatusName(status) return OptimizationResult( status=OptimizationStatus.INFEASIBLE if status == cp_model.INFEASIBLE # type: ignore[comparison-overlap,unused-ignore] else OptimizationStatus.ERROR, error_message=f"No solution found: {status_name}", execution_time=time.time() - start_time, ) except Exception as e: return OptimizationResult( status=OptimizationStatus.ERROR, error_message=f"Shift scheduling error: {str(e)}", execution_time=time.time() - start_time, )

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/dmitryanchikov/mcp-optimizer'

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