Skip to content

Repository files navigation

hospitable-mcp

Multi-tenant hosted MCP server that exposes the Hospitable Public API v2 as MCP tools. Runs at https://hospitable.nlma.io. Each user authenticates with their own Hospitable Personal Access Token (PAT).

Tools

Tool Endpoint
get_user GET /user
list_properties GET /properties (supports all: true to fetch every page)
get_property GET /properties/{uuid}
search_properties GET /properties/search (Direct site required)
get_calendar GET /properties/{uuid}/calendar
update_calendar PUT /properties/{uuid}/calendar (max 60 dates/call). Smart Rates properties return 422 with actionable guidance — see get_smart_rates_config and update_property instead.
list_reservations GET /reservations (requires properties[]; supports all: true)
get_reservation GET /reservations/{uuid} (supports include=financials,financialsV2,guest,properties,listings,review)
list_messages GET /reservations/{uuid}/messages
send_message POST /reservations/{uuid}/messages
list_property_reviews GET /properties/{uuid}/reviews (supports all: true)
respond_to_review POST /reviews/{uuid}/respond
list_transactions GET /transactions (undocumented)
get_transaction GET /transactions/{uuid}
list_payouts GET /payouts (undocumented — cleaner shape)
get_payout GET /payouts/{uuid}
list_listings flatten include=listings from properties
revenue_summary client-side aggregator over reservations.financialsV2
hospitable_request arbitrary call — escape hatch

Webhook-subscription endpoints are not exposed: the Hospitable v2 Public API does not provide them. Reservation financials are available via the include=financials / include=financialsV2 parameter on list_reservations / get_reservation. Webhook delivery is configured out-of-band through Hospitable support.

Internal-API tools (optional)

Hospitable's private web-app API at https://api.hospitable.com/v1 exposes owner-management accounting data that the documented Public API v2 doesn't: owner statements, reservation commissions, commission agreements, owners, profit-and-loss reports. These are only enabled on this server when the operator configures a JWT (see .env.example). They operate on the JWT owner's Hospitable account, not per-tenant, so they should be gated to one tenant via HOSPITABLE_INTERNAL_OPERATOR_TENANT_HASH on multi-user deployments.

Tool Endpoint / purpose
internal_whoami Decoded JWT payload + live /v1/user/permissions — shows days until JWT expiry
list_statements POST /v1/operations/v2/statements?status={draft|in_review|published|paid}
get_statement GET /v1/operations/v2/statements/{id} — full line items + summary
statement_commission_summary Synthesis: walks every statement, filters to a month, sums Reservation commissions to manager per property. Direct answer to "what did manager commissions total for ".
list_commission_agreements GET /v1/commissions
get_commission_agreement GET /v1/commissions/{id}
get_commission_agreement_properties GET /v1/commissions/{id}/properties
list_owners GET /v1/owners
get_owner GET /v1/owners/{id}
update_property Safe PUT /v1/properties/{uuid} — does GET-then-PUT and replays critical array fields (tags, amenities, room_details, photos, listings, ical_imports, languages, types) so a partial update never silently wipes them. Always use this instead of hospitable_internal_request PUT /properties/{uuid} for property metadata edits.
get_smart_rates_config GET /v1/properties/{uuid}/pricing — read Smart Rates dynamic-pricing config (dynamic_pricing_enabled, base_prices, last_minute_sensitivity curve, gap_night_rules, etc.)
run_profit_and_loss POST /v1/profit-and-loss
list_businesses GET /v1/businesses
list_teams GET /v1/teams/
hospitable_internal_request arbitrary call — escape hatch for the internal API

JWT capture & rotation: Log into https://my.hospitable.com in a browser, open DevTools → Application → Local Storage → my.hospitable.com, copy the accessToken value, paste into the server's HOSPITABLE_INTERNAL_JWT env var, restart. The JWT is valid for ~28 days. internal_whoami reports days_until_expiry so you can schedule refreshes.

These endpoints are undocumented and may drift without notice. They also have no public rate-limit guarantees; treat them as best-effort.

Authentication

Two flows are supported on the same /mcp endpoint:

  1. Raw PAT bearer (Claude Code)
    claude mcp add hospitable -t http https://hospitable.nlma.io/mcp \
      --header "Authorization: Bearer <your-hospitable-PAT>"
  2. MCP OAuth wrap (claude.ai) — point claude.ai at https://hospitable.nlma.io/mcp. You'll be redirected to a login page, paste your PAT, and claude.ai receives a server-minted bearer token that maps back to your PAT in memory. Access tokens live 1 hour; refresh tokens live 30 days.

