night-margin-mcp
An MCP server that shows every pricing decision for a short-term rental portfolio net of turnover costs, focusing on net per available night to protect profit margins.
README
night-margin-mcp — Margin Engine
Wheelhouse optimizes what guests pay. night-margin protects what you keep.
Built by a property manager who runs 73 listings in Lapland — for operators like us.
An MCP server that shows every pricing decision for a short-term rental portfolio net of turnover costs — the margin guard for Wheelhouse pricing, the profit layer underneath its revenue optimization (ADR, RevPAR). The key metric: net per available night.
Quick start
claude mcp add margin -- npx -y night-margin-mcp
That's it — works with zero config on synthetic demo data. Open Claude and ask:
Where is my portfolio leaking money in June 2026?
You'll get the leak figure, the net-per-night metric, and your best and worst properties in under a minute. When you're ready to run the analysis against your own Wheelhouse portfolio, see Wheelhouse API key below.
On live data the first call is slower: one request per listing, serial, to stay inside Wheelhouse's 60 req/min limit — 3 listings ≈ 5 s, 73 listings ≈ 90 s. Results are then cached for 10 minutes.
What you get
The first line is the pain, in euros — this is actual zero-config output (June 2026, demo data):
€132 is leaking from 8 bookings that don't cover their own turnover cost. (4.0% of booked nights are net-negative) Net per available night: €74.7 Occupancy 72.9% (175 booked, 65 gap nights) Gross €22,821 − turnover costs €4,900 = net €17,921
| Property | Net/night | Booked nights | Gap nights | Net € |
|---|---|---|---|---|
| demo-1br-03 | €21.4 | 15 | 15 | €643 |
| demo-1br-01 | €32 | 18 | 12 | €960 |
| demo-3br-08 | €178.1 | 30 | 0 | €5,344 |
Same math, your numbers: set WHEELHOUSE_API_KEY and the analysis runs on your live portfolio.
Live demo
Want to see the output before installing anything? https://mikki321.github.io/night-margin-mcp/ — portfolio KPIs, a per-property heatmap of net per available night, and a triage table of net-negative bookings, generated by this server from the synthetic demo portfolio that ships with the package.
Example prompts
The server provides ten tools: three read-only analysis tools, a four-tool decision loop (see The decision loop), a proactive alerts tool (see Proactive alerts), review_history — a read-only mirror of your own past months net of estimated turnover cost (a description of what happened, never a forecast) — and margin_copilot, which ranks the below-floor gap nights into money-moves, weighs the options for each and marks the recommended one (read-only; staging routes through propose_decisions → apply_decision). No dates needed — analyze_portfolio and compare_strategies default to the last 30 + next 90 days (recent turnovers + your booking horizon), propose_decisions to the next 30 days; pass from/to to change. Try these directly in Claude:
-
analyze_portfolio— where is the money leaking?Where is my portfolio leaking money?
-
compare_strategies— gross vs. net in the same picture:Compare two strategies for June 2026: (A) fill gap nights at a 40% discount and (B) raise the minimum stay to three nights with a 10% price increase. Which one delivers the better net per night?
-
gap_night_check— is a single gap night worth filling?Property demo-1br-01 has a gap night on 2026-06-23. What's the floor price, and is the night worth filling?
-
propose_decisions— which upcoming nights are at risk of selling below cost?Which gap nights are about to sell below my cost floor?
-
apply_decision— act on a proposal:Apply decision d3
Without explicit confirmation this is a dry run: it prints the exact rate payload that would be written and changes nothing. Prices change only when you confirm the write, and every applied decision can be undone with
revert_decision. -
Risk presets —
propose_decisionsandgap_night_checkboth take an optionalriskparameter (conservative|recommended|aggressive, defaultrecommended) that scales the floor's minimum margin — Wheelhouse's own CON/REC/AGG language:What should I do this week? Use the aggressive risk preset.
-
check_alerts— proactive check for gap-floor breaches and new bookings (see Proactive alerts):Check for any pricing alerts.
The core message of the demo: filling gaps at a discount can grow revenue while shrinking profit — a 2-night and a 7-night booking consume the same cleaning.
The decision loop
New in 0.3.0 — analysis turns into safe, reversible action:
propose_decisionsfinds upcoming gap nights (next 30 days by default) where the current price recommendation sits below your cost floor (turnover + travel +MIN_MARGIN, scaled by an optionalriskpreset — see Example prompts) and proposes fixing those nights at the floor so they can't sell below cost. Proposals go into a local decision log — proposing never touches prices.apply_decisionwrites one proposal to Wheelhouse as fixed custom rates — but only with explicitconfirm: true. The default is a dry run showing the exact payload without writing anything. Before the first real write, the prior custom rates are snapshotted to the decision log, sorevert_decisioncan always delete what was written and restore what was there before.set_targetstores a monthly gross revenue target per property;analyze_portfoliothen reports progress toward any targets whose month overlaps the analysis window.
The safety model in one line: the analysis tools (analyze_portfolio, compare_strategies, gap_night_check) and propose_decisions never change any prices; writes happen only through apply_decision/revert_decision with an explicit confirm: true, always target the listing's own channel, and every write is revertible. Without a WHEELHOUSE_API_KEY you can run the whole loop up to the dry run on demo data — real writes require the key and a fresh propose against your own portfolio. The decision log and targets live locally in NM_STATE_DIR (default ~/.night-margin).
Proactive alerts (watch mode)
check_alerts is read-only and checks two things: (1) upcoming gap nights priced below your cost floor (same data as propose_decisions, always the recommended risk preset, but this tool never saves proposals to the decision log), and (2) new bookings since the last check, each shown net of turnover costs — the same twist as the rest of the server, now in a booking notification. The first run establishes a baseline (records existing bookings without alerting on them); later runs alert on bookings that were not present in the previous check. The Wheelhouse reservation payload carries no creation timestamp, so a booking made earlier will surface the first time its dates enter the 120-day horizon — "new to this check", not "booked since yesterday".
By default check_alerts is silent — it just returns the report as text, same as every other tool. To get pushed notifications, configure a channel:
| Variable | Description |
|---|---|
NM_TELEGRAM_BOT_TOKEN / NM_TELEGRAM_CHAT_ID |
Send alerts via a Telegram bot (checked first). |
NM_WEBHOOK_URL |
Send alerts to a generic incoming webhook (Slack-compatible {text} JSON body) — used only if Telegram isn't configured. |
Neither is required — without either, check_alerts still works and simply says no channel is configured. Notifications are off unless you set one of these — nothing is ever sent by default, and the token/URL are never logged or echoed back in error messages.
For unattended checks (e.g. a daily cron job), run the server with --watch: it runs check_alerts once (sending a notification if a channel is configured), prints the report to stdout, and exits — no MCP server, no stdio transport:
0 8 * * * WHEELHOUSE_API_KEY=... NM_TELEGRAM_BOT_TOKEN=... NM_TELEGRAM_CHAT_ID=... npx -y night-margin-mcp --watch
Configuration (env)
Every setting is optional — without them, the server runs the manual cost model on synthetic demo data. Environment variables are passed via claude mcp add's -e flags:
claude mcp add margin -e WHEELHOUSE_API_KEY=xxx -e COST_SOURCE=csv -e CSV_PATH=/path/to/costs.csv -- npx -y night-margin-mcp
| Variable | Default | Description |
|---|---|---|
WHEELHOUSE_API_KEY |
– | Your own Wheelhouse integration key. When set, bookings are fetched from the Wheelhouse RM API; without it, synthetic demo data is used. |
WHEELHOUSE_CHANNEL |
– | Optional override for the channel parameter on Wheelhouse read calls (reservations, price recommendations). By default each listing's own channel field is used, which the API accepts. Writes (apply_decision) always go to the listing's own channel — this override never affects writes. |
COST_SOURCE |
manual |
Source of turnover costs: manual | csv | cleanhub. |
AVG_TURNOVER_COST |
70 |
Manual mode: € per turnover (cleaning). Also serves as the fallback in CSV mode when a booking has no cost row. |
COST_TIERS |
– | Optional tiering, e.g. 1br:55,2br:70,3br:95 — matched against the property's property_id by substring; everything else gets AVG_TURNOVER_COST. |
CSV_PATH |
– | CSV mode: path to the cost CSV (required when COST_SOURCE=csv). See CSV mode. |
CLEANHUB_API_URL / CLEANHUB_TOKEN |
– | CleanHub mode: actual turnover costs from the CleanHub field-operations platform over HTTP (both required when COST_SOURCE=cleanhub). |
MIN_MARGIN |
25 |
Minimum margin € for the gap-night floor: floor = turnover + travel + MIN_MARGIN (scaled by the risk preset — see Example prompts). Used in gap_night_check's fill/skip verdict and in propose_decisions'/check_alerts' floor proposals. |
NM_STATE_DIR |
~/.night-margin |
Local state directory for the decision log, monthly targets, and the seen-bookings set (used by propose_decisions, apply_decision, revert_decision, set_target, check_alerts). Nothing is ever stored in the package directory. |
NM_TELEGRAM_BOT_TOKEN / NM_TELEGRAM_CHAT_ID |
– | Optional: send check_alerts notifications via a Telegram bot. See Proactive alerts. |
NM_WEBHOOK_URL |
– | Optional: send check_alerts notifications to a Slack-compatible incoming webhook (used only if Telegram isn't configured). See Proactive alerts. |
CSV mode
If you know your actual turnover costs per booking, a CSV gives you the most accurate analysis. Schema (header row required):
reservation_id,property_id,checkin,checkout,nights,gross_revenue,cleaning_cost,travel_cost,laundry_cost,turnover_date,is_sunday_or_holiday
demo-1br-01-r1,demo-1br-01,2026-06-03,2026-06-10,7,571,56,16,9,2026-06-10,false
A synthetic sample file (887 rows, covering the full 2026 calendar year of the demo data) ships with the package at examples/sample-costs.csv, and can be downloaded directly:
https://raw.githubusercontent.com/mikki321/night-margin-mcp/main/examples/sample-costs.csv
Usage:
curl -o costs.csv "https://raw.githubusercontent.com/mikki321/night-margin-mcp/main/examples/sample-costs.csv"
claude mcp add margin -e COST_SOURCE=csv -e CSV_PATH="$PWD/costs.csv" -- npx -y night-margin-mcp
Cost rows are matched to bookings primarily by reservation_id, and secondarily by the composite key property_id|checkin|checkout. The tool response reports the attribution quality — e.g. "Cost attribution: 289/289 bookings matched by reservation_id", or with mixed sources "Cost attribution: 41 by reservation_id, 6 by composite key, 3 by average fallback (50 total)". If a row is missing and no AVG_TURNOVER_COST fallback is set, you get a clear error listing the affected bookings.
Wheelhouse API key
Without a key, the server runs on synthetic demo data (and says so in every response). With a key, you get your real bookings from the Wheelhouse RM API: the server fetches your listings and reservations, and every metric is computed from your own portfolio.
- The key is your own Wheelhouse integration key (the API's
X-Integration-Api-Keyheader). Get it from your Wheelhouse account's integration settings or from Wheelhouse support — API documentation: https://api.usewheelhouse.com/wheelhouse_rm_api. - Pass it as an environment variable:
claude mcp add margin -e WHEELHOUSE_API_KEY=xxx -- npx -y night-margin-mcp. - Never commit the key to git — not in a
.envfile in the repo, not in README examples, not in commits. The key travels only as an environment variable.
If the key is invalid or the network flakes, the server returns a clear error with instructions — it does not silently fall back to demo data, so you'll never mistake mock numbers for your own.
For developers
Local development:
npm install
npm test
npm run build
claude mcp add margin -- node "<repo path>/dist/index.js"
If the npm package doesn't work for any reason, the same server installs directly from GitHub:
claude mcp add margin -- npx -y github:mikki321/night-margin-mcp
Exploring the Wheelhouse API with curl
Use these commands to verify auth and inspect response shapes (redact guest names/emails before sharing; field names and formats are preserved):
# 1) Listings — confirms auth + provides listing_id and channel
curl -s -H "X-Integration-Api-Key: $WHEELHOUSE_API_KEY" \
"https://api.usewheelhouse.com/ss_api/v1/listings?per_page=5&page=1" | head -c 4000
# 2) Reservations — THE blocking one (full JSON for 2–3 reservations)
curl -s -H "X-Integration-Api-Key: $WHEELHOUSE_API_KEY" \
"https://api.usewheelhouse.com/ss_api/v1/listings/<LISTING_ID>/reservations?channel=<CHANNEL>" | head -c 6000
# 3) Price recommendations — for gap_night_check, lower priority
curl -s -H "X-Integration-Api-Key: $WHEELHOUSE_API_KEY" \
"https://api.usewheelhouse.com/ss_api/v1/listings/<LISTING_ID>/price_recommendations?channel=<CHANNEL>" | head -c 4000
The rate limit is 60 requests/min — the server runs requests serially and backs off on 429 responses (2 s / 4 s / 8 s).
Core math
net_per_night = (gross − cleaning − laundry − travel) / available nights
available = booked nights + gap nights in the analysis window
gap_floor = turnover cost + travel + MIN_MARGIN
leak = Σ(€ of net-negative bookings) and % of booked nights
All data shipped with the repo and the package is synthetic — no real customer or property data is included.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.