#
tokens: 14180/50000 12/12 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── Dockerfile
├── index.ts
├── LICENSE
├── manifest.json
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── sync-version.js
├── test-extension.js
├── tsconfig.json
├── types.d.ts
└── util.ts
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | dist
2 | node_modules
3 | *.dxt
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Airbnb Search & Listings - Desktop Extension (DXT)
  2 | 
  3 | A comprehensive Desktop Extension for searching Airbnb listings with advanced filtering capabilities and detailed property information retrieval. Built as a Model Context Protocol (MCP) server packaged in the Desktop Extension (DXT) format for easy installation and use with compatible AI applications.
  4 | 
  5 | ## Features
  6 | 
  7 | ### 🔍 Advanced Search Capabilities
  8 | - **Location-based search** with support for cities, states, and regions
  9 | - **Google Maps Place ID** integration for precise location targeting
 10 | - **Date filtering** with check-in and check-out date support
 11 | - **Guest configuration** including adults, children, infants, and pets
 12 | - **Price range filtering** with minimum and maximum price constraints
 13 | - **Pagination support** for browsing through large result sets
 14 | 
 15 | ### 🏠 Detailed Property Information
 16 | - **Comprehensive listing details** including amenities, policies, and highlights
 17 | - **Location information** with coordinates and neighborhood details
 18 | - **House rules and policies** for informed booking decisions
 19 | - **Property descriptions** and key features
 20 | - **Direct links** to Airbnb listings for easy booking
 21 | 
 22 | ### 🛡️ Security & Compliance
 23 | - **Robots.txt compliance** with configurable override for testing
 24 | - **Request timeout management** to prevent hanging requests
 25 | - **Enhanced error handling** with detailed logging
 26 | - **Rate limiting awareness** and respectful API usage
 27 | - **Secure configuration** through DXT user settings
 28 | 
 29 | ## Installation
 30 | 
 31 | ### For Claude Desktop
 32 | This extension is packaged as a Desktop Extension (DXT) file. To install:
 33 | 
 34 | 1. Download the `.dxt` file from the releases page
 35 | 2. Open your compatible AI application (e.g., Claude Desktop)
 36 | 3. Install the extension through the application's extension manager
 37 | 4. Configure the extension settings as needed
 38 | 
 39 | ### For Cursor, etc.
 40 | 
 41 | Before starting make sure [Node.js](https://nodejs.org/) is installed on your desktop for `npx` to work.
 42 | 1. Go to: Cursor Settings > Tools & Integrations > New MCP Server
 43 | 
 44 | 2. Add one the following to your `mcp.json`:
 45 |     ```json
 46 |     {
 47 |       "mcpServers": {
 48 |         "airbnb": {
 49 |           "command": "npx",
 50 |           "args": [
 51 |             "-y",
 52 |             "@openbnb/mcp-server-airbnb"
 53 |           ]
 54 |         }
 55 |       }
 56 |     }
 57 |     ```
 58 | 
 59 |     To ignore robots.txt for all requests, use this version with `--ignore-robots-txt` args
 60 | 
 61 |     ```json
 62 |     {
 63 |       "mcpServers": {
 64 |         "airbnb": {
 65 |           "command": "npx",
 66 |           "args": [
 67 |             "-y",
 68 |             "@openbnb/mcp-server-airbnb",
 69 |             "--ignore-robots-txt"
 70 |           ]
 71 |         }
 72 |       }
 73 |     }
 74 |     ```
 75 | 3. Restart.
 76 | 
 77 | 
 78 | ## Configuration
 79 | 
 80 | The extension provides the following user-configurable options:
 81 | 
 82 | ### Ignore robots.txt
 83 | - **Type**: Boolean (checkbox)
 84 | - **Default**: `false`
 85 | - **Description**: Bypass robots.txt restrictions when making requests to Airbnb
 86 | - **Recommendation**: Keep disabled unless needed for testing purposes
 87 | 
 88 | ## Tools
 89 | 
 90 | ### `airbnb_search`
 91 | 
 92 | Search for Airbnb listings with comprehensive filtering options.
 93 | 
 94 | **Parameters:**
 95 | - `location` (required): Location to search (e.g., "San Francisco, CA")
 96 | - `placeId` (optional): Google Maps Place ID (overrides location)
 97 | - `checkin` (optional): Check-in date in YYYY-MM-DD format
 98 | - `checkout` (optional): Check-out date in YYYY-MM-DD format
 99 | - `adults` (optional): Number of adults (default: 1)
100 | - `children` (optional): Number of children (default: 0)
101 | - `infants` (optional): Number of infants (default: 0)
102 | - `pets` (optional): Number of pets (default: 0)
103 | - `minPrice` (optional): Minimum price per night
104 | - `maxPrice` (optional): Maximum price per night
105 | - `cursor` (optional): Pagination cursor for browsing results
106 | - `ignoreRobotsText` (optional): Override robots.txt for this request
107 | 
108 | **Returns:**
109 | - Search results with property details, pricing, and direct links
110 | - Pagination information for browsing additional results
111 | - Search URL for reference
112 | 
113 | ### `airbnb_listing_details`
114 | 
115 | Get detailed information about a specific Airbnb listing.
116 | 
117 | **Parameters:**
118 | - `id` (required): Airbnb listing ID
119 | - `checkin` (optional): Check-in date in YYYY-MM-DD format
120 | - `checkout` (optional): Check-out date in YYYY-MM-DD format
121 | - `adults` (optional): Number of adults (default: 1)
122 | - `children` (optional): Number of children (default: 0)
123 | - `infants` (optional): Number of infants (default: 0)
124 | - `pets` (optional): Number of pets (default: 0)
125 | - `ignoreRobotsText` (optional): Override robots.txt for this request
126 | 
127 | **Returns:**
128 | - Detailed property information including:
129 |   - Location details with coordinates
130 |   - Amenities and facilities
131 |   - House rules and policies
132 |   - Property highlights and descriptions
133 |   - Direct link to the listing
134 | 
135 | ## Technical Details
136 | 
137 | ### Architecture
138 | - **Runtime**: Node.js 18+
139 | - **Protocol**: Model Context Protocol (MCP) via stdio transport
140 | - **Format**: Desktop Extension (DXT) v0.1
141 | - **Dependencies**: Minimal external dependencies for security and reliability
142 | 
143 | ### Error Handling
144 | - Comprehensive error logging with timestamps
145 | - Graceful degradation when Airbnb's page structure changes
146 | - Timeout protection for network requests
147 | - Detailed error messages for troubleshooting
148 | 
149 | ### Security Measures
150 | - Robots.txt compliance by default
151 | - Request timeout limits
152 | - Input validation and sanitization
153 | - Secure environment variable handling
154 | - No sensitive data storage
155 | 
156 | ### Performance
157 | - Efficient HTML parsing with Cheerio
158 | - Request caching where appropriate
159 | - Minimal memory footprint
160 | - Fast startup and response times
161 | 
162 | ## Compatibility
163 | 
164 | - **Platforms**: macOS, Windows, Linux
165 | - **Node.js**: 18.0.0 or higher
166 | - **Claude Desktop**: 0.10.0 or higher
167 | - **Other MCP clients**: Compatible with any MCP-supporting application
168 | 
169 | ## Development
170 | 
171 | ### Building from Source
172 | 
173 | ```bash
174 | # Install dependencies
175 | npm install
176 | 
177 | # Build the project
178 | npm run build
179 | 
180 | # Watch for changes during development
181 | npm run watch
182 | ```
183 | 
184 | ### Testing
185 | 
186 | The extension can be tested by running the MCP server directly:
187 | 
188 | ```bash
189 | # Run with robots.txt compliance (default)
190 | node dist/index.js
191 | 
192 | # Run with robots.txt ignored (for testing)
193 | node dist/index.js --ignore-robots-txt
194 | ```
195 | 
196 | ## Legal and Ethical Considerations
197 | 
198 | - **Respect Airbnb's Terms of Service**: This extension is for legitimate research and booking assistance
199 | - **Robots.txt Compliance**: The extension respects robots.txt by default
200 | - **Rate Limiting**: Be mindful of request frequency to avoid overwhelming Airbnb's servers
201 | - **Data Usage**: Only extract publicly available information for legitimate purposes
202 | 
203 | ## Support
204 | 
205 | - **Issues**: Report bugs and feature requests on [GitHub Issues](https://github.com/openbnb-org/mcp-server-airbnb/issues)
206 | - **Documentation**: Additional documentation available in the repository
207 | - **Community**: Join discussions about MCP and DXT development
208 | 
209 | ## License
210 | 
211 | MIT License - see [LICENSE](LICENSE) file for details.
212 | 
213 | ## Contributing
214 | 
215 | Contributions are welcome! Please read the contributing guidelines and submit pull requests for any improvements.
216 | 
217 | ---
218 | 
219 | **Note**: This extension is not affiliated with Airbnb, Inc. It is an independent tool designed to help users search and analyze publicly available Airbnb listings.
220 | 
```

--------------------------------------------------------------------------------
/types.d.ts:
--------------------------------------------------------------------------------

```typescript
 1 | declare module 'robots-parser' {
 2 |   interface RobotsParser {
 3 |     isAllowed(url: string, userAgent?: string): boolean;
 4 |   }
 5 | 
 6 |   function robotsParser(url: string, content: string): RobotsParser;
 7 |   
 8 |   export default robotsParser;
 9 | }
10 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2020",
 4 |     "module": "NodeNext",
 5 |     "moduleResolution": "NodeNext",
 6 |     "esModuleInterop": true,
 7 |     "strict": true,
 8 |     "outDir": "./dist",
 9 |     "rootDir": ".",
10 |     "declaration": true,
11 |     "skipLibCheck": true,
12 |     "forceConsistentCasingInFileNames": true
13 |   },
14 |   "include": [
15 |     "./**/*.ts"
16 |   ],
17 |   "exclude": [
18 |     "node_modules",
19 |     "dist"
20 |   ]
21 | }
22 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | FROM node:lts-alpine
 3 | 
 4 | WORKDIR /app
 5 | 
 6 | # Copy package files and install dependencies without running prepare scripts
 7 | COPY package*.json ./
 8 | RUN npm install --ignore-scripts
 9 | 
