> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hr-easy.nlead.ch/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP connector

> Endpoints, OAuth flow, scopes and the tool catalog of the HR Easy Model Context Protocol server.

HR Easy exposes a remote **MCP** server (Streamable HTTP, stateless JSON) and
acts as its own **OAuth 2.1 authorization server**. Any MCP client that
implements the standard authorization flow — Claude, ChatGPT connectors,
Cursor, n8n — can connect a person's account. The connector is enabled per
installation with `FEATURE_MCP_CONNECTOR=true` and the public origin in
`MCP_ISSUER_URL`.

Separately, this documentation site serves its own public MCP endpoint — see
[Two MCP servers](#two-mcp-servers) for which one to connect.

## Two MCP servers

An installation is reachable over MCP at two independent endpoints. They serve
different content under different authorization, and an assistant can be
connected to either or both.

| Server | URL | Authorization | Serves |
| - | - | - | - |
| **Documentation** | `https://docs.hr-easy.nlead.ch/mcp` | None — the content is public | This documentation site |
| **Connector** | `https://<installation-host>/api/mcp` | OAuth 2.1, consent behind Entra SSO | The installation's own people and records |

The rest of this page documents the **connector**. The documentation server is
described directly below and needs no setup.

### The documentation server

The documentation site is a [Mintlify](https://mintlify.com) site, and Mintlify
serves an MCP endpoint for it at `/mcp`. It is generated from the published
pages, so it is always as current as the site, and it exposes three tools:

| Tool | Does |
| - | - |
| `search_swiss_hr_easy` | Searches the site and returns snippets with titles and deep links |
| `query_docs_filesystem_swiss_hr_easy` | Navigates the page tree with shell-style commands |
| `submit_feedback` | Reports a wrong, outdated or incomplete page back to the docs team |

Add it to Claude Code with:

```bash theme={null}
claude mcp add --transport http hr-easy-docs https://docs.hr-easy.nlead.ch/mcp
```

It indexes only what is published here, which is product documentation and
nothing else. Customer-specific inventories, hostnames and configuration live
in the product repository under `docs/customers/<customer>/` and are never part
of this site, so they are not reachable through this server either.

<Note>
  Answers are only as good as the page behind them, and the guides are currently English-only — a
  German or French question is answered from the translated [What's new](/whats-new/overview) pages
  or from an English guide.
</Note>

## Endpoints

| Path | Purpose |
| - | - |
| `POST /api/mcp` | The MCP endpoint. Bearer access token required; `GET` answers `405`. |
| `/.well-known/oauth-protected-resource/api/mcp` | RFC 9728 protected-resource metadata (also served at the origin root). |
| `/.well-known/oauth-authorization-server` | RFC 8414 authorization-server metadata (also served with the `/api/mcp` suffix). |
| `POST /api/oauth/register` | RFC 7591 dynamic client registration — only while the installation allows it. |
| `GET /oauth/authorize` | Authorization request; SSO step-up and consent screen. |
| `POST /api/oauth/token` | Authorization-code (PKCE S256) and refresh-token grants. |
| `POST /api/oauth/revoke` | RFC 7009 revocation. |

Rules a client must follow:

* PKCE with `S256` on every authorization request; `plain` is refused.
* The `resource` parameter (RFC 8707) must equal the canonical MCP URL,
  `<issuer>/api/mcp`, on both the authorization and the token request.
* Refresh tokens rotate on every use; reusing a rotated token revokes the
  whole token family.
* Dynamic registrations are public clients (`token_endpoint_auth_method:
  "none"`); an installation may instead create a confidential client by hand and
  hand its id and secret to the assistant's advanced settings.
* Every unauthenticated call to `/api/mcp` answers `401` with a
  `WWW-Authenticate: Bearer` challenge carrying the `resource_metadata` URL.

## Scopes

| Scope | Grants |
| - | - |
| `self:read` | The connected person's own records |
| `hr:read:people` | Employees, positions, the HR overview and the approvals queue, as far as the role may see them |
| `hr:read:recruiting` | Applications, as far as the role may see them |
| `self:write` | The connected person's own leave requests, time entries and sick-leave reports (prepare → commit), withdraw and cancel |
| `hr:write` | Deciding a leave request, adding an onboarding task, noting on an application and moving one to another stage of its pipeline, for the people the role may already see (prepare → commit) |

A grant never widens: a refresh or a later consent carries at most the scopes
first approved. A person's role is re-read from the database on every call, so
a role change or deactivation takes effect on the next request.

## Limits and records

* 60 calls per minute per connection; the HR overview additionally 6 per hour;
  writes (commit, withdraw, cancel) 20 per day per connection.
* A daily allowance of distinct person records per connection (installation
  setting, default 1 000). Reads beyond it are refused until the next UTC day.
* Every list returns at most 50 rows; search at most 25.
* Every call writes an audit row: assistant, connection, tool, argument
  names (free text as length and hash only), the records touched, result
  count and outcome. A connection that touches 50 or more different people in
  a day is additionally recorded as an export.
* Writes are two steps. A `prepare_*` tool computes the request on the server
  (days deducted, balance after, approver; derived hours; days covered) and
  returns a preview plus a single-use `previewId` bound to the connection;
  the matching `commit_*` tool takes only that id and files the stored
  preview — nothing the assistant says in between changes what is written.
  Previews expire after 15 minutes and a retried commit replays the first
  result. For an HR write the caller's authority over the record — the
  approval scope, the HR scope, the recruiting scope — is checked again at the
  commit, so a reporting line, an assignment or a position role that changed
  inside those 15 minutes is honoured. A self-service write takes no free text
  at all (no reason, note or description). An HR write takes only the text that
  *is* the record — a rejection note, a task title, an application note — and it
  is taken at the prepare step, so the person approves the exact wording before
  the commit files it. An application note is always filed as a shared one: the
  read side never returns private HR notes, so the connector never writes one.
* A stage move names, before it happens, every automatic task the destination
  stage fires — flagging the ones that e-mail the candidate and whether the
  stage marks them as hired — because none of that is undoable. The
  compliance override that bypasses a stage's blocking-task gate is not
  offered over the connector at all: a move the gate blocks is refused, and
  releasing it stays a person's job inside HR Easy.

## Tool catalog

The catalog below is generated from the server's tool registry
(`npm run gen:mcp-catalog`); a drift check in CI keeps it identical to what
`tools/list` returns. Each tool declares its scope, the permission the role
must hold, MCP annotations (read tools and previews are read-only; commit,
withdraw and cancel are marked destructive so a client asks first), and JSON
Schemas for its arguments and its result.

<Card title="mcp-tools.json" icon="file-code" href="/api-reference/mcp-tools.json">
  The machine-readable catalog: 36 tools with input and output schemas, plus the resource template
  and the prompts.
</Card>

| Tool | Scope | Permission | Returns |
| - | - | - | - |
| `whoami` | `self:read` | `mcp:connect` | The acting user, role, granted scopes and available tools |
| `get_my_summary` | `self:read` | `mcp:connect` | Leave balance, current week, next payout, to-dos |
| `get_my_leave_balances` | `self:read` | `mcp:connect` | Balances per leave type for a leave year |
| `list_my_leave_requests` | `self:read` | `mcp:connect` | Own requests; reason only with `include_free_text` |
| `list_my_time_entries` | `self:read` | `mcp:connect` | Entries in a day range (≤ 400 days) and their sum |
| `get_my_sick_leave` | `self:read` | `mcp:connect` | Entries and totals for a calendar year; never the reason |
| `list_my_payslips` | `self:read` | `mcp:connect` | Period, payment date and a portal link; no amounts, no PDF |
| `get_public_holidays` | `self:read` | `mcp:connect` | Public (and school) holidays for the caller's canton |
| `get_closure_periods` | `self:read` | `mcp:connect` | Company closure days that apply to the caller |
| `search_people` | `hr:read:people` | `employees:view` | Active employees by name within the caller's scope (≤ 25) |
| `list_employees` | `hr:read:people` | `employees:view` | Employees within scope, active by default |
| `get_employee` | `hr:read:people` | `employees:view` | Master data without sensitive fields or salary |
| `list_positions` | `hr:read:people` | `positions:view` | Positions within scope; no salary bands |
| `get_position` | `hr:read:people` | `positions:view` | One position with capped ad text |
| `get_hr_overview` | `hr:read:people` | `ai-chat:use` (HR+) | Installation aggregates; counts below five withheld |
| `list_pending_approvals` | `hr:read:people` | `staff-absence:approve` | Submitted absences and expense reports awaiting the caller |
| `list_applications` | `hr:read:recruiting` | `applications:view` | Applications within scope; no AI scores or notes |
| `get_application` | `hr:read:recruiting` | `applications:view` | One application; cover letter and shared notes only with free text |
| `prepare_leave_request` | `self:write` | `leave:request` | Preview: days deducted, balance before/after, approver; `previewId` |
| `commit_leave_request` | `self:write` | `leave:request` | Files the previewed request; the manager is told it came via assistant |
| `withdraw_leave_request` | `self:write` | `leave:request` | Takes back an own request still awaiting approval |
| `prepare_time_entry` | `self:write` | `workhours:submit` | Preview: derived net hours and what the day already holds |
| `commit_time_entry` | `self:write` | `workhours:submit` | Records the previewed entry as a draft |
| `prepare_sick_report` | `self:write` | `sickleave:report` | Preview: days covered or ongoing; no reason is taken |
| `commit_sick_report` | `self:write` | `sickleave:report` | Files the previewed report |
| `cancel_sick_report` | `self:write` | `sickleave:report` | Takes back an own report within 24 hours, without a certificate on file |
| `list_documents` | `self:read` | `mcp:connect` | The installation's assistant-visible documents the caller may read |
| `get_document` | `self:read` | `mcp:connect` | One document as Markdown, in the caller's language |
| `prepare_leave_decision` | `hr:write` | `staff-absence:approve` | Preview: whose request, the days, their remaining days before and after |
| `commit_leave_decision` | `hr:write` | `staff-absence:approve` | Approves or rejects the previewed request in the caller's name |
| `prepare_onboarding_task` | `hr:write` | `employees:onboarding` | Preview: whose plan the task joins and where it lands |
| `commit_onboarding_task` | `hr:write` | `employees:onboarding` | Adds the previewed task to that plan |
| `prepare_application_note` | `hr:write` | `applications:view` | Preview: whose application, the position and stage, notes already on it |
| `commit_application_note` | `hr:write` | `applications:view` | Files the previewed note as a shared one; never a private HR note |
| `prepare_stage_move` | `hr:write` | `applications:move-stage` | Preview: both stages and every automatic task the destination fires |
| `commit_stage_move` | `hr:write` | `applications:move-stage` | Moves the application as previewed; the stage's tasks then run |

## Resources and prompts

The same documents are served as MCP **resources** under
`hr-easy://documents/{slug}` (`text/markdown`). `resources/list` returns only
the documents the connected person's role may read; `resources/read` applies
the same rule and writes an audit row like a tool call. Documents are the
installation's own texts — handbook chapters, policies — authored under
Settings → AI connections → Documents in German with optional English and
French translations; a reader without a translation gets the German original.
An audience of *all employees*, *managers and above* or *HR staff and above*
governs who may read each one.

Two **prompts** are offered to the roles that may use them:

| Prompt | Who | Argument | What it asks the assistant to do |
| - | - | - | - |
| `weekly_hr_briefing` | HR staff and above | — | This week's briefing from the overview, the approvals queue and the calendar; about the organisation, no rankings |
| `prepare_1on1` | Managers and above | `person` | Prepare a one-to-one as a conversation about fit and support: open questions, facts to know, answers owed |

Both are rendered in the person's language and end with the ADR-006 guard rail:
no ratings, no comparison between people, no speculation about health or
private matters.

Every HR-scope tool description ends with the same sentence: *never use this to
rank, score or compare people against each other*. HR Easy does not support
rankings; see the product's [design principles](/concepts/design-philosophy).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.