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 +51 -0
- package/README.md +19 -2
- package/package.json +3 -3
- package/server.json +3 -3
- package/src/enterprise-tools.js +320 -0
- package/src/http-server.js +16 -2
- package/src/iot-db.js +311 -0
- package/src/iot-tools.js +338 -0
- package/src/iot-webhook.js +364 -0
- package/src/license.js +5 -2
- package/src/server.js +15 -2
- package/src/webhook/iot-receiver-server.js +30 -0
- package/src/webhook/iot-receiver.js +110 -0
- package/.env.example +0 -2
- package/CONTRIBUTING.md +0 -66
- package/Dockerfile +0 -13
- package/HACKERNEWS-POST.md +0 -44
- package/PRODUCT-HUNT-LAUNCH.md +0 -28
- package/SECURITY.md +0 -25
- package/docs/multi-account-design.md +0 -32
- package/examples/docker-compose.yml +0 -13
- package/guesty-partnership-email.md +0 -48
- package/tripadvisor-partnership-email.md +0 -27
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
|
-
**
|
|
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
|
|
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.
|
|
4
|
-
"description": "The first MCP server for Guesty property management.
|
|
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-
|
|
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 —
|
|
6
|
-
"version": "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.
|
|
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
|
+
};
|
package/src/http-server.js
CHANGED
|
@@ -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.
|
|
27
|
-
description: "
|
|
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 }
|