guesty-mcp-server 0.7.0 → 0.8.2

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/src/iot-tools.js CHANGED
@@ -1,20 +1,30 @@
1
1
  /**
2
- * Guesty MCP Server — IoT / Property Health Monitoring Tools
2
+ * Guesty MCP Server — IoT / Property Health (Internal Helpers + 1 MCP Tool)
3
3
  *
4
- * Enterprise-tier gated tools for IoT device monitoring,
5
- * checkout photo submission, maintenance alerts, and
6
- * physical readiness scoring.
7
- *
8
- * Phase 1: Core data collection and scoring.
4
+ * Phase 1: Core data collection + readiness scoring.
9
5
  * Phase 2: Vision-based photo comparison (planned).
6
+ *
7
+ * ── MERGE NOTE (2026-04-17, per Owner greenlight msg 6406) ─────────────
8
+ * The three tools `get_property_health`, `submit_checkout_photos`,
9
+ * `get_maintenance_alerts` that previously lived HERE as IoT-only MCP tools
10
+ * have been EXTRACTED into plain-async internal helpers. Their canonical
11
+ * MCP-tool registration has MOVED to `enterprise-tools.js`, where each one
12
+ * becomes an Enterprise aggregator that calls these helpers AND layers in
13
+ * Guesty-side data (reservation status, review score, last-clean timestamp).
14
+ *
15
+ * Only one tool — `get_readiness_score` — is registered directly from here.
16
+ * It is Physical-Readiness only (sensor + photo signal) and has no Guesty
17
+ * aggregation need.
18
+ *
19
+ * All three helpers RETURN raw data objects (not MCP content envelopes) and
20
+ * THROW on error. Enterprise aggregators handle envelope + error shaping.
10
21
  */
11
22
 
12
23
  import { z } from "zod";
13
- import { getTier, gatedHandler } from "./license.js";
24
+ import { getTier } from "./license.js";
14
25
  import {
15
26
  getLatestReadings,
16
27
  getAlerts,
17
- saveBaseline,
18
28
  getBaseline,
19
29
  savePhotos,
20
30
  } from "./iot-db.js";
