luma-events-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dominik Grusemann
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,257 @@
1
+ # luma-events-mcp
2
+
3
+ <div align="center">
4
+
5
+ A Luma Calendar MCP server exposing event, guest, and ticket operations as tools for AI assistants.
6
+
7
+ [![Release](https://github.com/bettervibe-org/luma-events-mcp/actions/workflows/release.yml/badge.svg)](https://github.com/bettervibe-org/luma-events-mcp/actions/workflows/release.yml)
8
+ [![npm version](https://badge.fury.io/js/luma-events-mcp.svg)](https://www.npmjs.com/package/luma-events-mcp)
9
+ [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://choosealicense.com/licenses/mit/)
10
+ [![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-purple.svg)](https://modelcontextprotocol.io)
11
+ [![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release)
12
+
13
+ </div>
14
+
15
+ ## Features
16
+
17
+ - Get calendar info associated with your API key
18
+ - List, create, and update events
19
+ - List guests, get guest details, add guests, update guest status, send invites
20
+ - List and create ticket types
21
+ - List and create coupons
22
+ - Dual pagination mode: auto-paginate all results or manual cursor-based paging
23
+
24
+ ## Setup
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "luma": {
30
+ "command": "npx",
31
+ "args": ["luma-events-mcp"],
32
+ "env": {
33
+ "LUMA_API_KEY": "<your Luma API key>"
34
+ }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ Get your API key from your Luma dashboard (requires Luma Plus).
41
+
42
+ ## Development
43
+
44
+ ### Quick Start
45
+
46
+ Run the MCP server in development mode with auto-reload:
47
+ ```bash
48
+ npm run dev
49
+ ```
50
+
51
+ This will run the TypeScript code directly with watch mode and automatically load environment variables from `.env`.
52
+
53
+ ### Manual Build
54
+
55
+ ```bash
56
+ npm run build
57
+ node dist/index.js
58
+ ```
59
+
60
+ ## Available Tools
61
+
62
+ <!-- TOOLS:START - generated by scripts/gen-tool-docs.ts -->
63
+ ### get-calendar
64
+
65
+ Get the calendar associated with the current API key
66
+
67
+ Parameters: none
68
+
69
+ Returns:
70
+ - Calendar object with api_id, name, and geo fields
71
+
72
+ ### list-events
73
+
74
+ List events for the calendar. Optionally filter by date range. Without pagination params, returns all events; with them, returns one page.
75
+
76
+ Parameters:
77
+ - `after`: string (optional) — Only events starting after this ISO 8601 datetime
78
+ - `before`: string (optional) — Only events starting before this ISO 8601 datetime
79
+ - `pagination_limit`: number (optional) — Page size (max 50)
80
+ - `pagination_cursor`: string (optional) — Cursor from a previous page
81
+
82
+ Returns:
83
+ - Array of event objects, or paginated result with next_cursor
84
+
85
+ ### get-event
86
+
87
+ Get admin details of a specific event by its ID
88
+
89
+ Parameters:
90
+ - `event_id`: string — Event ID (e.g. evt-...)
91
+
92
+ Returns:
93
+ - Full event object including name, dates, location, description, and ticket info
94
+
95
+ ### list-guests
96
+
97
+ List guests for an event. Without pagination params, returns all guests; with them, returns one page.
98
+
99
+ Parameters:
100
+ - `event_id`: string — Event ID (e.g. evt-...)
101
+ - `approval_status`: enum (`approved` | `session` | `pending_approval` | `invited` | `declined` | `waitlist`) (optional) — Filter by approval status
102
+ - `sort_column`: enum (`name` | `email` | `created_at` | `registered_at` | `checked_in_at`) (optional) — Sort column
103
+ - `pagination_limit`: number (optional) — Page size
104
+ - `pagination_cursor`: string (optional) — Cursor from a previous page
105
+
106
+ Returns:
107
+ - Array of guest objects, or paginated result with next_cursor
108
+
109
+ ### get-guest
110
+
111
+ Get detailed info for a single event guest. The id field accepts a guest ID (gst-...), guest key (g-...), ticket key, or email address.
112
+
113
+ Parameters:
114
+ - `event_id`: string — Event ID (e.g. evt-...)
115
+ - `id`: string — Guest identifier: guest ID (gst-...), guest key (g-...), or email
116
+
117
+ Returns:
118
+ - Full guest object with approval status, ticket info, event_ticket_orders, and profile
119
+
120
+ ### list-ticket-types
121
+
122
+ List all ticket types for an event
123
+
124
+ Parameters:
125
+ - `event_id`: string — Event ID (e.g. evt-...)
126
+
127
+ Returns:
128
+ - Array of ticket type objects with name, price, and availability info
129
+
130
+ ### create-event
131
+
132
+ Create a new event on the calendar
133
+
134
+ Parameters:
135
+ - `name`: string — Event title
136
+ - `start_at`: string — Start datetime (ISO 8601)
137
+ - `timezone`: string — IANA timezone (e.g. America/New_York)
138
+ - `end_at`: string (optional) — End datetime (ISO 8601)
139
+ - `description_md`: string (optional) — Event description in markdown
140
+ - `meeting_url`: string (optional) — Virtual event URL
141
+ - `geo_address_json`: value (optional) — Address object with city, place, etc.
142
+ - `geo_latitude`: number (optional) — Latitude
143
+ - `geo_longitude`: number (optional) — Longitude
144
+ - `cover_url`: string (optional) — Cover image URL (must be Luma CDN)
145
+ - `visibility`: enum (`public` | `members-only` | `private`) (optional) — Event visibility
146
+ - `max_capacity`: number (optional) — Maximum attendee capacity
147
+
148
+ Returns:
149
+ - The created event object
150
+
151
+ ### update-event
152
+
153
+ Update an existing event. Only provided fields are changed.
154
+
155
+ Parameters:
156
+ - `event_id`: string — Event ID (e.g. evt-...)
157
+ - `suppress_notifications`: boolean (optional) — Prevent guest notifications for name/time/location changes
158
+ - `name`: string (optional) — Event title
159
+ - `start_at`: string (optional) — Start datetime (ISO 8601)
160
+ - `end_at`: string (optional) — End datetime (ISO 8601)
161
+ - `timezone`: string (optional) — IANA timezone
162
+ - `description_md`: string (optional) — Event description in markdown
163
+ - `meeting_url`: string (optional) — Virtual event URL
164
+ - `cover_url`: string (optional) — Cover image URL
165
+ - `visibility`: enum (`public` | `members-only` | `private`) (optional) — Event visibility
166
+ - `max_capacity`: number (optional) — Maximum attendee capacity
167
+
168
+ Returns:
169
+ - The updated event object
170
+
171
+ ### add-guests
172
+
173
+ Add guests to an event. By default guests are approved and receive one default ticket.
174
+
175
+ Parameters:
176
+ - `event_id`: string — Event ID (e.g. evt-...)
177
+ - `guests`: array of object — Array of guest objects with email
178
+ - `approval_status`: enum (`approved` | `pending_approval` | `waitlist`) (optional) — Approval status for added guests (default: approved)
179
+ - `send_email`: boolean (optional) — Send notification email (default: true)
180
+ - `ticket`: value (optional) — Ticket type to assign to all guests
181
+
182
+ Returns:
183
+ - Confirmation of added guests
184
+
185
+ ### update-guest-status
186
+
187
+ Update a guest's approval status (approve or decline)
188
+
189
+ Parameters:
190
+ - `event_id`: string — Event ID (e.g. evt-...)
191
+ - `guest`: value — Guest identifier object (e.g. { email: '...' } or { id: 'gst-...' })
192
+ - `status`: enum (`approved` | `declined`) — New status for the guest
193
+ - `should_refund`: boolean (optional) — Refund the guest if declining a paid ticket
194
+
195
+ Returns:
196
+ - Confirmation of status update
197
+
198
+ ### send-invites
199
+
200
+ Send invites to guests for an event. Sends email and SMS if phone is linked.
201
+
202
+ Parameters:
203
+ - `event_id`: string — Event ID (e.g. evt-...)
204
+ - `guests`: array of object — Array of guest objects with email and optional message
205
+
206
+ Returns:
207
+ - Confirmation of sent invites
208
+
209
+ ### create-ticket-type
210
+
211
+ Create a new ticket type for an event
212
+
213
+ Parameters:
214
+ - `event_id`: string — Event ID (e.g. evt-...)
215
+ - `name`: string — Ticket type name
216
+ - `type`: enum (`free` | `paid`) — Ticket pricing type
217
+ - `require_approval`: boolean (optional) — Require host approval
218
+ - `is_hidden`: boolean (optional) — Hide from public listing
219
+ - `description`: string (optional) — Ticket type description
220
+ - `max_capacity`: number (optional) — Maximum tickets available
221
+ - `cents`: number (optional) — Price in cents (for paid tickets)
222
+ - `currency`: string (optional) — Currency code (e.g. USD, EUR)
223
+
224
+ Returns:
225
+ - The created ticket type object
226
+
227
+ ### list-coupons
228
+
229
+ List all coupons for an event. Without pagination params, returns all coupons; with them, returns one page.
230
+
231
+ Parameters:
232
+ - `event_id`: string — Event ID (e.g. evt-...)
233
+ - `pagination_limit`: number (optional) — Page size
234
+ - `pagination_cursor`: string (optional) — Cursor from a previous page
235
+
236
+ Returns:
237
+ - Array of coupon objects, or paginated result with next_cursor
238
+
239
+ ### create-coupon
240
+
241
+ Create a coupon for an event. Coupon terms cannot be edited after creation.
242
+
243
+ Parameters:
244
+ - `event_id`: string — Event ID (e.g. evt-...)
245
+ - `code`: string — Coupon code (1-20 chars, case-insensitive)
246
+ - `discount`: value — Discount object (e.g. { type: 'percent', percent_off: 20 })
247
+ - `remaining_count`: number (optional) — Number of uses (0-1000000; use 1000000 for unlimited)
248
+ - `valid_start_at`: string (optional) — Validity start (ISO 8601)
249
+ - `valid_end_at`: string (optional) — Validity end (ISO 8601)
250
+
251
+ Returns:
252
+ - The created coupon object
253
+ <!-- TOOLS:END -->
254
+
255
+ ## License
256
+
257
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { LumaClient } from "./luma-client.js";
5
+ import { registerAddGuests } from "./tools/add-guests.js";
6
+ import { registerCreateCoupon } from "./tools/create-coupon.js";
7
+ import { registerCreateEvent } from "./tools/create-event.js";
8
+ import { registerCreateTicketType } from "./tools/create-ticket-type.js";
9
+ import { registerGetCalendar } from "./tools/get-calendar.js";
10
+ import { registerGetEvent } from "./tools/get-event.js";
11
+ import { registerGetGuest } from "./tools/get-guest.js";
12
+ import { registerListCoupons } from "./tools/list-coupons.js";
13
+ import { registerListEvents } from "./tools/list-events.js";
14
+ import { registerListGuests } from "./tools/list-guests.js";
15
+ import { registerListTicketTypes } from "./tools/list-ticket-types.js";
16
+ import { registerSendInvites } from "./tools/send-invites.js";
17
+ import { registerUpdateEvent } from "./tools/update-event.js";
18
+ import { registerUpdateGuestStatus } from "./tools/update-guest-status.js";
19
+ const server = new McpServer({
20
+ name: "luma-events-mcp",
21
+ version: "0.1.0",
22
+ });
23
+ async function main() {
24
+ const apiKey = process.env.LUMA_API_KEY;
25
+ if (!apiKey) {
26
+ console.error("Missing LUMA_API_KEY environment variable");
27
+ process.exit(1);
28
+ }
29
+ const client = new LumaClient(apiKey);
30
+ try {
31
+ await client.testConnection();
32
+ }
33
+ catch (error) {
34
+ console.error("Failed to connect to Luma API:", error);
35
+ process.exit(1);
36
+ }
37
+ registerGetCalendar(client, server);
38
+ registerListEvents(client, server);
39
+ registerGetEvent(client, server);
40
+ registerListGuests(client, server);
41
+ registerGetGuest(client, server);
42
+ registerListTicketTypes(client, server);
43
+ registerCreateEvent(client, server);
44
+ registerUpdateEvent(client, server);
45
+ registerAddGuests(client, server);
46
+ registerUpdateGuestStatus(client, server);
47
+ registerSendInvites(client, server);
48
+ registerCreateTicketType(client, server);
49
+ registerListCoupons(client, server);
50
+ registerCreateCoupon(client, server);
51
+ const transport = new StdioServerTransport();
52
+ await server.connect(transport);
53
+ }
54
+ main();
@@ -0,0 +1,68 @@
1
+ const DEFAULT_BASE_URL = "https://public-api.luma.com";
2
+ export class LumaApiError extends Error {
3
+ status;
4
+ body;
5
+ constructor(status, body) {
6
+ super(`Luma API ${status}: ${body}`);
7
+ this.status = status;
8
+ this.body = body;
9
+ this.name = "LumaApiError";
10
+ }
11
+ }
12
+ export class LumaClient {
13
+ apiKey;
14
+ baseUrl;
15
+ constructor(apiKey, baseUrl) {
16
+ this.apiKey = apiKey;
17
+ this.baseUrl = baseUrl ?? process.env.LUMA_BASE_URL ?? DEFAULT_BASE_URL;
18
+ }
19
+ async request(method, path, body) {
20
+ const url = `${this.baseUrl}${path}`;
21
+ const headers = {
22
+ "x-luma-api-key": this.apiKey,
23
+ "content-type": "application/json",
24
+ };
25
+ const res = await fetch(url, {
26
+ method,
27
+ headers,
28
+ ...(body !== undefined && { body: JSON.stringify(body) }),
29
+ });
30
+ const text = await res.text();
31
+ if (!res.ok) {
32
+ throw new LumaApiError(res.status, text);
33
+ }
34
+ return text ? JSON.parse(text) : undefined;
35
+ }
36
+ async paginate(path, params) {
37
+ const all = [];
38
+ let cursor;
39
+ do {
40
+ const searchParams = new URLSearchParams(params);
41
+ if (cursor)
42
+ searchParams.set("pagination_cursor", cursor);
43
+ const query = searchParams.toString();
44
+ const fullPath = query ? `${path}?${query}` : path;
45
+ const page = await this.request("GET", fullPath);
46
+ all.push(...page.entries);
47
+ cursor = page.has_more ? page.next_cursor : undefined;
48
+ } while (cursor);
49
+ return all;
50
+ }
51
+ async paginateSingle(path, params, paginationLimit, paginationCursor) {
52
+ const searchParams = new URLSearchParams(params);
53
+ if (paginationLimit)
54
+ searchParams.set("pagination_limit", String(paginationLimit));
55
+ if (paginationCursor)
56
+ searchParams.set("pagination_cursor", paginationCursor);
57
+ const query = searchParams.toString();
58
+ const fullPath = query ? `${path}?${query}` : path;
59
+ const page = await this.request("GET", fullPath);
60
+ return {
61
+ entries: page.entries,
62
+ next_cursor: page.has_more ? page.next_cursor : undefined,
63
+ };
64
+ }
65
+ async testConnection() {
66
+ await this.request("GET", "/v1/calendar/get");
67
+ }
68
+ }
@@ -0,0 +1,48 @@
1
+ import { z } from "zod";
2
+ const guestSchema = z.object({
3
+ email: z.string().describe("Guest email address"),
4
+ name: z.string().optional().describe("Guest display name"),
5
+ phone_number: z.string().optional().describe("Phone number"),
6
+ });
7
+ export const addGuestsDefinition = {
8
+ name: "add-guests",
9
+ description: "Add guests to an event. By default guests are approved and receive one default ticket.",
10
+ inputSchema: {
11
+ event_id: z.string().describe("Event ID (e.g. evt-...)"),
12
+ guests: z.array(guestSchema).describe("Array of guest objects with email"),
13
+ approval_status: z
14
+ .enum(["approved", "pending_approval", "waitlist"])
15
+ .optional()
16
+ .describe("Approval status for added guests (default: approved)"),
17
+ send_email: z
18
+ .boolean()
19
+ .optional()
20
+ .describe("Send notification email (default: true)"),
21
+ ticket: z
22
+ .record(z.string(), z.unknown())
23
+ .optional()
24
+ .describe("Ticket type to assign to all guests"),
25
+ },
26
+ returns: "Confirmation of added guests",
27
+ };
28
+ export function registerAddGuests(client, server) {
29
+ server.registerTool(addGuestsDefinition.name, {
30
+ description: addGuestsDefinition.description,
31
+ inputSchema: addGuestsDefinition.inputSchema,
32
+ }, async (args) => {
33
+ const body = {
34
+ event_id: args.event_id,
35
+ guests: args.guests,
36
+ };
37
+ if (args.approval_status !== undefined)
38
+ body.approval_status = args.approval_status;
39
+ if (args.send_email !== undefined)
40
+ body.send_email = args.send_email;
41
+ if (args.ticket !== undefined)
42
+ body.ticket = args.ticket;
43
+ const data = await client.request("POST", "/v1/event/add-guests", body);
44
+ return {
45
+ content: [{ type: "text", text: JSON.stringify(data) }],
46
+ };
47
+ });
48
+ }
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ export const createCouponDefinition = {
3
+ name: "create-coupon",
4
+ description: "Create a coupon for an event. Coupon terms cannot be edited after creation.",
5
+ inputSchema: {
6
+ event_id: z.string().describe("Event ID (e.g. evt-...)"),
7
+ code: z.string().describe("Coupon code (1-20 chars, case-insensitive)"),
8
+ discount: z
9
+ .record(z.string(), z.unknown())
10
+ .describe("Discount object (e.g. { type: 'percent', percent_off: 20 })"),
11
+ remaining_count: z
12
+ .number()
13
+ .optional()
14
+ .describe("Number of uses (0-1000000; use 1000000 for unlimited)"),
15
+ valid_start_at: z.string().optional().describe("Validity start (ISO 8601)"),
16
+ valid_end_at: z.string().optional().describe("Validity end (ISO 8601)"),
17
+ },
18
+ returns: "The created coupon object",
19
+ };
20
+ export function registerCreateCoupon(client, server) {
21
+ server.registerTool(createCouponDefinition.name, {
22
+ description: createCouponDefinition.description,
23
+ inputSchema: createCouponDefinition.inputSchema,
24
+ }, async (args) => {
25
+ const body = {
26
+ event_id: args.event_id,
27
+ code: args.code,
28
+ discount: args.discount,
29
+ };
30
+ if (args.remaining_count !== undefined)
31
+ body.remaining_count = args.remaining_count;
32
+ if (args.valid_start_at !== undefined)
33
+ body.valid_start_at = args.valid_start_at;
34
+ if (args.valid_end_at !== undefined)
35
+ body.valid_end_at = args.valid_end_at;
36
+ const data = await client.request("POST", "/v1/event/create-coupon", body);
37
+ return {
38
+ content: [{ type: "text", text: JSON.stringify(data) }],
39
+ };
40
+ });
41
+ }
@@ -0,0 +1,74 @@
1
+ import { z } from "zod";
2
+ export const createEventDefinition = {
3
+ name: "create-event",
4
+ description: "Create a new event on the calendar",
5
+ inputSchema: {
6
+ name: z.string().describe("Event title"),
7
+ start_at: z
8
+ .string()
9
+ .datetime({ offset: true })
10
+ .describe("Start datetime (ISO 8601)"),
11
+ timezone: z.string().describe("IANA timezone (e.g. America/New_York)"),
12
+ end_at: z
13
+ .string()
14
+ .datetime({ offset: true })
15
+ .optional()
16
+ .describe("End datetime (ISO 8601)"),
17
+ description_md: z
18
+ .string()
19
+ .optional()
20
+ .describe("Event description in markdown"),
21
+ meeting_url: z.string().optional().describe("Virtual event URL"),
22
+ geo_address_json: z
23
+ .record(z.string(), z.unknown())
24
+ .optional()
25
+ .describe("Address object with city, place, etc."),
26
+ geo_latitude: z.number().optional().describe("Latitude"),
27
+ geo_longitude: z.number().optional().describe("Longitude"),
28
+ cover_url: z
29
+ .string()
30
+ .optional()
31
+ .describe("Cover image URL (must be Luma CDN)"),
32
+ visibility: z
33
+ .enum(["public", "members-only", "private"])
34
+ .optional()
35
+ .describe("Event visibility"),
36
+ max_capacity: z.number().optional().describe("Maximum attendee capacity"),
37
+ },
38
+ returns: "The created event object",
39
+ };
40
+ export function registerCreateEvent(client, server) {
41
+ server.registerTool(createEventDefinition.name, {
42
+ description: createEventDefinition.description,
43
+ inputSchema: createEventDefinition.inputSchema,
44
+ }, async (args) => {
45
+ const body = {
46
+ name: args.name,
47
+ start_at: args.start_at,
48
+ timezone: args.timezone,
49
+ };
50
+ if (args.end_at !== undefined)
51
+ body.end_at = args.end_at;
52
+ if (args.description_md !== undefined)
53
+ body.description_md = args.description_md;
54
+ if (args.meeting_url !== undefined)
55
+ body.meeting_url = args.meeting_url;
56
+ if (args.geo_address_json !== undefined)
57
+ body.geo_address_json = args.geo_address_json;
58
+ if (args.geo_latitude !== undefined)
59
+ body.coordinate = {
60
+ latitude: args.geo_latitude,
61
+ longitude: args.geo_longitude,
62
+ };
63
+ if (args.cover_url !== undefined)
64
+ body.cover_url = args.cover_url;
65
+ if (args.visibility !== undefined)
66
+ body.visibility = args.visibility;
67
+ if (args.max_capacity !== undefined)
68
+ body.max_capacity = args.max_capacity;
69
+ const data = await client.request("POST", "/v1/event/create", body);
70
+ return {
71
+ content: [{ type: "text", text: JSON.stringify(data) }],
72
+ };
73
+ });
74
+ }
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+ export const createTicketTypeDefinition = {
3
+ name: "create-ticket-type",
4
+ description: "Create a new ticket type for an event",
5
+ inputSchema: {
6
+ event_id: z.string().describe("Event ID (e.g. evt-...)"),
7
+ name: z.string().describe("Ticket type name"),
8
+ type: z.enum(["free", "paid"]).describe("Ticket pricing type"),
9
+ require_approval: z.boolean().optional().describe("Require host approval"),
10
+ is_hidden: z.boolean().optional().describe("Hide from public listing"),
11
+ description: z.string().optional().describe("Ticket type description"),
12
+ max_capacity: z.number().optional().describe("Maximum tickets available"),
13
+ cents: z.number().optional().describe("Price in cents (for paid tickets)"),
14
+ currency: z.string().optional().describe("Currency code (e.g. USD, EUR)"),
15
+ },
16
+ returns: "The created ticket type object",
17
+ };
18
+ export function registerCreateTicketType(client, server) {
19
+ server.registerTool(createTicketTypeDefinition.name, {
20
+ description: createTicketTypeDefinition.description,
21
+ inputSchema: createTicketTypeDefinition.inputSchema,
22
+ }, async (args) => {
23
+ const body = {
24
+ event_id: args.event_id,
25
+ name: args.name,
26
+ type: args.type,
27
+ };
28
+ if (args.require_approval !== undefined)
29
+ body.require_approval = args.require_approval;
30
+ if (args.is_hidden !== undefined)
31
+ body.is_hidden = args.is_hidden;
32
+ if (args.description !== undefined)
33
+ body.description = args.description;
34
+ if (args.max_capacity !== undefined)
35
+ body.max_capacity = args.max_capacity;
36
+ if (args.cents !== undefined)
37
+ body.cents = args.cents;
38
+ if (args.currency !== undefined)
39
+ body.currency = args.currency;
40
+ const data = await client.request("POST", "/v1/event/ticket-types/create", body);
41
+ return {
42
+ content: [{ type: "text", text: JSON.stringify(data) }],
43
+ };
44
+ });
45
+ }
@@ -0,0 +1,17 @@
1
+ export const getCalendarDefinition = {
2
+ name: "get-calendar",
3
+ description: "Get the calendar associated with the current API key",
4
+ inputSchema: {},
5
+ returns: "Calendar object with api_id, name, and geo fields",
6
+ };
7
+ export function registerGetCalendar(client, server) {
8
+ server.registerTool(getCalendarDefinition.name, {
9
+ description: getCalendarDefinition.description,
10
+ inputSchema: getCalendarDefinition.inputSchema,
11
+ }, async () => {
12
+ const data = await client.request("GET", "/v1/calendar/get");
13
+ return {
14
+ content: [{ type: "text", text: JSON.stringify(data) }],
15
+ };
16
+ });
17
+ }
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ export const getEventDefinition = {
3
+ name: "get-event",
4
+ description: "Get admin details of a specific event by its ID",
5
+ inputSchema: {
6
+ event_id: z.string().describe("Event ID (e.g. evt-...)"),
7
+ },
8
+ returns: "Full event object including name, dates, location, description, and ticket info",
9
+ };
10
+ export function registerGetEvent(client, server) {
11
+ server.registerTool(getEventDefinition.name, {
12
+ description: getEventDefinition.description,
13
+ inputSchema: getEventDefinition.inputSchema,
14
+ }, async (args) => {
15
+ const data = await client.request("GET", `/v1/event/get?id=${encodeURIComponent(args.event_id)}`);
16
+ return {
17
+ content: [{ type: "text", text: JSON.stringify(data) }],
18
+ };
19
+ });
20
+ }
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ export const getGuestDefinition = {
3
+ name: "get-guest",
4
+ description: "Get detailed info for a single event guest. The id field accepts a guest ID (gst-...), guest key (g-...), ticket key, or email address.",
5
+ inputSchema: {
6
+ event_id: z.string().describe("Event ID (e.g. evt-...)"),
7
+ id: z
8
+ .string()
9
+ .describe("Guest identifier: guest ID (gst-...), guest key (g-...), or email"),
10
+ },
11
+ returns: "Full guest object with approval status, ticket info, event_ticket_orders, and profile",
12
+ };
13
+ export function registerGetGuest(client, server) {
14
+ server.registerTool(getGuestDefinition.name, {
15
+ description: getGuestDefinition.description,
16
+ inputSchema: getGuestDefinition.inputSchema,
17
+ }, async (args) => {
18
+ const params = new URLSearchParams({
19
+ event_id: args.event_id,
20
+ id: args.id,
21
+ });
22
+ const data = await client.request("GET", `/v1/event/get-guest?${params.toString()}`);
23
+ return {
24
+ content: [{ type: "text", text: JSON.stringify(data) }],
25
+ };
26
+ });
27
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ export const listCouponsDefinition = {
3
+ name: "list-coupons",
4
+ description: "List all coupons for an event. Without pagination params, returns all coupons; with them, returns one page.",
5
+ inputSchema: {
6
+ event_id: z.string().describe("Event ID (e.g. evt-...)"),
7
+ pagination_limit: z.number().optional().describe("Page size"),
8
+ pagination_cursor: z
9
+ .string()
10
+ .optional()
11
+ .describe("Cursor from a previous page"),
12
+ },
13
+ returns: "Array of coupon objects, or paginated result with next_cursor",
14
+ };
15
+ export function registerListCoupons(client, server) {
16
+ server.registerTool(listCouponsDefinition.name, {
17
+ description: listCouponsDefinition.description,
18
+ inputSchema: listCouponsDefinition.inputSchema,
19
+ }, async (args) => {
20
+ const params = { event_id: args.event_id };
21
+ const usePagination = args.pagination_limit !== undefined ||
22
+ args.pagination_cursor !== undefined;
23
+ if (usePagination) {
24
+ const result = await client.paginateSingle("/v1/event/coupons", params, args.pagination_limit, args.pagination_cursor);
25
+ return {
26
+ content: [{ type: "text", text: JSON.stringify(result) }],
27
+ };
28
+ }
29
+ const entries = await client.paginate("/v1/event/coupons", params);
30
+ return {
31
+ content: [{ type: "text", text: JSON.stringify(entries) }],
32
+ };
33
+ });
34
+ }
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+ export const listEventsDefinition = {
3
+ name: "list-events",
4
+ description: "List events for the calendar. Optionally filter by date range. Without pagination params, returns all events; with them, returns one page.",
5
+ inputSchema: {
6
+ after: z
7
+ .string()
8
+ .optional()
9
+ .describe("Only events starting after this ISO 8601 datetime"),
10
+ before: z
11
+ .string()
12
+ .optional()
13
+ .describe("Only events starting before this ISO 8601 datetime"),
14
+ pagination_limit: z.number().optional().describe("Page size (max 50)"),
15
+ pagination_cursor: z
16
+ .string()
17
+ .optional()
18
+ .describe("Cursor from a previous page"),
19
+ },
20
+ returns: "Array of event objects, or paginated result with next_cursor",
21
+ };
22
+ export function registerListEvents(client, server) {
23
+ server.registerTool(listEventsDefinition.name, {
24
+ description: listEventsDefinition.description,
25
+ inputSchema: listEventsDefinition.inputSchema,
26
+ }, async (args) => {
27
+ const params = {};
28
+ if (args.after)
29
+ params.after = args.after;
30
+ if (args.before)
31
+ params.before = args.before;
32
+ const usePagination = args.pagination_limit !== undefined ||
33
+ args.pagination_cursor !== undefined;
34
+ if (usePagination) {
35
+ const result = await client.paginateSingle("/v1/calendar/list-events", params, args.pagination_limit, args.pagination_cursor);
36
+ return {
37
+ content: [{ type: "text", text: JSON.stringify(result) }],
38
+ };
39
+ }
40
+ const entries = await client.paginate("/v1/calendar/list-events", params);
41
+ return {
42
+ content: [{ type: "text", text: JSON.stringify(entries) }],
43
+ };
44
+ });
45
+ }
@@ -0,0 +1,55 @@
1
+ import { z } from "zod";
2
+ export const listGuestsDefinition = {
3
+ name: "list-guests",
4
+ description: "List guests for an event. Without pagination params, returns all guests; with them, returns one page.",
5
+ inputSchema: {
6
+ event_id: z.string().describe("Event ID (e.g. evt-...)"),
7
+ approval_status: z
8
+ .enum([
9
+ "approved",
10
+ "session",
11
+ "pending_approval",
12
+ "invited",
13
+ "declined",
14
+ "waitlist",
15
+ ])
16
+ .optional()
17
+ .describe("Filter by approval status"),
18
+ sort_column: z
19
+ .enum(["name", "email", "created_at", "registered_at", "checked_in_at"])
20
+ .optional()
21
+ .describe("Sort column"),
22
+ pagination_limit: z.number().optional().describe("Page size"),
23
+ pagination_cursor: z
24
+ .string()
25
+ .optional()
26
+ .describe("Cursor from a previous page"),
27
+ },
28
+ returns: "Array of guest objects, or paginated result with next_cursor",
29
+ };
30
+ export function registerListGuests(client, server) {
31
+ server.registerTool(listGuestsDefinition.name, {
32
+ description: listGuestsDefinition.description,
33
+ inputSchema: listGuestsDefinition.inputSchema,
34
+ }, async (args) => {
35
+ const params = {
36
+ event_id: args.event_id,
37
+ };
38
+ if (args.approval_status)
39
+ params.approval_status = args.approval_status;
40
+ if (args.sort_column)
41
+ params.sort_column = args.sort_column;
42
+ const usePagination = args.pagination_limit !== undefined ||
43
+ args.pagination_cursor !== undefined;
44
+ if (usePagination) {
45
+ const result = await client.paginateSingle("/v1/event/get-guests", params, args.pagination_limit, args.pagination_cursor);
46
+ return {
47
+ content: [{ type: "text", text: JSON.stringify(result) }],
48
+ };
49
+ }
50
+ const entries = await client.paginate("/v1/event/get-guests", params);
51
+ return {
52
+ content: [{ type: "text", text: JSON.stringify(entries) }],
53
+ };
54
+ });
55
+ }
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ export const listTicketTypesDefinition = {
3
+ name: "list-ticket-types",
4
+ description: "List all ticket types for an event",
5
+ inputSchema: {
6
+ event_id: z.string().describe("Event ID (e.g. evt-...)"),
7
+ },
8
+ returns: "Array of ticket type objects with name, price, and availability info",
9
+ };
10
+ export function registerListTicketTypes(client, server) {
11
+ server.registerTool(listTicketTypesDefinition.name, {
12
+ description: listTicketTypesDefinition.description,
13
+ inputSchema: listTicketTypesDefinition.inputSchema,
14
+ }, async (args) => {
15
+ const data = await client.request("GET", `/v1/event/ticket-types/list?event_id=${encodeURIComponent(args.event_id)}`);
16
+ return {
17
+ content: [{ type: "text", text: JSON.stringify(data) }],
18
+ };
19
+ });
20
+ }
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ const inviteGuestSchema = z.object({
3
+ email: z.string().describe("Guest email address"),
4
+ message: z
5
+ .string()
6
+ .optional()
7
+ .describe("Personalized invite message (max 200 chars)"),
8
+ });
9
+ export const sendInvitesDefinition = {
10
+ name: "send-invites",
11
+ description: "Send invites to guests for an event. Sends email and SMS if phone is linked.",
12
+ inputSchema: {
13
+ event_id: z.string().describe("Event ID (e.g. evt-...)"),
14
+ guests: z
15
+ .array(inviteGuestSchema)
16
+ .describe("Array of guest objects with email and optional message"),
17
+ },
18
+ returns: "Confirmation of sent invites",
19
+ };
20
+ export function registerSendInvites(client, server) {
21
+ server.registerTool(sendInvitesDefinition.name, {
22
+ description: sendInvitesDefinition.description,
23
+ inputSchema: sendInvitesDefinition.inputSchema,
24
+ }, async (args) => {
25
+ const data = await client.request("POST", "/v1/event/send-invites", {
26
+ event_id: args.event_id,
27
+ guests: args.guests,
28
+ });
29
+ return {
30
+ content: [{ type: "text", text: JSON.stringify(data) }],
31
+ };
32
+ });
33
+ }
@@ -0,0 +1,68 @@
1
+ import { z } from "zod";
2
+ export const updateEventDefinition = {
3
+ name: "update-event",
4
+ description: "Update an existing event. Only provided fields are changed.",
5
+ inputSchema: {
6
+ event_id: z.string().describe("Event ID (e.g. evt-...)"),
7
+ suppress_notifications: z
8
+ .boolean()
9
+ .optional()
10
+ .describe("Prevent guest notifications for name/time/location changes"),
11
+ name: z.string().optional().describe("Event title"),
12
+ start_at: z
13
+ .string()
14
+ .datetime({ offset: true })
15
+ .optional()
16
+ .describe("Start datetime (ISO 8601)"),
17
+ end_at: z
18
+ .string()
19
+ .datetime({ offset: true })
20
+ .optional()
21
+ .describe("End datetime (ISO 8601)"),
22
+ timezone: z.string().optional().describe("IANA timezone"),
23
+ description_md: z
24
+ .string()
25
+ .optional()
26
+ .describe("Event description in markdown"),
27
+ meeting_url: z.string().optional().describe("Virtual event URL"),
28
+ cover_url: z.string().optional().describe("Cover image URL"),
29
+ visibility: z
30
+ .enum(["public", "members-only", "private"])
31
+ .optional()
32
+ .describe("Event visibility"),
33
+ max_capacity: z.number().optional().describe("Maximum attendee capacity"),
34
+ },
35
+ returns: "The updated event object",
36
+ };
37
+ export function registerUpdateEvent(client, server) {
38
+ server.registerTool(updateEventDefinition.name, {
39
+ description: updateEventDefinition.description,
40
+ inputSchema: updateEventDefinition.inputSchema,
41
+ }, async (args) => {
42
+ const body = { event_id: args.event_id };
43
+ if (args.suppress_notifications !== undefined)
44
+ body.suppress_notifications = args.suppress_notifications;
45
+ if (args.name !== undefined)
46
+ body.name = args.name;
47
+ if (args.start_at !== undefined)
48
+ body.start_at = args.start_at;
49
+ if (args.end_at !== undefined)
50
+ body.end_at = args.end_at;
51
+ if (args.timezone !== undefined)
52
+ body.timezone = args.timezone;
53
+ if (args.description_md !== undefined)
54
+ body.description_md = args.description_md;
55
+ if (args.meeting_url !== undefined)
56
+ body.meeting_url = args.meeting_url;
57
+ if (args.cover_url !== undefined)
58
+ body.cover_url = args.cover_url;
59
+ if (args.visibility !== undefined)
60
+ body.visibility = args.visibility;
61
+ if (args.max_capacity !== undefined)
62
+ body.max_capacity = args.max_capacity;
63
+ const data = await client.request("POST", "/v1/event/update", body);
64
+ return {
65
+ content: [{ type: "text", text: JSON.stringify(data) }],
66
+ };
67
+ });
68
+ }
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ export const updateGuestStatusDefinition = {
3
+ name: "update-guest-status",
4
+ description: "Update a guest's approval status (approve or decline)",
5
+ inputSchema: {
6
+ event_id: z.string().describe("Event ID (e.g. evt-...)"),
7
+ guest: z
8
+ .record(z.string(), z.unknown())
9
+ .describe("Guest identifier object (e.g. { email: '...' } or { id: 'gst-...' })"),
10
+ status: z
11
+ .enum(["approved", "declined"])
12
+ .describe("New status for the guest"),
13
+ should_refund: z
14
+ .boolean()
15
+ .optional()
16
+ .describe("Refund the guest if declining a paid ticket"),
17
+ },
18
+ returns: "Confirmation of status update",
19
+ };
20
+ export function registerUpdateGuestStatus(client, server) {
21
+ server.registerTool(updateGuestStatusDefinition.name, {
22
+ description: updateGuestStatusDefinition.description,
23
+ inputSchema: updateGuestStatusDefinition.inputSchema,
24
+ }, async (args) => {
25
+ const body = {
26
+ event_id: args.event_id,
27
+ guest: args.guest,
28
+ status: args.status,
29
+ };
30
+ if (args.should_refund !== undefined)
31
+ body.should_refund = args.should_refund;
32
+ const data = await client.request("POST", "/v1/event/update-guest-status", body);
33
+ return {
34
+ content: [{ type: "text", text: JSON.stringify(data) }],
35
+ };
36
+ });
37
+ }
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "luma-events-mcp",
3
+ "mcpName": "io.github.bettervibe-org/luma-events-mcp",
4
+ "description": "A Luma Calendar MCP server exposing event, guest, and ticket operations as tools for AI assistants.",
5
+ "version": "0.1.0",
6
+ "main": "dist/index.js",
7
+ "type": "module",
8
+ "bin": {
9
+ "luma-events-mcp": "dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc && shx chmod +x dist/*.js",
16
+ "dev": "tsx --watch --env-file=.env src/index.ts",
17
+ "prepare": "lefthook install",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest",
20
+ "test:coverage": "vitest run --coverage",
21
+ "check": "biome check .",
22
+ "check:fix": "biome check --write .",
23
+ "check:ci": "biome ci .",
24
+ "docs": "tsx scripts/gen-tool-docs.ts",
25
+ "docs:check": "tsx scripts/gen-tool-docs.ts --check",
26
+ "validate": "npm run check && npm test && npm run knip && npm run docs:check && npm run build",
27
+ "smoke": "npm run build && tsx --env-file=.env scripts/smoke.ts",
28
+ "watch": "tsc --watch",
29
+ "semantic-release": "semantic-release",
30
+ "knip": "knip"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/bettervibe-org/luma-events-mcp.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/bettervibe-org/luma-events-mcp/issues"
38
+ },
39
+ "homepage": "https://github.com/bettervibe-org/luma-events-mcp#readme",
40
+ "author": "Dominik Grusemann",
41
+ "license": "MIT",
42
+ "engines": {
43
+ "node": ">=18.0.0",
44
+ "npm": ">=9.0.0"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.29.0",
48
+ "zod": "^4.4.3"
49
+ },
50
+ "devDependencies": {
51
+ "@biomejs/biome": "^2.4.15",
52
+ "@commitlint/cli": "^21.0.1",
53
+ "@commitlint/config-conventional": "^21.0.1",
54
+ "@semantic-release/changelog": "^6.0.3",
55
+ "@semantic-release/git": "^10.0.1",
56
+ "@semantic-release/github": "^12.0.8",
57
+ "@semantic-release/npm": "^13.1.5",
58
+ "@types/node": "^25.9.1",
59
+ "@vitest/coverage-v8": "^4.1.7",
60
+ "knip": "^6.14.2",
61
+ "lefthook": "^2.1.8",
62
+ "semantic-release": "^25.0.3",
63
+ "shx": "^0.4.0",
64
+ "tsx": "^4.22.3",
65
+ "typescript": "^6.0.3",
66
+ "vitest": "^4.1.7"
67
+ }
68
+ }