guesty-mcp-server 0.8.1 → 0.9.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,23 @@
2
2
 
3
3
  All notable changes to the Guesty MCP Server will be documented in this file.
4
4
 
5
+ ## [0.9.1] - 2026-04-22
6
+
7
+ ### Fixed
8
+ - **Issue #1** — `get_reservations`, `get_revenue_summary`, `get_financials`, and `get_owner_statements` silently ignored `checkInFrom`/`checkInTo`/`from`/`to` date params, returning only upcoming reservations regardless of the requested window. Root cause: `checkIn[$gte]` / `checkIn[$lte]` bracket-style query params are not honored by the Guesty Open API v1. New `buildReservationFilters()` helper emits the correct `filters=[{field, operator, from, to}]` JSON-array shape and moves `listingId` + `status` inside the array so they survive alongside date windows. No `context:"now"` scoping (the original upcoming-only culprit).
9
+ - **Issue #1** — `get_listing_occupancy` returned `totalDays: 0` because the calendar response shape wasn't mapped defensively. Now normalizes across `days` / `data` / `results` / bare-array variants, and falls back to a reservation-derived occupancy calculation when the calendar endpoint returns no per-day rows.
10
+
11
+ ### Added
12
+ - MCP Resources primitive — 7 addressable `guesty://` templates (listing, reservation, review, guest, thread, report-revenue, listing-tasks) wired via `src/resources.js`. Capabilities now advertise `tools` + `resources` (server-side half of the 0.9.0 ship that was deployed 2026-04-20 but not previously committed).
13
+ - `tests/test-issue-1-filters.mjs` — 8-case offline regression covering filter-builder output for historical windows, listing scoping, checkout bounds, and `context:"now"` leak detection.
14
+
15
+ ## [0.8.2] - 2026-04-19
16
+
17
+ ### Fixed
18
+ - npm description synced to "43 production tools" (was stale at "38 tools" on npm page)
19
+ - Removed `claude-code` and `openclaw` from npm keywords (AI-disclosure hygiene)
20
+ - Added `iot` and `enterprise` keywords for discoverability
21
+
5
22
  ## [0.8.1] - 2026-04-19
6
23
 
7
24
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "guesty-mcp-server",
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.",
3
+ "version": "0.9.1",
4
+ "description": "MCP server for Guesty property management 43 production tools + 7 addressable resource templates covering reservations, guests, messaging, pricing, revenue, tasks, webhooks, and IoT/property-health Enterprise tier.",
5
5
  "main": "src/server.js",
6
6
  "bin": {
7
7
  "guesty-mcp-server": "src/server.js",
@@ -23,10 +23,9 @@
23
23
  "vrbo",
24
24
  "vacation-rental",
25
25
  "ai-agent",
26
- "claude",
27
- "claude-code",
28
- "openclaw",
29
26
  "pms",
27
+ "iot",
28
+ "enterprise",
30
29
  "hospitality",
31
30
  "real-estate",
32
31
  "automation"
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "guesty-mcp-server",
3
+ "version": "0.8.2",
4
+ "description": "MCP server for Guesty property management — 43 production tools covering reservations, guests, messaging, pricing, revenue, tasks, webhooks, and IoT/property-health Enterprise tier.",
5
+ "main": "src/server.js",
6
+ "bin": {
7
+ "guesty-mcp-server": "src/server.js",
8
+ "guesty-cli": "src/cli.js"
9
+ },
10
+ "type": "module",
11
+ "scripts": {
12
+ "start": "node src/server.js",
13
+ "test": "node tests/test-enterprise.js && node tests/test-iot.js"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "mcp-server",
18
+ "model-context-protocol",
19
+ "guesty",
20
+ "property-management",
21
+ "short-term-rental",
22
+ "airbnb",
23
+ "vrbo",
24
+ "vacation-rental",
25
+ "ai-agent",
26
+ "pms",
27
+ "iot",
28
+ "enterprise",
29
+ "hospitality",
30
+ "real-estate",
31
+ "automation"
32
+ ],
33
+ "author": "DLJ Properties",
34
+ "license": "MIT",
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "latest",
37
+ "@vercel/analytics": "^2.0.1",
38
+ "express": "^5.2.1"
39
+ },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/DLJRealty/guesty-mcp-server.git"
43
+ },
44
+ "homepage": "https://github.com/DLJRealty/guesty-mcp-server",
45
+ "mcpName": "io.github.DLJRealty/guesty",
46
+ "engines": {
47
+ "node": ">=18.0.0"
48
+ }
49
+ }
package/server.json CHANGED
@@ -2,43 +2,49 @@
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 43 tools (39 Guesty + 1 IoT + 3 Enterprise aggregators) for STR operations.",
6
- "version": "0.8.0",
5
+ "description": "MCP server for Guesty property management. 43 tools for STR operations.",
6
+ "version": "0.8.2",
7
7
  "repository": {
8
8
  "url": "https://github.com/DLJRealty/guesty-mcp-server",
9
9
  "source": "github"
10
10
  },
