MCP Server

Shift Scheduler

com.kindoflost.staff-scheduling/shift-scheduler
Business & Operations Public & reachable MCP 2025-11-25

What this MCP does

Optimizes staff rosters using shift coverage, skills, rest, night-work, fairness, and workload constraints.

build_shift_roster
Build an optimized shift roster
Solve a staff rostering problem to optimality with a MILP solver and return who works which shift each day, coverage, unfilled posts, hours per person and warnings. Solves take 5-60 s. Pass either `example` (to solve a built-in example as-is) or the roster input fields. If the result has status 'running', call get_shift_roster_result with its job_id. If it has status 'too_large', shorten the horizon or reduce people/shift types and retry. Roster input. Minimal example: {"days": ["2026-10-05", "2026-10-06"], "shifts": [{"shift": "Day", "start": "07:00", "end": "19:00"}, {"shift": "Night", "start": "19:00", "end": "07:00", "night": "yes"}], "demands": [{"name": "RN", "penalty": 500}], "people": [{"name": "Ana", "cost": 40, "max_hw": 48}, ...], "grids": {"RN": {"2026-10-05|Day": 2, "2026-10-05|Night": 1}}, "parameters": {"MinRestHours": 11}}. Call get_shift_example for complete, realistic inputs.
Read only Idempotent
Input schema
{'type': 'object', 'properties': {'days': {'type': 'array', 'items': {'type': 'string'}, 'maxItems': 42, 'minItems': 1, 'description': "Day labels in order: ISO dates ('2026-10-05') or weekday names ('Mon'). Dates or weekdays enable weekend rules."}, 'grids': {'type': 'object', 'description': "Optional per-day/shift cells, keyed by a demand name or a person name, each an object keyed 'DAY|SHIFT'. Demand grid value = headcount required (demand with no grid = 1 on every shift). Person grid value: 'a' or 1 = available, 'f' = must work, 0 or missing = UNAVAILABLE (person with no grid = available everywhere).", 'additionalProperties': {'type': 'object'}}, 'people': {'type': 'array', 'items': {'type': 'object', 'required': ['name'], 'properties': {'cost': {'type': 'number', 'description': 'Cost per hour (default 1).'}, 'name': {'type': 'string'}, 'max_dw': {'type': 'number', 'description': 'Max days worked over the WHOLE roster.'}, 'max_hd': {'type': 'number', 'description': 'Max hours per day (default 24).'}, 'max_hw': {'type': 'number', 'description': 'Max hours over the WHOLE roster (hard). Not per week: for a 28-day roster at 40h/week use 160.'}, 'min_dw': {'type': 'number', 'description': 'Min days worked over the WHOLE roster.'}, 'min_hd': {'type': 'number', 'description': 'Min hours per worked day.'}, 'min_hw': {'type': 'number', 'description': 'Min hours over the WHOLE roster (soft).'}}}, 'maxItems': 60, 'minItems': 1}, 'shifts': {'type': 'array', 'items': {'type': 'object', 'required': ['shift', 'start', 'end'], 'properties': {'end': {'type': 'string', 'description': 'HH:MM'}, 'night': {'type': ['string', 'boolean'], 'description': "'yes' = counts as a night shift. Omitted = decided from NightStartsAfter."}, 'shift': {'type': 'string', 'description': "Shift name, e.g. 'Day'."}, 'start': {'type': 'string', 'description': 'HH:MM'}}}, 'maxItems': 12, 'minItems': 1, 'description': 'Shift types. A shift may cross midnight (end < start).'}, 'skills': {'type': 'array', 'items': {'type': 'object', 'required': ['person', 'demand'], 'properties': {'rate': {'type': 'number', 'description': 'Posts covered per person (default 1).'}, 'demand': {'type': 'string'}, 'person': {'type': 'string'}}}, 'description': 'Who can cover which demand. Omitted = everyone can cover every demand.'}, 'demands': {'type': 'array', 'items': {'type': 'object', 'required': ['name'], 'properties': {'name': {'type': 'string'}, 'penalty': {'type': 'number', 'description': "Cost of leaving ONE post unfilled for one shift. Must be well above one person's pay for a shift or the solver will prefer gaps. Omitted = 10 x the highest hourly cost x the longest shift."}, 'concurrent': {'type': 'boolean', 'description': 'true = the same person may cover this and another demand at once.'}}}, 'maxItems': 12, 'description': 'Posts to fill (roles). Headcount per day/shift goes in grids.'}, 'example': {'enum': ['hospital-ward', 'three-shift-plant', 'security-site', 'fire-station', 'support-desk'], 'type': 'string', 'description': 'Solve this built-in example instead of passing the fields below.'}, 'parameters': {'type': 'object', 'description': "Optional rules. MinRestHours (11), MaxConsecutiveDays (6), MaxConsecutiveNights (4), ForbidDayAfterNight ('yes'), NightStartsAfter ('18:00'), MinBlockDays (1), WeekendFairnessPenalty (0), EquityPenalty (0) = even out total hours, NightEquityPenalty (0) = even out total nights (try 20), NightDifferential (1.0), WeekendDifferential (1.0), TimeLimitSeconds (max 60 here), GapPercent (2)."}, 'wait_seconds': {'type': 'integer', 'maximum': 45, 'minimum': 0, 'description': 'How long to wait for the solve inside this call (default 25).'}}}
get_shift_example
Get an example roster input
Return the full input JSON of one example, in exactly the shape build_shift_roster accepts. Edit it (names, days, headcounts, rules) to match the user's situation.
Read only
Input schema
{'type': 'object', 'required': ['name'], 'properties': {'lang': {'enum': ['en', 'es'], 'type': 'string', 'description': 'es = Spanish shift and post names.'}, 'name': {'enum': ['hospital-ward', 'three-shift-plant', 'security-site', 'fire-station', 'support-desk'], 'type': 'string'}}, 'additionalProperties': False}
get_shift_roster_result
Get a roster result
Fetch the result of a build_shift_roster job that returned status 'running'. Waits up to wait_seconds; call again while status is 'running'. Jobs expire 15 minutes after they finish.
Read only
Input schema
{'type': 'object', 'required': ['job_id'], 'properties': {'job_id': {'type': 'string'}, 'wait_seconds': {'type': 'integer', 'maximum': 45, 'minimum': 0}}, 'additionalProperties': False}
list_shift_examples
List example rosters
List the built-in example rostering problems (hospital ward, 3-shift plant, security post, fire station, 24/7 help desk). Use one as a starting point for the user's own roster.
Read only
Input schema
{'type': 'object', 'properties': {}, 'additionalProperties': False}
Added
get_shift_roster_result
Sept. 28, 2026, 2:40 a.m.
Added
build_shift_roster
Sept. 28, 2026, 2:40 a.m.
Added
get_shift_example
Sept. 28, 2026, 2:40 a.m.
Added
list_shift_examples
Sept. 28, 2026, 2:40 a.m.