# cuddlynest-mcp [Health: Active]

**Category:** 💻 Developer Tools  
**Repository:** https://github.com/mafedelahoz/mcp-cuddlynest  
**GitHub Stars:** 0  
**Views:** 0  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/cuddlynest-mcp

## Description
Search CuddlyNest hotels and read rooms, prices, availability and cancellation policies

## Claude Desktop Quick Installation
Install path detected from listing signals. Uses `npx` (confidence: high):

```json
"mcpServers": {
  "cuddlynest-mcp": {
    "command": "npx",
    "args": ["-y","cuddlynest-mcp"]
  }
}
```

## Documentation & README

<img src="https://raw.githubusercontent.com/mafedelahoz/mcp-cuddlynest/HEAD/assets/icon.png" alt="CuddlyNest" width="88" align="right">

# CuddlyNest Search & Listings — MCP Server

[![npm](https://img.shields.io/npm/v/cuddlynest-mcp)](https://www.npmjs.com/package/cuddlynest-mcp)
[![MCP Registry](https://img.shields.io/badge/MCP_Registry-io.github.mafedelahoz%2Fcuddlynest--mcp-blue)](https://registry.modelcontextprotocol.io/v0/servers?search=cuddlynest)

A Model Context Protocol (MCP) server for searching [CuddlyNest](https://www.cuddlynest.com)
hotels and retrieving listing details, including **room options, prices,
availability and cancellation policies**.

Read-only by design: search and listing details only. No booking, no payment.


## How it gets the data

This server reads that data **the same way a visitor does**: it opens the real,
public listing page in a headless browser (Playwright/Chromium), lets *the
page's own JavaScript* load the rooms, waits for them to render, and reads the
result out of the DOM.


| Data | Source |
| --- | --- |
| Name, description, address, coordinates, star rating, amenities, images | Listing page `schema.org` `ld+json` + Open Graph tags (`cuddlynest.ts`) |
| 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`) |
| Destination → place candidates + top hotels | `autosuggestion-2-0.cuddlynest.com` (public, no auth) |
| 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 |
| `product_id` → name / city / breadcrumbs | `ldp-2-0-product-details.cuddlynest.com/api/v1/productDetail` (public) |
| `product_id` → canonical listing path | `/hotel/-<id>` server redirect |

### The DOM extraction, and how it breaks

`extractRoomsFromDom()` walks every price-shaped text node (`COL$742,637`), then
walks up its **React fiber tree** to the nearest ancestor component whose props
carry both `unit_price` and `roomGroups`. Those props are the room offer the
page already rendered.

This is coupled to CuddlyNest's current frontend internals (a React prop shape,
not a stable contract). If they ship a frontend change it can start returning
zero rooms even though the public page still shows prices. The single place to
update is the detector condition `'unit_price' in p && 'roomGroups' in p` in
[scrape-listing.ts](https://github.com/mafedelahoz/mcp-cuddlynest/blob/HEAD/scrape-listing.ts). `npm run e2e:sansiraka` is meant to
catch that early (non-zero exit, not a silent empty result).

The `fromPriceText` ("From COL$…") field uses a looser heuristic and can come
back `null` even on a healthy scrape; `rooms.fromPrice` (cheapest extracted
unit) is the reliable figure.

---

## Requirements

- Node.js 18+
- A Chromium build for Playwright. `npm install` runs `playwright install
  chromium` automatically (postinstall); if that is blocked in your
  environment, run `npx playwright install chromium` once by hand.

## Installation

Published as [`cuddlynest-mcp`](https://www.npmjs.com/package/cuddlynest-mcp) on
npm and listed in the [official MCP registry](https://registry.modelcontextprotocol.io/v0/servers?search=cuddlynest)
as `io.github.mafedelahoz/cuddlynest-mcp`.

```json
{
  "mcpServers": {
    "cuddlynest": {
      "command": "npx",
      "args": ["-y", "cuddlynest-mcp"]
    }
  }
}
```

Add `"--ignore-robots-txt"` to `args` to bypass `robots.txt` for the
listing-page fetches. `CUDDLYNEST_SCRAPE_TIMEOUT_MS` (default `35000`) caps how
long the browser waits for prices to render.

An MCPB bundle (`.mcpb`) for Claude Desktop is attached to each
[GitHub release](https://github.com/mafedelahoz/mcp-cuddlynest/releases) — note it
does **not** bundle Chromium, so run `npx playwright install chromium` once after
installing it that way.

### Remote / Streamable HTTP

Default transport is stdio. For a hosted deployment, run it over Streamable HTTP:

```bash
node dist/index.js --http 8080      # or: MCP_TRANSPORT=http PORT=8080 node dist/index.js
#   POST  http://<host>:8080/mcp    — JSON-RPC (stateless, no sessions)
#   GET   http://<host>:8080/health — liveness
```


---

## Tools

Both tools are annotated `readOnlyHint: true` — they never write, book, or pay.

### `cuddlynest_search`

Search a destination and the top hotels there, from public CuddlyNest APIs
(`autosuggestion-2-0` for the fuzzy match, `discovery-pages` geo pages for the
broader city list). Prices are **not** here — pass a hotel's `productId` to
`cuddlynest_listing_details`.

| Parameter | Required | Description |
| --- | --- | --- |
| `destination` | yes | City / area string, e.g. `"Cartagena, Colombia"` |
| `hotelsOnly` | no | Omit the `places[]` block (default `false`) |
| `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`) |
| `checkin`, `checkout`, `adults`, `children`, `childAges`, `infants`, `rooms`, `currency` | no | echoed back for downstream use |

**Returns:** `{ query, guests, places[], city, hotelSource, hotelCount, hotels[], note }`.
Each `hotels[]` entry: `productId`, `name`, `url`, `slug`, `propertyType`,
`starRating`, `guestRating` (/10) + `guestRatingText`, `reviewCount`, and — from
the geo page — `images[]`, `distanceFromCenterKm`, `featuredAmenities[]`.
`hotelSource` is `"autosuggest"` or `"autosuggest+geopage"`. The list is
top-matches scale, **not** full inventory — CuddlyNest's real results page
(`/sr/…`) is bot-blocked and `Disallow`ed in robots.txt.

### `cuddlynest_listing_details`

Static basics **and** rooms/pricing for one hotel.

| Parameter | Required | Description |
| --- | --- | --- |
| `hotel` | yes | Listing URL **or** numeric `product_id` (trailing number in the URL) |
| `checkin`, `checkout` | for pricing | `YYYY-MM-DD` — required to read rooms/prices |
| `adults`, `children`, `childAges`, `infants`, `rooms` | no | defaults 2 / 0 / – / 0 / 1 |
| `currency` | no | ISO 4217, default `USD` |
| `ignoreRobotsText` | no | ignore robots.txt for the static fetch |

**Returns:** `{ productId, hotelUrl, guests, staticListing, staticError, rooms, roomsError, notes }`.
`rooms.units[]` is the extracted room offers, each with `title`, `partnerName`,
`unitPrice`, `currency`, `remainingRooms`, `guests`, `cancellationPolicyType`,
`cancellationPolicyText`, `priceBreakdown`, `roomFilters`. `rooms` also carries
`fromPrice`, `partnersSeen`, `listingUrl`, `scrapedAt`.

---

## Development

```bash
npm install          # installs deps + Chromium (postinstall)
npm run build        # sync-version + tsc -> dist/
npm run typecheck
npm test             # offline: smoke test (stdio) + scraper tests
npm run e2e:sansiraka # ONLINE: real scrape of cuddlynest.com, structural asserts
npm run watch
```


## Architecture

- `index.ts` — MCP server, tool schemas, stdio **and** Streamable HTTP transports,
  `robots.txt` handling
- `cuddlynest.ts` — hotel-URL parsing, static-listing `ld+json` parse,
  destination autosuggestion, result shaping
- `scrape-listing.ts` — `resolveListingPath`, `buildListingUrl`, `scrapeListing`
  (headless browser), `extractRoomsFromDom` (React-fiber walk)
- `util.ts` — generic object/JSON helpers


## License

MIT — see [LICENSE](https://github.com/mafedelahoz/mcp-cuddlynest/blob/HEAD/LICENSE).

