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.
This server cannot be deployed
Maintenance
Related MCP Connectors
Read-only MCP for AI usage profiles, leaderboards, stats, and docs; no writes or private data.
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.
Query your org's data in natural language — read-only MCP access to SQL, NoSQL, files & warehouses.
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