Skip to main content
Glama
dragosh29

Timesheet Portal MCP server

by dragosh29

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

list_timesheets

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.

POST /reports/timesheets

list_invoices

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.

POST /invoices

list_leave

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.

POST /leavebookings/reports/bookings

list_leave_balances

Standard

Every employee's leave year: allowance, carried over, approved booked leave, non-deductible leave, adjustments and end-of-year balance.

GET /leavebookings/summary

list_projects

Standard

Projects with client, category, dates, charge budget, manager, cost centre and purchase order.

GET /projects

list_jobs

Recruitment

Jobs (placements) with client, category, cost centre, dates, frequencies, owner, purchase order and assigned contractors (names and codes).

GET /jobs

list_clients

both

Clients with category, cost centre, currency, payment terms, invoicing settings and notes.

GET /clients

list_users

both

Users with name, job title, role, employment type, group, home cost centre and line manager.

GET /users

list_cost_centres

both

Cost centre codes and descriptions.

GET /costcentres

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 and DELETE /jobs are left out, so there is no write flag.

  • Expense reports (POST /expenseentries, POST /expenseforms): the request is a documented ReportSettingsModel (its field enum includes 66 Expense… names), but the only documented response is a 201 whose ResponseModel.returnObject is an untyped object, so there is no documented shape to read the report from. (The spec's V2 expense definitions are not used by any documented path.)

  • The invoice reports (/reports/invoicereport, /reports/invoicereport2): list_invoices already reads invoices from POST /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/search and GET /contractors/search (documented as GETs with a request body, which standard HTTP clients cannot send; list_users with user_id covers a single lookup), GET /rates (pay rates), GET /thirdparties (includes bank accounts), GET /chargecodes (its id is typed as an integer while its example is a code), /reports/builtinreport, /reports/reportfields, /customfields, /logintoken, and the email/password POST /token route.

Related MCP server: Dezrez Rezi MCP server

Setup

Requires Node 18 or later.

npm install
npm run build

You 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.js

Variable

Required

Meaning

TSP_CLIENT_ID

yes

The API client id.

TSP_CLIENT_SECRET

yes

The API client secret.

TSP_EDITION

no

standard (the Standard / Project edition, default) or recruitment. It decides which tools are registered and which report fields are requested.

TSP_BASE_URL

no

Defaults to https://tenant.api.timesheetportal.com, the host the documentation names for the token endpoint and the API. The spec's security definitions name yourhost.api.timesheetportal.com instead; if your account has its own API host, set it here. Also used by the tests.

TSP_CALL_BUDGET_SECONDS

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.

TSP_REQUESTS_PER_MINUTE

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: true and destructiveHint: false. The only POSTs the server sends are POST /oauth/token, POST /reports/timesheets, POST /invoices and POST /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 sends include=address,billing only 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 sends Include=assignedrates only then; Include=assignedcontractorcodes is 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, TotalPay in Standard; TotalPay, NetMargin, GrossMargin, Deductions in 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-billing invoice_type counts as self-billing, since the invoiceSelfBilling flag 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, swiftCode and bankCountryCode fields), the national insurance and tax number fields, the payroll data in employmentDetails, the HR data in personalDetails (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. The includedFields parameter of GET /users (whose documented values include bankDetails and personalDetails) 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: +/00 international numbers, bracketed UK area codes and UK 0… 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 with include_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 without include_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 Requests with a Retry-After header giving the seconds until the limit resets, and the response states which limit was reached. This server spaces its requests by TSP_REQUESTS_PER_MINUTE (2 seconds apart by default). A 429 is retried at most twice, for any method, waiting for Retry-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 GET only (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_invoices and list_leave fetch 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-based page parameter. Only /projects documents 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 ignoring page) is reported as incomplete rather than looped over. Each call fetches at most max_pages pages (2 for projects, 4 otherwise), stops once max_results records 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-DD dates; date-times are YYYY-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_ID and TSP_CLIENT_SECRET and 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 ResponseModel with success: false, say), is reported as an error naming TSP_BASE_URL and 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 and Retry-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 test

The suite downloads both specs (docs/standardJson to spec-standard.json, docs/recruitmentJson to spec-recruitment.json) on the first run and then:

  1. 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, InvoiceModel with InvoiceItemModel, LeaveSummaryModel, and the report rows against the documented POST /reports/timesheets and POST /leavebookings/reports/bookings responses (arrays of arrays of strings), with every leave field name checked against the LeaveReportSettings enum. 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, so date-time is checked with the zone optional; the suite proves the published example request fails the strict format and passes this one (after leaving out the null filters 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.

  2. 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, ResponseModel and an array of ValidationErrorModel where those are documented, and a 429 with Retry-After using 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 with MissingAdministratorPermission for a non-administrator, an empty array for an unknown user id.

  3. 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=true adds 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 from next_page; every documented filter sent under its documented name and date format (lastModified as yyyy-MM-ddTHH:mm:ss, ModifiedSince as yyyy-mm-dd HH:MM:SS, EndsAfter/StartsBefore as yyyy-MM-dd HH:mm); the report, leave report and invoice request bodies validated against ReportSettingsModel, LeaveReportSettings and InvoiceReportSettingsModel of 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_results capping 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 optional invoiceSelfBilling flag 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 with Retry-After in 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 for GET and 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 of TSP_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 the TSP_CALL_BUDGET_SECONDS budget 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.com is literally the shared host (as the general guidance writes it) or a placeholder for a per-account host (the spec's security definitions write yourhost.api.timesheetportal.com).

  • The token request end to end: that a form-encoded body is accepted (the guidance says JSON or form), that expiry_time comes back as documented (the server also reads expires_in and assumes an hour when neither is present), and the HTTP status of the documented invalid_client error (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 with MissingAdministratorPermission, an error code the spec lists).

  • Paging: the page size of /clients, /users and /jobs (undocumented), that page is zero-based on /projects too (the request models say zero-based; /projects only 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 reportFormat ValuesArray each row is an array of strings in the order of reportFields (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, that All in timesheetStatusFilters includes every status, that pageIndex is 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, contractorFilter and chargeCodeGroupFilter have no description in the spec, and whether jobFilter takes a job code.

  • Date formats and zones: the parameters document several formats (above), and the EndsAfter/StartsBefore parameters of GET /jobs say yyyy-MM-dd HH:mm while its 400 description says yyyy-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 of reportFields (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 out statusFilters means every status (the spec says "Set to null for no status filtering"; this server never sends a null), how startDate and endDate select bookings that span them, whether the report is paged (nothing is documented), and whether it counts against the report limits.

  • POST /invoices: that invoiceReportType selects client or self-billing invoices and credit notes (its spec description reads "Deprecated - (only used by /invoices/ POST endpoint)"), that an empty invoiceStatusFilters returns every status as documented, that costCentreCode and chargeCodeGroupCodes filter 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 the leaveYear format.

  • Which fields the live API fills: whether GET /users returns email, mobile and date of birth without includedFields, whether GET /clients needs include=address,billing for the address and billing fields, and whether GET /jobs returns the assigned contractors' names without an Include value (their codes are requested with Include=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_MINUTE can 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.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables 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.
    -
  • A
    license
    A
    quality
    C
    maintenance
    It 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.
    10
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    Enables 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.
    9
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Lets 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