Edays 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., "@Edays MCP serverWho is off next week, and is anything still waiting for approval?"
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.
Edays MCP server
An MCP server that lets Claude, ChatGPT and other MCP clients work with an Edays absence management system: employees, absences, absence types, entitlement balances, rotas, public holiday and custom day patterns and groups, and (when enabled) booking, updating and deleting absences. It is built from the public API V2 documentation at developer.e-days.co.uk. Edays publishes no OpenAPI document for API V2, only JSON examples, so the tests validate against schemas written from those examples and check the schemas against the examples themselves.
Once it's connected, an HR or line manager on the system can ask things like:
"Who is off next week, and is anything still waiting for approval?"
"How much holiday does Dana Barrett have left this year, and how many sick days has she had in the last three months?"
"Which team is Willie in, and who approves his leave?"
"What rota is Priya on, and which public holiday pattern applies to her?"
With writes enabled: "Book Dana two days' holiday on 13 and 14 October." / "Approve Willie's pending holiday request."
Tools
Tool | What it does | API calls |
| Every user on the system, filtered locally by part of the name or partner ID; leavers skipped unless asked. The endpoint is not marked as paged in the documentation; the paging headers are read anyway and further pages fetched if the system does page it, with a |
|
| One user by partner ID with their groups and their authorisation hierarchy (step one and two authorisers and alternates). |
|
| Absence records system-wide or for one user, with status, start and end, duration and absence type ID, in whole API pages up to |
|
| One absence record by GUID. |
|
| Absence types with record type (planned, unplanned) and booking and calendar flags; the API returns Custom Day Groups (5) and Public Holiday Groups (6) from the same endpoint. |
|
| A user's deducting balances (annual entitlement, transfers, pending, booked, taken, untaken, remaining, per booking period and element), summing balances (year to date, last 6 and 3 months, last 30 days) and entitlement pots. |
|
| A user's rota assignments with start dates, and their public holiday and custom day patterns, named from the system's lists. |
|
| The public holiday patterns and custom day patterns held in the system (id and name; API V2 does not expose the dates inside a pattern). |
|
| Group types (Country, Location, Team...) and the groups in each, or one type. |
|
| Creates an absence for a user GUID with the documented body ( |
|
| Fetches an absence and PUTs the documented body with your changes merged in: type, status (the only documented way to approve, reject or cancel through API V2), start, end, open flag, details. Writes only, marked destructive. |
|
| Deletes an absence record with the documented DELETE. Writes only, marked destructive. |
|
There are no approve_absence or reject_absence tools because API V2 documents no such endpoints; the "Managing Absences" section says the status of an existing record is changed with PUT /api/v2/absences/{id}, which is what update_absence does with status: "Approved" or "Rejected".
Not covered on purpose: creating, editing, patching and deleting users, marking leavers and reinstating, user settings and email notifications, roles and bulk roles, rota, public holiday and custom day assignment, entitlement adjustments, authorisation hierarchies (writes), user templates, group and group type writes, bulk user-group membership, global entitlement configuration and user balances, SSO certificates and IdP configuration, and the remaining lookup lists.
Related MCP server: mcp-server-personio
Setup
Requires Node 18 or later.
npm install
npm run buildYou need API client credentials for your Edays system. As the Authentication section documents: create a dedicated user in Edays, on its Roles tab select the account type Api Client, and generate a Client ID and Client Secret there (the secret is shown once; regenerating it replaces the old one). The server exchanges them for a Bearer access token at https://YOUR-SYSTEM.e-days.co.uk/token (OAuth 2.0 client credentials, form-encoded), which the documentation says is valid for one hour.
YOUR-SYSTEM is the subdomain of the address you sign in at.
Claude Desktop: add this to claude_desktop_config.json:
{
"mcpServers": {
"edays": {
"command": "node",
"args": ["/absolute/path/to/edays-mcp/dist/index.js"],
"env": { "EDAYS_SYSTEM": "your-system", "EDAYS_CLIENT_ID": "your-client-id", "EDAYS_CLIENT_SECRET": "your-client-secret" }
}
}
}Claude Code:
claude mcp add edays -e EDAYS_SYSTEM=your-system -e EDAYS_CLIENT_ID=your-client-id -e EDAYS_CLIENT_SECRET=your-client-secret -- node /absolute/path/to/edays-mcp/dist/index.jsVariable | Required | Meaning |
| yes, unless | The subdomain of your Edays system: |
| yes | The Api Client user's Client ID, sent in the token request body. |
| yes | The Api Client user's Client Secret, sent in the token request body. Never logged or included in an error message. |
| no |
|
| no | Overrides the system URL, e.g. |
Safety defaults
Read-only unless
EDAYS_ALLOW_WRITES=true. Read tools carry the MCPreadOnlyHintannotation;update_absenceandcancel_absenceare marked destructive (a PUT replaces the whole record, and DELETE removes it);book_absenceis not.Employee records are third-party personal and HR data. By default a user's
Email,Login,HomeEmail,HomePhone,WorkPhone,WorkPhoneExt,HomeAddress,NextOfKin,NextOfKinContactDetails,Dob,PayrollNumber,EmployeeNumber,SsoUserId,ClientProvidedIdandAnnualPayare not returned, an absence'sPayrollNumberandEmployeeNumberare not returned, and an entitlement'sLoginis not returned. In every other free-text field (names, job titles, absence type, entitlement, rota, pattern, group and group type names, and Edays' own error messages, including the excerpt of a non-JSON body) email addresses are replaced with[email redacted], phone-number-like sequences with[phone redacted]and UK postcodes with[postcode redacted]. Dates inside free text are not redacted (a date in a rota or group name is far more likely to be a schedule than a date of birth; theDobfield itself is withheld).include_contact_details=truereturns all of it as stored. Names and partner IDs are always returned as stored, including the authoriser partner IDs onget_user: the partner ID is the key every user endpoint is addressed by, so on a system whose partner IDs are login email addresses those addresses appear by default. The phone match is a heuristic: it covers international numbers written with+or00(including the+44 (0)7700 …form), UK numbers with a bracketed area code such as(020) 7946 0958, and UK-style0…numbers of 9 to 11 digits with spaces, dots or hyphens between groups; other digit strings that happen to start with0are redacted too, while GUIDs, numeric IDs and timestamps are left alone. The postcode match is upper case only (CF64 3DH,SW1A 1AA,EC1A1BB), so another code written in that shape would be redacted too. Bank and payment details do not exist in API V2.The access token is held in memory only, refreshed a minute before its documented one-hour expiry, and fetched afresh once when a call answers 401; it never appears in logs or error messages, and neither does the client secret. Only a JSON message from the token endpoint is ever passed on, never its raw body: a 200 without an
access_tokenwhere the server looks is reported with the body's top-level key names (a bare value could be the token under another key), and a 5xx fromPOST /tokenis retried like a GET (fetching a token is idempotent) and reported without the gateway's HTML.list_absencesreturns whole API pages, never part of one. The page size sent ismin(100, max_results), and a call stops before a page that could take it pastmax_results(allowing for a shorter last page when the total says fewer remain), socountcan be belowmax_results. The result carriespage_size,next_pageand anotesaying to call again with that page and the samemax_results, because page numbers only line up for one page size. The end of the list is judged from the records actually received against the documentededays-pagination-totalheader, not from theedays-pagination-page-sizeheader alone: no maximum page size is documented, and a cap that still echoed the requested size would otherwise end a list after one page. A continuation call that starts on a short page therefore confirms the end with one extra request, which returns an empty page.IDs are checked before any call is made: partner IDs may be any single path segment (spaces included, as in the documented
"Phil Jones"; no slashes, control characters or leading or trailing spaces; at most 200 characters) and are URL-encoded; absence and user GUIDs must look like GUIDs; absence type IDs must be positive integers. The four names documented directly under/api/v2/users/(autosetupauthorisers,recalculateauthorisationhierarchy,authorisation,applyRotaToUsers) are refused as user IDs in any letter case: the first two change data when fetched with GET (they add users to the authoriser role and reassign pending requests), so a read-only tool must never reach them.date_from/date_totakeYYYY-MM-DD(or the documentedYYYYMMDD) and real calendar dates only. Absence times takeYYYY-MM-DD HH:MMor ISO 8601 with aTand are sent in the documentedYYYY-MM-DD HH:MMform; no time zone is accepted because none is documented.12/08/2026, a bare year,25:00and a trailingZare refused locally.book_absenceandupdate_absencerefuse an end before the start,update_absencerefuses a call with no change, andlist_absencesrefuses the system-wide filters together withpartner_user_id, because the per-user endpoint documents onlyrecordtype,absencetype,datestartanddateend.Edays does not document a rate limit anywhere on the API V2 page. Requests are spaced 250 ms apart (about four per second). A 429 is retried at most twice for any method, including
POST /api/v2/absences, on the assumption that a rate-limited request was not processed. The retry waits forRetry-After(whole or fractional seconds, or an HTTP-date; 2 s then 4 s when the header is absent or unreadable). Each wait is capped at 10 seconds so a single request stays well under the MCP client's default 60-second request timeout: if Edays asks for a longer wait the call gives up at once and the message says how long to wait. The cap is per request, not per tool call:list_absencescan make up to 20 requests in one call andget_user_rotaup to six, so a long run of 429s across those could still exceed the client's timeout.502, 503 and 504 are retried the same way for
GETand forPOST /tokenonly; 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. APOST,PUTorDELETEon an absence is never retried after a gateway error, because the request may already have been processed and a retry could book the same absence twice; the error says to check withlist_absencesorget_absencebefore repeating it.A 200 whose body is not JSON (a proxy or a login page in the way) is reported as an error naming
EDAYS_SYSTEM/EDAYS_BASE_URL, never as an empty list or an empty record; the 60-character excerpt of the body in that message goes through the same contact redaction as everything else.Rejected client credentials produce a message that says which variables to fix and where the credentials come from; a 401 that survives a token refresh says to check the Api Client user; a 403 says the user's roles do not allow the operation; a 400 passes on Edays' message and, if present, its
ModelStatevalidation errors field by field.
Tests
npm testThe test suite:
Extracts the documentation into
spec.jsonon the first run:test/extract-examples.mjsfetcheshttps://developer.e-days.co.uk/, drops the parts of the page that sit in HTML comments (they are not published), and records every "Resource URL" with its "Supported HTTP Methods" line and every "Example ... Request/Response" JSON block (119 examples on 90 resources; 6 examples are not valid JSON on the page and are recorded as such; a repeated Resource URL continues the same resource, and the one resource with no methods line, the bulk authorisation PATCH, takes its method from its example and is marked as inferred). The JSON schemas intest/schemas.mjswere written by hand from those examples (every documented key required, no other keys, types as shown); the first check validates each schema against the documented example it came from (26 examples), confirms every endpoint this server calls is documented with that method, that the two data-changing GET endpoints under/api/v2/users/are indeed documented as such, and that the mock's client ID and secret are obviously fake values that do not appear on the documentation page. The second check validates every fixture record against the schemas.Starts a local mock of the system:
POST /tokenwith the documented form-encoded client-credentials grant (answering the documented one-element array, or a plain object when told to), a 400invalid_clientfor wrong credentials, a 401 for any API call without a token the mock issued, the endpoints used here with the documentedpage/pagesizepaging and the fouredays-pagination-*headers (absences capped at 4 per page so lists span several pages; on request the page-size header echoes the requested size instead, andGET /api/v2/userspages too), 404s for unknown IDs,POST /api/v2/absencesanswering 201 (with the record, or with no body when told to), 204 on PUT and DELETE, injected failures on any endpoint including/token, and a one-off 429 on the firstGET /api/v2/absencetypes. The third check validates the mock's token and list, detail and write responses against the schemas.Starts the built server and drives it over stdio with the official MCP client: 29 checks (32 in the whole suite) covering every tool, tool annotations, the token fetched with the documented body before the first API call and reused on every call as a Bearer header, a revoked token refreshed once with the call retried, a token near expiry refreshed before it expires, the object-shaped token response, page-based pagination stopping at
edays-pagination-totaland returning whole pages only (following the tool's own continuation notes from a first page and from a later one yields every record exactly once;max_resultsequal to the total fetches the short last page; a page-size header that echoes the requested size while serving fewer does not end the walk early),list_usersfollowing the paging headers whenGET /api/v2/userspages and saying so, every documentedlist_absencesfilter passed through exactly (datestart/dateendfromYYYY-MM-DDandYYYYMMDD,recordtype,absencetype,userId,groupId,dateCreated,dateModified) with bad dates (including a mix of the two date forms) and IDs refused before any call, the four reserved endpoint names under/api/v2/users/(read from the documentation, in any letter case) refused by every user tool with no request made, a partner ID with a space sent as one encoded segment and the single-user URL sent with its documented trailing slash, the per-user endpoint with its three filters and the refusal of the others, redaction of contact and HR details and of emails, phone numbers and postcodes typed into names, job titles, rota and group names by default and their return on request, authoriser partner IDs returned as stored, absence types named for record types 1, 2, 5 and 6, entitlement balances with booking period and time unit names, rotas and patterns named from the lists with the lists cached, thePOSTandPUTbodies validated against the schemas from the documented POST and PUT examples (times normalised,Detailssent empty on a booking when omitted and left out of a PUT when not given), the DELETE, the 429 retry waiting forRetry-Afterin the seconds, fractional-seconds and HTTP-date forms, giving up after three attempts on a persistent 429 and at once on aRetry-Afterabove the cap, a 429 onPOSTretried once, a 502 retried forGETand never forPOST,PUTorDELETE, a 401 that survives a token refresh reported with the Api Client advice, aGETfailing three times with 503 reported with advice and without the gateway HTML, a 503 onPOST /tokenretried and reported the same way, a 429 onPOST /tokenwithoutRetry-Afterretried after the 2 s fallback, a 200 fromPOST /tokenwithout a token reported with key names only (never a value), Edays' own error text passed on with contact details redacted andModelStateerrors listed, the 403 message, a 200 with a non-JSON body reported as an error with the excerpt redacted, the write gate with the variable unset and set tofalse, the 400 for wrong client credentials naming the variables without echoing the secret, a badEDAYS_SYSTEMor a missing secret stopping the server at start-up,EDAYS_SYSTEMalone producinghttps://<system>.e-days.co.uk(read from the start-up line; no request is made to that host), and that every request either carried the documented form body to/tokenwith noAuthorizationheader or carried a Bearer token the mock issued to a documented method and path, none of them a reserved endpoint name in place of a user.
Status
This is a working prototype. It has not yet been run against the live API, because it was built without an Edays system or Api Client credentials. Everything below is taken from the documentation page and should be confirmed on a real system:
The token endpoint: whether the response is the one-element array shown in the documented example or a plain object (both are accepted), what a wrong Client ID or Secret answers (the mock uses the OAuth 2.0
400 invalid_client; the page documents nothing), and whether the API answers 401 for an expired token (the server refreshes once on a 401 and a minute before the documented hour either way).Whether
GET /api/v2/users/{partnerUserId}returnsEdaysId. The documented example omits it while the list example has it, soget_userreports noedays_idandlist_usersdoes; the mock follows the examples. The server sends the documented URL with its trailing slash (the mock accepts both forms).Whether
GET /api/v2/userspages. The page marks only the two absence lists as paged, but that marker is not exhaustive (the/usergroupsendpoint says in prose that it pages at 500), solist_usersreads the paging headers and fetches further pages if the first answer is short ofedays-pagination-total; untested against a real system, as is how long the call takes on a large one. Whether it includes leavers (IsLeaveris filtered locally), and whether a real record ever carriesnullwhere the examples show empty strings (the formatter copes; the schemas allownullonly forEmploymentStartDateandContinuousStartDate, where the examples show it).Whether a partner ID with a space (
"Phil Jones"in the documented authorisation example) really is addressable as/api/v2/users/Phil%20Jones/; the server accepts and encodes it.Paging: the query parameter is written
pageSizein the Paging text andpagesizein every example; the server sendspagesize. No maximum page size is documented (the default is 50, the paging example shows 500); the server asks for at most 100 and ends the list when the records received reachedays-pagination-total, taking a page shorter than requested as the end only when that header is missing. What a page past the end returns is not documented (the mock answers 200 with an empty list).The
datestart/dateendfilters: whether a record must start inside the range or merely overlap it, and whether the bounds are inclusive (the mock uses inclusive overlap). The formats ofdateCreatedanddateModifiedand whethergroupIdtakes the group's GUID or its partner ID are not documented; those three values are sent exactly as given.userIdis assumed to be the GUID shown asUserIdon absence records andEdaysIdon users (the examples show the same value for both).POST /api/v2/absences: whether it answers 200 or 201, what the body is (the page documents neither; the tool formats a body that looks like an absence record and passes anything else through), and whether aLocationheader is sent. WhichStatusvalues it accepts on creation (the example usesPending; the tool offers the seven texts from/api/v2/lists/recordstatus) and whether the caller's roles allow booking for others.PUT /api/v2/absences/{id}: whether it answers 200, 201 or 204 (all documented; the tool re-reads the record when no body comes back), and what happens toDetailswhen the key is omitted, sinceGETdoes not return it. Whether settingStatustoApproved,RejectedorCancelledthrough PUT really approves, rejects or cancels the request as the UI would, including notifications.DELETE /api/v2/absences/{id}: whether the record is removed or kept as Cancelled, and what the response is beyond the documented 204.The time zone of
StartTime/EndTime(none is documented; the server sends the values as given) and the "midnight to midnight" rule for day-based absences quoted from the Managing Absences section.The record type discriminator names 5 (Custom Day Group) and 6 (Public Holiday Group) come from the Absence Types section; 1 and 2 from the lists section. Booking period and time unit names come from the documented
/api/v2/lists/bookingperiodsand/api/v2/lists/timeunitsexamples and are mapped locally rather than fetched.The documented example for
GET /api/v2/users/{partnerUserId}/customdaysis not valid JSON ({ [ "Pattern": 3 ] }); the server reads that endpoint likepublicholidays([{"Pattern": n}]).Error bodies: none are documented. The server reads
Message,message,error,error_description,ExceptionMessageandModelStateand redacts contact details from them regardless.Rate limits: nothing is documented anywhere on the page (the word does not appear), so the 250 ms spacing here is a guess on the polite side, and a 429 on
POSTis retried on the assumption that a rate-limited request was not processed.Which roles the Api Client user needs for each endpoint; a 403 is reported with that explanation but the page does not describe the role model.
Going to production
This version runs locally over stdio, with the customer's own Api Client credentials. For customers to connect from claude.ai or ChatGPT without handling credentials, the next step is a remote server (Streamable HTTP) behind OAuth, hosted by Edays, and then a listing in the Claude and ChatGPT connector directories.
Licence
MIT. Built by Alexandru Dragoș (alexandru.dragos96@gmail.com) with an AI agent (Claude) working under his direction.
Available Tools
9 toolsget_absenceGet an absenceARead-only
One absence record by its GUID: user, absence type, status, start and end, duration, time unit, created and modified dates. Payroll and employee numbers only with include_contact_details.
| Name | Required | Description | Default |
|---|---|---|---|
| absence_id | Yes | Absence ID (GUID) | |
| include_contact_details | No | Include payroll and employee numbers, and stop redacting email addresses, phone numbers and UK postcodes typed into names and other text |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and openWorldHint=true, so safety is covered. The description adds real behavioral context beyond that: payload/employee numbers and unredacted contact data appear only when include_contact_details is set, which tells the agent the default response is redacted.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two compact sentences with the identifier and returned fields front-loaded, then the conditional flag behavior. The enumerated field list is dense but each item is informative; nothing is wasted.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description usefully enumerates the returned fields and flags the conditional redaction, which is the key behavioral detail. It is nearly complete; only error/not-found behavior and any pagination-free nature remain unaddressed.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so both parameters are already fully documented in the schema (including the redaction behavior of include_contact_details). The description restates that linkage but adds no syntax, format, or edge-case detail beyond what the schema provides; baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource+retrieval key ('One absence record by its GUID') and enumerates the returned fields, which cleanly separates it from the sibling list_absences. An agent can tell immediately this is a single-record fetch rather than a filtered list.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The phrase 'One absence record by its GUID' implies the single-record retrieval use case and contrasts implicitly with list_absences, but it never states when to prefer this over listing or what happens if the GUID is unknown. No explicit alternatives or exclusions are given.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_userGet a userARead-only
One employee by partner ID, with the groups they belong to (team, location and so on) and their authorisation hierarchy (who approves their requests: step one and step two authorisers and alternates, as partner IDs). Contact and HR details only with include_contact_details.
| Name | Required | Description | Default |
|---|---|---|---|
| include_groups | No | Also fetch the user's groups (one extra call) | |
| partner_user_id | Yes | The user's partner ID (partner_id from list_users) | |
| include_authorisers | No | Also fetch the user's authorisation hierarchy (one extra call) | |
| include_contact_details | No | Include email, login, phones, address, next of kin, date of birth, payroll and employee numbers, SSO and client-provided IDs and annual pay, and stop redacting email addresses, phone numbers and UK postcodes typed into names and other text |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations cover read-only and open-world safety, but the description adds genuinely useful behavior: contact and HR details are withheld/redacted unless include_contact_details is set. That privacy default is a meaningful trait not carried by the annotations. It doesn't cover error behavior for missing users, keeping it from a 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single dense sentence that front-loads the core identity (one employee by ID) before enumerating the optional payloads. No filler, though the heavily parenthetical authorisation clause is slightly harder to parse than it needs to be.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 4-parameter read with no output schema, the description adequately describes what comes back and the default redaction behavior. It omits what happens when the user is not found or when include flags are false, which would fully close the gap.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so all four parameters (including the detailed include_contact_details redaction note) are already documented. The description restates the contact-details gating and the composite meaning of the group/authoriser flags, adding only marginal value. Baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (get) and resource (one employee by partner ID) and enumerates the payload: groups and authorisation hierarchy. This cleanly distinguishes it from list_users and other siblings at a glance.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied by the singular scope ('One employee by partner ID'), which signals this is the single-lookup counterpart to list_users. However, it never explicitly says when to prefer this over list_users or that partner IDs come from list_users, leaving routing to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_user_entitlementsGet a user's entitlement balancesBRead-only
Entitlement balances for one user. Deducting entitlements (holiday and the like, which count down: annual entitlement, transfers, pending, booked, taken, untaken, remaining, per booking period and element) and summing entitlements (sickness and the like, which count up: year to date, last 6 and 3 months, last 30 days), plus the entitlement pots they belong to. Booking period and time unit numbers are named from the documented lists (Current, MinusOne, PlusOne; Days, Minutes, Hours).
| Name | Required | Description | Default |
|---|---|---|---|
| include_pots | No | Also fetch the user's entitlement pots (one extra call) | |
| partner_user_id | Yes | The user's partner ID | |
| include_contact_details | No | Include the user's login, and stop redacting email addresses, phone numbers and UK postcodes typed into names and other text |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations only cover the safety profile (readOnlyHint, openWorldHint), yet the description adds real behavioral context: which balance types count down (annual, transfers, pending, booked, taken, untaken, remaining) versus up (sickness: YTD, 6/3 months, 30 days), that pots are also returned, and how booking period/time unit values are named. The include_pots extra-call cost is only in the schema, not the description.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The single dense paragraph is not front-loaded and packs enumerations into parentheticals, making it hard to parse. Some length is defensible since there is no output schema, but the structure wastes that budget.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description does carry the return-shape burden and enumerates balance fields and pots reasonably well. However, it omits error/missing-user behavior and the booking-period vocabulary it introduces maps to no visible input, leaving the agent unsure whether those values are parameters or output fields.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so all three parameters are already documented, establishing the baseline of 3. The description's 'booking period and time unit numbers' sentence references values that do not correspond to any input parameter, which adds confusion rather than meaning for the actual parameters.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The opening line 'Entitlement balances for one user' names a specific resource and scope, and the singular 'one user' contrasts implicitly with the list_* siblings. It is clear what the tool fetches, though it never states the verb (get/read) explicitly and buries the scope in a long second sentence.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is no when-to-use guidance: nothing tells the agent when to prefer this over get_user, get_user_rota, or get_absence. The only hint is the phrase 'for one user', leaving the agent to infer the trigger condition.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_user_rotaGet a user's rota, public holidays and custom daysBRead-only
The rota (working pattern) assignments of one user with their start dates, named from the system's rota list, and with include_patterns the public holiday and custom day patterns applied to the user, named from those lists. Up to six API calls.
| Name | Required | Description | Default |
|---|---|---|---|
| partner_user_id | Yes | The user's partner ID | |
| include_patterns | No | Also fetch the user's public holiday and custom day patterns | |
| include_contact_details | No | Include nothing extra (rota and pattern names are the system's own text), and stop redacting email addresses, phone numbers and UK postcodes typed into names and other text |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and openWorldHint=true, so safety is covered. The description adds genuinely useful behavioral context beyond that: it discloses that the call may fan out to 'up to six API calls,' which matters for latency/cost planning, and that returned names come from the system's own rota and pattern lists.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single long, dense, run-on sentence that is grammatically hard to parse ('named from the system's rota list, and with include_patterns the public holiday...'). It is not padded with filler, but the structure obscures rather than front-loads the meaning.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a read-only tool with no output schema and fully documented parameters, the definition covers what the agent needs: what is returned, the conditional pattern fetch, and the API-call cost. It is slightly incomplete on return shape, but annotations cover safety and the schema covers inputs, so little is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents all three parameters fully, and the baseline is 3. The description does reinforce what include_patterns pulls in (public holiday and custom day patterns), but it adds little about include_contact_details or partner_user_id beyond what the schema states.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource: it returns one user's rota (working pattern) assignments with start dates, optionally plus public holiday and custom day patterns. It is clear what the tool retrieves, but it makes no attempt to distinguish itself from siblings like get_user_entitlements or get_user, so it falls short of the 5 mark.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is no explicit when-to-use guidance, no statement of prerequisites, and no mention of any alternative tool the caller might want instead. The only implied usage cue is the include_patterns toggle, which is already documented in the schema rather than used to route the agent.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_absencesList absencesARead-only
Absence records (holiday, sickness and other absence types) across the system or for one user, with status, start and end, duration and the absence_type_id (name it with list_absence_types). Filters are sent to the API as documented: date_from/date_to (datestart/dateend), record_type (1 planned, 2 unplanned), absence_type_id, and on the system-wide endpoint also user_id (Edays GUID), group_id, created_since and modified_since. With partner_user_id the per-user endpoint GET /api/v2/users/{id}/absences is used instead, which documents only the first three filters. Whole API pages of min(100, max_results) records are returned; the note says how to continue. Payroll and employee numbers only with include_contact_details.
| Name | Required | Description | Default |
|---|---|---|---|
| page | No | 1-based page to start from (for continuing a previous call) | |
| date_to | No | End of the date range, YYYY-MM-DD (sent as dateend=YYYYMMDD) | |
| user_id | No | Only records for this Edays user GUID (the userId filter; not with partner_user_id) | |
| group_id | No | Only records whose user is in this group (the groupId filter, e.g. a location; not with partner_user_id). The documentation does not say whether this is the group's GUID or partner ID; the value is sent as given | |
| date_from | No | Start of the date range, YYYY-MM-DD (sent as datestart=YYYYMMDD) | |
| max_results | No | Maximum number of records to return; also sets the API page size (up to 100) | |
| record_type | No | 1 for planned absences (holiday), 2 for unplanned (sickness); omitted returns all | |
| created_since | No | The dateCreated filter: 'filter out records created before' this value. Its format is not documented; the value is sent as given (the other date filters use YYYYMMDD). Not with partner_user_id | |
| modified_since | No | The dateModified filter: 'filter out records modified before' this value. Format not documented; sent as given. Not with partner_user_id | |
| absence_type_id | No | Absence type ID (integer, see list_absence_types) | |
| partner_user_id | No | Only this user's absences, via GET /api/v2/users/{partnerUserId}/absences | |
| include_contact_details | No | Include payroll and employee numbers, and stop redacting email addresses, phone numbers and UK postcodes typed into names and other text |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Goes well beyond the readOnly/openWorld annotations: it discloses pagination behavior (whole API pages of min(100, max_results)), a continuation note, which filters are supported on each endpoint, the documented format-mismatch caveat for date filters, and the redaction behavior governed by include_contact_details. It falls short of explaining what happens when conflicting filters are combined or result truncation specifics.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single dense paragraph covering many facts — record fields, filter routing, endpoint switch, pagination, and redaction — with no headings or bullet grouping. Every sentence carries weight, but the sentence structure is overloaded and the key filtering distinction is buried mid-paragraph.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 12-parameter, filter-heavy list tool with no output schema and annotations covering only read-only/open-world status, the description supplies the routing, pagination, and redaction context an agent needs. It is not fully complete: it omits the default max_results cap behavior when results exceed the page, and does not confirm whether filters can be freely combined.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the schema already documents the 12 parameters; the description adds real routing value by mapping date_from/date_to to datestart/dateend, distinguishing the system-wide endpoint filters (user_id, group_id, created_since, modified_since) from the three filters on the per-user endpoint, and flagging the date-format uncertainty.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (list) and resource (absence records), names the covered absence types, and identifies the endpoint used when partner_user_id is supplied. It does not directly distinguish from the sibling get_absence, though the singular/plural naming and endpoint detail make the collection-level purpose evident.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage guidance is implied rather than explicit: it notes that absence_type_id should be resolved via list_absence_types and that partner_user_id switches to the per-user endpoint, but it never states when to use list_absences versus get_absence or list_users. No exclusions, prerequisites, or typical scenarios are given.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_absence_typesList absence typesARead-only
The absence types configured on the system (Holiday, Sickness and so on) with their record type (1 planned, 2 unplanned) and booking and calendar-visibility flags. The API also returns Custom Day Groups (record type discriminator 5) and Public Holiday Groups (6) from the same endpoint; they are kept unless record_type filters them out.
| Name | Required | Description | Default |
|---|---|---|---|
| record_type | No | Only types with this record type discriminator: 1 planned, 2 unplanned, 5 custom day groups, 6 public holiday groups | |
| include_contact_details | No | Include nothing extra (type names are the system's own text), and stop redacting email addresses, phone numbers and UK postcodes typed into names and other text |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already establish readOnlyHint=true/openWorldHint=true, so the safety profile is covered. The description adds genuine behavioral context beyond that: additional record-type discriminators (5 and 6) surface from the same endpoint and are retained unless record_type filters them, which is a non-obvious return-set behavior.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, front-loaded with the primary resource and then the endpoint quirk; no filler or repetition of the schema.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a read-only, two-optional-parameter list tool with full schema coverage and no output schema, the description covers scope and the surprising sibling data well. Minor gap: no explicit note on ordering or response shape, though that is not required without an output schema.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so baseline is 3; the description nevertheless adds meaning by explaining that record_type filtering removes the extra group types from the result rather than merely narrowing a column. It also confirms type names are the system's own text, complementing the include_contact_details parameter.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('absence types configured on the system') and enumerates what is returned: record type discriminator plus booking and calendar-visibility flags. It also clarifies the non-obvious scope (Custom Day Groups and Public Holiday Groups come from the same endpoint), which helps an agent distinguish this from siblings like list_public_holidays.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied by the content it lists, but there is no explicit when-to-use guidance or naming of alternatives (e.g., list_public_holidays, list_groups). An agent can infer this is the reference/config lookup for absence types, but nothing routes it against siblings.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_groupsList group types and groupsARead-only
The group types on the system (Country, Location, Team and so on) and the groups within each (England, Nottingham, Programming), with partner IDs. One call for the types plus one per type for its groups; give group_type_partner_id to fetch a single type.
| Name | Required | Description | Default |
|---|---|---|---|
| group_type_partner_id | No | Only this group type's groups (its partner ID, e.g. loc) | |
| include_contact_details | No | Include nothing extra (group names are the system's own text), and stop redacting email addresses, phone numbers and UK postcodes typed into names and other text |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations cover the safety profile (readOnlyHint, openWorldHint), and the description adds useful operational context beyond them: the unusual one-call-per-type amplification pattern and the redaction behavior implied for include_contact_details. It stops short of describing pagination or output shape, but the amplification disclosure is the important behavioral trait here.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with what is returned, then the call pattern, then the parameter hint. No padding sentences, though the parenthetical examples make it slightly denser than needed.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
There is no output schema, yet the description conveys what comes back (type names, group names, partner IDs) and the multi-call fan-out needed to assemble groups per type. That is enough for an agent to call it correctly, with only pagination/limits left unspecified.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so baseline is 3. The description reinforces that group_type_partner_id narrows to a single type's groups, matching the schema, but adds no format or syntax detail beyond it.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Names a specific resource (group types and the groups within each), gives concrete examples (Country, Location, Team; England, Nottingham, Programming), and states the returned field (partner IDs). This clearly separates it from siblings like list_absence_types or list_users.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explains the working pattern explicitly: 'One call for the types plus one per type for its groups; give group_type_partner_id to fetch a single type.' This tells the agent both the default usage and the narrowing option, though it doesn't state when to avoid the tool or name an alternative for edge cases.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_public_holidaysList public holiday and custom day patternsARead-only
The public holiday patterns held in the system (for example 'UK Public Holidays') and, with include_custom_days, the custom day patterns (for example 'Christmas Shutdown'), as id and name. The API lists the patterns only; the dates inside a pattern are not exposed by API V2.
| Name | Required | Description | Default |
|---|---|---|---|
| include_custom_days | No | Also list custom day patterns | |
| include_contact_details | No | Include nothing extra (pattern names are the system's own text), and stop redacting email addresses, phone numbers and UK postcodes typed into names and other text |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations only declare readOnlyHint and openWorldHint; the description adds a genuinely useful limitation: the API exposes pattern names only and 'the dates inside a pattern are not exposed by API V2'. That pre-empts an agent expecting date data, which the annotations do not convey.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, front-loaded with the primary resource and output shape, and the API limitation is placed last as a caveat. The first sentence is dense but every clause carries information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description usefully states the return shape ('as id and name') and a key data limitation. Both parameters are covered by the schema, so the only gap is that include_contact_details' redaction behavior is never surfaced in prose.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents both booleans; baseline is 3. The description adds an example of what include_custom_days returns ('Christmas Shutdown') but says nothing about include_contact_details and its unusual redaction-stripping semantics.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Specific verb plus resource: it lists 'public holiday patterns held in the system' and, with a flag, 'custom day patterns', with concrete examples of each. The resource is clearly distinct from the sibling list tools (groups, absence types, users), though no sibling is named explicitly.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The phrase 'with include_custom_days, the custom day patterns' implies when the optional mode applies, but there is no explicit when-to-use/when-not guidance or naming of an alternative tool. Usage is inferable rather than stated.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_usersList usersARead-only
Employees on this Edays system with partner_id (the key other tools take), name, job title, leaver flag, settings template, start dates, FTE and hours per day. GET /api/v2/users is not marked as paged in the documentation; the whole list is fetched (following the edays-pagination-* headers page by page if the system does page it) and filtered locally: query matches part of the name or partner ID, and leavers are skipped unless asked for. Contact and HR details (email, login, phones, address, next of kin, date of birth, payroll and employee numbers, pay) only with include_contact_details.
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Case-insensitive fragment of the person's name or partner ID | |
| max_results | No | Maximum number of users to return | |
| include_leavers | No | Include users flagged IsLeaver | |
| include_contact_details | No | Include email, login, home and work phones and emails, address, next of kin, date of birth, payroll and employee numbers, SSO and client-provided IDs and annual pay, and stop redacting email addresses, phone numbers and UK postcodes typed into names and other text |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations only declare readOnly and openWorld; the description adds critical behavior: pagination handling, local filtering semantics, leaver default, and conditional PII redaction. These details go well beyond the annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the returned data fields, then behavioral constraints. The second sentence is dense but each clause earns its place; there is minor verbosity in the contact-detail parenthesis.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
No output schema exists, so the description must carry return-value context; it lists the fields and conditional contact details, and explains pagination and filtering. Sufficient for an agent to call correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the schema already documents each parameter. The description reinforces query and include_leavers behavior but adds little parameter-specific meaning beyond the schema's own descriptions.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
It names the specific resource (employees on the Edays system) and enumerates many exact fields returned, including partner_id and its role as the key other tools use. This clearly distinguishes it from single-user siblings like get_user.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explains local filtering, leaver exclusion, and when contact details are included, giving clear usage context. However, it never explicitly names an alternative tool (e.g., get_user for one employee) or states when not to use this list endpoint.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
9 tool updates
v0.1.0- First observed
get_absence - First observed
get_user - First observed
get_user_entitlements - First observed
get_user_rota - First observed
list_absence_types - First observed
list_absences - First observed
list_groups - First observed
list_public_holidays - First observed
list_users
TDQS
Scored across 9 tools
Most tools target clearly distinct resources and verbs (list_users vs get_user, list_absences vs get_absence, entitlements vs rota). The main overlap is conceptual: list_absence_types also returns custom day and public holiday groups, blurring its boundary with list_public_holidays, but the descriptions call this out and help steer selection.
Every tool follows a predictable get_/list_ + resource pattern in snake_case (list_users, get_user, get_user_rota), including consistent handling of nested resources like user entitlements and rota.
9 tools is well-scoped for an HR/absence domain, each covering a distinct facet (holidays, groups, absence types, users, absences, entitlements, rota) without redundant entries.
The surface is entirely read-only: there is no way to book, update, delete, or approve an absence, yet the domain is fundamentally about managing absences, leaving an obvious lifecycle gap. Read coverage of users, absences, entitlements and rota is solid, so an agent can inspect but not act.
Maintenance
Related MCP Connectors
MCP server for Cronofy — read calendars, events and free/busy, and create, update or delete events.
Read appointments, types, calendars and availability; create, cancel or reschedule bookings.
isolved and ApplicantPro jobs, tenant discovery, and change detection as an MCP server.
- mcp-serverOAuthio.klokin
MCP server exposing klokin time-tracking operations (employees, time entries, stores) to AI clients.
Related MCP Servers
- -licenseNot gradedqualityNot gradedmaintenanceEnables interaction with employee management systems through a standardized MCP interface. Supports comprehensive employee operations including CRUD operations, search, filtering by level/status, and data synchronization.-
- AlicenseAqualityBmaintenanceMCP server for the Personio HR API with employee and HR profiles, enabling self-service and HR operations like managing absences, attendances, documents, and organizational data.10Apache 2.0
- AlicenseNot gradedqualityBmaintenanceEnables managing Personio HR and recruiting data through MCP tools, including employees, absences, time tracking, documents, custom reports, and recruiting workflows such as jobs, candidates, and applications.19 npmMIT
- AlicenseNot gradedqualityCmaintenanceEnables Claude and other MCP clients to query and register HR data from TramitApp, including employees, clockings, absences, shifts, and vacation balances, with multi-company support.MIT