11
- "packages": [{
12
- "registryType": "npm",
13
- "identifier": "guesty-mcp-server",
14
- "version": "0.8.0",
15
- "transport": { "type": "stdio" },
16
- "environmentVariables": [
17
- {
18
- "description": "Guesty OAuth2 Client ID",
19
- "isRequired": true,
20
- "format": "string",
21
- "isSecret": true,
22
- "name": "GUESTY_CLIENT_ID"
11
+ "packages": [
12
+ {
13
+ "registryType": "npm",
14
+ "identifier": "guesty-mcp-server",
15
+ "version": "0.8.2",
16
+ "transport": {
17
+ "type": "stdio"
23
18
  },
24
- {
25
- "description": "Guesty OAuth2 Client Secret",
26
- "isRequired": true,
27
- "format": "string",
28
- "isSecret": true,
29
- "name": "GUESTY_CLIENT_SECRET"
30
- },
31
- {
32
- "description": "License key for Pro/Business/Enterprise features (optional for free tier)",
33
- "isRequired": false,
34
- "format": "string",
35
- "isSecret": true,
36
- "name": "GUESTY_MCP_LICENSE_KEY"
37
- }
38
- ]
39
- }],
40
- "remotes": [{
41
- "transportType": "streamable-http",
42
- "url": "https://guesty-mcp-server.vercel.app"
43
- }]
44
- }
19
+ "environmentVariables": [
20
+ {
21
+ "description": "Guesty OAuth2 Client ID",
22
+ "isRequired": true,
23
+ "format": "string",
24
+ "isSecret": true,
25
+ "name": "GUESTY_CLIENT_ID"
26
+ },
27
+ {
28
+ "description": "Guesty OAuth2 Client Secret",
29
+ "isRequired": true,
30
+ "format": "string",
31
+ "isSecret": true,
32
+ "name": "GUESTY_CLIENT_SECRET"
33
+ },
34
+ {
35
+ "description": "License key for Pro/Business/Enterprise features (optional for free tier)",
36
+ "isRequired": false,
37
+ "format": "string",
38
+ "isSecret": true,
39
+ "name": "GUESTY_MCP_LICENSE_KEY"
40
+ }
41
+ ]
42
+ }
43
+ ],
44
+ "remotes": [
45
+ {
46
+ "type": "streamable-http",
47
+ "url": "https://guesty-mcp-server.vercel.app"
48
+ }
49
+ ]
50
+ }
@@ -5,23 +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";
11
8
 
12
9
  const app = express();
13
10
  app.use(express.json());
14
11
 
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);
12
+ // IoT routes require filesystem for JSON DB — skip on Vercel serverless
13
+ // (Vercel has read-only /tmp only; initDB() writes to ~/.dlj-scripts/data/)
14
+ if (!process.env.VERCEL) {
15
+ try {
16
+ const { default: iotRouter } = await import("./iot-webhook.js");
17
+ const { default: iotReceiverRouter } = await import("./webhook/iot-receiver.js");
18
+ const { initDB } = await import("./iot-db.js");
19
+ initDB();
20
+ app.use(iotRouter);
21
+ app.use(iotReceiverRouter);
22
+ } catch (e) {
23
+ console.warn("[http-server] IoT routes skipped:", e.message);
24
+ }
25
+ }
25
26
 