Generate a PAT at my.hospitable.com/settings/api. Some tools (send_message, respond_to_review) need extra scopes (message:write, reviews:write) which you must request from team-platform@hospitable.com.

Only sha256(pat + API_KEY_HASH_SALT) is persisted (as the tenant identifier). PATs themselves are held only in process memory. Every successful or failed tool call is recorded in audit_log keyed by that tenant hash.

Local development

npm install
cp .env.example .env            # set DATABASE_URL and API_KEY_HASH_SALT
npm run build
npm start                       # listens on :3004
curl http://localhost:3004/health

Point Claude Code at the local server:

claude mcp add hospitable-local -t http http://localhost:3004/mcp \
  --header "Authorization: Bearer <your-hospitable-PAT>"

Required env:

Var Notes
DATABASE_URL Postgres connection string
API_KEY_HASH_SALT Random 32+ char string; used when hashing PATs for tenant IDs
BASE_URL Public URL, e.g. https://hospitable.nlma.io (defaults to http://localhost:$PORT)
PORT default 3004

Docker

cp .env.example .env   # set DATABASE_URL/PGHOST and API_KEY_HASH_SALT
docker compose up -d --build
curl http://localhost:3004/health

The container listens on HOST:PORT (default 0.0.0.0:3004) and runs on Docker's default bridge network with the port published — no network_mode: host needed. Point DATABASE_URL (or the discrete PGHOST/PGPORT/PGUSER/PGPASSWORD/PGDATABASE vars) at a Postgres instance reachable from inside the container: localhost in DATABASE_URL resolves to the container itself on a bridge network, not the Docker host, so use an external/managed Postgres or the host's LAN/gateway address.

Deployment (Hostinger VPS)

The server runs as a plain Node process under systemd on the Hostinger VPS at 178.16.141.166, alongside the other *.nlma.io MCP servers. No Docker.

Push an update:

bash scripts/push-to-vps.sh

This tars the working tree, ships it to root@178.16.141.166:/opt/hospitable-mcp/, runs npm ci && npm run build, restarts the systemd unit, and hits /health.

Server layout:

  • Source: /opt/hospitable-mcp/
  • Env: /opt/hospitable-mcp/.env (chmod 600 — contains DATABASE_URL, API_KEY_HASH_SALT)
  • systemd unit: /etc/systemd/system/hospitable-mcp.service
  • nginx site: /etc/nginx/sites-available/hospitable.nlma.io
  • TLS: /etc/letsencrypt/live/hospitable.nlma.io/ (certbot auto-renew)
  • Listens on 127.0.0.1:3004 (nginx passthrough from :443)
  • Postgres: local instance, DB hospitable_mcp, role hospitable_mcp

Tail logs:

ssh root@178.16.141.166 'journalctl -u hospitable-mcp -f'

Rotate a tenant's PAT: Tenants are identified by sha256(pat + API_KEY_HASH_SALT) — when a user rotates their PAT on Hospitable's side, the next call with the new token creates a new tenants row automatically. No server-side action needed. To reclaim disk from stale rows, delete directly:

ssh root@178.16.141.166 \
  "sudo -u postgres psql hospitable_mcp -c 'DELETE FROM tenants WHERE last_active_at < NOW() - INTERVAL ''90 days'';'"

Restart:

ssh root@178.16.141.166 'systemctl restart hospitable-mcp'

Architecture

  • src/http.ts — Express app, Streamable HTTP MCP transport, OAuth routes.
  • src/auth.ts — bearer middleware; accepts raw PATs or OAuth opaque tokens and resolves to a TenantContext.
  • src/oauth.ts — HTML login form + MCP OAuth provider (in-memory token store).
  • src/hospitable/client.ts — fetch wrapper with per-minute throttle, 429/503 retry, 30s timeout, and per-endpoint methods. paginate() helper walks all pages for list endpoints.
  • src/tools/register.ts — registers every MCP tool and validates inputs with Zod.
  • src/tools/_shared.tsrunTool() wraps every handler with error formatting and writes to audit_log.
  • src/server.tscreateMcpServer(ctx) — instantiated per MCP session so ctx.pat is closed over and never shared between tenants.

License

Copyright © 2026 Next Level Management Advisors, LLC.

Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0) — see LICENSE. If you run a modified version over a network, the AGPL requires you to make your modified source available to its users.

Commercial licensing: to use this in a closed-source or commercial product, or to host a modified version without publishing your source, a commercial license is available — contact forrest@nlma.io.

About

Multi-tenant hosted MCP server exposing the Hospitable Public API v2

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages