guesty-mcp-server 0.6.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,57 @@
2
2
 
3
3
  All notable changes to the Guesty MCP Server will be documented in this file.
4
4
 
5
+ ## [0.8.1] - 2026-04-19
6
+
7
+ ### Fixed
8
+ - Added `.npmignore` to exclude token files, tests, and non-essential markdown from npm package
9
+ - Added `.mcpregistry_*` patterns to `.gitignore` (credential hygiene)
10
+ - Package size reduced from 42.3kB to 35.0kB (26→23 files)
11
+
12
+ ## [0.8.0] - 2026-04-17
13
+
14
+ ### Changed — Enterprise Tier MVP Merge (Owner-approved option-c path, msg 6406)
15
+ - Enterprise aggregators (`get_property_health`, `submit_checkout_photos`,
16
+ `get_maintenance_alerts`) now layer Guesty-side data (reservation status,
17
+ review score, last-clean timestamp) on top of IoT helpers. Single-call
18
+ snapshots for ops dashboards.
19
+ - IoT-only handlers extracted from `iot-tools.js` to internal async helpers
20
+ (`getIoTPropertyHealth`, `submitIoTCheckoutPhotos`, `getIoTMaintenanceAlerts`);
21
+ canonical MCP tool registration moved to `enterprise-tools.js`.
22
+ - Graceful degradation: Guesty sub-fetch failures degrade to null value +
23
+ per-field error note (aggregator still returns IoT data).
24
+ - `iot-tools.js` retains single MCP registration for `get_readiness_score`.
25
+ - Tool count reconciled across README + license.js + package.json + server.json
26
+ to 43 total (39 Guesty + 1 IoT + 3 Enterprise aggregators). Previous 3-way
27
+ drift (README:38, license.js:38, actual registrations:43) resolved.
28
+
29
+ ### Added
30
+ - `__handlers` export on `enterprise-tools.js` for direct smoke-test invocation
31
+ (renamed from legacy `__stubs` — real handlers, not stubs, post-merge).
32
+ - `tests/test-enterprise.js` rewritten: exports + free-tier-gate + enterprise-lift
33
+ smoke tests. Dynamic import + env-stub so test runs without real Guesty creds.
34
+
35
+ ### Fixed
36
+ - `package.json` test script referenced non-existent `tests/test-tools.js`.
37
+ Now runs `tests/test-enterprise.js && tests/test-iot.js`.
38
+
39
+ ## [0.7.0] - 2026-04-15
40
+
41
+ ### Added
42
+ - **IoT/Property Health Monitoring** (Enterprise tier)
43
+ - `get_property_health` — Real-time device status for any property
44
+ - `submit_checkout_photos` — Photo submission for post-checkout analysis
45
+ - `get_maintenance_alerts` — Active IoT alerts filtered by property/severity
46
+ - `get_readiness_score` — 0-100 Physical Readiness Score with 6 weighted checks
47
+ - **IoT Webhook Receiver** (`POST /webhooks/iot`)
48
+ - Supports Tuya, Google Nest, SmartThings, and generic payloads
49
+ - Auto-normalizes all formats to standard schema
50
+ - Auto-creates alerts for out-of-range readings
51
+ - **IoT Data Layer** (`iot-db.js`)
52
+ - Zero-dependency JSON file store for devices, readings, alerts, baselines
53
+ - Auto-pruning at 50K readings and 10K alerts
54
+ - Tool count: 38 → 42 (4 new Enterprise-tier tools)
55
+
5
56
  ## [0.6.0] - 2026-04-10
6
57
 
