Skip to main content
Glama
teslakoile

wishlist-mcp

by teslakoile

wishlist-mcp

The hosted MCP server for wishlist.fit. It lets Claude, ChatGPT, or Codex read and update your wishlist on your behalf.

https://mcp.wishlist.fit/mcp

Connect it from app.wishlist.fit/connect, which has per-client setup steps. You need a wishlist account first; this server never creates one.

docs/connecting.md has the longer version: exact commands for each client, what has actually been verified against production, and the two Codex flags that waste an afternoon if you get them wrong.

What it is

A pure OAuth 2.1 resource server in front of the wishlist REST API. It holds no database and no business rules. Visibility, rate limits, and invite handling all live in the wishlist API, and this server inherits them by calling that API with the user's own access token.

That split is deliberate. The alternative, a second service with its own copy of the rules, is how two surfaces quietly start disagreeing about who may see what.

Claude / ChatGPT / Codex
        │  MCP over HTTP, bearer token
        ▼
   this server ──── verifies the token (issuer + audience)
        │
        │  the same token, forwarded
        ▼
   api.wishlist.fit ──── applies visibility, rate limits, invite rules
        │
        ▼
     Postgres

WorkOS AuthKit is the authorization server. This server issues nothing and shows no consent screen; it only verifies what AuthKit signed.

Related MCP server: Personal Toolkit MCP Server

The tools

Twenty-nine, one per thing you can already do by hand in the web app. Parity is the rule: no agent-only privileges, and nothing the app itself cannot do.

Tool

Kind

wishlist_search_people

read

wishlist_get_gift_guide

read

wishlist_get_profile

read

wishlist_get_wishlist

read

wishlist_get_my_profile

read

wishlist_get_my_wishlist

read

wishlist_list_circle

read

wishlist_list_circle_requests

read

wishlist_list_nudge_prompts

read

wishlist_list_nudges

read

wishlist_preview_invite

read

wishlist_upcoming_occasions

read

wishlist_list_notifications

read

wishlist_get_reminder_preferences

read

wishlist_add_item

write

wishlist_update_item

write

wishlist_update_my_profile

write

wishlist_request_circle

write

wishlist_accept_circle_request

write

wishlist_decline_circle_request

write

wishlist_accept_invite

write

wishlist_update_reminder_preferences

write

wishlist_mark_notifications_read

write

wishlist_send_nudge

write

wishlist_answer_nudge

write

wishlist_dismiss_nudge

write

wishlist_create_invite

destructive

wishlist_delete_item

destructive

wishlist_remove_circle_member

destructive

wishlist_get_gift_guide returns a person's profile and wishlist together. It is the reason anyone connects this server, and without it the gift-giver journey costs three round trips. It carries the fields that actually rule a gift in or out: dietary rules and allergies, interests, price comfort, what they already own, and their birthday without the year.

wishlist_upcoming_occasions answers "whose birthday is next" in one call, and pairs with the gift guide: it hands back a username and a date, and the guide turns that into something to buy. Birthdays in it are only people whose circle the caller is in, and only those who filled one in, so an absent birthday is not evidence that someone has none.

The three destructive tools carry destructiveHint: true, so clients that confirm irreversible actions will ask first. Sending an invite emails a real person and cannot be unsent. Accepting a request or an invite is reciprocal: the other person gains access to your circle-only fields as well as you gaining access to theirs. Removing a member cuts both ways at once, and getting back in needs a fresh request they have to accept.

Nudges

A nudge is a short question between two people already in the same circle: is your wishlist still current, do you still want this item, are your sizes still right, could you add a few ideas.

It is anonymous by default. wishlist_send_nudge takes signed, which defaults to false; leave it there unless the user asks to be named, because asking whether someone still wants an item, under your user's name, tells that person who is buying it. On the receiving side an incoming nudge with a null username was sent anonymously: report it as "someone in your circle" and do not try to work out who from wishlist_list_circle. The server withheld the name deliberately, and naming a guess is worse than naming nobody.

The wording is not the caller's. wishlist_list_nudge_prompts serves the catalogue, wishlist_send_nudge takes a prompt key, and nothing an agent writes is delivered to the recipient. That is the point: with no free-text field there is nothing to moderate and nothing a model can be argued into sending on someone's behalf.

Sending one emails a real person, so confirm the question and the name first. The API refuses a nudge to anyone outside your circle, to anyone who has nudges off, more than once a day per person, for a week after they dismiss one, and after ten in a day. Each refusal names which and when to try again; report it rather than retrying.

Answering still_current records that your wishlist or profile was confirmed today, which everyone in your circle sees on wishlist_reviewed_at and profile_reviewed_at. The answer is the user's to give: ask which option they want rather than inferring one.

Two ways into a circle

