guesty-mcp-server 0.3.0 → 0.4.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/package.json +2 -1
- package/server.json +36 -0
- package/smithery.yaml +17 -0
- package/src/server.js +278 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guesty-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "MCP Server for Guesty Property Management - The first Guesty MCP server",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"url": "https://github.com/DLJRealty/guesty-mcp-server.git"
|
|
31
31
|
},
|
|
32
32
|
"homepage": "https://github.com/DLJRealty/guesty-mcp-server",
|
|
33
|
+
"mcpName": "io.github.dljrealty/guesty",
|
|
33
34
|
"engines": {
|
|
34
35
|
"node": ">=18.0.0"
|
|
35
36
|
}
|
package/server.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.dljrealty/guesty",
|
|
4
|
+
"description": "First-ever MCP server for Guesty property management. 38 tools for reservations, listings, guests, financials, messaging, tasks, webhooks, pricing, and more.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/DLJRealty/guesty-mcp-server",
|
|
7
|
+
"source": "github"
|
|
8
|
+
},
|
|
9
|
+
"version": "0.4.0",
|
|
10
|
+
"packages": [
|
|
11
|
+
{
|
|
12
|
+
"registryType": "npm",
|
|
13
|
+
"identifier": "guesty-mcp-server",
|
|
14
|
+
"version": "0.4.0",
|
|
15
|
+
"transport": {
|
|
16
|
+
"type": "stdio"
|
|
17
|
+
},
|
|
18
|
+
"environmentVariables": [
|
|
19
|
+
{
|
|
20
|
+
"description": "Guesty OAuth2 Client ID",
|
|
21
|
+
"isRequired": true,
|
|
22
|
+
"format": "string",
|
|
23
|
+
"isSecret": true,
|
|
24
|
+
"name": "GUESTY_CLIENT_ID"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"description": "Guesty OAuth2 Client Secret",
|
|
28
|
+
"isRequired": true,
|
|
29
|
+
"format": "string",
|
|
30
|
+
"isSecret": true,
|
|
31
|
+
"name": "GUESTY_CLIENT_SECRET"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
package/smithery.yaml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
startCommand:
|
|
2
|
+
type: stdio
|
|
3
|
+
configSchema:
|
|
4
|
+
type: object
|
|
5
|
+
required:
|
|
6
|
+
- guestyClientId
|
|
7
|
+
- guestyClientSecret
|
|
8
|
+
properties:
|
|
9
|
+
guestyClientId:
|
|
10
|
+
type: string
|
|
11
|
+
description: Guesty OAuth2 Client ID
|
|
12
|
+
guestyClientSecret:
|
|
13
|
+
type: string
|
|
14
|
+
description: Guesty OAuth2 Client Secret
|
|
15
|
+
commandFunction:
|
|
16
|
+
|-
|
|
17
|
+
(config) => ({ command: 'npx', args: ['-y', 'guesty-mcp-server'], env: { GUESTY_CLIENT_ID: config.guestyClientId, GUESTY_CLIENT_SECRET: config.guestyClientSecret } })
|
package/src/server.js
CHANGED
|
@@ -103,10 +103,28 @@ async function guestyPut(path, body, retries = 2) {
|
|
|
103
103
|
return res.json();
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
async function guestyDelete(path, retries = 2) {
|
|
107
|
+
const token = await getToken();
|
|
108
|
+
const res = await fetch(`${GUESTY_API_BASE}${path}`, {
|
|
109
|
+
method: "DELETE",
|
|
110
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (res.status === 429 && retries > 0) {
|
|
114
|
+
const wait = Math.min(parseInt(res.headers.get("retry-after") || "5", 10), 30) * 1000;
|
|
115
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
116
|
+
return guestyDelete(path, retries - 1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!res.ok) throw new Error(`Guesty API error ${res.status}: ${await res.text()}`);
|
|
120
|
+
const text = await res.text();
|
|
121
|
+
return text ? JSON.parse(text) : { success: true };
|
|
122
|
+
}
|
|
123
|
+
|
|
106
124
|
// Create MCP Server
|
|
107
125
|
const server = new McpServer({
|
|
108
126
|
name: "guesty-mcp-server",
|
|
109
|
-
version: "0.
|
|
127
|
+
version: "0.4.0",
|
|
110
128
|
});
|
|
111
129
|
|
|
112
130
|
// Tool 1: Get Reservations
|
|
@@ -734,19 +752,23 @@ server.tool(
|
|
|
734
752
|
if (params.vendor) body.vendor = params.vendor;
|
|
735
753
|
if (params.date) body.date = params.date;
|
|
736
754
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
755
|
+
try {
|
|
756
|
+
const data = await guestyPost("/expenses", body);
|
|
757
|
+
return {
|
|
758
|
+
content: [{
|
|
759
|
+
type: "text",
|
|
760
|
+
text: JSON.stringify({
|
|
761
|
+
success: true,
|
|
762
|
+
expenseId: data._id,
|
|
763
|
+
title: params.title,
|
|
764
|
+
amount: `${params.currency} ${params.amount}`,
|
|
765
|
+
listing: params.listingId,
|
|
766
|
+
}, null, 2),
|
|
767
|
+
}],
|
|
768
|
+
};
|
|
769
|
+
} catch (e) {
|
|
770
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Expenses endpoint not available on your Guesty plan.", details: e.message }, null, 2) }] };
|
|
771
|
+
}
|
|
750
772
|
}
|
|
751
773
|
);
|
|
752
774
|
|
|
@@ -1091,6 +1113,248 @@ server.tool(
|
|
|
1091
1113
|
}
|
|
1092
1114
|
);
|
|
1093
1115
|
|
|
1116
|
+
// Tool 30: Get Webhooks
|
|
1117
|
+
server.tool(
|
|
1118
|
+
"get_webhooks",
|
|
1119
|
+
"List all registered webhooks for your Guesty account.",
|
|
1120
|
+
{
|
|
1121
|
+
limit: z.number().optional().default(25).describe("Max results"),
|
|
1122
|
+
},
|
|
1123
|
+
async (params) => {
|
|
1124
|
+
try {
|
|
1125
|
+
const data = await guestyGet("/webhooks", { limit: params.limit });
|
|
1126
|
+
const webhooks = (data.results || data || []).map((w) => ({
|
|
1127
|
+
id: w._id,
|
|
1128
|
+
url: w.url,
|
|
1129
|
+
events: w.events,
|
|
1130
|
+
active: w.active,
|
|
1131
|
+
createdAt: w.createdAt?.slice(0, 10),
|
|
1132
|
+
}));
|
|
1133
|
+
return { content: [{ type: "text", text: JSON.stringify({ total: webhooks.length, webhooks }, null, 2) }] };
|
|
1134
|
+
} catch (e) {
|
|
1135
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Webhooks endpoint not available.", details: e.message }, null, 2) }] };
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
);
|
|
1139
|
+
|
|
1140
|
+
// Tool 31: Create Webhook
|
|
1141
|
+
server.tool(
|
|
1142
|
+
"create_webhook",
|
|
1143
|
+
"Register a new webhook to receive event notifications from Guesty.",
|
|
1144
|
+
{
|
|
1145
|
+
url: z.string().describe("The URL to receive webhook events"),
|
|
1146
|
+
events: z.array(z.string()).describe("Events to subscribe to (e.g., 'reservation.created', 'reservation.updated', 'guest.checked_in')"),
|
|
1147
|
+
secret: z.string().optional().describe("Webhook signing secret for verification"),
|
|
1148
|
+
},
|
|
1149
|
+
async (params) => {
|
|
1150
|
+
try {
|
|
1151
|
+
const body = { url: params.url, events: params.events };
|
|
1152
|
+
if (params.secret) body.secret = params.secret;
|
|
1153
|
+
const data = await guestyPost("/webhooks", body);
|
|
1154
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, webhookId: data._id, url: params.url, events: params.events }, null, 2) }] };
|
|
1155
|
+
} catch (e) {
|
|
1156
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Failed to create webhook.", details: e.message }, null, 2) }] };
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
);
|
|
1160
|
+
|
|
1161
|
+
// Tool 32: Delete Webhook
|
|
1162
|
+
server.tool(
|
|
1163
|
+
"delete_webhook",
|
|
1164
|
+
"Delete a registered webhook by ID.",
|
|
1165
|
+
{
|
|
1166
|
+
webhookId: z.string().describe("The webhook ID to delete"),
|
|
1167
|
+
},
|
|
1168
|
+
async (params) => {
|
|
1169
|
+
try {
|
|
1170
|
+
await guestyDelete(`/webhooks/${params.webhookId}`);
|
|
1171
|
+
return { content: [{ type: "text", text: `Webhook ${params.webhookId} deleted successfully.` }] };
|
|
1172
|
+
} catch (e) {
|
|
1173
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Failed to delete webhook.", details: e.message }, null, 2) }] };
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
// Tool 33: Get Custom Fields
|
|
1179
|
+
server.tool(
|
|
1180
|
+
"get_custom_fields",
|
|
1181
|
+
"Fetch custom fields configured for listings or reservations.",
|
|
1182
|
+
{
|
|
1183
|
+
entity: z.string().optional().default("listing").describe("Entity type: listing or reservation"),
|
|
1184
|
+
},
|
|
1185
|
+
async (params) => {
|
|
1186
|
+
try {
|
|
1187
|
+
const data = await guestyGet("/custom-fields", { entity: params.entity });
|
|
1188
|
+
const fields = (data.results || data || []).map((f) => ({
|
|
1189
|
+
id: f._id,
|
|
1190
|
+
key: f.key || f.fieldId,
|
|
1191
|
+
label: f.label || f.name,
|
|
1192
|
+
type: f.type,
|
|
1193
|
+
entity: f.entity,
|
|
1194
|
+
}));
|
|
1195
|
+
return { content: [{ type: "text", text: JSON.stringify({ total: fields.length, fields }, null, 2) }] };
|
|
1196
|
+
} catch (e) {
|
|
1197
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Custom fields endpoint not available.", details: e.message }, null, 2) }] };
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
);
|
|
1201
|
+
|
|
1202
|
+
// Tool 36: Get Account Info
|
|
1203
|
+
server.tool(
|
|
1204
|
+
"get_account_info",
|
|
1205
|
+
"Get current Guesty account information and subscription details.",
|
|
1206
|
+
{},
|
|
1207
|
+
async () => {
|
|
1208
|
+
try {
|
|
1209
|
+
const data = await guestyGet("/accounts/me");
|
|
1210
|
+
return {
|
|
1211
|
+
content: [{
|
|
1212
|
+
type: "text",
|
|
1213
|
+
text: JSON.stringify({
|
|
1214
|
+
id: data._id,
|
|
1215
|
+
name: data.name,
|
|
1216
|
+
email: data.email,
|
|
1217
|
+
company: data.companyName,
|
|
1218
|
+
timezone: data.timezone,
|
|
1219
|
+
currency: data.currency,
|
|
1220
|
+
plan: data.plan || data.subscription?.plan,
|
|
1221
|
+
}, null, 2),
|
|
1222
|
+
}],
|
|
1223
|
+
};
|
|
1224
|
+
} catch (e) {
|
|
1225
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Account info not available.", details: e.message }, null, 2) }] };
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
);
|
|
1229
|
+
|
|
1230
|
+
// Tool 37: Get Reservation Financials
|
|
1231
|
+
server.tool(
|
|
1232
|
+
"get_reservation_financials",
|
|
1233
|
+
"Get detailed financial breakdown for a specific reservation including payments, charges, and adjustments.",
|
|
1234
|
+
{
|
|
1235
|
+
reservationId: z.string().describe("The reservation ID"),
|
|
1236
|
+
},
|
|
1237
|
+
async (params) => {
|
|
1238
|
+
const data = await guestyGet(`/reservations/${params.reservationId}`, {
|
|
1239
|
+
fields: "money guest listing checkIn checkOut status confirmationCode",
|
|
1240
|
+
});
|
|
1241
|
+
const money = data.money || {};
|
|
1242
|
+
return {
|
|
1243
|
+
content: [{
|
|
1244
|
+
type: "text",
|
|
1245
|
+
text: JSON.stringify({
|
|
1246
|
+
confirmationCode: data.confirmationCode,
|
|
1247
|
+
guest: data.guest?.fullName,
|
|
1248
|
+
listing: data.listing?.title,
|
|
1249
|
+
checkIn: data.checkIn?.slice(0, 10),
|
|
1250
|
+
checkOut: data.checkOut?.slice(0, 10),
|
|
1251
|
+
status: data.status,
|
|
1252
|
+
financials: {
|
|
1253
|
+
totalPrice: money.totalPrice,
|
|
1254
|
+
totalPaid: money.totalPaid,
|
|
1255
|
+
balanceDue: money.balanceDue,
|
|
1256
|
+
hostPayout: money.hostPayout,
|
|
1257
|
+
commission: money.commission,
|
|
1258
|
+
cleaningFee: money.cleaningFee,
|
|
1259
|
+
channelCommission: money.channelCommission,
|
|
1260
|
+
currency: money.currency,
|
|
1261
|
+
payments: money.payments || [],
|
|
1262
|
+
},
|
|
1263
|
+
}, null, 2),
|
|
1264
|
+
}],
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
);
|
|
1268
|
+
|
|
1269
|
+
// Tool 38: Create Reservation Note
|
|
1270
|
+
server.tool(
|
|
1271
|
+
"create_reservation_note",
|
|
1272
|
+
"Add an internal note to a reservation visible only to the property management team.",
|
|
1273
|
+
{
|
|
1274
|
+
reservationId: z.string().describe("The reservation ID"),
|
|
1275
|
+
note: z.string().describe("Note text to add"),
|
|
1276
|
+
},
|
|
1277
|
+
async (params) => {
|
|
1278
|
+
try {
|
|
1279
|
+
const data = await guestyPost(`/reservations/${params.reservationId}/notes`, {
|
|
1280
|
+
body: params.note,
|
|
1281
|
+
});
|
|
1282
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, noteId: data._id, reservation: params.reservationId }, null, 2) }] };
|
|
1283
|
+
} catch (e) {
|
|
1284
|
+
// Fallback: try updating reservation with note field
|
|
1285
|
+
try {
|
|
1286
|
+
await guestyPut(`/reservations/${params.reservationId}`, { note: params.note });
|
|
1287
|
+
return { content: [{ type: "text", text: `Note added to reservation ${params.reservationId} via update.` }] };
|
|
1288
|
+
} catch (e2) {
|
|
1289
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Failed to add note.", details: e2.message }, null, 2) }] };
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
);
|
|
1294
|
+
|
|
1295
|
+
// Tool 39: Get Listing Pricing
|
|
1296
|
+
server.tool(
|
|
1297
|
+
"get_listing_pricing",
|
|
1298
|
+
"Get pricing details for a listing including base price, weekly/monthly discounts, and extra fees.",
|
|
1299
|
+
{
|
|
1300
|
+
listingId: z.string().describe("The listing ID"),
|
|
1301
|
+
},
|
|
1302
|
+
async (params) => {
|
|
1303
|
+
const data = await guestyGet(`/listings/${params.listingId}`, {
|
|
1304
|
+
fields: "prices terms financials title nickname",
|
|
1305
|
+
});
|
|
1306
|
+
return {
|
|
1307
|
+
content: [{
|
|
1308
|
+
type: "text",
|
|
1309
|
+
text: JSON.stringify({
|
|
1310
|
+
listing: data.title || data.nickname,
|
|
1311
|
+
pricing: {
|
|
1312
|
+
basePrice: data.prices?.basePrice,
|
|
1313
|
+
weeklyDiscount: data.prices?.weeklyPriceFactor,
|
|
1314
|
+
monthlyDiscount: data.prices?.monthlyPriceFactor,
|
|
1315
|
+
cleaningFee: data.prices?.cleaningFee,
|
|
1316
|
+
extraPersonFee: data.prices?.extraPersonFee,
|
|
1317
|
+
currency: data.prices?.currency,
|
|
1318
|
+
},
|
|
1319
|
+
terms: {
|
|
1320
|
+
minNights: data.terms?.minNights,
|
|
1321
|
+
maxNights: data.terms?.maxNights,
|
|
1322
|
+
cancellationPolicy: data.terms?.cancellationPolicy,
|
|
1323
|
+
},
|
|
1324
|
+
}, null, 2),
|
|
1325
|
+
}],
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
);
|
|
1329
|
+
|
|
1330
|
+
// Tool 40: Update Listing Pricing
|
|
1331
|
+
server.tool(
|
|
1332
|
+
"update_listing_pricing",
|
|
1333
|
+
"Update pricing for a listing — base price, cleaning fee, extra person fee, and discounts.",
|
|
1334
|
+
{
|
|
1335
|
+
listingId: z.string().describe("The listing ID"),
|
|
1336
|
+
basePrice: z.number().optional().describe("Nightly base price"),
|
|
1337
|
+
cleaningFee: z.number().optional().describe("Cleaning fee"),
|
|
1338
|
+
extraPersonFee: z.number().optional().describe("Fee per extra person"),
|
|
1339
|
+
weeklyPriceFactor: z.number().optional().describe("Weekly discount factor (e.g., 0.9 for 10% off)"),
|
|
1340
|
+
monthlyPriceFactor: z.number().optional().describe("Monthly discount factor (e.g., 0.8 for 20% off)"),
|
|
1341
|
+
currency: z.string().optional().describe("Currency code (e.g., USD)"),
|
|
1342
|
+
},
|
|
1343
|
+
async (params) => {
|
|
1344
|
+
const prices = {};
|
|
1345
|
+
if (params.basePrice !== undefined) prices.basePrice = params.basePrice;
|
|
1346
|
+
if (params.cleaningFee !== undefined) prices.cleaningFee = params.cleaningFee;
|
|
1347
|
+
if (params.extraPersonFee !== undefined) prices.extraPersonFee = params.extraPersonFee;
|
|
1348
|
+
if (params.weeklyPriceFactor !== undefined) prices.weeklyPriceFactor = params.weeklyPriceFactor;
|
|
1349
|
+
if (params.monthlyPriceFactor !== undefined) prices.monthlyPriceFactor = params.monthlyPriceFactor;
|
|
1350
|
+
if (params.currency) prices.currency = params.currency;
|
|
1351
|
+
|
|
1352
|
+
const data = await guestyPut(`/listings/${params.listingId}`, { prices });
|
|
1353
|
+
const updated = Object.keys(prices).join(", ");
|
|
1354
|
+
return { content: [{ type: "text", text: `Pricing updated for ${params.listingId}. Fields changed: ${updated}` }] };
|
|
1355
|
+
}
|
|
1356
|
+
);
|
|
1357
|
+
|
|
1094
1358
|
// Start server
|
|
1095
1359
|
const transport = new StdioServerTransport();
|
|
1096
1360
|
await server.connect(transport);
|