guesty-mcp-server 0.3.0
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/.dockerignore +6 -0
- package/.env.example +2 -0
- package/CHANGELOG.md +32 -0
- package/CONTRIBUTING.md +66 -0
- package/Dockerfile +13 -0
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/SECURITY.md +25 -0
- package/docs/multi-account-design.md +32 -0
- package/examples/claude-code-config.json +12 -0
- package/examples/docker-compose.yml +13 -0
- package/package.json +36 -0
- package/src/cli.js +101 -0
- package/src/health.js +43 -0
- package/src/http-transport.js +50 -0
- package/src/server.js +1096 -0
- package/src/token-cache.js +20 -0
- package/src/webhooks.js +83 -0
package/src/server.js
ADDED
|
@@ -0,0 +1,1096 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
// Guesty API Configuration
|
|
7
|
+
const GUESTY_CLIENT_ID = process.env.GUESTY_CLIENT_ID;
|
|
8
|
+
const GUESTY_CLIENT_SECRET = process.env.GUESTY_CLIENT_SECRET;
|
|
9
|
+
const GUESTY_API_BASE = "https://open-api.guesty.com/v1";
|
|
10
|
+
|
|
11
|
+
if (!GUESTY_CLIENT_ID || !GUESTY_CLIENT_SECRET) {
|
|
12
|
+
console.error("Error: GUESTY_CLIENT_ID and GUESTY_CLIENT_SECRET environment variables are required.");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let cachedToken = null;
|
|
17
|
+
let tokenExpiry = 0;
|
|
18
|
+
|
|
19
|
+
async function getToken() {
|
|
20
|
+
if (cachedToken && Date.now() < tokenExpiry) return cachedToken;
|
|
21
|
+
|
|
22
|
+
const res = await fetch("https://open-api.guesty.com/oauth2/token", {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
25
|
+
body: new URLSearchParams({
|
|
26
|
+
grant_type: "client_credentials",
|
|
27
|
+
scope: "open-api",
|
|
28
|
+
client_id: GUESTY_CLIENT_ID,
|
|
29
|
+
client_secret: GUESTY_CLIENT_SECRET,
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const data = await res.json();
|
|
34
|
+
if (!data.access_token) throw new Error(`Auth failed: ${JSON.stringify(data)}`);
|
|
35
|
+
|
|
36
|
+
cachedToken = data.access_token;
|
|
37
|
+
tokenExpiry = Date.now() + (data.expires_in - 60) * 1000;
|
|
38
|
+
return cachedToken;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function guestyGet(path, params = {}, retries = 2) {
|
|
42
|
+
const token = await getToken();
|
|
43
|
+
const url = new URL(`${GUESTY_API_BASE}${path}`);
|
|
44
|
+
Object.entries(params).forEach(([k, v]) => {
|
|
45
|
+
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const res = await fetch(url.toString(), {
|
|
49
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (res.status === 429 && retries > 0) {
|
|
53
|
+
const wait = Math.min(parseInt(res.headers.get("retry-after") || "5", 10), 30) * 1000;
|
|
54
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
55
|
+
return guestyGet(path, params, retries - 1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!res.ok) throw new Error(`Guesty API error ${res.status}: ${await res.text()}`);
|
|
59
|
+
return res.json();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function guestyPost(path, body, retries = 2) {
|
|
63
|
+
const token = await getToken();
|
|
64
|
+
const res = await fetch(`${GUESTY_API_BASE}${path}`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: {
|
|
67
|
+
Authorization: `Bearer ${token}`,
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
Accept: "application/json",
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify(body),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (res.status === 429 && retries > 0) {
|
|
75
|
+
const wait = Math.min(parseInt(res.headers.get("retry-after") || "5", 10), 30) * 1000;
|
|
76
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
77
|
+
return guestyPost(path, body, retries - 1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!res.ok) throw new Error(`Guesty API error ${res.status}: ${await res.text()}`);
|
|
81
|
+
return res.json();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function guestyPut(path, body, retries = 2) {
|
|
85
|
+
const token = await getToken();
|
|
86
|
+
const res = await fetch(`${GUESTY_API_BASE}${path}`, {
|
|
87
|
+
method: "PUT",
|
|
88
|
+
headers: {
|
|
89
|
+
Authorization: `Bearer ${token}`,
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
Accept: "application/json",
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify(body),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (res.status === 429 && retries > 0) {
|
|
97
|
+
const wait = Math.min(parseInt(res.headers.get("retry-after") || "5", 10), 30) * 1000;
|
|
98
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
99
|
+
return guestyPut(path, body, retries - 1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!res.ok) throw new Error(`Guesty API error ${res.status}: ${await res.text()}`);
|
|
103
|
+
return res.json();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Create MCP Server
|
|
107
|
+
const server = new McpServer({
|
|
108
|
+
name: "guesty-mcp-server",
|
|
109
|
+
version: "0.3.0",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Tool 1: Get Reservations
|
|
113
|
+
server.tool(
|
|
114
|
+
"get_reservations",
|
|
115
|
+
"Fetch reservations from Guesty. Filter by date range, listing, status, or guest name.",
|
|
116
|
+
{
|
|
117
|
+
limit: z.number().optional().default(10).describe("Max results (default 10)"),
|
|
118
|
+
skip: z.number().optional().default(0).describe("Offset for pagination"),
|
|
119
|
+
checkInFrom: z.string().optional().describe("Check-in date from (YYYY-MM-DD)"),
|
|
120
|
+
checkInTo: z.string().optional().describe("Check-in date to (YYYY-MM-DD)"),
|
|
121
|
+
checkOutFrom: z.string().optional().describe("Check-out date from (YYYY-MM-DD)"),
|
|
122
|
+
checkOutTo: z.string().optional().describe("Check-out date to (YYYY-MM-DD)"),
|
|
123
|
+
listingId: z.string().optional().describe("Filter by listing ID"),
|
|
124
|
+
status: z.string().optional().describe("Filter by status: confirmed, canceled, inquiry, etc."),
|
|
125
|
+
},
|
|
126
|
+
async (params) => {
|
|
127
|
+
const queryParams = {
|
|
128
|
+
limit: params.limit,
|
|
129
|
+
skip: params.skip,
|
|
130
|
+
sort: "checkIn",
|
|
131
|
+
order: "desc",
|
|
132
|
+
};
|
|
133
|
+
if (params.checkInFrom) queryParams["checkIn[$gte]"] = params.checkInFrom;
|
|
134
|
+
if (params.checkInTo) queryParams["checkIn[$lte]"] = params.checkInTo;
|
|
135
|
+
if (params.checkOutFrom) queryParams["checkOut[$gte]"] = params.checkOutFrom;
|
|
136
|
+
if (params.checkOutTo) queryParams["checkOut[$lte]"] = params.checkOutTo;
|
|
137
|
+
if (params.listingId) queryParams.listingId = params.listingId;
|
|
138
|
+
if (params.status) queryParams.status = params.status;
|
|
139
|
+
|
|
140
|
+
const data = await guestyGet("/reservations", queryParams);
|
|
141
|
+
|
|
142
|
+
const results = data.results || [];
|
|
143
|
+
const summary = results.map((r) => ({
|
|
144
|
+
id: r._id,
|
|
145
|
+
guest: r.guest?.fullName || "Unknown",
|
|
146
|
+
guestEmail: r.guest?.email || "",
|
|
147
|
+
guestPhone: r.guest?.phone || "",
|
|
148
|
+
checkIn: r.checkIn?.slice(0, 10),
|
|
149
|
+
checkOut: r.checkOut?.slice(0, 10),
|
|
150
|
+
nights: r.nightsCount,
|
|
151
|
+
status: r.status,
|
|
152
|
+
source: r.source,
|
|
153
|
+
listing: r.listing?.title || "Unknown",
|
|
154
|
+
listingId: r.listingId,
|
|
155
|
+
totalPaid: r.money?.totalPaid,
|
|
156
|
+
currency: r.money?.currency,
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
content: [{ type: "text", text: JSON.stringify({ total: data.count, results: summary }, null, 2) }],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// Tool 2: Get Listing
|
|
166
|
+
server.tool(
|
|
167
|
+
"get_listing",
|
|
168
|
+
"Fetch details about a specific property listing or all listings.",
|
|
169
|
+
{
|
|
170
|
+
listingId: z.string().optional().describe("Specific listing ID. Omit to get all listings."),
|
|
171
|
+
limit: z.number().optional().default(25).describe("Max results when fetching all"),
|
|
172
|
+
},
|
|
173
|
+
async (params) => {
|
|
174
|
+
let data;
|
|
175
|
+
if (params.listingId) {
|
|
176
|
+
data = await guestyGet(`/listings/${params.listingId}`);
|
|
177
|
+
const l = data;
|
|
178
|
+
const summary = {
|
|
179
|
+
id: l._id,
|
|
180
|
+
title: l.title,
|
|
181
|
+
nickname: l.nickname,
|
|
182
|
+
address: l.address?.full,
|
|
183
|
+
bedrooms: l.bedrooms,
|
|
184
|
+
bathrooms: l.bathrooms,
|
|
185
|
+
maxGuests: l.accommodates,
|
|
186
|
+
propertyType: l.propertyType,
|
|
187
|
+
prices: l.prices,
|
|
188
|
+
status: l.active ? "active" : "inactive",
|
|
189
|
+
};
|
|
190
|
+
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
191
|
+
} else {
|
|
192
|
+
data = await guestyGet("/listings", { limit: params.limit });
|
|
193
|
+
const listings = (data.results || []).map((l) => ({
|
|
194
|
+
id: l._id,
|
|
195
|
+
title: l.title,
|
|
196
|
+
nickname: l.nickname,
|
|
197
|
+
address: l.address?.full,
|
|
198
|
+
bedrooms: l.bedrooms,
|
|
199
|
+
bathrooms: l.bathrooms,
|
|
200
|
+
maxGuests: l.accommodates,
|
|
201
|
+
active: l.active,
|
|
202
|
+
}));
|
|
203
|
+
return { content: [{ type: "text", text: JSON.stringify({ total: data.count, listings }, null, 2) }] };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Tool 3: Get Conversations
|
|
209
|
+
server.tool(
|
|
210
|
+
"get_conversations",
|
|
211
|
+
"Fetch guest conversations/messages from Guesty. Get message history for a reservation or listing.",
|
|
212
|
+
{
|
|
213
|
+
reservationId: z.string().optional().describe("Filter by reservation ID"),
|
|
214
|
+
limit: z.number().optional().default(10).describe("Max conversations to return"),
|
|
215
|
+
},
|
|
216
|
+
async (params) => {
|
|
217
|
+
const queryParams = { limit: params.limit };
|
|
218
|
+
if (params.reservationId) queryParams["filters[reservationId]"] = params.reservationId;
|
|
219
|
+
|
|
220
|
+
const data = await guestyGet("/communication/conversations", queryParams);
|
|
221
|
+
const convos = (data.results || []).map((c) => ({
|
|
222
|
+
id: c._id,
|
|
223
|
+
guestName: c.guest?.fullName || "Unknown",
|
|
224
|
+
listing: c.listing?.title || "Unknown",
|
|
225
|
+
lastMessage: c.lastMessage?.body?.slice(0, 200) || "",
|
|
226
|
+
lastMessageAt: c.lastMessage?.sentAt,
|
|
227
|
+
unread: c.unreadCount,
|
|
228
|
+
reservationId: c.reservationId,
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
return { content: [{ type: "text", text: JSON.stringify({ total: data.count, conversations: convos }, null, 2) }] };
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Tool 4: Send Guest Message
|
|
236
|
+
server.tool(
|
|
237
|
+
"send_guest_message",
|
|
238
|
+
"Send a message to a guest in a Guesty conversation.",
|
|
239
|
+
{
|
|
240
|
+
conversationId: z.string().describe("The conversation ID to reply in"),
|
|
241
|
+
message: z.string().describe("The message text to send to the guest"),
|
|
242
|
+
},
|
|
243
|
+
async (params) => {
|
|
244
|
+
const data = await guestyPost(`/communication/conversations/${params.conversationId}/send-message`, {
|
|
245
|
+
body: params.message,
|
|
246
|
+
});
|
|
247
|
+
return { content: [{ type: "text", text: `Message sent successfully. ID: ${data._id || "OK"}` }] };
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Tool 5: Get Financials
|
|
252
|
+
server.tool(
|
|
253
|
+
"get_financials",
|
|
254
|
+
"Fetch financial data including revenue, payouts, and reservation financials.",
|
|
255
|
+
{
|
|
256
|
+
listingId: z.string().optional().describe("Filter by listing ID"),
|
|
257
|
+
from: z.string().optional().describe("Start date (YYYY-MM-DD)"),
|
|
258
|
+
to: z.string().optional().describe("End date (YYYY-MM-DD)"),
|
|
259
|
+
limit: z.number().optional().default(25).describe("Max results"),
|
|
260
|
+
},
|
|
261
|
+
async (params) => {
|
|
262
|
+
// Pull reservations with financial data, sorted by most recent
|
|
263
|
+
const queryParams = {
|
|
264
|
+
limit: params.limit,
|
|
265
|
+
fields: "money guest checkIn checkOut listing status nightsCount",
|
|
266
|
+
sort: "checkIn",
|
|
267
|
+
order: "desc",
|
|
268
|
+
};
|
|
269
|
+
if (params.listingId) queryParams.listingId = params.listingId;
|
|
270
|
+
if (params.from) queryParams["checkIn[$gte]"] = params.from;
|
|
271
|
+
if (params.to) queryParams["checkIn[$lte]"] = params.to;
|
|
272
|
+
|
|
273
|
+
const data = await guestyGet("/reservations", queryParams);
|
|
274
|
+
const financials = (data.results || []).map((r) => ({
|
|
275
|
+
guest: r.guest?.fullName || "Unknown",
|
|
276
|
+
listing: r.listing?.title || "Unknown",
|
|
277
|
+
checkIn: r.checkIn?.slice(0, 10),
|
|
278
|
+
checkOut: r.checkOut?.slice(0, 10),
|
|
279
|
+
status: r.status,
|
|
280
|
+
totalPaid: r.money?.totalPaid,
|
|
281
|
+
hostPayout: r.money?.hostPayout,
|
|
282
|
+
commission: r.money?.commission,
|
|
283
|
+
currency: r.money?.currency,
|
|
284
|
+
}));
|
|
285
|
+
|
|
286
|
+
const totalRevenue = financials.reduce((sum, r) => sum + (r.totalPaid || 0), 0);
|
|
287
|
+
const totalPayout = financials.reduce((sum, r) => sum + (r.hostPayout || 0), 0);
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
content: [{
|
|
291
|
+
type: "text",
|
|
292
|
+
text: JSON.stringify({
|
|
293
|
+
summary: { totalRevenue, totalPayout, reservationCount: financials.length },
|
|
294
|
+
reservations: financials,
|
|
295
|
+
}, null, 2),
|
|
296
|
+
}],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// Tool 6: Update Pricing
|
|
302
|
+
server.tool(
|
|
303
|
+
"update_pricing",
|
|
304
|
+
"Update the base price for a listing on specific dates or the default base price.",
|
|
305
|
+
{
|
|
306
|
+
listingId: z.string().describe("The listing ID to update pricing for"),
|
|
307
|
+
basePrice: z.number().optional().describe("New default base price per night"),
|
|
308
|
+
dateFrom: z.string().optional().describe("Start date for date-specific pricing (YYYY-MM-DD)"),
|
|
309
|
+
dateTo: z.string().optional().describe("End date for date-specific pricing (YYYY-MM-DD)"),
|
|
310
|
+
price: z.number().optional().describe("Price per night for the date range"),
|
|
311
|
+
},
|
|
312
|
+
async (params) => {
|
|
313
|
+
if (params.basePrice) {
|
|
314
|
+
const data = await guestyPut(`/listings/${params.listingId}`, {
|
|
315
|
+
prices: { basePrice: params.basePrice },
|
|
316
|
+
});
|
|
317
|
+
return { content: [{ type: "text", text: `Base price updated to $${params.basePrice} for listing ${params.listingId}` }] };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (params.dateFrom && params.dateTo && params.price) {
|
|
321
|
+
const data = await guestyPut(`/listings/${params.listingId}/calendar`, {
|
|
322
|
+
dateFrom: params.dateFrom,
|
|
323
|
+
dateTo: params.dateTo,
|
|
324
|
+
price: params.price,
|
|
325
|
+
});
|
|
326
|
+
return { content: [{ type: "text", text: `Price set to $${params.price}/night from ${params.dateFrom} to ${params.dateTo}` }] };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { content: [{ type: "text", text: "Error: Provide either basePrice or dateFrom+dateTo+price" }] };
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
// ============ V2 TOOLS ============
|
|
334
|
+
|
|
335
|
+
// Tool 7: Create Reservation (Direct Booking)
|
|
336
|
+
server.tool(
|
|
337
|
+
"create_reservation",
|
|
338
|
+
"Create a new reservation/booking in Guesty. Use for direct bookings from your website.",
|
|
339
|
+
{
|
|
340
|
+
listingId: z.string().describe("The listing ID to book"),
|
|
341
|
+
checkIn: z.string().describe("Check-in date (YYYY-MM-DD)"),
|
|
342
|
+
checkOut: z.string().describe("Check-out date (YYYY-MM-DD)"),
|
|
343
|
+
guestName: z.string().describe("Guest full name"),
|
|
344
|
+
guestEmail: z.string().optional().describe("Guest email address"),
|
|
345
|
+
guestPhone: z.string().optional().describe("Guest phone number"),
|
|
346
|
+
numberOfGuests: z.number().optional().default(1).describe("Number of guests"),
|
|
347
|
+
source: z.string().optional().default("direct").describe("Booking source (direct, website, etc.)"),
|
|
348
|
+
},
|
|
349
|
+
async (params) => {
|
|
350
|
+
const body = {
|
|
351
|
+
listingId: params.listingId,
|
|
352
|
+
checkInDateLocalized: params.checkIn,
|
|
353
|
+
checkOutDateLocalized: params.checkOut,
|
|
354
|
+
status: "confirmed",
|
|
355
|
+
guest: {
|
|
356
|
+
fullName: params.guestName,
|
|
357
|
+
email: params.guestEmail,
|
|
358
|
+
phone: params.guestPhone,
|
|
359
|
+
},
|
|
360
|
+
guestsCount: params.numberOfGuests,
|
|
361
|
+
source: params.source,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const data = await guestyPost("/reservations", body);
|
|
365
|
+
return {
|
|
366
|
+
content: [{
|
|
367
|
+
type: "text",
|
|
368
|
+
text: JSON.stringify({
|
|
369
|
+
success: true,
|
|
370
|
+
reservationId: data._id,
|
|
371
|
+
confirmationCode: data.confirmationCode,
|
|
372
|
+
guest: params.guestName,
|
|
373
|
+
listing: params.listingId,
|
|
374
|
+
dates: `${params.checkIn} → ${params.checkOut}`,
|
|
375
|
+
}, null, 2),
|
|
376
|
+
}],
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
// Tool 8: Get Reviews
|
|
382
|
+
server.tool(
|
|
383
|
+
"get_reviews",
|
|
384
|
+
"Fetch guest reviews for your properties from all channels.",
|
|
385
|
+
{
|
|
386
|
+
listingId: z.string().optional().describe("Filter by listing ID"),
|
|
387
|
+
limit: z.number().optional().default(10).describe("Max results"),
|
|
388
|
+
},
|
|
389
|
+
async (params) => {
|
|
390
|
+
const queryParams = { limit: params.limit };
|
|
391
|
+
if (params.listingId) queryParams.listingId = params.listingId;
|
|
392
|
+
|
|
393
|
+
const data = await guestyGet("/reviews", queryParams);
|
|
394
|
+
const reviews = (data.results || []).map((r) => ({
|
|
395
|
+
id: r._id,
|
|
396
|
+
listing: r.listing?.title || "Unknown",
|
|
397
|
+
guestName: r.guest?.fullName || "Unknown",
|
|
398
|
+
rating: r.rating,
|
|
399
|
+
comment: r.comment?.slice(0, 300),
|
|
400
|
+
response: r.response?.slice(0, 200),
|
|
401
|
+
channel: r.source,
|
|
402
|
+
date: r.createdAt?.slice(0, 10),
|
|
403
|
+
}));
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
content: [{ type: "text", text: JSON.stringify({ total: data.count, reviews }, null, 2) }],
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
// Tool 9: Get Calendar
|
|
412
|
+
server.tool(
|
|
413
|
+
"get_calendar",
|
|
414
|
+
"Fetch calendar availability and pricing for a listing over a date range.",
|
|
415
|
+
{
|
|
416
|
+
listingId: z.string().describe("The listing ID"),
|
|
417
|
+
from: z.string().describe("Start date (YYYY-MM-DD)"),
|
|
418
|
+
to: z.string().describe("End date (YYYY-MM-DD)"),
|
|
419
|
+
},
|
|
420
|
+
async (params) => {
|
|
421
|
+
const data = await guestyGet(`/listings/${params.listingId}/calendar`, {
|
|
422
|
+
from: params.from,
|
|
423
|
+
to: params.to,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const days = (data.days || data || []).map ? (data.days || []).map((d) => ({
|
|
427
|
+
date: d.date,
|
|
428
|
+
available: d.status === "available",
|
|
429
|
+
price: d.price,
|
|
430
|
+
minNights: d.minNights,
|
|
431
|
+
blockReason: d.blockReason,
|
|
432
|
+
})) : [];
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
content: [{ type: "text", text: JSON.stringify({ listing: params.listingId, days }, null, 2) }],
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
// Tool 10: Update Calendar
|
|
441
|
+
server.tool(
|
|
442
|
+
"update_calendar",
|
|
443
|
+
"Block or unblock dates, set minimum nights, or update availability for a listing.",
|
|
444
|
+
{
|
|
445
|
+
listingId: z.string().describe("The listing ID"),
|
|
446
|
+
dateFrom: z.string().describe("Start date (YYYY-MM-DD)"),
|
|
447
|
+
dateTo: z.string().describe("End date (YYYY-MM-DD)"),
|
|
448
|
+
status: z.string().optional().describe("Set to 'available' or 'unavailable'"),
|
|
449
|
+
minNights: z.number().optional().describe("Minimum night stay"),
|
|
450
|
+
blockReason: z.string().optional().describe("Reason for blocking: owner, maintenance, other"),
|
|
451
|
+
},
|
|
452
|
+
async (params) => {
|
|
453
|
+
const body = {};
|
|
454
|
+
if (params.status) body.status = params.status;
|
|
455
|
+
if (params.minNights) body.minNights = params.minNights;
|
|
456
|
+
if (params.blockReason) body.note = params.blockReason;
|
|
457
|
+
|
|
458
|
+
const data = await guestyPut(`/listings/${params.listingId}/calendar`, {
|
|
459
|
+
dateFrom: params.dateFrom,
|
|
460
|
+
dateTo: params.dateTo,
|
|
461
|
+
...body,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
content: [{ type: "text", text: `Calendar updated for ${params.listingId}: ${params.dateFrom} to ${params.dateTo}` }],
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
// Tool 11: Respond to Review
|
|
471
|
+
server.tool(
|
|
472
|
+
"respond_to_review",
|
|
473
|
+
"Post a response to a guest review.",
|
|
474
|
+
{
|
|
475
|
+
reviewId: z.string().describe("The review ID to respond to"),
|
|
476
|
+
response: z.string().describe("Your response text"),
|
|
477
|
+
},
|
|
478
|
+
async (params) => {
|
|
479
|
+
const data = await guestyPut(`/reviews/${params.reviewId}`, {
|
|
480
|
+
response: params.response,
|
|
481
|
+
});
|
|
482
|
+
return { content: [{ type: "text", text: `Review response posted successfully for review ${params.reviewId}` }] };
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
// Tool 12: Get Owner Statements
|
|
487
|
+
server.tool(
|
|
488
|
+
"get_owner_statements",
|
|
489
|
+
"Fetch owner revenue statements/reports for properties.",
|
|
490
|
+
{
|
|
491
|
+
listingId: z.string().optional().describe("Filter by listing ID"),
|
|
492
|
+
from: z.string().optional().describe("Start date (YYYY-MM-DD)"),
|
|
493
|
+
to: z.string().optional().describe("End date (YYYY-MM-DD)"),
|
|
494
|
+
limit: z.number().optional().default(10).describe("Max results"),
|
|
495
|
+
},
|
|
496
|
+
async (params) => {
|
|
497
|
+
const queryParams = { limit: params.limit };
|
|
498
|
+
if (params.listingId) queryParams.listingId = params.listingId;
|
|
499
|
+
if (params.from) queryParams["from"] = params.from;
|
|
500
|
+
if (params.to) queryParams["to"] = params.to;
|
|
501
|
+
|
|
502
|
+
// Owner statements not available via Open API v1 — fall back to financial data from reservations
|
|
503
|
+
const resData = await guestyGet("/reservations", {
|
|
504
|
+
limit: params.limit,
|
|
505
|
+
fields: "money guest checkIn checkOut listing status nightsCount",
|
|
506
|
+
...(params.listingId && { listingId: params.listingId }),
|
|
507
|
+
...(params.from && { "checkIn[$gte]": params.from }),
|
|
508
|
+
...(params.to && { "checkIn[$lte]": params.to }),
|
|
509
|
+
});
|
|
510
|
+
const results = (resData.results || []).map((r) => ({
|
|
511
|
+
guest: r.guest?.fullName || "Unknown",
|
|
512
|
+
listing: r.listing?.title || "Unknown",
|
|
513
|
+
checkIn: r.checkIn?.slice(0, 10),
|
|
514
|
+
checkOut: r.checkOut?.slice(0, 10),
|
|
515
|
+
status: r.status,
|
|
516
|
+
totalPaid: r.money?.totalPaid,
|
|
517
|
+
hostPayout: r.money?.hostPayout,
|
|
518
|
+
commission: r.money?.commission,
|
|
519
|
+
currency: r.money?.currency,
|
|
520
|
+
}));
|
|
521
|
+
const totalRevenue = results.reduce((sum, r) => sum + (r.totalPaid || 0), 0);
|
|
522
|
+
const totalPayout = results.reduce((sum, r) => sum + (r.hostPayout || 0), 0);
|
|
523
|
+
return {
|
|
524
|
+
content: [{ type: "text", text: JSON.stringify({ summary: { totalRevenue, totalPayout, count: results.length }, results }, null, 2) }],
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
// Tool 13: Get Expenses
|
|
530
|
+
server.tool(
|
|
531
|
+
"get_expenses",
|
|
532
|
+
"Fetch operational expenses tracked in Guesty.",
|
|
533
|
+
{
|
|
534
|
+
listingId: z.string().optional().describe("Filter by listing ID"),
|
|
535
|
+
from: z.string().optional().describe("Start date (YYYY-MM-DD)"),
|
|
536
|
+
to: z.string().optional().describe("End date (YYYY-MM-DD)"),
|
|
537
|
+
limit: z.number().optional().default(25).describe("Max results"),
|
|
538
|
+
},
|
|
539
|
+
async (params) => {
|
|
540
|
+
const queryParams = { limit: params.limit };
|
|
541
|
+
if (params.listingId) queryParams.listingId = params.listingId;
|
|
542
|
+
if (params.from) queryParams["from"] = params.from;
|
|
543
|
+
if (params.to) queryParams["to"] = params.to;
|
|
544
|
+
|
|
545
|
+
// Expenses endpoint may not be available on all Guesty plans
|
|
546
|
+
let data;
|
|
547
|
+
try {
|
|
548
|
+
data = await guestyGet("/expenses", queryParams);
|
|
549
|
+
} catch (e) {
|
|
550
|
+
// Fall back to listing expenses if main endpoint not available
|
|
551
|
+
return { content: [{ type: "text", text: JSON.stringify({ note: "Expenses endpoint not available on your Guesty plan. Use get_financials for reservation-based financial data." }, null, 2) }] };
|
|
552
|
+
}
|
|
553
|
+
const expenses = (data.results || []).map((e) => ({
|
|
554
|
+
id: e._id,
|
|
555
|
+
title: e.title,
|
|
556
|
+
amount: e.amount,
|
|
557
|
+
currency: e.currency,
|
|
558
|
+
category: e.category,
|
|
559
|
+
listing: e.listing?.title || "Unknown",
|
|
560
|
+
date: e.date?.slice(0, 10),
|
|
561
|
+
vendor: e.vendor,
|
|
562
|
+
}));
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
content: [{
|
|
566
|
+
type: "text",
|
|
567
|
+
text: JSON.stringify({ total: data.count, expenses }, null, 2),
|
|
568
|
+
}],
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
// Tool 14: Get Channels (Connected OTAs)
|
|
574
|
+
server.tool(
|
|
575
|
+
"get_channels",
|
|
576
|
+
"List connected booking channels (Airbnb, VRBO, Booking.com, etc.) and their status.",
|
|
577
|
+
{
|
|
578
|
+
listingId: z.string().optional().describe("Filter by listing ID to see which channels a property is on"),
|
|
579
|
+
},
|
|
580
|
+
async (params) => {
|
|
581
|
+
if (params.listingId) {
|
|
582
|
+
const listing = await guestyGet(`/listings/${params.listingId}`);
|
|
583
|
+
const channels = (listing.integrations || []).map((i) => ({
|
|
584
|
+
channel: i.platform,
|
|
585
|
+
externalId: i.externalId,
|
|
586
|
+
externalUrl: i.externalUrl,
|
|
587
|
+
status: i.status,
|
|
588
|
+
}));
|
|
589
|
+
return {
|
|
590
|
+
content: [{ type: "text", text: JSON.stringify({ listing: listing.title, channels }, null, 2) }],
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
// Get all listings with their channel info
|
|
594
|
+
const data = await guestyGet("/listings", { limit: 50 });
|
|
595
|
+
const listings = (data.results || []).map((l) => ({
|
|
596
|
+
id: l._id,
|
|
597
|
+
title: l.title,
|
|
598
|
+
nickname: l.nickname,
|
|
599
|
+
channels: (l.integrations || []).map((i) => i.platform).join(", ") || "none",
|
|
600
|
+
active: l.active,
|
|
601
|
+
}));
|
|
602
|
+
return {
|
|
603
|
+
content: [{ type: "text", text: JSON.stringify({ total: data.count, listings }, null, 2) }],
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
// Tool 15: Get Tasks (Cleaning/Maintenance)
|
|
609
|
+
server.tool(
|
|
610
|
+
"get_tasks",
|
|
611
|
+
"Fetch cleaning and maintenance tasks from Guesty.",
|
|
612
|
+
{
|
|
613
|
+
listingId: z.string().optional().describe("Filter by listing ID"),
|
|
614
|
+
status: z.string().optional().describe("Filter by status: pending, confirmed, completed, canceled"),
|
|
615
|
+
limit: z.number().optional().default(25).describe("Max results (minimum 25 per Guesty API)"),
|
|
616
|
+
},
|
|
617
|
+
async (params) => {
|
|
618
|
+
const queryParams = {
|
|
619
|
+
limit: Math.max(params.limit, 25),
|
|
620
|
+
columns: "id,status,type,listingId,scheduledFor,assignee,description",
|
|
621
|
+
};
|
|
622
|
+
if (params.listingId) queryParams.listingId = params.listingId;
|
|
623
|
+
if (params.status) queryParams.status = params.status;
|
|
624
|
+
|
|
625
|
+
const data = await guestyGet("/tasks-open-api/tasks", queryParams);
|
|
626
|
+
const tasks = (data.data || data.results || []).map((t) => ({
|
|
627
|
+
id: t._id || t.id,
|
|
628
|
+
type: t.type,
|
|
629
|
+
status: t.status,
|
|
630
|
+
listingId: t.listingId,
|
|
631
|
+
assignee: t.assignee?.fullName || t.assignee || "Unassigned",
|
|
632
|
+
scheduledFor: t.scheduledFor?.slice?.(0, 10) || t.scheduledFor,
|
|
633
|
+
description: t.description?.slice?.(0, 200) || t.description,
|
|
634
|
+
}));
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
content: [{ type: "text", text: JSON.stringify({ total: data.count || tasks.length, tasks }, null, 2) }],
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
// ============ V3 TOOLS ============
|
|
643
|
+
|
|
644
|
+
// Tool 16: Get Photos
|
|
645
|
+
server.tool(
|
|
646
|
+
"get_photos",
|
|
647
|
+
"Fetch photos for a specific listing including URLs, captions, and sort order.",
|
|
648
|
+
{
|
|
649
|
+
listingId: z.string().describe("The listing ID to get photos for"),
|
|
650
|
+
},
|
|
651
|
+
async (params) => {
|
|
652
|
+
const data = await guestyGet(`/listings/${params.listingId}`);
|
|
653
|
+
const photos = (data.pictures || []).map((p) => ({
|
|
654
|
+
url: p.original || p.thumbnail,
|
|
655
|
+
caption: p.caption || "",
|
|
656
|
+
sortOrder: p.sortOrder,
|
|
657
|
+
}));
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
content: [{ type: "text", text: JSON.stringify({ listing: params.listingId, photoCount: photos.length, photos }, null, 2) }],
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
// Tool 17: Update Photos
|
|
666
|
+
server.tool(
|
|
667
|
+
"update_photos",
|
|
668
|
+
"Replace or reorder photos for a listing. Provide the full array of photos in desired order.",
|
|
669
|
+
{
|
|
670
|
+
listingId: z.string().describe("The listing ID to update photos for"),
|
|
671
|
+
photos: z.array(z.object({
|
|
672
|
+
url: z.string().describe("Photo URL"),
|
|
673
|
+
caption: z.string().optional().describe("Photo caption"),
|
|
674
|
+
})).describe("Array of photo objects with url and optional caption"),
|
|
675
|
+
},
|
|
676
|
+
async (params) => {
|
|
677
|
+
const data = await guestyPut(`/listings/${params.listingId}`, {
|
|
678
|
+
pictures: params.photos,
|
|
679
|
+
});
|
|
680
|
+
return { content: [{ type: "text", text: `Photos updated for listing ${params.listingId}. ${params.photos.length} photos set.` }] };
|
|
681
|
+
}
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
// Tool 18: Get Calendar Blocks
|
|
685
|
+
server.tool(
|
|
686
|
+
"get_calendar_blocks",
|
|
687
|
+
"Get blocked dates and their reasons for a listing over a date range.",
|
|
688
|
+
{
|
|
689
|
+
listingId: z.string().describe("The listing ID"),
|
|
690
|
+
from: z.string().describe("Start date (YYYY-MM-DD)"),
|
|
691
|
+
to: z.string().describe("End date (YYYY-MM-DD)"),
|
|
692
|
+
},
|
|
693
|
+
async (params) => {
|
|
694
|
+
const data = await guestyGet(`/listings/${params.listingId}/calendar`, {
|
|
695
|
+
from: params.from,
|
|
696
|
+
to: params.to,
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
const blockedDays = (data.days || [])
|
|
700
|
+
.filter((d) => d.status !== "available")
|
|
701
|
+
.map((d) => ({
|
|
702
|
+
date: d.date,
|
|
703
|
+
blockReason: d.blockReason || d.note || "unknown",
|
|
704
|
+
status: d.status,
|
|
705
|
+
}));
|
|
706
|
+
|
|
707
|
+
return {
|
|
708
|
+
content: [{ type: "text", text: JSON.stringify({ listing: params.listingId, blockedCount: blockedDays.length, blockedDays }, null, 2) }],
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
// Tool 19: Create Expense
|
|
714
|
+
server.tool(
|
|
715
|
+
"create_expense",
|
|
716
|
+
"Create a new expense record for a listing in Guesty.",
|
|
717
|
+
{
|
|
718
|
+
listingId: z.string().describe("The listing ID the expense is for"),
|
|
719
|
+
title: z.string().describe("Expense title/description"),
|
|
720
|
+
amount: z.number().describe("Expense amount"),
|
|
721
|
+
currency: z.string().optional().default("USD").describe("Currency code (default USD)"),
|
|
722
|
+
category: z.string().optional().describe("Expense category (e.g., cleaning, maintenance, supplies)"),
|
|
723
|
+
vendor: z.string().optional().describe("Vendor/supplier name"),
|
|
724
|
+
date: z.string().optional().describe("Expense date (YYYY-MM-DD)"),
|
|
725
|
+
},
|
|
726
|
+
async (params) => {
|
|
727
|
+
const body = {
|
|
728
|
+
listingId: params.listingId,
|
|
729
|
+
title: params.title,
|
|
730
|
+
amount: params.amount,
|
|
731
|
+
currency: params.currency,
|
|
732
|
+
};
|
|
733
|
+
if (params.category) body.category = params.category;
|
|
734
|
+
if (params.vendor) body.vendor = params.vendor;
|
|
735
|
+
if (params.date) body.date = params.date;
|
|
736
|
+
|
|
737
|
+
const data = await guestyPost("/expenses", body);
|
|
738
|
+
return {
|
|
739
|
+
content: [{
|
|
740
|
+
type: "text",
|
|
741
|
+
text: JSON.stringify({
|
|
742
|
+
success: true,
|
|
743
|
+
expenseId: data._id,
|
|
744
|
+
title: params.title,
|
|
745
|
+
amount: `${params.currency} ${params.amount}`,
|
|
746
|
+
listing: params.listingId,
|
|
747
|
+
}, null, 2),
|
|
748
|
+
}],
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
// Tool 20: Get Guests
|
|
754
|
+
server.tool(
|
|
755
|
+
"get_guests",
|
|
756
|
+
"Fetch guest database/profiles. Search by name or email.",
|
|
757
|
+
{
|
|
758
|
+
limit: z.number().optional().default(10).describe("Max results (default 10)"),
|
|
759
|
+
skip: z.number().optional().describe("Offset for pagination"),
|
|
760
|
+
query: z.string().optional().describe("Search by guest name or email"),
|
|
761
|
+
},
|
|
762
|
+
async (params) => {
|
|
763
|
+
const queryParams = { limit: params.limit };
|
|
764
|
+
if (params.skip) queryParams.skip = params.skip;
|
|
765
|
+
if (params.query) queryParams.q = params.query;
|
|
766
|
+
|
|
767
|
+
const data = await guestyGet("/guests", queryParams);
|
|
768
|
+
const guests = (data.results || []).map((g) => ({
|
|
769
|
+
id: g._id,
|
|
770
|
+
fullName: g.fullName,
|
|
771
|
+
email: g.email,
|
|
772
|
+
phone: g.phone,
|
|
773
|
+
reservationCount: g.reservationsCount,
|
|
774
|
+
createdAt: g.createdAt?.slice(0, 10),
|
|
775
|
+
}));
|
|
776
|
+
|
|
777
|
+
return {
|
|
778
|
+
content: [{ type: "text", text: JSON.stringify({ total: data.count, guests }, null, 2) }],
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
// Tool 21: Get Guest by ID
|
|
784
|
+
server.tool(
|
|
785
|
+
"get_guest_by_id",
|
|
786
|
+
"Get detailed guest profile by guest ID.",
|
|
787
|
+
{
|
|
788
|
+
guestId: z.string().describe("The guest ID"),
|
|
789
|
+
},
|
|
790
|
+
async (params) => {
|
|
791
|
+
const data = await guestyGet(`/guests/${params.guestId}`);
|
|
792
|
+
const guest = {
|
|
793
|
+
id: data._id,
|
|
794
|
+
fullName: data.fullName,
|
|
795
|
+
firstName: data.firstName,
|
|
796
|
+
lastName: data.lastName,
|
|
797
|
+
email: data.email,
|
|
798
|
+
phone: data.phone,
|
|
799
|
+
address: data.address,
|
|
800
|
+
reservationsCount: data.reservationsCount,
|
|
801
|
+
notes: data.notes,
|
|
802
|
+
tags: data.tags,
|
|
803
|
+
createdAt: data.createdAt?.slice(0, 10),
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
return {
|
|
807
|
+
content: [{ type: "text", text: JSON.stringify(guest, null, 2) }],
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
// Tool 22: Update Listing
|
|
813
|
+
server.tool(
|
|
814
|
+
"update_listing",
|
|
815
|
+
"Update listing details such as title, description, amenities, min nights, and max guests.",
|
|
816
|
+
{
|
|
817
|
+
listingId: z.string().describe("The listing ID to update"),
|
|
818
|
+
title: z.string().optional().describe("New listing title"),
|
|
819
|
+
publicDescription: z.string().optional().describe("Public-facing description"),
|
|
820
|
+
privateDescription: z.string().optional().describe("Private/internal description"),
|
|
821
|
+
amenities: z.array(z.string()).optional().describe("Array of amenity strings"),
|
|
822
|
+
minNights: z.number().optional().describe("Minimum night stay"),
|
|
823
|
+
maxGuests: z.number().optional().describe("Maximum number of guests"),
|
|
824
|
+
},
|
|
825
|
+
async (params) => {
|
|
826
|
+
const body = {};
|
|
827
|
+
if (params.title) body.title = params.title;
|
|
828
|
+
if (params.publicDescription) body.publicDescription = { summary: params.publicDescription };
|
|
829
|
+
if (params.privateDescription) body.privateDescription = params.privateDescription;
|
|
830
|
+
if (params.amenities) body.amenities = params.amenities;
|
|
831
|
+
if (params.minNights) body.minNights = params.minNights;
|
|
832
|
+
if (params.maxGuests) body.accommodates = params.maxGuests;
|
|
833
|
+
|
|
834
|
+
const data = await guestyPut(`/listings/${params.listingId}`, body);
|
|
835
|
+
const updated = Object.keys(body).join(", ");
|
|
836
|
+
return { content: [{ type: "text", text: `Listing ${params.listingId} updated. Fields changed: ${updated}` }] };
|
|
837
|
+
}
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
// Tool 23: Get Automation Rules
|
|
841
|
+
server.tool(
|
|
842
|
+
"get_automation_rules",
|
|
843
|
+
"List automation and workflow rules configured in Guesty.",
|
|
844
|
+
{
|
|
845
|
+
limit: z.number().optional().default(25).describe("Max results (default 25)"),
|
|
846
|
+
},
|
|
847
|
+
async (params) => {
|
|
848
|
+
// Automations endpoint may not be available on Open API v1
|
|
849
|
+
try {
|
|
850
|
+
const data = await guestyGet("/automations", { limit: params.limit });
|
|
851
|
+
const automations = (data.results || []).map((a) => ({
|
|
852
|
+
id: a._id,
|
|
853
|
+
title: a.title,
|
|
854
|
+
active: a.active,
|
|
855
|
+
trigger: a.trigger,
|
|
856
|
+
createdAt: a.createdAt?.slice(0, 10),
|
|
857
|
+
}));
|
|
858
|
+
return { content: [{ type: "text", text: JSON.stringify({ total: data.count, automations }, null, 2) }] };
|
|
859
|
+
} catch (e) {
|
|
860
|
+
return { content: [{ type: "text", text: JSON.stringify({ note: "Automations endpoint not available on your Guesty plan or API version. Check Guesty dashboard for automation rules." }, null, 2) }] };
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
);
|
|
864
|
+
|
|
865
|
+
// Tool 24: Create Task
|
|
866
|
+
server.tool(
|
|
867
|
+
"create_task",
|
|
868
|
+
"Create a cleaning or maintenance task for a listing.",
|
|
869
|
+
{
|
|
870
|
+
listingId: z.string().describe("The listing ID the task is for"),
|
|
871
|
+
type: z.string().describe("Task type: cleaning or maintenance"),
|
|
872
|
+
scheduledFor: z.string().describe("Scheduled date (YYYY-MM-DD)"),
|
|
873
|
+
assigneeId: z.string().optional().describe("Assignee user ID"),
|
|
874
|
+
description: z.string().optional().describe("Task description/notes"),
|
|
875
|
+
},
|
|
876
|
+
async (params) => {
|
|
877
|
+
const body = {
|
|
878
|
+
listingId: params.listingId,
|
|
879
|
+
type: params.type,
|
|
880
|
+
scheduledFor: params.scheduledFor,
|
|
881
|
+
};
|
|
882
|
+
if (params.assigneeId) body.assigneeId = params.assigneeId;
|
|
883
|
+
if (params.description) body.description = params.description;
|
|
884
|
+
|
|
885
|
+
const data = await guestyPost("/tasks-open-api/tasks", body);
|
|
886
|
+
return {
|
|
887
|
+
content: [{
|
|
888
|
+
type: "text",
|
|
889
|
+
text: JSON.stringify({
|
|
890
|
+
success: true,
|
|
891
|
+
taskId: data._id,
|
|
892
|
+
type: params.type,
|
|
893
|
+
listing: params.listingId,
|
|
894
|
+
scheduledFor: params.scheduledFor,
|
|
895
|
+
}, null, 2),
|
|
896
|
+
}],
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
// Tool 25: Update Reservation
|
|
902
|
+
server.tool(
|
|
903
|
+
"update_reservation",
|
|
904
|
+
"Update reservation details such as status, dates, guest info, or add notes.",
|
|
905
|
+
{
|
|
906
|
+
reservationId: z.string().describe("The reservation ID to update"),
|
|
907
|
+
status: z.string().optional().describe("New status: confirmed, canceled, inquiry, etc."),
|
|
908
|
+
checkIn: z.string().optional().describe("New check-in date (YYYY-MM-DD)"),
|
|
909
|
+
checkOut: z.string().optional().describe("New check-out date (YYYY-MM-DD)"),
|
|
910
|
+
guestName: z.string().optional().describe("Updated guest full name"),
|
|
911
|
+
guestEmail: z.string().optional().describe("Updated guest email"),
|
|
912
|
+
note: z.string().optional().describe("Add a note to the reservation"),
|
|
913
|
+
},
|
|
914
|
+
async (params) => {
|
|
915
|
+
const body = {};
|
|
916
|
+
if (params.status) body.status = params.status;
|
|
917
|
+
if (params.checkIn) body.checkInDateLocalized = params.checkIn;
|
|
918
|
+
if (params.checkOut) body.checkOutDateLocalized = params.checkOut;
|
|
919
|
+
if (params.guestName || params.guestEmail) {
|
|
920
|
+
body.guest = {};
|
|
921
|
+
if (params.guestName) body.guest.fullName = params.guestName;
|
|
922
|
+
if (params.guestEmail) body.guest.email = params.guestEmail;
|
|
923
|
+
}
|
|
924
|
+
if (params.note) body.note = params.note;
|
|
925
|
+
|
|
926
|
+
const data = await guestyPut(`/reservations/${params.reservationId}`, body);
|
|
927
|
+
const updated = Object.keys(body).join(", ");
|
|
928
|
+
return { content: [{ type: "text", text: `Reservation ${params.reservationId} updated. Fields changed: ${updated}` }] };
|
|
929
|
+
}
|
|
930
|
+
);
|
|
931
|
+
|
|
932
|
+
// Tool 26: Get Supported Languages
|
|
933
|
+
server.tool(
|
|
934
|
+
"get_supported_languages",
|
|
935
|
+
"Get supported languages configured for a listing.",
|
|
936
|
+
{
|
|
937
|
+
listingId: z.string().describe("The listing ID"),
|
|
938
|
+
},
|
|
939
|
+
async (params) => {
|
|
940
|
+
// Try supported-languages endpoint, fall back to listing data
|
|
941
|
+
let data;
|
|
942
|
+
try {
|
|
943
|
+
data = await guestyGet(`/listings/${params.listingId}/supported-languages`);
|
|
944
|
+
} catch (e) {
|
|
945
|
+
// Fall back to extracting language info from listing
|
|
946
|
+
const listing = await guestyGet(`/listings/${params.listingId}`);
|
|
947
|
+
data = { languages: listing.languages || listing.supportedLanguages || [], note: "Extracted from listing data" };
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
content: [{ type: "text", text: JSON.stringify({ listing: params.listingId, languages: data }, null, 2) }],
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
);
|
|
954
|
+
|
|
955
|
+
// Tool 27: Search Reservations
|
|
956
|
+
server.tool(
|
|
957
|
+
"search_reservations",
|
|
958
|
+
"Search reservations by guest name, email, or confirmation code.",
|
|
959
|
+
{
|
|
960
|
+
query: z.string().describe("Search query — guest name, email, or confirmation code"),
|
|
961
|
+
limit: z.number().optional().default(10).describe("Max results (default 10)"),
|
|
962
|
+
},
|
|
963
|
+
async (params) => {
|
|
964
|
+
const data = await guestyGet("/reservations", {
|
|
965
|
+
limit: params.limit,
|
|
966
|
+
q: params.query,
|
|
967
|
+
sort: "checkIn",
|
|
968
|
+
order: "desc",
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
const results = (data.results || []).map((r) => ({
|
|
972
|
+
id: r._id,
|
|
973
|
+
confirmationCode: r.confirmationCode,
|
|
974
|
+
guest: r.guest?.fullName || "Unknown",
|
|
975
|
+
guestEmail: r.guest?.email || "",
|
|
976
|
+
checkIn: r.checkIn?.slice(0, 10),
|
|
977
|
+
checkOut: r.checkOut?.slice(0, 10),
|
|
978
|
+
status: r.status,
|
|
979
|
+
listing: r.listing?.title || "Unknown",
|
|
980
|
+
listingId: r.listingId,
|
|
981
|
+
totalPaid: r.money?.totalPaid,
|
|
982
|
+
}));
|
|
983
|
+
|
|
984
|
+
return {
|
|
985
|
+
content: [{ type: "text", text: JSON.stringify({ total: data.count, results }, null, 2) }],
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
);
|
|
989
|
+
|
|
990
|
+
// Tool 28: Get Listing Occupancy
|
|
991
|
+
server.tool(
|
|
992
|
+
"get_listing_occupancy",
|
|
993
|
+
"Calculate occupancy rate for a listing over a date range.",
|
|
994
|
+
{
|
|
995
|
+
listingId: z.string().describe("The listing ID"),
|
|
996
|
+
from: z.string().describe("Start date (YYYY-MM-DD)"),
|
|
997
|
+
to: z.string().describe("End date (YYYY-MM-DD)"),
|
|
998
|
+
},
|
|
999
|
+
async (params) => {
|
|
1000
|
+
const data = await guestyGet(`/listings/${params.listingId}/calendar`, {
|
|
1001
|
+
from: params.from,
|
|
1002
|
+
to: params.to,
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
const days = data.days || [];
|
|
1006
|
+
const totalDays = days.length;
|
|
1007
|
+
let bookedDays = 0;
|
|
1008
|
+
let blockedDays = 0;
|
|
1009
|
+
let availableDays = 0;
|
|
1010
|
+
|
|
1011
|
+
days.forEach((d) => {
|
|
1012
|
+
if (d.status === "booked" || d.status === "reserved") bookedDays++;
|
|
1013
|
+
else if (d.status === "unavailable" || d.status === "blocked") blockedDays++;
|
|
1014
|
+
else availableDays++;
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
const occupancyRate = totalDays > 0 ? Math.round((bookedDays / totalDays) * 10000) / 100 : 0;
|
|
1018
|
+
|
|
1019
|
+
return {
|
|
1020
|
+
content: [{
|
|
1021
|
+
type: "text",
|
|
1022
|
+
text: JSON.stringify({
|
|
1023
|
+
listing: params.listingId,
|
|
1024
|
+
from: params.from,
|
|
1025
|
+
to: params.to,
|
|
1026
|
+
totalDays,
|
|
1027
|
+
bookedDays,
|
|
1028
|
+
blockedDays,
|
|
1029
|
+
availableDays,
|
|
1030
|
+
occupancyRate: `${occupancyRate}%`,
|
|
1031
|
+
}, null, 2),
|
|
1032
|
+
}],
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
);
|
|
1036
|
+
|
|
1037
|
+
// Tool 29: Get Revenue Summary
|
|
1038
|
+
server.tool(
|
|
1039
|
+
"get_revenue_summary",
|
|
1040
|
+
"Get aggregated revenue summary across all or specific listings for a date range.",
|
|
1041
|
+
{
|
|
1042
|
+
from: z.string().optional().describe("Start date (YYYY-MM-DD)"),
|
|
1043
|
+
to: z.string().optional().describe("End date (YYYY-MM-DD)"),
|
|
1044
|
+
listingId: z.string().optional().describe("Filter by listing ID"),
|
|
1045
|
+
},
|
|
1046
|
+
async (params) => {
|
|
1047
|
+
const queryParams = {
|
|
1048
|
+
limit: 100,
|
|
1049
|
+
fields: "money nightsCount listing checkIn checkOut status",
|
|
1050
|
+
sort: "checkIn",
|
|
1051
|
+
order: "desc",
|
|
1052
|
+
status: "confirmed",
|
|
1053
|
+
};
|
|
1054
|
+
if (params.listingId) queryParams.listingId = params.listingId;
|
|
1055
|
+
if (params.from) queryParams["checkIn[$gte]"] = params.from;
|
|
1056
|
+
if (params.to) queryParams["checkIn[$lte]"] = params.to;
|
|
1057
|
+
|
|
1058
|
+
const data = await guestyGet("/reservations", queryParams);
|
|
1059
|
+
const reservations = data.results || [];
|
|
1060
|
+
|
|
1061
|
+
let totalRevenue = 0;
|
|
1062
|
+
let totalPayout = 0;
|
|
1063
|
+
let totalNights = 0;
|
|
1064
|
+
|
|
1065
|
+
reservations.forEach((r) => {
|
|
1066
|
+
totalRevenue += r.money?.totalPaid || 0;
|
|
1067
|
+
totalPayout += r.money?.hostPayout || 0;
|
|
1068
|
+
totalNights += r.nightsCount || 0;
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
const reservationCount = reservations.length;
|
|
1072
|
+
const averageNightlyRate = totalNights > 0 ? Math.round((totalRevenue / totalNights) * 100) / 100 : 0;
|
|
1073
|
+
|
|
1074
|
+
return {
|
|
1075
|
+
content: [{
|
|
1076
|
+
type: "text",
|
|
1077
|
+
text: JSON.stringify({
|
|
1078
|
+
totalRevenue,
|
|
1079
|
+
totalPayout,
|
|
1080
|
+
averageNightlyRate,
|
|
1081
|
+
totalNights,
|
|
1082
|
+
reservationCount,
|
|
1083
|
+
period: {
|
|
1084
|
+
from: params.from || "all-time",
|
|
1085
|
+
to: params.to || "present",
|
|
1086
|
+
},
|
|
1087
|
+
listingId: params.listingId || "all",
|
|
1088
|
+
}, null, 2),
|
|
1089
|
+
}],
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
);
|
|
1093
|
+
|
|
1094
|
+
// Start server
|
|
1095
|
+
const transport = new StdioServerTransport();
|
|
1096
|
+
await server.connect(transport);
|