livekit-ops-mcp
README.md
# livekit-ops-mcp
An MCP server that gives an AI agent operator control of a LiveKit deployment, plus a workshop kit that runs a hackathon room-by-room in one call.
24 tools over stdio. TypeScript, `@modelcontextprotocol/sdk`, `livekit-server-sdk`. Works against `livekit-server --dev` with zero configuration and against LiveKit Cloud with three environment variables.
```
npx livekit-ops-mcp # after publish; or: node dist/index.js
```
## What it is
LiveKit already has MCP on the agent side: the Agents framework can consume MCP tools, and docs.livekit.io serves a documentation MCP. What did not exist was the other direction: an MCP server that exposes the LiveKit **server API** (rooms, access tokens, participants, data messages, egress, ingress) so that Claude, Cursor, or any MCP client can operate a deployment by talking to it.
On top of that control plane sits a workshop kit, because the operator who needs this most is the person running a room full of developers:
- `workshop_spinup`: one call creates a room per team, mints a scoped join token per seat, and a mentor token with admin and record rights per room.
- `workshop_join_sheet`: turns that manifest into a printable HTML sheet with a QR code per seat (or Markdown).
- `workshop_status`: which teams are in their rooms, which rooms are empty, who is publishing.
- `workshop_broadcast`: "30 minutes left, submit your repo link" to every team room at once.
- `workshop_teardown`: delete everything under the event prefix when the day ends.
## Why
Events and community work run on repetition: the same room setup, the same token hand-out, the same "who is stuck" walk around the room, the same cleanup. Making that a five-prompt conversation with an agent means a meetup or hackathon host spends the evening with developers instead of with a terminal. The same tools double as the feedback loop: `workshop_status` and `participant_list` show, in real time, where builders drop off, and every session leaves a manifest that says what was tried. That is the signal a developer advocate carries back to Product.
## Install
```bash
git clone https://github.com/ExpertVagabond/livekit-ops-mcp
cd livekit-ops-mcp
npm install
npm run build
```
Run a local LiveKit server (no account needed):
```bash
# Docker
docker run --rm -p 7880:7880 -p 7881:7881 -p 7882:7882/udp \
livekit/livekit-server --dev --bind 0.0.0.0 --node-ip 127.0.0.1
# or Homebrew
brew install livekit && livekit-server --dev
```
`--dev` uses the well-known keys `devkey` / `secret`, which are also this server's defaults. For LiveKit Cloud or your own deployment:
| Variable | Default | Notes |
|---|---|---|
| `LIVEKIT_URL` | `http://localhost:7880` | `wss://<project>.livekit.cloud` works; the http host is derived |
| `LIVEKIT_API_KEY` | `devkey` | |
| `LIVEKIT_API_SECRET` | `secret` | |
| `LIVEKIT_MEET_URL` | `https://meet.livekit.io/custom` | Base for join links (`?liveKitUrl=&token=`) |
Claude Desktop / Claude Code / Cursor config:
```json
{
"mcpServers": {
"livekit": {
"command": "node",
"args": ["/path/to/livekit-ops-mcp/dist/index.js"],
"env": {
"LIVEKIT_URL": "wss://your-project.livekit.cloud",
"LIVEKIT_API_KEY": "APIxxxxxxxx",
"LIVEKIT_API_SECRET": "..."
}
}
}
}
```
Claude Code one-liner: `claude mcp add livekit -- node /path/to/livekit-ops-mcp/dist/index.js`
## Tools
| Tool | What it does |
|---|---|
| `server_health` | Round-trips ListRooms; reports host, key, latency, active rooms. Run first. |
| `room_create` | Create a room with empty/departure timeouts, participant cap, metadata |
| `room_list` | List rooms, filter by exact names or prefix |
| `room_delete` | Delete a room and disconnect everyone |
| `room_update_metadata` | Replace room metadata (pushed to all participants) |
| `participant_list` | Participants with state, permissions, published tracks |
| `participant_get` | One participant, including track SIDs |
| `participant_remove` | Kick a participant |
| `participant_mute_track` | Server-side mute/unmute of a published track |
| `participant_update` | Name, metadata, attributes, permissions |
| `token_create` | Mint a scoped join JWT (room, identity, TTL, publish/subscribe/data/admin/record grants) and a Meet join link |
| `token_verify` | Verify a JWT against the configured secret and return its claims |
| `data_send` | Publish a data message into a room from the server (topic, reliable/lossy, per-identity targeting) |
| `egress_start_room_composite` | Record a room to MP4/OGG/MP3 or push to RTMP/SRT |
| `egress_list` | List egress jobs (filter by room, active only) |
| `egress_stop` | Stop an egress job |
| `ingress_create` | RTMP / WHIP / URL ingress into a room; returns URL and stream key for OBS |
| `ingress_list` | List ingress endpoints |
| `ingress_delete` | Delete an ingress endpoint |
| `workshop_spinup` | N team rooms + seat tokens + mentor tokens in one call; returns a manifest |
| `workshop_join_sheet` | Printable HTML (QR per seat) or Markdown join sheet from the manifest |
| `workshop_status` | Live per-room participant and publisher counts, empty rooms |
| `workshop_broadcast` | One data message to every room under the event prefix |
| `workshop_teardown` | Delete every room under the event prefix (`dryRun` to preview) |
Every tool returns both human-readable text and `structuredContent`. SDK errors come back as MCP error results with a hint (for example, egress and ingress need their services running; `--dev` alone has neither).
## Run a hackathon in five prompts
With the server connected to Claude:
1. **"Check the LiveKit server is up."**
`server_health` returns `ok: true`, the host, and latency.
2. **"Spin up the SF Voice Agent Hackathon: teams Otters, Herons, Foxes, Kestrels, four seats each, tokens good for ten hours."**
`workshop_spinup` creates `sf-voice-agent-hackathon-otters` and the rest, mints 16 seat tokens and 4 mentor tokens, and returns the manifest.
3. **"Make me a printable join sheet."**
`workshop_join_sheet` renders one page per team with a QR code per seat. Save it as HTML, print it, cut it into strips.
4. **"How are the teams doing? Tell everyone there are 30 minutes left."**
`workshop_status` shows two rooms empty and one team with nobody publishing (go help them); `workshop_broadcast` drops the timer message into every room.
5. **"Tear it all down."**
`workshop_teardown` deletes every room under the prefix. `room_list` confirms zero.
The same manifest feeds a recap: which teams stayed in their room the whole session, which never joined, and how many published media. Those numbers go into the event write-up and back to the product team.
## Tests
Unit tests run against mocked SDK clients through a real MCP client (in-memory transport):
```
$ npm test
Test Files 3 passed (3)
Tests 21 passed (21)
$ npx tsc --noEmit
(exit 0)
```
The integration smoke test runs against a real `livekit-server --dev`, drives every step through `tools/call`, and joins a real WebRTC participant with `@livekit/rtc-node` so `participant_list` and `data_send` are exercised for real. Output from 2026-09-14 against livekit-server 1.13.7 in Docker:
```
$ npm run smoke
[01] tools/list -> 24 tools: server_health, room_create, room_list, room_delete, room_update_metadata, participant_list, participant_get, participant_remove, participant_mute_track, participant_update, token_create, token_verify, data_send, egress_start_room_composite, egress_list, egress_stop, ingress_create, ingress_list, ingress_delete, workshop_spinup, workshop_join_sheet, workshop_status, workshop_broadcast, workshop_teardown
[02] server_health -> ok=true apiHost=http://localhost:7880 latencyMs=48.5 activeRooms=0
[03] workshop_spinup -> 2 rooms: smoke-mu1wxi2g-otters (sid RM_2CTLxz9gvyRy, 2 seats + mentor); smoke-mu1wxi2g-herons (sid RM_Kjqpeyyvvrwv, 2 seats + mentor)
[04] token_verify -> valid=true sub=otters-seat-1 room=smoke-mu1wxi2g-otters roomJoin=true
[05] room_list prefix=smoke-mu1wxi2g -> 2 rooms
[06] rtc-node Room.connect(ws://localhost:7880) as otters-seat-1 -> isConnected=true, server-side sid PA_5Z8GYhBztT3s
[07] participant_list smoke-mu1wxi2g-otters -> 1: otters-seat-1 [ACTIVE]
[08] participant_update -> name="Otter One"
[09] data_send -> 23 bytes; participant received "hello from the operator"
[10] workshop_broadcast -> 2 rooms
[11] workshop_status -> rooms=2 participants=1 empty=["smoke-mu1wxi2g-herons"]
[12] participant_remove -> removed=otters-seat-1; client saw Disconnected
[13] egress_list -> error (expected on --dev, no egress service): twirp error unknown: egress not connected (redis required)
[14] ingress_list -> error (expected on --dev, no ingress service): twirp error unknown: ingress not connected (redis required)
[15] workshop_join_sheet(markdown) -> 3157 chars, first line "# smoke mu1wxi2g: join sheet"
[16] workshop_teardown -> deleted 2: smoke-mu1wxi2g-otters, smoke-mu1wxi2g-herons
[17] room_list prefix=smoke-mu1wxi2g -> 0 rooms
SMOKE PASS: 17 steps, 0 failures against http://localhost:7880
```
Docker on macOS note: pass `--node-ip 127.0.0.1` to the server so ICE candidates point at the mapped ports; without it the signalling connects but media never does.
## Layout
```
src/index.ts stdio entry
src/server.ts McpServer + tool registration
src/config.ts LIVEKIT_* env, dev defaults
src/clients.ts RoomService / Egress / Ingress / AccessToken behind one interface (mockable)
src/tools/*.ts rooms, participants, tokens, data, egress, ingress, workshop
scripts/smoke.ts integration smoke against a real server
test/unit/*.test.ts vitest, mocked clients through a real MCP client
```
## Prior art
- LiveKit Agents MCP client support (agents consuming MCP tools) and `livekit-examples/basic-mcp`
- LiveKit Docs MCP at `https://docs.livekit.io/mcp` (documentation search)
- Community voice agents wired to MCP servers (home-assistant-mcp-agent, voice-mcp-agent, and others)
None of these expose the server API to an MCP client; that is what this repo does.
## License
MIT. Copyright (c) 2026 Matthew Karsten.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues