vitta-mcp
# vitta-mcp
An [MCP](https://modelcontextprotocol.io) server that lets AI agents book appointments at the beauty and health businesses that run on [Vitta](https://vitta-nu.vercel.app): salons, barbershops, nail and brow studios and clinics in Brazil.
> "Book me a manicure at Studio Ana on Friday afternoon."
>
> The agent looks the business up, finds Friday's free times, reads them back, waits for a yes, and books. Or, if you haven't given it your Vitta login, it hands you the booking link with the time already chosen.
Vitta is a scheduling SaaS I built. Until now its only user was a person tapping through the booking page. This server makes agents a second kind of user, without giving them any power that person doesn't have.
## Try it in a minute
With no configuration the server runs **Vitta's demo**: the same two sample businesses the app shows, `studio-ana` (nails) and `barbearia-nove` (barbershop), with their services, prices, opening hours and a week of appointments counted from today. You act as a demo customer, and bookings live in memory until the server stops.
```bash
git clone https://github.com/leonardobrigolini/vitta-mcp.git
cd vitta-mcp
npm install
npm run build
claude mcp add vitta -- node "$PWD/dist/index.js"
```
Then ask Claude something like *"What can I book at studio-ana, and when is the first free time for a gel manicure?"*.
## Tools
| Tool | What it does | Needs a customer login |
|---|---|---|
| `get_business` | Services (id, duration, price in BRL), opening hours, contact, booking rules | No |
| `find_available_slots` | Free start times for one service, grouped by day, in the business's time zone | No |
| `book_appointment` | Books one of those times | Yes (otherwise returns the booking link) |
| `list_my_appointments` | The customer's appointments across every Vitta business | Yes |
| `cancel_appointment` | Cancels one of them | Yes |
The server also sends the agent short instructions on the expected flow: look up, find, confirm with the person, book.
## Built for an agent, not a form
The booking page can lean on its UI. An agent can't, so the guarantees moved into the server:
- **Same answer as the booking page.** The availability engine is the app's own (`src/lib`, copied with its tests). An agent can never be offered a time a person on the page wouldn't see. This matters more than it looks: the database function that books checks double booking and minimum notice, but not opening hours. On the page, the UI keeps people inside opening hours. For agents, `book_appointment` re-computes the free times and refuses anything that isn't on that list.
- **Errors say what to do next.** "Someone else just booked 09:00. Call find_available_slots again and offer the person new times." Every failure names the tool to call or the link to hand over, so the agent can recover without a human reading a stack trace.
- **No time-zone math for the model.** Every time goes out as ISO 8601 with the business's offset (`2026-10-02T09:00:00-03:00`), plus the date, weekday and wall-clock time. `starts_at` only accepts that format: `Date.parse` would happily read "amanhã às 9" as a date, and a timestamp without an offset would land in whatever zone the server runs in.
- **Honest annotations.** Look-ups are `readOnlyHint`, cancelling is `destructiveHint`, and the booking and cancelling descriptions tell the agent to get an explicit yes first.
- **Read-only without a login.** Connected to a real Vitta but without a customer login, the server can't write anything. It still does the useful part, finding the time, and returns the booking link to finish by hand.
## Connect to a real Vitta
Point the server at a Vitta deployment with its Supabase URL and *publishable* key, the same pair its booking page ships in its JavaScript. Add the customer's own Vitta login (WhatsApp number + password) to let the agent book:
```bash
claude mcp add vitta \
-e VITTA_SUPABASE_URL="https://<project>.supabase.co" \
-e VITTA_SUPABASE_KEY="sb_publishable_..." \
-e VITTA_CUSTOMER_WHATSAPP="(54) 99999-1234" \
-e VITTA_CUSTOMER_PASSWORD="your-vitta-password" \
-- node /absolute/path/to/vitta-mcp/dist/index.js
```
For Claude Desktop, Cursor and other clients, the same goes in the MCP config:
```json
{
"mcpServers": {
"vitta": {
"command": "node",
"args": ["/absolute/path/to/vitta-mcp/dist/index.js"],
"env": {
"VITTA_SUPABASE_URL": "https://<project>.supabase.co",
"VITTA_SUPABASE_KEY": "sb_publishable_...",
"VITTA_CUSTOMER_WHATSAPP": "(54) 99999-1234",
"VITTA_CUSTOMER_PASSWORD": "your-vitta-password"
}
}
}
}
```
| Variable | Default | |
|---|---|---|
| `VITTA_SUPABASE_URL` | none (demo) | The Vitta deployment to talk to. Set both or neither. |
| `VITTA_SUPABASE_KEY` | none (demo) | Its publishable key. |
| `VITTA_CUSTOMER_WHATSAPP` | none | The customer's Vitta login. Set both or neither. |
| `VITTA_CUSTOMER_PASSWORD` | none | |
| `VITTA_APP_URL` | `https://vitta-nu.vercel.app` | Base of the booking links it hands out. |
| `VITTA_CUSTOMER_EMAIL_DOMAIN` | `clientes.vitta.app` | Vitta signs customers in with an e-mail derived from their WhatsApp. |
## Security
- Against a real Vitta it uses only the four database functions the public booking page already calls: `public_org`, `book_appointment`, `my_bookings`, `cancel_booking`. Row-level security answers per signed-in user, so the server can't see another customer's data even if an agent asks for it.
- The customer login lives only in the person's local MCP config. It's used to sign in once per process and is never logged.
- No secrets in this repository: the demo needs none, and a real deployment's publishable key is public by design.
## Development
```bash
npm test # 82 tests
npm run smoke # the built server over stdio: demo by default, read-only against a real Vitta
```
The tests come in four layers:
- **The availability engine**, with the app's own tests: lunch breaks, existing appointments, services that don't fit before closing, businesses that close at midnight, computing in the business's time zone instead of the machine's.
- **Every tool, in memory**, against a fake API: the times it refuses to book, the messages it sends back, the read-only mode.
- **The demo**, end to end: the seeded agenda blocks the right times, and a booking takes its slot until it's canceled.
- **The real process over stdio**, with the real Supabase client talking HTTP to a fake Vitta that answers like the SQL functions. This layer covers what the in-memory tests can't: argument names on the wire, the sign-in, the JSON shapes coming back, the environment handling.
`src/lib` and the demo data are copied from the Vitta app. If the booking page's rules or the demo change, copy them again, so the two never disagree.
---
Built by [Leonardo Brigolini](https://lbcode.com.br) · MIT license
TDQS
Scored across 5 tools
Each tool targets a distinct step of the booking lifecycle: business lookup, availability discovery, booking, listing the user's appointments, and cancellation. There is no overlap or risk of misselection, and descriptions explicitly state prerequisites (service_id, appointment_id, starts_at).
All names follow a consistent verb_noun snake_case pattern (get_business, list_my_appointments, find_available_slots, book_appointment, cancel_appointment). Verbs are distinct and predictable throughout.
Five tools map cleanly onto the minimal end-to-end booking flow, with each tool earning its place. Nothing feels padded or thin for a single-purpose booking server.
The core lifecycle (discover business → check availability → book → list → cancel) is fully covered with no dead ends. Minor gaps remain: no reschedule/update appointment and no business search by name or location, though agents can work around these.