Skip to main content
Glama
David-Solano

invitaai-mcp

by David-Solano
README.md
# invitaai-mcp

MCP server for [InvitaAI](https://invitaai.com), a digital invitations platform. Create events, invitations and
personalized guest links, and follow RSVPs from a phone, Claude Code, or any MCP client.

> "Create a wedding invitation for Dec 12 at Hacienda Los Arcos, champagne theme, and add my aunt Rosa with 2 seats."

## Two ways to run the same tools

| | Remote (default) | Local |
|---|---|---|
| Where it runs | Mounted at `https://invitaai.com/mcp` | On your machine, over stdio |
| Who connects | Any AI app, phone included: add the URL as a custom connector | Claude Code / Codex on that machine |
| Login | OAuth 2.1: dynamic client registration + authorization code with PKCE | Device Authorization Grant (RFC 8628): a code you approve in the browser |
| Token | Issued by the OAuth flow, sent on every request | Stored in `~/.invitaai/credentials.json` (`0600`) |

Both paths end with the same 90-day `inv_` token and the same tools. The server holds **no database
credentials** either way: it acts as the user through the same public API as the web app, so ownership
checks and business rules stay server-side.

### Remote: connecting from a phone

```mermaid
sequenceDiagram
    participant U as User (phone)
    participant C as AI app (Claude, ChatGPT…)
    participant S as invitaai.com/mcp + OAuth
    U->>C: adds the connector URL
    C->>S: POST /register (dynamic client registration)
    C->>U: opens /authorize -> consent screen
    U->>S: logs in, clicks Authorize
    S-->>C: authorization code
    C->>S: POST /token (code + PKCE verifier)
    S-->>C: token (90 days)
    C->>S: MCP calls with Authorization: Bearer inv_...
```

### Local: connecting a CLI on your own machine

```mermaid
sequenceDiagram
    participant U as User
    participant A as AI client + invitaai-mcp (stdio)
    participant API as invitaai.com API
    A->>API: POST /api/device/code
    API-->>A: user_code + link
    A->>U: "Open the link and confirm BCDF-GHJK"
    U->>API: logs in, clicks Approve
    A->>API: POST /api/device/token (polling)
    API-->>A: token (90 days), stored locally
    A->>API: Authorization: Bearer inv_...
```

## Security model

| Decision | Why |
|---|---|
| Remote login with OAuth 2.1: registration is dynamic (RFC 7591), the code is single-use and bound by PKCE | An app we've never seen can ask for access, but only a human in the browser grants it, and a stolen code is useless without the verifier. |
| Local login with the Device Authorization Grant (RFC 8628) | Same idea without a redirect: the user approves a short code in the browser; no password ever reaches the agent. |
| No refresh tokens | After 90 days the user approves again — renewal always passes through a person. |
| Tokens are random, stored server-side only as SHA-256, expire in 90 days, revocable at `/agentes` | A leaked database doesn't leak usable tokens; access is time-boxed and can be cut. |
| A token can't create or list tokens | A stolen token can't make itself permanent. Renewal always needs a human. |
| Warning from 14 days before expiry | Every tool result carries an `aviso` the agent relays to the user. |
| `device_code` and token never returned to the model | Tool outputs contain links and codes for the user, never secrets. |
| Local token file created with `0600`; remotely the token only lives in the request | Nothing readable is left behind on either path. |
| No destructive tools (delete event/guest) and no local file access | Limits damage from prompt injection. |
| Guest RSVP messages returned as `guest_message` and flagged in the instructions | Third-party text is data, not instructions. |
| Event type and theme are enums in the tool schema | Invalid values are rejected before reaching the API. |

## Tools

| Tool | Type |
|---|---|
| `connect_account`, `finish_connection` | Connect or renew access (local mode only) |
| `connection_status` | Read |
| `list_events`, `get_event` | Read |
| `create_event`, `update_event` | Write |
| `get_invitation` | Read |
| `create_invitation`, `update_invitation`, `set_invitation_active` | Write |
| `get_design_options`, `search_photos` | Read |
| `create_photo_upload_link` | Write |
| `customize_design`, `set_cover_photo`, `add_gallery_photos`, `set_music` | Write |
| `add_guest`, `list_guests` | Write / Read |
| `get_rsvps`, `get_event_stats` | Read |

Prompt: `guided_invitation` walks the user through event data, theme, photos, texts and
guests one question at a time (a slash command in clients that support prompts).

### Design notes

- **Edits merge, never replace.** The API stores invitation texts and design as whole objects,
  so every edit tool reads the current one and writes back only the requested change. Changing
  the music can't wipe the gallery.
- **Creating a second invitation for an event is refused**, pointing the model at `update_invitation`.
  Without that, an agent asked to "change the colour" creates a duplicate and the shared link goes stale.
- **The client model writes the invitation texts.** The platform's templates fill the rest, so no
  section is ever left blank and no extra LLM bill is added.
- **Everything the platform already measures is reachable.** Views, seats allowed vs. confirmed,
  who answered and when, contact details, response rate and per-event totals were all being
  collected and only half-exposed; an agent that can't see them can't help the host follow up.
- **No option lists live in this repo.** Event types and themes used to be duplicated here and
  drifted from the platform; every value is now validated against the served catalog, and a wrong
  one comes back with the real options.
- **The agent designs, within a catalog.** `get_design_options` returns the themes, textures,
  ornaments, fonts, layouts and cover styles the platform actually renders — served by the app, so
  the agent can't drift from what exists — and `customize_design` applies a chosen combination
  plus a custom palette. Free-form CSS is deliberately not exposed: an invitation shown to guests
  shouldn't depend on a model writing stylesheets.
- **Addresses are geocoded, and failures are reported.** A map button built from raw text opens an
  empty search; the API resolves the address first and the tool tells the agent when it couldn't,
  so it asks the user instead of leaving a dead button in front of the guests.
- **Song links are verified, not trusted.** Models invent plausible YouTube/Spotify URLs, so
  `set_music` resolves the link through the provider's oEmbed endpoint: a fake link is refused
  and a real one supplies the actual track title. Spotify answers come with a note that guests
  without a session only hear a 30-second preview.
- **The agent is blind to the result.** Its instructions say so: propose named looks, apply, and ask
  the user to open the link and react. The loop is human-in-the-eye, not guesswork.
- **Only public https image links** reach the invitation (`javascript:`, `http:` and non-images are rejected).
- **The user's own photos travel by link, not through the model.** Tools can't receive files, so
  `create_photo_upload_link` returns a short-lived, single-invitation upload link the user opens
  on their phone. Errors about image URLs point the model at that tool instead of dead-ending.

## Use it

**Remote (phone or desktop, nothing to install):** add `https://invitaai.com/mcp` as a custom connector in your
AI app — no client ID or secret, the server registers the app itself — and approve the consent screen.
Instructions per app live at [invitaai.com/agentes](https://invitaai.com/agentes).

**Local (stdio):**

```bash
git clone https://github.com/David-Solano/invitaai-mcp && cd invitaai-mcp
python -m venv .venv && .venv/bin/pip install -e .     # Windows: .venv\Scripts\pip
claude mcp add invitaai -- /absolute/path/to/.venv/bin/invitaai-mcp
```

Then ask your agent: *"conéctame a InvitaAI"*. `INVITAAI_URL` points it at another deployment (e.g. local dev).

The deployment mounts this package with `build_server(client, with_local_login=False)`, which drops the two
device-login tools (OAuth already authenticated the user) and takes the token from the request instead of a file.

## Development

```bash
pip install -e ".[dev]"
pytest
```

Tests drive the server through the MCP protocol (in-memory client) against a fake of the InvitaAI API.

Tool names, arguments and results are in English; the strings a person reads (errors, notes the
assistant relays) stay in Spanish, the product's language.

Built with Claude Code as a pair programmer; design decisions and review by the author.

MIT

TDQS

A3.7/5.0

Scored across 18 tools

Disambiguation5/5

Each tool targets a distinct action or resource: account connection steps, event CRUD, invitation customization, media handling, and guest management. Even similar tools like ver_evento and ver_invitacion are clearly separated by scope (event details vs. invitation content).

Naming Consistency4/5

Tool names follow a mostly consistent verb_noun pattern in lowercase with underscores (crear_evento, editar_invitacion, listar_invitados). The only deviation is estado_conexion, which uses a noun instead of a verb, slightly breaking the otherwise uniform convention.

Tool Count4/5

With 18 tools, the set is on the heavier side but each tool addresses a specific need across account, events, invitations, media, and guests. The count feels justified given the breadth of the domain, though it is above the typical sweet spot of 3-15.

Completeness4/5

The tool set covers the main lifecycle: account connection, event creation/viewing/editing, invitation creation/editing/activation, photo selection, music, and guest RSVPs. A few obvious deletions are missing (e.g., delete event, delete invitation, remove guest), but these are minor gaps that can be worked around.

Maintenance

ActivityMaintained
ResponsivenessNo issues