26
27
  const PORT = process.env.PORT || 3001;
27
28
 
@@ -37,8 +38,8 @@ app.use((req, res, next) => {
37
38
  // Server info
38
39
  const SERVER_INFO = {
39
40
  name: "guesty-mcp-server",
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.",
41
+ version: "0.8.2",
42
+ description: "MCP server for Guesty property management 43 production tools covering reservations, guests, messaging, pricing, revenue, tasks, webhooks, and IoT/property-health Enterprise tier.",
42
43
  capabilities: {
43
44
  tools: { listChanged: false },
44
45
  resources: { listChanged: false }
@@ -60,7 +61,9 @@ const TOOLS = [
60
61
  "list_listing_photos", "get_listing_availability",
61
62
  "check_in_guest", "check_out_guest",
62
63
  "list_cleaning_tasks", "assign_cleaning_task",
63
- "get_revenue_report", "get_occupancy_report", "get_channel_distribution"
64
+ "get_revenue_report", "get_occupancy_report", "get_channel_distribution",
65
+ // Enterprise tier (IoT / property health)
66
+ "get_property_health", "submit_checkout_photos", "get_maintenance_alerts", "get_readiness_score"
64
67
  ];
65
68
 
66
69
  // Health
@@ -0,0 +1,273 @@
1
+ // src/resources.js
2
+ //
3
+ // MCP Resources primitive for Guesty MCP Server — v0.9.0 (2026-04-20).
4
+ //
5
+ // Exposes read-only addressable resources via guesty:// URI scheme so clients
6
+ // can surface Guesty entities as @-mentionable context rather than forcing a
7
+ // tool call for every read. Reuses the existing `guestyGet` helper from server.js.
8
+ //
9
+ // URI templates registered:
10
+ // guesty://listing/{id} — listing detail
11
+ // guesty://reservation/{id} — reservation detail
12
+ // guesty://review/{id} — review detail
13
+ // guesty://guest/{id} — guest profile
14
+ // guesty://thread/{conversation_id} — message thread
15
+ // guesty://report/revenue/{month} — monthly revenue snapshot (YYYY-MM)
16
+ // guesty://listing/{listing_id}/tasks — open tasks for a listing
17
+ //
18
+ // Capabilities declared by McpServer when at least one resource is registered:
19
+ // resources: { listChanged: false, subscribe: false }
20
+ //
21
+ // Subscribe + listChanged are intentionally OFF for v0.9.0 — keeps scope tight.
22
+ // Enable in v0.10.0 after per-resource caching strategy lands.
23
+
24
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
25
+
26
+ /**
27
+ * @param {object} server - McpServer instance
28
+ * @param {object} deps - { guestyGet } dependency injection to avoid circular imports
29
+ */
30
+ export function registerResources(server, { guestyGet }) {
31
+ // --- Helper: shape a ReadResourceResult ---
32
+ const asJsonResource = (uri, data, mimeType = "application/json") => ({
33
+ contents: [
34
+ {
35
+ uri: uri.toString(),
36
+ mimeType,
37
+ text: typeof data === "string" ? data : JSON.stringify(data, null, 2),
38
+ },
39
+ ],
40
+ });
41
+
42
+ const asErrorResource = (uri, err) => ({
43
+ contents: [
44
+ {
45
+ uri: uri.toString(),
46
+ mimeType: "text/plain",
47
+ text: `Error reading ${uri}: ${err.message || String(err)}`,
48
+ },
49
+ ],
50
+ });
51
+
52
+ // --- Helper: list callback factory ---
53
+ // For each template we provide a list callback that returns the top N of that
54
+ // entity so clients can enumerate them for @-mention pickers. Required per SDK.
55
+ const listTopN = async (path, buildUri, limit = 25) => {
56
+ try {
57
+ const data = await guestyGet(path, { limit });
58
+ const items = data.results || [];
59
+ return {
60
+ resources: items.map((item) => ({
61
+ uri: buildUri(item),
62
+ name: item.title || item.guest?.fullName || item.name || item._id,
63
+ description: item.nickname || item.publicReview || undefined,
64
+ mimeType: "application/json",
65
+ })),
66
+ };
67
+ } catch (e) {
68
+ return { resources: [] };
69
+ }
70
+ };
71
+
72
+ // --- 1. Listing detail ---
73
+ server.registerResource(
74
+ "listing",
75
+ new ResourceTemplate("guesty://listing/{id}", {
76
+ list: async () =>
77
+ listTopN("/listings", (l) => `guesty://listing/${l._id}`),
78
+ }),
79
+ {
80
+ title: "Guesty Listing",
81
+ description: "Full detail for a single Guesty listing (property).",
82
+ mimeType: "application/json",
83
+ },
84
+ async (uri, { id }) => {
85
+ try {
86
+ const data = await guestyGet(`/listings/${id}`);
87
+ return asJsonResource(uri, data);
88
+ } catch (e) {
89
+ return asErrorResource(uri, e);
90
+ }
91
+ }
92
+ );
93
+
94
+ // --- 2. Reservation detail ---
95
+ server.registerResource(
96
+ "reservation",
97
+ new ResourceTemplate("guesty://reservation/{id}", {
98
+ list: async () =>
99
+ listTopN("/reservations", (r) => `guesty://reservation/${r._id}`),
100
+ }),
101
+ {
102
+ title: "Guesty Reservation",
103
+ description: "Full detail for a single reservation incl. guest, dates, money.",
104
+ mimeType: "application/json",
105
+ },
106
+ async (uri, { id }) => {
107
+ try {
108
+ const data = await guestyGet(`/reservations/${id}`);
109
+ return asJsonResource(uri, data);
110
+ } catch (e) {
111
+ return asErrorResource(uri, e);
112
+ }
113
+ }
114
+ );
115
+
116
+ // --- 3. Review detail ---
117
+ server.registerResource(
118
+ "review",
119
+ new ResourceTemplate("guesty://review/{id}", {
120
+ list: async () =>
121
+ listTopN("/reviews", (r) => `guesty://review/${r._id}`),
122
+ }),
123
+ {
124
+ title: "Guesty Review",
125
+ description: "Full detail for a single review (Airbnb, Booking.com, etc.).",
126
+ mimeType: "application/json",
127
+ },
128
+ async (uri, { id }) => {
129
+ try {
130
+ const data = await guestyGet(`/reviews/${id}`);
131
+ return asJsonResource(uri, data);
132
+ } catch (e) {
133
+ return asErrorResource(uri, e);
134
+ }
135
+ }
136
+ );
137
+
138
+ // --- 4. Guest profile ---
139
+ server.registerResource(
140
+ "guest",
141
+ new ResourceTemplate("guesty://guest/{id}", {
142
+ list: async () =>
143
+ listTopN("/guests", (g) => `guesty://guest/${g._id}`),
144
+ }),
145
+ {
146
+ title: "Guesty Guest",
147
+ description: "Guest profile: contact, tags, prior-stay history.",
148
+ mimeType: "application/json",
149
+ },
150
+ async (uri, { id }) => {
151
+ try {
152
+ const data = await guestyGet(`/guests/${id}`);
153
+ return asJsonResource(uri, data);
154
+ } catch (e) {
155
+ return asErrorResource(uri, e);
156
+ }
157
+ }
158
+ );
159
+
160
+ // --- 5. Message thread ---
161
+ server.registerResource(
162
+ "thread",
163
+ new ResourceTemplate("guesty://thread/{conversation_id}", {
164
+ // No bulk-list for threads (usually fetched per-reservation).
165
+ list: undefined,
166
+ }),
167
+ {
168
+ title: "Guesty Message Thread",
169
+ description: "All messages on a single conversation thread.",
170
+ mimeType: "application/json",
171
+ },
172
+ async (uri, { conversation_id }) => {
173
+ try {
174
+ const data = await guestyGet(
175
+ `/communication/conversations/${conversation_id}/messages`,
176
+ { limit: 100 }
177
+ );
178
+ return asJsonResource(uri, data);
179
+ } catch (e) {
180
+ return asErrorResource(uri, e);
181
+ }
182
+ }
183
+ );
184
+
185
+ // --- 6. Monthly revenue snapshot ---
186
+ // Uses the existing /reservations endpoint filtered by checkInFrom/checkInTo.
187
+ // Month format: YYYY-MM. Returns aggregate money fields.
188
+ server.registerResource(
189
+ "revenue-month",
190
+ new ResourceTemplate("guesty://report/revenue/{month}", {
191
+ list: undefined,
192
+ }),
193
+ {
194
+ title: "Monthly Revenue",
195
+ description:
196
+ "Aggregate revenue snapshot for a given month (YYYY-MM). Sums nightly rate, cleaning, extras, discounts.",
197
+ mimeType: "application/json",
198
+ },
199
+ async (uri, { month }) => {
200
+ try {
201
+ // Validate YYYY-MM
202
+ if (!/^\d{4}-\d{2}$/.test(month)) {
203
+ return asErrorResource(uri, new Error(`Invalid month '${month}' — expected YYYY-MM`));
204
+ }
205
+ const [year, mo] = month.split("-").map(Number);
206
+ const from = `${month}-01`;
207
+ const lastDay = new Date(year, mo, 0).getDate();
208
+ const to = `${month}-${String(lastDay).padStart(2, "0")}`;
209
+
210
+ const data = await guestyGet("/reservations", {
211
+ limit: 100,
212
+ "checkIn[$gte]": from,
213
+ "checkIn[$lte]": to,
214
+ status: "confirmed",
215
+ });
216
+ const results = data.results || [];
217
+
218
+ const agg = results.reduce(
219
+ (acc, r) => {
220
+ const m = r.money || {};
221
+ acc.count += 1;
222
+ acc.nights += r.nightsCount || 0;
223
+ acc.fareAccommodation += m.fareAccommodation || 0;
224
+ acc.fareCleaning += m.fareCleaning || 0;
225
+ acc.hostPayout += m.hostPayout || 0;
226
+ acc.totalPaid += m.totalPaid || 0;
227
+ return acc;
228
+ },
229
+ {
230
+ month,
231
+ count: 0,
232
+ nights: 0,
233
+ fareAccommodation: 0,
234
+ fareCleaning: 0,
235
+ hostPayout: 0,
236
+ totalPaid: 0,
237
+ }
238
+ );
239
+
240
+ return asJsonResource(uri, agg);
241
+ } catch (e) {
242
+ return asErrorResource(uri, e);
243
+ }
244
+ }
245
+ );
246
+
247
+ // --- 7. Open tasks for a listing ---
248
+ server.registerResource(
249
+ "listing-tasks",
250
+ new ResourceTemplate("guesty://listing/{listing_id}/tasks", {
251
+ list: undefined,
252
+ }),
253
+ {
254
+ title: "Open Tasks for Listing",
255
+ description: "All open/pending tasks for a given listing.",
256
+ mimeType: "application/json",
257
+ },
258
+ async (uri, { listing_id }) => {
259
+ try {
260
+ const data = await guestyGet("/tasks", {
261
+ listingId: listing_id,
262
+ status: "pending",
263
+ limit: 50,
264
+ });
265
+ return asJsonResource(uri, data);
266
+ } catch (e) {
267
+ return asErrorResource(uri, e);
268
+ }
269
+ }
270
+ );
271
+
272
+ console.error("[resources] Registered 7 Guesty resource templates (guesty://)");
273
+ }