Timesheet Portal 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., "@Timesheet Portal MCP serverHow many hours did the team log on Acme last week?"
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.
Timesheet Portal MCP server
An MCP server that lets Claude, ChatGPT and other MCP clients read a Timesheet Portal account: timesheet (time and cost) reports, invoices, leave bookings and balances, projects or jobs, clients, users and cost centres. It supports both editions of the product, Standard (Project) and Recruitment, and it is read-only. It is built from Timesheet Portal's public API documentation and its two published Swagger 2.0 specs.
Once it's connected, an administrator can ask things like:
"How many hours did the team log on the Acme account last week, and how much is that at the charge rate?"
"Which timesheets for October are still only submitted, not approved?"
"List this month's client invoices that are still in draft."
"Who is off next week?"
"Who has the most annual leave left this year?"
"Which placements for Beta Logistics end before Christmas?" (Recruitment edition)
Tools
Tool | Edition | What it does | API calls |
| both | Time recorded for a date range, summed per task and employee (Standard) or employee and job (Recruitment) for each period of the chosen time grouping, with status, client, project, task, approver, quantity, units, rate (Standard) and charge. Filters: statuses, client, cost centre, employee group, employee/project/task (Standard) or contractor/job (Recruitment), approval dates, modified after. One page per call. |
|
| both | Invoices dated in a range: number, status, dates, client, project, task, worker, currency, net, tax and total, and optionally the line items. Client invoices by default; client credit notes, self-billing invoices or self-billing credit notes on request. |
|
| Standard | Leave bookings in a date range: employee, leave category, status, start and end dates, working days, units, request and review dates. Filters: statuses, employee, employee group, modified after. The comments only on request. |
|
| Standard | Every employee's leave year: allowance, carried over, approved booked leave, non-deductible leave, adjustments and end-of-year balance. |
|
| Standard | Projects with client, category, dates, charge budget, manager, cost centre and purchase order. |
|
| Recruitment | Jobs (placements) with client, category, cost centre, dates, frequencies, owner, purchase order and assigned contractors (names and codes). |
|
| both | Clients with category, cost centre, currency, payment terms, invoicing settings and notes. |
|
| both | Users with name, job title, role, employment type, group, home cost centre and line manager. |
|
| both | Cost centre codes and descriptions. |
|
list_timesheets, list_invoices and list_leave call endpoints that are POSTs in the API but only read: they take the report settings in the body and change nothing. They carry the MCP readOnlyHint annotation like the other tools.
Every tool except list_cost_centres takes max_results (100 by default; at most 1,000, or 500 for invoices) and says how many records it left out and how to narrow the query.
Report field names are documented in both specs, as an enum of 1,272 names shared by ReportSettingsModel, LeaveReportSettings and the other report settings, but without descriptions. The fields and groupings list_timesheets asks for are the ones in each edition's documented example request for POST /reports/timesheets, whose use in that report the vendor shows, minus the pay columns unless they are requested. POST /leavebookings/reports/bookings has no example request; list_leave asks for EmployeeReference, EmployeeName, LeaveCategory, LeaveStatus, LeaveStartDate, LeaveEndDate, LeaveTotalWorkingDays, LeaveUnits, LeaveRequestDate and LeaveReviewedDate, names from that enum that plainly say what they hold, and LeaveComments only with include_contact_details.
Not covered on purpose:
Writes. The only write this prototype would have offered is approving or rejecting a timesheet, and neither spec documents an endpoint for that. Invoice approval, emailing and exporting (
/invoiceaction/*), marking timesheets exported (/timesheetaction/export), every "Add or update multiple" endpoint andDELETE /jobsare left out, so there is no write flag.Expense reports (
POST /expenseentries,POST /expenseforms): the request is a documentedReportSettingsModel(its field enum includes 66Expense…names), but the only documented response is a 201 whoseResponseModel.returnObjectis an untyped object, so there is no documented shape to read the report from. (The spec'sV2expense definitions are not used by any documented path.)The invoice reports (
/reports/invoicereport,/reports/invoicereport2):list_invoicesalready reads invoices fromPOST /invoices, whose records are typed (InvoiceModel), which makes it clear which values are amounts, pay or free text; the report endpoints return untyped rows of strings.Custom reports (
/reports/customreports,/reports/customreport): their columns are defined by each account and can include bank or pay data that this server could not reliably withhold.Files: expense receipts and invoice PDFs are never downloaded.
GET /users/searchandGET /contractors/search(documented as GETs with a request body, which standard HTTP clients cannot send;list_userswithuser_idcovers a single lookup),GET /rates(pay rates),GET /thirdparties(includes bank accounts),GET /chargecodes(itsidis typed as an integer while its example is a code),/reports/builtinreport,/reports/reportfields,/customfields,/logintoken, and the email/passwordPOST /tokenroute.
Related MCP server: Dezrez Rezi MCP server
Setup
Requires Node 18 or later.
npm install
npm run buildYou need API credentials for your Timesheet Portal account: log in as a user with full administrator rights, go to Settings > Account > Account, open the API access tab, select a user account to associate with the credentials (Timesheet Portal says it should have the System Administrator role) and click Generate API credentials. The server uses the OAuth2 client-credentials flow only.
Claude Desktop: add this to claude_desktop_config.json:
{
"mcpServers": {
"timesheetportal": {
"command": "node",
"args": ["/absolute/path/to/timesheetportal-mcp/dist/index.js"],
"env": {
"TSP_CLIENT_ID": "your-client-id",
"TSP_CLIENT_SECRET": "your-client-secret",
"TSP_EDITION": "standard"
}
}
}
}Claude Code:
claude mcp add timesheetportal -e TSP_CLIENT_ID=your-client-id -e TSP_CLIENT_SECRET=your-client-secret -e TSP_EDITION=standard -- node /absolute/path/to/timesheetportal-mcp/dist/index.jsVariable | Required | Meaning |
| yes | The API client id. |
| yes | The API client secret. |
| no |
|
| no | Defaults to |
| no | How long one tool call may spend walking pages (default 25, from 1 to 50): no new page is started after it, and the answer says where to continue. With the retry waits of the page in flight (about 20 s at most), a call stays under the 60-second default request timeout of MCP clients. One test uses 1. |
| no | How many requests per minute this server may send, spread evenly (default 30, the documented per-account limit, so one request every 2 seconds). Lower it if other integrations use the same account. Values above 30 exceed the documented limit and are only useful against a mock; the tests use 6000. |
Safety defaults
Read-only. There are no write tools, whatever the environment says. Every tool carries
readOnlyHint: trueanddestructiveHint: false. The only POSTs the server sends arePOST /oauth/token,POST /reports/timesheets,POST /invoicesandPOST /leavebookings/reports/bookings, and the last three only read.Personal data is withheld unless a tool is called with
include_contact_details=true: email addresses, mobile numbers, dates of birth, gender and title, home addresses, join and leave dates, the accounting code (which the spec says "is usually used to reference payroll numbers") and a limited-company contractor's company details (users); client addresses, billing name and email, tax and company numbers (clients, which also sendsinclude=address,billingonly then); the billing contact, address and pay budget (projects); the billing contact, IR35 status, pay currency and frequency, pay budget and assigned pay and charge rates (jobs, which also sendsInclude=assignedratesonly then;Include=assignedcontractorcodesis always sent, for the contractor codes); the leave comments (LeaveComments, free text that can hold the reason for an absence, not even requested from the API otherwise); the pay columns of the timesheet report (PayRate,TotalPayin Standard;TotalPay,NetMargin,GrossMargin,Deductionsin Recruitment), which are not even requested from the API otherwise; and the amounts of self-billing invoices, which are what a contractor is paid (every invoice returned for a self-billinginvoice_typecounts as self-billing, since theinvoiceSelfBillingflag is optional in the spec). Names of users, approvers, managers and workers, and the leave category (such as sickness) of a leave booking, are returned by default.Never returned, even on request: bank details (the
bankName,bankAccountName,branchCode,bankAccountNo,iBAN,swiftCodeandbankCountryCodefields), the national insurance and tax number fields, the payroll data inemploymentDetails, the HR data inpersonalDetails(next of kin, nationality, ethnic origin, marital status, passport number, home phone), custom field values (their content is defined by each account) and audit events. TheincludedFieldsparameter ofGET /users(whose documented values includebankDetailsandpersonalDetails) is never sent.Free text (notes, descriptions including cost centre descriptions, timesheet notes, names, job titles, purchase orders, invoice external references, rate names, error messages, and every value of a report row) is redacted by default: email addresses become
[email redacted], phone-number-like sequences[phone redacted](the same heuristic as the other servers in this series:+/00international numbers, bracketed UK area codes and UK0…numbers of 9 to 11 digits), UK national insurance numbers[NI number redacted]and upper-case UK postcodes[postcode redacted]. Other digit strings that happen to look like these are redacted too; the raw text is available withinclude_contact_details. Card numbers (13 to 19 digits passing the Luhn check), IBANs, and sort codes or account numbers introduced by the words "sort code" or "account" are replaced with[card number redacted]or[bank details redacted]always, with or withoutinclude_contact_details. A bare 6- or 8-digit number cannot be told from a date or a reference and is left alone.The report columns returned are only the ones the server asked for: anything else the API sends in a row is dropped. Rows of values are matched to the requested field names by position, so a row with more or fewer values than fields requested is refused with an error rather than returned under the wrong names.
Rate limits. The documentation's usage policy says, per account and in UTC windows: every request counts towards 30 requests per minute and 5,000 per day; "basic entity updating / creating" has 1,000 requests per hour, 2,000 per day, 500 record updates per hour and 3,000 per day; "Timesheet & Invoice Reports" have 24 requests per hour, 576 per day, 500 record reads per hour and 3,000 per day. A refused request gets
429 Too Many Requestswith aRetry-Afterheader giving the seconds until the limit resets, and the response states which limit was reached. This server spaces its requests byTSP_REQUESTS_PER_MINUTE(2 seconds apart by default). A 429 is retried at most twice, for any method, waiting forRetry-After(whole or fractional seconds, or an HTTP-date; 2 s then 4 s when absent). Each wait is capped at 10 seconds: if the API asks for longer (as it will for an hourly or daily limit), the call gives up at once and passes on how long to wait and the API's message.502, 503 and 504 are retried the same way for
GETonly (the suite exercises 502 and 503). A POST (the three report POSTs and the token request) is never retried after a gateway error; since the reports only read, the error says it is safe to call the tool again, and that each call counts against the report limits.The usage policy says the API "is not intended to be a live data source" for dashboards that pull data on every refresh. The server's instructions tell the assistant about the limits and to answer with as few calls as possible;
list_timesheets,list_invoicesandlist_leavefetch one request's worth of data per call and refuse ranges over 366 days before any request.Paged lists (
GET /projects,/clients,/users,/jobs) use the documented zero-basedpageparameter. Only/projectsdocuments its page size (250). The walk stops at an empty page, at a page shorter than the documented size, or at a page shorter than the one before it; a lookup by code fetches one page. A page that repeats the previous page's records (an API ignoringpage) is reported as incomplete rather than looped over. Each call fetches at mostmax_pagespages (2 for projects, 4 otherwise), stops oncemax_resultsrecords are in hand (a page is fetched whole, so the rest of the last page is left out and counted), and says where to continue.Codes (client, project, job, user, cost centre, employee group, task) are checked before any request: 1 to 100 characters, no control characters, no surrounding spaces. Dates must be real
YYYY-MM-DDdates; date-times areYYYY-MM-DDTHH:MM[:SS]without a zone.The token is fetched with the documented form body, cached, refreshed before it expires (a minute before the end of its lifetime, which the documentation gives as an hour, or after half of it when the lifetime is under two minutes; the suite tests the latter with a 2-second token), and fetched afresh once when a call answers 401. The client secret and the token are scrubbed from any text the API echoes back. Rejected credentials produce a message that names
TSP_CLIENT_IDandTSP_CLIENT_SECRETand where to generate them; a 403 says the credentials need a System Administrator user; an unknown code passes on the API's own message.A 200 whose body is not JSON (a proxy or login page in the way), or is an object where the documented response is an array (a
ResponseModelwithsuccess: false, say), is reported as an error namingTSP_BASE_URLand passing on the API's message, never as an empty list.Time. A paged walk starts no new page once
TSP_CALL_BUDGET_SECONDS(25 s) has passed, and says where to continue. A call the MCP client cancels, including when it times out, stops: the server passes the MCP request's abort signal to its API requests and its waits (throttle spacing andRetry-After), so the request in flight is aborted and nothing more is sent for that call. A token request already under way is left to finish, because concurrent calls share it.
Tests
npm testThe suite downloads both specs (docs/standardJson to spec-standard.json, docs/recruitmentJson to spec-recruitment.json) on the first run and then:
Validates every fixture record against the definitions in the specs with Ajv in JSON Schema draft-04 mode (Swagger 2.0), for each edition that has the endpoint:
ChargeCodeGroupModel(253 projects, crossing the documented page size),JobModel,ClientModel,EmployeeModel,CostCentreModel,InvoiceModelwithInvoiceItemModel,LeaveSummaryModel, and the report rows against the documentedPOST /reports/timesheetsandPOST /leavebookings/reports/bookingsresponses (arrays of arrays of strings), with every leave field name checked against theLeaveReportSettingsenum. The definitions allow any extra key, so a separate walk fails on any key a definition does not declare. The two editions' definitions are asserted identical. The vendor's own examples write date-times without a zone (2022-12-01T00:00:00), which strict RFC 3339 rejects, sodate-timeis checked with the zone optional; the suite proves the published example request fails the strict format and passes this one (after leaving out thenullfilters it sends, which Swagger 2.0 cannot declare). It also checks that the mock's client id and secret are obviously fake (tsp-test-…-not-real), appear nowhere in the specs, and that none of the example tokens and ids in the vendor's documentation appears in the test files.Starts a local mock of the API that implements
POST /oauth/token(form-encoded or JSON, as documented), Bearer auth, the zero-based paging (250 per page for projects, 3 per page, its own choice, where no size is documented), the documented filters, the leave bookings report, and the documented error bodies:{ "Message": … }for the 400/404 examples,ResponseModeland an array ofValidationErrorModelwhere those are documented, and a 429 withRetry-Afterusing the documented throttle message. The mock's list, report, token and error responses are validated against the response schemas; the token responses, which the documentation describes only by example (access_token,token_type,expiry_time;error,error_description), against schemas written from those examples. What the specs do not document is the mock's own choice and says so in its comments: the status of a rejected token request (401), the body of a 401 for a bad Bearer token, a 403 withMissingAdministratorPermissionfor a non-administrator, an empty array for an unknown user id.Starts the built server and drives it over stdio with the official MCP client, first as the Standard edition, then as the Recruitment edition: 37 checks in all. They cover the tool lists and annotations of both editions, that
TSP_ALLOW_WRITES=trueadds nothing (Standard), and the server instructions about the limits; the token request's exact form body and the token's reuse; paging to each end condition (a page shorter than 250, a shorter page after a full one, an empty page after a single page, a page repeating the previous one) and continuing fromnext_page; every documented filter sent under its documented name and date format (lastModifiedasyyyy-MM-ddTHH:mm:ss,ModifiedSinceasyyyy-mm-dd HH:MM:SS,EndsAfter/StartsBeforeasyyyy-MM-dd HH:mm); the report, leave report and invoice request bodies validated againstReportSettingsModel,LeaveReportSettingsandInvoiceReportSettingsModelof the edition, and equal to the documented example's fields minus pay; report rows with a header row, as objects or as Key/Value pairs (injected responses that are deliberately off-schema), and a row with one value too many or too few refused rather than mislabelled;max_resultscapping projects (100 of the first page of 250) and invoices (100 of 150) with the number left out, and its hard maximum; the self-billing amounts withheld for a self-billing request even when the record's optionalinvoiceSelfBillingflag is absent; redaction by default (emails, phone numbers, NI numbers and postcodes in users, clients, projects, jobs, timesheet notes and any other report column, invoice line items and rate names, purchase orders, invoice external references, cost centre descriptions and leave names; a bare 8-digit reference and a hyphenated purchase order left alone) and the opt-in (users with their dates and accounting code, clients, projects, jobs, timesheet notes and pay columns, leave comments, self-billing amounts); bank, NI, tax, payroll, HR, custom field and audit data never returned even on request; card, IBAN, sort code and account numbers in free text removed even on request; the 429 retry withRetry-Afterin seconds, fractional seconds and HTTP-date form, the 2 s fallback, giving up after three attempts and at once above the 10 s cap (11 s gives up, exactly 10 s is waited out); the token endpoint's own 429, waited out on a session's first request and, three times over, reported with its own message; a 429 on a report POST retried; a 502 retried forGETand never for a POST; three 503s reported without the gateway HTML; a non-JSON 200 reported as an error with the excerpt redacted before it is cut; a 200 carrying an object where an array is documented reported as an error for cost centres, leave balances, users and the three report POSTs; the token refreshed after a 401 and before expiry, a second 401 after the refresh ending the call (one refresh, one retry), three concurrent calls on a fresh session sharing one token request, the secret and the cached token scrubbed from echoed messages, the token request not retried after a 503, the 403 message; invalid codes, dates, statuses and ranges rejected before any request; the request spacing ofTSP_REQUESTS_PER_MINUTE; a call the client times out on sending nothing more (the retry and the next page are never sent); a paged walk stopping at theTSP_CALL_BUDGET_SECONDSbudget with where to continue; startup refusing missing credentials and bad settings; wrong credentials giving an actionable message; and, last, that every request of the whole suite carried a Bearer token the mock issued (or the documented token form body), hit a method and path documented in its edition's spec with only query parameter names that operation documents and body keys its body schema declares, never touched/token, a write endpoint or a file endpoint, and never put the secret in a query string.
The suite takes about 30 seconds (the Retry-After boundary check waits the full 10 seconds).
Status
This is a working prototype. It has not yet been run against the live API, because it was built without a Timesheet Portal account. Everything below is taken from the published documentation and should be confirmed on a real account (the 30-day free trial may do, if it includes the API access tab):
The API host: whether
tenant.api.timesheetportal.comis literally the shared host (as the general guidance writes it) or a placeholder for a per-account host (the spec's security definitions writeyourhost.api.timesheetportal.com).The token request end to end: that a form-encoded body is accepted (the guidance says JSON or form), that
expiry_timecomes back as documented (the server also readsexpires_inand assumes an hour when neither is present), and the HTTP status of the documentedinvalid_clienterror (not documented; the server treats 400, 401 and 403 alike). Also the body and status the API answers for an expired or wrong Bearer token, and what a user without the System Administrator role gets (the mock answers 403 withMissingAdministratorPermission, an error code the spec lists).Paging: the page size of
/clients,/usersand/jobs(undocumented), thatpageis zero-based on/projectstoo (the request models say zero-based;/projectsonly says "page size is 250 records"), and what a page past the end returns (the mock answers an empty array).The timesheet report: that with
reportFormatValuesArrayeach row is an array of strings in the order ofreportFields(the columns are mapped by position, and a row of any other length is refused as an error; a first row repeating the field names is dropped; rows as objects or Key/Value pairs are also read by name), that the fields and groupings of the documented examples work as sent, thatAllintimesheetStatusFiltersincludes every status, thatpageIndexis zero-based, and the report's page size (undocumented; the tool asks the assistant to request the next page if more rows are expected).Which filters are exact-match codes:
employeeFilter,contractorFilterandchargeCodeGroupFilterhave no description in the spec, and whetherjobFiltertakes a job code.Date formats and zones: the parameters document several formats (above), and the
EndsAfter/StartsBeforeparameters ofGET /jobssayyyyy-MM-dd HH:mmwhile its 400 description saysyyyy-MM-dd HH:mm:ss. Dates are sent without a zone, as in the vendor's examples; which time zone the API reads them in is not documented.The leave bookings report (
POST /leavebookings/reports/bookings): that each row is an array of strings in the order ofreportFields(mapped by position as above), that the chosen field names are accepted and hold what their names say (the enum has no descriptions), that leaving outstatusFiltersmeans every status (the spec says "Set to null for no status filtering"; this server never sends a null), howstartDateandendDateselect bookings that span them, whether the report is paged (nothing is documented), and whether it counts against the report limits.POST /invoices: thatinvoiceReportTypeselects client or self-billing invoices and credit notes (its spec description reads "Deprecated - (only used by /invoices/ POST endpoint)"), that an emptyinvoiceStatusFiltersreturns every status as documented, thatcostCentreCodeandchargeCodeGroupCodesfilter as their names say, and whether the endpoint counts against the "Timesheet & Invoice Reports" limits.GET /leavebookings/summary: the array-of-arrays response shape (flattened here) and theleaveYearformat.Which fields the live API fills: whether
GET /usersreturns email, mobile and date of birth withoutincludedFields, whetherGET /clientsneedsinclude=address,billingfor the address and billing fields, and whetherGET /jobsreturns the assigned contractors' names without anIncludevalue (their codes are requested withInclude=assignedcontractorcodes).What
GET /users?id=expects (the docs say "a single user specified by the id"; the server sends the value given, tested with a user code) and what it answers for an unknown id (only a 200 is documented; the mock answers an empty array).The wording of the API's error and 429 messages and whether any echo request data; the server redacts contact details from them and scrubs its own secrets regardless.
How the per-account limits interact with other integrations on the same account;
TSP_REQUESTS_PER_MINUTEcan be lowered to leave room.
Going to production
This version runs locally over stdio with the account's own API 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 Timesheet Portal, that maps each signed-in user to their own permissions rather than to one System Administrator's, and then a listing in the Claude and ChatGPT connector directories. Timesheet approval and rejection can follow once an endpoint for it is documented, and expense reports once their response shape is documented; both need testing on a real account.
Licence
MIT. Built by Alexandru Dragoș (alexandru.dragos96@gmail.com) with an AI agent (Claude) working under his direction.
Available Tools
8 toolslist_clientsList clientsARead-onlyIdempotent
Clients (GET /clients) with code, name, category, cost centre, currency, payment terms, invoicing settings and notes. Filters sent as documented: id (a client code), costCentreCode, clientCategoryCode, ModifiedSince. Addresses, billing name and email, tax and company numbers only with include_contact_details (which also sends include=address,billing).
| Name | Required | Description | Default |
|---|---|---|---|
| max_pages | No | Maximum number of pages to fetch in this call (each page is one request against the 30-per-minute limit) | |
| start_page | No | Zero-based page to start from (next_page from a previous call) | |
| client_code | No | Only this client (sent as id) | |
| max_results | No | Most clients to return in this call (default 100); the answer says how many were left out | |
| category_code | No | Sent as clientCategoryCode | |
| modified_since | No | ||
| cost_centre_code | No | ||
| include_contact_details | No | Include client addresses, billing name, billing email, tax number and company registration number, and stop redacting email addresses, phone numbers, national insurance numbers and UK postcodes typed into names and other text (bank and card numbers are always removed) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent/non-destructive/openWorld, so the safety profile is covered. The description adds real behavioral context beyond that: the redaction rules that are lifted by include_contact_details (emails, phones, NI numbers, UK postcodes) and the note that bank/card numbers are always removed, plus the wire-level mapping of filters.
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, no filler, with the resource and returned fields front-loaded before the filter and include semantics. The second sentence is dense with sub-clauses but each clause carries distinct information, so 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?
For an 8-parameter, no-required-argument list tool with no output schema, the description covers the returned fields, the filter set, and the one parameter with privacy implications, which is enough to call it correctly. Missing only an explicit note that pagination is driven by the schema's start_page/max_pages, which the schema already explains.
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 75%, so the schema documents most parameters, but the description adds value by mapping the friendly filter names to the wire parameters actually sent (id, costCentreCode, clientCategoryCode, ModifiedSince) and by explaining the compounded side effect of include_contact_details. It stops short of describing pagination interaction (start_page/max_pages), which the schema covers.
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 names a specific verb+resource ('Clients (GET /clients)') and enumerates the returned fields (code, name, category, cost centre, currency, payment terms, invoicing settings, notes), so an agent immediately knows it is a client-listing read. It does not explicitly differentiate itself from the sibling list_* tools, but the resource noun is unambiguous.
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 documents which filters exist and the conditional behavior of include_contact_details (which also sends include=address,billing), which implies when to reach for that flag. However, there is no explicit 'use this when' statement and no routing to sibling tools (e.g. list_projects for project data); the agent must infer context from the resource name alone.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_cost_centresList cost centresBRead-onlyIdempotent
Cost centres (GET /costcentres): code and description (contact details typed into a description are redacted). Filter sent as documented: Code.
| Name | Required | Description | Default |
|---|---|---|---|
| cost_centre_code | No | Only this cost centre (sent as Code) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, destructiveHint=false and openWorldHint, so the safety profile is covered. The description adds one genuinely useful behavioral fact beyond the annotations: contact details typed into a description are redacted. It does not mention pagination, result limits, or ordering, which is the main remaining gap.
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 compact sentence with the resource and endpoint front-loaded. It is dense and slightly telegraphic ('Filter sent as documented: Code'), but nothing is wasted and it reads quickly.
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 names the returned fields (code, description) and flags redaction of contact details, which compensates for the missing return schema. For a one-param list tool it is nearly complete, though it omits any note on result size or pagination.
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% with a single optional parameter, so the schema already documents cost_centre_code as 'Only this cost centre (sent as Code)'. The description's 'Filter sent as documented: Code' largely restates the schema and adds no syntax or matching-semantics detail, so the 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?
The description names the resource (cost centres) and the backing endpoint GET /costcentres, plus the fields returned (code, description). The listing verb is carried by the tool name rather than the description, but the purpose is unambiguous and clearly distinct from siblings like list_projects or list_clients.
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 or when-not-to-use guidance, and no alternatives are named. The one-param filter is mentioned but the description never says when an agent should reach for this tool versus another list_* sibling.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_invoicesList invoicesARead-onlyIdempotent
Invoices dated in a range (POST /invoices, 'Get invoices'; a POST that changes nothing): number, status, dates, client, project, task, worker, currency, net, tax and total, and optionally the line items. invoice_type picks client invoices (default), client credit notes, self-billing invoices or self-billing credit notes (sent as invoiceReportType, whose spec description reads 'Deprecated - (only used by /invoices/ POST endpoint)'). Other filters sent as documented: invoiceStatusFilters (empty returns every status, Draft included), clientCode, costCentreCode, chargeCodeGroupCodes, exportedFilter, modifiedAfter. Self-billing invoices are contractor pay: their amounts only with include_contact_details (any invoice returned for a self-billing type is treated as self-billing, whatever its invoiceSelfBilling flag says). Probably counts against the report limits (24 per hour); the endpoint documents no paging, so at most max_results invoices are shown.
| Name | Required | Description | Default |
|---|---|---|---|
| end_date | Yes | Invoice date on or before, YYYY-MM-DD (at most 366 days after start_date) | |
| exported | No | Filter on export status (exportedFilter) | |
| statuses | No | Invoice statuses to include; empty for all, Draft included | |
| start_date | Yes | Invoice date on or after, YYYY-MM-DD | |
| client_code | No | ||
| max_results | No | Most invoices to return in this call (default 100); the answer says how many were left out | |
| invoice_type | No | ClientInvoices | |
| include_items | No | Include each invoice's line items | |
| project_codes | No | Only these projects (branches), sent as chargeCodeGroupCodes | |
| modified_after | No | Only invoices modified after this date/time (modifiedAfter) | |
| cost_centre_code | No | ||
| include_contact_details | No | Include self-billing (contractor pay) amounts, and stop redacting email addresses, phone numbers, national insurance numbers and UK postcodes typed into names and other text (bank and card numbers are always removed) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent, and the description adds genuinely extra behavior: the 'POST that changes nothing' clarification, an approximate rate limit (24/hour), the absence of server-side paging plus the max_results cap, and the redaction rules for contact details with the always-removed bank/card numbers.
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?
One run-on paragraph mixing return fields, filter mappings, deprecation trivia, and rate limits; every claim is roughly relevant but there is filler ('sent as documented') and no structure to help an agent scan. Front-loading the date range is good, but readability suffers.
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 describe returns, and it does: the field list, the optional line items, and the note that the response reports how many invoices were omitted. Combined with the rate-limit and no-paging disclosures, an agent has everything needed to call this 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?
With 12 params and only 75% schema coverage, the description compensates well: invoice_type maps to the deprecated invoiceReportType, statuses-empty returns everything including Draft, project_codes is sent as chargeCodeGroupCodes, and the self-billing flag quirk (any invoice returned for a self-billing type is treated as self-billing) is spelled out.
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 gives a precise verb+resource ('Invoices dated in a range ... POST that changes nothing') and enumerates the returned fields, so it is unmistakably the invoice-listing tool and not list_projects, list_clients, or list_timesheets.
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 states the date-range scoping requirement, explains that invoice_type selects client invoices vs credit notes vs self-billing, and ties include_contact_details to getting self-billing amounts. It does not name an alternative tool or state when-not-to-use, but the sibling set is a different resource so there is little ambiguity to resolve.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_leaveLeave bookings reportARead-onlyIdempotent
Leave bookings (holidays and other absences) in a date range (POST /leavebookings/reports/bookings, "Get leave bookings report"; a POST that changes nothing). Columns: EmployeeReference, EmployeeName, LeaveCategory, LeaveStatus, LeaveStartDate, LeaveEndDate, LeaveTotalWorkingDays, LeaveUnits, LeaveRequestDate, LeaveReviewedDate; LeaveComments only with include_contact_details. Filters sent as documented: statusFilters (omitted for every status), employeeFilter, employeeGroupFilter, modifiedAfterDate. The endpoint documents no paging; whether it counts against the report limits is not documented.
| Name | Required | Description | Default |
|---|---|---|---|
| end_date | Yes | Last date, YYYY-MM-DD (at most 366 days after start_date) | |
| statuses | No | Leave statuses to include (statusFilters); every status when omitted | |
| start_date | Yes | First date, YYYY-MM-DD | |
| max_results | No | Most bookings to return in this call (default 100); the answer says how many were left out | |
| employee_code | No | Sent as employeeFilter | |
| modified_after | No | Only bookings modified after this date/time (modifiedAfterDate) | |
| employee_group_code | No | Sent as employeeGroupFilter | |
| include_contact_details | No | Include the leave comments (LeaveComments, free text that can hold the reason for an absence), and stop redacting email addresses, phone numbers, national insurance numbers and UK postcodes typed into names and other text (bank and card numbers are always removed) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Adds value beyond annotations: it clarifies that the POST 'changes nothing' (consistent with readOnlyHint), discloses the effect of include_contact_details on redaction (email, phone, NI, postcodes; bank/card always removed), and honestly flags undocumented behaviors (no paging, unknown report-limit counting). The redaction detail is a genuine behavioral disclosure the annotations do not carry.
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-loads purpose and columns, but the column enumeration is long and the trailing caveats ('whether it counts against the report limits is not documented') add length. Some of the filter-name mapping duplicates the schema. Adequate but not tight.
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?
Complete for an 8-param list tool with no output schema: it names the returned columns, notes the max_results 'answer says how many were left out', and transparently flags undocumented paging/limits. A note on ordering or the report-limit uncertainty resolution would round it out.
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 parameters are already documented; description largely maps parameter names to API filter names (statusFilters, employeeFilter, etc.), which the schema descriptions also provide. It adds little syntax 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?
States a specific verb+resource ('Leave bookings (holidays and other absences) in a date range'), names the endpoint and operation, and enumerates the returned columns. Distinguishes it from sibling list_leave_balances by scope (bookings vs balances).
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?
Implies usage via the date-range scoping and the note that statusFilters covers 'every status' when omitted, but names no alternative tool and gives no explicit when-to-use/when-not. For an agent picking between list_leave and list_leave_balances, guidance is only implied.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_leave_balancesLeave balancesBRead-onlyIdempotent
Annual leave summary for every employee for one leave year (GET /leavebookings/summary): allowance, carried over, approved booked leave, non-deductible leave, manual adjustments and end-of-year balance, in the units the account records (hours or days). leave_year is any date in the wanted leave year (the API uses the leave year that date falls in); today when omitted.
| Name | Required | Description | Default |
|---|---|---|---|
| leave_year | No | A date in the leave year, YYYY-MM-DD (defaults to today) | |
| max_results | No | Most employees to return in this call (default 100); the answer says how many were left out | |
| employee_code | No | Only this employee's row (filtered by this server; the endpoint has no employee filter) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, destructiveHint:false and openWorldHint, covering the safety profile. The description adds genuine context beyond that: the underlying GET endpoint, that results are 'for every employee' (i.e. unfiltered by default), and that values are expressed in account-configured units (hours or days). It does not cover pagination/truncation behavior, so it stays at a solid 3.
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 core purpose is front-loaded in the first clause, and the sentence earns its length by listing the exact metrics returned. It is slightly dense with the parenthetical endpoint, but there is little waste.
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 (allowance, carried over, booked, non-deductible, adjustments, balance) and their units, and all three optional parameters are documented. What is missing is any indication of default scope size or result-limit behavior, so it is not fully complete.
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%, which sets a baseline of 3, and the description adds real meaning on top: leave_year accepts any date within the desired leave year (the API maps that date to the containing leave year), which the schema only vaguely calls 'a date in the leave year'. It adds nothing for max_results or employee_code beyond the schema, but the leave_year clarification is a substantive addition.
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+resource (annual leave summary for every employee, per leave year) and even enumerates the returned fields, so an agent knows exactly what this returns. It does not name or differentiate from the sibling list_leave, so it falls short of a 5 that would route the agent among alternatives.
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 / when-not-to-use guidance and no reference to any sibling tool (e.g. list_leave). The only guidance is about leave_year resolution ('today when omitted'), which is parameter semantics rather than usage context.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_projectsList projectsARead-onlyIdempotent
Projects (GET /projects, documented page size 250) with client, category, dates, charge budget, manager, cost centre and purchase order. Filters sent as documented: id (a project code), clientCode, categoryCode, lastModified. The pay budget, billing contact and address only with include_contact_details.
| Name | Required | Description | Default |
|---|---|---|---|
| max_pages | No | Maximum number of pages to fetch in this call (each page is one request against the 30-per-minute limit) | |
| start_page | No | Zero-based page to start from (next_page from a previous call) | |
| client_code | No | ||
| max_results | No | Most projects to return in this call (default 100); the answer says how many were left out | |
| project_code | No | Only this project (sent as id) | |
| category_code | No | ||
| modified_since | No | Only projects modified since this date or date and time | |
| include_contact_details | No | Include the pay budget, billing contact name and email, and project address, and stop redacting email addresses, phone numbers, national insurance numbers and UK postcodes typed into names and other text (bank and card numbers are always removed) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover the safety profile (readOnly, idempotent, non-destructive, openWorld), so the description's value-add is its disclosure of the documented page size of 250 and, more importantly, the redaction behavior: contact/pay-budget/address fields are hidden and PII (emails, phones, NI numbers, postcodes) is redacted unless include_contact_details is set. That is meaningful behavioral context 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?
Two dense sentences with the resource, fields, filters, and the redaction condition front-loaded; almost no filler. Phrasing is terse to the point of being slightly clipped, 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 enumerates the returned fields and notes the page size and pagination limits. It covers filters and the conditional-contact behavior, which is sufficient for an 8-parameter, zero-required listing tool, though pagination mechanics (max_pages/start_page) are left entirely to the 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 description coverage is 75%, so the schema already documents most parameters. The description adds the API-level wire names (id, clientCode, categoryCode, lastModified) and maps project_code to 'id', which helps cross-reference, but its include_contact_details note largely restates the schema's own detailed description. 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 and resource ('Projects', GET /projects) and enumerates the fields returned (client, category, dates, charge budget, manager, cost centre, purchase order) plus the supported filters. It is unambiguous what the tool fetches. It does not explicitly differentiate itself from siblings, but those list entirely different resources, so confusion risk is low.
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 rather than stated: the agent can infer this is the listing call for projects and that filters narrow results. There is no explicit when-to-use, when-not-to-use, or alternative routing guidance, nor any note on prerequisites. Adequate but leaves selection logic to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_timesheetsTimesheet (time and cost) reportARead-onlyIdempotent
Time recorded on timesheets for a date range (POST /reports/timesheets, the documented "time and cost report"; a POST that changes nothing), summed per task and employee for each period of time_grouping. Columns are the documented example's: Date, Status, CompanyName, CompanyReference, ChargeCodeGroup, ChargeCodeGroupReference, ChargeCode, ChargeCodeDesc, EmployeeReference, EmployeeName, ApproverName, EntryQuantity, RateCode, RateName, Units, ChargeRate, TotalCharge, TimesheetUpdateSequence; PayRate, TotalPay only with include_contact_details. Filters sent as documented: timesheetStatusFilters, clientFilter, costCentreFilter, employeeGroupFilter, employeeFilter, chargeCodeGroupFilter, taskCodeFilter, approvedDateFilterStart/End, modifiedAfterDate. Each call counts against the documented limit of 24 report requests and 500 records read per hour: keep ranges narrow. One page per call (pageIndex); the page size is not documented. Every value is redacted like free text, and a row whose length does not match the requested columns is refused rather than mislabelled.
| Name | Required | Description | Default |
|---|---|---|---|
| end_date | Yes | Last date, YYYY-MM-DD (at most 366 days after start_date) | |
| statuses | No | Timesheet statuses to include (default All) | |
| task_code | No | Sent as taskCodeFilter | |
| page_index | No | Page of the report (pageIndex, zero-based) | |
| start_date | Yes | First date, YYYY-MM-DD | |
| approved_to | No | Only time approved before this date/time (approvedDateFilterEnd) | |
| client_code | No | Sent as clientFilter | |
| max_results | No | Most rows to return in this call (default 100); the answer says how many were left out | |
| project_code | No | Sent as chargeCodeGroupFilter | |
| approved_from | No | Only time approved since this date/time (approvedDateFilterStart) | |
| employee_code | No | Sent as employeeFilter | |
| time_grouping | No | Time grouping of the rows (reportTimeGrouping) | Weekly |
| modified_after | No | Only records modified after this date/time (modifiedAfterDate) | |
| cost_centre_code | No | Sent as costCentreFilter | |
| employee_group_code | No | Sent as employeeGroupFilter | |
| include_contact_details | No | Include the pay columns (PayRate, TotalPay), and stop redacting email addresses, phone numbers, national insurance numbers and UK postcodes typed into names and other text (bank and card numbers are always removed) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Goes well beyond the annotations: clarifies that the POST 'changes nothing' (reinforcing readOnly/idempotent), discloses the documented rate limits (24 report requests and 500 records read per hour), pagination behavior (one page per call, page size undocumented), blanket redaction of values, and that malformed rows are refused rather than mislabelled. The rate limits and redaction/no-op-row rules are exactly the kind of context structured fields cannot carry.
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-loads the purpose and endpoint before the column list and constraints, and given 16 parameters and no output schema the length is largely justified. It is dense and slightly repetitious in its 'the documented ...' phrasing, but each sentence carries real 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 compensates by enumerating the returned columns, explaining the pay-column dependency on include_contact_details, and documenting pagination and truncation reporting ('the answer says how many were left out'). An agent has everything needed to call and interpret the result.
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 every parameter is already documented, including the filter name mappings (taskCodeFilter, clientFilter, etc.). The description restates the filter set and the include_contact_details pay-column rule but adds little syntax or nuance beyond the schema, so the 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 (time recorded on timesheets for a date range), names the underlying endpoint (POST /reports/timesheets) and identifies it as the documented 'time and cost report'. It also clarifies the aggregation ('summed per task and employee for each period of time_grouping'), which distinguishes it from the sibling entity-listing tools like list_projects and 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?
Gives clear situational context: this is the reporting endpoint, and the caller should 'keep ranges narrow' because of the hourly request/record limits. It does not explicitly name when to choose this over a sibling report/list tool, so it falls just short of the 5 bar for named alternatives.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_usersList usersARead-onlyIdempotent
Users (GET /users): code, name, job title, role, employment type, group, home cost centre and line manager. Filters sent as documented: id (a single user), groupCode, ModifiedSince. Join and leave dates, the accounting code (which the spec says usually holds the payroll number), email, mobile, date of birth, gender, address and a limited-company contractor's company details only with include_contact_details. Bank details, national insurance and tax numbers, the payroll details in employmentDetails and HR personal details are never returned (includedFields is never sent).
| Name | Required | Description | Default |
|---|---|---|---|
| user_id | No | Only this user (sent as id; the docs say 'Return a single user specified by the id') | |
| max_pages | No | Maximum number of pages to fetch in this call (each page is one request against the 30-per-minute limit) | |
| group_code | No | Sent as groupCode | |
| start_page | No | Zero-based page to start from (next_page from a previous call) | |
| max_results | No | Most users to return in this call (default 100); the answer says how many were left out | |
| modified_since | No | ||
| include_contact_details | No | Include join and leave dates, accounting (payroll) code, email, mobile, date of birth, gender, title, home address and limited-company details, and stop redacting email addresses, phone numbers, national insurance numbers and UK postcodes typed into names and other text (bank and card numbers are always removed) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent/openWorld/non-destructive, but the description adds real behavioral context absent from structured fields: bank details, NI/tax numbers, payroll and HR personal details are never returned, and include_contact_details controls redaction of text fields. Rate-limit context exists but is carried 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?
Front-loads the resource and its fields, then the filter contract, then the explicit exclusions. Dense but every sentence carries distinct information; nothing is padded.
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 7 params and no output schema, the description compensates by enumerating returned fields and what is withheld, which is what an agent needs to interpret results. Pagination/leftover-count semantics live in the schema, so the combined picture is nearly complete.
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 86%, so the schema does most of the work. The description still adds value by mapping caller concepts to wire names ('Filters sent as documented: id ... groupCode, ModifiedSince') and explaining the include_contact_details redaction scope.
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 the resource (Users / GET /users) and enumerates the returned fields, so an agent knows exactly what this tool retrieves. Sibling list_* tools are differentiated only implicitly by resource name, which is distinct enough but not called out.
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 names the supported filters (id, groupCode, ModifiedSince) and explains when include_contact_details matters, implying usage. However, it never states when to prefer this over the sibling list_* tools or any exclusion conditions.
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.
8 tool updates
v0.1.0- First observed
list_clients - First observed
list_cost_centres - First observed
list_invoices - First observed
list_leave - First observed
list_leave_balances - First observed
list_projects - First observed
list_timesheets - First observed
list_users
TDQS
Scored across 8 tools
Each tool wraps a distinct resource (projects, clients, users, cost centres, timesheets, invoices, leave balances, leave bookings) with a clear resource+action pairing. The only near-neighbour pair, list_leave_balances (aggregate annual summary) vs list_leave (individual bookings), is clearly differentiated by descriptions and parameters.
Uniform list_<plural_noun> snake_case pattern across all eight tools, with consistent British spelling for 'cost_centres'. No mixing of verb styles or casing conventions.
Eight tools is well-scoped for a read/reporting-oriented timesheet API, and each covers a genuinely distinct reporting need. No redundant or filler tools.
Broad read coverage of the portal's core entities and reports, but reference data used by filters (charge codes, task codes, rate codes) has no listing tool, and the surface is entirely read-only so no create/update/approve workflows exist. These are workable gaps for a reporting server but leave some dead ends.
Maintenance
Related MCP Connectors
Read-only MCP for AI usage profiles, leaderboards, stats, and docs; no writes or private data.
AI Visibility Isn’t Luck. It’s Signal Alignment. Read-only MCP access to Market Brew, a patented search engineering platform for SEO and AI visibility. Market Brew uses particle swarm optimization and template models to reverse engineer observed search behavior. Inspect ranking models, content alignment, knowledge bases, crawl data, and AI visibility history through a hosted Streamable HTTP MCP server. Requires an enabled Market Brew account and OAuth. Setup guide: https://brew.marketbrew.ai/customer-manual.htm#api-and-chatgpt-read-access
Read-only MCP access to sessions, funnels, campaigns, errors, live visitors, and anomalies.
Read-only MCP access to your DEXUN AdWhiz account: ad accounts, AI recommendations, savings.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceEnables read-only access to company data across PostgreSQL, MongoDB Atlas, and flat files through MCP tools, allowing AI assistants to query and retrieve information via natural language.-
- AlicenseAqualityCmaintenanceIt enables MCP clients such as Claude and ChatGPT to read an estate agency's Dezrez Rezi CRM data, including properties and their timelines, people, groups, and offers. It is read-only and applies privacy redactions by default.10MIT
- AlicenseAqualityCmaintenanceEnables Claude, ChatGPT and other MCP clients to read an Amiqus ID account—clients, onboarding records and steps, check results, templates, case status counts and webhooks—and, when writes are enabled, create records.9MIT
- AlicenseNot gradedqualityCmaintenanceLets MCP clients such as Claude and ChatGPT read a rota and time-and-attendance account, exposing venues, groups, shifts, absences and absence types, time entries, venue events and staff names through read-only tools that never return pay data.MIT