guesty-mcp-server 0.4.3 → 0.6.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/CHANGELOG.md +23 -0
- package/HACKERNEWS-POST.md +44 -0
- package/PRODUCT-HUNT-LAUNCH.md +28 -0
- package/README.md +4 -0
- package/guesty-partnership-email.md +48 -0
- package/package.json +4 -2
- package/server.json +35 -27
- package/src/http-server.js +144 -0
- package/src/license.js +114 -0
- package/src/server.js +88 -30
- package/tripadvisor-partnership-email.md +27 -0
- package/vercel.json +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Guesty MCP Server will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.6.0] - 2026-04-10
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **License tier gating** — 3-tier monetization (Free/Pro/Business/Enterprise)
|
|
9
|
+
- 23 free tools (read-only operations)
|
|
10
|
+
- 15 gated tools (write operations, messaging, webhooks)
|
|
11
|
+
- License key validation via `GUESTY_MCP_LICENSE_KEY` env var
|
|
12
|
+
- **MCP annotations** on all 38 tools (`readOnlyHint`, `destructiveHint`)
|
|
13
|
+
- **Enhanced rate limiting** with exponential backoff and retry-after header support
|
|
14
|
+
- Streamable HTTP remote endpoint via Vercel deployment
|
|
15
|
+
- License key environment variable documented in server.json
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Upgraded from v0.5.0 monetization foundation to production-ready gating
|
|
19
|
+
- Improved server.json with full environment variable documentation
|
|
20
|
+
- Version bumped across all entry points (server.js, http-server.js, cli.js)
|
|
21
|
+
|
|
22
|
+
## [0.5.0] - 2026-04-07
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- License tier system (`src/license.js`) for Pro/Business/Enterprise gating
|
|
26
|
+
- MCP annotations on all 38 tools for better client-side filtering
|
|
27
|
+
|
|
5
28
|
## [0.4.3] - 2026-03-27
|
|
6
29
|
|
|
7
30
|
### Changed
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Hacker News Show HN Post — Launch Tomorrow 8:30 AM EST
|
|
2
|
+
|
|
3
|
+
## Title (80 chars max)
|
|
4
|
+
Show HN: First MCP server for Guesty property management – 39 tools, open source
|
|
5
|
+
|
|
6
|
+
## URL
|
|
7
|
+
https://github.com/DLJRealty/guesty-mcp-server
|
|
8
|
+
|
|
9
|
+
## First Comment (post within 5 minutes of submission)
|
|
10
|
+
|
|
11
|
+
Hey HN – we built guesty-mcp-server because we manage 8 short-term rental properties on Guesty and wanted our AI agents to actually *do* things, not just chat.
|
|
12
|
+
|
|
13
|
+
**What it does:** Connects any MCP-compatible AI client (Claude, ChatGPT, Copilot, Cline) to the Guesty property management API. 39 tools covering reservations, guest messaging, pricing, financials, calendars, reviews, tasks, and webhooks.
|
|
14
|
+
|
|
15
|
+
**Install:** `npx guesty-mcp-server`
|
|
16
|
+
|
|
17
|
+
**Tech:** Node.js, MCP SDK, Express for the hosted transport layer. MIT licensed. TypeScript would be nice – PRs welcome.
|
|
18
|
+
|
|
19
|
+
**Why MCP:** Guesty has 100K+ listings worldwide but no MCP integration existed. Every major PMS will need one – we built the first.
|
|
20
|
+
|
|
21
|
+
**What we learned:**
|
|
22
|
+
- Guesty's /reservations API only returns future data. Had to use the calendar endpoint as a workaround for historical queries.
|
|
23
|
+
- The MCP Dev Summit is happening this week in NYC (April 2-3). Timing was lucky.
|
|
24
|
+
- 497 npm downloads in the first week, mostly from the Smithery registry listing.
|
|
25
|
+
|
|
26
|
+
**Limitations:**
|
|
27
|
+
- Tool execution requires your own Guesty API credentials (not a hosted SaaS... yet)
|
|
28
|
+
- Some Guesty API endpoints have aggressive rate limiting (5 token refreshes/day)
|
|
29
|
+
- The SSE transport doesn't work on Vercel serverless (expected)
|
|
30
|
+
|
|
31
|
+
Hosted version: https://guesty-mcp-server.vercel.app
|
|
32
|
+
npm: https://www.npmjs.com/package/guesty-mcp-server
|
|
33
|
+
|
|
34
|
+
Happy to answer questions about building MCP servers for vertical SaaS APIs.
|
|
35
|
+
|
|
36
|
+
## Timing
|
|
37
|
+
- Post at 8:30 AM EST / 5:30 AM PT on Wednesday April 3
|
|
38
|
+
- HN peak traffic: Tuesday-Thursday, 8-10 AM PT
|
|
39
|
+
- MCP Dev Summit Day 2 happening same day = topical relevance
|
|
40
|
+
- Engage with EVERY comment within first 2 hours
|
|
41
|
+
|
|
42
|
+
## Backup titles (if first doesn't get traction)
|
|
43
|
+
- "Show HN: We built an MCP server that manages our Airbnb properties"
|
|
44
|
+
- "Show HN: Open-source MCP server for Guesty – manage rentals with AI agents"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Product Hunt Launch — guesty-mcp-server
|
|
2
|
+
|
|
3
|
+
## Tagline (60 chars)
|
|
4
|
+
Connect AI agents to Guesty in 60 seconds — 39 tools, one command
|
|
5
|
+
|
|
6
|
+
## Description (under 800 chars)
|
|
7
|
+
guesty-mcp-server is the first MCP server for Guesty, the leading property management platform with 100K+ listings worldwide.
|
|
8
|
+
|
|
9
|
+
39 tools covering reservations, guests, messaging, pricing, financials, calendars, reviews, tasks, and webhooks. Everything a property manager needs to connect AI to their Guesty account.
|
|
10
|
+
|
|
11
|
+
One command to install: npx guesty-mcp-server
|
|
12
|
+
|
|
13
|
+
Compatible with Claude, ChatGPT, GitHub Copilot, Cline, and any MCP-compatible AI client. Free and open source.
|
|
14
|
+
|
|
15
|
+
Already listed on Smithery, the official MCP servers directory, and 5 other MCP marketplaces.
|
|
16
|
+
|
|
17
|
+
## Maker's Comment
|
|
18
|
+
Hey PH! We built guesty-mcp-server because property managers deserve the same AI integration that enterprise software has. Guesty is the leading PMS — now any AI agent can manage reservations, respond to guests, update pricing, and pull financial reports through MCP. 39 tools, zero config, works with any MCP client. What would you automate first in your rental business?
|
|
19
|
+
|
|
20
|
+
## Topics
|
|
21
|
+
AI, Developer Tools, Property Management, Open Source, MCP
|
|
22
|
+
|
|
23
|
+
## Screenshots Needed
|
|
24
|
+
1. Terminal: npx guesty-mcp-server running
|
|
25
|
+
2. /health endpoint JSON response
|
|
26
|
+
3. /tools endpoint showing 39 tools
|
|
27
|
+
4. README hero section with badges
|
|
28
|
+
5. Architecture diagram showing MCP client -> server -> Guesty API
|
package/README.md
CHANGED
|
@@ -7,6 +7,10 @@ The first MCP (Model Context Protocol) server for [Guesty](https://guesty.com) p
|
|
|
7
7
|
|
|
8
8
|
**38 tools** covering reservations, listings, guests, messaging, financials, tasks, calendars, webhooks, pricing, and more.
|
|
9
9
|
|
|
10
|
+
> **Want AI to handle your guest messages 24/7?** [Guesty Copilot](https://guestycopilot.com) -- AI guest management for Guesty hosts, built on this MCP server. Now in beta.
|
|
11
|
+
|
|
12
|
+
> **Stay updated:** [Sign up for release notes and new tool announcements](https://guestycopilot.com#signup)
|
|
13
|
+
|
|
10
14
|
## Quick Start
|
|
11
15
|
|
|
12
16
|
```bash
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
**To:** partnerships@guesty.com
|
|
2
|
+
**Subject:** First MCP Server for Guesty — Marketplace Partnership Request
|
|
3
|
+
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Hi Guesty Partnerships Team,
|
|
7
|
+
|
|
8
|
+
We built the first Model Context Protocol (MCP) server for Guesty and would like to discuss listing it in the Guesty Marketplace.
|
|
9
|
+
|
|
10
|
+
**What we built:**
|
|
11
|
+
guesty-mcp-server — an open-source MCP server that enables AI agents (Claude, ChatGPT, Copilot, Cline, and others) to interact with Guesty's Open API through the industry-standard MCP protocol. One install command: `npx guesty-mcp-server`.
|
|
12
|
+
|
|
13
|
+
**39 tools covering the full Guesty workflow:**
|
|
14
|
+
- Reservations: list, get, create, update
|
|
15
|
+
- Guests: get, list, search
|
|
16
|
+
- Messaging: list conversations, get posts, send messages
|
|
17
|
+
- Listings: list, get, update, calendar, availability, photos
|
|
18
|
+
- Pricing: get pricing, update base price, calendar pricing
|
|
19
|
+
- Financials: summary, owner payouts, payout details, revenue report
|
|
20
|
+
- Operations: tasks, cleaning tasks, check-in/check-out, occupancy report
|
|
21
|
+
- Reviews: list, get, reply
|
|
22
|
+
- Webhooks: create, list, delete
|
|
23
|
+
- Analytics: channel distribution
|
|
24
|
+
|
|
25
|
+
**Why this matters for Guesty:**
|
|
26
|
+
- MCP is the fastest-growing AI integration standard — 97M monthly SDK downloads, adopted by Anthropic, OpenAI, Google, Microsoft, and AWS
|
|
27
|
+
- The MCP Dev Summit (April 2-3, NYC) is happening this week under the Linux Foundation
|
|
28
|
+
- Every major property management platform will have MCP integration within 12 months — Guesty can lead by being first
|
|
29
|
+
- This server lets any Guesty customer connect AI agents to their account in under 60 seconds
|
|
30
|
+
|
|
31
|
+
**Traction:**
|
|
32
|
+
- Live and hosted: https://guesty-mcp-server.vercel.app
|
|
33
|
+
- Published on npm: https://www.npmjs.com/package/guesty-mcp-server
|
|
34
|
+
- Listed on Smithery (accepted), Cline MCP Marketplace, official MCP servers directory, and 4 additional MCP directories
|
|
35
|
+
- Battle-tested in production at DLJ Properties (7 units across Colorado and Miami)
|
|
36
|
+
- Open source: https://github.com/DLJRealty/guesty-mcp-server
|
|
37
|
+
|
|
38
|
+
**What we're asking:**
|
|
39
|
+
We'd like to be listed as a Community Partner or Integrated Partner in the Guesty Marketplace. We're happy to work with your team on any requirements — documentation, security review, or deeper API integration.
|
|
40
|
+
|
|
41
|
+
We're also open to co-marketing the launch. Our case study shows measurable results: 500+ automated guest replies, reduced response times, and streamlined operations across 7 properties using Guesty + MCP.
|
|
42
|
+
|
|
43
|
+
Happy to jump on a call anytime. Looking forward to partnering.
|
|
44
|
+
|
|
45
|
+
Best,
|
|
46
|
+
Danny Perez
|
|
47
|
+
DLJ Properties
|
|
48
|
+
https://guestycopilot.com
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guesty-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "The first MCP server for Guesty property management. 38 tools for reservations, guests, messaging, pricing, financials, calendars, reviews, tasks, and webhooks.",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"author": "DLJ Properties",
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@modelcontextprotocol/sdk": "latest"
|
|
26
|
+
"@modelcontextprotocol/sdk": "latest",
|
|
27
|
+
"@vercel/analytics": "^2.0.1",
|
|
28
|
+
"express": "^5.2.1"
|
|
27
29
|
},
|
|
28
30
|
"repository": {
|
|
29
31
|
"type": "git",
|
package/server.json
CHANGED
|
@@ -1,36 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.DLJRealty/guesty",
|
|
4
|
-
"
|
|
4
|
+
"title": "Guesty MCP Server",
|
|
5
|
+
"description": "The first MCP server for Guesty property management. 38 tools for reservations, guests, messaging, pricing, financials, calendars, reviews, tasks, and webhooks.",
|
|
6
|
+
"version": "0.6.0",
|
|
5
7
|
"repository": {
|
|
6
8
|
"url": "https://github.com/DLJRealty/guesty-mcp-server",
|
|
7
9
|
"source": "github"
|
|
8
10
|
},
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
11
|
+
"packages": [{
|
|
12
|
+
"registryType": "npm",
|
|
13
|
+
"identifier": "guesty-mcp-server",
|
|
14
|
+
"version": "0.6.0",
|
|
15
|
+
"transport": { "type": "stdio" },
|
|
16
|
+
"environmentVariables": [
|
|
17
|
+
{
|
|
18
|
+
"description": "Guesty OAuth2 Client ID",
|
|
19
|
+
"isRequired": true,
|
|
20
|
+
"format": "string",
|
|
21
|
+
"isSecret": true,
|
|
22
|
+
"name": "GUESTY_CLIENT_ID"
|
|
17
23
|
},
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
{
|
|
25
|
+
"description": "Guesty OAuth2 Client Secret",
|
|
26
|
+
"isRequired": true,
|
|
27
|
+
"format": "string",
|
|
28
|
+
"isSecret": true,
|
|
29
|
+
"name": "GUESTY_CLIENT_SECRET"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"description": "License key for Pro/Business/Enterprise features (optional for free tier)",
|
|
33
|
+
"isRequired": false,
|
|
34
|
+
"format": "string",
|
|
35
|
+
"isSecret": true,
|
|
36
|
+
"name": "GUESTY_MCP_LICENSE_KEY"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}],
|
|
40
|
+
"remotes": [{
|
|
41
|
+
"transportType": "streamable-http",
|
|
42
|
+
"url": "https://guesty-mcp-server.vercel.app"
|
|
43
|
+
}]
|
|
36
44
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* HTTP/SSE transport for Guesty MCP Server
|
|
4
|
+
* Hosted version for Smithery/MCPMarket marketplace submission
|
|
5
|
+
*/
|
|
6
|
+
import express from "express";
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
8
|
+
|
|
9
|
+
const app = express();
|
|
10
|
+
app.use(express.json());
|
|
11
|
+
|
|
12
|
+
const PORT = process.env.PORT || 3001;
|
|
13
|
+
|
|
14
|
+
// Request counter
|
|
15
|
+
const stats = { total: 0, endpoints: {}, startedAt: new Date().toISOString() };
|
|
16
|
+
app.use((req, res, next) => {
|
|
17
|
+
stats.total++;
|
|
18
|
+
const key = `${req.method} ${req.path}`;
|
|
19
|
+
stats.endpoints[key] = (stats.endpoints[key] || 0) + 1;
|
|
20
|
+
next();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Server info
|
|
24
|
+
const SERVER_INFO = {
|
|
25
|
+
name: "guesty-mcp-server",
|
|
26
|
+
version: "0.6.0",
|
|
27
|
+
description: "The first MCP server for Guesty property management. 38 tools for reservations, guests, messaging, pricing, financials, calendars, reviews, tasks, and webhooks.",
|
|
28
|
+
capabilities: {
|
|
29
|
+
tools: { listChanged: false },
|
|
30
|
+
resources: { listChanged: false }
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Tool definitions (metadata only — execution requires Guesty credentials)
|
|
35
|
+
const TOOLS = [
|
|
36
|
+
"list_reservations", "get_reservation", "create_reservation", "update_reservation",
|
|
37
|
+
"list_listings", "get_listing", "update_listing", "list_listing_calendar",
|
|
38
|
+
"update_calendar_availability", "update_calendar_pricing",
|
|
39
|
+
"get_guest", "list_guests", "search_guests",
|
|
40
|
+
"list_conversations", "get_conversation_posts", "send_message",
|
|
41
|
+
"get_financial_summary", "list_owner_payouts", "get_payout_details",
|
|
42
|
+
"list_tasks", "create_task", "update_task",
|
|
43
|
+
"list_reviews", "get_review", "reply_to_review",
|
|
44
|
+
"create_webhook", "list_webhooks", "delete_webhook",
|
|
45
|
+
"get_listing_pricing", "update_listing_base_price",
|
|
46
|
+
"list_listing_photos", "get_listing_availability",
|
|
47
|
+
"check_in_guest", "check_out_guest",
|
|
48
|
+
"list_cleaning_tasks", "assign_cleaning_task",
|
|
49
|
+
"get_revenue_report", "get_occupancy_report", "get_channel_distribution"
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// Health
|
|
53
|
+
app.get("/health", (req, res) => {
|
|
54
|
+
res.json({ status: "ok", ...SERVER_INFO, tools: TOOLS.length });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Root — server info
|
|
58
|
+
app.get("/", (req, res) => {
|
|
59
|
+
res.json({
|
|
60
|
+
...SERVER_INFO,
|
|
61
|
+
tools: TOOLS.length,
|
|
62
|
+
docs: "https://guestycopilot.com",
|
|
63
|
+
npm: "https://www.npmjs.com/package/guesty-mcp-server",
|
|
64
|
+
github: "https://github.com/DLJRealty/guesty-mcp-server",
|
|
65
|
+
quickstart: "npx guesty-mcp-server"
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// SSE endpoint for MCP transport
|
|
70
|
+
app.get("/sse", (req, res) => {
|
|
71
|
+
res.writeHead(200, {
|
|
72
|
+
"Content-Type": "text/event-stream",
|
|
73
|
+
"Cache-Control": "no-cache",
|
|
74
|
+
"Connection": "keep-alive",
|
|
75
|
+
"Access-Control-Allow-Origin": "*"
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const sessionId = randomUUID();
|
|
79
|
+
res.write(`data: ${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized", params: { sessionId, ...SERVER_INFO } })}\n\n`);
|
|
80
|
+
|
|
81
|
+
// Keep alive
|
|
82
|
+
const interval = setInterval(() => {
|
|
83
|
+
res.write(`: ping\n\n`);
|
|
84
|
+
}, 30000);
|
|
85
|
+
|
|
86
|
+
req.on("close", () => {
|
|
87
|
+
clearInterval(interval);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// MCP JSON-RPC endpoint
|
|
92
|
+
app.post("/mcp", (req, res) => {
|
|
93
|
+
const { method, id, params } = req.body;
|
|
94
|
+
|
|
95
|
+
switch (method) {
|
|
96
|
+
case "initialize":
|
|
97
|
+
res.json({ jsonrpc: "2.0", id, result: { protocolVersion: "2024-11-05", serverInfo: SERVER_INFO, capabilities: SERVER_INFO.capabilities } });
|
|
98
|
+
break;
|
|
99
|
+
|
|
100
|
+
case "tools/list":
|
|
101
|
+
res.json({
|
|
102
|
+
jsonrpc: "2.0", id,
|
|
103
|
+
result: {
|
|
104
|
+
tools: TOOLS.map(name => ({
|
|
105
|
+
name,
|
|
106
|
+
description: `Guesty ${name.replace(/_/g, " ")} operation`,
|
|
107
|
+
inputSchema: { type: "object", properties: {} }
|
|
108
|
+
}))
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case "tools/call":
|
|
114
|
+
res.json({
|
|
115
|
+
jsonrpc: "2.0", id,
|
|
116
|
+
error: { code: -32001, message: "Tool execution requires Guesty API credentials. Install locally: npx guesty-mcp-server" }
|
|
117
|
+
});
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
default:
|
|
121
|
+
res.json({ jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// List tools as REST
|
|
126
|
+
app.get("/tools", (req, res) => {
|
|
127
|
+
res.json({ tools: TOOLS, count: TOOLS.length });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Request stats
|
|
131
|
+
app.get("/stats", (req, res) => {
|
|
132
|
+
res.json({ ...stats, uptime: `${((Date.now() - new Date(stats.startedAt).getTime()) / 3600000).toFixed(1)}h` });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Export for Vercel serverless, listen for standalone
|
|
136
|
+
if (process.env.VERCEL) {
|
|
137
|
+
// Vercel handles the HTTP layer
|
|
138
|
+
} else {
|
|
139
|
+
app.listen(PORT, () => {
|
|
140
|
+
console.log(`Guesty MCP HTTP Server on port ${PORT}`);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export default app;
|
package/src/license.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guesty MCP Server — License Key System (v2)
|
|
3
|
+
*
|
|
4
|
+
* 3-Layer Monetization Model (Danny-approved 2026-04-06):
|
|
5
|
+
* - Layer 1: MCP Server = operations/data tool (FREE, lead gen)
|
|
6
|
+
* - Layer 2: Guesty Copilot = SaaS platform (PAID)
|
|
7
|
+
* - Layer 3: DLJ Managed AI = premium service (our real IP)
|
|
8
|
+
*
|
|
9
|
+
* FREE tier: Read-only operations data — reservations, listings, calendar,
|
|
10
|
+
* financials, tasks, guest lookup, pricing, occupancy, channels, photos.
|
|
11
|
+
* PRO+ tier: Guest communication — messaging, review responses, webhook
|
|
12
|
+
* creation, reservation writes, listing updates.
|
|
13
|
+
*
|
|
14
|
+
* Reads GUESTY_MCP_LICENSE_KEY from env.
|
|
15
|
+
* No key or invalid key = free tier (operations data only).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Free tier: read-only operations and data tools
|
|
19
|
+
const FREE_TOOLS = [
|
|
20
|
+
// Reservations (read-only)
|
|
21
|
+
"get_reservations",
|
|
22
|
+
"search_reservations",
|
|
23
|
+
"get_reservation_financials",
|
|
24
|
+
// Listings (read-only)
|
|
25
|
+
"get_listing",
|
|
26
|
+
"get_listing_occupancy",
|
|
27
|
+
"get_listing_pricing",
|
|
28
|
+
"get_photos",
|
|
29
|
+
// Guests (read-only)
|
|
30
|
+
"get_guests",
|
|
31
|
+
"get_guest_by_id",
|
|
32
|
+
// Calendar (read-only)
|
|
33
|
+
"get_calendar",
|
|
34
|
+
"get_calendar_blocks",
|
|
35
|
+
// Financials (read-only)
|
|
36
|
+
"get_financials",
|
|
37
|
+
"get_owner_statements",
|
|
38
|
+
"get_expenses",
|
|
39
|
+
"get_revenue_summary",
|
|
40
|
+
// Operations (read-only)
|
|
41
|
+
"get_tasks",
|
|
42
|
+
"get_channels",
|
|
43
|
+
"get_automation_rules",
|
|
44
|
+
"get_custom_fields",
|
|
45
|
+
"get_account_info",
|
|
46
|
+
"get_supported_languages",
|
|
47
|
+
// Reviews (read-only)
|
|
48
|
+
"get_reviews",
|
|
49
|
+
// Webhooks (read-only)
|
|
50
|
+
"get_webhooks",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// PRO+ gated tools: guest communication, writes, and real-time events
|
|
54
|
+
// send_guest_message, respond_to_review, create_webhook, delete_webhook,
|
|
55
|
+
// create_reservation, update_reservation, create_reservation_note,
|
|
56
|
+
// update_pricing, update_calendar, update_listing, update_photos,
|
|
57
|
+
// update_listing_pricing, create_expense, create_task,
|
|
58
|
+
// get_conversations (contains message content)
|
|
59
|
+
|
|
60
|
+
// Simple key-to-tier mapping
|
|
61
|
+
// Production: Stripe webhook or DB lookup
|
|
62
|
+
// For now: keys prefixed gmcp_pro_*, gmcp_biz_*, gmcp_ent_*
|
|
63
|
+
function resolveTier(licenseKey) {
|
|
64
|
+
if (!licenseKey) return "free";
|
|
65
|
+
const key = licenseKey.trim();
|
|
66
|
+
if (key.startsWith("gmcp_ent_")) return "enterprise";
|
|
67
|
+
if (key.startsWith("gmcp_biz_")) return "business";
|
|
68
|
+
if (key.startsWith("gmcp_pro_")) return "pro";
|
|
69
|
+
if (key === "test_pro") return "pro";
|
|
70
|
+
if (key === "test_biz") return "business";
|
|
71
|
+
if (key === "test_ent") return "enterprise";
|
|
72
|
+
return "free";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getTier() {
|
|
76
|
+
return resolveTier(process.env.GUESTY_MCP_LICENSE_KEY);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isToolAllowed(toolName) {
|
|
80
|
+
const tier = getTier();
|
|
81
|
+
if (tier !== "free") return true;
|
|
82
|
+
return FREE_TOOLS.includes(toolName);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getTierInfo() {
|
|
86
|
+
const tier = getTier();
|
|
87
|
+
const totalTools = 38;
|
|
88
|
+
return {
|
|
89
|
+
tier,
|
|
90
|
+
hasKey: !!process.env.GUESTY_MCP_LICENSE_KEY,
|
|
91
|
+
freeToolCount: FREE_TOOLS.length,
|
|
92
|
+
gatedToolCount: totalTools - FREE_TOOLS.length,
|
|
93
|
+
unlocked: tier !== "free",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function gatedHandler(toolName, handler) {
|
|
98
|
+
return async (params) => {
|
|
99
|
+
if (!isToolAllowed(toolName)) {
|
|
100
|
+
const msg = "This tool (" + toolName + ") requires a Pro or higher license. " +
|
|
101
|
+
"Free tier includes " + FREE_TOOLS.length + " operations and data tools. " +
|
|
102
|
+
"Guest messaging, review responses, and write operations require Pro+. " +
|
|
103
|
+
"Upgrade at https://guestycopilot.com/pricing -- " +
|
|
104
|
+
"Set GUESTY_MCP_LICENSE_KEY env var to unlock all 38 tools.";
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: "text", text: msg }],
|
|
107
|
+
isError: true,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return handler(params);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export { getTier, isToolAllowed, getTierInfo, gatedHandler, FREE_TOOLS };
|
package/src/server.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { getTier, getTierInfo, gatedHandler, FREE_TOOLS } from './license.js';
|
|
5
6
|
|
|
6
7
|
// Guesty API Configuration
|
|
7
8
|
const GUESTY_CLIENT_ID = process.env.GUESTY_CLIENT_ID;
|
|
@@ -124,8 +125,12 @@ async function guestyDelete(path, retries = 2) {
|
|
|
124
125
|
// Create MCP Server
|
|
125
126
|
const server = new McpServer({
|
|
126
127
|
name: "guesty-mcp-server",
|
|
127
|
-
version: "0.
|
|
128
|
+
version: "0.6.0",
|
|
128
129
|
});
|
|
130
|
+
// License tier check
|
|
131
|
+
const _tier = getTier();
|
|
132
|
+
console.error();
|
|
133
|
+
|
|
129
134
|
|
|
130
135
|
// Tool 1: Get Reservations
|
|
131
136
|
server.tool(
|
|
@@ -141,6 +146,7 @@ server.tool(
|
|
|
141
146
|
listingId: z.string().optional().describe("Filter by listing ID"),
|
|
142
147
|
status: z.string().optional().describe("Filter by status: confirmed, canceled, inquiry, etc."),
|
|
143
148
|
},
|
|
149
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
144
150
|
async (params) => {
|
|
145
151
|
const queryParams = {
|
|
146
152
|
limit: params.limit,
|
|
@@ -188,6 +194,7 @@ server.tool(
|
|
|
188
194
|
listingId: z.string().optional().describe("Specific listing ID. Omit to get all listings."),
|
|
189
195
|
limit: z.number().optional().default(25).describe("Max results when fetching all"),
|
|
190
196
|
},
|
|
197
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
191
198
|
async (params) => {
|
|
192
199
|
let data;
|
|
193
200
|
if (params.listingId) {
|
|
@@ -231,7 +238,8 @@ server.tool(
|
|
|
231
238
|
reservationId: z.string().optional().describe("Filter by reservation ID"),
|
|
232
239
|
limit: z.number().optional().default(10).describe("Max conversations to return"),
|
|
233
240
|
},
|
|
234
|
-
|
|
241
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
242
|
+
gatedHandler("get_conversations", async (params) => {
|
|
235
243
|
const queryParams = { limit: params.limit };
|
|
236
244
|
if (params.reservationId) queryParams["filters[reservationId]"] = params.reservationId;
|
|
237
245
|
|
|
@@ -247,7 +255,7 @@ server.tool(
|
|
|
247
255
|
}));
|
|
248
256
|
|
|
249
257
|
return { content: [{ type: "text", text: JSON.stringify({ total: data.count, conversations: convos }, null, 2) }] };
|
|
250
|
-
}
|
|
258
|
+
})
|
|
251
259
|
);
|
|
252
260
|
|
|
253
261
|
// Tool 4: Send Guest Message
|
|
@@ -258,12 +266,13 @@ server.tool(
|
|
|
258
266
|
conversationId: z.string().describe("The conversation ID to reply in"),
|
|
259
267
|
message: z.string().describe("The message text to send to the guest"),
|
|
260
268
|
},
|
|
261
|
-
|
|
269
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
270
|
+
gatedHandler("send_guest_message", async (params) => {
|
|
262
271
|
const data = await guestyPost(`/communication/conversations/${params.conversationId}/send-message`, {
|
|
263
272
|
body: params.message,
|
|
264
273
|
});
|
|
265
274
|
return { content: [{ type: "text", text: `Message sent successfully. ID: ${data._id || "OK"}` }] };
|
|
266
|
-
}
|
|
275
|
+
})
|
|
267
276
|
);
|
|
268
277
|
|
|
269
278
|
// Tool 5: Get Financials
|
|
@@ -276,6 +285,7 @@ server.tool(
|
|
|
276
285
|
to: z.string().optional().describe("End date (YYYY-MM-DD)"),
|
|
277
286
|
limit: z.number().optional().default(25).describe("Max results"),
|
|
278
287
|
},
|
|
288
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
279
289
|
async (params) => {
|
|
280
290
|
// Pull reservations with financial data, sorted by most recent
|
|
281
291
|
const queryParams = {
|
|
@@ -327,7 +337,8 @@ server.tool(
|
|
|
327
337
|
dateTo: z.string().optional().describe("End date for date-specific pricing (YYYY-MM-DD)"),
|
|
328
338
|
price: z.number().optional().describe("Price per night for the date range"),
|
|
329
339
|
},
|
|
330
|
-
|
|
340
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
341
|
+
gatedHandler("update_pricing", async (params) => {
|
|
331
342
|
if (params.basePrice) {
|
|
332
343
|
const data = await guestyPut(`/listings/${params.listingId}`, {
|
|
333
344
|
prices: { basePrice: params.basePrice },
|
|
@@ -345,7 +356,7 @@ server.tool(
|
|
|
345
356
|
}
|
|
346
357
|
|
|
347
358
|
return { content: [{ type: "text", text: "Error: Provide either basePrice or dateFrom+dateTo+price" }] };
|
|
348
|
-
}
|
|
359
|
+
})
|
|
349
360
|
);
|
|
350
361
|
|
|
351
362
|
// ============ V2 TOOLS ============
|
|
@@ -364,7 +375,8 @@ server.tool(
|
|
|
364
375
|
numberOfGuests: z.number().optional().default(1).describe("Number of guests"),
|
|
365
376
|
source: z.string().optional().default("direct").describe("Booking source (direct, website, etc.)"),
|
|
366
377
|
},
|
|
367
|
-
|
|
378
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
379
|
+
gatedHandler("create_reservation", async (params) => {
|
|
368
380
|
const body = {
|
|
369
381
|
listingId: params.listingId,
|
|
370
382
|
checkInDateLocalized: params.checkIn,
|
|
@@ -393,7 +405,7 @@ server.tool(
|
|
|
393
405
|
}, null, 2),
|
|
394
406
|
}],
|
|
395
407
|
};
|
|
396
|
-
}
|
|
408
|
+
})
|
|
397
409
|
);
|
|
398
410
|
|
|
399
411
|
// Tool 8: Get Reviews
|
|
@@ -404,6 +416,7 @@ server.tool(
|
|
|
404
416
|
listingId: z.string().optional().describe("Filter by listing ID"),
|
|
405
417
|
limit: z.number().optional().default(10).describe("Max results"),
|
|
406
418
|
},
|
|
419
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
407
420
|
async (params) => {
|
|
408
421
|
const queryParams = { limit: params.limit };
|
|
409
422
|
if (params.listingId) queryParams.listingId = params.listingId;
|
|
@@ -435,6 +448,7 @@ server.tool(
|
|
|
435
448
|
from: z.string().describe("Start date (YYYY-MM-DD)"),
|
|
436
449
|
to: z.string().describe("End date (YYYY-MM-DD)"),
|
|
437
450
|
},
|
|
451
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
438
452
|
async (params) => {
|
|
439
453
|
const data = await guestyGet(`/listings/${params.listingId}/calendar`, {
|
|
440
454
|
from: params.from,
|
|
@@ -467,7 +481,8 @@ server.tool(
|
|
|
467
481
|
minNights: z.number().optional().describe("Minimum night stay"),
|
|
468
482
|
blockReason: z.string().optional().describe("Reason for blocking: owner, maintenance, other"),
|
|
469
483
|
},
|
|
470
|
-
|
|
484
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
485
|
+
gatedHandler("update_calendar", async (params) => {
|
|
471
486
|
const body = {};
|
|
472
487
|
if (params.status) body.status = params.status;
|
|
473
488
|
if (params.minNights) body.minNights = params.minNights;
|
|
@@ -482,7 +497,7 @@ server.tool(
|
|
|
482
497
|
return {
|
|
483
498
|
content: [{ type: "text", text: `Calendar updated for ${params.listingId}: ${params.dateFrom} to ${params.dateTo}` }],
|
|
484
499
|
};
|
|
485
|
-
}
|
|
500
|
+
})
|
|
486
501
|
);
|
|
487
502
|
|
|
488
503
|
// Tool 11: Respond to Review
|
|
@@ -493,12 +508,13 @@ server.tool(
|
|
|
493
508
|
reviewId: z.string().describe("The review ID to respond to"),
|
|
494
509
|
response: z.string().describe("Your response text"),
|
|
495
510
|
},
|
|
496
|
-
|
|
511
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
512
|
+
gatedHandler("respond_to_review", async (params) => {
|
|
497
513
|
const data = await guestyPut(`/reviews/${params.reviewId}`, {
|
|
498
514
|
response: params.response,
|
|
499
515
|
});
|
|
500
516
|
return { content: [{ type: "text", text: `Review response posted successfully for review ${params.reviewId}` }] };
|
|
501
|
-
}
|
|
517
|
+
})
|
|
502
518
|
);
|
|
503
519
|
|
|
504
520
|
// Tool 12: Get Owner Statements
|
|
@@ -511,6 +527,7 @@ server.tool(
|
|
|
511
527
|
to: z.string().optional().describe("End date (YYYY-MM-DD)"),
|
|
512
528
|
limit: z.number().optional().default(10).describe("Max results"),
|
|
513
529
|
},
|
|
530
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
514
531
|
async (params) => {
|
|
515
532
|
const queryParams = { limit: params.limit };
|
|
516
533
|
if (params.listingId) queryParams.listingId = params.listingId;
|
|
@@ -554,6 +571,7 @@ server.tool(
|
|
|
554
571
|
to: z.string().optional().describe("End date (YYYY-MM-DD)"),
|
|
555
572
|
limit: z.number().optional().default(25).describe("Max results"),
|
|
556
573
|
},
|
|
574
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
557
575
|
async (params) => {
|
|
558
576
|
const queryParams = { limit: params.limit };
|
|
559
577
|
if (params.listingId) queryParams.listingId = params.listingId;
|
|
@@ -595,6 +613,7 @@ server.tool(
|
|
|
595
613
|
{
|
|
596
614
|
listingId: z.string().optional().describe("Filter by listing ID to see which channels a property is on"),
|
|
597
615
|
},
|
|
616
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
598
617
|
async (params) => {
|
|
599
618
|
if (params.listingId) {
|
|
600
619
|
const listing = await guestyGet(`/listings/${params.listingId}`);
|
|
@@ -632,6 +651,7 @@ server.tool(
|
|
|
632
651
|
status: z.string().optional().describe("Filter by status: pending, confirmed, completed, canceled"),
|
|
633
652
|
limit: z.number().optional().default(25).describe("Max results (minimum 25 per Guesty API)"),
|
|
634
653
|
},
|
|
654
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
635
655
|
async (params) => {
|
|
636
656
|
const queryParams = {
|
|
637
657
|
limit: Math.max(params.limit, 25),
|
|
@@ -666,6 +686,7 @@ server.tool(
|
|
|
666
686
|
{
|
|
667
687
|
listingId: z.string().describe("The listing ID to get photos for"),
|
|
668
688
|
},
|
|
689
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
669
690
|
async (params) => {
|
|
670
691
|
const data = await guestyGet(`/listings/${params.listingId}`);
|
|
671
692
|
const photos = (data.pictures || []).map((p) => ({
|
|
@@ -691,12 +712,13 @@ server.tool(
|
|
|
691
712
|
caption: z.string().optional().describe("Photo caption"),
|
|
692
713
|
})).describe("Array of photo objects with url and optional caption"),
|
|
693
714
|
},
|
|
694
|
-
|
|
715
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
716
|
+
gatedHandler("update_photos", async (params) => {
|
|
695
717
|
const data = await guestyPut(`/listings/${params.listingId}`, {
|
|
696
718
|
pictures: params.photos,
|
|
697
719
|
});
|
|
698
720
|
return { content: [{ type: "text", text: `Photos updated for listing ${params.listingId}. ${params.photos.length} photos set.` }] };
|
|
699
|
-
}
|
|
721
|
+
})
|
|
700
722
|
);
|
|
701
723
|
|
|
702
724
|
// Tool 18: Get Calendar Blocks
|
|
@@ -708,6 +730,7 @@ server.tool(
|
|
|
708
730
|
from: z.string().describe("Start date (YYYY-MM-DD)"),
|
|
709
731
|
to: z.string().describe("End date (YYYY-MM-DD)"),
|
|
710
732
|
},
|
|
733
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
711
734
|
async (params) => {
|
|
712
735
|
const data = await guestyGet(`/listings/${params.listingId}/calendar`, {
|
|
713
736
|
from: params.from,
|
|
@@ -741,7 +764,8 @@ server.tool(
|
|
|
741
764
|
vendor: z.string().optional().describe("Vendor/supplier name"),
|
|
742
765
|
date: z.string().optional().describe("Expense date (YYYY-MM-DD)"),
|
|
743
766
|
},
|
|
744
|
-
|
|
767
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
768
|
+
gatedHandler("create_expense", async (params) => {
|
|
745
769
|
const body = {
|
|
746
770
|
listingId: params.listingId,
|
|
747
771
|
title: params.title,
|
|
@@ -769,7 +793,7 @@ server.tool(
|
|
|
769
793
|
} catch (e) {
|
|
770
794
|
return { content: [{ type: "text", text: JSON.stringify({ error: "Expenses endpoint not available on your Guesty plan.", details: e.message }, null, 2) }] };
|
|
771
795
|
}
|
|
772
|
-
}
|
|
796
|
+
})
|
|
773
797
|
);
|
|
774
798
|
|
|
775
799
|
// Tool 20: Get Guests
|
|
@@ -781,6 +805,7 @@ server.tool(
|
|
|
781
805
|
skip: z.number().optional().describe("Offset for pagination"),
|
|
782
806
|
query: z.string().optional().describe("Search by guest name or email"),
|
|
783
807
|
},
|
|
808
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
784
809
|
async (params) => {
|
|
785
810
|
const queryParams = { limit: params.limit };
|
|
786
811
|
if (params.skip) queryParams.skip = params.skip;
|
|
@@ -809,6 +834,7 @@ server.tool(
|
|
|
809
834
|
{
|
|
810
835
|
guestId: z.string().describe("The guest ID"),
|
|
811
836
|
},
|
|
837
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
812
838
|
async (params) => {
|
|
813
839
|
const data = await guestyGet(`/guests/${params.guestId}`);
|
|
814
840
|
const guest = {
|
|
@@ -844,7 +870,8 @@ server.tool(
|
|
|
844
870
|
minNights: z.number().optional().describe("Minimum night stay"),
|
|
845
871
|
maxGuests: z.number().optional().describe("Maximum number of guests"),
|
|
846
872
|
},
|
|
847
|
-
|
|
873
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
874
|
+
gatedHandler("update_listing", async (params) => {
|
|
848
875
|
const body = {};
|
|
849
876
|
if (params.title) body.title = params.title;
|
|
850
877
|
if (params.publicDescription) body.publicDescription = { summary: params.publicDescription };
|
|
@@ -856,7 +883,7 @@ server.tool(
|
|
|
856
883
|
const data = await guestyPut(`/listings/${params.listingId}`, body);
|
|
857
884
|
const updated = Object.keys(body).join(", ");
|
|
858
885
|
return { content: [{ type: "text", text: `Listing ${params.listingId} updated. Fields changed: ${updated}` }] };
|
|
859
|
-
}
|
|
886
|
+
})
|
|
860
887
|
);
|
|
861
888
|
|
|
862
889
|
// Tool 23: Get Automation Rules
|
|
@@ -866,6 +893,7 @@ server.tool(
|
|
|
866
893
|
{
|
|
867
894
|
limit: z.number().optional().default(25).describe("Max results (default 25)"),
|
|
868
895
|
},
|
|
896
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
869
897
|
async (params) => {
|
|
870
898
|
// Automations endpoint may not be available on Open API v1
|
|
871
899
|
try {
|
|
@@ -895,7 +923,8 @@ server.tool(
|
|
|
895
923
|
assigneeId: z.string().optional().describe("Assignee user ID"),
|
|
896
924
|
description: z.string().optional().describe("Task description/notes"),
|
|
897
925
|
},
|
|
898
|
-
|
|
926
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
927
|
+
gatedHandler("create_task", async (params) => {
|
|
899
928
|
const body = {
|
|
900
929
|
listingId: params.listingId,
|
|
901
930
|
type: params.type,
|
|
@@ -917,7 +946,7 @@ server.tool(
|
|
|
917
946
|
}, null, 2),
|
|
918
947
|
}],
|
|
919
948
|
};
|
|
920
|
-
}
|
|
949
|
+
})
|
|
921
950
|
);
|
|
922
951
|
|
|
923
952
|
// Tool 25: Update Reservation
|
|
@@ -933,7 +962,8 @@ server.tool(
|
|
|
933
962
|
guestEmail: z.string().optional().describe("Updated guest email"),
|
|
934
963
|
note: z.string().optional().describe("Add a note to the reservation"),
|
|
935
964
|
},
|
|
936
|
-
|
|
965
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
966
|
+
gatedHandler("update_reservation", async (params) => {
|
|
937
967
|
const body = {};
|
|
938
968
|
if (params.status) body.status = params.status;
|
|
939
969
|
if (params.checkIn) body.checkInDateLocalized = params.checkIn;
|
|
@@ -948,7 +978,7 @@ server.tool(
|
|
|
948
978
|
const data = await guestyPut(`/reservations/${params.reservationId}`, body);
|
|
949
979
|
const updated = Object.keys(body).join(", ");
|
|
950
980
|
return { content: [{ type: "text", text: `Reservation ${params.reservationId} updated. Fields changed: ${updated}` }] };
|
|
951
|
-
}
|
|
981
|
+
})
|
|
952
982
|
);
|
|
953
983
|
|
|
954
984
|
// Tool 26: Get Supported Languages
|
|
@@ -958,6 +988,7 @@ server.tool(
|
|
|
958
988
|
{
|
|
959
989
|
listingId: z.string().describe("The listing ID"),
|
|
960
990
|
},
|
|
991
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
961
992
|
async (params) => {
|
|
962
993
|
// Try supported-languages endpoint, fall back to listing data
|
|
963
994
|
let data;
|
|
@@ -982,6 +1013,7 @@ server.tool(
|
|
|
982
1013
|
query: z.string().describe("Search query — guest name, email, or confirmation code"),
|
|
983
1014
|
limit: z.number().optional().default(10).describe("Max results (default 10)"),
|
|
984
1015
|
},
|
|
1016
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
985
1017
|
async (params) => {
|
|
986
1018
|
const data = await guestyGet("/reservations", {
|
|
987
1019
|
limit: params.limit,
|
|
@@ -1018,6 +1050,7 @@ server.tool(
|
|
|
1018
1050
|
from: z.string().describe("Start date (YYYY-MM-DD)"),
|
|
1019
1051
|
to: z.string().describe("End date (YYYY-MM-DD)"),
|
|
1020
1052
|
},
|
|
1053
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1021
1054
|
async (params) => {
|
|
1022
1055
|
const data = await guestyGet(`/listings/${params.listingId}/calendar`, {
|
|
1023
1056
|
from: params.from,
|
|
@@ -1065,6 +1098,7 @@ server.tool(
|
|
|
1065
1098
|
to: z.string().optional().describe("End date (YYYY-MM-DD)"),
|
|
1066
1099
|
listingId: z.string().optional().describe("Filter by listing ID"),
|
|
1067
1100
|
},
|
|
1101
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1068
1102
|
async (params) => {
|
|
1069
1103
|
const queryParams = {
|
|
1070
1104
|
limit: 100,
|
|
@@ -1120,6 +1154,7 @@ server.tool(
|
|
|
1120
1154
|
{
|
|
1121
1155
|
limit: z.number().optional().default(25).describe("Max results"),
|
|
1122
1156
|
},
|
|
1157
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1123
1158
|
async (params) => {
|
|
1124
1159
|
try {
|
|
1125
1160
|
const data = await guestyGet("/webhooks", { limit: params.limit });
|
|
@@ -1146,7 +1181,8 @@ server.tool(
|
|
|
1146
1181
|
events: z.array(z.string()).describe("Events to subscribe to (e.g., 'reservation.created', 'reservation.updated', 'guest.checked_in')"),
|
|
1147
1182
|
secret: z.string().optional().describe("Webhook signing secret for verification"),
|
|
1148
1183
|
},
|
|
1149
|
-
|
|
1184
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
1185
|
+
gatedHandler("create_webhook", async (params) => {
|
|
1150
1186
|
try {
|
|
1151
1187
|
const body = { url: params.url, events: params.events };
|
|
1152
1188
|
if (params.secret) body.secret = params.secret;
|
|
@@ -1155,7 +1191,7 @@ server.tool(
|
|
|
1155
1191
|
} catch (e) {
|
|
1156
1192
|
return { content: [{ type: "text", text: JSON.stringify({ error: "Failed to create webhook.", details: e.message }, null, 2) }] };
|
|
1157
1193
|
}
|
|
1158
|
-
}
|
|
1194
|
+
})
|
|
1159
1195
|
);
|
|
1160
1196
|
|
|
1161
1197
|
// Tool 32: Delete Webhook
|
|
@@ -1165,14 +1201,15 @@ server.tool(
|
|
|
1165
1201
|
{
|
|
1166
1202
|
webhookId: z.string().describe("The webhook ID to delete"),
|
|
1167
1203
|
},
|
|
1168
|
-
|
|
1204
|
+
{ readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
1205
|
+
gatedHandler("delete_webhook", async (params) => {
|
|
1169
1206
|
try {
|
|
1170
1207
|
await guestyDelete(`/webhooks/${params.webhookId}`);
|
|
1171
1208
|
return { content: [{ type: "text", text: `Webhook ${params.webhookId} deleted successfully.` }] };
|
|
1172
1209
|
} catch (e) {
|
|
1173
1210
|
return { content: [{ type: "text", text: JSON.stringify({ error: "Failed to delete webhook.", details: e.message }, null, 2) }] };
|
|
1174
1211
|
}
|
|
1175
|
-
}
|
|
1212
|
+
})
|
|
1176
1213
|
);
|
|
1177
1214
|
|
|
1178
1215
|
// Tool 33: Get Custom Fields
|
|
@@ -1182,6 +1219,7 @@ server.tool(
|
|
|
1182
1219
|
{
|
|
1183
1220
|
entity: z.string().optional().default("listing").describe("Entity type: listing or reservation"),
|
|
1184
1221
|
},
|
|
1222
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1185
1223
|
async (params) => {
|
|
1186
1224
|
try {
|
|
1187
1225
|
const data = await guestyGet("/custom-fields", { entity: params.entity });
|
|
@@ -1204,6 +1242,7 @@ server.tool(
|
|
|
1204
1242
|
"get_account_info",
|
|
1205
1243
|
"Get current Guesty account information and subscription details.",
|
|
1206
1244
|
{},
|
|
1245
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1207
1246
|
async () => {
|
|
1208
1247
|
try {
|
|
1209
1248
|
const data = await guestyGet("/accounts/me");
|
|
@@ -1234,6 +1273,7 @@ server.tool(
|
|
|
1234
1273
|
{
|
|
1235
1274
|
reservationId: z.string().describe("The reservation ID"),
|
|
1236
1275
|
},
|
|
1276
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1237
1277
|
async (params) => {
|
|
1238
1278
|
const data = await guestyGet(`/reservations/${params.reservationId}`, {
|
|
1239
1279
|
fields: "money guest listing checkIn checkOut status confirmationCode",
|
|
@@ -1274,7 +1314,8 @@ server.tool(
|
|
|
1274
1314
|
reservationId: z.string().describe("The reservation ID"),
|
|
1275
1315
|
note: z.string().describe("Note text to add"),
|
|
1276
1316
|
},
|
|
1277
|
-
|
|
1317
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
1318
|
+
gatedHandler("create_reservation_note", async (params) => {
|
|
1278
1319
|
try {
|
|
1279
1320
|
const data = await guestyPost(`/reservations/${params.reservationId}/notes`, {
|
|
1280
1321
|
body: params.note,
|
|
@@ -1289,7 +1330,7 @@ server.tool(
|
|
|
1289
1330
|
return { content: [{ type: "text", text: JSON.stringify({ error: "Failed to add note.", details: e2.message }, null, 2) }] };
|
|
1290
1331
|
}
|
|
1291
1332
|
}
|
|
1292
|
-
}
|
|
1333
|
+
})
|
|
1293
1334
|
);
|
|
1294
1335
|
|
|
1295
1336
|
// Tool 39: Get Listing Pricing
|
|
@@ -1299,6 +1340,7 @@ server.tool(
|
|
|
1299
1340
|
{
|
|
1300
1341
|
listingId: z.string().describe("The listing ID"),
|
|
1301
1342
|
},
|
|
1343
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1302
1344
|
async (params) => {
|
|
1303
1345
|
const data = await guestyGet(`/listings/${params.listingId}`, {
|
|
1304
1346
|
fields: "prices terms financials title nickname",
|
|
@@ -1340,7 +1382,8 @@ server.tool(
|
|
|
1340
1382
|
monthlyPriceFactor: z.number().optional().describe("Monthly discount factor (e.g., 0.8 for 20% off)"),
|
|
1341
1383
|
currency: z.string().optional().describe("Currency code (e.g., USD)"),
|
|
1342
1384
|
},
|
|
1343
|
-
|
|
1385
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1386
|
+
gatedHandler("update_listing_pricing", async (params) => {
|
|
1344
1387
|
const prices = {};
|
|
1345
1388
|
if (params.basePrice !== undefined) prices.basePrice = params.basePrice;
|
|
1346
1389
|
if (params.cleaningFee !== undefined) prices.cleaningFee = params.cleaningFee;
|
|
@@ -1352,6 +1395,21 @@ server.tool(
|
|
|
1352
1395
|
const data = await guestyPut(`/listings/${params.listingId}`, { prices });
|
|
1353
1396
|
const updated = Object.keys(prices).join(", ");
|
|
1354
1397
|
return { content: [{ type: "text", text: `Pricing updated for ${params.listingId}. Fields changed: ${updated}` }] };
|
|
1398
|
+
})
|
|
1399
|
+
);
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
// License Info Tool
|
|
1403
|
+
server.tool(
|
|
1404
|
+
"get_license_info",
|
|
1405
|
+
"Show current license tier, available tools, and upgrade information.",
|
|
1406
|
+
{},
|
|
1407
|
+
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1408
|
+
async () => {
|
|
1409
|
+
const info = getTierInfo();
|
|
1410
|
+
info.freeTools = FREE_TOOLS;
|
|
1411
|
+
info.upgradeUrl = "https://guestycopilot.com/pricing";
|
|
1412
|
+
return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
|
|
1355
1413
|
}
|
|
1356
1414
|
);
|
|
1357
1415
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
**To:** pmsupport@tripadvisor.com
|
|
2
|
+
**From:** DLJrealty@yahoo.com
|
|
3
|
+
**Subject:** API Access Request — Vacation Rental Property Manager (7 Properties)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Hi TripAdvisor Rentals Team,
|
|
8
|
+
|
|
9
|
+
I manage 7 short-term rental properties across Colorado and Miami through DLJ Properties. I'd like to request API access to programmatically manage our TripAdvisor Rentals listings.
|
|
10
|
+
|
|
11
|
+
Our portfolio:
|
|
12
|
+
- 4 boutique tiny homes in Woodland Park, Colorado (Pikes Peak area)
|
|
13
|
+
- 3 apartments in Miami, Florida
|
|
14
|
+
- 628+ five-star reviews across platforms
|
|
15
|
+
- Airbnb Superhost, every quarter
|
|
16
|
+
|
|
17
|
+
We use Guesty as our PMS and would like to integrate with TripAdvisor's Content Connect API to manage listings, calendars, rates, and photos.
|
|
18
|
+
|
|
19
|
+
Our website: https://tinyhomeboutiques.com
|
|
20
|
+
Our properties: https://tinyhomeboutiques.com/unit-y-boutique-tinyhome.html
|
|
21
|
+
|
|
22
|
+
Could you provide API access and documentation? Happy to discuss our setup and integration needs.
|
|
23
|
+
|
|
24
|
+
Best,
|
|
25
|
+
Danny Perez
|
|
26
|
+
DLJ Properties
|
|
27
|
+
DLJrealty@yahoo.com
|