wishlist_create_invite takes an email address and wishlist_request_circle takes a username, but the API decides which path an address takes, not the caller. An address with no account behind it gets a tokenized email; one that already belongs to someone gets an in-app request and no email at all. wishlist_create_invite reports which happened in its outcome field, and a caller that assumes an email went out will tell the user something untrue.

One connection, everything

There are no scopes. WorkOS cannot express custom ones, so a connection carries the whole tool surface. Protection on writes is behavioural rather than structural: the annotations above, the tool descriptions, and the rate limits the API applies.

Running it locally

uv sync
cp .env.example .env      # point it at a local API and the Local WorkOS environment
uv run python -m wishlist_mcp.main
uv run pytest
uv run ruff format --check . && uv run ruff check .

Tests stub the wishlist API with respx. What they check is this server's own job: verifying tokens, shaping requests, and turning API errors into sentences a model can act on. The rules themselves belong to the API and are tested there.

Keeping parity

The wishlist API ships from another repo on another deploy, so an endpoint or a field can change there, reach the website, and leave this surface where it was. Nothing here detects that.

GiftProfile and MyProfile in schemas.py list readable fields explicitly, and wishlist_update_my_profile lists writable ones as arguments, so a new column stops at this boundary until someone moves it. The Dietary, GiftFormat, PriceComfort, and InterestCategory literals are copies of the API's enums for the same reason.

Watch for a second failure mode that does not look like a gap: a tool reads one key out of a response body, and an endpoint that starts answering with a second shape makes it report the wrong thing confidently rather than fail. POST /invites did exactly that.

The workspace repo tracks the ledger in docs/mcp-parity.md.

Configuration

Every variable is prefixed WISHLIST_MCP_. See .env.example.

Variable

Purpose

API_BASE_URL

The wishlist REST API to call

AUTHKIT_DOMAIN

WorkOS AuthKit issuer. Empty means the server refuses to start

RESOURCE_URI

Canonical URI. Every token's audience must match it exactly

HOST

The server 404s on any other host

API_TIMEOUT

Seconds to wait on the API

RESOURCE_URI must match the resource indicator configured in WorkOS character for character, including the /mcp path. A mismatch is the most common reason a client refuses to connect.

Things worth knowing before you change this

Each of these is a bug that reached production once.

  • get_http_headers() strips authorization. Ask for it explicitly. Without that, every tool call reports "no access token" while initialize and tools/list still succeed, because those are answered before any tool body runs. A green handshake is not evidence the tools work.

  • The MCP app is mounted at the root and owns its own path. Mounting it at /mcp makes Starlette redirect /mcp to /mcp/, and the address every client is handed has no trailing slash.

  • X-Forwarded-Proto is trusted. Cloud Run terminates TLS, so without it every generated URL claims http://. Combined with the redirect above, that once meant a client would have sent its bearer token in the clear.

  • FastMCP's lifespan is chained into the app's. Mounting an ASGI app does not start its lifespan, and without it every tool call fails with "Task group is not initialized" while unit tests still pass.

  • TestClient follows redirects by default. Pass follow_redirects=False when the point is that a path answers directly.

  • Every path segment is encoded with segment(). httpx applies RFC 3986 dot-segment removal before a request goes out, so an unencoded username or invite_token containing ../ is not a 404: it is a different endpoint, called with the user's own token and reported to the model under the name of the tool that was invoked. A ? does the same by starting a query string. These arguments are chosen by a model that has read item names and bios other people wrote, so treat them as hostile.

  • FastMCP matches Accept by substring. It answers 406 Client must accept application/json to a client sending */*, which already does, and to one sending no header at all, which under RFC 9110 also does. WildcardAccept spells those out before FastMCP sees them. Plain curl sends */*, so this was the first thing anyone hit.

Deployment

Cloud Run, asia-southeast1, in the same project as the rest of wishlist. main deploys on push. Infrastructure lives in wishlist-infrastructure.

Licence

MIT. See LICENSE.

Related MCP Connectors

Related MCP Servers

  • F
    license
    A
    quality
    D
    maintenance
    MCP server for the forme.gifts wishlist app that enables managing wishlists and gifts from Claude Code, Claude Desktop, Cursor, and other MCP clients.
    10
    6 npm
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    A custom MCP server that turns Claude into an agentic shopping assistant for searching products, comparing options, managing a cart, and checking out with built-in guardrails for safe autonomous commerce.
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    A production-ready MCP server that gives an LLM agent standalone-equivalent control over a Mineflayer Minecraft bot — movement, mining, crafting, inventory, combat, containers, chat, and much more — exposed as 110 strongly-typed tools across 23 groups, with full bot lifecycle management and dual (poll + push) event streaming.
    50 npm
    2
    MIT