mydatavalue-mcp
by Elev8-OS
README.md
# mydatavalue-mcp
A small remote MCP (Model Context Protocol) server that lets Claude query the
[MyDataValue](https://app.mydatavalue.com) API — Booking.com + Airbnb
properties, pricing, promotions, ranking, reviews, demand, compsets,
performance, and the change log — as a native connector, instead of copying
curl commands around.
It is **read-only by design** (GET requests only), matching the scope of the
MyDataValue API key it was issued.
## Why this exists / how the OAuth dance is handled
MyDataValue issues a `refresh_token` that:
- is exchanged for a 1-hour `access_token` via `POST /oauth/token`
- **rotates on every exchange** — the old `refresh_token` stops working
the instant a new one is issued
- gets your whole integration **shut off** if an old, already-used
`refresh_token` is replayed (their theft-detection)
That means the refresh token can never live only in a chat transcript, a
`.env` file on a laptop, or this repo — it has to be persisted durably by
whatever is running the service, updated atomically on every rotation, and
never reused. `src/tokenStore.js` + `src/mdvClient.js` do exactly that:
- the current `refresh_token` / `access_token` / expiry live in a JSON file
at `TOKEN_STORE_PATH` (defaults to `/data/token-store.json` — point this
at a mounted persistent volume in production)
- writes are atomic (temp file + rename) so a crash mid-write can't corrupt
the store or leave you holding a half-written token
- concurrent requests are coalesced onto a single in-flight refresh, so two
simultaneous tool calls can never race each other into burning the
refresh token twice
- 401s trigger exactly one forced refresh + retry; 429s honor `Retry-After`
### A note on durability
The first version of this service persisted the rotating `refresh_token` to
a Railway volume mounted at `/data`. In testing, that turned out **not** to
reliably survive redeploys on this service — a plain redeploy with zero code
or config changes came back up unable to find the file it had written
seconds earlier, twice in a row. Railway's own docs say this shouldn't
happen for a volume-attached service, so the exact cause is unconfirmed
(possibly an interaction with this service's `multiRegionConfig`), but it's
reproducible here, and reusing a stale `refresh_token` is exactly the
failure mode MyDataValue treats as a stolen-token replay and locks accounts
over — so this service does not rely on the volume for correctness.
Instead, `MDV_SEED_REFRESH_TOKEN` is the durable source of truth, and it's
kept current in place: every time the service rotates the token, it calls
Railway's public API (`variableUpsert`, with `skipDeploys: true` so it
doesn't force a redeploy) to overwrite `MDV_SEED_REFRESH_TOKEN` with the new
value. Env vars have reliably round-tripped through every redeploy in
testing, unlike the volume. The next boot - whenever that happens - reads
the current value straight from that env var. `src/railwayVariable.js`
implements this; it needs `RAILWAY_PROJECT_TOKEN`, `RAILWAY_PROJECT_ID`, and
`RAILWAY_ENVIRONMENT_ID` (`RAILWAY_SERVICE_ID` optional) to be set. Without
those, the service still runs (rotations just live only in memory), but it
logs a loud warning on every rotation, because a restart before persistence
succeeds means the next boot replays a now-stale token.
The local file at `TOKEN_STORE_PATH` is still written on a best-effort
basis (useful for poking at from Railway's file browser while debugging),
but nothing in this service depends on it being there.
## Endpoints wired up
Confirmed against MyDataValue's own OpenAPI reference (Booking.com uses
`property_id` path params under `/booking/...`; Airbnb uses `listing_id`
path params under `/airbnb/...`, and its collection is specifically named
`listings`, not `properties`):
- `list_properties` — `/booking/properties/`, `/airbnb/listings/`
- `get_property` — single property/listing detail, by id
- `get_pricing` — `/{channel}/pricing/{id}/`
- `get_compset` — `/{channel}/compset/{id}/`
- `get_promotions` — `/{channel}/promotions/`, filterable by id, paginated
- `get_ranking` — `/{channel}/ranking/`
- `get_reviews` — `/{channel}/reviews/`
- `get_performance` — `/{channel}/performance/`
- `get_demand` — `/booking/demand/` (Booking.com only — MyDataValue doesn't
expose an Airbnb demand endpoint)
- `get_change_log` — `/change-log/` (a single team-wide feed, not per-channel)
- `get_cancellation_policy_options` — `/airbnb/cancellation-policy-options/{listing_id}/`
(Airbnb only)
- `mdv_raw_get` — a generic, read-only passthrough for anything above that's
missing a param, plus endpoints intentionally left out of scope (auto-refresh
state, sync-jobs, tags, webhooks — these are either write-oriented or not
part of what this connector's token was issued to read)
### A note on MyDataValue's own hosted MCP server
MyDataValue's docs mention they also run an official hosted MCP server at
`https://mcp.mydatavalue.com/mcp` — sign in with your MyDataValue account
directly, no Railway service needed. It's a legitimate alternative, but per
their own docs it only covers properties, pricing, promotions, and the
write levers to act on them; it doesn't expose ranking, reviews, demand,
compsets, or the change log the way the REST API (and this connector) does.
Worth knowing about if you ever want to *act* on pricing/promotions from
chat, not just read — this connector is deliberately read-only.
## Running locally
```bash
cp .env.example .env # fill in MDV_CLIENT_SECRET, MDV_SEED_REFRESH_TOKEN, MCP_ACCESS_TOKEN
npm install
npm start
```
## Deploying
Designed to run on Railway (or any host that gives you a persistent volume):
1. Deploy this repo.
2. Attach a persistent volume, mounted at `/data`.
3. Set env vars: `MDV_CLIENT_ID`, `MDV_CLIENT_SECRET`, `MDV_SEED_REFRESH_TOKEN`
(kept current automatically after the first rotation - see "A note on
durability" above), `TOKEN_STORE_PATH=/data/token-store.json` (best-effort
only), `MCP_PUBLIC_URL` (the exact public HTTPS URL Railway gives the
service, no trailing slash), `MCP_OAUTH_CLIENT_ID`, `MCP_OAUTH_CLIENT_SECRET`
(generate with `openssl rand -hex 32`), and `MCP_ACCESS_TOKEN` (also
`openssl rand -hex 32` — this is now the one-time consent passphrase,
see below, not a request header).
4. For durable refresh-token persistence, create a Railway **Project
Token** (Project Settings → Tokens, scoped to this project) and set it
as `RAILWAY_PROJECT_TOKEN`, plus `RAILWAY_PROJECT_ID` and
`RAILWAY_ENVIRONMENT_ID` (and `RAILWAY_SERVICE_ID` if you want variable
updates scoped to just this service). Skipping this makes rotations
memory-only - fine for a quick test, risky for anything left running.
5. Generate a public domain.
## Connecting it in Claude
Claude's "Add custom connector" dialog only takes a server URL plus,
optionally, an **OAuth Client ID** and **OAuth Client Secret** in Advanced
settings — there's no field for a raw bearer header. So this server
implements a minimal (but spec-following) OAuth 2.1 authorization server in
front of `/mcp`: RFC 9728 protected resource metadata, RFC 8414
authorization server metadata, and the authorization_code + PKCE grant plus
refresh_token, per the [MCP Authorization spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization).
There's exactly one pre-shared client (this connector) - no Dynamic Client
Registration, which the spec allows as an alternative.
To connect:
1. In Claude, add a custom connector with URL `https://<your-domain>/`
(either the bare domain or `https://<your-domain>/mcp` works - both are
handled, since it's easy to paste one or the other into that one field).
2. In Advanced settings, enter `MCP_OAUTH_CLIENT_ID` as the OAuth Client ID
and `MCP_OAUTH_CLIENT_SECRET` as the OAuth Client Secret.
3. Claude will open `/authorize` in a browser tab. Enter the
`MCP_ACCESS_TOKEN` value as the passphrase to approve the connection.
This is a one-time step (until the issued token expires/is revoked).
`MCP_OAUTH_ALLOWED_REDIRECT_URIS` defaults to `*` (accept any redirect_uri)
on first deploy, since Claude's exact callback URL isn't documented. Once
you've connected once, check the deploy logs for the `redirect_uri` that
was actually used (`[oauthServer] /authorize request - redirect_uri=...`)
and set `MCP_OAUTH_ALLOWED_REDIRECT_URIS` to that exact value (comma-separate
if there's more than one) to close the open-redirect surface.
## Security notes
- `MCP_ACCESS_TOKEN` gates the one-time `/authorize` consent step — anyone
who has it (and the client ID) can complete the OAuth flow and mint a
token. Treat it like a password. It is no longer sent as a request header.
- `MCP_OAUTH_CLIENT_SECRET` and the issued access/refresh tokens are the
keys to `/mcp` itself going forward - keep them as secret as
`MCP_ACCESS_TOKEN`.
- `MDV_CLIENT_SECRET` and the rotating MyDataValue `refresh_token` never
leave the server process, Railway's env var store, and (best-effort) its
volume. They are not logged in full (only the last 6 characters, for
audit purposes).
- `RAILWAY_PROJECT_TOKEN` can rewrite this project's env vars - treat it
with the same care as the other secrets here.
- If MyDataValue ever locks you out for a suspected replayed token, contact
them to get re-issued, then set a fresh `MDV_SEED_REFRESH_TOKEN` before
redeploying.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues