diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml deleted file mode 100644 index 4e29e524ad..0000000000 --- a/.github/workflows/typescript.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: TypeScript - -on: - push: - branches: - - main - pull_request: - release: - types: [published] - -jobs: - detect-packages: - runs-on: ubuntu-latest - outputs: - packages: ${{ steps.find-packages.outputs.packages }} - steps: - - uses: actions/checkout@v6 - - name: Find JS packages - id: find-packages - working-directory: src - run: | - PACKAGES=$(find . -name package.json -not -path "*/node_modules/*" -exec dirname {} \; | sed 's/^\.\///' | jq -R -s -c 'split("\n")[:-1]') - echo "packages=$PACKAGES" >> $GITHUB_OUTPUT - - test: - needs: [detect-packages] - strategy: - matrix: - package: ${{ fromJson(needs.detect-packages.outputs.packages) }} - name: Test ${{ matrix.package }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - uses: actions/setup-node@v6 - with: - node-version: 22 - cache: npm - - - name: Install dependencies - working-directory: src/${{ matrix.package }} - run: npm ci - - - name: Run tests - working-directory: src/${{ matrix.package }} - run: npm test --if-present - - build: - needs: [detect-packages, test] - strategy: - matrix: - package: ${{ fromJson(needs.detect-packages.outputs.packages) }} - name: Build ${{ matrix.package }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - uses: actions/setup-node@v6 - with: - node-version: 22 - cache: npm - - - name: Install dependencies - working-directory: src/${{ matrix.package }} - run: npm ci - - - name: Build package - working-directory: src/${{ matrix.package }} - run: npm run build - - publish: - runs-on: ubuntu-latest - needs: [build, detect-packages] - if: github.event_name == 'release' - environment: release - - strategy: - matrix: - package: ${{ fromJson(needs.detect-packages.outputs.packages) }} - name: Publish ${{ matrix.package }} - - permissions: - contents: read - id-token: write - - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 - with: - node-version: 22 - cache: npm - registry-url: "https://registry.npmjs.org" - - - name: Install dependencies - working-directory: src/${{ matrix.package }} - run: npm ci - - - name: Publish package - working-directory: src/${{ matrix.package }} - run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/README.md b/README.md index 874916cab3..af7dc9dfc7 100644 --- a/README.md +++ b/README.md @@ -256,4 +256,4 @@ If you find MCP servers useful, please consider starring the repository and cont --- -Managed by Anthropic, but built together with the community. The Model Context Protocol is open source and we encourage everyone to contribute their own servers and improvements! +Managed by Anthropic, but built together with the community. The Model Context Protocol is open source and we encourage everyone to contribute their own servers and improvement! diff --git a/package.json b/package.json index 386927172f..af0fc561cf 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,26 @@ { - "name": "@modelcontextprotocol/servers", - "private": true, - "version": "0.6.2", - "description": "Model Context Protocol servers", - "license": "SEE LICENSE IN LICENSE", - "author": "Model Context Protocol a Series of LF Projects, LLC.", - "homepage": "https://modelcontextprotocol.io", - "bugs": "https://github.com/modelcontextprotocol/servers/issues", + "name": "business-hotels-mcp", + "version": "2.0.2", "type": "module", - "workspaces": [ - "src/*" - ], - "files": [], + "bin": { + "business-hotels-mcp": "./dist/index.js" + }, + "main": "./dist/index.js", "scripts": { - "build": "npm run build --workspaces", - "watch": "npm run watch --workspaces", - "publish-all": "npm publish --workspaces --access public", - "link-all": "npm link --workspaces" + "build": "tsc", + "prepare": "npm run build", + "start": "node dist/index.js", + "inspect": "mcp-inspect dist/index.js" }, "dependencies": { - "@modelcontextprotocol/server-everything": "*", - "@modelcontextprotocol/server-memory": "*", - "@modelcontextprotocol/server-filesystem": "*", - "@modelcontextprotocol/server-sequential-thinking": "*" + "@modelcontextprotocol/sdk": "^1.0.1", + "dotenv": "^16.4.5", + "zod": "^3.22.4", + "axios": "^1.6.7" + }, + "devDependencies": { + "typescript": "^5.3.3", + "@types/node": "^20.11.24", + "@modelcontextprotocol/inspector": "^0.1.0" } } diff --git a/release.yml b/release.yml new file mode 100644 index 0000000000..acf2a89a57 --- /dev/null +++ b/release.yml @@ -0,0 +1,53 @@ +name: Automatic Release Creation + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install & Build + run: | + npm ci + npm run build + + - name: Check Version & Release + id: check_version + shell: bash + run: | + VERSION=$(node -p "require('./package.json').version") + if gh release view v$VERSION >/dev/null 2>&1; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + fi + env: + GH_TOKEN: ${{ github.token }} + + - name: Create GitHub Release + if: steps.check_version.outputs.exists == 'false' + uses: softprops/action-gh-release@v1 + with: + tag_name: v${{ steps.check_version.outputs.version }} + name: Release v${{ steps.check_version.outputs.version }} + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/server.json b/server.json new file mode 100644 index 0000000000..53ae999bc1 --- /dev/null +++ b/server.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.businesshotelsdeveloper-dot/businesshotels.com", + "description": "BusinessHotels.com Universal Agentic API providing real-time hotel price verification.", + "repository": { + "url": "https://github.com/businesshotelsdeveloper-dot/businesshotels.com", + "source": "github" + }, + "version": "2.0.2", + "packages": [ + { + "registryType": "npm", + "identifier": "business-hotels-mcp", + "version": "2.0.2", + "transport": { + "type": "stdio" + } + } + ] +} diff --git a/src/business-hotels-mcp/README.md b/src/business-hotels-mcp/README.md new file mode 100644 index 0000000000..efcdc83472 --- /dev/null +++ b/src/business-hotels-mcp/README.md @@ -0,0 +1,568 @@ + +[![smithery badge](https://smithery.ai/badge/businesshotelsdeveloper/business-hotels)](https://smithery.ai/servers/businesshotelsdeveloper/business-hotels) +![MCP Version](https://img.shields.io/badge/MCP-1.0-blue) +![Travel](https://img.shields.io/badge/Travel-Global_Inventory-blueviolet) +![Hotel Rates](https://img.shields.io/badge/Hotel_Prices-Live_GDS-red) +![Latency](https://img.shields.io/badge/Latency-%3C800ms-green?style=flat-square&logo=speedtest&logoColor=white) +![Focus](https://img.shields.io/badge/Focus-Bleisure-orange) + + +## 🌍 BusinessHotels.com Agentic API - Universal LLM Compatibility. + +--- + +## ⚑ Performance Showcase + +The [BusinessHotels.com MCP Server](https://github.com/businesshotelsdeveloper-dot/businesshotels.com) is built for speed. By optimizing for **sub-second latency**, we enable autonomous agents to perform complex travel logicβ€”like ranking multiple propertiesβ€”in real-time without timing out. + +### πŸ”„ Multi-Hotel Agentic Loop (Sub-Second Ranking) +Demonstrates the **"Loop, Collect, Then Respond"** pattern. The agent queries 5 luxury properties and ranks them by value in under 1 second. +[![BusinessHotels.com MCP Multi-Hotel Demo](https://img.youtube.com/vi/xwaulgWBqJQ/0.jpg)](https://youtu.be/xwaulgWBqJQ) +* **Goal:** Ranking and comparing live inventory. +* **Speed:** Total verification for 5 properties in **< 1 second**. + +### πŸ” Single Property Verification & Deep Dive +Demonstrates real-time price validation and booking hand-off for a specific hotel. +[![BusinessHotels.com MCP Single Hotel Demo](https://img.youtube.com/vi/wg-NCa0ga1A/0.jpg)](https://youtu.be/wg-NCa0ga1A) +* **Goal:** Final price verification and generating "Agentic Booking" URLs. +* **Speed:** Complete JSON payload returned in **~531ms**. + +--- +The **BusinessHotels.com MCP Server** is built on the OpenAI-compatible JSON Schema formatβ€”the universal standard accepted by all leading AI platforms. This enables zero-config auto-registration across the entire agentic ecosystem. + +| Platform | Integration Method | Status | +| :--- | :--- | :--- | +| 🟣 **Claude** | **Native MCP Connector** (auto-discovery) | βœ… **Fully supported** | +| 🟒 **ChatGPT** | **Function Calling API** & GPT Assistant Actions | βœ… **Fully supported** | +| πŸ”΅ **Google Gemini** | **Function Calling API** (JSON Schema) | βœ… **Fully supported** | +| πŸ”΄ **Perplexity** | **Function calling + MCP Connectors** | βœ… **Fully supported** | +| πŸͺŸ **MS Copilot** | **Copilot Studio** or MCP plugin manifest | βœ… **Fully supported** | +| πŸ’» **Cursor / Windsurf** | **Native MCP Connector** (IDE integration) | βœ… **Fully supported** | +| 🌐 **Any MCP Client** | **Auto-discovery** via `?route=tools` | βœ… **Protocol-native** | + +--- + +### πŸ”— Technical Discovery Endpoints +For platforms requiring manifest files or discovery URLs: + +* **MCP Tools Endpoint:** `https://www.businesshotels.com/mcp-server.php?route=tools` +* **MCP Discovery Spec:** `https://www.businesshotels.com/.well-known/mcp.json` +* **OpenAPI Specification:** `https://www.businesshotels.com/openapi.json` +* **AI Plugin Manifest:** `https://www.businesshotels.com/.well-known/ai-plugin.json` + +> [!TIP] +> **Zero-Prompt Engineering:** Because we adhere to the **Model Context Protocol**, agents can autonomously determine required parameters like `hotelName` and `checkinDate` without manual instructions. +> Because we adhere to the **Model Context Protocol**, agents can autonomously determine the required parameters (`hotelName`, `checkinDate`, `checkoutDate`) without manual prompt engineering. + +This is the official [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server for **BusinessHotels.com**. It provides autonomous AI agents with **instant (<1s)** access to live hotel inventory, rates, and booking capabilities worldwide. + + +--- + +## Connection & Discovery + +This server is optimized for autonomous agents and "Bleisure" (business + leisure) travel workflows. + +| Resource | URL | +|---|---| +| **MCP Tools Configuration** | [https://www.businesshotels.com/mcp-server.php?route=config](https://www.businesshotels.com/mcp-server.php?route=config) | +| **MCP Tools Endpoint** | [https://www.businesshotels.com/mcp-server.php?route=tools](https://www.businesshotels.com/mcp-server.php?route=tools) | +| **OpenAPI Spec** | [https://www.businesshotels.com/openapi.json](https://www.businesshotels.com/openapi.json) | +| **MCP Discovery Spec** | [https://www.businesshotels.com/.well-known/mcp.json](https://www.businesshotels.com/.well-known/mcp.json) | +| **Plugin Manifest** | [https://www.businesshotels.com/.well-known/ai-plugin.json](https://www.businesshotels.com/.well-known/ai-plugin.json) | +| **Full API Docs** | [https://www.businesshotels.com/tool-config.html](https://www.businesshotels.com/tool-config.html) | + +--- + +... +| [Full API Docs](https://www.businesshotels.com/tool-config.html) | + +## πŸš€ Performance & Reliability +* **Ultra-Low Latency:** Engineered for agentic workflows where speed is critical. Most requests return in **under 800ms**, allowing agents to compare multiple hotels in parallel without hitting LLM timeout limits. +* **Real-Time Accuracy:** Unlike cached databases, our "Agentic API" fetches live inventory directly from the global distribution system (GDS) the moment the tool is called. +* **Optimized for Parallelism:** Use the [Multi-Hotel Comparison Pattern](#-multi-hotel-comparison-pattern) below to fetch rates for 5+ hotels simultaneously in under 2 seconds. + + + +## Quick Configuration + +### Option 1: Local Stdio (Best for Claude Desktop) +Add the following block to the `mcpServers` section of your `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "businesshotels-universal-agentic-api": { + "command": "npx", + "args": [ + "-y", + "@businesshotels/business-hotels-mcp" + ], + "env": { + "BUSINESS_HOTELS_API_KEY": "test-live-hotel-rates2025" + } + } + } +} + +``` + +**Option 2: Claude.ai Custom Connector (Remote MCP β€” No install required)** + +For [claude.ai](https://claude.ai) users (Free, Pro, Max, Team & Enterprise): + +1. Go to **Settings β†’ Connectors β†’ Add custom connector** +2. Enter the following: + +| Field | Value | +|---|---| +| Name | `BusinessHotels Live Rates` | +| Remote MCP server URL | `https://www.businesshotels.com/mcp-server-claude.php` | + +3. Click **Add** β†’ then **Connect** + +Claude will automatically discover the `get_live_hotel_rates` tool and use it in any conversation. No npm, no local install, no API key required. + +> **Note:** This endpoint (`mcp-server-claude.php`) is dedicated to claude.ai and implements the full MCP JSON-RPC 2.0 handshake (`initialize` β†’ `tools/list` β†’ `tools/call`) required by Claude's custom connector protocol. For Claude Desktop (local), use Option 1 above. + + ## Remote SSE (Best for Cursor & Windsurf) + + + +```json +{ + "mcpServers": { + "businesshotels-universal-agentic-api": { + "command": "npx", + "args": ["-y", "@businesshotels/business-hotels-mcp"], + "env": { + "BUSINESS_HOTELS_API_KEY": "test-live-hotel-rates2025" + } + } + } +} +``` + +## API Reference + +| Property | Value | +|---|---| +| **Endpoint** | `POST https://www.businesshotels.com/mcp-server.php?route=tools/get_live_hotel_rates` | +| **Auth Header** | `X-API-KEY: test-live-hotel-rates2025` | +| **Content-Type** | `application/json` | +| **Test API Key** | `test-live-hotel-rates2025` *(light production β€” email [ai@businesshotels.com](mailto:ai@businesshotels.com) for high-volume access)* | + +### Request Parameters + +| Parameter | Type | Required | Notes | +|---|---|---|---| +| `hotelName` | string | βœ… | Hotel + city + state, country, **use commas**. E.g. `"Wynn Las Vegas, NV, US"` | +| `checkinDate` | string | βœ… | Format: `YYYY-MM-DD` | +| `checkoutDate` | string | βœ… | Format: `YYYY-MM-DD` | +| `adults` | integer | β€” | Default: `2` | +| `currency` | string | β€” | Default: `"USD"` | + +### ⚠️ Response Gotchas + +- `display_all_in_total` is a **comma-formatted STRING** (e.g. `"1,250.00"`) β€” always strip commas before numeric operations +- `rates` may be **`null`** when the hotel is sold out β€” always guard against this before accessing nested fields +- `best_match_score` below `0.85` = low confidence β€” verify hotel identity with user before booking + +--- + +## Quick-Start Tests + +Choose your preferred language or integration method: + +| # | Method | Best For | +|---|---|---| +| [1](#test-1--browser-devtools-console) | Browser DevTools Console | Fastest test, zero setup | +| [2](#test-2--python-requests) | Python `requests` | Backend scripts, data pipelines | +| [3](#test-3--curl) | cURL | CLI, shell scripts, CI/CD | +| [4](#test-4--javascript-asyncawait) | JavaScript (async/await) | Frontend apps, Node.js | +| [5](#test-5--openai-function-calling-python) | OpenAI Function Calling β€” Python | GPT-4o agent integration | +| [6](#test-6--openai-function-calling-javascript) | OpenAI Function Calling β€” JavaScript | GPT-4o agent integration (JS) | +| [7](#test-7--google-gemini-python) | Google Gemini β€” Python | Gemini 1.5 Pro agent integration | + +--- + +### Test 1 Β· Browser DevTools Console + +> Open any page on **BusinessHotels.com** β†’ press `F12` β†’ go to the **Console** tab β†’ paste and hit Enter. + +```js +fetch("https://www.businesshotels.com/mcp-server.php?route=tools/get_live_hotel_rates", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "test-live-hotel-rates2025" + }, + body: JSON.stringify({ + hotelName: "JW Marriott, Las Vegas, NV, US", + checkinDate: "2026-07-15", + checkoutDate: "2026-07-16", + adults: 2, + currency: "USD" + }) +}) + .then(r => r.json()) + .then(data => { + console.log("βœ… Hotel:", data.hotel_name); + console.log("πŸ’° Price:", `$${data.rates?.display_all_in_total} ${data.rates?.currency}`); + console.log("πŸ”— Book:", data.booking_page_live_rates); + console.log("πŸ“Š Score:", data.best_match_score); + console.log("Full response:", data); + }); +``` + +--- + +### Test 2 Β· Python `requests` + +```python +import requests + +URL = "https://www.businesshotels.com/mcp-server.php?route=tools/get_live_hotel_rates" +HEADERS = {"X-API-KEY": "test-live-hotel-rates2025", "Content-Type": "application/json"} + +payload = { + "hotelName": "San Francisco Marriott Marquis, San Francisco, CA US", + "checkinDate": "2026-06-20", + "checkoutDate": "2026-06-21", + "adults": 2, + "currency": "USD" +} + +data = requests.post(URL, json=payload, headers=HEADERS, timeout=10).json() +rates = data.get("rates") or {} +raw_price = rates.get("display_all_in_total", "") + +if not raw_price or str(raw_price).strip() == "": + print("βšͺ Sold out β€” no inventory for these dates / occupancy") +else: + price = float(str(raw_price).replace(",", "")) + print(f"Hotel: {data.get('hotel_name')}, {data.get('city_name')}") + print(f"Price: ${price:.2f} {rates.get('currency', 'USD')} (taxes & fees included)") + print(f"Score: {data.get('best_match_score', 0):.2f} (1.0 = perfect match)") + print(f"Book Now: {data.get('booking_page_live_rates')}") + if data.get("best_match_score", 1) < 0.85: + print("⚠️ Low confidence β€” confirm hotel identity before booking") +``` + +--- + +### Test 3 Β· cURL + +```bash +curl -s -X POST \ + "https://www.businesshotels.com/mcp-server.php?route=tools/get_live_hotel_rates" \ + -H "Content-Type: application/json" \ + -H "X-API-KEY: test-live-hotel-rates2025" \ + -d '{ + "hotelName": "Luxor Las Vegas, Las Vegas, NV, US", + "checkinDate": "2026-07-20", + "checkoutDate": "2026-07-21", + "adults": 2, + "currency": "USD" + }' | python3 -m json.tool +``` + +--- + +### Test 4 Β· JavaScript (async/await) + +Works in the browser or Node.js. Exposes `window.businessHotelsAPI` for reuse in browser contexts. + +```js +async function getHotelRates(hotelName, checkinDate, checkoutDate, adults = 2, currency = "USD") { + const res = await fetch( + "https://www.businesshotels.com/mcp-server.php?route=tools/get_live_hotel_rates", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-KEY": "test-live-hotel-rates2025" + }, + body: JSON.stringify({ hotelName, checkinDate, checkoutDate, adults, currency }) + } + ); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); +} + +// --- Usage --- +const data = await getHotelRates("Bellagio, Las Vegas, NV, US", "2026-07-20", "2026-07-21"); + +const rawPrice = data?.rates?.display_all_in_total; +if (!rawPrice || String(rawPrice).trim() === "") { + console.log("βšͺ Sold out / no inventory for these dates."); +} else { + const price = parseFloat(String(rawPrice).replace(/,/g, "")); + console.log(`${data.hotel_name} β€” $${price.toFixed(2)} total (taxes included)`); + console.log(`Book: ${data.booking_page_live_rates}`); + if (data.best_match_score < 0.85) + console.warn(`⚠️ Low confidence (${data.best_match_score}) β€” verify hotel before booking.`); +} + +if (typeof window !== "undefined") window.businessHotelsAPI = { getHotelRates }; +``` + +--- + +### Test 5 Β· OpenAI Function Calling β€” Python + +Integrates `get_live_hotel_rates` as a GPT-4o tool. + +```python +from openai import OpenAI +import requests, json + +client = OpenAI(api_key="YOUR_OPENAI_API_KEY") +BH_KEY = "test-live-hotel-rates2025" +BH_URL = "https://www.businesshotels.com/mcp-server.php?route=tools/get_live_hotel_rates" + +tools = [{ + "type": "function", + "function": { + "name": "get_live_hotel_rates", + "description": ( + "Get live, all-inclusive hotel rates and a direct booking URL. " + "NOTE: 'display_all_in_total' is a comma-formatted STRING (e.g. '1,250.00') β€” " + "strip commas before numeric operations. " + "If rates is null or display_all_in_total is empty, the property is sold out." + ), + "parameters": { + "type": "object", + "properties": { + "hotelName": {"type": "string", "description": "Hotel + city + country, use commas. E.g. 'Wynn Las Vegas, NV, US'"}, + "checkinDate": {"type": "string", "format": "date"}, + "checkoutDate": {"type": "string", "format": "date"}, + "adults": {"type": "integer", "default": 2}, + "currency": {"type": "string", "default": "USD"} + }, + "required": ["hotelName", "checkinDate", "checkoutDate"] + } + } +}] + +# Step 1 β€” model decides to call the tool +messages = [{"role": "user", "content": "Rates for Luxor Las Vegas, July 20-21 2026?"}] +r1 = client.chat.completions.create(model="gpt-4o", messages=messages, tools=tools, tool_choice="auto") +msg = r1.choices.message +messages.append(msg) + +# Step 2 β€” execute tool call(s) and return results +if msg.tool_calls: + for tc in msg.tool_calls: + result = requests.post( + BH_URL, + headers={"X-API-KEY": BH_KEY, "Content-Type": "application/json"}, + json=json.loads(tc.function.arguments), + timeout=10 + ).json() + messages.append({"role": "tool", "tool_call_id": tc.id, "content": json.dumps(result)}) + + # Step 3 β€” get the final natural-language response + r2 = client.chat.completions.create(model="gpt-4o", messages=messages) + print(r2.choices.message.content) +``` + +--- + +### Test 6 Β· OpenAI Function Calling β€” JavaScript + +```js +import OpenAI from "openai"; + +const client = new OpenAI({ apiKey: "YOUR_OPENAI_API_KEY" }); +const BH_KEY = "test-live-hotel-rates2025"; +const BH_URL = "https://www.businesshotels.com/mcp-server.php?route=tools/get_live_hotel_rates"; + +const tools = [{ + type: "function", + function: { + name: "get_live_hotel_rates", + description: + "Get live, all-inclusive hotel rates and a direct booking URL. " + + "NOTE: 'display_all_in_total' is a comma-formatted STRING β€” strip commas before numeric ops. " + + "If rates is null or price is empty, the property is sold out.", + parameters: { + type: "object", + properties: { + hotelName: { type: "string" }, + checkinDate: { type: "string", format: "date" }, + checkoutDate: { type: "string", format: "date" }, + adults: { type: "integer", default: 2 }, + currency: { type: "string", default: "USD" } + }, + required: ["hotelName", "checkinDate", "checkoutDate"] + } + } +}]; + +// Step 1 β€” model decides to call the tool +const messages = [{ role: "user", content: "Rates for Luxor Las Vegas, July 20-21 2026?" }]; +const r1 = await client.chat.completions.create({ model: "gpt-4o", messages, tools, tool_choice: "auto" }); +const msg = r1.choices.message; +messages.push(msg); + +// Step 2 β€” execute tool call(s) and return results +if (msg.tool_calls) { + for (const tc of msg.tool_calls) { + const result = await fetch(BH_URL, { + method: "POST", + headers: { "X-API-KEY": BH_KEY, "Content-Type": "application/json" }, + body: tc.function.arguments + }).then(r => r.json()); + messages.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(result) }); + } + + // Step 3 β€” get the final natural-language response + const r2 = await client.chat.completions.create({ model: "gpt-4o", messages }); + console.log(r2.choices.message.content); +} +``` + +--- + +### Test 7 Β· Google Gemini β€” Python + +Integrates `get_live_hotel_rates` as a Gemini 1.5 Pro function call. + +```python +import google.generativeai as genai +import requests, json + +genai.configure(api_key="YOUR_GEMINI_API_KEY") +BH_KEY = "test-live-hotel-rates2025" +BH_URL = "https://www.businesshotels.com/mcp-server.php?route=tools/get_live_hotel_rates" + +get_live_hotel_rates = genai.protos.Tool( + function_declarations=[genai.protos.FunctionDeclaration( + name="get_live_hotel_rates", + description=( + "Fetch live hotel rates and a direct booking URL. " + "NOTE: 'display_all_in_total' is a comma-formatted STRING β€” strip commas before numeric ops. " + "If rates is null or price is empty, the property is sold out." + ), + parameters=genai.protos.Schema( + type=genai.protos.Type.OBJECT, + properties={ + "hotelName": genai.protos.Schema(type=genai.protos.Type.STRING), + "checkinDate": genai.protos.Schema(type=genai.protos.Type.STRING), + "checkoutDate": genai.protos.Schema(type=genai.protos.Type.STRING), + "adults": genai.protos.Schema(type=genai.protos.Type.NUMBER), + "currency": genai.protos.Schema(type=genai.protos.Type.STRING) + }, + required=["hotelName", "checkinDate", "checkoutDate"] + ) + )] +) + +model = genai.GenerativeModel(model_name="gemini-1.5-pro", tools=[get_live_hotel_rates]) +chat = model.start_chat(history=[]) + +# Step 1 β€” send the user message +resp = chat.send_message("Find live rates for Luxor Las Vegas, June 20-21 2026.") + +# Step 2 β€” execute any function calls Gemini requested +for part in resp.candidates.content.parts: + if part.function_call.name == "get_live_hotel_rates": + result = requests.post( + BH_URL, + headers={"X-API-KEY": BH_KEY, "Content-Type": "application/json"}, + json=dict(part.function_call.args), + timeout=10 + ).json() + chat.send_message(genai.protos.Content(parts=[genai.protos.Part( + function_response=genai.protos.FunctionResponse( + name="get_live_hotel_rates", + response={"result": result} + ) + )])) + +# Step 3 β€” get the final natural-language response +final = chat.send_message("Summarize the best price and booking link.") +print(final.text) +``` + +--- + +## πŸ” Multi-Hotel Comparison Pattern (High Performance) - Python +This API uses a **one-hotel-per-request** architecture. To maintain sub-second response times for comparisons, agents must use **asynchronous parallelism** rather than sequential loops. + +## βœ… Correct Pattern β€” Parallel Fetching (Under 1 Second) +In a sequential loop, 5 hotels would take ~4 seconds. With the parallel code below using `concurrent.futures`, all requests fire simultaneously, finishing the entire comparison in the time it takes for a single request. + +```python + +import requests, json + +url = "https://www.businesshotels.com/mcp-server.php?route=tools/get_live_hotel_rates" +headers = {"Content-Type": "application/json", "X-API-KEY": "test-live-hotel-rates2025"} + +hotels_to_check = [ + "Fairmont San Francisco, San Francisco, US", + "Four Seasons San Francisco at Embarcadero, San Francisco, US", + "Ritz-Carlton San Francisco, San Francisco, US", + "St. Regis San Francisco, San Francisco, US", + "Palace Hotel a Luxury Collection Hotel, San Francisco, US" +] + +params = {"checkinDate": "2026-07-12", "checkoutDate": "2026-07-14", "adults": 2, "currency": "USD"} +results = [] + +for hotel in hotels_to_check: + r = requests.post(url, headers=headers, json={**params, "hotelName": hotel}) + data = r.json() + rates = data.get("rates") + if rates and rates.get("display_all_in_total"): + price = float(str(rates["display_all_in_total"]).replace(",", "")) + results.append({"name": data["hotel_name"], "price": price, "url": data["booking_page_live_rates"]}) + +# Sort and present ALL results together β€” never respond mid-loop +results.sort(key=lambda x: x["price"]) +for i, h in enumerate(results, 1): + print(f"{i}. {h['name']}: ${h['price']:.2f}") + +cheapest = results[0] +print(f"\nπŸ† Best Value: {cheapest['name']} at ${cheapest['price']:.2f}") +print(f"πŸ‘‰ Book Now: {cheapest['url']}") + + + +``` + +### ⚠️ Critical Implementation Guardrails +* **Confidence Scoring:** Always check the `best_match_score`. If it is below **0.85**, the agent must verify the hotel identity (City/State) before providing a booking link. +* **Price Lock Expiry:** The `ppn_bundle` and price are valid for **~20 minutes**. Agents should re-fetch the rate if the user returns to an old session. +* **Discovery Specs:** For non-MCP integrations, use our [OpenAPI 3.0 Spec](https://www.businesshotels.com/openapi.json) or [AI Plugin Manifest](https://www.businesshotels.com/.well-known/ai-plugin.json). + + + +### Architectural Rules + +> πŸ“Œ **Complete all requests before responding.** Always finish the full loop before presenting any results. Responding mid-loop creates a fragmented, incomplete user experience. + +> πŸ“Œ **No batch endpoint.** Do not pass a `hotels[]` array in a single request β€” the endpoint accepts one `hotelName` string per call only. + +> ⚠️ **Rate lock timer starts at API response time.** The `ppn_bundle` token and quoted price are valid for approximately 20 minutes from when the API responded β€” not from when the user views results. If significant time has elapsed before the user clicks Book Now, warn them: *"This rate was fetched X minutes ago β€” prices may have changed. Refresh to confirm."* + +> ⚠️ **Never modify `ppn_bundle`.** This token is an opaque rate-lock credential. Do not truncate, re-encode, or expose it to the user. It is already embedded in the `booking_page_live_rates` URL. + +> βœ… **Session continuity.** Store `hotel_id` for each result during the session. If the user asks a follow-up like *"Does the Fairmont have a pool?"* or *"Show me the Fairmont again"*, reference the stored `hotel_id` without re-querying by name. + +### 🧠 Example Agent Workflows + +| Workflow | Description | +|---|---| +| **Best Value Finder** | Query up to 10 hotels, sanitize prices, sort by `display_all_in_total`, return the cheapest with a booking link | +| **Proximity Filter** | Use latitude/longitude to shortlist hotels within 0.5 miles of a specific address | +| **Luxury Rate Monitor** | Periodically scan a saved list of `hotel_id`s to alert when a suite drops below a target price | +| **Sold-Out Fallback** | If `display_all_in_total` is empty, automatically suggest nearby hotels or alternate dates without crashing | + +--- + +## Support + +πŸ“§ High-volume / production access: [ai@businesshotels.com](mailto:ai@businesshotels.com) diff --git a/src/business-hotels-mcp/index.ts b/src/business-hotels-mcp/index.ts new file mode 100644 index 0000000000..f7f41e6787 --- /dev/null +++ b/src/business-hotels-mcp/index.ts @@ -0,0 +1,118 @@ +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; + +/** + * Initialize the BusinessHotels MCP Server + */ +const server = new Server( + { + name: "business-hotels-mcp", + version: "1.0.0", + }, + { + capabilities: { + tools: {}, + }, + } +); + +/** + * Tool Listing: + * This tells the AI what capabilities are available. + */ +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "get_live_hotel_rates", + description: "Get live hotel rates by hotel name, dates, adults, and currency.", + inputSchema: { + type: "object", + properties: { + hotelName: { + type: "string", + description: "Full hotel name, ideally with city/state/country." + }, + checkinDate: { + type: "string", + description: "Check-in date (YYYY-MM-DD)" + }, + checkoutDate: { + type: "string", + description: "Check-out date (YYYY-MM-DD)" + }, + adults: { + type: "integer", + minimum: 1, + default: 2 + }, + currency: { + type: "string", + default: "USD" + } + }, + required: ["hotelName", "checkinDate", "checkoutDate"] + } + } + ] + }; +}); + +/** + * Tool Execution: + * Connects the AI request to your live PHP backend. + */ +server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (request.params.name === "get_live_hotel_rates") { + const args = request.params.arguments as any; + + try { + // Build the URL for your mcp-server.php endpoint + const url = new URL("https://www.businesshotels.com/mcp-server.php"); + url.searchParams.append("route", "tools"); + url.searchParams.append("hotel", args.hotelName); + url.searchParams.append("checkin", args.checkinDate); + url.searchParams.append("checkout", args.checkoutDate); + url.searchParams.append("adults", (args.adults || 2).toString()); + url.searchParams.append("currency", args.currency || "USD"); + + const response = await fetch(url.toString()); + + if (!response.ok) { + throw new Error(`Backend error: ${response.statusText}`); + } + + const data = await response.json(); + + return { + content: [ + { + type: "text", + text: JSON.stringify(data, null, 2) + } + ] + }; + } catch (error: any) { + return { + isError: true, + content: [ + { + type: "text", + text: `Error fetching rates from BusinessHotels: ${error.message}` + } + ] + }; + } + } + throw new Error("Tool not found"); +}); + +/** + * Start the server using Standard Input/Output + */ +const transport = new StdioServerTransport(); +await server.connect(transport); diff --git a/src/business-hotels-mcp/package.json b/src/business-hotels-mcp/package.json new file mode 100644 index 0000000000..01e4416dd6 --- /dev/null +++ b/src/business-hotels-mcp/package.json @@ -0,0 +1,60 @@ +{ + "name": "business-hotels-mcp", + "mcpName": "io.github.businesshotelsdeveloper-dot/businesshotels", + "version": "1.0.2", + "description": "Universal Agentic API for real-time hotel pricing and booking automation via BusinessHotels.com", + "main": "build/index.js", + "type": "module", + "bin": { + "business-hotels-mcp": "build/index.js" + }, + "mcp": { + "name": "Business Hotels AI Price Finder", + "description": "Autonomous protocol for hotel discovery and real-time rate verification using MCP 1.0.", + "logo": "https://www.businesshotels.com/img/BusinessHotels-ai-logo-f.png", + "categories": [ + "travel", + "hospitality", + "booking" + ], + "links": { + "homepage": "https://www.businesshotels.com/ai-hotel-finder.html", + "documentation": "https://www.businesshotels.com/tool-config.html", + "repository": "https://github.com/businesshotelsdeveloper-dot/businesshotels.com" + } + }, + "files": [ + "build" + ], + "scripts": { + "build": "tsc", + "start": "node build/index.js", + "prepare": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/businesshotelsdeveloper-dot/businesshotels.com.git" + }, + "keywords": [ + "mcp", + "model-context-protocol", + "travel", + "hotels", + "booking", + "agentic-ai", + "mcp-server", + "real-time-pricing" + ], + "author": "Drago Maximov", + "license": "MIT", + "homepage": "https://www.businesshotels.com", + "bugs": { + "url": "https://github.com/businesshotelsdeveloper-dot/businesshotels.com/issues" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.1" + }, + "devDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/src/business-hotels-mcp/tsconfig.json b/src/business-hotels-mcp/tsconfig.json new file mode 100644 index 0000000000..0a9e5be55a --- /dev/null +++ b/src/business-hotels-mcp/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["index.ts"] +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000000..c092be64b6 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,79 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import axios from "axios"; + +/** + * Initialize the BusinessHotels MCP Server + * Version 2.0.0 aligned with Agentic Discovery Endpoints + */ +const server = new McpServer({ + name: "business-hotels-mcp", + version: "2.0.2", +}); + +/** + * Tool: get_live_hotel_rates + * Connects to the PHP tools route for real-time ARI data. + */ +server.tool( + "get_live_hotel_rates", + { + hotelName: z.string().describe("Hotel name only, NO COMMAS (e.g. Marriott Marquis San Francisco US)"), + checkinDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Check-in date in YYYY-MM-DD format"), + checkoutDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Check-out date in YYYY-MM-DD format"), + adults: z.number().min(1).max(4).optional().default(2), + currency: z.string().length(3).optional().default("USD"), + }, + async ({ hotelName, checkinDate, checkoutDate, adults, currency }) => { + try { + // Connects to your unified PHP tools route + const response = await axios.get("https://www.businesshotels.com/mcp-server.php", { + params: { + route: "tools", + hotelName, + checkinDate, + checkoutDate, + adults, + currency, + // Using the live test key from your tool-config.html + apiKey: "test-live-hotel-rates2025" + }, + timeout: 5000 // Optimized for your sub-2-second response goal + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + } catch (error: any) { + return { + isError: true, + content: [ + { + type: "text", + text: `API Error: ${error.message}`, + }, + ], + }; + } + } +); + +/** + * Main execution block using Stdio transport + */ +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("BusinessHotels Agentic MCP Server running on stdio"); +} + +main().catch((error) => { + console.error("Fatal Server Error:", error); + process.exit(1); +}); diff --git a/tsconfig.json b/tsconfig.json index 208ca01e41..43dae7bda5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,18 @@ { "compilerOptions": { "target": "ES2022", - "module": "Node16", - "moduleResolution": "Node16", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "declaration": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true + "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "dist"] }