StaffSavvy MCP Server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@StaffSavvy MCP ServerWho's working in the Main Theatre on Saturday, and in which roles?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
StaffSavvy MCP server
An MCP server that lets Claude, ChatGPT and other MCP clients read a StaffSavvy rota and time-and-attendance account: venues, groups (skills or roles), shifts, absences and absence types, time entries, venue events and staff names. It is built from StaffSavvy's public documentation: the OpenAPI 3.0 document "StaffSavvy REST API" 1.1.0 published on SwaggerHub (SmartBlue/StaffSavvy) and the support article StaffSavvy Open API.
Once it's connected, a manager can ask things like:
"Who's working in the Main Theatre on Saturday, and in which roles?"
"Who is off this week, and is it sickness or holiday?"
"What events are on at the Studio in October, and how many shifts are rostered for the Macbeth performance?"
"Show Sam Evans's clocked hours for last week and whether they've been approved."
"Which staff records changed since the start of the month?"
This version is read-only. There are no write tools at all (see "Why there are no write tools" below), and no tool returns pay data: /salaries and /wagesheets are never called, and the pay elements that StaffSavvy includes in time-entry responses are dropped, never returned.
Tools
Tool | What it does | API calls |
| Checks that the credentials work and returns the software and API version numbers. The API has no endpoint that says which user the credentials belong to. |
|
| Venues with the groups and shift actions allowed at each. |
|
| Groups (skills or roles) with id and title. |
|
| Shifts filtered by date range (on the shift start, whole days), venue, group and venue event, with the staff member's name. |
|
| One shift by ID. |
|
| Absences and holidays with the staff member's name, type, dates and hours, filtered by staff member and by date range, and optionally including deleted or rejected ones (marked |
|
| Absence types with id, title and whether each is an absence or a holiday. |
|
| Clocked time with staff member, start, end, venue, role, status, approval and notes, filtered by date range (on the entry start, whole days), staff member and venue event. |
|
| Events at venues with date, start hour, venue, title and cost codes, filtered by date range and venue. |
|
| Staff accounts: name, known-as name, default venues; email, phone and legal name only on request. Filters by first name and last name (the documented equals filter), a list of IDs, or changed since a date. |
|
Every list tool takes page (where to start) and max_pages (how many API pages to fetch, default 4, at most 20). StaffSavvy sets the page size; a result with complete: false names the next_page to continue from, or, when the listing cannot be continued, says why in note.
Date filters. Shift and time-entry starts are date-times, so from/to are sent as whole days in the date-time "between" form the docs show for /shifts (start=[2026-10-03 00:00:00~2026-10-03 23:59:59]), with lte:2026-10-03 23:59:59 for to alone and the documented gte:2026-10-03 for from alone. Absence starts and venue-event dates are dates, so those use the date form ([2026-10-01:2026-10-31]). StaffSavvy documents absence filters on the start only, so an absence that began before from (Monday's "who is off this week" when someone's leave started on Friday) would be missed by a plain start filter. list_absences therefore asks for absences starting from lookback_days before from up to to, drops the ones whose period_end is before from, and says in range_note how many it dropped. An absence that started more than lookback_days before from (long-term sickness, parental leave) is not found unless lookback_days is raised (at most 366). In that mode total is left out, because StaffSavvy's total counts the wider start range.
The staff-name lookup uses the documented "one of the options" filter on /accounts (id=[101,102,103]), 50 IDs per query, following each query's pages to the end, and can be switched off with include_staff_names: false. If the API user may not read accounts (a 403, or a 400), the tool still answers, with staff IDs only and a note. If the lookup is cut short (the time budget ran out, or the paging could not be followed), the note names the IDs that were not looked up; only when the lookup finished does it say that no account record was returned for an ID.
Not covered on purpose: /salaries, /wagesheets/* and the pay elements on time entries and groups (pay data, never exposed); /reports/* and /report/* (custom reports); /my/shifts (the API user's own shifts); the shift and time-entry histories; the /help endpoints; get-by-id for absences, time entries, venue events and accounts; /shift-actions; and every PUT and PATCH.
Why there are no write tools
The candidates were creating an absence and accepting or rejecting a shift. Neither is documented clearly enough to ship without testing on a real account:
PUT /absences/newtakes everything as query parameters. It requiresrepeat, whose values are not documented; it has no parameter that says whose absence it is;absence-typeis typed as a string although absence types have integer IDs; and the only documented answers are201("absence updated") and304, with no body.PATCH /my/shifts/{shiftid}/actiontakes a required_actionquery parameter typed as an object withaccept,rejectandrequest-coverbooleans, without saying how that object is written into the URL.
A write that sends the wrong thing to a live rota is worse than no write, so these wait for an account to test on (see "Going to production"). STAFFSAVVY_ALLOW_WRITES is accepted but has no effect in this version.
Related MCP server: Tipsoi MCP
Setup
Requires Node 18 or later.
npm install
npm run buildYou need an API User and API Key for your StaffSavvy instance. From the support article: give the account's level the API permissions it needs (System > Levels & Permissions > Manage Permissions; a dedicated level can be set to "API only" so the account cannot log in to the main interface), then open My Account > API Access as that account to see the API User and API Key. The same page can restrict the key to certain IP addresses, and a replaced key keeps working for 14 days unless it is cancelled.
The base URL is your instance's address followed by /api/v1 (the article: "The API endpoint is [your instance url]/api/v1/").
Claude Desktop: add this to claude_desktop_config.json:
{
"mcpServers": {
"staffsavvy": {
"command": "node",
"args": ["/absolute/path/to/staffsavvy-mcp/dist/index.js"],
"env": {
"STAFFSAVVY_BASE_URL": "https://your-instance-address/api/v1",
"STAFFSAVVY_API_USER": "your-api-user-id",
"STAFFSAVVY_API_KEY": "your-api-key"
}
}
}
}Claude Code:
claude mcp add staffsavvy -e STAFFSAVVY_BASE_URL=https://your-instance-address/api/v1 -e STAFFSAVVY_API_USER=your-api-user-id -e STAFFSAVVY_API_KEY=your-api-key -- node /absolute/path/to/staffsavvy-mcp/dist/index.jsVariable | Required | Meaning |
| yes | Your instance address followed by |
| yes | The API User ID from My Account > API Access, sent as the |
| yes | The API Key from the same page, sent as the |
| no | Has no effect: this version has no write tools. |
| no | Seconds a tool call may spend on retry waits and further pages (default 45; see Safety defaults). Lowered by the tests. |
Safety defaults
Read-only. Every tool carries the MCP
readOnlyHintannotation (anddestructiveHint: false), and the server only sendsGETrequests.The API User and API Key are sent only to
GET /auth, as thex-userandx-authrequest headers, never in a URL. The spec lists them as query parameters onGET /auth; the support article says they "can be passed in the GET, POST or REQUEST HEADER", and headers keep the key out of proxy and server logs. The token that comes back is sent asAuthorization: Bearer …on every other request. It is kept until a call answers 401 (the article: tokens "expire after a period of inactivity"), then fetched again once; if the fresh token is refused too, the error says to check the API user's permissions. The API key and every token issued are replaced with[redacted]in any message passed on from StaffSavvy.Staff are identified by ID and name by default. Email addresses, phone numbers and legal names (
legal_firstname,middlenames,legal_lastname) are only returned bylist_accountswithinclude_contact_details=true.On absences, the free-text
period_Category(the spec's example is "Sinus infection"),period_titleandabsence_shift_excusecan hold health or other personal information and are only returned withinclude_contact_details=true. The absence type (for example "Sickness") is returned by default, since that is what the tool is for.Pay data is never returned, with or without
include_contact_details: thepay-elementandpay-element-valueof time entries are dropped, the group formatter reads onlyidandtitle(the spec'sGroupscomponent carries a pay rate), and/salariesand/wagesheetsare never called.In free text (shift arrival information, time-entry notes, status and role titles, staff names, absence shift-repost text) email addresses, phone-number-like sequences, UK postcodes and dates of birth are replaced with
[email redacted],[phone redacted],[postcode redacted]and[date of birth redacted]by default, and returned as stored withinclude_contact_details=true. Venue, group and absence type titles, the absence type'stype, and venue event titles,uuidand cost codes are always redacted this way (those tools have no switch). Payment card numbers (13 to 19 digits that pass the Luhn check) are replaced with[card number redacted]whether or not contact details were requested. These are heuristics: the phone match covers international numbers written with+or00(including the+44 (0)7700 …form),44…without a prefix (like the spec's own447815000000example), UK numbers with a bracketed area code such as(020) 7946 0958, UK-style0…numbers of 9 to 11 digits and 10-digit mobiles written without the0(7700 900123), with spaces, dots or hyphens between groups; other digit strings of those shapes are redacted too, while numeric IDs, dates and hyphenated references are left alone. Postcodes are matched in capitals only (BH9 2SQ). A date is treated as a date of birth only after "DOB", "date of birth" or "born"; other dates are left alone. Street addresses without a postcode are not detected. The same redaction is applied to StaffSavvy's error messages before they are passed on.Shift records: the spec's
Shiftschema documents onlyarrival-info. The server readsid,start,end,venue,group,account,taskandevent(names inferred from the documented/shiftsfilters and parameters; see Status). Any other key a shift carries is listed by name inother_fields, never by value.Input is checked before any call: IDs must be positive whole numbers (every path ID in the spec is an integer); dates must be real
YYYY-MM-DDcalendar dates (2026-02-30 is refused) withfromnot afterto; first and last names may not contain the filter operator characters[ ] : ~ ,.StaffSavvy documents no rate limit. Requests are spaced 250 ms apart (about four per second). A 429 is retried at most twice, waiting for
Retry-After(whole or fractional seconds, or an HTTP-date; 2 s then 4 s when the header is absent or unreadable). If StaffSavvy asks for a wait longer than 10 seconds, that request gives up at once and the message says how long to wait, so one request waits at most about 20 seconds. One tool call can make many requests (a token request, up to 20 list pages, the staff-name lookups), so the cap alone does not keep a call under the MCP client's default 60-second request timeout. Each tool call therefore also has a 45-second budget shared by all its requests (STAFFSAVVY_TOOL_BUDGET_S): a retry whose wait would end after it is not attempted; a list that runs out of budget after its first page returns the pages it has withcomplete: falseand thenext_pageto continue from; a staff-name lookup that runs out returns the records by staff ID with a note; otherwise the call fails saying so. 45 seconds leaves room for the last request under the 60-second default, but a single request that is slow to answer is not cut off, so a very slow StaffSavvy can still make a call time out. The tests check the mechanism with a 3-second budget, not the 60-second outcome.Paging always advances from the page that was asked for. If a response's
currentis not that page (an API that ignorespagewould answer page 1 every time), the listing stops withcomplete: falseand a note, and that response's records are dropped, so the same records are never returned twice.502, 503 and 504 are retried the same way for
GETonly (the only method this server sends); when all three attempts fail the error says the service may be unavailable and to try again in a few minutes, without the gateway's HTML.A 200 whose body is not a JSON object (a proxy, a login page, a wrong base URL) and a 200 with
success: falseare reported as errors, never as empty lists. A get-by-id answered with an emptydataarray is reported as not found, like a 404.Rejected credentials, a wrong base URL (404 on
/auth), a 403 and a 400 each produce a message that says what to check.
Tests
npm testThe test suite runs in about 50 seconds:
Validates every fixture record against the schemas in StaffSavvy's published OpenAPI document (
Venue,AccountItemand theGET /accounts/{accountid}item,Shift,AbsenceItem,TimeEntry,VenueEventItem,Paging, and the inline items ofGET /groups,/absence-typesand/info) with Ajv andajv-formats. The document is downloaded fromapi.swaggerhub.com/apis/SmartBlue/StaffSavvy/1.1.0tospec.jsonon the first run. Because the schemas set noadditionalProperties: false, every fixture is also walked key by key against its schema, and a key the schema does not declare fails the check, except for a listed set of inferred keys: the shift fields (theShiftschema declares onlyarrival-info),idon groups and venue events, andapprovedon the deleted absence (from theinc-deletedfilter note). Shifts are additionally validated against a schema written for this suite from the documented/shiftsfilter and parameter names, which is an assumption, not StaffSavvy's. Negative controls check that the schemas and the key walk still reject what they should.Starts a local mock of the API under
/api/v1that serves those fixtures with the documentedpagepagination (25 per page, the spec'sper-pageexample), the documented filter operators (equals,gte:,lte:,[a:b]and[a~b]between,[a,b,c]one of, andinc-deleted=1) compared strictly (a date-only bound does not reach into that day's date-times, which the suite checks, so the server's filters cannot pass on a lenient reading of the docs),GET /authwithx-user/x-authheaders issuing bearer tokens, 401 on bad credentials or an unknown token, 404 for unknown IDs, 400 for an unexpected query parameter, and 429, 502, 503, HTML,success: false, delayed and other substitute answers when armed (per path, optionally per page). The mock's/auth, list, get-by-id and error responses are validated against the documented response schemas. Error bodies are{success: false, message}: the spec documents no error body, so this shape is an assumption.Starts the built server and drives it over stdio with the official MCP client: 28 checks covering tools/list and annotations (10 read-only tools,
STAFFSAVVY_ALLOW_WRITES=trueadding nothing); the token fetched once fromGET /authwith header credentials and reused; every tool; paging topaging.lastacross pages 1, 2 and 3 with no fourth request, requests spaced at least about 250 ms apart, andmax_pages/pagecontinuation; the paging fallbacks (no paging object,total-data-countalone,page-countalone, an empty last page) and an API that ignorespage; each filter passed through exactly (shift and time-entrystartas[from 00:00:00~to 23:59:59],gte:fromandlte:to 23:59:59, absencestartas[from minus lookback:to],venue,group,event,account,inc-deleted=1,dateon venue events,firstname,lastname,id=[…],_last_history_record=gte:…); absences overlapping the range found (leave that started beforefrom), ended ones dropped, and none found withlookback_days: 0; a deleted absence marked by its negativeapproved; staff names for 60 staff looked up in twoid=[…]queries of 50 and 10 IDs, the first spanning two pages, and skipped when switched off; the "no account record" note for an unknown staff ID and the "cut short" note when the lookup cannot finish; contact details, legal names and absence reasons withheld by default and returned on request; redaction of emails and phone numbers in arrival information, notes, names and venue, group, absence type and venue event titles, and names returned as stored on request; one text with a postcode, a date of birth,44…,7…and+44phone numbers, a card number and an email injected into the free-text fields ofget_shift,list_venue_events(title,uuid, cost codes),list_time_entries(notes, status, role),list_absence_types(title, type),list_groups,list_venues,list_accountsandlist_absences, and into an error message, with none of them coming back by default, and the arrival information returned as stored on request except for the card number; pay elements and values on time entries and a pay rate on a group never returned; unread shift keys named without their values; ID, date and name validation before any call; the 404 and empty-datanot-found messages; an expired token fetched again once and a persistently refused token reported; the 429 retry waiting forRetry-Afterin the seconds, fractional-seconds and HTTP-date forms and 2 s then 4 s without it, giving up after three attempts and at once above the 10 s cap; with a 3-second tool budget, retry waits on/authand/shiftsadding up past the budget (each well under the cap) ending the call, a rate-limited second page, or a slow first page that uses up the budget, returning the first page withcomplete: falseandnext_page, and a rate-limited name lookup returning the shifts by staff ID with a note, the rate-limited cases answering in under 3.5 s; a 502 retried and three 503s reported without HTML; non-JSON andsuccess: false200s reported as errors; an error message with an email, a phone number, the API key and the token passed on redacted and scrubbed; a 403 or a 400 on the name lookup degrading to IDs with a note; wrong credentials and a wrong base URL; start-up refused without the three variables, or with a base URL that is nothttp(s), carries a username and password (not repeated in the message) or has a query string; and, last, that every request of the whole run (including the wrong-credential and wrong-base-URL ones) was aGETto a documented path, with credentials only as/authheaders, an issued bearer token and nox-userorx-authheader everywhere else, and no salary, wage-sheet or report endpoint called.
Status
This is a working prototype. It has not yet been run against the live API, because it was built without a StaffSavvy account (no self-service trial was found; the pricing page offers a demo). Everything below comes from the published spec and support article and should be confirmed on a real instance:
Authentication: that
GET /authacceptsx-userandx-authas request headers (the article says headers, GET or POST variables work; the spec lists query parameters only), that it answers{success, token}, that the token is sent asAuthorization: Bearer <token>(the spec'sbearerAuthscheme), how long a token lives, and what an expired token returns (the article says tokens expire after inactivity; the server assumes a 401).Error bodies and status codes. The spec documents 401 "Unauthorised" and 400 "bad input parameter" without a body, and no 403, 404 or 429. The server reads a
messagefield when there is one. How an unknown ID is reported (404, 400, or 200 with an emptydataarray) is not documented; all three are handled.Pagination: that
pageis 1-based, thatcurrentechoes the page asked for (the server stops if it does not), whatlastandpage-countmean (the server treatslastas the last page number, falling back topage-count; the spec's own example givestotal-data-count100,per-page25,page-count1 andlast10, which do not agree with each other), what the page size is (no page-size parameter is documented), and what a page past the end returns.Shift fields. The
Shiftschema documents onlyarrival-info. The fieldsid,start,end,venue,group,account,taskandeventare inferred from the/shiftsfilter examples (group,start,venue), the parameters ofPUT /shifts/newandPUT /shifts/{shiftid}(start,end,account,group,venue,task) and theeventquery parameter; whether they come back as numbers or as objects is unknown (both are handled). A live run shows any other keys inother_fields.GET /venues: the spec's 200 schema has nodataarray (onlyallowed-groups,allowed-shift-actionsand_available_actionsat the top level); the server readsdatalike every other list.idis not in theGET /groupsitem schema or inVenueEventItem; the server reads it when present.Filters. The spec's
filtersparameter says to "use variable name as the parameter", so the server sendsstart=[2026-10-01:2026-10-07]rather thanfilters=…. To confirm: that bracketed and colon values are accepted URL-encoded; whether "between" includes both ends; that the date-time forms the server sends for shift and time-entry starts ([… 00:00:00~… 23:59:59],lte:… 23:59:59) are accepted (the~form is documented for/shifts,/absencesand/venue-events; the/time-entriesdocs show only the generic[valueStart:valueEnd]); whether an absence can be filtered on its end (none is documented, hence thelookback_daysapproach); that absences filter onstart(the documented examples usestartalthough the record field isperiod_start); that venue events filter ondate(their filter examples are copied from absences and usestart); that time entries filter onstart; howinc-deleted=1behaves; that_last_history_record=gte:…works on/accounts; and whetherfirstname/lastnamematching is exact and case-sensitive.The sort order of every list. None is documented; the server returns what the API returns.
approvedon absences: theAbsenceItemschema does not declare it; theinc-deletedfilter note says deleted or rejected absences are "Denoted by a negative approved integer", so it is read when present. What an active absence carries there is not documented.The shape of
accounton absences and time entries (Accountis{id, access}in the spec), and whatperiod_end_leastmeans on an absence (it is passed through asperiod_end_least).Date-times are documented as local time (
YYYY-MM-DD HH:MM:SS); the server passes them through unchanged.What a level without a given API permission returns (403, 401 or
success: false), and what an IP-restricted key returns from a non-allowed address.How many requests per second the API tolerates; nothing is documented, so the 250 ms spacing is a guess on the polite side.
Going to production
This version runs locally over stdio, with an API User and Key that each customer creates in their own instance. For managers to connect from claude.ai or ChatGPT without handling keys, the next step is a remote server (Streamable HTTP) behind OAuth, hosted by StaffSavvy, where each instance's own login decides what a user can see, and then a listing in the Claude and ChatGPT connector directories. Write tools (booking absences, accepting or rejecting shifts, creating shifts) can follow once their parameters are confirmed on a test instance.
Licence
MIT. Built by Alexandru Dragoș (alexandru.dragos96@gmail.com) with an AI agent (Claude) working under his direction.
This server cannot be deployed
Maintenance
Related MCP Connectors
Read-only MCP tools for AI agent discovery, structured resources, and NIULAI information.
Read-only MCP access to a documented IT fleet: state, changes, posture. 15 tools.
Read-only MCP server for Muovi, Argentina's trust-first local services marketplace (6 tools).
Read-only finance and operations controls for AI agents with evidence and safe next actions.
Related MCP Servers
- AlicenseBqualityBmaintenanceEnables Claude, Cursor, and other MCP clients to query PeopleForce HRIS data (employees, time-off, recruitment) via 27 read-only tools.284MIT
- FlicenseAqualityDmaintenanceRead-only MCP server for the Tipsoi HRM API, exposing 15 tools to read employee data, attendance, leave, overtime, and more.151-
- AlicenseBqualityDmaintenanceEnables querying ChurchSuite bookings, resources, ministries, and serving patterns via MCP tools, providing read-only access to church management data.16MIT
- FlicenseNot gradedqualityBmaintenanceEnables read-only access to Workday HCM data such as workers, organizations, locations, job profiles, and cost centers through MCP tools an LLM can call.-