Salta ai contenuti

@guestcraft/mcp

Questi contenuti non sono ancora disponibili nella tua lingua.

A Model Context Protocol server for a GuestCraft instance. It gives an AI assistant GuestCraft’s public operations as MCP tools and resources, plus composed front-desk tools, calls them with the caller’s own identity, and runs a destructive tool only once the caller confirmed it. MIT licensed; it is a client of the public API only and imports no other GuestCraft package than @guestcraft/sdk. Every instance also serves it at /api/mcp, composed by @guestcraft/server with OAuth.

This is a private pnpm workspace package, version 0.0.0, not published to npm. Its entry points are TypeScript sources that Node.js 24 runs directly. Setup for Claude Desktop, Claude Code, Cursor, VS Code and the MCP Inspector is in the MCP guide.

stdio (the default): one process per assistant, acting with one credential read from the environment.

Terminal window
GUESTCRAFT_BASE_URL=https://hotel.example.com \
GUESTCRAFT_API_KEY=gc_live_… \
node packages/mcp/src/main.ts
Variable Meaning
GUESTCRAFT_BASE_URL The instance root, such as https://hotel.example.com, without /api/v1. Required.
GUESTCRAFT_API_KEY A workspace API key (gc_live_… or gc_test_…).
GUESTCRAFT_ORGANIZATION_TOKEN An organization token (gc_org_…). Exactly one of the two credentials is required.
GUESTCRAFT_WORKSPACE_ID Optional default workspace (ws_…), for an organization token that reaches several.

Streamable HTTP: node packages/mcp/src/main.ts --http [--host 127.0.0.1] [--port 3100] [--allow-origin <origin>]... serves one stateless endpoint at /mcp (MCP protocol revision 2026-07-28, with the stateless fallback for 2025 clients). It holds no credential of its own and refuses to start when one of the credential variables is set: each request brings its caller’s credential as Authorization: Bearer gc_live_… (or gc_test_…, gc_org_…), and optionally its default workspace as GuestCraft-Workspace-Id. A missing or malformed credential answers 401 with WWW-Authenticate: Bearer; the instance checks the credential itself on every call. It binds to 127.0.0.1 unless --host says otherwise; put it behind TLS before exposing it. A request whose Origin header is not one of the --allow-origin values (none by default) answers 403, which protects a local server from DNS rebinding; an allowed origin gets CORS answers (its preflight without a credential, WWW-Authenticate readable, never cookies).

--help prints the usage. Wrong settings exit with status 2 and say what to change.

import { createGuestCraftMcpHandler, createGuestCraftMcpServers } from "@guestcraft/mcp";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
// stdio, or any transport of the MCP SDK: one caller's servers.
serveStdio(createGuestCraftMcpServers({ baseUrl, apiKey, workspaceId }));
// Streamable HTTP in any runtime with web-standard Request and Response.
const handler = createGuestCraftMcpHandler({ baseUrl, allowedOrigins: ["https://app.example.com"] });
const response = await handler.fetch(request);

Both take an optional fetch for every request to the instance, confirmations (below) and callTimeoutMs, the bound of one tool call. The handler also takes authenticate, which decides who each request is before anything else runs (the default reads an API key or organization token from the bearer): it answers the caller’s connection, either a credential or a fetch that carries an already verified identity in process, and a binding that identifies the caller for confirmations, and for an OAuth access token the permissions its scopes name (scopes). A refused request gets 401 with resource_metadata when resourceMetadataUrl is set, or 429. A caller with scopes whose tool call or resource read needs a permission they lack is answered 403 with WWW-Authenticate: Bearer error="insufficient_scope", scope="…" before the call runs (src/scopes.ts): the scope names the permissions of the operation, one of which is enough, and for a composed tool those of the operations it always calls. The options also take now, the clock of the readiness tools’ dates. toolCatalog(document) is the pure conversion of an OpenAPI document (parseOpenApiDocument(json)) into the tools below.

The server reads the instance’s own OpenAPI document, GET {baseUrl}/api/v1/openapi.json, once per process (a failed read is retried on the next request), so its tools always match the installed version.

  • Which operations: one tool per operation whose security lists apiKey or organizationToken. Browser-only operations (sessions only, such as API key management), person-only ones (device sessions) and public ones (sign-in, discovery) have no tool.
  • Names: the operationId in snake case: reservationRooms.checkIn becomes reservation_rooms_check_in. The title is the operation’s summary; the description is the summary, the operation’s description (which names the required permission) and its method and path.
  • Arguments: one object with path (path parameters), query (query parameters), body (the JSON request body), if_match (the If-Match version), idempotency_key (Idempotency-Key) and workspace_id (GuestCraft-Workspace-Id, overriding the default workspace), each present when the operation takes it and required when the operation requires it. Referenced component schemas are copied into the schema’s $defs, only those it reaches. Arguments are validated against the schema before the call; the API validates them again.
  • Permissions: each tool keeps the permissions its operation declares in x-guestcraft-permissions, of which every call needs one; the scope check of OAuth tokens uses them. An instance before API revision 6.12.0 declares none, and nothing is checked before the call.
  • Answers: a JSON answer is the tool result’s structuredContent (a list or null is wrapped as { "data": … }) and a JSON text block; a CSV report is its text. An API problem is a result with isError: true and the problem body: status, code, detail, field errors and request_id. It never carries the credential, a request header or the request body.
  • Resources: every GET tool without a required query parameter is also a resource, guestcraft://v1/<path>, a template when the path has parameters (guestcraft://v1/properties/{id}). Reads use the server’s credential and default workspace.

Every operation of the API declares what it does to the instance’s state, as x-guestcraft-effect in the OpenAPI document: read, create (something new, nothing else changed), update (a change or replacement of what exists), transition (a check-in, a cancellation, a revocation, opening a drawer, …) or delete. The tool annotations follow it (annotationsOf in src/catalog.ts):

Declared effect readOnlyHint destructiveHint idempotentHint
read (also a quote or a simulation sent as POST) true — true
create false false true for PUT, else false
update, transition, delete false true true for PUT and DELETE, else false
none: a mutation of an instance before API revision 6.2.0 false true as above

The description of every write tool starts with “Writes data”, and of a destructive one with “Writes data: a state transition or a deletion, which may not be reversible”. A mutation that declares no effect counts as destructive, and every destructive tool runs only once confirmed.

availability_and_quotes, find_reservation, change_booking, check_in_readiness, check_out_readiness and room_status (src/composed.ts) each call several public operations with the caller’s transport. A composed tool is listed only when the instance’s document has every operation it calls. A refusal of an operation it always calls fails the call; one of a part (a quote, a unit’s status, a balance) is reported in its result. Their inputs are Zod schemas, published as JSON Schema. change_booking is destructive; its preview is GuestCraft’s own dry run (reservationRooms.update with validate_only=true), and a change the dry run refuses is refused at once.

A destructive tool’s first call changes nothing (src/gate.ts): it runs the tool’s preview and issues a challenge, HMAC-SHA256-sealed with the server’s key over the caller’s binding, the tool, a digest of its arguments in canonical JSON (so if_match and every body field count) and a random nonce, valid for ten minutes (src/confirmation.ts).

  • A client that offers form elicitation (its capabilities, per request on revision 2026-07-28, or from the handshake of a 2025 connection) receives an input_required result asking one yes/no question, with the challenge as requestState; the SDK’s requestState.verify hook refuses a forged, expired or foreign state with -32602. On a 2025 stdio connection the SDK’s shim asks the client directly. Accept runs the tool; decline and cancel use the challenge without running.
  • Any other client, including a stateless 2025 HTTP request, receives the challenge as confirmation_token, an optional argument of every destructive tool, which the model sends back with the same arguments.

The ConfirmationLedger port claims each nonce once and records the outcome, identifiers and status only; a retry answers that outcome and never runs again. The key and the ledger must be shared by every process that serves the same callers: @guestcraft/server derives the key from the instance’s secret and keeps the ledger in PostgreSQL. The default, a random key and memoryConfirmationLedger, suits one process, as stdio is.

  • Every call goes to the public API with the caller’s credential (or, in the built-in server, the caller’s principal in process) and no cookie, so the API’s own checks decide: an API key acts in its workspace, with its permissions and properties, narrowed to what its creator still holds; an organization token in the workspaces it reaches; and, in the built-in server, an OAuth application’s access token for the member who authorized it, within the grant they gave and what they still hold. The MCP server holds no other credential and widens nothing. A tool the credential may not use answers 403 auth.forbidden as an error result.
  • Over HTTP, each request is authenticated first and served by a fresh server with that caller only; the shared parts are the public OpenAPI document, the argument validator and the confirmation key and ledger. Nothing of a caller outlives its request.
  • The environment is read only by the command (src/main.ts); the library takes options.
Terminal window
pnpm --filter @guestcraft/mcp typecheck
pnpm --filter @guestcraft/mcp test
pnpm exec vitest run --project server-integration packages/server/test/integration/mcp.integration.test.ts
pnpm exec vitest run --project server-integration packages/server/test/integration/mcp-endpoint.integration.test.ts

The unit tests drive the server with the real MCP client in process (as over stdio, and over HTTP through handler.fetch and a loopback socket), in both protocol eras, against a fake instance behind an injected fetch: the confirmation by token and by elicitation, and the refusal of forged, foreign, stale and replayed confirmations; the composed tools against the repository’s document. A contract test checks that every operation of openapi/v1.json maps to exactly one tool or is excluded because no integration credential may call it, and that exactly the updates, transitions and deletions need a confirmation. The server’s integration tests drive this package against the composed HTTP runtime on PostgreSQL, through stdio and through the built-in /api/mcp endpoint.