10 | # Copy rest of the source code
11 | COPY . .
12 | 
13 | # Build the project explicitly
14 | RUN npm run build
15 | 
16 | # Expose the MCP server on stdio
17 | CMD [ "node", "dist/index.js" ]
18 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     properties:
 9 |       ignoreRobotsTxt:
10 |         type: boolean
11 |         default: false
12 |         description: If set to true, the server will ignore robots.txt rules.
13 |   commandFunction:
14 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
15 |     |-
16 |     (config) => { const args = ['dist/index.js']; if (config.ignoreRobotsTxt) { args.push('--ignore-robots-txt'); } return { command: 'node', args }; }
17 |   exampleConfig:
18 |     ignoreRobotsTxt: false
19 | 
```

--------------------------------------------------------------------------------
/sync-version.js:
--------------------------------------------------------------------------------

```javascript
 1 | #!/usr/bin/env node
 2 | 
 3 | import fs from 'fs';
 4 | import path from 'path';
 5 | import { fileURLToPath } from 'url';
 6 | 
 7 | const __filename = fileURLToPath(import.meta.url);
 8 | const __dirname = path.dirname(__filename);
 9 | 
10 | // Read package.json
11 | const packageJsonPath = path.join(__dirname, 'package.json');
12 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
13 | 
14 | // Read manifest.json
15 | const manifestJsonPath = path.join(__dirname, 'manifest.json');
16 | const manifestJson = JSON.parse(fs.readFileSync(manifestJsonPath, 'utf8'));
17 | 
18 | // Update manifest version to match package.json version
19 | manifestJson.version = packageJson.version;
20 | 
21 | // Write updated manifest.json
22 | fs.writeFileSync(manifestJsonPath, JSON.stringify(manifestJson, null, 2) + '\n');
23 | 
24 | console.log(`✅ Synced version to ${packageJson.version} in manifest.json`);
25 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@openbnb/mcp-server-airbnb",
 3 |   "version": "0.1.3",
 4 |   "description": "MCP server for Airbnb search and listing details",
 5 |   "license": "MIT",
 6 |   "type": "module",
 7 |   "author": "OpenBnB (https://openbnb.org)",
 8 |   "keywords": [
 9 |     "airbnb",
10 |     "vacation rental",
11 |     "travel"
12 |   ],
13 |   "publishConfig": {
14 |     "access": "public"
15 |   },
16 |   "bin": {
17 |     "mcp-server-airbnb": "dist/index.js"
18 |   },
19 |   "files": [
20 |     "dist",
21 |     "sync-version.js"
22 |   ],
23 |   "scripts": {
24 |     "build": "node sync-version.js && tsc && shx chmod +x dist/*.js",
25 |     "prepare": "npm run build",
26 |     "watch": "tsc --watch",
27 |     "sync-version": "node sync-version.js"
28 |   },
29 |   "dependencies": {
30 |     "@modelcontextprotocol/sdk": "^1.0.1",
31 |     "cheerio": "^1.0.0",
32 |     "node-fetch": "^3.3.2",
33 |     "robots-parser": "^3.0.1"
34 |   },
35 |   "devDependencies": {
36 |     "@types/node": "^22.13.9",
37 |     "@types/node-fetch": "^2.6.12",
38 |     "shx": "^0.3.4",
39 |     "typescript": "^5.8.2"
40 |   }
41 | }
42 | 
```

--------------------------------------------------------------------------------
/util.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export function cleanObject(obj: any) {
 2 |   Object.keys(obj).forEach(key => {
 3 |     if (!obj[key] || key === "__typename") {
 4 |       delete obj[key];
 5 |     } else if (typeof obj[key] === "object") {
 6 |       cleanObject(obj[key]);
 7 |     }
 8 |   });
 9 | }
10 | 
11 | export function pickBySchema(obj: any, schema: any): any {
12 |   if (typeof obj !== 'object' || obj === null) return obj;
13 |   
14 |   // If the object is an array, process each item
15 |   if (Array.isArray(obj)) {
16 |     return obj.map(item => pickBySchema(item, schema));
17 |   }
18 |   
19 |   const result: Record<string, any> = {};
20 |   for (const key in schema) {
21 |     if (Object.prototype.hasOwnProperty.call(obj, key)) {
22 |       const rule = schema[key];
23 |       // If the rule is true, copy the value as-is
24 |       if (rule === true) {
25 |         result[key] = obj[key];
26 |       }
27 |       // If the rule is an object, apply the schema recursively
28 |       else if (typeof rule === 'object' && rule !== null) {
29 |         result[key] = pickBySchema(obj[key], rule);
30 |       }
31 |     }
32 |   }
33 |   return result;
34 | }
35 | 
36 | export function flattenArraysInObject(input: any, inArray: boolean = false): any {
37 |   if (Array.isArray(input)) {
38 |     // Process each item in the array with inArray=true so that any object
39 |     // inside the array is flattened to a string.
40 |     const flatItems = input.map(item => flattenArraysInObject(item, true));
41 |     return flatItems.join(', ');
42 |   } else if (typeof input === 'object' && input !== null) {
43 |     if (inArray) {
44 |       // When inside an array, ignore the keys and flatten the object's values.
45 |       const values = Object.values(input).map(value => flattenArraysInObject(value, true));
46 |       return values.join(': ');
47 |     } else {
48 |       // When not in an array, process each property recursively.
49 |       const result: Record<string, any> = {};
50 |       for (const key in input) {
51 |         if (Object.prototype.hasOwnProperty.call(input, key)) {
52 |           result[key] = flattenArraysInObject(input[key], false);
53 |         }
54 |       }
55 |       return result;
56 |     }
57 |   } else {
58 |     // For primitives, simply return the value.
59 |     return input;
60 |   }
61 | }
```

--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "dxt_version": "0.1",
 3 |   "name": "airbnb-search",
 4 |   "display_name": "Airbnb Search & Listings",
 5 |   "version": "0.1.3",
 6 |   "description": "Search Airbnb listings with advanced filtering and get detailed property information",
 7 |   "long_description": "A comprehensive Desktop Extension for searching Airbnb listings with various filters including location, dates, guest count, price range, and more. Get detailed information about specific properties including amenities, policies, location details, and highlights. Respects robots.txt by default with option to override for testing purposes.",
 8 |   "author": {
 9 |     "name": "OpenBnB",
10 |     "email": "[email protected]",
11 |     "url": "https://www.openbnb.org/"
12 |   },
13 |   "repository": {
14 |     "type": "git",
15 |     "url": "https://github.com/openbnb-org/mcp-server-airbnb"
16 |   },
17 |   "homepage": "https://github.com/openbnb-org/mcp-server-airbnb",
18 |   "documentation": "https://github.com/openbnb-org/mcp-server-airbnb#readme",
19 |   "support": "https://github.com/openbnb-org/mcp-server-airbnb/issues",
20 |   "license": "MIT",
21 |   "keywords": [
22 |     "airbnb",
23 |     "vacation rental",
24 |     "travel"
25 |   ],
26 |   "server": {
27 |     "type": "node",
28 |     "entry_point": "dist/index.js",
29 |     "mcp_config": {
30 |       "command": "node",
31 |       "args": [
32 |         "${__dirname}/dist/index.js"
33 |       ],
34 |       "env": {
35 |         "NODE_ENV": "production",
36 |         "IGNORE_ROBOTS_TXT": "${user_config.ignore_robots_txt}"
37 |       }
38 |     }
39 |   },
40 |   "tools": [
41 |     {
42 |       "name": "airbnb_search",
43 |       "description": "Search for Airbnb listings with various filters including location, dates, guests, and price range. Returns paginated results with direct links."
44 |     },
45 |     {
46 |       "name": "airbnb_listing_details",
47 |       "description": "Get detailed information about a specific Airbnb listing including amenities, policies, location details, and highlights."
48 |     }
49 |   ],
50 |   "tools_generated": false,
51 |   "prompts_generated": false,
52 |   "compatibility": {
53 |     "claude_desktop": ">=0.10.0",
54 |     "platforms": [
55 |       "darwin",
56 |       "win32",
57 |       "linux"
58 |     ],
59 |     "runtimes": {
60 |       "node": ">=18.0.0"
61 |     }
62 |   },
63 |   "user_config": {
64 |     "ignore_robots_txt": {
65 |       "type": "boolean",
66 |       "title": "Ignore robots.txt",
67 |       "description": "Bypass robots.txt restrictions when making requests to Airbnb. Use with caution and respect Airbnb's terms of service.",
68 |       "default": false,
69 |       "required": false
70 |     }
71 |   }
72 | }
73 | 
```

--------------------------------------------------------------------------------
/test-extension.js:
--------------------------------------------------------------------------------

```javascript
  1 | #!/usr/bin/env node
  2 | 
  3 | /**
  4 |  * Simple test script for the Airbnb DXT extension
  5 |  * This script validates that the MCP server responds correctly to tool calls
  6 |  */
  7 | 
  8 | import { spawn } from 'child_process';
  9 | import { fileURLToPath } from 'url';
 10 | import { dirname, join } from 'path';
 11 | 
 12 | const __filename = fileURLToPath(import.meta.url);
 13 | const __dirname = dirname(__filename);
 14 | 
 15 | // Test configuration
 16 | const TEST_TIMEOUT = 30000; // 30 seconds
 17 | const SERVER_PATH = join(__dirname, 'dist', 'index.js');
 18 | 
 19 | class MCPTester {
 20 |   constructor() {
 21 |     this.server = null;
 22 |     this.requestId = 1;
 23 |   }
 24 | 
 25 |   async startServer() {
 26 |     console.log('🚀 Starting MCP server...');
 27 |     
 28 |     this.server = spawn('node', [SERVER_PATH, '--ignore-robots-txt'], {
 29 |       stdio: ['pipe', 'pipe', 'pipe'],
 30 |       env: { ...process.env, IGNORE_ROBOTS_TXT: 'true' }
 31 |     });
 32 | 
 33 |     this.server.stderr.on('data', (data) => {
 34 |       console.log('📋 Server log:', data.toString().trim());
 35 |     });
 36 | 
 37 |     // Wait for server to start
 38 |     await new Promise(resolve => setTimeout(resolve, 2000));
 39 |     
 40 |     if (this.server.killed) {
 41 |       throw new Error('Server failed to start');
 42 |     }
 43 |     
 44 |     console.log('✅ Server started successfully');
 45 |   }
 46 | 
 47 |   async sendRequest(method, params = {}) {
 48 |     return new Promise((resolve, reject) => {
 49 |       const request = {
 50 |         jsonrpc: '2.0',
 51 |         id: this.requestId++,
 52 |         method,
 53 |         params
 54 |       };
 55 | 
 56 |       const timeout = setTimeout(() => {
 57 |         reject(new Error(`Request timeout after ${TEST_TIMEOUT}ms`));
 58 |       }, TEST_TIMEOUT);
 59 | 
 60 |       let responseData = '';
 61 |       
 62 |       const onData = (data) => {
 63 |         responseData += data.toString();
 64 |         
 65 |         // Check if we have a complete JSON response
 66 |         try {
 67 |           const lines = responseData.split('\n').filter(line => line.trim());
 68 |           for (const line of lines) {
 69 |             const response = JSON.parse(line);
 70 |             if (response.id === request.id) {
 71 |               clearTimeout(timeout);
 72 |               this.server.stdout.off('data', onData);
 73 |               resolve(response);
 74 |               return;
 75 |             }
 76 |           }
 77 |         } catch (e) {
 78 |           // Not a complete JSON yet, continue waiting
 79 |         }
 80 |       };
 81 | 
 82 |       this.server.stdout.on('data', onData);
 83 |       
 84 |       console.log(`📤 Sending request: ${method}`);
 85 |       this.server.stdin.write(JSON.stringify(request) + '\n');
 86 |     });
 87 |   }
 88 | 
 89 |   async testListTools() {
 90 |     console.log('\n🔧 Testing list_tools...');
 91 |     
 92 |     try {
 93 |       const response = await this.sendRequest('tools/list');
 94 |       
 95 |       if (response.error) {
 96 |         throw new Error(`Server error: ${response.error.message}`);
 97 |       }
 98 |       
 99 |       const tools = response.result?.tools || [];
100 |       console.log(`✅ Found ${tools.length} tools:`);
101 |       
102 |       tools.forEach(tool => {
103 |         console.log(`   - ${tool.name}: ${tool.description}`);
104 |       });
105 |       
106 |       // Validate expected tools
107 |       const expectedTools = ['airbnb_search', 'airbnb_listing_details'];
108 |       const foundTools = tools.map(t => t.name);
109 |       
110 |       for (const expectedTool of expectedTools) {
111 |         if (!foundTools.includes(expectedTool)) {
112 |           throw new Error(`Missing expected tool: ${expectedTool}`);
113 |         }
114 |       }
115 |       
116 |       return true;
117 |     } catch (error) {
118 |       console.error('❌ list_tools test failed:', error.message);
119 |       return false;
120 |     }
121 |   }
122 | 
123 |   async testSearchTool() {
124 |     console.log('\n🔍 Testing airbnb_search tool...');
125 |     
126 |     try {
127 |       const response = await this.sendRequest('tools/call', {
128 |         name: 'airbnb_search',
129 |         arguments: {
130 |           location: 'San Francisco, CA',
131 |           adults: 2,
132 |           ignoreRobotsText: true
133 |         }
134 |       });
135 |       
136 |       if (response.error) {
137 |         throw new Error(`Server error: ${response.error.message}`);
138 |       }
139 |       
140 |       const result = response.result;
141 |       if (!result || !result.content || !result.content[0]) {
142 |         throw new Error('Invalid response format');
143 |       }
144 |       
145 |       const content = JSON.parse(result.content[0].text);
146 |       
147 |       if (content.error) {
148 |         console.log('⚠️  Search returned error (expected for robots.txt):', content.error);
149 |         return true; // This is expected behavior
150 |       }
151 |       
152 |       if (content.searchResults) {
153 |         console.log(`✅ Search successful, found ${content.searchResults.length} results`);
154 |         if (content.searchResults.length > 0) {
155 |           console.log(`   First result: ${content.searchResults[0].id}`);
156 |         }
157 |       }
158 |       
159 |       return true;
160 |     } catch (error) {
161 |       console.error('❌ airbnb_search test failed:', error.message);
162 |       return false;
163 |     }
164 |   }
165 | 
166 |   async testListingDetailsTool() {
167 |     console.log('\n🏠 Testing airbnb_listing_details tool...');
168 |     
169 |     try {
170 |       const response = await this.sendRequest('tools/call', {
171 |         name: 'airbnb_listing_details',
172 |         arguments: {
173 |           id: '670214003022775198',
174 |           ignoreRobotsText: true
175 |         }
176 |       });
177 |       
178 |       if (response.error) {
179 |         throw new Error(`Server error: ${response.error.message}`);
180 |       }
181 |       
182 |       const result = response.result;
183 |       if (!result || !result.content || !result.content[0]) {
184 |         throw new Error('Invalid response format');
185 |       }
186 |       
187 |       const content = JSON.parse(result.content[0].text);
188 |       
189 |       if (content.error) {
190 |         console.log('⚠️  Listing details returned error (expected for dummy ID):', content.error);
191 |         return true; // This is expected behavior
192 |       }
193 |       
194 |       console.log('✅ Listing details tool responded correctly');
195 |       return true;
196 |     } catch (error) {
197 |       console.error('❌ airbnb_listing_details test failed:', error.message);
198 |       return false;
199 |     }
200 |   }
201 | 
202 |   async stopServer() {
203 |     if (this.server && !this.server.killed) {
204 |       console.log('\n🛑 Stopping server...');
205 |       this.server.kill('SIGTERM');
206 |       
207 |       // Wait for graceful shutdown
208 |       await new Promise(resolve => {
209 |         this.server.on('exit', resolve);
210 |         setTimeout(() => {
211 |           if (!this.server.killed) {
212 |             this.server.kill('SIGKILL');
213 |           }
214 |           resolve();
215 |         }, 5000);
216 |       });
217 |       
218 |       console.log('✅ Server stopped');
219 |     }
220 |   }
221 | 
222 |   async runTests() {
223 |     let allPassed = true;
224 |     
225 |     try {
226 |       await this.startServer();
227 |       
228 |       // Run all tests
229 |       const tests = [
230 |         () => this.testListTools(),
231 |         () => this.testSearchTool(),
232 |         () => this.testListingDetailsTool()
233 |       ];
234 |       
235 |       for (const test of tests) {
236 |         const passed = await test();
237 |         allPassed = allPassed && passed;
238 |       }
239 |       
240 |     } catch (error) {
241 |       console.error('❌ Test suite failed:', error.message);
242 |       allPassed = false;
243 |     } finally {
244 |       await this.stopServer();
245 |     }
246 |     
247 |     console.log('\n' + '='.repeat(50));
248 |     if (allPassed) {
249 |       console.log('🎉 All tests passed! Extension is ready for use.');
250 |     } else {
251 |       console.log('❌ Some tests failed. Please check the issues above.');
252 |       process.exit(1);
253 |     }
254 |   }
255 | }
256 | 
257 | // Run tests if this script is executed directly
258 | if (import.meta.url === `file://${process.argv[1]}`) {
259 |   const tester = new MCPTester();
260 |   tester.runTests().catch(error => {
261 |     console.error('💥 Test runner crashed:', error);
262 |     process.exit(1);
263 |   });
264 | }
265 | 
266 | export default MCPTester;
267 | 
```

--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
  4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  5 | import {
  6 |   CallToolRequestSchema,
  7 |   ListToolsRequestSchema,
  8 |   Tool,
  9 |   McpError,
 10 |   ErrorCode,
 11 | } from "@modelcontextprotocol/sdk/types.js";
 12 | import fetch from "node-fetch";
 13 | import * as cheerio from "cheerio";
 14 | import { cleanObject, flattenArraysInObject, pickBySchema } from "./util.js";
 15 | import robotsParser from "robots-parser";
 16 | import { readFileSync } from 'fs';
 17 | import { fileURLToPath } from 'url';
 18 | import { dirname, join } from 'path';
 19 | 
 20 | // Get version from package.json
 21 | const __filename = fileURLToPath(import.meta.url);
 22 | const __dirname = dirname(__filename);
 23 | 
 24 | function getVersion(): string {
 25 |   try {
 26 |     const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
 27 |     return process.env.MCP_SERVER_VERSION || packageJson.version || "unknown";
 28 |   } catch (error) {
 29 |     return process.env.MCP_SERVER_VERSION || "unknown";
 30 |   }
 31 | }
 32 | 
 33 | const VERSION = getVersion();
 34 | 
 35 | // Tool definitions
 36 | const AIRBNB_SEARCH_TOOL: Tool = {
 37 |   name: "airbnb_search",
 38 |   description: "Search for Airbnb listings with various filters and pagination. Provide direct links to the user",
 39 |   inputSchema: {
 40 |     type: "object",
 41 |     properties: {
 42 |       location: {
 43 |         type: "string",
 44 |         description: "Location to search for (city, state, etc.)"
 45 |       },
 46 |       placeId: {
 47 |         type: "string",
 48 |         description: "Google Maps Place ID (overrides the location parameter)"
 49 |       },
 50 |       checkin: {
 51 |         type: "string",
 52 |         description: "Check-in date (YYYY-MM-DD)"
 53 |       },
 54 |       checkout: {
 55 |         type: "string",
 56 |         description: "Check-out date (YYYY-MM-DD)"
 57 |       },
 58 |       adults: {
 59 |         type: "number",
 60 |         description: "Number of adults"
 61 |       },
 62 |       children: {
 63 |         type: "number",
 64 |         description: "Number of children"
 65 |       },
 66 |       infants: {
 67 |         type: "number",
 68 |         description: "Number of infants"
 69 |       },
 70 |       pets: {
 71 |         type: "number",
 72 |         description: "Number of pets"
 73 |       },
 74 |       minPrice: {
 75 |         type: "number",
 76 |         description: "Minimum price for the stay"
 77 |       },
 78 |       maxPrice: {
 79 |         type: "number",
 80 |         description: "Maximum price for the stay"
 81 |       },
 82 |       cursor: {
 83 |         type: "string",
 84 |         description: "Base64-encoded string used for Pagination"
 85 |       },
 86 |       ignoreRobotsText: {
 87 |         type: "boolean",
 88 |         description: "Ignore robots.txt rules for this request"
 89 |       }
 90 |     },
 91 |     required: ["location"]
 92 |   }
 93 | };
 94 | 
 95 | const AIRBNB_LISTING_DETAILS_TOOL: Tool = {
 96 |   name: "airbnb_listing_details",
 97 |   description: "Get detailed information about a specific Airbnb listing. Provide direct links to the user",
 98 |   inputSchema: {
 99 |     type: "object",
100 |     properties: {
101 |       id: {
102 |         type: "string",
103 |         description: "The Airbnb listing ID"
104 |       },
105 |       checkin: {
106 |         type: "string",
107 |         description: "Check-in date (YYYY-MM-DD)"
108 |       },
109 |       checkout: {
110 |         type: "string",
111 |         description: "Check-out date (YYYY-MM-DD)"
112 |       },
113 |       adults: {
114 |         type: "number",
115 |         description: "Number of adults"
116 |       },
117 |       children: {
118 |         type: "number",
119 |         description: "Number of children"
120 |       },
121 |       infants: {
122 |         type: "number",
123 |         description: "Number of infants"
124 |       },
125 |       pets: {
126 |         type: "number",
127 |         description: "Number of pets"
128 |       },
129 |       ignoreRobotsText: {
130 |         type: "boolean",
131 |         description: "Ignore robots.txt rules for this request"
132 |       }
133 |     },
134 |     required: ["id"]
135 |   }
136 | };
137 | 
138 | const AIRBNB_TOOLS = [
139 |   AIRBNB_SEARCH_TOOL,
140 |   AIRBNB_LISTING_DETAILS_TOOL,
141 | ] as const;
142 | 
143 | // Utility functions
144 | const USER_AGENT = "ModelContextProtocol/1.0 (Autonomous; +https://github.com/modelcontextprotocol/servers)";
145 | const BASE_URL = "https://www.airbnb.com";
146 | 
147 | // Configuration from environment variables (set by DXT host)
148 | const IGNORE_ROBOTS_TXT = process.env.IGNORE_ROBOTS_TXT === "true" || process.argv.slice(2).includes("--ignore-robots-txt");
149 | 
150 | const robotsErrorMessage = "This path is disallowed by Airbnb's robots.txt to this User-agent. You may or may not want to run the server with '--ignore-robots-txt' args"
151 | let robotsTxtContent = "";
152 | 
153 | // Enhanced robots.txt fetch with timeout and error handling
154 | async function fetchRobotsTxt() {
155 |   if (IGNORE_ROBOTS_TXT) {
156 |     log('info', 'Skipping robots.txt fetch (ignored by configuration)');
157 |     return;
158 |   }
159 | 
160 |   try {
161 |     log('info', 'Fetching robots.txt from Airbnb');
162 |     
163 |     // Add timeout to prevent hanging
164 |     const controller = new AbortController();
165 |     const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
166 |     
167 |     const response = await fetch(`${BASE_URL}/robots.txt`, {
168 |       headers: {
169 |         "User-Agent": USER_AGENT,
170 |       },
171 |       signal: controller.signal
172 |     });
173 |     
174 |     clearTimeout(timeoutId);
175 |     
176 |     if (!response.ok) {
177 |       throw new Error(`HTTP ${response.status}: ${response.statusText}`);
178 |     }
179 |     
180 |     robotsTxtContent = await response.text();
181 |     log('info', 'Successfully fetched robots.txt');
182 |   } catch (error) {
183 |     log('warn', 'Error fetching robots.txt, assuming all paths allowed', {
184 |       error: error instanceof Error ? error.message : String(error)
185 |     });
186 |     robotsTxtContent = ""; // Empty robots.txt means everything is allowed
187 |   }
188 | }
189 | 
190 | function isPathAllowed(path: string): boolean {  
191 |   if (!robotsTxtContent) {
192 |     return true; // If we couldn't fetch robots.txt, assume allowed
193 |   }
194 | 
195 |   try {
196 |     const robots = robotsParser(`${BASE_URL}/robots.txt`, robotsTxtContent);
197 |     const allowed = robots.isAllowed(path, USER_AGENT);
198 |     
199 |     if (!allowed) {
200 |       log('warn', 'Path disallowed by robots.txt', { path, userAgent: USER_AGENT });
201 |     }
202 |     
203 |     return allowed;
204 |   } catch (error) {
205 |     log('warn', 'Error parsing robots.txt, allowing path', {
206 |       path,
207 |       error: error instanceof Error ? error.message : String(error)
208 |     });
209 |     return true; // If parsing fails, be permissive
210 |   }
211 | }
212 | 
213 | async function fetchWithUserAgent(url: string, timeout: number = 30000) {
214 |   const controller = new AbortController();
215 |   const timeoutId = setTimeout(() => controller.abort(), timeout);
216 |   
217 |   try {
218 |     const response = await fetch(url, {
219 |       headers: {
220 |         "User-Agent": USER_AGENT,
221 |         "Accept-Language": "en-US,en;q=0.9",
222 |         "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
223 |         "Cache-Control": "no-cache",
224 |       },
225 |       signal: controller.signal
226 |     });
227 |     
228 |     clearTimeout(timeoutId);
229 |     
230 |     if (!response.ok) {
231 |       throw new Error(`HTTP ${response.status}: ${response.statusText}`);
232 |     }
233 |     
234 |     return response;
235 |   } catch (error) {
236 |     clearTimeout(timeoutId);
237 |     
238 |     if (error instanceof Error && error.name === 'AbortError') {
239 |       throw new Error(`Request timeout after ${timeout}ms`);
240 |     }
241 |     
242 |     throw error;
243 |   }
244 | }
245 | 
246 | // API handlers
247 | async function handleAirbnbSearch(params: any) {
248 |   const {
249 |     location,
250 |     placeId,
251 |     checkin,
252 |     checkout,
253 |     adults = 1,
254 |     children = 0,
255 |     infants = 0,
256 |     pets = 0,
257 |     minPrice,
258 |     maxPrice,
259 |     cursor,
260 |     ignoreRobotsText = false,
261 |   } = params;
262 | 
263 |   // Build search URL
264 |   const searchUrl = new URL(`${BASE_URL}/s/${encodeURIComponent(location)}/homes`);
265 |   
266 |   // Add placeId
267 |   if (placeId) searchUrl.searchParams.append("place_id", placeId);
268 |   
269 |   // Add query parameters
270 |   if (checkin) searchUrl.searchParams.append("checkin", checkin);
271 |   if (checkout) searchUrl.searchParams.append("checkout", checkout);
272 |   
273 |   // Add guests
274 |   const adults_int = parseInt(adults.toString());
275 |   const children_int = parseInt(children.toString());
276 |   const infants_int = parseInt(infants.toString());
277 |   const pets_int = parseInt(pets.toString());
278 |   
279 |   const totalGuests = adults_int + children_int;
280 |   if (totalGuests > 0) {
281 |     searchUrl.searchParams.append("adults", adults_int.toString());
282 |     searchUrl.searchParams.append("children", children_int.toString());
283 |     searchUrl.searchParams.append("infants", infants_int.toString());
284 |     searchUrl.searchParams.append("pets", pets_int.toString());
285 |   }
286 |   
287 |   // Add price range
288 |   if (minPrice) searchUrl.searchParams.append("price_min", minPrice.toString());
289 |   if (maxPrice) searchUrl.searchParams.append("price_max", maxPrice.toString());
290 |   
291 |   // Add room type
292 |   // if (roomType) {
293 |   //   const roomTypeParam = roomType.toLowerCase().replace(/\s+/g, '_');
294 |   //   searchUrl.searchParams.append("room_types[]", roomTypeParam);
295 |   // }
296 | 
297 |   // Add cursor for pagination
298 |   if (cursor) {
299 |     searchUrl.searchParams.append("cursor", cursor);
300 |   }
301 | 
302 |   // Check if path is allowed by robots.txt
303 |   const path = searchUrl.pathname + searchUrl.search;
304 |   if (!ignoreRobotsText && !isPathAllowed(path)) {
305 |     log('warn', 'Search blocked by robots.txt', { path, url: searchUrl.toString() });
306 |     return {
307 |       content: [{
308 |         type: "text",
309 |         text: JSON.stringify({
310 |           error: robotsErrorMessage,
311 |           url: searchUrl.toString(),
312 |           suggestion: "Consider enabling 'ignore_robots_txt' in extension settings if needed for testing"
313 |         }, null, 2)
314 |       }],
315 |       isError: true
316 |     };
317 |   }
318 | 
319 |   const allowSearchResultSchema: Record<string, any> = {
320 |     demandStayListing : {
321 |       id: true,
322 |       description: true,
323 |       location: true,
324 |     },
325 |     badges: {
326 |       text: true,
327 |     },
328 |     structuredContent: {
329 |       mapCategoryInfo: {
330 |         body: true
331 |       },
332 |       mapSecondaryLine: {
333 |         body: true
334 |       },
335 |       primaryLine: {
336 |         body: true
337 |       },
338 |       secondaryLine: {
339 |         body: true
340 |       },
341 |     },
342 |     avgRatingA11yLabel: true,
343 |     listingParamOverrides: true,
344 |     structuredDisplayPrice: {
345 |       primaryLine: {
346 |         accessibilityLabel: true,
347 |       },
348 |       secondaryLine: {
349 |         accessibilityLabel: true,
350 |       },
351 |       explanationData: {
352 |         title: true,
353 |         priceDetails: {
354 |           items: {
355 |             description: true,
356 |             priceString: true
357 |           }
358 |         }
359 |       }
360 |     },
361 |     // contextualPictures: {
362 |     //   picture: true
363 |     // }
364 |   };
365 | 
366 |   try {
367 |     log('info', 'Performing Airbnb search', { location, checkin, checkout, adults, children });
368 |     
369 |     const response = await fetchWithUserAgent(searchUrl.toString());
370 |     const html = await response.text();
371 |     const $ = cheerio.load(html);
372 |     
373 |     let staysSearchResults: any = {};
374 |     
375 |     try {
376 |       const scriptElement = $("#data-deferred-state-0").first();
377 |       if (scriptElement.length === 0) {
378 |         throw new Error("Could not find data script element - page structure may have changed");
379 |       }
380 |       
381 |       const scriptContent = $(scriptElement).text();
382 |       if (!scriptContent) {
383 |         throw new Error("Data script element is empty");
384 |       }
385 |       
386 |       const clientData = JSON.parse(scriptContent).niobeClientData[0][1];
387 |       const results = clientData.data.presentation.staysSearch.results;
388 |       cleanObject(results);
389 |       
390 |       staysSearchResults = {
391 |         searchResults: results.searchResults
392 |           .map((result: any) => flattenArraysInObject(pickBySchema(result, allowSearchResultSchema)))
393 |           .map((result: any) => {
394 |             const id = atob(result.demandStayListing.id).split(":")[1];
395 |             return {id, url: `${BASE_URL}/rooms/${id}`, ...result }
396 |           }),
397 |         paginationInfo: results.paginationInfo
398 |       }
399 |       
400 |       log('info', 'Search completed successfully', { 
401 |         resultCount: staysSearchResults.searchResults?.length || 0 
402 |       });
403 |     } catch (parseError) {
404 |       log('error', 'Failed to parse search results', {
405 |         error: parseError instanceof Error ? parseError.message : String(parseError),
406 |         url: searchUrl.toString()
407 |       });
408 |       
409 |       return {
410 |         content: [{
411 |           type: "text",
412 |           text: JSON.stringify({
413 |             error: "Failed to parse search results from Airbnb. The page structure may have changed.",
414 |             details: parseError instanceof Error ? parseError.message : String(parseError),
415 |             searchUrl: searchUrl.toString()
416 |           }, null, 2)
417 |         }],
418 |         isError: true
419 |       };
420 |     }
421 | 
422 |     return {
423 |       content: [{
424 |         type: "text",
425 |         text: JSON.stringify({
426 |           searchUrl: searchUrl.toString(),
427 |           ...staysSearchResults
428 |         }, null, 2)
429 |       }],
430 |       isError: false
431 |     };
432 |   } catch (error) {
433 |     log('error', 'Search request failed', {
434 |       error: error instanceof Error ? error.message : String(error),
435 |       url: searchUrl.toString()
436 |     });
437 |     
438 |     return {
439 |       content: [{
440 |         type: "text",
441 |         text: JSON.stringify({
442 |           error: error instanceof Error ? error.message : String(error),
443 |           searchUrl: searchUrl.toString(),
444 |           timestamp: new Date().toISOString()
445 |         }, null, 2)
446 |       }],
447 |       isError: true
448 |     };
449 |   }
450 | }
451 | 
452 | async function handleAirbnbListingDetails(params: any) {
453 |   const {
454 |     id,
455 |     checkin,
456 |     checkout,
457 |     adults = 1,
458 |     children = 0,
459 |     infants = 0,
460 |     pets = 0,
461 |     ignoreRobotsText = false,
462 |   } = params;
463 | 
464 |   // Build listing URL
465 |   const listingUrl = new URL(`${BASE_URL}/rooms/${id}`);
466 |   
467 |   // Add query parameters
468 |   if (checkin) listingUrl.searchParams.append("check_in", checkin);
469 |   if (checkout) listingUrl.searchParams.append("check_out", checkout);
470 |   
471 |   // Add guests
472 |   const adults_int = parseInt(adults.toString());
473 |   const children_int = parseInt(children.toString());
474 |   const infants_int = parseInt(infants.toString());
475 |   const pets_int = parseInt(pets.toString());
476 |   
477 |   const totalGuests = adults_int + children_int;
478 |   if (totalGuests > 0) {
479 |     listingUrl.searchParams.append("adults", adults_int.toString());
480 |     listingUrl.searchParams.append("children", children_int.toString());
481 |     listingUrl.searchParams.append("infants", infants_int.toString());
482 |     listingUrl.searchParams.append("pets", pets_int.toString());
483 |   }
484 | 
485 |   // Check if path is allowed by robots.txt
486 |   const path = listingUrl.pathname + listingUrl.search;
487 |   if (!ignoreRobotsText && !isPathAllowed(path)) {
488 |     log('warn', 'Listing details blocked by robots.txt', { path, url: listingUrl.toString() });
489 |     return {
490 |       content: [{
491 |         type: "text",
492 |         text: JSON.stringify({
493 |           error: robotsErrorMessage,
494 |           url: listingUrl.toString(),
495 |           suggestion: "Consider enabling 'ignore_robots_txt' in extension settings if needed for testing"
496 |         }, null, 2)
497 |       }],
498 |       isError: true
499 |     };
500 |   }
501 | 
502 |   const allowSectionSchema: Record<string, any> = {
503 |     "LOCATION_DEFAULT": {
504 |       lat: true,
505 |       lng: true,
506 |       subtitle: true,
507 |       title: true
508 |     },
509 |     "POLICIES_DEFAULT": {
510 |       title: true,
511 |       houseRulesSections: {
512 |         title: true,
513 |         items : {
514 |           title: true
515 |         }
516 |       }
517 |     },
518 |     "HIGHLIGHTS_DEFAULT": {
519 |       highlights: {
520 |         title: true
521 |       }
522 |     },
523 |     "DESCRIPTION_DEFAULT": {
524 |       htmlDescription: {
525 |         htmlText: true
526 |       }
527 |     },
528 |     "AMENITIES_DEFAULT": {
529 |       title: true,
530 |       seeAllAmenitiesGroups: {
531 |         title: true,
532 |         amenities: {
533 |           title: true
534 |         }
535 |       }
536 |     },
537 |     //"AVAILABLITY_CALENDAR_DEFAULT": true,
538 |   };
539 | 
540 |   try {
541 |     log('info', 'Fetching listing details', { id, checkin, checkout, adults, children });
542 |     
543 |     const response = await fetchWithUserAgent(listingUrl.toString());
544 |     const html = await response.text();
545 |     const $ = cheerio.load(html);
546 |     
547 |     let details = {};
548 |     
549 |     try {
550 |       const scriptElement = $("#data-deferred-state-0").first();
551 |       if (scriptElement.length === 0) {
552 |         throw new Error("Could not find data script element - page structure may have changed");
553 |       }
554 |       
555 |       const scriptContent = $(scriptElement).text();
556 |       if (!scriptContent) {
557 |         throw new Error("Data script element is empty");
558 |       }
559 |       
560 |       const clientData = JSON.parse(scriptContent).niobeClientData[0][1];
561 |       const sections = clientData.data.presentation.stayProductDetailPage.sections.sections;
562 |       sections.forEach((section: any) => cleanObject(section));
563 |       
564 |       details = sections
565 |         .filter((section: any) => allowSectionSchema.hasOwnProperty(section.sectionId))
566 |         .map((section: any) => {
567 |           return {
568 |             id: section.sectionId,
569 |             ...flattenArraysInObject(pickBySchema(section.section, allowSectionSchema[section.sectionId]))
570 |           }
571 |         });
572 |         
573 |       log('info', 'Listing details fetched successfully', { 
574 |         id, 
575 |         sectionsFound: Array.isArray(details) ? details.length : 0 
576 |       });
577 |     } catch (parseError) {
578 |       log('error', 'Failed to parse listing details', {
579 |         error: parseError instanceof Error ? parseError.message : String(parseError),
580 |         id,
581 |         url: listingUrl.toString()
582 |       });
583 |       
584 |       return {
585 |         content: [{
586 |           type: "text",
587 |           text: JSON.stringify({
588 |             error: "Failed to parse listing details from Airbnb. The page structure may have changed.",
589 |             details: parseError instanceof Error ? parseError.message : String(parseError),
590 |             listingUrl: listingUrl.toString()
591 |           }, null, 2)
592 |         }],
593 |         isError: true
594 |       };
595 |     }
596 | 
597 |     return {
598 |       content: [{
599 |         type: "text",
600 |         text: JSON.stringify({
601 |           listingUrl: listingUrl.toString(),
602 |           details: details
603 |         }, null, 2)
604 |       }],
605 |       isError: false
606 |     };
607 |   } catch (error) {
608 |     log('error', 'Listing details request failed', {
609 |       error: error instanceof Error ? error.message : String(error),
610 |       id,
611 |       url: listingUrl.toString()
612 |     });
613 |     
614 |     return {
615 |       content: [{
616 |         type: "text",
617 |         text: JSON.stringify({
618 |           error: error instanceof Error ? error.message : String(error),
619 |           listingUrl: listingUrl.toString(),
620 |           timestamp: new Date().toISOString()
621 |         }, null, 2)
622 |       }],
623 |       isError: true
624 |     };
625 |   }
626 | }
627 | 
628 | // Server setup
629 | const server = new Server(
630 |   {
631 |     name: "airbnb",
632 |     version: VERSION,
633 |   },
634 |   {
635 |     capabilities: {
636 |       tools: {},
637 |     },
638 |   },
639 | );
640 | 
641 | // Enhanced logging for DXT
642 | function log(level: 'info' | 'warn' | 'error', message: string, data?: any) {
643 |   const timestamp = new Date().toISOString();
644 |   const logMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
645 |   
646 |   if (data) {
647 |     console.error(`${logMessage}:`, JSON.stringify(data, null, 2));
648 |   } else {
649 |     console.error(logMessage);
650 |   }
651 | }
652 | 
653 | log('info', 'Airbnb MCP Server starting', {
654 |   version: VERSION,
655 |   ignoreRobotsTxt: IGNORE_ROBOTS_TXT,
656 |   nodeVersion: process.version,
657 |   platform: process.platform
658 | });
659 | 
660 | // Set up request handlers
661 | server.setRequestHandler(ListToolsRequestSchema, async () => ({
662 |   tools: AIRBNB_TOOLS,
663 | }));
664 | 
665 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
666 |   const startTime = Date.now();
667 |   
668 |   try {
669 |     // Validate request parameters
670 |     if (!request.params.name) {
671 |       throw new McpError(ErrorCode.InvalidParams, "Tool name is required");
672 |     }
673 |     
674 |     if (!request.params.arguments) {
675 |       throw new McpError(ErrorCode.InvalidParams, "Tool arguments are required");
676 |     }
677 |     
678 |     log('info', 'Tool call received', { 
679 |       tool: request.params.name,
680 |       arguments: request.params.arguments 
681 |     });
682 |     
683 |     // Ensure robots.txt is loaded
684 |     if (!robotsTxtContent && !IGNORE_ROBOTS_TXT) {
685 |       await fetchRobotsTxt();
686 |     }
687 | 
688 |     let result;
689 |     switch (request.params.name) {
690 |       case "airbnb_search": {
691 |         result = await handleAirbnbSearch(request.params.arguments);
692 |         break;
693 |       }
694 | 
695 |       case "airbnb_listing_details": {
696 |         result = await handleAirbnbListingDetails(request.params.arguments);
697 |         break;
698 |       }
699 | 
700 |       default:
701 |         throw new McpError(
702 |           ErrorCode.MethodNotFound,
703 |           `Unknown tool: ${request.params.name}`
704 |         );
705 |     }
706 |     
707 |     const duration = Date.now() - startTime;
708 |     log('info', 'Tool call completed', { 
709 |       tool: request.params.name, 
710 |       duration: `${duration}ms`,
711 |       success: !result.isError 
712 |     });
713 |     
714 |     return result;
715 |   } catch (error) {
716 |     const duration = Date.now() - startTime;
717 |     log('error', 'Tool call failed', {
718 |       tool: request.params.name,
719 |       duration: `${duration}ms`,
720 |       error: error instanceof Error ? error.message : String(error)
721 |     });
722 |     
723 |     if (error instanceof McpError) {
724 |       throw error;
725 |     }
726 |     
727 |     return {
728 |       content: [{
729 |         type: "text",
730 |         text: JSON.stringify({
731 |           error: error instanceof Error ? error.message : String(error),
732 |           timestamp: new Date().toISOString()
733 |         }, null, 2)
734 |       }],
735 |       isError: true
736 |     };
737 |   }
738 | });
739 | 
740 | async function runServer() {
741 |   try {
742 |     // Initialize robots.txt on startup
743 |     await fetchRobotsTxt();
744 |     
745 |     const transport = new StdioServerTransport();
746 |     await server.connect(transport);
747 |     
748 |     log('info', 'Airbnb MCP Server running on stdio', {
749 |       version: VERSION,
750 |       robotsRespected: !IGNORE_ROBOTS_TXT
751 |     });
752 |     
753 |     // Graceful shutdown handling
754 |     process.on('SIGINT', () => {
755 |       log('info', 'Received SIGINT, shutting down gracefully');
756 |       process.exit(0);
757 |     });
758 |     
759 |     process.on('SIGTERM', () => {
760 |       log('info', 'Received SIGTERM, shutting down gracefully');
761 |       process.exit(0);
762 |     });
763 |     
764 |   } catch (error) {
765 |     log('error', 'Failed to start server', {
766 |       error: error instanceof Error ? error.message : String(error)
767 |     });
768 |     process.exit(1);
769 |   }
770 | }
771 | 
772 | runServer().catch((error) => {
773 |   log('error', 'Fatal error running server', {
774 |     error: error instanceof Error ? error.message : String(error)
775 |   });
776 |   process.exit(1);
777 | });
778 | 
```