Integrations
MCP agents
Connect an AI agent with an API key. What it is handed is an intent — a job — not a list of endpoints, the list is filtered to what that key can do, sensitive jobs ask first, and every call lands in the audit log.
Last updated
Who this is for: Owner — requires
integration.mcp.connect, andintegration.job.runto submit jobs. Where: Integrations → MCP Agents
An MCP agent is an AI system you let operate the hotel. Nerve exposes its capabilities at /api/mcp, and an agent connects with an ordinary API key — the same kind of credential a script would use, carrying the same permissions, writing to the same audit log.
That is the whole design, and it is worth saying plainly before the mechanics: an agent is not a new kind of user with a new kind of access. It is a credential inside the model you already have. See Open Hotel Protocols for why the product is built this way.
What an agent is handed is an intent, not an endpoint#
This is the part that changed, and it changes how you should think about the whole surface.
The original tool set mapped one-to-one onto API calls — list_reservations, create_guest, and so on. An agent that wanted to check a guest in had to discover those primitives, sequence them itself, and get the order right, including the order in which money moves. Three things followed, and all three were bad:
- the orchestration lived outside the boundary that governs it, in a client nobody here reviews;
- the audit trail was a sequence of unlabelled calls whose intent had to be inferred afterwards — which is exactly when inference is least reliable;
- the confirmation gate bound each fragment rather than the decision, so a human approved four writes and nobody ever approved "check this guest in".
A job is the repair. A job is one declared intent — check a guest in, settle a folio — carrying its input and output schema, the permission set it resolves, the authority envelope it runs inside, whether it is sensitive, and the primitives it is allowed to call. The agent asks for the outcome. Nerve does the sequencing, inside the boundary that governs it.

Each job declares everything about itself on one row: the permissions it resolves, the primitives it composes — get_arrivals, check_in_stay, get_stay_folio — and its authority envelope, in words. "May assign a free room of the booked type… may not override the room-readiness gate."
Important
Jobs roll out per organisation. The job surface ships behind a flag. Until it is switched on for your organisation, the job endpoints and the job tools report as though jobs do not exist — that is the intended dark state, not a fault. The per-endpoint tools below keep working throughout.
The starter jobs#
| Job | Intent | Sensitive |
|---|---|---|
check_in_reservation |
Verify the stay, assign a room if needed, open the folio and take any deposit. | yes |
settle_folio |
Take the closing payment if one is needed, then issue the invoice. | yes |
quote_and_hold |
Quote a stay for a date range and report the offers that are actually sellable. | |
report_period_performance |
Occupancy, ADR and RevPAR for a period, with the bookings behind them. |
Each one resolves its own permission set. A key sees a job only if it holds every permission that job resolves, plus integration.job.run.
Connect an agent#
- Create an API key with
integration.mcp.connect, plusintegration.job.run, plus the permissions behind each job or tool the agent should reach. - Point the agent at
POST /api/mcp. - Send the key as
Authorization: Bearer nrvk_….
The endpoint speaks single-shot JSON-RPC over POST — one request, one response, no session to hold open.
curl -X POST http://localhost:8080/api/mcp \
-H "Authorization: Bearer nrvk_..." \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Important
integration.mcp.connectis a separate permission from the tools. A key withcrs.booking.readand nothing else is refused at the door — it never gets as far as being told which tools exist.
What tools/list returns, in order#
Jobs first, then job_status, then whichever per-endpoint tools are still inside their deprecation window. The order is deliberate: an agent picks from the top of a list, and the thing it should be picking is "check this guest in", not the four primitives it would otherwise have to sequence.
The primitives those jobs call — check_in_stay, take_folio_payment, quote_stay and the rest — are internal. They never appear on this list at all, for any key. They exist to be composed into an intent, not handed out as fragments.
The list is filtered, not just enforced#
This is stronger than it sounds. A key does not see what it cannot use and then get refused. It does not see it.
Calling something that was filtered out does not produce a permission error — it produces unknown tool:
{
"error": { "code": -32601, "message": "unknown tool: get_kpis" }
}That is deliberate. An agent reasons about what it can do from the list it was given, so a filtered surface is the only surface it knows about — and a refusal never tells it what it was not allowed to see.
The same shape holds on the job endpoints: a job that does not exist and a job you may not run answer identically. Telling an under-scoped caller that a job exists but is out of reach is a disclosure.
The practical consequence: scope the key, not the prompt. An instruction not to touch guest data is a request; a key without frontdesk.guest.read is a fact.
Submitting a job over HTTP#
Jobs are also a plain REST surface, for callers that are not speaking MCP.
# What can this key actually run?
curl -H "Authorization: Bearer nrvk_..." \
http://localhost:8080/api/v1/jobs
# Submit an intent. The idempotency key is yours to choose.
curl -X POST http://localhost:8080/api/v1/jobs \
-H "Authorization: Bearer nrvk_..." \
-H "Content-Type: application/json" \
-d '{
"job": "report_period_performance",
"arguments": { "start": "2026-08-01", "end": "2026-08-31" },
"idempotencyKey": "august-report-1"
}'The response is a durable handle — an id, a status, and the intent and authority envelope echoed back so the caller can see what it just authorised.
| Call | What it does |
|---|---|
GET /api/v1/jobs |
The jobs this key may run. A job it could not run is not listed. |
POST /api/v1/jobs |
Submit an intent. 201 for a new handle, 200 for a repeat. |
GET /api/v1/jobs/{id} |
Where the run got to. |
GET /api/v1/jobs/{id}/result |
The output. 409 while it is still running. |
GET /api/v1/jobs/{id}/stream |
The same progress, as it happens. |
Two details worth knowing before you build against it:
- Re-submitting is free. The same
idempotencyKeyreturns the same handle with200instead of201, and runs nothing twice. A caller that loses its connection retries the submission rather than reasoning about whether the check-in happened. - "Still running" and "finished with nothing" are different answers. Asking for a result too early is a
409, not an empty body.
The handle is state about the job, never about the agent. There is no session, no stored cognition, nothing that remembers a caller between submissions. Nerve runs a loop for the job; it does not become your agent's runtime.
A sensitive job asks before it acts#
Submitting a sensitive job without confirmation is refused with confirmation_required. Re-submit with "confirm": true and it is accepted.
This is a deliberate speed bump between "the model decided to" and "the hotel did" — and now it sits on the decision rather than on each fragment of it. Confirming check_in_reservation approves checking the guest in, which is a thing a person can actually agree to.
It is not a substitute for scoping the key. An agent that holds the permissions and confirms will check the guest in.
Every call is in the audit log#
- Open Settings → Audit Log.