@@ -22,8 +32,11 @@ import {
22
32
  /**
23
33
  * Enterprise-tier gate. Returns an error response if the current
24
34
  * license is not enterprise. Wraps the inner handler otherwise.
35
+ *
36
+ * Exported so enterprise-tools.js can reuse the same gate without
37
+ * duplicating the string copy.
25
38
  */
26
- function enterpriseGated(toolName, handler) {
39
+ export function enterpriseGated(toolName, handler) {
27
40
  return async (params) => {
28
41
  const tier = getTier();
29
42
  if (tier !== "enterprise") {
@@ -34,7 +47,7 @@ function enterpriseGated(toolName, handler) {
34
47
  text:
35
48
  `This tool (${toolName}) requires an Enterprise license. ` +
36
49
  `Your current tier is "${tier}". ` +
37
- "IoT monitoring, property health, and readiness scoring are Enterprise-only features. " +
50
+ "IoT monitoring, property health aggregation, and readiness scoring are Enterprise-only features. " +
38
51
  "Upgrade at https://guestycopilot.com/pricing -- " +
39
52
  "Set GUESTY_MCP_LICENSE_KEY with an Enterprise key (gmcp_ent_*) to unlock.",
40
53
  },
@@ -52,7 +65,7 @@ function enterpriseGated(toolName, handler) {
52
65
  * - "warning" if ANY device has a warning alert
53
66
  * - "healthy" otherwise
54
67
  */
55
- function deriveOverallStatus(devices) {
68
+ export function deriveOverallStatus(devices) {
56
69
  let status = "healthy";
57
70
  for (const d of devices) {
58
71
  if (d.alert_level === "critical") return "critical";
@@ -61,242 +74,96 @@ function deriveOverallStatus(devices) {
61
74
  return status;
62
75
  }
63
76
 
77
+ // ─────────────────────────────────────────────────────────────────────────
78
+ // INTERNAL HELPERS (exported for enterprise-tools.js aggregators)
79
+ // Return raw data objects. Throw on error. No MCP envelope wrapping.
80
+ // ─────────────────────────────────────────────────────────────────────────
81
+
64
82
  /**
65
- * Register all 4 IoT / Property Health tools on the given MCP server.
66
- *
67
- * @param {import("@modelcontextprotocol/sdk/server/mcp.js").McpServer} server
83
+ * Fetch IoT device health snapshot for a single property.
84
+ * @param {string} listingId Guesty listing ID
85
+ * @returns {Promise<{property_id: string, devices: Array, overall_status: string}>}
68
86
  */
69
- export function registerIoTTools(server) {
70
- // ── Tool 1: get_property_health ─────────────────────────────────
71
- server.tool(
72
- "get_property_health",
73
- "Get the current health status of all IoT devices at a property. Returns latest readings for every device (temperature, humidity, leak, lock, etc.) and an overall property status.",
74
- {
75
- listingId: z.string().describe("The Guesty listing ID for the property"),
76
- },
77
- {
78
- readOnlyHint: true,
79
- destructiveHint: false,
80
- idempotentHint: true,
81
- openWorldHint: false,
82
- },
83
- enterpriseGated("get_property_health", async (params) => {
84
- try {
85
- const readings = await getLatestReadings(params.listingId);
86
- const devices = (readings || []).map((r) => ({
87
- device_id: r.device_id,
88
- type: r.type,
89
- location: r.location,
90
- last_reading: r.last_reading,
91
- last_seen: r.last_seen,
92
- alert_level: r.alert_level || null,
93
- }));
94
-
95
- const overallStatus = deriveOverallStatus(devices);
96
-
97
- return {
98
- content: [
99
- {
100
- type: "text",
101
- text: JSON.stringify(
102
- {
103
- property_id: params.listingId,
104
- devices,
105
- overall_status: overallStatus,
106
- },
107
- null,
108
- 2
109
- ),
110
- },
111
- ],
112
- };
113
- } catch (e) {
114
- return {
115
- content: [
116
- {
117
- type: "text",
118
- text: JSON.stringify(
119
- {
120
- error: "Failed to retrieve property health data.",
121
- details: e.message,
122
- },
123
- null,
124
- 2
125
- ),
126
- },
127
- ],
128
- isError: true,
129
- };
130
- }
131
- })
132
- );
133
-
134
- // ── Tool 2: submit_checkout_photos ──────────────────────────────
135
- server.tool(
136
- "submit_checkout_photos",
137
- "Submit post-checkout photos for a property. Stores photo URLs/paths with reservation metadata for later comparison against baseline images. (Vision analysis is Phase 2.)",
138
- {
139
- listingId: z
140
- .string()
141
- .describe("The Guesty listing ID for the property"),
142
- reservationId: z
143
- .string()
144
- .describe("The reservation ID this checkout is for"),
145
- photos: z
146
- .array(z.string())
147
- .describe("Array of photo URLs or file paths to submit"),
148
- },
149
- {
150
- readOnlyHint: false,
151
- destructiveHint: false,
152
- idempotentHint: false,
153
- openWorldHint: false,
154
- },
155
- enterpriseGated("submit_checkout_photos", async (params) => {
156
- try {
157
- if (!params.photos || params.photos.length === 0) {
158
- return {
159
- content: [
160
- {
161
- type: "text",
162
- text: JSON.stringify(
163
- { error: "No photos provided. Include at least one photo URL or path." },
164
- null,
165
- 2
166
- ),
167
- },
168
- ],
169
- isError: true,
170
- };
171
- }
172
-
173
- const result = await savePhotos({
174
- listingId: params.listingId,
175
- reservationId: params.reservationId,
176
- photos: params.photos,
177
- timestamp: new Date().toISOString(),
178
- });
179
-
180
- return {
181
- content: [
182
- {
183
- type: "text",
184
- text: JSON.stringify(
185
- {
186
- submission_id: result.submission_id,
187
- photo_count: params.photos.length,
188
- status: "received",
189
- message: "Photos queued for analysis",
190
- },
191
- null,
192
- 2
193
- ),
194
- },
195
- ],
196
- };
197
- } catch (e) {
198
- return {
199
- content: [
200
- {
201
- type: "text",
202
- text: JSON.stringify(
203
- {
204
- error: "Failed to submit checkout photos.",
205
- details: e.message,
206
- },
207
- null,
208
- 2
209
- ),
210
- },
211
- ],
212
- isError: true,
213
- };
214
- }
215
- })
216
- );
87
+ export async function getIoTPropertyHealth(listingId) {
88
+ const readings = await getLatestReadings(listingId);
89
+ const devices = (readings || []).map((r) => ({
90
+ device_id: r.device_id,
91
+ type: r.type,
92
+ location: r.location,
93
+ last_reading: r.last_reading,
94
+ last_seen: r.last_seen,
95
+ alert_level: r.alert_level || null,
96
+ }));
97
+ return {
98
+ property_id: listingId,
99
+ devices,
100
+ overall_status: deriveOverallStatus(devices),
101
+ };
102
+ }
217
103
 
218
- // ── Tool 3: get_maintenance_alerts ──────────────────────────────
219
- server.tool(
220
- "get_maintenance_alerts",
221
- "Get active maintenance alerts from IoT devices. Filter by property, severity, or active-only status. Returns alerts with device info, severity, and timestamps.",
222
- {
223
- listingId: z
224
- .string()
225
- .optional()
226
- .describe("Filter alerts to a specific listing ID. Omit for all properties."),
227
- severity: z
228
- .enum(["critical", "warning", "info", "all"])
229
- .optional()
230
- .default("all")
231
- .describe("Filter by alert severity level (default: all)"),
232
- active_only: z
233
- .boolean()
234
- .optional()
235
- .default(true)
236
- .describe("Only return active (unresolved) alerts (default: true)"),
237
- },
238
- {
239
- readOnlyHint: true,
240
- destructiveHint: false,
241
- idempotentHint: true,
242
- openWorldHint: false,
243
- },
244
- enterpriseGated("get_maintenance_alerts", async (params) => {
245
- try {
246
- const filters = {
247
- listingId: params.listingId || null,
248
- severity: params.severity === "all" ? null : params.severity,
249
- activeOnly: params.active_only,
250
- };
104
+ /**
105
+ * Save submitted post-checkout photos for a property+reservation.
106
+ * @param {{listingId: string, reservationId: string, photos: string[]}} args
107
+ * @returns {Promise<{submission_id: string, photo_count: number, status: string, message: string}>}
108
+ */
109
+ export async function submitIoTCheckoutPhotos({ listingId, reservationId, photos }) {
110
+ if (!photos || photos.length === 0) {
111
+ throw new Error("No photos provided. Include at least one photo URL or path.");
112
+ }
113
+ const result = await savePhotos({
114
+ listingId,
115
+ reservationId,
116
+ photos,
117
+ timestamp: new Date().toISOString(),
118
+ });
119
+ return {
120
+ submission_id: result.submission_id,
121
+ photo_count: photos.length,
122
+ status: "received",
123
+ message: "Photos queued for analysis",
124
+ };
125
+ }
251
126
 
252
- const alertData = await getAlerts(filters);
253
- const alerts = (alertData || []).map((a) => ({
254
- id: a.id,
255
- device_id: a.device_id,
256
- property_id: a.property_id,
257
- alert_type: a.alert_type,
258
- severity: a.severity,
259
- message: a.message,
260
- created_at: a.created_at,
261
- }));
127
+ /**
128
+ * Fetch IoT maintenance alerts, optionally filtered by listing / severity / active-only.
129
+ * @param {{listingId?: string, severity?: string, activeOnly?: boolean}} [filters]
130
+ * @returns {Promise<{alerts: Array, total_count: number}>}
131
+ */
132
+ export async function getIoTMaintenanceAlerts(filters = {}) {
133
+ const dbFilters = {
134
+ listingId: filters.listingId || null,
135
+ severity:
136
+ !filters.severity || filters.severity === "all"
137
+ ? null
138
+ : filters.severity,
139
+ activeOnly: filters.activeOnly !== false, // default true
140
+ };
141
+ const alertData = await getAlerts(dbFilters);
142
+ const alerts = (alertData || []).map((a) => ({
143
+ id: a.id,
144
+ device_id: a.device_id,
145
+ property_id: a.property_id,
146
+ alert_type: a.alert_type,
147
+ severity: a.severity,
148
+ message: a.message,
149
+ created_at: a.created_at,
150
+ }));
151
+ return { alerts, total_count: alerts.length };
152
+ }
262
153
 
263
- return {
264
- content: [
265
- {
266
- type: "text",
267
- text: JSON.stringify(
268
- {
269
- alerts,
270
- total_count: alerts.length,
271
- },
272
- null,
273
- 2
274
- ),
275
- },
276
- ],
277
- };
278
- } catch (e) {
279
- return {
280
- content: [
281
- {
282
- type: "text",
283
- text: JSON.stringify(
284
- {
285
- error: "Failed to retrieve maintenance alerts.",
286
- details: e.message,
287
- },
288
- null,
289
- 2
290
- ),
291
- },
292
- ],
293
- isError: true,
294
- };
295
- }
296
- })
297
- );
154
+ // ─────────────────────────────────────────────────────────────────────────
155
+ // MCP TOOL REGISTRATION (only 1 tool — get_readiness_score)
156
+ // The other 3 tools live in enterprise-tools.js post-merge.
157
+ // ─────────────────────────────────────────────────────────────────────────
298
158
 
299
- // ── Tool 4: get_readiness_score ─────────────────────────────────
159
+ /**
160
+ * Register the Physical Readiness Score tool on the given MCP server.
161
+ * Enterprise-gated — requires Enterprise license key.
162
+ *
163
+ * @param {import("@modelcontextprotocol/sdk/server/mcp.js").McpServer} server
164
+ */
165
+ export function registerIoTTools(server) {
166
+ // ── Tool: get_readiness_score ───────────────────────────────────
300
167
  server.tool(
301
168
  "get_readiness_score",
302
169
  "Calculate a 0-100 Physical Readiness Score for a property before guest check-in. Evaluates temperature, leak detection, door lock, humidity, critical alerts, and baseline photos.",
package/src/license.js CHANGED
@@ -84,7 +84,10 @@ function isToolAllowed(toolName) {
84
84
 
85
85
  function getTierInfo() {
86
86
  const tier = getTier();
87
- const totalTools = 38;
87
+ // 39 Guesty core tools + 1 IoT (get_readiness_score) + 3 Enterprise aggregators
88
+ // (get_property_health, submit_checkout_photos, get_maintenance_alerts) = 43.
89
+ // Updated 2026-04-17 for Enterprise Tier MVP merge (Owner msg 6406).
90
+ const totalTools = 43;
88
91
  return {
89
92
  tier,
90
93
  hasKey: !!process.env.GUESTY_MCP_LICENSE_KEY,
@@ -101,7 +104,7 @@ function gatedHandler(toolName, handler) {
101
104
  "Free tier includes " + FREE_TOOLS.length + " operations and data tools. " +
102
105
  "Guest messaging, review responses, and write operations require Pro+. " +
103
106
  "Upgrade at https://guestycopilot.com/pricing -- " +
104
- "Set GUESTY_MCP_LICENSE_KEY env var to unlock all 38 tools.";
107
+ "Set GUESTY_MCP_LICENSE_KEY env var to unlock all 43 tools.";
105
108
  return {
106
109
  content: [{ type: "text", text: msg }],
107
110
  isError: true,
package/src/server.js CHANGED
@@ -4,6 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { z } from "zod";
5
5
  import { getTier, getTierInfo, gatedHandler, FREE_TOOLS } from './license.js';
6
6
  import { registerIoTTools } from './iot-tools.js';
7
+ import { registerEnterpriseTools } from './enterprise-tools.js';
7
8
  import { initDB } from './iot-db.js';
8
9
 
9
10
  // Guesty API Configuration
@@ -41,7 +42,7 @@ async function getToken() {
41
42
  return cachedToken;
42
43
  }
43
44
 
44
- async function guestyGet(path, params = {}, retries = 2) {
45
+ export async function guestyGet(path, params = {}, retries = 2) {
45
46
  const token = await getToken();
46
47
  const url = new URL(`${GUESTY_API_BASE}${path}`);
47
48
  Object.entries(params).forEach(([k, v]) => {
@@ -1415,9 +1416,15 @@ server.tool(
1415
1416
  }
1416
1417
  );
1417
1418
 
1418
- // Initialize IoT database and register IoT/Property Health tools (Enterprise tier)
1419
+ // Initialize IoT database and register Enterprise-tier tools.
1420
+ // After the 2026-04-17 MERGE:
1421
+ // - iot-tools.js registers `get_readiness_score` only (1 tool).
1422
+ // - enterprise-tools.js registers the 3 aggregator tools
1423
+ // (get_property_health, submit_checkout_photos, get_maintenance_alerts)
1424
+ // which wrap the extracted IoT helpers + Guesty-layer data.
1419
1425
  initDB();
1420
1426
  registerIoTTools(server);
1427
+ registerEnterpriseTools(server);
1421
1428
 
1422
1429
  // Start server
1423
1430
  const transport = new StdioServerTransport();
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone boot for the IoT webhook receiver (Enterprise Tier stub).
4
+ *
5
+ * Runs the iot-receiver router on its own Express app, listening on
6
+ * IOT_WEBHOOK_PORT (default 3100). Use this for local dev / smoke tests
7
+ * when you do not want to start the full MCP HTTP server.
8
+ *
9
+ * node src/webhook/iot-receiver-server.js
10
+ * IOT_WEBHOOK_PORT=4100 node src/webhook/iot-receiver-server.js
11
+ *
12
+ * Production: run behind a reverse proxy that terminates TLS and enforces
13
+ * a real HMAC check against IOT_WEBHOOK_SECRET. Do NOT expose this port
14
+ * directly on the public internet.
15
+ */
16
+
17
+ import express from "express";
18
+ import iotReceiverRouter from "./iot-receiver.js";
19
+
20
+ const PORT = parseInt(process.env.IOT_WEBHOOK_PORT || "3100", 10);
21
+
22
+ const app = express();
23
+ app.use(express.json({ limit: "256kb" }));
24
+ app.use(iotReceiverRouter);
25
+
26
+ app.listen(PORT, "127.0.0.1", () => {
27
+ console.log(`[iot-receiver] stub listening on http://127.0.0.1:${PORT}`);
28
+ console.log(`[iot-receiver] POST /webhook/iot/:property_id`);
29
+ console.log(`[iot-receiver] GET /webhook/iot/health`);
30
+ });
@@ -0,0 +1,110 @@
1
+ /**
2
+ * IoT Webhook Receiver — Enterprise Tier (STUB)
3
+ *
4
+ * Endpoints:
5
+ * POST /webhook/iot/:property_id — accept inbound IoT events
6
+ * GET /webhook/iot/health — liveness probe
7
+ *
8
+ * Responsibilities (stub scope only):
9
+ * - Require a non-empty `x-dlj-webhook-signature` header
10
+ * (HMAC verification is stubbed — see TODO below).
11
+ * - Parse JSON body: { device_id, event_type, severity, timestamp, metadata }
12
+ * - Append one JSON line per event to ~/.dlj-scripts/logs/iot-webhook.log
13
+ * - Return 202 Accepted with { received: true, event_id: <uuid> }
14
+ *
15
+ * NOT IN SCOPE (future phases):
16
+ * - Real HMAC verification against IOT_WEBHOOK_SECRET
17
+ * - Rate limiting
18
+ * - Durable persistence / DB writes
19
+ * - Fan-out to `get_maintenance_alerts` MCP tool
20
+ *
21
+ * Production deployment note:
22
+ * This receiver is intended to run behind a reverse proxy (nginx / Caddy /
23
+ * Cloudflare Tunnel) that terminates TLS and enforces real HMAC checks.
24
+ * Do NOT expose port IOT_WEBHOOK_PORT (default 3100) on the public internet.
25
+ */
26
+
27
+ import { Router } from "express";
28
+ import { randomUUID } from "node:crypto";
29
+ import { appendFile, mkdir } from "node:fs/promises";
30
+ import { dirname } from "node:path";
31
+ import { homedir } from "node:os";
32
+
33
+ const router = Router();
34
+
35
+ const LOG_PATH = `${homedir()}/.dlj-scripts/logs/iot-webhook.log`;
36
+
37
+ // Best-effort: ensure logs dir exists at import time.
38
+ mkdir(dirname(LOG_PATH), { recursive: true }).catch(() => {});
39
+
40
+ /**
41
+ * Append a single JSON line to the rotating log file.
42
+ * Rotation itself is delegated to logrotate / launchd — this module is
43
+ * append-only.
44
+ */
45
+ async function logEvent(entry) {
46
+ try {
47
+ await appendFile(LOG_PATH, JSON.stringify(entry) + "\n", "utf8");
48
+ } catch (err) {
49
+ // Surface a line to stderr but do not fail the request — observability
50
+ // is best-effort at this tier.
51
+ console.error("[iot-receiver] log write failed:", err.message);
52
+ }
53
+ }
54
+
55
+ // ── Health ────────────────────────────────────────────────────────────────
56
+
57
+ router.get("/webhook/iot/health", (req, res) => {
58
+ res.status(200).json({
59
+ status: "ok",
60
+ receiver: "iot-webhook-stub",
61
+ log_path: LOG_PATH
62
+ });
63
+ });
64
+
65
+ // ── Inbound event ─────────────────────────────────────────────────────────
66
+
67
+ router.post("/webhook/iot/:property_id", async (req, res) => {
68
+ const signature = req.get("x-dlj-webhook-signature");
69
+
70
+ // TODO: verify HMAC against IOT_WEBHOOK_SECRET
71
+ // Expected: HMAC-SHA256(body, IOT_WEBHOOK_SECRET) === signature
72
+ if (!signature || typeof signature !== "string" || signature.length === 0) {
73
+ return res.status(401).json({
74
+ received: false,
75
+ error: "missing x-dlj-webhook-signature header"
76
+ });
77
+ }
78
+
79
+ const body = req.body;
80
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
81
+ return res.status(400).json({
82
+ received: false,
83
+ error: "body must be a JSON object"
84
+ });
85
+ }
86
+
87
+ const { device_id, event_type, severity, timestamp, metadata } = body;
88
+
89
+ const event_id = randomUUID();
90
+ const entry = {
91
+ event_id,
92
+ property_id: req.params.property_id,
93
+ device_id: device_id ?? null,
94
+ event_type: event_type ?? null,
95
+ severity: severity ?? null,
96
+ timestamp: timestamp ?? new Date().toISOString(),
97
+ metadata: metadata ?? null,
98
+ signature_present: true,
99
+ received_at: new Date().toISOString()
100
+ };
101
+
102
+ await logEvent(entry);
103
+
104
+ return res.status(202).json({
105
+ received: true,
106
+ event_id
107
+ });
108
+ });
109
+
110
+ export default router;
package/.env.example DELETED
@@ -1,2 +0,0 @@
1
- GUESTY_CLIENT_ID=your-client-id-here
2
- GUESTY_CLIENT_SECRET=your-client-secret-here
package/CONTRIBUTING.md DELETED
@@ -1,66 +0,0 @@
1
- # Contributing to Guesty MCP Server
2
-
3
- Thank you for your interest in contributing! This is the first MCP server for property management, and community contributions help make it better for everyone.
4
-
5
- ## Getting Started
6
-
7
- 1. Fork the repository
8
- 2. Clone your fork: `git clone https://github.com/YOUR-USERNAME/guesty-mcp-server.git`
9
- 3. Install dependencies: `npm install`
10
- 4. Create a `.env` file with your Guesty API credentials (see `.env.example`)
11
- 5. Run the server: `npm start`
12
-
13
- ## Adding New Tools
14
-
15
- Each tool wraps a Guesty API endpoint. To add a new tool:
16
-
17
- 1. Identify the Guesty API endpoint from [Guesty Open API docs](https://open-api.guesty.com/api-docs)
18
- 2. Add the tool in `src/server.js` using the `server.tool()` pattern
19
- 3. Follow the existing naming convention: `verb_noun` (e.g., `get_reservations`, `update_pricing`)
20
- 4. Include proper Zod schema validation for all parameters
21
- 5. Return structured JSON in the response
22
-
23
- ### Tool Template
24
-
25
- ```javascript
26
- server.tool(
27
- "tool_name",
28
- "Clear description of what this tool does",
29
- {
30
- param1: z.string().describe("What this parameter does"),
31
- param2: z.number().optional().describe("Optional parameter"),
32
- },
33
- async (params) => {
34
- const data = await guestyGet("/endpoint", { key: params.param1 });
35
- return {
36
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
37
- };
38
- }
39
- );
40
- ```
41
-
42
- ## Desired Contributions
43
-
44
- We'd love help with:
45
- - **New tools**: Reviews management, task/cleaning scheduling, owner statements, channel management
46
- - **Testing**: Integration tests, error handling edge cases
47
- - **Documentation**: Usage examples, video tutorials, blog posts
48
- - **PMS integrations**: Adapting this pattern for Hostaway, Lodgify, OwnerRez, etc.
49
-
50
- ## Code Style
51
-
52
- - ES modules (import/export)
53
- - Async/await for all API calls
54
- - Descriptive error messages
55
- - Keep tools focused -- one API action per tool
56
-
57
- ## Pull Requests
58
-
59
- 1. Create a feature branch: `git checkout -b feature/new-tool`
60
- 2. Make your changes
61
- 3. Test against the Guesty API
62
- 4. Submit a PR with a clear description of what the tool does and why
63
-
64
- ## Questions?
65
-
66
- Open an issue on GitHub or reach out to [DLJ Properties](https://tinyhomeboutiques.com).
package/Dockerfile DELETED
@@ -1,13 +0,0 @@
1
- FROM node:18-alpine
2
-
3
- WORKDIR /app
4
- COPY package*.json ./
5
- RUN npm ci --production
6
- COPY . .
7
-
8
- ENV GUESTY_CLIENT_ID=""
9
- ENV GUESTY_CLIENT_SECRET=""
10
-
11
- EXPOSE 3001 3002
12
-
13
- CMD ["node", "src/server.js"]