Builds optimal 24/7 staff rosters with a real MILP solver: rest rules, skills, nights, fairness.
Maintain this server? Claim it
Call this server's tools from your browser. Nothing to install.
Requests go through mcptry because browsers can't call most MCP servers directly. Header values are sent to the server for that request only and are never stored or logged.
Tool list from our last check. Connect live to refresh it.
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. If it has status 'busy', wait the retry_after_seconds it gives and call again -- one call at a time, never in parallel. Safe defaults (e.g. the next 7 days when `days` is omitted) are listed in the result's `assumptions`; tell the user about them. 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.
Day labels in order: ISO dates ('2026-10-05') or weekday names ('Mon'). Dates or weekdays enable weekend rules.
One sentence, in the user's own words, of what they are trying to do (e.g. 'a 4-week nurse rota for 12 nurses on 12-hour shifts'). Optional; helps improve the tool.
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).
Shift types. A shift may cross midnight (end < start).
Who can cover which demand. Omitted = everyone can cover every demand.
Posts to fill (roles). Headcount per day/shift goes in grids.
Solve this built-in example instead of passing the fields below.
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).
How long to wait for the solve inside this call (default 25).
Tool list last changed Oct 25, 2026.
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. If it has status 'busy', wait the retry_after_seconds it gives and call again -- one call at a time, never in parallel. Safe defaults (e.g. the next 7 days when `days` is omitted) are listed in the result's `assumptions`; tell the user about them. 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.
| Argument | Type | Description |
|---|---|---|
| days | array | Day labels in order: ISO dates ('2026-10-05') or weekday names ('Mon'). Dates or weekdays enable weekend rules. |
| goal | string | One sentence, in the user's own words, of what they are trying to do (e.g. 'a 4-week nurse rota for 12 nurses on 12-hour shifts'). Optional; helps improve the tool. |
| grids | object | 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). |
| people | array | |
| shifts | array | Shift types. A shift may cross midnight (end < start). |
| skills | array | Who can cover which demand. Omitted = everyone can cover every demand. |
| demands | array | Posts to fill (roles). Headcount per day/shift goes in grids. |
| example | string | Solve this built-in example instead of passing the fields below. |
| parameters | object | 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 | integer | How long to wait for the solve inside this call (default 25). |
{
"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."
},
"goal": {
"type": "string",
"maxLength": 300,
"description": "One sentence, in the user's own words, of what they are trying to do (e.g. 'a 4-week nurse rota for 12 nurses on 12-hour shifts'). Optional; helps improve the tool."
},
"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)."
}
}
}Describe what the user wants in plain words and get back whether this server can do it, which limits it would hit, and the closest thing it CAN do. Instant, no solve. Use it when unsure the tool fits (shift swaps, payroll, time clocks, very large teams or long horizons).
| Argument | Type | Description |
|---|---|---|
| description* | string | The user's request in their own words, e.g. 'rota for 80 nurses over 3 months with shift swaps'. |
{
"type": "object",
"required": [
"description"
],
"properties": {
"description": {
"type": "string",
"maxLength": 2000,
"description": "The user's request in their own words, e.g. 'rota for 80 nurses over 3 months with shift swaps'."
}
},
"additionalProperties": false
}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.
| Argument | Type | Description |
|---|---|---|
| lang | string | es = Spanish shift and post names. |
| name* | string |
{
"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
}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.
| Argument | Type | Description |
|---|---|---|
| job_id* | string | |
| wait_seconds | integer |
{
"type": "object",
"required": [
"job_id"
],
"properties": {
"job_id": {
"type": "string"
},
"wait_seconds": {
"type": "integer",
"maximum": 45,
"minimum": 0
}
},
"additionalProperties": false
}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.
No arguments.
{
"type": "object",
"properties": {},
"additionalProperties": false
}