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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guesty-mcp-server",
3
- "version": "0.3.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.3.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
- 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
- };
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);