{
  "markdown": "# CuddlyNest Search & Listings — MCP Server\n\n[![npm](https://img.shields.io/npm/v/cuddlynest-mcp)](https://www.npmjs.com/package/cuddlynest-mcp)\n[![MCP Registry](https://img.shields.io/badge/MCP_Registry-io.github.mafedelahoz%2Fcuddlynest--mcp-blue)](https://registry.modelcontextprotocol.io/v0/servers?search=cuddlynest)\n\nA Model Context Protocol (MCP) server for searching [CuddlyNest](https://www.cuddlynest.com)\nhotels and retrieving listing details, including **room options, prices,\navailability and cancellation policies**.\n\nRead-only by design: search and listing details only. No booking, no payment.\n\n\n## How it gets the data\n\nCuddlyNest does not serve room prices in its static HTML. Pricing renders\nclient-side on the **public listing page**, backed by the site's own\ninfrastructure and third-party wholesale suppliers (`dida travels`,\n`hotelplanner`, `ratehawk`, `hxpro`, `rakuten`, …).\n\nThis server reads that data **the same way a visitor does**: it opens the real,\npublic listing page in a headless browser (Playwright/Chromium), lets *the\npage's own JavaScript* load the rooms, waits for them to render, and reads the\nresult out of the DOM.\n\n\n| Data | Source |\n| --- | --- |\n| Name, description, address, coordinates, star rating, amenities, images | Listing page `schema.org` `ld+json` + Open Graph tags (`cuddlynest.ts`) |\n| Room title, partner, `unit_price`, `remaining_rooms`, `price_breakdown`, `cancellation_policy` (incl. `.text`), `room_filters` | Rendered listing page DOM, via a React-fiber walk (`scrape-listing.ts`) |\n| Destination → place candidates + top hotels | `autosuggestion-2-0.cuddlynest.com` (public, no auth) |\n| Destination → broader city hotel list (~60–250) | `discovery-pages.cuddlynest.com/fetch_geopage/<ct-id>` (public); `<ct-id>` recovered from a hotel's product-detail breadcrumbs |\n| `product_id` → name / city / breadcrumbs | `ldp-2-0-product-details.cuddlynest.com/api/v1/productDetail` (public) |\n| `product_id` → canonical listing path | `/hotel/-<id>` server redirect |\n\n### The DOM extraction, and how it breaks\n\n`extractRoomsFromDom()` walks every price-shaped text node (`COL$742,637`), then\nwalks up its **React fiber tree** to the nearest ancestor component whose props\ncarry both `unit_price` and `roomGroups`. Those props are the room offer the\npage already rendered.\n\nThis is coupled to CuddlyNest's current frontend internals (a React prop shape,\nnot a stable contract). If they ship a frontend change it can start returning\nzero rooms even though the public page still shows prices. The single place to\nupdate is the detector condition `'unit_price' in p && 'roomGroups' in p` in\n[scrape-listing.ts](scrape-listing.ts). `npm run e2e:sansiraka` is meant to\ncatch that early (non-zero exit, not a silent empty result).\n\nThe `fromPriceText` (\"From COL$…\") field uses a looser heuristic and can come\nback `null` even on a healthy scrape; `rooms.fromPrice` (cheapest extracted\nunit) is the reliable figure.\n\n---\n\n## Requirements\n\n- Node.js 18+\n- A Chromium build for Playwright. `npm install` runs `playwright install\n  chromium` automatically (postinstall); if that is blocked in your\n  environment, run `npx playwright install chromium` once by hand.\n\n## Installation\n\nPublished as [`cuddlynest-mcp`](https://www.npmjs.com/package/cuddlynest-mcp) on\nnpm and listed in the [official MCP registry](https://registry.modelcontextprotocol.io/v0/servers?search=cuddlynest)\nas `io.github.mafedelahoz/cuddlynest-mcp`.\n\n```json\n{\n  \"mcpServers\": {\n    \"cuddlynest\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"cuddlynest-mcp\"]\n    }\n  }\n}\n```\n\nAdd `\"--ignore-robots-txt\"` to `args` to bypass `robots.txt` for the\nlisting-page fetches. `CUDDLYNEST_SCRAPE_TIMEOUT_MS` (default `35000`) caps how\nlong the browser waits for prices to render.\n\nAn MCPB bundle (`.mcpb`) for Claude Desktop is attached to each\n[GitHub release](https://github.com/mafedelahoz/mcp-cuddlynest/releases) — note it\ndoes **not** bundle Chromium, so run `npx playwright install chromium` once after\ninstalling it that way.\n\n### Remote / Streamable HTTP\n\nDefault transport is stdio. For a hosted deployment, run it over Streamable HTTP:\n\n```bash\nnode dist/index.js --http 8080      # or: MCP_TRANSPORT=http PORT=8080 node dist/index.js\n#   POST  http://<host>:8080/mcp    — JSON-RPC (stateless, no sessions)\n#   GET   http://<host>:8080/health — liveness\n```\n\n\n---\n\n## Tools\n\nBoth tools are annotated `readOnlyHint: true` — they never write, book, or pay.\n\n### `cuddlynest_search`\n\nSearch a destination and the top hotels there, from public CuddlyNest APIs\n(`autosuggestion-2-0` for the fuzzy match, `discovery-pages` geo pages for the\nbroader city list). Prices are **not** here — pass a hotel's `productId` to\n`cuddlynest_listing_details`.\n\n| Parameter | Required | Description |\n| --- | --- | --- |\n| `destination` | yes | City / area string, e.g. `\"Cartagena, Colombia\"` |\n| `hotelsOnly` | no | Omit the `places[]` block (default `false`) |\n| `fullCityList` | no | Also pull the geo-page city list (~60–250 hotels) when it can be resolved and verified against the destination — a few extra requests (default `true`) |\n| `checkin`, `checkout`, `adults`, `children`, `childAges`, `infants`, `rooms`, `currency` | no | echoed back for downstream use |\n\n**Returns:** `{ query, guests, places[], city, hotelSource, hotelCount, hotels[], note }`.\nEach `hotels[]` entry: `productId`, `name`, `url`, `slug`, `propertyType`,\n`starRating`, `guestRating` (/10) + `guestRatingText`, `reviewCount`, and — from\nthe geo page — `images[]`, `distanceFromCenterKm`, `featuredAmenities[]`.\n`hotelSource` is `\"autosuggest\"` or `\"autosuggest+geopage\"`. The list is\ntop-matches scale, **not** full inventory — CuddlyNest's real results page\n(`/sr/…`) is bot-blocked and `Disallow`ed in robots.txt.\n\n### `cuddlynest_listing_details`\n\nStatic basics **and** rooms/pricing for one hotel.\n\n| Parameter | Required | Description |\n| --- | --- | --- |\n| `hotel` | yes | Listing URL **or** numeric `product_id` (trailing number in the URL) |\n| `checkin`, `checkout` | for pricing | `YYYY-MM-DD` — required to read rooms/prices |\n| `adults`, `children`, `childAges`, `infants`, `rooms` | no | defaults 2 / 0 / – / 0 / 1 |\n| `currency` | no | ISO 4217, default `USD` |\n| `ignoreRobotsText` | no | ignore robots.txt for the static fetch |\n\n**Returns:** `{ productId, hotelUrl, guests, staticListing, staticError, rooms, roomsError, notes }`.\n`rooms.units[]` is the extracted room offers, each with `title`, `partnerName`,\n`unitPrice`, `currency`, `remainingRooms`, `guests`, `cancellationPolicyType`,\n`cancellationPolicyText`, `priceBreakdown`, `roomFilters`. `rooms` also carries\n`fromPrice`, `partnersSeen`, `listingUrl`, `scrapedAt`.\n\n---\n\n## Development\n\n```bash\nnpm install          # installs deps + Chromium (postinstall)\nnpm run build        # sync-version + tsc -> dist/\nnpm run typecheck\nnpm test             # offline: smoke test (stdio) + scraper tests\nnpm run e2e:sansiraka # ONLINE: real scrape of cuddlynest.com, structural asserts\nnpm run watch\n```\n\n- `test-scrape.js` — `buildListingUrl` (pure) + `extractRoomsFromDom` replayed\n  against `fixtures/hotel-sansiraka-2026-10-05.json` (a **real** capture from\n  cuddlynest.com on 2026-09-01: Hotel Sansiraka `4395541`, 2026-10-05→08, 2\n  adults + 1 child age 2, COP — 9 rooms across dida travels / hxpro / ratehawk /\n  rakuten). A local headless Chromium rebuilds the page's DOM+fiber shape from\n  that fixture and checks the extractor reconstructs it — no network.\n- `test-extension.js` — MCP handshake, tool listing, `cuddlynest_search`\n  (hits the autosuggestion API), `cuddlynest_listing_details` product_id parsing.\n- `scripts/e2e-hotel-sansiraka.mjs` — runs a real scrape of the Sansiraka\n  listing and asserts the live result matches the fixture's **structure** (room\n  object shape/keys, non-empty, partner variety). Live prices and the exact\n  partner set drift from the fixture — that's expected.\n\n\n## Architecture\n\n- `index.ts` — MCP server, tool schemas, stdio **and** Streamable HTTP transports,\n  `robots.txt` handling\n- `cuddlynest.ts` — hotel-URL parsing, static-listing `ld+json` parse,\n  destination autosuggestion, result shaping\n- `scrape-listing.ts` — `resolveListingPath`, `buildListingUrl`, `scrapeListing`\n  (headless browser), `extractRoomsFromDom` (React-fiber walk)\n- `util.ts` — generic object/JSON helpers\n\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n",
  "bytes": 8437,
  "sha": "3a70e9f671c8956124b7a7591af577a363c17b918374db0455379ec7f627936b",
  "repo_slug": "mafedelahoz/mcp-cuddlynest",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_mafedelahoz_cuddlynest_mcp_7562b4ff/readme"
}