Line-Up MCP server
# Line-Up MCP server
An [MCP](https://modelcontextprotocol.io) server that lets Claude, ChatGPT and other MCP clients work with a [Line-Up](https://www.line-up.tickets) ticketing channel: events, performances with pricing and capacity, seating, packages, visits (who is coming to a performance), transactions, the organisation's venue plans, payment, delivery and seat types, and (when enabled) creating and releasing reservations. It is built from Line-Up's public Transactional API documentation: the OpenAPI 3.1 document the API serves at `https://api.line-up.tickets/api/openapi.json` (Swagger UI at `/api/docs`).
Once it's connected, someone at the venue can ask things like:
- "What's on in the next two weeks, and how many seats are left for each night?"
- "How much is a concession in the stalls for Friday's Macbeth?"
- "Which rows in Stalls Left still have seats together?"
- "Who is coming tonight? Show me booking LU-TEST1 and its notes."
- "What packages do we sell for Macbeth, and what payment methods does this channel take?"
- With writes enabled: "Hold two adult stalls seats for tonight for 15 minutes under the name Box office hold."
## Tools
| Tool | What it does | API calls |
|---|---|---|
| `list_events` | Events with description, run time, venue and address, tags. Filters `venue_id`, `hasPackage`; sort by id, name or venue name; paged. | `GET /event/` |
| `get_event` | One event in full: description, booking information, organisation currency, gallery, seat-view base URL. | `GET /event/{event_id}/` |
| `list_performances` | Performances with start/end, time zone, total capacity and remaining, and pricing per price band and variant (including the `price_id` a reservation needs). Filters `event_id`, `venue_id`, `start_date`, `start_date.gte/.lte`, `end_date.gte/.lte`, `tags`, `day_of_week`, `code`; paged. | `GET /performance/` |
| `get_performance` | One performance with the same pricing and capacity detail. | `GET /performance/{performance_id}/` |
| `get_performance_seating` | Seating groups (areas, sections, blocks, rows) with capacity and capacity remaining; optionally the individual available seats with price band, seat type and group, capped and summarised per group. Filters `parent_id`, `include_root`, `package_id`, `code`. | `GET /performance/{id}/seating-group/`, `GET /performance/{id}/seating-object/` |
| `list_packages_and_add_ons` | Packages per performance for one event (paged) or for one performance, with capacity remaining and total price; with a `transaction_id`, also the products and add-ons available to that basket. | `GET /package/`, `GET /performance/{id}/package/`, `GET /product/`, `GET /add-on/` |
| `list_visits` | Visits: for each performance a customer holds tickets to, the event, the performance and the ticket items with their transaction (reference, lead booker name, status). Filters `time_filter` (upcoming/past), `performance_buffer_minutes`, `sort_direction`; paged. | `GET /visit/` |
| `get_visit` | One visit with every ticket item and, for shared tickets, whether the share was claimed. | `GET /visit/{visit_id}/` |
| `get_transaction` | One transaction: status, reference, totals, customer name, ticket items with seat, price and barcode status (paged), product, delivery, payment, add-on, package and adjuster items, coupons, staff notes (paged). | `GET /transaction/{id}/`, `GET /transaction/{id}/ticket-item/`, `GET /transaction/{id}/note/` |
| `get_organisation_context` | The organisation's currency, venue plans with admission type, payment methods by name and type, seat types (optionally for one performance) and, with a `transaction_id`, the delivery methods available to that basket. | `GET /meta/`, `GET /venue-plan/`, `GET /payment-method/`, `GET /seat-type/`, `GET /delivery-method/` |
| `create_reservation` | Creates a transaction, then adds one ticket item per request (reserved seat, general admission or best available). Refuses locally when seat fields do not match the reservation type. Only registered when writes are enabled. | `POST /transaction/`, `POST /transaction/{id}/ticket-item/` |
| `release_reservation` | Deletes a transaction, releasing the seats it holds. Marked destructive. Writes only. | `DELETE /transaction/{id}/` |
Not covered on purpose: customer accounts, login and password reset, addresses, opt-ins, forms, discounts, payments and payment actions, terminal readiness, completing a transaction, product/package/delivery/add-on items on a basket, notes creation and editing, and the `PUT`/`PATCH` transaction updates. The spec's `x-purchase-flow-id` header and the `group_preset`/`group_by` query parameters are not sent. `GET /health` is not called by any tool (the test suite checks the mock serves it unauthenticated, as the live API does: an anonymous `curl https://api.line-up.tickets/api/health` answers `{"status":"ok"}`, while `/meta/` answers 401; the spec itself lists the bearer scheme on `/health`, with no scopes, so the open access is an observation, not documentation).
## Setup
Requires Node 18 or later.
```bash
npm install
npm run build
```
You need a channel API key from Line-Up. Keys are not self-serve: Line-Up issues one per sales channel on request to their support (see their guide on setting up a third-party seller).
**Claude Desktop:** add this to `claude_desktop_config.json`:
```json
{
"mcpServers": {
"lineup": {
"command": "node",
"args": ["/absolute/path/to/lineup-mcp/dist/index.js"],
"env": { "LINEUP_API_KEY": "your-channel-key" }
}
}
}
```
**Claude Code:**
```bash
claude mcp add lineup -e LINEUP_API_KEY=your-channel-key -- node /absolute/path/to/lineup-mcp/dist/index.js
```
| Variable | Required | Meaning |
|---|---|---|
| `LINEUP_API_KEY` | yes | Your channel API key, sent as `Authorization: Bearer <key>` by default. |
| `LINEUP_AUTH` | no | `bearer` (default) or `basic`. The OpenAPI document declares an OAuth2 password/bearer scheme and an unauthenticated call is answered `401` with `WWW-Authenticate: Bearer`; the older docs at docs.lineupnow.com describe HTTP Basic with the key as the username and no password. If Bearer is rejected, try `basic`. |
| `LINEUP_CHANNEL` | no | An integer sent as the optional `x-channel` header, only on the operations the spec declares it on; it is not sent to `GET /meta/`, `/venue-plan/`, `/visit/` and `/visit/{id}/`, which do not declare it. Leave unset unless Line-Up tells you to set it. |
| `LINEUP_ALLOW_WRITES` | no | `true` to register `create_reservation` and `release_reservation`. Off by default. |
| `LINEUP_BASE_URL` | no | Defaults to `https://api.line-up.tickets/api`. Used by the tests. |
## Safety defaults
- Read-only unless `LINEUP_ALLOW_WRITES=true`. Read tools carry the MCP `readOnlyHint` annotation; `release_reservation` is marked destructive.
- Customer names are returned by default. The customer's email address, postal address and phone number on a transaction, and the purchaser's and recipient's email addresses on a shared ticket, are only returned when a tool is called with `include_contact_details=true`. The same flag governs one rule for text: every string a person typed, whether a customer or the organisation (event, venue, performance, package, product, add-on, venue plan, payment method, delivery method, seat type, price band, variant and adjuster names and descriptions, seat and seating group labels, tag names, seat type notes, coupon codes, transaction references and source references, lead booker names, customer names, staff notes, API warnings and error messages), has email addresses replaced with `[email redacted]` and phone-number-like sequences with `[phone redacted]` by default; IDs, external IDs, enums, dates, times, colours and URLs are returned as stored. Every read tool takes `include_contact_details`; `create_reservation` always returns the redacted form. The phone match is a heuristic: it covers international numbers written with `+` or `00` (including the `+44 (0)7700 …` form), UK numbers with a bracketed area code such as `(020) 7946 0958`, and UK-style `0…` numbers of 9 to 11 digits with spaces, dots or hyphens between groups; other digit strings starting with `0` are redacted too, while public IDs such as `txn_test1`, numeric IDs and timestamps are left alone. Only emails and phone numbers are matched: a postcode, a date of birth or a document number typed into a staff note, a lead booker name or a reference is returned as stored. A venue's address is a business address and is returned.
- Ticket barcode codes (what gets scanned at the door) are only returned with `include_barcodes=true`; their status (type, redeemed, scan status) is always returned.
- Payment provider data is never returned: the Stripe continuation secret, Adyen session data, additional actions, payment references, the SOLT `lastFour` digits, and the provider keys on payment methods (Stripe publishable key and account id, Adyen client key, Square application and location ids). A payment item is reported as method type, amount, currency and status. The customer's identity-provider id (`idpId`) is never returned either.
- Input is checked before any call is made: integer IDs must be positive whole numbers; transaction IDs must match the spec's `txn_[a-zA-Z0-9]+` and visit IDs `^vis_\d+$`; dates must be real calendar dates in `YYYY-MM-DD`; enum parameters (`sort_by`, `sort_direction`, `time_filter`, `reservation_type`) must be one of the documented values; `page`, `max_results`, `performance_buffer_minutes`, `day_of_week`, `max_seats`, `max_notes` and `seconds_to_book` must be within their documented or stated ranges.
- `create_reservation` refuses locally a `seating_object_id` on anything but `RESERVED` and a `seating_group_id` on anything but `BEST_AVAILABLE` (the spec's `ReservedTicketCreate` and `BestAvailableTicketCreate`). Line-Up documents no idempotency key, so none is sent.
- Line-Up documents no rate limit anywhere in the OpenAPI document or the older docs. Requests are spaced 250 ms apart (about four per second). A 429 is retried at most twice for any method, including the two `POST`s of `create_reservation`, on the assumption that a rate-limited request was not processed (see Status). The retry waits for `Retry-After` (whole or fractional seconds, or an HTTP-date; 2 s then 4 s when the header is absent or unreadable). Each wait is capped at 10 seconds so a tool call stays under the MCP client's default 60-second request timeout: if Line-Up asks for a longer wait the call gives up at once and the message says how long to wait.
- 502, 503 and 504 are retried the same way for `GET` only; when all three attempts fail the error says the service may be unavailable and to try again in a few minutes, without the gateway's HTML. A `POST` or `DELETE` is never retried after a gateway error, because the request may already have been processed and a retry could reserve seats twice; the error tells the assistant to check with `get_transaction` first.
- A 200 whose body is not JSON (a proxy or a login page in the way), or whose JSON is not the documented shape (a list without the `Page` envelope's `metadata` and `data`; a record or transaction without `data`), is reported as an error naming `LINEUP_BASE_URL` and the keys it did get, never as an empty list or an empty transaction.
- A rejected API key produces a message that says which variable to fix and which scheme was used (Bearer by default, suggesting `LINEUP_AUTH=basic`; or Basic, suggesting the default); a 403 says the key lacks the scope (`api_read`, `api_write`, `api_list`, `api_delete` in the spec); a 422 is passed on with FastAPI's field-level messages.
## Tests
```bash
npm test
```
The test suite:
1. Validates every fixture record against the component schemas in Line-Up's OpenAPI document (`Event`, `EventDetail`, `PerformancePricing`, `SeatingGroupCapacity`, `PerformanceSeatingObjectDetail`, `PerformancePackages`, `ProductPricing`, `AddOn`, `VisitSummary`, `Visit`, `Transaction`, `TicketTransactionItem`, `NoteDetail`, `Meta`, `VenuePlanListItem`, `DeliveryMethod`, `PaymentMethod`, `SeatType`) with Ajv 2020 (the document is OpenAPI 3.1). The document is downloaded from `api.line-up.tickets/api/openapi.json` to `spec.json` on the first run. One format is relaxed: the spec types `startTime`/`endTime` as `format: time`, which ajv-formats reads as RFC 3339 full-time with a mandatory zone, while FastAPI serialises a naive time as `19:30:00`; the suite accepts `HH:MM:SS` with an optional fraction and zone.
2. Starts a local mock of the API under `/api` that serves those fixtures with the documented `page`/`size` pagination and `MetaData`, serves `/health` without auth (as the live API does; the spec lists the bearer scheme on it), answers a wrong key with the live API's unauthenticated `401` body (`{"detail":"Could not validate credentials"}` and `WWW-Authenticate: Bearer`, which anyone can observe), unknown routes and ids with `{"detail":"Not Found"}` (the spec documents 404 with no body schema), invalid input with the spec's `HTTPValidationError`, and the first `GET /event/` with a 429. Every list, detail, write and error response of the mock is validated against the spec's response schemas.
3. Starts the built server and drives it over stdio with the official MCP client: 32 checks (34 in all with the two above) covering every tool, tool annotations, page-based pagination stopping at the documented `totalPages` (events, performances, packages, ticket items, notes) and reporting `next_page`, every exposed filter passed through under its documented name (including repeated keys for array parameters), redaction of contact details by default (customer email, address and phone; share emails; lead booker names; notes; event text) and their return on request, a probe that types a phone number and an email address into every text field the fixtures carry (names, descriptions, labels, notes, tag names, coupon codes, on every record type) and checks that no tool returns them by default and every read tool returns them with `include_contact_details`, barcodes withheld by default, payment provider data and keys absent from every output, the 429 retry waiting for `Retry-After` in the seconds, fractional-seconds and HTTP-date forms, giving up after three attempts on a persistent 429 and at once on a `Retry-After` above the cap, a 429 on `POST /transaction/` retried once, a 502 retried for `GET` and never for `POST`, a 503 on `DELETE` not retried, a `GET` failing three times with 503 reported with advice and without the gateway HTML, a 200 with a non-JSON body and a 200 whose JSON lacks the `Page` or `data` envelope reported as errors, the write gate with the variable unset and set to `false`, the `POST /transaction/` body validated against `TransactionCreate` and each ticket-item body compared field by field with the documented shape (the spec's branches allow extra properties, so schema validation alone would not catch a stray seat field), the local refusal of mismatched seat fields, a refused later ticket item reported with the transaction id, the `DELETE`, refusal before any call of bad IDs, unknown enum values, out-of-range numbers and impossible dates, the 401 message in both auth schemes, the 403 and 404 messages, `LINEUP_AUTH=basic`, `LINEUP_CHANNEL` sent exactly on the operations the spec declares `x-channel` on (checked against the spec) and not on `/meta/`, `/venue-plan/`, `/visit/` and `/visit/{id}/`, that the main session used only the Bearer key and every call template it should, and that every request of the run, later sessions included, hit a documented method and path with its trailing slash, without the `x-purchase-flow-id` header.
## Status
This is a working prototype. It has **not yet been run against the live API**, because it was built without a Line-Up channel key (keys are issued by Line-Up on request, not self-serve). Everything below is taken from the published OpenAPI document and should be confirmed on a real channel:
- Authentication: whether the channel key is accepted as a Bearer token (the OpenAPI document's scheme, and what the unauthenticated 401 advertises) or must be sent as the HTTP Basic username (the older docs' scheme, `LINEUP_AUTH=basic`); whether `POST /purchase-channel/{key}/` (a token exchange the spec lists) is needed first; what a 403 looks like when a key lacks a scope; and whether the `x-channel` header is needed at all.
- Pagination: that `page` is 1-based and `size` up to 1000 are honoured as documented, that `totalPages` and `numberOfResults` are filled (the spec allows null; the server then ends at a short page), and what a page past the end returns (the mock answers an empty `data`).
- The sort order of every list. The spec documents `sort_by`/`sort_direction` only on events and `sort_direction` on visits; the server returns whatever order the API uses.
- The meaning of `code` on performances, seats and products (typed as a list of strings; the mock records it and does not interpret it), the numbering of `day_of_week`, the `expand` parameter on `GET /performance/` (not exposed), and whether `hasPackage=false` means "no filter" or "events without packages" (the server only sends the parameter when given).
- Which id is `priceId` for a ticket item. The server reports a pricing variant's `price.id` as `price_id` because a ticket item's `Price` in a transaction has the same `{id, value, priceBand, priceVariant}` shape; the variant's own id is reported as `variant_id`. Confirm which one `POST .../ticket-item/` expects.
- The reservation flow: the default and maximum `secondsToBook`, the status a new transaction has (`IN_BASKET` or `RESERVATION` in the enum), whether ticket items can be added one request at a time as this server does (the spec also accepts `ticketItems` inside `TransactionCreate`), what a refused ticket item answers (the mock uses 422 for an unknown price and 404 for an unknown seat), and whether `DELETE /transaction/{id}/` releases a basket (the spec calls it "Delete Transaction", describes it as "Patch an existing transaction" and answers with the object's id; what it does to a completed transaction is undocumented, so only release reservations you created).
- `GET /product/` without a `transaction_id` (the spec makes it optional; the server only calls it with one), and whether `GET /add-on/` and `GET /delivery-method/` really need a transaction (they are required parameters in the spec).
- Time and date formats: `startTime` with or without a zone (see Tests), and `timeZone` values.
- Whether `GET /transaction/{id}/ticket-item/` fills `barcode` for reservations and completed bookings alike, and which fields the live API fills on `Transaction` (`customer`, `address`, `phoneNumber`, `parentTransaction`, `coupons`).
- The wording of Line-Up's error messages and whether any echo request data; the server passes on FastAPI's `detail` strings and redacts contact details from them regardless.
- Rate limits: nothing is documented, so the 250 ms spacing is a guess on the polite side, and a 429 on `POST` is retried on the assumption that a rate-limited request was not processed. Confirm on a live channel that Line-Up never creates the transaction before answering 429.
## Going to production
This version runs locally over stdio, with the channel's own API key. For venues to connect from claude.ai or ChatGPT without handling keys, the next step is a remote server (Streamable HTTP) behind OAuth, hosted by Line-Up, and then a listing in the Claude and ChatGPT connector directories. Customer-facing flows (customer accounts, payment, completing a transaction) can follow once they can be tested on a real channel.
## Licence
MIT. Built by Alexandru Dragoș (alexandru.dragos96@gmail.com) with an AI agent (Claude) working under his direction.
TDQS
Scored across 10 tools
The list_/get_ pattern cleanly separates collection vs. detail for events, performances, visits and transactions, and get_performance_seating and get_organisation_context are clearly distinct. The only slight ambiguity is list_packages_and_add_ons, which merges packages, products and add-ons with conditional behavior depending on event_id/performance_id/transaction_id.
Every tool follows a consistent verb_noun snake_case convention (list_events, get_event, list_performances, get_performance, get_visit, etc.). The pattern is predictable throughout with no mixed styles or vague verbs.
Ten tools is well within the ideal range and each earns its place, covering the read/query surface for events, performances, seating, packages, visits, transactions and org context without redundancy.
The lifecycle for querying the domain is well covered: list/detail for events, performances, seating, packages, visits and transactions plus configuration context. It is a read-only surface with no reservation/booking creation or update tools, but the descriptions imply a query-oriented purpose rather than a gap.