Every call, including the refused ones. An agent's tool call sits in the same log as a person's action, and the denials are recorded as carefully as the successes.
Each row carries the key prefix as the actor, the channel (mcp for a tool call, api for the transport call underneath it, app for a human action), the tool or endpoint name, and the outcome.
A job is recorded as one record naming the intent — with its authority envelope, the permissions it ran under, and every primitive beneath it — at submission and again at completion. That is the difference the whole change is for: the log says "check in reservation 4471", not four calls someone has to reassemble into a guess.
Two things follow. There is no separate "AI log" to go and find — an agent's activity is in the same place as everything else, which is what makes "who did this" answerable at all. And a denied row is evidence, not noise: it is the record of the permission model refusing something, and a run of them is how you notice an agent trying to do a job it has not been scoped for.
The per-endpoint tools are deprecated#
The five original tools still answer, and every call inside the window carries a notice naming the job that replaces it and the date it stops answering. They become internal primitives on 1 March 2027.

The seam, on one screen. Above the divider, jobs. Below it, the same capabilities as per-endpoint tools — each badged deprecated, each naming its superseding job and its sunset date, so an integrator learns from the surface rather than from a changelog they may never read.
| Tool | Superseded by |
|---|---|
list_reservations |
check_in_reservation |
list_guests |
check_in_reservation |
list_bookings |
report_period_performance |
get_kpis |
report_period_performance |
create_guest |
check_in_reservation |
That date is set in the source rather than in configuration, on purpose: a deprecation window is a promise made to integrators, and a promise that can be shortened by an environment variable is not one.
list_engagements and submit_debrief are not deprecated — they belong to Protocols, which is an engagement surface rather than a per-endpoint one.
Every tool and every job is bound to the key's property. There is no argument for choosing a hotel and no way to reach another one, so an agent scoped to one property cannot read across a group.
Turning it off#
An Owner can disable a single job or stop the job surface entirely from the registry. A disabled job then answers exactly as if it had never existed; a stopped surface answers job_surface_disabled. Neither corrupts a handle that already exists.
What's next#
- The playground — submit a job from inside the product before wiring an agent up to it.
- API keys — the credential this all hangs off.
- Connect an agent — the same ground as a walkthrough.
- Open Hotel Protocols — why the agent surface and the human surface are the same surface.