MCP server
GuestCraft lets an AI assistant work with an instance through the Model Context Protocol. Every public operation that an integration may call is an MCP tool, generated from the instance’s own OpenAPI document, and a few composed tools cover common front-desk tasks. The assistant acts with the credential you give it, so it can do only what that credential allows, and every destructive tool asks for a confirmation before it changes anything. How tools are generated, named and marked is described in the package README.
There are two ways to run it:
- Built into every instance, at
https://<your instance>/api/mcp, on Vercel and Docker alike. Nothing to install: give your MCP client the URL. It signs in with OAuth, or takes an API key or organization token. - From a checkout of this repository, over stdio or as a local Streamable HTTP endpoint, with an API key or organization token (Running the package yourself). The package is not published to npm.
The built-in server
Section titled “The built-in server”The endpoint speaks Streamable HTTP, protocol revision 2026-07-28, and serves 2025-era clients
statelessly as well. It keeps no session: each request is authenticated on its own, which is why
it runs on serverless hosts. The examples use https://hotel.example.com for your instance.
Sign in with OAuth
Section titled “Sign in with OAuth”A client that supports MCP authorization needs the URL only; never put a credential in its configuration.
- Its first request is answered
401withWWW-Authenticate: Bearer resource_metadata="https://hotel.example.com/.well-known/oauth-protected-resource/api/mcp". - The client reads that protected-resource metadata (RFC 9728), finds the instance’s authorization server, registers itself (dynamic client registration) and opens the consent page in your browser.
- You sign in, choose the workspace, the properties and the permissions the assistant gets, and approve. The application never receives more than you hold yourself.
- The client receives a token for
https://hotel.example.com/api/mcponly: it is refused at the REST API and at any other instance. Revoking the application in the workspace’s settings ends its access at the next request. - A tool call or resource read that needs a permission the token’s scopes do not name is
answered
403before it runs, withWWW-Authenticate: Bearer error="insufficient_scope", scope="rates.read", resource_metadata="…"(RFC 6750). The scope lists the permissions the operation takes, one of which is enough. A client that supports step-up authorization asks for those scopes with the ones it has, the consent page opens again, and the call is retried once you approve.
Client by client:
| Client | Add the server | Then |
|---|---|---|
| Claude Code | claude mcp add --transport http guestcraft https://hotel.example.com/api/mcp |
Run /mcp inside Claude Code and choose Authenticate. |
| Claude Desktop | Settings → Connectors → Add custom connector, with the URL. | Connect opens the sign-in. |
| Cursor | In ~/.cursor/mcp.json: { "mcpServers": { "guestcraft": { "url": "https://hotel.example.com/api/mcp" } } } |
Cursor shows Needs login next to the server: select it. |
| VS Code | In .vscode/mcp.json: { "servers": { "guestcraft": { "type": "http", "url": "https://hotel.example.com/api/mcp" } } } |
Start the server and allow the sign-in. |
| MCP Inspector | Transport Streamable HTTP, the URL, Connect. | The Inspector runs the OAuth flow in its browser tab. |
With an API key or organization token
Section titled “With an API key or organization token”A client that sends headers can use an integration’s credential instead: in Settings → API keys,
create a key with only the permissions the assistant needs, then add
Authorization: Bearer gc_live_… to the server’s entry, for example
claude mcp add --transport http guestcraft https://hotel.example.com/api/mcp --header "Authorization: Bearer gc_live_…".
With an organization token, add GuestCraft-Workspace-Id: ws_… to choose the default workspace;
a tool’s workspace_id argument chooses another one for a single call. In a shared project file,
refer to the secret through the client’s variables instead of writing it.
Client sessions (gc_cst_…) and the mobile app’s device tokens are refused: they are not an
integration’s delegation for this endpoint.
How it acts
Section titled “How it acts”The server authenticates each request with the instance’s own credential gateway and calls the API in the same process with your principal. It never forwards your token to the REST API and never uses a credential of its own, so the API’s permission checks, property scopes, the live ceiling of the person behind the credential and the audit log apply to every tool call exactly as to your own requests: the audit entry names your API key, organization token or application.
Each MCP request is admitted once against your credential’s budget, like any other request.
The API calls a tool then makes in process are not admitted again: a composed tool such as
availability_and_quotes makes one call per room type within that single admission.
Web pages may call the endpoint only from the instance’s own origin and the origins in
GUESTCRAFT_TRUSTED_ORIGINS: those get CORS answers, without cookies; any other origin is refused.
Native clients, which send no Origin, are not affected.
- One tool per operation that an API key or organization token may call, named after its
operationId (
reservation_rooms_check_in), with its summary, description, required permission and arguments. The operation’s permissions are also in itsx-guestcraft-permissionsin the OpenAPI document: every call needs one of them at least. Reads without required query parameters are also resources (guestcraft://v1/…). - Composed tools, each over several public operations:
| Tool | What it does | Changes data |
|---|---|---|
availability_and_quotes |
Free rooms per room type and night of a stay, and each rate plan’s price, with the reasons a plan is not bookable. | No |
find_reservation |
Reservations by confirmation code or guest name, arrival or departure date, or a night in house. | No |
change_booking |
One booked room’s dates, room type, unit or number of guests; new dates are priced with the room’s rate plan. | Yes, after confirmation |
check_in_readiness |
Whether each room can be checked in now, or why not: unconfirmed, not expected, the night audit not run for an arrival that has come (night_audit.required), a business date outside the stay (check_in.outside_stay), no unit for tonight, a dirty unit. Room blocks and a required inspection are not checked. |
No |
check_out_readiness |
What checking out each in-house room would leave on the folio, and whether it must be settled, or the night audit run (night_audit.required), first. |
No |
room_status |
The housekeeping status of the rooms, with a count per status. | No |
Write tools say “Writes data” at the start of their description and carry
readOnlyHint: false. Each tool follows the effect its operation declares in the API
(x-guestcraft-effect): only a purely additive creation is destructiveHint: false; updates and
replacements, state transitions and deletions carry destructiveHint: true. Some clients limit how
many tools they enable at once: enable only those you need in the client’s tool list.
Confirmation of destructive tools
Section titled “Confirmation of destructive tools”Every tool whose operation updates, transitions or deletes (a cancellation, a check-in, a payment,
a booking, which takes rooms out of inventory), and change_booking, changes nothing on its first
call. It answers a preview of what it would do: the request it would send, or for
change_booking what GuestCraft’s own dry run says the reservation would become. A change the dry
run refuses is refused at once.
- Clients with form elicitation (they declare
elicitationwithformin their capabilities) show you the preview and ask Make this change. Only yes runs the tool. No, or dismissing the question, leaves everything as it is. - Other clients receive a
confirmation_tokenwith the preview. The assistant must show you the preview and, only if you agree, call the same tool again with the same arguments and that token.
Either way the confirmation is issued by the server, valid for ten minutes, bound to your
credential and workspace choice, the tool and its exact arguments (including the if_match
version), and used once across every instance of the deployment. A retry of a used confirmation
answers what happened the first time (identifiers and status only) and never acts twice. The tool
still runs with your live permissions: a permission removed in the meantime refuses it.
An elicitation answer is your client’s report of what you chose; it is not proof that a person, rather than software, answered. Keep assistants’ credentials to the permissions they need.
Limits
Section titled “Limits”- Each tool call ends within 25 seconds. If a destructive call is cut off, the API may still have completed it: a retry of its confirmation then says the outcome is unknown, and you should read the resource to see whether the change happened.
- Only an OAuth token’s missing scope is an
insufficient_scopechallenge. Everything else a credential may not do answers the API’s403 auth.forbiddenas the tool’s error result: an API key’s or organization token’s permissions, a property outside the grant, and a permission the member no longer holds, which signing in again cannot give back. A composed tool that answers in parts (prices, a unit’s status, a balance) reports a refused part in its result; only the operations it always calls are challenged. - The budgets of credentials apply per instance, as for the REST API.
- Agents read text written by guests, such as messages and notes: treat their suggestions with the care you would give an email from a stranger.
When something fails
Section titled “When something fails”| What you see | Why |
|---|---|
HTTP 401 with WWW-Authenticate: Bearer … resource_metadata="…" |
No credential, or one the endpoint does not accept (expired, revoked, a client session or device token, a token for another resource). OAuth clients sign in again. |
HTTP 403 before any tool runs |
The request’s Origin is not allowed. |
HTTP 403 with WWW-Authenticate: Bearer error="insufficient_scope", scope="…" |
The OAuth token’s scopes do not name a permission the call needs: approve the scopes listed, one of which is enough, and retry. |
HTTP 429 |
The credential’s budget is spent: retry after the Retry-After delay. |
A result with confirmation_required |
Nothing changed yet: confirm and call again with the token. |
A result with confirmation: "refused" |
The token or answer was for other arguments, another tool or caller, or expired: call again without it. |
A tool result with isError and auth.forbidden |
The credential, or the person behind it, lacks the permission or property; the tool’s description names the permission. |
A version error (412) |
Read the resource again and pass its current version as if_match. |
Running the package yourself
Section titled “Running the package yourself”The same server runs from a checkout of this repository, for local development or an instance without the built-in endpoint. It authenticates with API keys and organization tokens only.
Before you start
Section titled “Before you start”-
A credential. In Settings → API keys, a member with
api_keys.managecreates a key with only the permissions the assistant needs, for examplereservations.readfor questions about bookings. Its secret (gc_live_…, orgc_test_…in a sandbox workspace) is shown once. Try a sandbox workspace first. See API keys and webhooks. -
The server. Node.js 24 and a checkout with its dependencies installed:
Terminal window git clone <your GuestCraft repository> guestcraft && cd guestcraftpnpm installnode packages/mcp/src/main.ts --helpThe examples below use
/path/to/guestcraftfor that checkout andhttps://hotel.example.comfor your instance. Replacegc_live_…with your key; never commit it.
Over stdio the process serves one assistant and keeps its confirmations in memory; a confirmation token works only with the process that issued it.
Claude Desktop
Section titled “Claude Desktop”Add the server to claude_desktop_config.json (Settings → Developer → Edit Config), then restart
Claude Desktop:
{ "mcpServers": { "guestcraft": { "command": "node", "args": ["/path/to/guestcraft/packages/mcp/src/main.ts"], "env": { "GUESTCRAFT_BASE_URL": "https://hotel.example.com", "GUESTCRAFT_API_KEY": "gc_live_…" } } }}If Claude Desktop does not find node, give its absolute path as command. With an organization
token, set GUESTCRAFT_ORGANIZATION_TOKEN instead of GUESTCRAFT_API_KEY, and
GUESTCRAFT_WORKSPACE_ID to the default workspace (ws_…); a tool’s workspace_id argument
chooses another one for a single call.
Claude Code
Section titled “Claude Code”Over stdio:
claude mcp add guestcraft \ --env GUESTCRAFT_BASE_URL=https://hotel.example.com \ --env GUESTCRAFT_API_KEY=gc_live_… \ -- node /path/to/guestcraft/packages/mcp/src/main.tsOver HTTP, with the server started as in Streamable HTTP:
claude mcp add --transport http guestcraft http://127.0.0.1:3100/mcp \ --header "Authorization: Bearer gc_live_…"Add --header "GuestCraft-Workspace-Id: ws_…" to choose the default workspace of an organization
token. claude mcp list shows the server; /mcp inside Claude Code shows its tools. In a shared
project file (--scope project, .mcp.json), refer to the key as ${GUESTCRAFT_API_KEY} instead
of writing it.
Cursor
Section titled “Cursor”In ~/.cursor/mcp.json, or .cursor/mcp.json of a project:
{ "mcpServers": { "guestcraft": { "command": "node", "args": ["/path/to/guestcraft/packages/mcp/src/main.ts"], "env": { "GUESTCRAFT_BASE_URL": "https://hotel.example.com", "GUESTCRAFT_API_KEY": "gc_live_…" } } }}Over HTTP, the entry is { "url": "http://127.0.0.1:3100/mcp", "headers": { "Authorization": "Bearer gc_live_…" } }.
VS Code
Section titled “VS Code”In .vscode/mcp.json, VS Code asks for the key once and stores it as a secret:
{ "inputs": [ { "type": "promptString", "id": "guestcraft-key", "description": "GuestCraft API key", "password": true } ], "servers": { "guestcraft": { "type": "stdio", "command": "node", "args": ["/path/to/guestcraft/packages/mcp/src/main.ts"], "env": { "GUESTCRAFT_BASE_URL": "https://hotel.example.com", "GUESTCRAFT_API_KEY": "${input:guestcraft-key}" } } }}Over HTTP, the server entry is { "type": "http", "url": "http://127.0.0.1:3100/mcp", "headers": { "Authorization": "Bearer ${input:guestcraft-key}" } }.
MCP Inspector
Section titled “MCP Inspector”The MCP Inspector lists the tools and
resources and calls them by hand; use a sandbox key (gc_test_…):
npx @modelcontextprotocol/inspector \ -e GUESTCRAFT_BASE_URL=https://hotel.example.com \ -e GUESTCRAFT_API_KEY=gc_test_… \ node /path/to/guestcraft/packages/mcp/src/main.tsFor the HTTP endpoint, choose the Streamable HTTP transport, the URL
http://127.0.0.1:3100/mcp and an Authorization: Bearer gc_test_… header, and connect through
the Inspector’s proxy, its default.
Streamable HTTP
Section titled “Streamable HTTP”GUESTCRAFT_BASE_URL=https://hotel.example.com \node /path/to/guestcraft/packages/mcp/src/main.ts --http --port 3100One server serves several people: it holds no credential, and each request acts with the bearer
credential it carries. It listens on 127.0.0.1 by default; another host (--host) needs TLS in
front of it. Requests from web pages are refused unless their origin is allowed with
--allow-origin https://app.example.com, which also gives that origin CORS answers. Its
confirmations live in the process’s memory: a confirmation token works only with the process that
issued it. The command exits with status 2 when a setting is missing or wrong, and says which.