@striderlabs/mcp-opentable 0.1.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/README.md +226 -0
- package/dist/auth.d.ts +36 -0
- package/dist/auth.js +97 -0
- package/dist/browser.d.ts +140 -0
- package/dist/browser.js +608 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +412 -0
- package/package.json +39 -0
- package/server.json +20 -0
- package/src/auth.ts +122 -0
- package/src/browser.ts +877 -0
- package/src/index.ts +503 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# @striderlabs/mcp-opentable
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@striderlabs/mcp-opentable)
|
|
4
|
+
[](https://registry.modelcontextprotocol.io/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
MCP server for OpenTable — let AI agents search restaurants and book reservations.
|
|
8
|
+
|
|
9
|
+
Built by [Strider Labs](https://striderlabs.ai).
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Search restaurants by location, cuisine, party size, date and time
|
|
14
|
+
- Get detailed restaurant info — description, address, hours, and features
|
|
15
|
+
- Check real-time availability for any date, time, and party size
|
|
16
|
+
- Book reservations with a confirmation step before committing
|
|
17
|
+
- View all upcoming reservations in one place
|
|
18
|
+
- Cancel reservations safely with a confirm gate
|
|
19
|
+
- Persistent sessions — stay logged in across restarts
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g @striderlabs/mcp-opentable
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or with npx:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx @striderlabs/mcp-opentable
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
Add to your MCP client configuration (e.g., Claude Desktop `~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"opentable": {
|
|
41
|
+
"command": "npx",
|
|
42
|
+
"args": ["-y", "@striderlabs/mcp-opentable"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Authentication
|
|
49
|
+
|
|
50
|
+
This connector uses browser automation via Playwright. On first use:
|
|
51
|
+
|
|
52
|
+
1. Call `opentable_status` — it returns a login URL
|
|
53
|
+
2. Open the login URL in your browser and sign in to OpenTable
|
|
54
|
+
3. Session cookies are automatically saved to `~/.strider/opentable/cookies.json`
|
|
55
|
+
4. Sessions persist across restarts — no need to log in again
|
|
56
|
+
|
|
57
|
+
To log out or reset your session:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
opentable_logout
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Available Tools
|
|
64
|
+
|
|
65
|
+
### Session Management
|
|
66
|
+
|
|
67
|
+
| Tool | Description |
|
|
68
|
+
|------|-------------|
|
|
69
|
+
| `opentable_status` | Check login status; returns login URL if not authenticated |
|
|
70
|
+
| `opentable_login` | Get the OpenTable login URL and instructions |
|
|
71
|
+
| `opentable_logout` | Clear stored session cookies (log out) |
|
|
72
|
+
|
|
73
|
+
### Discovery
|
|
74
|
+
|
|
75
|
+
| Tool | Description |
|
|
76
|
+
|------|-------------|
|
|
77
|
+
| `opentable_search` | Search restaurants by location, cuisine, party size, date/time |
|
|
78
|
+
| `opentable_get_restaurant` | Get full details for a specific restaurant |
|
|
79
|
+
| `opentable_check_availability` | List available time slots for a restaurant |
|
|
80
|
+
|
|
81
|
+
### Reservations
|
|
82
|
+
|
|
83
|
+
| Tool | Description |
|
|
84
|
+
|------|-------------|
|
|
85
|
+
| `opentable_make_reservation` | Book a reservation (requires `confirm=true`) |
|
|
86
|
+
| `opentable_get_reservations` | List all upcoming reservations |
|
|
87
|
+
| `opentable_cancel_reservation` | Cancel a reservation (requires `confirm=true`) |
|
|
88
|
+
|
|
89
|
+
## Example Usage
|
|
90
|
+
|
|
91
|
+
### Search for restaurants
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"tool": "opentable_search",
|
|
96
|
+
"arguments": {
|
|
97
|
+
"location": "San Francisco",
|
|
98
|
+
"cuisine": "italian",
|
|
99
|
+
"partySize": 4,
|
|
100
|
+
"date": "2026-03-20",
|
|
101
|
+
"time": "19:30"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Check availability
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"tool": "opentable_check_availability",
|
|
111
|
+
"arguments": {
|
|
112
|
+
"restaurantId": "restaurant-slug-or-id",
|
|
113
|
+
"date": "2026-03-20",
|
|
114
|
+
"time": "19:30",
|
|
115
|
+
"partySize": 4
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Book a reservation (two-step)
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
// Step 1: Preview
|
|
124
|
+
{
|
|
125
|
+
"tool": "opentable_make_reservation",
|
|
126
|
+
"arguments": {
|
|
127
|
+
"restaurantId": "restaurant-slug-or-id",
|
|
128
|
+
"date": "2026-03-20",
|
|
129
|
+
"time": "19:30",
|
|
130
|
+
"partySize": 4,
|
|
131
|
+
"specialRequests": "Window table if possible",
|
|
132
|
+
"confirm": false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Step 2: Confirm and book
|
|
137
|
+
{
|
|
138
|
+
"tool": "opentable_make_reservation",
|
|
139
|
+
"arguments": {
|
|
140
|
+
"restaurantId": "restaurant-slug-or-id",
|
|
141
|
+
"date": "2026-03-20",
|
|
142
|
+
"time": "19:30",
|
|
143
|
+
"partySize": 4,
|
|
144
|
+
"specialRequests": "Window table if possible",
|
|
145
|
+
"confirm": true
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### View upcoming reservations
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"tool": "opentable_get_reservations",
|
|
155
|
+
"arguments": {}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Cancel a reservation
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
// Step 1: Preview cancellation
|
|
163
|
+
{
|
|
164
|
+
"tool": "opentable_cancel_reservation",
|
|
165
|
+
"arguments": {
|
|
166
|
+
"reservationId": "res-abc123",
|
|
167
|
+
"confirm": false
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Step 2: Confirm cancellation
|
|
172
|
+
{
|
|
173
|
+
"tool": "opentable_cancel_reservation",
|
|
174
|
+
"arguments": {
|
|
175
|
+
"reservationId": "res-abc123",
|
|
176
|
+
"confirm": true
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Requirements
|
|
182
|
+
|
|
183
|
+
- Node.js 18+
|
|
184
|
+
- Playwright browsers (auto-installed on first run via `playwright install chromium`)
|
|
185
|
+
|
|
186
|
+
## How It Works
|
|
187
|
+
|
|
188
|
+
This connector uses Playwright for browser automation:
|
|
189
|
+
|
|
190
|
+
1. **Headless Chrome** — runs a real browser in the background
|
|
191
|
+
2. **Cookie persistence** — maintains logged-in state across sessions
|
|
192
|
+
3. **Stealth mode** — uses realistic browser fingerprints to avoid detection
|
|
193
|
+
4. **Structured responses** — all data returned as JSON
|
|
194
|
+
|
|
195
|
+
## Security
|
|
196
|
+
|
|
197
|
+
- Session cookies are stored locally in `~/.strider/opentable/cookies.json`
|
|
198
|
+
- No credentials are stored — authentication uses browser-based login
|
|
199
|
+
- Cookies are only readable by the current OS user
|
|
200
|
+
|
|
201
|
+
## Limitations
|
|
202
|
+
|
|
203
|
+
- OpenTable must be available in your region
|
|
204
|
+
- Some reservation flows may require additional profile information on your OpenTable account
|
|
205
|
+
- Bot-detection countermeasures on OpenTable's site may occasionally interrupt automation
|
|
206
|
+
|
|
207
|
+
## Development
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
git clone https://github.com/markswendsen-code/mcp-opentable.git
|
|
211
|
+
cd mcp-opentable
|
|
212
|
+
npm install
|
|
213
|
+
npx playwright install chromium
|
|
214
|
+
npm run build
|
|
215
|
+
npm start
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT © [Strider Labs](https://striderlabs.ai)
|
|
221
|
+
|
|
222
|
+
## Related
|
|
223
|
+
|
|
224
|
+
- [@striderlabs/mcp-doordash](https://www.npmjs.com/package/@striderlabs/mcp-doordash) - DoorDash MCP connector
|
|
225
|
+
- [@striderlabs/mcp-gmail](https://www.npmjs.com/package/@striderlabs/mcp-gmail) - Gmail MCP connector
|
|
226
|
+
- [Model Context Protocol](https://modelcontextprotocol.io) - Learn more about MCP
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTable Authentication & Session Management
|
|
3
|
+
*
|
|
4
|
+
* Handles cookie persistence and login state detection.
|
|
5
|
+
*/
|
|
6
|
+
import type { BrowserContext } from "playwright";
|
|
7
|
+
export interface AuthState {
|
|
8
|
+
isLoggedIn: boolean;
|
|
9
|
+
email?: string;
|
|
10
|
+
firstName?: string;
|
|
11
|
+
lastName?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Save cookies from browser context to disk
|
|
15
|
+
*/
|
|
16
|
+
export declare function saveCookies(context: BrowserContext): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Load cookies from disk and add to browser context
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadCookies(context: BrowserContext): Promise<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* Clear stored cookies
|
|
23
|
+
*/
|
|
24
|
+
export declare function clearCookies(): void;
|
|
25
|
+
/**
|
|
26
|
+
* Check if we have stored cookies
|
|
27
|
+
*/
|
|
28
|
+
export declare function hasStoredCookies(): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Extract auth state from OpenTable page cookies
|
|
31
|
+
*/
|
|
32
|
+
export declare function getAuthState(context: BrowserContext): Promise<AuthState>;
|
|
33
|
+
/**
|
|
34
|
+
* Get the path where cookies are stored
|
|
35
|
+
*/
|
|
36
|
+
export declare function getCookiesPath(): string;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTable Authentication & Session Management
|
|
3
|
+
*
|
|
4
|
+
* Handles cookie persistence and login state detection.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
// Cookie storage location: ~/.strider/opentable/
|
|
10
|
+
const CONFIG_DIR = join(homedir(), ".strider", "opentable");
|
|
11
|
+
const COOKIES_FILE = join(CONFIG_DIR, "cookies.json");
|
|
12
|
+
/**
|
|
13
|
+
* Ensure config directory exists
|
|
14
|
+
*/
|
|
15
|
+
function ensureConfigDir() {
|
|
16
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
17
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Save cookies from browser context to disk
|
|
22
|
+
*/
|
|
23
|
+
export async function saveCookies(context) {
|
|
24
|
+
ensureConfigDir();
|
|
25
|
+
const cookies = await context.cookies();
|
|
26
|
+
writeFileSync(COOKIES_FILE, JSON.stringify(cookies, null, 2));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Load cookies from disk and add to browser context
|
|
30
|
+
*/
|
|
31
|
+
export async function loadCookies(context) {
|
|
32
|
+
if (!existsSync(COOKIES_FILE)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const cookiesData = readFileSync(COOKIES_FILE, "utf-8");
|
|
37
|
+
const cookies = JSON.parse(cookiesData);
|
|
38
|
+
if (cookies.length > 0) {
|
|
39
|
+
await context.addCookies(cookies);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error("Failed to load cookies:", error);
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Clear stored cookies
|
|
50
|
+
*/
|
|
51
|
+
export function clearCookies() {
|
|
52
|
+
if (existsSync(COOKIES_FILE)) {
|
|
53
|
+
writeFileSync(COOKIES_FILE, "[]");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check if we have stored cookies
|
|
58
|
+
*/
|
|
59
|
+
export function hasStoredCookies() {
|
|
60
|
+
if (!existsSync(COOKIES_FILE)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const cookiesData = readFileSync(COOKIES_FILE, "utf-8");
|
|
65
|
+
const cookies = JSON.parse(cookiesData);
|
|
66
|
+
return Array.isArray(cookies) && cookies.length > 0;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Extract auth state from OpenTable page cookies
|
|
74
|
+
*/
|
|
75
|
+
export async function getAuthState(context) {
|
|
76
|
+
const cookies = await context.cookies("https://www.opentable.com");
|
|
77
|
+
// OpenTable uses various session/auth cookies
|
|
78
|
+
const sessionCookie = cookies.find((c) => c.name === "OT_SESSION" ||
|
|
79
|
+
c.name === "ot_session" ||
|
|
80
|
+
c.name === "otd" ||
|
|
81
|
+
c.name === "OTUserInfo" ||
|
|
82
|
+
c.name === "ot_userid");
|
|
83
|
+
if (sessionCookie) {
|
|
84
|
+
return {
|
|
85
|
+
isLoggedIn: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
isLoggedIn: false,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get the path where cookies are stored
|
|
94
|
+
*/
|
|
95
|
+
export function getCookiesPath() {
|
|
96
|
+
return COOKIES_FILE;
|
|
97
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTable Browser Automation
|
|
3
|
+
*
|
|
4
|
+
* Playwright-based automation for OpenTable reservation operations.
|
|
5
|
+
*/
|
|
6
|
+
import { AuthState } from "./auth.js";
|
|
7
|
+
export interface Restaurant {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
cuisine: string;
|
|
11
|
+
location: string;
|
|
12
|
+
neighborhood?: string;
|
|
13
|
+
rating?: number;
|
|
14
|
+
reviewCount?: number;
|
|
15
|
+
priceRange?: string;
|
|
16
|
+
imageUrl?: string;
|
|
17
|
+
profileUrl?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface RestaurantDetails extends Restaurant {
|
|
20
|
+
description?: string;
|
|
21
|
+
address?: string;
|
|
22
|
+
phone?: string;
|
|
23
|
+
hours?: string;
|
|
24
|
+
website?: string;
|
|
25
|
+
features?: string[];
|
|
26
|
+
}
|
|
27
|
+
export interface AvailabilitySlot {
|
|
28
|
+
time: string;
|
|
29
|
+
partySize: number;
|
|
30
|
+
date: string;
|
|
31
|
+
reservationToken?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface Reservation {
|
|
34
|
+
id: string;
|
|
35
|
+
restaurantName: string;
|
|
36
|
+
date: string;
|
|
37
|
+
time: string;
|
|
38
|
+
partySize: number;
|
|
39
|
+
status: string;
|
|
40
|
+
confirmationNumber?: string;
|
|
41
|
+
specialRequests?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check if user is logged in to OpenTable
|
|
45
|
+
*/
|
|
46
|
+
export declare function checkAuth(): Promise<AuthState>;
|
|
47
|
+
/**
|
|
48
|
+
* Return login URL and instructions for the user to authenticate
|
|
49
|
+
*/
|
|
50
|
+
export declare function getLoginUrl(): Promise<{
|
|
51
|
+
url: string;
|
|
52
|
+
instructions: string;
|
|
53
|
+
}>;
|
|
54
|
+
/**
|
|
55
|
+
* Search restaurants on OpenTable
|
|
56
|
+
*/
|
|
57
|
+
export declare function searchRestaurants(params: {
|
|
58
|
+
location: string;
|
|
59
|
+
cuisine?: string;
|
|
60
|
+
partySize?: number;
|
|
61
|
+
date?: string;
|
|
62
|
+
time?: string;
|
|
63
|
+
}): Promise<{
|
|
64
|
+
success: boolean;
|
|
65
|
+
restaurants?: Restaurant[];
|
|
66
|
+
error?: string;
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* Get detailed information about a specific restaurant
|
|
70
|
+
*/
|
|
71
|
+
export declare function getRestaurantDetails(restaurantId: string): Promise<{
|
|
72
|
+
success: boolean;
|
|
73
|
+
restaurant?: RestaurantDetails;
|
|
74
|
+
error?: string;
|
|
75
|
+
}>;
|
|
76
|
+
/**
|
|
77
|
+
* Check available reservation times for a restaurant
|
|
78
|
+
*/
|
|
79
|
+
export declare function checkAvailability(params: {
|
|
80
|
+
restaurantId: string;
|
|
81
|
+
date: string;
|
|
82
|
+
time: string;
|
|
83
|
+
partySize: number;
|
|
84
|
+
}): Promise<{
|
|
85
|
+
success: boolean;
|
|
86
|
+
slots?: AvailabilitySlot[];
|
|
87
|
+
restaurantName?: string;
|
|
88
|
+
error?: string;
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* Make a reservation at a restaurant
|
|
92
|
+
*/
|
|
93
|
+
export declare function makeReservation(params: {
|
|
94
|
+
restaurantId: string;
|
|
95
|
+
date: string;
|
|
96
|
+
time: string;
|
|
97
|
+
partySize: number;
|
|
98
|
+
firstName?: string;
|
|
99
|
+
lastName?: string;
|
|
100
|
+
email?: string;
|
|
101
|
+
phone?: string;
|
|
102
|
+
specialRequests?: string;
|
|
103
|
+
confirm: boolean;
|
|
104
|
+
}): Promise<{
|
|
105
|
+
success: boolean;
|
|
106
|
+
reservation?: Partial<Reservation>;
|
|
107
|
+
requiresConfirmation?: boolean;
|
|
108
|
+
preview?: {
|
|
109
|
+
restaurantName: string;
|
|
110
|
+
date: string;
|
|
111
|
+
time: string;
|
|
112
|
+
partySize: number;
|
|
113
|
+
specialRequests?: string;
|
|
114
|
+
};
|
|
115
|
+
error?: string;
|
|
116
|
+
}>;
|
|
117
|
+
/**
|
|
118
|
+
* Get list of upcoming reservations
|
|
119
|
+
*/
|
|
120
|
+
export declare function getReservations(): Promise<{
|
|
121
|
+
success: boolean;
|
|
122
|
+
reservations?: Reservation[];
|
|
123
|
+
error?: string;
|
|
124
|
+
}>;
|
|
125
|
+
/**
|
|
126
|
+
* Cancel a reservation
|
|
127
|
+
*/
|
|
128
|
+
export declare function cancelReservation(params: {
|
|
129
|
+
reservationId: string;
|
|
130
|
+
confirm: boolean;
|
|
131
|
+
}): Promise<{
|
|
132
|
+
success: boolean;
|
|
133
|
+
requiresConfirmation?: boolean;
|
|
134
|
+
message?: string;
|
|
135
|
+
error?: string;
|
|
136
|
+
}>;
|
|
137
|
+
/**
|
|
138
|
+
* Cleanup browser resources
|
|
139
|
+
*/
|
|
140
|
+
export declare function cleanup(): Promise<void>;
|