officient-mcp
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., "@officient-mcp show me who is on leave this 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.
officient-mcp
An MCP server for the Officient HR API (https://api.officient.io), so an AI assistant such as Claude Code can read people, calendars, contracts, expenses, documents, fleet and webhooks — and book time off — through typed tools instead of raw HTTP.
It ships the complete Officient API surface without dumping 99 tool definitions into your context:
2 discovery tools that read a bundled copy of the API spec (no network, no scopes needed).
1 generic escape hatch (
officient_request) that can call any documented endpoint, validated against the spec before it fires.21 curated, typed tools for the workflows people actually use every day.
Requirements
Node.js 20 or newer
An Officient access token (see below)
Related MCP server: Tipsoi MCP
Install and build
git clone <this-repo> officient-mcp
cd officient-mcp
npm install # runs the build automatically
npm run build # or build again by hand
npm test # build + unit/integration tests, no network callsGetting an access token
There is no OAuth flow here. Claude Code's OAuth support is for HTTP and SSE servers only — a stdio server cannot participate in it. This server manages its own credential, which means a long-lived personal access token that you generate and rotate yourself.
Log in to Officient as an admin.
Open the avatar menu (top right) → Developers, i.e.
https://<your-tenant>.officient.io/developer.Create a client app if you do not have one, then Manage app → Generate access token and pick an expiry. Copy the token immediately — Officient shows it once.
Put it in the environment as
OFFICIENT_ACCESS_TOKEN(see Connecting it to Claude Code).Diary the expiry. When the token lapses, generate a new one the same way; nothing else changes.
Scopes live on the app, not on the token
This trips everyone up: you do not choose scopes while generating a token. Scopes are configured on the client app, under Manage app → Edit scopes. A token inherits whatever the app had ticked at the moment it was issued.
So if a call comes back with HTTP 401 and {"error":"insufficient_scope"}, the token is fine —
the app was never granted that scope. The fix is:
Manage app → Edit scopes, tick the scope you need.
Generate a fresh access token (existing tokens do not pick up newly added scopes).
Update
OFFICIENT_ACCESS_TOKENand restart the server.
A plain 401 without insufficient_scope is the other problem entirely: the token itself is invalid,
revoked or expired.
Officient scopes are <object>:<action>, e.g. calendar:read. Grant the app only what you need:
Scope | Unlocks |
| people list, teams, assets, fleet, roles |
| person detail, people search |
| calendars, event types, days-off requests |
| adding / overwriting / deleting calendar events |
| weekly schedules |
| contracts |
| documents (Officient spells these with a dot) |
| expenses |
| wages, cost centers, cost units, departments, functions |
| custom fields on a person |
| webhook subscriptions |
Every tool description names the scope it needs. officient_describe_endpoint does the same for all 99 endpoints.
Configuration
Configuration comes from environment variables only. A local .env is loaded for development (see .env.example).
Variable | Required | Default | Meaning |
| yes | — | Bearer token; the server refuses to start without it |
| no |
| Override for sandboxes/proxies |
| no |
| Per-request timeout in milliseconds |
The token is never logged and is redacted from anything the server prints.
Connecting it to Claude Code
Claude Code has three config scopes. Which one you want depends on whether the config is allowed to touch disk with a secret in it:
Scope | Stored in | Shared? | Use it when |
|
| no | Recommended here. Personal machine, secret-bearing server |
|
| yes, via git | A team shares the config; the token must come from each dev's shell |
|
| no, but all your projects | You want Officient available everywhere you work |
Registering the server
claude mcp add officient --scope local --env OFFICIENT_ACCESS_TOKEN=YOUR_TOKEN_HERE \
-- node /absolute/path/to/officient-mcp/dist/index.jsTwo CLI quirks worth knowing, both verified against Claude Code as of 2026-07:
Put the server name first. --env is variadic, so it swallows the following argument as a
second environment variable. Writing --env KEY=value officient fails with:
Invalid environment variable format: officient,
environment variables should be added as: -e KEY1=value1 -e KEY2=value2--env requires KEY=value. A bare --env OFFICIENT_ACCESS_TOKEN, meaning "pass my shell's
value through", is rejected the same way. If a later version adds passthrough, prefer it.
The trade-off is therefore real and unavoidable on this path: the literal token is persisted in
plaintext in ~/.claude.json, where it survives rotation, ends up in backups, and has to be
scrubbed by hand. If that matters to you, use the project-scoped .mcp.json route below instead —
that one does expand ${OFFICIENT_ACCESS_TOKEN} from the environment, so the secret stays in your
shell profile or password manager and never touches Claude Code's config.
Alternative: project scope via the committed .mcp.json
This repo ships a .mcp.json at its root:
{
"mcpServers": {
"officient": {
"type": "stdio",
"command": "node",
"args": ["${CLAUDE_PROJECT_DIR:-.}/dist/index.js"],
"env": {
"OFFICIENT_ACCESS_TOKEN": "${OFFICIENT_ACCESS_TOKEN}"
}
}
}
}Claude Code expands ${VAR} and ${VAR:-default} inside command, args, env, url and
headers, and ${CLAUDE_PROJECT_DIR} resolves to the project root — so the file is committed with
no secret and no absolute path in it. Each developer supplies the token from their own shell:
export OFFICIENT_ACCESS_TOKEN=...
npm install && npm run build # .mcp.json points at dist/, so it must exist
claude # Claude Code prompts once to approve the project serverNever replace ${OFFICIENT_ACCESS_TOKEN} with a literal in this file — it is committed to git.
Verifying
claude mcp list # should show officient as connected
OFFICIENT_ACCESS_TOKEN=... npm start # or boot it by hand; it stays silent when healthyIf the token is missing, the server writes an actionable message to stderr and exits with code 1. Claude Code does not auto-restart stdio servers, so a crash is visible rather than silently retried. Runtime API failures (401, 429, 4xx) are returned as MCP tool errors instead — the process stays up.
Tools
Discovery and escape hatch
Tool | Purpose |
| Filter all 99 documented endpoints by tag / method / free text; returns compact one-liners including the required scope. Offline. |
| Everything about one |
| Call any endpoint by |
Curated tools
Tool | Purpose | Scope |
| Account behind the current token |
|
| List employees (30/page, zero-indexed) |
|
| Find people by name / email / national number |
|
| Full detail for one employee |
|
| Custom field values on a person |
|
| Current weekly working schedule |
|
| Teams and their members |
|
| One person, one day |
|
| One person, one month |
|
| One person, one year ( |
|
| Custom event types for a year, incl. the id needed to book a custom event |
|
| Days-off requests, filterable by status |
|
| Writes. Add one or more events (day off, sick day, education, overtime) |
|
| Writes. Idempotent upsert of one event of a type on a date |
|
| Writes. Remove one calendar event |
|
| Employment contracts |
|
| Expenses for a year, or one month |
|
| Documents on an employee / asset / car |
|
| Fleet vehicles, optionally by owner |
|
| Company assets, optionally by owner |
|
| Webhook subscriptions |
|
Anything not in this table — wages, budgets, dimonas, cost centers, performance reviews, uploads, person/team/vehicle mutations — is reachable through officient_request. Start with officient_list_endpoints.
Gotchas worth knowing
Pagination is zero-indexed. ?page=0 is the first page, ?page=1 is the second. 30 items per page. The curated tools default page to 0.
Rate limit: 30 requests per 5 seconds. Officient answers a breach with HTTP 429 and an empty body, and sends no Retry-After header. The server therefore runs a client-side sliding-window limiter: calls beyond the budget are queued, never dropped, and a 429 that still slips through is retried once with backoff before being surfaced.
401 is two different problems. A missing OAuth scope returns HTTP 401 with {"error":"insufficient_scope"} — the token is fine, the app just was not granted that scope; fix it in the developer dashboard and issue a new token. A plain 401 means the token itself is invalid, revoked or expired. The server reports these as kind: "insufficient_scope" and kind: "invalid_token" respectively.
Validation errors carry a human sentence. Officient replies to bad writes with {"status_code": 400, "reason_phrase": "There is already too much time off planned on <date>."}. That phrase is surfaced verbatim.
overwrite-event silently does nothing if only the start time differs. Observed live 2026-07: when an event of the given type already exists on that date with the same duration_minutes, the endpoint returns {"success": 1, "info": "no action taken"} and leaves the event untouched — it does not compare start_time_minutes. Note that success: 1 here means "request accepted", not "your change was applied". To move an existing event's start time you have to fall back to delete-event + add-event, which is exactly the delete-and-add cycle this endpoint was documented to spare you. Read the calendar back after any write rather than trusting success: 1.
A full-day event still needs a sensible start_time_minutes. duration_minutes: "all_day" correctly resolves to the person's scheduled minutes, but the start time is stored as given. Passing 0 yields a full-day event pinned to 00:00, which renders oddly next to events created in the UI. Read an existing event on a normal working day to see the house convention (typically 540, i.e. 09:00).
Team membership is not readable. list-teams returns id and name only — no members, despite what the docs imply — and there is no team-detail endpoint. The org structure lives entirely in the manager relations: walk list-people and call person-manager per person.
person-manager returns the manager, not the pairing. The response is {person_id, person_name, start_date} describing the manager, with nothing identifying whose manager it is. Fan out over several people concurrently and you can only map results back to subjects by request order — so either issue the calls sequentially, or tag each response yourself at the call site. Silently mis-mapping an org chart is an easy and expensive mistake here.
Uploads are base64 JSON, not multipart. Document and avatar endpoints take document_base64 / photo_base64 fields inside a normal JSON body.
Responses are lightly de-enveloped. Officient wraps payloads in a single data key; the server strips exactly that one level and returns compact JSON. Nothing else is filtered and nothing is truncated.
Two tools raise the output cap. Claude Code warns at 10k tokens of tool output and cuts off at 25k
(override with MAX_MCP_OUTPUT_TOKENS). Two tools declare a higher ceiling via
_meta["anthropic/maxResultSizeChars"], because truncating them loses data silently rather than
failing loudly: officient_get_year_calendar (200 000 chars — 365 day entries, each with an events
array) and officient_request (300 000 chars — it can reach any endpoint, so it needs the worst
case covered). Every other tool stays on the client default; the caps were measured, not guessed —
officient_describe_endpoint, for instance, peaks at ~2.6 KB and needs nothing.
The server never writes to stdout. stdout carries the JSON-RPC stream, so a single stray byte
breaks the transport. All diagnostics go to stderr, and installStdoutGuard() repoints every
stdout-writing console method at stderr before config is loaded — dotenv ≥ 17 prints its banner with
console.log and honours DOTENV_CONFIG_DEBUG from the inherited environment, which would otherwise
override the quiet: true this server passes.
Regenerating the spec
npm run harvest # node scripts/harvest-spec.mjsThe harvester refetches every documentation page from https://apidocs.officient.io and writes three
things deterministically, so re-running with no upstream change produces no diff:
Output | Committed? | What it is |
| yes | Flat endpoint index the tools load at runtime |
| yes | Merged OpenAPI 3.1 document |
| no (gitignored) | ~100 documentation pages copied verbatim |
spec/raw/ is Officient's copyrighted documentation, so this repo does not redistribute it: it is
gitignored and excluded from the npm tarball, but kept on disk as the harvester's cache. Run
npm run harvest to regenerate it from the public docs — nothing at runtime ever reads it, so a
fresh clone without spec/raw/ works exactly the same (the test suite asserts both halves of that).
Rebuild afterwards (npm run build) and re-run npm test — the suite asserts that every curated tool
still maps onto a real operationId.
Development
npm run dev # tsc --watch
npm run typecheck # tsc --noEmit
npm test # build + node:test suiteThe test suite makes no real API calls: HTTP is stubbed, and the stdio smoke test boots the server with a dummy token against an unreachable base URL.
License
MIT — see LICENSE.
Available Tools
24 toolsofficient_add_calendar_eventAdd calendar eventsADestructive
Add one or more events (day off, sick day, education, overtime, …) to a person’s calendar. All events in one call must fall in the same year. Set type="custom" together with custom_day_off_type_id from officient_list_event_types; omit that id for sick_day / education. duration_minutes may be the string "all_day". Required scope: calendar:write. WRITES DATA in Officient.
Inputs: type (required), events (required), person_id (required), custom_day_off_type_id.
officient_delete_calendar_eventDelete a calendar eventADestructive
Permanently remove one calendar event (e.g. a day off) from a person’s calendar. Get the event_id from a calendar read tool first. Required scope: calendar:write. WRITES DATA in Officient.
Inputs: event_id (required), person_id (required).
officient_describe_endpointDescribe an Officient API endpointARead-only
Full detail for one Officient endpoint from the bundled spec (no network call, no scope needed): path/query parameters, request body schema and a worked example, the required OAuth scope, the documentation prose and the doc URL. Use before calling officient_request. READ-ONLY.
Inputs: operationId (required).
officient_get_day_calendarGet a person’s calendar for one dayARead-only
Time off, overtime and scheduled minutes for a single person on a single date. Required scope: calendar:read. READ-ONLY.
Inputs: day (required), year (required), month (required), person_id (required).
officient_get_month_calendarGet a person’s calendar for one monthARead-only
Time off, overtime and scheduled minutes for a single person for one month. Required scope: calendar:read. READ-ONLY.
Inputs: year (required), month (required), person_id (required).
officient_get_own_accountGet own Officient accountARead-only
Information about the account behind the current access token (company, user, locale). Required scope: basics:read. READ-ONLY.
Inputs: no named parameters.
officient_get_personGet person detailARead-only
Full detail for one employee: personal data, contract summary, team, manager and more. Required scope: personal.info:read. READ-ONLY.
Inputs: person_id (required).
officient_get_person_custom_fieldsGet a person’s custom fieldsARead-only
Custom field values configured on one employee (text, number, date, money, email, select_option). Required scope: personal.info.custom.fields:read. READ-ONLY.
Inputs: person_id (required).
officient_get_weekly_scheduleGet current weekly scheduleARead-only
The employee’s current weekly working schedule (scheduled minutes per weekday). Required scope: weekly.schedules:read. READ-ONLY.
Inputs: person_id (required).
officient_get_year_calendarGet a person’s calendar for one yearARead-only
Every day of the year for one person, each with scheduled_minutes and any events. Use days_with_events to keep the response small. Required scope: calendar:read. READ-ONLY.
Inputs: year (required), person_id (required), filter.
officient_list_assetsList assetsARead-only
Company assets (laptops, phones, badges, …), optionally filtered by owner. Zero-indexed pagination. Required scope: basics:read. READ-ONLY.
Inputs: page, person_id.
officient_list_contractsList contractsARead-only
All employment contracts in the account, 30 per page, zero-indexed pagination. Required scope: contracts:read. READ-ONLY.
Inputs: page.
officient_list_days_off_requestsList days-off requestsARead-only
Calendar (days off) requests across the account, 30 per page, zero-indexed pagination. Required scope: calendar:read. READ-ONLY.
Inputs: page, status.
officient_list_documentsList documentsARead-only
Documents attached to an employee, asset or car. Zero-indexed pagination. Use officient_request with operationId "download-document" to get a download URL. Required scope: documents.read. READ-ONLY.
Inputs: object_id (required), object_type (required), page.
officient_list_endpointsList Officient API endpointsARead-only
Discover Officient API endpoints from the bundled spec (no network call, no scope needed). Returns compact one-liners: operationId | METHOD path | summary | required scope | read/WRITE. Filter by tag, HTTP method or free text, then feed an operationId into officient_describe_endpoint or officient_request. READ-ONLY.
Inputs: tag, limit, method, search, +1 more.
officient_list_event_typesList custom calendar event types for a yearARead-only
Custom event types (holiday, remote work, overtime types, …) available in a given year, with the custom_day_off_type_id needed to add a "custom" calendar event. Required scope: calendar:read. READ-ONLY.
Inputs: year (required).
officient_list_expensesList expensesARead-only
Expenses for a whole year, or for one month when month is given. Zero-indexed pagination. Required scope: expenses:read. READ-ONLY.
Inputs: year (required), page, month, include_deleted.
officient_list_peopleList peopleARead-only
List employees, 30 per page. Pagination is zero-indexed: page 0 is the first page. Required scope: basics:read. READ-ONLY.
Inputs: page, include_archived.
officient_list_teamsList teamsARead-only
All teams in the account. Returns id and name only — despite what the upstream docs imply, the response carries NO member list (verified live 2026-07). There is no team-detail endpoint either, so team membership is not readable over the API. To reconstruct the org structure, walk officient_list_people and call the "person-manager" operation per person via officient_request instead. Required scope: basics:read. READ-ONLY.
Inputs: no named parameters.
officient_list_vehiclesList fleet vehiclesARead-only
Vehicles in the fleet, optionally only those assigned to one person. Zero-indexed pagination. Required scope: basics:read. READ-ONLY.
Inputs: page, person_id.
officient_list_webhooksList webhooksARead-only
Webhook subscriptions configured on the account. Officient POSTs {account, event_type, related_object_id} to the target URL and retries on non-200. Required scope: webhooks:read. READ-ONLY.
Inputs: no named parameters.
officient_overwrite_calendar_eventCreate or overwrite one calendar eventADestructive
Idempotent upsert of a single event of a given type on a given date: overwrites the existing event of that type or creates it. Handy for syncing time tracking. The event type itself cannot be changed. CAVEAT (observed live 2026-07): when an event of that type already exists on that date with the same duration_minutes, the API returns {"success":1,"info":"no action taken"} and changes NOTHING — it does not compare start_time_minutes. To move an existing event's start time, delete it with officient_delete_calendar_event and re-add it with officient_add_calendar_event. Always read the calendar back to confirm the change actually landed. Required scope: calendar:write. WRITES DATA in Officient.
Inputs: day (required), type (required), year (required), month (required), +4 more.
officient_requestCall any Officient API endpointADestructive
Escape hatch that reaches every one of the documented Officient endpoints by operationId. Input is validated against the bundled spec before anything is sent. Required scope depends on the endpoint — officient_describe_endpoint tells you which one, and whether it writes. CAN WRITE: this tool will happily fire POST/PATCH/PUT/DELETE endpoints, so check the endpoint first. Pagination is zero-indexed (page=0 is the first page). Prefer the dedicated officient_* tools when one exists.
Inputs: operationId (required), body, query, dry_run, +1 more.
officient_search_peopleSearch peopleARead-only
Find people by name, work email or national number. Supply at least one criterion. Required scope: personal.info:read. READ-ONLY.
Inputs: name, email, national_number.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 24 tools
Every tool has a clearly distinct purpose, with no overlap. Even the calendar-related tools (get_day_calendar, get_month_calendar, add_calendar_event, etc.) are well-differentiated by granularity and action.
All tools follow a consistent 'officient_verb_noun' snake_case pattern. Verbs like list, get, search, add, overwrite, delete are used predictably, with no mixed conventions.
24 tools is appropriate for a comprehensive HR API covering people, calendar, expenses, contracts, and more. Each tool addresses a distinct operation, and the meta-tools (list_endpoints, describe_endpoint, request) add valuable discoverability without bloat.
The tool set covers most core HR workflows (people, calendar, expenses, contracts, etc.) with dedicated tools. Minor gaps exist (e.g., no create/update for people, no webhook management beyond listing), but the officient_request escape hatch mitigates these, and the surface handles primary use cases well.
Maintenance
Related MCP Connectors
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
MCP server that lets AI assistants use all OneSchema features exposed via the public API.
One MCP server for the services your AI needs: memory, files, Microsoft 365, kanban and more.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceA comprehensive MCP server that provides AI assistants with full access to FactorialHR to manage employees, teams, time off, projects, training, recruiting, and more, all with built-in safety guardrails.314 npm3MIT
- FlicenseAqualityDmaintenanceRead-only MCP server for the Tipsoi HRM API, exposing 15 tools to read employee data, attendance, leave, overtime, and more.153-
- AlicenseNot gradedqualityDmaintenanceAn MCP server that exposes the Lattice Talent API as tools, enabling AI assistants like Claude to read and manage people data, goals, feedback, reviews, and more.304 npmMIT
- AlicenseNot gradedqualityDmaintenanceMCP server for the HR platform 'МояКоманда' that enables AI assistants to access and interact with HR data like employees, teams, calendar, absences, requests, knowledge base, surveys, and more via its REST API.MIT