better_luma
# Better Luma FastMCP
A read-only [FastMCP](https://gofastmcp.com/) server for the existing Better Luma / WAP events backend. It gives MCP clients such as Codex structured tools for discovering Luma, Posh, and Partiful events.
This public repository contains the MCP adapter only. It contains no session keys, browser cookies, or private backend code.
## How it works
```text
Codex or another MCP client
|
| MCP over local stdio
v
Better Luma FastMCP (this repository)
|
| HTTP + optional private auth headers
v
WAP FastAPI backend at 127.0.0.1:8000
|
+---- Luma / Posh / Partiful
```
FastMCP defines the tool schemas and handles the MCP protocol. This adapter validates each tool call, maps it to a read-only HTTP request, and returns the backend's JSON. The existing WAP backend remains responsible for platform integrations and scraping.
## Tools
| Tool | Purpose |
| --- | --- |
| `health` | Check backend availability |
| `list_cities` | List cities supported by a platform |
| `list_event_types` | List event category filters |
| `list_events` | Browse platform events by city |
| `get_event` | Fetch one event by ID or Luma slug |
| `search_events` | Search one platform |
| `get_event_guests` | Fetch a permitted guest list |
| `scrape_city` | Collect events and visible attendees without auto-RSVP |
| `list_my_luma_events` | Read the authenticated user's managed, upcoming, or past events |
| `verify_luma_host` | Verify whether the authenticated user hosts an event |
Every MCP tool is annotated read-only. OTP, token-pool, token-revocation, and RSVP routes are deliberately not exposed. Partiful scrape requests always set `auto_rsvp=false`.
## Folder separation
| Folder | Role |
| --- | --- |
| `/Users/samsavage/Documents/luma-mcp` | Public FastMCP adapter |
| `/Users/samsavage/wap/src/better-luma` | Existing Better Luma frontend |
| `/Users/samsavage/wap/src/luma/api/main.py` | Existing private FastAPI backend |
Keeping the adapter separate makes it safe to publish and lets the web app and MCP reuse the same backend logic.
## Install and run
Requirements: Python 3.12+, [uv](https://docs.astral.sh/uv/), and the existing WAP backend.
Install the MCP environment:
```bash
cd /Users/samsavage/Documents/luma-mcp
uv sync --locked
```
Start the WAP backend in a separate terminal:
```bash
cd /Users/samsavage/wap
source venv/bin/activate
uvicorn src.luma.api.main:app --host 127.0.0.1 --port 8000
```
Run the MCP server over stdio:
```bash
cd /Users/samsavage/Documents/luma-mcp
uv run better-luma-mcp
```
An MCP client owns that process's stdin and stdout, so do not type into it directly.
## Configure Codex
Set any private tokens in the environment that launches Codex, then add this to `~/.codex/config.toml`:
```toml
[mcp_servers.better_luma]
command = "/Users/samsavage/Documents/luma-mcp/.venv/bin/better-luma-mcp"
startup_timeout_sec = 10
tool_timeout_sec = 200
env_vars = ["LUMA_AUTH_SESSION_KEY", "POSH_JWT_TOKEN", "PARTIFUL_AUTH_TOKEN"]
[mcp_servers.better_luma.env]
BETTER_LUMA_API_BASE_URL = "http://127.0.0.1:8000"
```
`env_vars` forwards only those named variables from the local environment into the MCP child process. The command, environment, and timeout fields follow the [official Codex stdio MCP configuration](https://learn.chatgpt.com/docs/extend/mcp#stdio-servers). Restart Codex after changing its configuration.
Public discovery tools work without credentials.
## Luma authentication
The agent does **not** receive or request your Luma session key as a tool argument. The boundary is:
1. You sign in to Luma in your browser.
2. In browser developer tools, open **Application** (or **Storage**) > **Cookies** and select the Luma origin (`luma.com` or `lu.ma`).
3. Copy the value of the cookie named `luma.auth-session-key`.
4. Export it as `LUMA_AUTH_SESSION_KEY` in the environment that launches Codex.
5. Restart Codex so it can forward the named variable to the MCP process.
The MCP process reads the variable and sends it to the local WAP backend in the private `x-auth-token` header. It is never included in the MCP tool schema or normal tool results, so the model does not need to see it.
Do not paste a session key into a prompt, commit it, or put it into this repository. Treat it like a password and rotate it by signing out of Luma if it is exposed. `BETTER_LUMA_AUTH_TOKEN` is accepted as a backwards-compatible alias.
## Configuration
| Variable | Default | Description |
| --- | --- | --- |
| `BETTER_LUMA_API_BASE_URL` | `http://127.0.0.1:8000` | WAP FastAPI base URL |
| `BETTER_LUMA_REQUEST_TIMEOUT_SECONDS` | `30` | Normal backend request timeout |
| `BETTER_LUMA_SCRAPE_TIMEOUT_SECONDS` | `180` | Long scrape request timeout |
| `LUMA_AUTH_SESSION_KEY` | unset | Optional Luma session key for private reads |
| `POSH_JWT_TOKEN` | unset | Optional Posh JWT |
| `PARTIFUL_AUTH_TOKEN` | unset | Optional Partiful bearer token |
## Develop and test
```bash
uv sync --locked
uv run pytest -q
uv build
```
The tests use in-memory FastMCP clients and mocked HTTP transports; they do not require real credentials or contact event platforms.
## License
MIT
TDQS
Scored across 10 tools
Most tools target distinct actions, but some overlap exists: list_events and scrape_city both collect events, get_event_guests and scrape_city both collect attendees, and list_my_luma_events overlaps with verify_luma_host in checking host status. The descriptions reduce ambiguity but the boundaries are not always crisp.
The naming pattern is mostly consistent verb_noun snake_case, e.g. list_events, get_event, search_events, verify_luma_host. The lone 'health' tool breaks the pattern by not using a verb, though it is still understandable.
Ten tools is a well-scoped count for an event aggregation/read-only API. Each tool covers a distinct aspect of the domain without unnecessary bloat or obvious redundancy.
The toolset covers the core read-only workflow: event types, cities, event listing/search/detail, guests, city scraping, and authenticated user event queries. Minor gaps exist such as no explicit platform-list tool or direct attendee export beyond guests, but agents can mostly accomplish the intended tasks.