Shift Scheduler
Builds optimal 24/7 staff rosters with a real MILP solver: rest rules, skills, nights, fairness.
Should I use this
Quality & Safety
Based on automated analysis of tool definitions and protocol compliance.
Context Cost
This is the approximate number of tokens consumed each time the server's tools are loaded into a model's context. Higher counts reduce the attention available for other tasks.
Install
One-Click Install
Add this to your `claude_desktop_config.json` file:
{
"mcpServers": {
"shift-scheduler": {
"url": "https://staff-scheduling.kindoflost.com/mcp"
}
}
}Remote endpoints
https://staff-scheduling.kindoflost.com/mcpstreamable-httpWhat it can do
Tool inventory
Tools (4)
🟢list_shift_examples
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.
Input Schema
{
"type": "object",
"properties": {},
"additionalProperties": false
}🟢get_shift_example(name, lang)
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.
Input Schema
{
"type": "object",
"properties": {
"name": {
"type": "string",
"enum": [
"hospital-ward",
"three-shift-plant",
"security-site",
"fire-station",
"support-desk"
]
},
"lang": {
"type": "string",
"enum": [
"en",
"es"
],
"description": "es = Spanish shift and post names."
}
},
"required": [
"name"
],
"additionalProperties": false
}🟢build_shift_roster(example, days, shifts, demands, people, ...)
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. 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.
Input Schema
{
"type": "object",
"properties": {
"example": {
"type": "string",
"enum": [
"hospital-ward",
"three-shift-plant",
"security-site",
"fire-station",
"support-desk"
],
"description": "Solve this built-in example instead of passing the fields below."
},
"days": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"maxItems": 42,
"description": "Day labels in order: ISO dates ('2026-10-05') or weekday names ('Mon'). Dates or weekdays enable weekend rules."
},
"shifts": {
"type": "array",
"minItems": 1,
"maxItems": 12,
"description": "Shift types. A shift may cross midnight (end < start).",
"items": {
"type": "object",
"required": [
"shift",
"start",
"end"
],
"properties": {
"shift": {
"type": "string",
"description": "Shift name, e.g. 'Day'."
},
"start": {
"type": "string",
"description": "HH:MM"
},
"end": {
"type": "string",
"description": "HH:MM"
},
"night": {
"type": [
"string",
"boolean"
],
"description": "'yes' = counts as a night shift. Omitted = decided from NightStartsAfter."
}
}
}
},
"demands": {
"type": "array",
"maxItems": 12,
"description": "Posts to fill (roles). Headcount per day/shift goes in grids.",
"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."
}
}
}
},
"people": {
"type": "array",
"minItems": 1,
"maxItems": 60,
"items": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
},
"cost": {
"type": "number",
"description": "Cost per hour (default 1)."
},
"min_hd": {
"type": "number",
"description": "Min hours per worked day."
},
"max_hd": {
"type": "number",
"description": "Max hours per day (default 24)."
},
"min_dw": {
"type": "number",
"description": "Min days worked over the WHOLE roster."
},
"max_dw": {
"type": "number",
"description": "Max days worked over the WHOLE roster."
},
"min_hw": {
"type": "number",
"description": "Min hours over the WHOLE roster (soft)."
},
"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."
}
}
}
},
"skills": {
"type": "array",
"description": "Who can cover which demand. Omitted = everyone can cover every demand.",
"items": {
"type": "object",
"required": [
"person",
"demand"
],
"properties": {
"person": {
"type": "string"
},
"demand": {
"type": "string"
},
"rate": {
"type": "number",
"description": "Posts covered per person (default 1)."
}
}
}
},
"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"
}
},
"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 120 here), GapPercent (2)."
},
"wait_seconds": {
"type": "integer",
"minimum": 0,
"maximum": 45,
"description": "How long to wait for the solve inside this call (default 25)."
}
}
}🟢get_shift_roster_result(job_id, wait_seconds)
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.
Input Schema
{
"type": "object",
"properties": {
"job_id": {
"type": "string"
},
"wait_seconds": {
"type": "integer",
"minimum": 0,
"maximum": 45
}
},
"required": [
"job_id"
],
"additionalProperties": false
}Community
Evidence