7
58
  ### Added
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  The first MCP (Model Context Protocol) server for [Guesty](https://guesty.com) property management. Connect AI agents directly to your Guesty account to manage reservations, communicate with guests, track finances, and update pricing -- all autonomously.
7
7
 
8
- **38 tools** covering reservations, listings, guests, messaging, financials, tasks, calendars, webhooks, pricing, and more.
8
+ **43 tools** covering reservations, listings, guests, messaging, financials, tasks, calendars, webhooks, and pricing — plus **1 IoT tool** (`get_readiness_score`) and **3 Enterprise-tier aggregators** (`get_property_health`, `submit_checkout_photos`, `get_maintenance_alerts`) for property health aggregation, checkout photo intake, and portfolio maintenance alerts.
9
9
 
10
10
  > **Want AI to handle your guest messages 24/7?** [Guesty Copilot](https://guestycopilot.com) -- AI guest management for Guesty hosts, built on this MCP server. Now in beta.
11
11
 
@@ -41,7 +41,7 @@ Or add to your Claude Code settings (`~/.claude/settings.json`):
41
41
  3. Create an API application with `open-api` scope
42
42
  4. Copy your **Client ID** and **Client Secret**
43
43
 
44
- ## All 38 Tools
44
+ ## All 43 Tools
45
45
 
46
46
  ### Reservations & Guests
47
47
  | Tool | Description |
@@ -105,6 +105,15 @@ Or add to your Claude Code settings (`~/.claude/settings.json`):
105
105
  | `get_custom_fields` | Fetch custom fields for listings or reservations |
106
106
  | `get_account_info` | Get account info and subscription details |
107
107
 
108
+ ### Enterprise Tier
109
+ | Tool | Description |
110
+ |------|-------------|
111
+ | `get_property_health` | Aggregate health signal per property: reservation status, open maintenance alerts, review-score, last-clean timestamp, IoT hub status |
112
+ | `submit_checkout_photos` | Accept post-checkout photo uploads and log them to the property's maintenance/cleaning record |
113
+ | `get_maintenance_alerts` | List or filter open maintenance alerts for a property or portfolio |
114
+
115
+ Requires `GUESTY_MCP_LICENSE_KEY` with an Enterprise key (`gmcp_ent_*`). See [pricing](https://guestycopilot.com/pricing).
116
+
108
117
  ## Use Cases
109
118
 
110
119
  - **Guest Communication**: AI agents auto-respond to guest inquiries using real reservation data
@@ -119,6 +128,14 @@ Or add to your Claude Code settings (`~/.claude/settings.json`):
119
128
  - Guesty account with API access (Professional plan or higher)
120
129
  - MCP-compatible AI client (Claude Code, Cursor, Windsurf, etc.)
121
130
 
131
+ ## Environment Variables
132
+
133
+ | Variable | Default | Purpose |
134
+ |----------|---------|---------|
135
+ | `GUESTY_CLIENT_ID` | — | OAuth2 client id (required) |
136
+ | `GUESTY_CLIENT_SECRET` | — | OAuth2 client secret (required) |
137
+ | `IOT_WEBHOOK_PORT` | `3100` | Port for the Enterprise-tier IoT webhook receiver stub (`src/webhook/iot-receiver-server.js`). Local/reverse-proxy only — do not expose publicly. Production requires a reverse proxy that terminates TLS and enforces real HMAC against `IOT_WEBHOOK_SECRET`. |
138
+
122
139
  ## API Reference
123
140
 
124
141
  This server wraps the [Guesty Open API](https://open-api.guesty.com/api-docs). Authentication uses OAuth2 client credentials flow with automatic token caching, retry logic, and rate limit handling.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "guesty-mcp-server",
3
- "version": "0.6.1",
4
- "description": "The first MCP server for Guesty property management. 38 tools for reservations, guests, messaging, pricing, financials, calendars, reviews, tasks, and webhooks.",
3
+ "version": "0.8.1",
4
+ "description": "The first MCP server for Guesty property management. 43 tools (39 Guesty + 1 IoT + 3 Enterprise aggregators) for reservations, guests, messaging, pricing, financials, calendars, reviews, tasks, webhooks, and Enterprise-tier property health / checkout photos / maintenance alerts.",
5
5
  "main": "src/server.js",
6
6
  "bin": {
7
7
  "guesty-mcp-server": "src/server.js",
@@ -10,7 +10,7 @@
10
10
  "type": "module",
11
11
  "scripts": {
12
12
  "start": "node src/server.js",
13
- "test": "node tests/test-tools.js"
13
+ "test": "node tests/test-enterprise.js && node tests/test-iot.js"
14
14
  },
15
15
  "keywords": [
16
16
  "mcp",
package/server.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.DLJRealty/guesty",
4
4
  "title": "Guesty MCP Server",
5
- "description": "MCP server for Guesty property management — 38 tools for STR operations.",
6
- "version": "0.6.0",
5
+ "description": "MCP server for Guesty property management — 43 tools (39 Guesty + 1 IoT + 3 Enterprise aggregators) for STR operations.",
6
+ "version": "0.8.0",
7
7
  "repository": {
8
8
  "url": "https://github.com/DLJRealty/guesty-mcp-server",
9
9
  "source": "github"
@@ -11,7 +11,7 @@
11
11
  "packages": [{
12
12
  "registryType": "npm",
13
13
  "identifier": "guesty-mcp-server",
14
- "version": "0.6.0",
14
+ "version": "0.8.0",
15
15
  "transport": { "type": "stdio" },
16
16
  "environmentVariables": [
17
17
  {
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Guesty MCP Server — Enterprise Tier Aggregation Tools
3
+ *
4
+ * Three Enterprise-tier MCP tools that aggregate IoT device signal with
5
+ * Guesty-side data (reservation status, review score, last-clean timestamp)
6
+ * into single-call snapshots for ops dashboards.
7
+ *
8
+ * Tools:
9
+ * - get_property_health aggregated single-property snapshot
10
+ * (IoT devices + reservation + reviews + clean)
11
+ * - submit_checkout_photos post-checkout photo intake (writes through to
12
+ * the IoT baseline store via iot-db.js)
13
+ * - get_maintenance_alerts active IoT maintenance alerts, filterable
14
+ * per listing or portfolio-wide
15
+ *
16
+ * ── MERGE STATUS (2026-04-17, Owner greenlight msg 6406) ────────────────
17
+ * These tools are now the CANONICAL registrations at these names. The
18
+ * same names previously lived in iot-tools.js as IoT-only views — those
19
+ * have been extracted into plain-async internal helpers and are imported
20
+ * here. This is the Owner-approved option-c merge path: one tool name
21
+ * per MCP surface, Enterprise aggregator wraps the IoT getter.
22
+ *
23
+ * Guesty-side layering (reservation status, review score, last-clean)
24
+ * uses guestyGet() from server.js. Each call is wrapped in try/catch so
25
+ * the aggregator degrades gracefully — if Guesty is slow or fails on one
26
+ * sub-query, the tool still returns IoT data with the failing field set
27
+ * to null + a non-fatal error note.
28
+ */
29
+
30
+ import { z } from "zod";
31
+ import { guestyGet } from "./server.js";
32
+ import {
33
+ enterpriseGated,
34
+ getIoTPropertyHealth,
35
+ submitIoTCheckoutPhotos,
36
+ getIoTMaintenanceAlerts,
37
+ } from "./iot-tools.js";
38
+
39
+ // ─────────────────────────────────────────────────────────────────────────
40
+ // Graceful Guesty fetchers — each returns { value, error } so the aggregator
41
+ // can surface partial data even when one sub-call fails.
42
+ // ─────────────────────────────────────────────────────────────────────────
43
+
44
+ async function safeFetchReservationStatus(listingId) {
45
+ try {
46
+ // Current active / upcoming reservation for this listing
47
+ const data = await guestyGet("/v1/reservations", {
48
+ listingId,
49
+ limit: 1,
50
+ sort: "-checkIn",
51
+ "filters[status]": "confirmed,checked_in",
52
+ });
53
+ const r = (data && data.results && data.results[0]) || null;
54
+ if (!r) return { value: { status: "no_active_reservation" }, error: null };
55
+ return {
56
+ value: {
57
+ reservation_id: r._id,
58
+ status: r.status,
59
+ check_in: r.checkIn,
60
+ check_out: r.checkOut,
61
+ guest: r.guest && r.guest.fullName,
62
+ },
63
+ error: null,
64
+ };
65
+ } catch (e) {
66
+ return { value: null, error: e.message };
67
+ }
68
+ }
69
+
70
+ async function safeFetchReviewScore(listingId) {
71
+ try {
72
+ const data = await guestyGet("/v1/reviews", { listingId, limit: 50 });
73
+ const reviews = (data && data.results) || [];
74
+ if (reviews.length === 0) {
75
+ return { value: { average: null, count: 0 }, error: null };
76
+ }
77
+ const sum = reviews.reduce(
78
+ (acc, rv) => acc + (typeof rv.overallRating === "number" ? rv.overallRating : 0),
79
+ 0
80
+ );
81
+ return {
82
+ value: {
83
+ average: Math.round((sum / reviews.length) * 10) / 10,
84
+ count: reviews.length,
85
+ },
86
+ error: null,
87
+ };
88
+ } catch (e) {
89
+ return { value: null, error: e.message };
90
+ }
91
+ }
92
+
93
+ async function safeFetchLastClean(listingId) {
94
+ try {
95
+ // Tasks filtered to cleaning/turnover type, sorted by completion time desc
96
+ const data = await guestyGet("/v1/tasks", {
97
+ "filters[listing]": listingId,
98
+ "filters[type]": "cleaning,turnover",
99
+ "filters[status]": "completed",
100
+ limit: 1,
101
+ sort: "-completedAt",
102
+ });
103
+ const t = (data && data.results && data.results[0]) || null;
104
+ if (!t) return { value: { completed_at: null }, error: null };
105
+ return {
106
+ value: {
107
+ completed_at: t.completedAt || t.updatedAt || null,
108
+ task_id: t._id,
109
+ assignee: t.assignee && t.assignee.fullName,
110
+ },
111
+ error: null,
112
+ };
113
+ } catch (e) {
114
+ return { value: null, error: e.message };
115
+ }
116
+ }
117
+
118
+ // ─────────────────────────────────────────────────────────────────────────
119
+ // Utility: wrap a raw data object in the MCP text-content envelope.
120
+ // ─────────────────────────────────────────────────────────────────────────
121
+ function envelope(obj) {
122
+ return {
123
+ content: [
124
+ { type: "text", text: JSON.stringify(obj, null, 2) },
125
+ ],
126
+ };
127
+ }
128
+
129
+ function errorEnvelope(message, details) {
130
+ return {
131
+ content: [
132
+ {
133
+ type: "text",
134
+ text: JSON.stringify({ error: message, details: details || null }, null, 2),
135
+ },
136
+ ],
137
+ isError: true,
138
+ };
139
+ }
140
+
141
+ // ─────────────────────────────────────────────────────────────────────────
142
+ // Register the 3 Enterprise-tier aggregation tools on the MCP server.
143
+ // ─────────────────────────────────────────────────────────────────────────
144
+
145
+ export function registerEnterpriseTools(server) {
146
+ // ── Tool 1: get_property_health (aggregated) ────────────────────
147
+ server.tool(
148
+ "get_property_health",
149
+ "Aggregate health signal per property: IoT device status + overall IoT state, current reservation status, 50-review average score, and last-clean timestamp. Single-call snapshot for ops dashboards. Sub-fetches that fail degrade gracefully (null value + error note).",
150
+ {
151
+ listingId: z.string().describe("The Guesty listing ID for the property"),
152
+ },
153
+ {
154
+ readOnlyHint: true,
155
+ destructiveHint: false,
156
+ idempotentHint: true,
157
+ openWorldHint: false,
158
+ },
159
+ enterpriseGated("get_property_health", async (params) => {
160
+ try {
161
+ const [iot, reservation, reviews, lastClean] = await Promise.all([
162
+ getIoTPropertyHealth(params.listingId),
163
+ safeFetchReservationStatus(params.listingId),
164
+ safeFetchReviewScore(params.listingId),
165
+ safeFetchLastClean(params.listingId),
166
+ ]);
167
+ const partialErrors = [];
168
+ if (reservation.error) partialErrors.push({ field: "reservation_status", error: reservation.error });
169
+ if (reviews.error) partialErrors.push({ field: "review_score", error: reviews.error });
170
+ if (lastClean.error) partialErrors.push({ field: "last_clean", error: lastClean.error });
171
+
172
+ return envelope({
173
+ property_id: params.listingId,
174
+ iot: {
175
+ devices: iot.devices,
176
+ overall_status: iot.overall_status,
177
+ device_count: iot.devices.length,
178
+ },
179
+ reservation_status: reservation.value,
180
+ review_score: reviews.value,
181
+ last_clean: lastClean.value,
182
+ partial_errors: partialErrors.length ? partialErrors : null,
183
+ fetched_at: new Date().toISOString(),
184
+ });
185
+ } catch (e) {
186
+ return errorEnvelope("Failed to aggregate property health.", e.message);
187
+ }
188
+ })
189
+ );
190
+
191
+ // ── Tool 2: submit_checkout_photos ──────────────────────────────
192
+ server.tool(
193
+ "submit_checkout_photos",
194
+ "Accept post-checkout photo uploads and log them to the property's maintenance/cleaning record. Photos are queued for downstream inspection workflows (Phase 2 vision comparison).",
195
+ {
196
+ listingId: z
197
+ .string()
198
+ .describe("The Guesty listing ID for the property"),
199
+ reservationId: z
200
+ .string()
201
+ .describe("The reservation ID this checkout is for"),
202
+ photos: z
203
+ .array(z.string())
204
+ .describe("Array of photo URLs or file paths to submit"),
205
+ },
206
+ {
207
+ readOnlyHint: false,
208
+ destructiveHint: false,
209
+ idempotentHint: false,
210
+ openWorldHint: false,
211
+ },
212
+ enterpriseGated("submit_checkout_photos", async (params) => {
213
+ try {
214
+ const result = await submitIoTCheckoutPhotos({
215
+ listingId: params.listingId,
216
+ reservationId: params.reservationId,
217
+ photos: params.photos,
218
+ });
219
+ return envelope(result);
220
+ } catch (e) {
221
+ return errorEnvelope("Failed to submit checkout photos.", e.message);
222
+ }
223
+ })
224
+ );
225
+
226
+ // ── Tool 3: get_maintenance_alerts (portfolio-capable) ──────────
227
+ server.tool(
228
+ "get_maintenance_alerts",
229
+ "List or filter open maintenance alerts for a specific property or across the whole portfolio. Supports severity filtering and active-only (unresolved) filtering. IoT-sourced today; future Phase will merge Guesty-native task alerts.",
230
+ {
231
+ listingId: z
232
+ .string()
233
+ .optional()
234
+ .describe("Filter alerts to a specific listing ID. Omit for portfolio-wide."),
235
+ severity: z
236
+ .enum(["critical", "warning", "info", "all"])
237
+ .optional()
238
+ .default("all")
239
+ .describe("Filter by alert severity level (default: all)"),
240
+ active_only: z
241
+ .boolean()
242
+ .optional()
243
+ .default(true)
244
+ .describe("Only return active (unresolved) alerts (default: true)"),
245
+ },
246
+ {
247
+ readOnlyHint: true,
248
+ destructiveHint: false,
249
+ idempotentHint: true,
250
+ openWorldHint: false,
251
+ },
252
+ enterpriseGated("get_maintenance_alerts", async (params) => {
253
+ try {
254
+ const result = await getIoTMaintenanceAlerts({
255
+ listingId: params.listingId,
256
+ severity: params.severity,
257
+ activeOnly: params.active_only,
258
+ });
259
+ return envelope({
260
+ ...result,
261
+ scope: params.listingId ? "listing" : "portfolio",
262
+ fetched_at: new Date().toISOString(),
263
+ });
264
+ } catch (e) {
265
+ return errorEnvelope("Failed to retrieve maintenance alerts.", e.message);
266
+ }
267
+ })
268
+ );
269
+ }
270
+
271
+ // Exported for direct invocation from smoke tests without constructing
272
+ // a full McpServer instance. Each is a real call now, not a stub — tests
273
+ // may need to stub the IoT db + guestyGet for isolated execution.
274
+ export const __handlers = {
275
+ get_property_health: enterpriseGated("get_property_health", async (params) => {
276
+ const [iot, reservation, reviews, lastClean] = await Promise.all([
277
+ getIoTPropertyHealth(params.listingId),
278
+ safeFetchReservationStatus(params.listingId),
279
+ safeFetchReviewScore(params.listingId),
280
+ safeFetchLastClean(params.listingId),
281
+ ]);
282
+ return envelope({
283
+ property_id: params.listingId,
284
+ iot: {
285
+ devices: iot.devices,
286
+ overall_status: iot.overall_status,
287
+ device_count: iot.devices.length,
288
+ },
289
+ reservation_status: reservation.value,
290
+ review_score: reviews.value,
291
+ last_clean: lastClean.value,
292
+ partial_errors: [
293
+ reservation.error && { field: "reservation_status", error: reservation.error },
294
+ reviews.error && { field: "review_score", error: reviews.error },
295
+ lastClean.error && { field: "last_clean", error: lastClean.error },
296
+ ].filter(Boolean),
297
+ fetched_at: new Date().toISOString(),
298
+ });
299
+ }),
300
+ submit_checkout_photos: enterpriseGated("submit_checkout_photos", async (params) => {
301
+ const result = await submitIoTCheckoutPhotos({
302
+ listingId: params.listingId,
303
+ reservationId: params.reservationId,
304
+ photos: params.photos,
305
+ });
306
+ return envelope(result);
307
+ }),
308
+ get_maintenance_alerts: enterpriseGated("get_maintenance_alerts", async (params) => {
309
+ const result = await getIoTMaintenanceAlerts({
310
+ listingId: params.listingId,
311
+ severity: params.severity,
312
+ activeOnly: params.active_only,
313
+ });
314
+ return envelope({
315
+ ...result,
316
+ scope: params.listingId ? "listing" : "portfolio",
317
+ fetched_at: new Date().toISOString(),
318
+ });
319
+ }),
320
+ };
@@ -5,10 +5,24 @@
5
5
  */
6
6
  import express from "express";
7
7
  import { randomUUID } from "crypto";
8
+ import iotRouter from "./iot-webhook.js";
9
+ import iotReceiverRouter from "./webhook/iot-receiver.js";
10
+ import { initDB } from "./iot-db.js";
8
11
 
9
12
  const app = express();
10
13
  app.use(express.json());
11
14
 
15
+ // Initialize IoT database and mount webhook routes
16
+ initDB();
17
+ app.use(iotRouter);
18
+
19
+ // Enterprise Tier — inbound IoT event receiver (stub).
20
+ // Mounted on the same app so hosted deployments can accept events at
21
+ // /webhook/iot/:property_id. For local dev, the standalone boot file
22
+ // src/webhook/iot-receiver-server.js exposes the same router on
23
+ // IOT_WEBHOOK_PORT (default 3100).
24
+ app.use(iotReceiverRouter);
25
+
12
26
  const PORT = process.env.PORT || 3001;
13
27
 
14
28
  // Request counter
@@ -23,8 +37,8 @@ app.use((req, res, next) => {
23
37
  // Server info
24
38
  const SERVER_INFO = {
25
39
  name: "guesty-mcp-server",
26
- version: "0.6.0",
27
- description: "The first MCP server for Guesty property management. 38 tools for reservations, guests, messaging, pricing, financials, calendars, reviews, tasks, and webhooks.",
40
+ version: "0.7.0",
41
+ description: "MCP server for Guesty property management. 42 tools including IoT monitoring, property health scores, and checkout photo analysis.",
28
42
  capabilities: {
29
43
  tools: { listChanged: false },
30
44
  resources: { listChanged: false }