@chrischall/eventbrite-mcp 0.0.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/.claude-plugin/marketplace.json +34 -0
- package/.claude-plugin/plugin.json +21 -0
- package/.mcp.json +11 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/bundle.js +40401 -0
- package/dist/client.js +76 -0
- package/dist/discovery.js +530 -0
- package/dist/eventbrite-auth.js +18 -0
- package/dist/index.js +31 -0
- package/dist/tools/account.js +170 -0
- package/dist/tools/discovery.js +126 -0
- package/dist/tools/events.js +131 -0
- package/dist/tools/lookup.js +96 -0
- package/dist/tools/params.js +39 -0
- package/dist/transport-fetchproxy.js +57 -0
- package/dist/transport.js +5 -0
- package/dist/version.js +1 -0
- package/package.json +72 -0
- package/server.json +29 -0
- package/skills/eventbrite/SKILL.md +70 -0
- package/skills/eventbrite/references/discovery-api.md +113 -0
- package/skills/eventbrite/references/token-api.md +102 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { textResult } from '@chrischall/mcp-utils';
|
|
3
|
+
import { enc, qs, schemaContinuation } from './params.js';
|
|
4
|
+
/**
|
|
5
|
+
* Account-side tools on the documented API (`eventbriteapi.com/v3`, bearer
|
|
6
|
+
* token). Transport-neutral: the hosted connector registers these with a
|
|
7
|
+
* per-user client.
|
|
8
|
+
*/
|
|
9
|
+
export function registerAccountTools(server, deps) {
|
|
10
|
+
const { client } = deps;
|
|
11
|
+
server.registerTool('eb_me', {
|
|
12
|
+
description: "Get the authenticated Eventbrite user's profile (id, name, primary email).",
|
|
13
|
+
annotations: { readOnlyHint: true },
|
|
14
|
+
}, async () => {
|
|
15
|
+
const data = await client.request('GET', '/users/me/');
|
|
16
|
+
return textResult(data);
|
|
17
|
+
});
|
|
18
|
+
server.registerTool('eb_my_orders', {
|
|
19
|
+
description: "List the authenticated user's ticket orders (attendee side), with the event expanded. time_filter narrows to upcoming or past events.",
|
|
20
|
+
annotations: { readOnlyHint: true },
|
|
21
|
+
inputSchema: {
|
|
22
|
+
time_filter: z
|
|
23
|
+
.enum(['all', 'current_future', 'past'])
|
|
24
|
+
.optional()
|
|
25
|
+
.describe('Filter orders by event time (default all)'),
|
|
26
|
+
continuation: schemaContinuation,
|
|
27
|
+
},
|
|
28
|
+
}, async ({ time_filter, continuation }) => {
|
|
29
|
+
const params = new URLSearchParams({ expand: 'event' });
|
|
30
|
+
if (time_filter)
|
|
31
|
+
params.set('time_filter', time_filter);
|
|
32
|
+
if (continuation)
|
|
33
|
+
params.set('continuation', continuation);
|
|
34
|
+
const data = await client.request('GET', `/users/me/orders/?${params}`);
|
|
35
|
+
return textResult(data);
|
|
36
|
+
});
|
|
37
|
+
server.registerTool('eb_my_organizations', {
|
|
38
|
+
description: 'List the organizations the authenticated user belongs to (organizer side). Use the returned org id with eb_org_events / eb_org_attendees / eb_org_orders.',
|
|
39
|
+
annotations: { readOnlyHint: true },
|
|
40
|
+
inputSchema: { continuation: schemaContinuation },
|
|
41
|
+
}, async ({ continuation }) => {
|
|
42
|
+
const params = new URLSearchParams();
|
|
43
|
+
if (continuation)
|
|
44
|
+
params.set('continuation', continuation);
|
|
45
|
+
const data = await client.request('GET', `/users/me/organizations/${qs(params)}`);
|
|
46
|
+
return textResult(data);
|
|
47
|
+
});
|
|
48
|
+
server.registerTool('eb_org_events', {
|
|
49
|
+
description: "List an organization's events (as organizer), optionally filtered by status.",
|
|
50
|
+
annotations: { readOnlyHint: true },
|
|
51
|
+
inputSchema: {
|
|
52
|
+
org_id: z.string().describe('Organization id (from eb_my_organizations)'),
|
|
53
|
+
status: z
|
|
54
|
+
.enum(['all', 'live', 'draft', 'started', 'ended', 'completed', 'canceled'])
|
|
55
|
+
.optional()
|
|
56
|
+
.describe('Event status filter (default all)'),
|
|
57
|
+
order_by: z
|
|
58
|
+
.enum(['start_asc', 'start_desc', 'created_asc', 'created_desc'])
|
|
59
|
+
.optional(),
|
|
60
|
+
continuation: schemaContinuation,
|
|
61
|
+
},
|
|
62
|
+
}, async ({ org_id, status, order_by, continuation }) => {
|
|
63
|
+
const params = new URLSearchParams();
|
|
64
|
+
if (status)
|
|
65
|
+
params.set('status', status);
|
|
66
|
+
if (order_by)
|
|
67
|
+
params.set('order_by', order_by);
|
|
68
|
+
if (continuation)
|
|
69
|
+
params.set('continuation', continuation);
|
|
70
|
+
const data = await client.request('GET', `/organizations/${enc(org_id)}/events/${qs(params)}`);
|
|
71
|
+
return textResult(data);
|
|
72
|
+
});
|
|
73
|
+
server.registerTool('eb_org_attendees', {
|
|
74
|
+
description: "List attendees across an organization's events (organizer side).",
|
|
75
|
+
annotations: { readOnlyHint: true },
|
|
76
|
+
inputSchema: {
|
|
77
|
+
org_id: z.string().describe('Organization id (from eb_my_organizations)'),
|
|
78
|
+
status: z
|
|
79
|
+
.enum(['attending', 'not_attending', 'unpaid'])
|
|
80
|
+
.optional()
|
|
81
|
+
.describe('Attendee status filter'),
|
|
82
|
+
continuation: schemaContinuation,
|
|
83
|
+
},
|
|
84
|
+
}, async ({ org_id, status, continuation }) => {
|
|
85
|
+
const params = new URLSearchParams();
|
|
86
|
+
if (status)
|
|
87
|
+
params.set('status', status);
|
|
88
|
+
if (continuation)
|
|
89
|
+
params.set('continuation', continuation);
|
|
90
|
+
const data = await client.request('GET', `/organizations/${enc(org_id)}/attendees/${qs(params)}`);
|
|
91
|
+
return textResult(data);
|
|
92
|
+
});
|
|
93
|
+
server.registerTool('eb_org_orders', {
|
|
94
|
+
description: "List orders across an organization's events (organizer side).",
|
|
95
|
+
annotations: { readOnlyHint: true },
|
|
96
|
+
inputSchema: {
|
|
97
|
+
org_id: z.string().describe('Organization id (from eb_my_organizations)'),
|
|
98
|
+
continuation: schemaContinuation,
|
|
99
|
+
},
|
|
100
|
+
}, async ({ org_id, continuation }) => {
|
|
101
|
+
const params = new URLSearchParams();
|
|
102
|
+
if (continuation)
|
|
103
|
+
params.set('continuation', continuation);
|
|
104
|
+
const data = await client.request('GET', `/organizations/${enc(org_id)}/orders/${qs(params)}`);
|
|
105
|
+
return textResult(data);
|
|
106
|
+
});
|
|
107
|
+
// Simple org-scoped collections: same shape, same pagination, different noun.
|
|
108
|
+
const orgCollections = [
|
|
109
|
+
['eb_org_venues', 'venues', "List an organization's saved venues (name, address, geo)."],
|
|
110
|
+
[
|
|
111
|
+
'eb_org_discounts',
|
|
112
|
+
'discounts',
|
|
113
|
+
"List an organization's discount and access codes, including their usage limits.",
|
|
114
|
+
],
|
|
115
|
+
[
|
|
116
|
+
'eb_org_ticket_groups',
|
|
117
|
+
'ticket_groups',
|
|
118
|
+
"List an organization's ticket groups (ticket classes bundled across events).",
|
|
119
|
+
],
|
|
120
|
+
[
|
|
121
|
+
'eb_org_webhooks',
|
|
122
|
+
'webhooks',
|
|
123
|
+
"List an organization's registered webhooks and the actions they subscribe to.",
|
|
124
|
+
],
|
|
125
|
+
];
|
|
126
|
+
for (const [name, noun, description] of orgCollections) {
|
|
127
|
+
server.registerTool(name, {
|
|
128
|
+
description,
|
|
129
|
+
annotations: { readOnlyHint: true },
|
|
130
|
+
inputSchema: {
|
|
131
|
+
org_id: z.string().describe('Organization id (from eb_my_organizations)'),
|
|
132
|
+
continuation: schemaContinuation,
|
|
133
|
+
},
|
|
134
|
+
}, async ({ org_id, continuation }) => {
|
|
135
|
+
const params = new URLSearchParams();
|
|
136
|
+
if (continuation)
|
|
137
|
+
params.set('continuation', continuation);
|
|
138
|
+
return textResult(await client.request('GET', `/organizations/${enc(org_id)}/${noun}/${qs(params)}`));
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
server.registerTool('eb_org_report', {
|
|
142
|
+
description: "Run an organization's sales or attendees report — the aggregated analytics behind its events, optionally windowed by date.",
|
|
143
|
+
annotations: { readOnlyHint: true },
|
|
144
|
+
inputSchema: {
|
|
145
|
+
org_id: z.string().describe('Organization id (from eb_my_organizations)'),
|
|
146
|
+
kind: z.enum(['sales', 'attendees']).describe('Which report to run'),
|
|
147
|
+
start_date: z.string().optional().describe('ISO date lower bound (YYYY-MM-DD)'),
|
|
148
|
+
end_date: z.string().optional().describe('ISO date upper bound (YYYY-MM-DD)'),
|
|
149
|
+
event_status: z.enum(['live', 'started', 'ended', 'completed', 'canceled']).optional(),
|
|
150
|
+
group_by: z
|
|
151
|
+
.string()
|
|
152
|
+
.optional()
|
|
153
|
+
.describe("Grouping dimension, e.g. 'event', 'day', 'ticket_class'"),
|
|
154
|
+
continuation: schemaContinuation,
|
|
155
|
+
},
|
|
156
|
+
}, async ({ org_id, kind, start_date, end_date, event_status, group_by, continuation }) => {
|
|
157
|
+
const params = new URLSearchParams();
|
|
158
|
+
if (start_date)
|
|
159
|
+
params.set('start_date', start_date);
|
|
160
|
+
if (end_date)
|
|
161
|
+
params.set('end_date', end_date);
|
|
162
|
+
if (event_status)
|
|
163
|
+
params.set('event_status', event_status);
|
|
164
|
+
if (group_by)
|
|
165
|
+
params.set('group_by', group_by);
|
|
166
|
+
if (continuation)
|
|
167
|
+
params.set('continuation', continuation);
|
|
168
|
+
return textResult(await client.request('GET', `/organizations/${enc(org_id)}/reports/${kind}/${qs(params)}`));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { textResult } from '@chrischall/mcp-utils';
|
|
3
|
+
import { toCompactEvent } from '../discovery.js';
|
|
4
|
+
/**
|
|
5
|
+
* Public event discovery. Verified live 2026-07-30: the documented host serves
|
|
6
|
+
* the consumer search at POST /destination/search/ with a plain bearer token —
|
|
7
|
+
* no WAF, no CSRF, no cookies — so these tools no longer require a browser and
|
|
8
|
+
* ARE registered by the hosted connector. The fetchproxy bridge remains a
|
|
9
|
+
* fallback on the stdio path.
|
|
10
|
+
*/
|
|
11
|
+
export async function registerDiscoveryTools(server, deps) {
|
|
12
|
+
const { discovery, transport } = deps;
|
|
13
|
+
server.registerTool('eb_resolve_place', {
|
|
14
|
+
description: "Resolve a location to Eventbrite's internal place id for eb_search_events. Accepts a plain location ('Charlotte, NC', 'Berlin, Germany') or a browse slug ('nc--charlotte'). A city on its own is rejected — include the state or country. Returns {placeId, name, slug, region, country} plus `shelves` — curated browse shelves (Popular, This Weekend, Online) harvested free from the same fetch.",
|
|
15
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
16
|
+
inputSchema: {
|
|
17
|
+
location: z
|
|
18
|
+
.string()
|
|
19
|
+
.min(2)
|
|
20
|
+
.describe("Location, e.g. 'Charlotte, NC', 'Berlin, Germany', or the slug 'nc--charlotte'"),
|
|
21
|
+
},
|
|
22
|
+
}, async ({ location }) => {
|
|
23
|
+
const place = await discovery.resolveLocation(location);
|
|
24
|
+
return textResult(place);
|
|
25
|
+
});
|
|
26
|
+
server.registerTool('eb_search_events', {
|
|
27
|
+
description: 'Search public Eventbrite events (the consumer search absent from the documented API). Resolve the location to a place id first with eb_resolve_place. Filters: keyword, dates, category/subcategory/format ids (see eb_reference), free/paid, online-only. Set compact=true for slim results suited to browsing/ranking.',
|
|
28
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
29
|
+
inputSchema: {
|
|
30
|
+
q: z.string().optional().describe('Keyword query'),
|
|
31
|
+
place_id: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Eventbrite place id from eb_resolve_place (e.g. 85981333 = Charlotte NC)'),
|
|
35
|
+
date_keyword: z
|
|
36
|
+
.enum(['today', 'tomorrow', 'this_weekend', 'this_week', 'next_week', 'this_month'])
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('Relative date filter'),
|
|
39
|
+
date_range_from: z.string().optional().describe('ISO date lower bound (YYYY-MM-DD)'),
|
|
40
|
+
date_range_to: z.string().optional().describe('ISO date upper bound (YYYY-MM-DD)'),
|
|
41
|
+
category_id: z.string().optional().describe('Category id (eb_reference categories)'),
|
|
42
|
+
subcategory_id: z.string().optional().describe('Subcategory id'),
|
|
43
|
+
format_id: z.string().optional().describe('Format id'),
|
|
44
|
+
price: z.enum(['free', 'paid']).optional(),
|
|
45
|
+
online_events_only: z.boolean().optional(),
|
|
46
|
+
page: z.number().int().positive().optional().describe('Page number (default 1)'),
|
|
47
|
+
page_size: z.number().int().positive().max(50).optional().describe('Results per page (default 20)'),
|
|
48
|
+
aggs: z
|
|
49
|
+
.array(z.enum(['places_borough', 'places_neighborhood']))
|
|
50
|
+
.optional()
|
|
51
|
+
.describe('Facet buckets to aggregate alongside results'),
|
|
52
|
+
compact: z
|
|
53
|
+
.boolean()
|
|
54
|
+
.optional()
|
|
55
|
+
.describe('Return slim event summaries instead of full records (default false)'),
|
|
56
|
+
},
|
|
57
|
+
}, async (args) => {
|
|
58
|
+
const data = await discovery.search({
|
|
59
|
+
q: args.q,
|
|
60
|
+
placeId: args.place_id,
|
|
61
|
+
dateKeyword: args.date_keyword,
|
|
62
|
+
dateRangeFrom: args.date_range_from,
|
|
63
|
+
dateRangeTo: args.date_range_to,
|
|
64
|
+
categoryId: args.category_id,
|
|
65
|
+
subcategoryId: args.subcategory_id,
|
|
66
|
+
formatId: args.format_id,
|
|
67
|
+
price: args.price,
|
|
68
|
+
onlineEventsOnly: args.online_events_only,
|
|
69
|
+
page: args.page,
|
|
70
|
+
pageSize: args.page_size,
|
|
71
|
+
aggs: args.aggs,
|
|
72
|
+
});
|
|
73
|
+
if (args.compact) {
|
|
74
|
+
const results = data.events?.results;
|
|
75
|
+
// Drift fallback: if the envelope isn't the shape we know, return the
|
|
76
|
+
// raw response rather than an empty/wrong projection.
|
|
77
|
+
if (!Array.isArray(results)) {
|
|
78
|
+
console.error('[eventbrite-mcp] destination search response missing events.results — returning raw response');
|
|
79
|
+
return textResult(data);
|
|
80
|
+
}
|
|
81
|
+
return textResult({
|
|
82
|
+
pagination: data.events?.pagination,
|
|
83
|
+
results: results.map(toCompactEvent),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return textResult(data);
|
|
87
|
+
});
|
|
88
|
+
server.registerTool('eb_event_details', {
|
|
89
|
+
description: "Batch-fetch public event details by id. Uses your bearer token by default, falling back to the browser bridge when no token is configured. For ticket-class detail prefer eb_ticket_classes.",
|
|
90
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
91
|
+
inputSchema: {
|
|
92
|
+
event_ids: z.array(z.string()).min(1).max(20).describe('Numeric event ids'),
|
|
93
|
+
expand: z
|
|
94
|
+
.string()
|
|
95
|
+
.optional()
|
|
96
|
+
.describe('Comma-separated expansions (default primary_venue,image,ticket_availability,event_sales_status,primary_organizer)'),
|
|
97
|
+
},
|
|
98
|
+
}, async ({ event_ids, expand }) => {
|
|
99
|
+
const data = await discovery.eventsByIds(event_ids, expand ? expand.split(',').map((s) => s.trim()) : undefined);
|
|
100
|
+
return textResult(data);
|
|
101
|
+
});
|
|
102
|
+
// eb_healthcheck diagnoses the BRIDGE. With no bridge (the Worker connector)
|
|
103
|
+
// there is nothing for it to report on, so it is not registered at all —
|
|
104
|
+
// better than a tool that always answers "no transport".
|
|
105
|
+
if (!transport)
|
|
106
|
+
return;
|
|
107
|
+
// Imported lazily, AFTER the guard: a static import would drag the fetchproxy
|
|
108
|
+
// helper into the Worker bundle where it can never run.
|
|
109
|
+
const { registerBridgeHealthcheckTool } = await import('@chrischall/mcp-utils/fetchproxy');
|
|
110
|
+
// The categories endpoint answers 200 JSON on the www host regardless of
|
|
111
|
+
// login state, so it isolates bridge problems from Eventbrite-side problems.
|
|
112
|
+
registerBridgeHealthcheckTool({
|
|
113
|
+
server,
|
|
114
|
+
prefix: 'eb',
|
|
115
|
+
hostLabel: 'www.eventbrite.com',
|
|
116
|
+
probePath: '/api/v3/categories/',
|
|
117
|
+
transport,
|
|
118
|
+
probeFn: async (path) => {
|
|
119
|
+
const result = await transport.fetch({ path, method: 'GET' });
|
|
120
|
+
if (result.status !== 200) {
|
|
121
|
+
throw new Error(`probe returned HTTP ${result.status}`);
|
|
122
|
+
}
|
|
123
|
+
return typeof result.body === 'string' ? result.body : '';
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { textResult } from '@chrischall/mcp-utils';
|
|
3
|
+
import { enc, qs, schemaContinuation } from './params.js';
|
|
4
|
+
/** Reference lists served under /system/ rather than the API root. */
|
|
5
|
+
const SYSTEM_LISTS = new Set(['timezones', 'countries', 'regions']);
|
|
6
|
+
/**
|
|
7
|
+
* Event lookup + reference-data tools on the documented API. `eb_event` works
|
|
8
|
+
* for ANY public event by id (not just your own) — event ids are the trailing
|
|
9
|
+
* digits of an event URL (`…-tickets-<id>`).
|
|
10
|
+
*/
|
|
11
|
+
export function registerEventTools(server, deps) {
|
|
12
|
+
const { client } = deps;
|
|
13
|
+
server.registerTool('eb_event', {
|
|
14
|
+
description: 'Get an Eventbrite event by id (works for any public event, not just yours). Event ids are the trailing digits in an event URL (…-tickets-<id>).',
|
|
15
|
+
annotations: { readOnlyHint: true },
|
|
16
|
+
inputSchema: {
|
|
17
|
+
event_id: z.string().describe('Numeric event id'),
|
|
18
|
+
expand: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe('Comma-separated expansions (default venue,organizer,ticket_availability)'),
|
|
22
|
+
},
|
|
23
|
+
}, async ({ event_id, expand }) => {
|
|
24
|
+
const exp = expand ?? 'venue,organizer,ticket_availability';
|
|
25
|
+
const data = await client.request('GET', `/events/${encodeURIComponent(event_id)}/?expand=${encodeURIComponent(exp)}`);
|
|
26
|
+
return textResult(data);
|
|
27
|
+
});
|
|
28
|
+
server.registerTool('eb_ticket_classes', {
|
|
29
|
+
description: "List an event's ticket classes (name, cost, free/paid, on-sale status).",
|
|
30
|
+
annotations: { readOnlyHint: true },
|
|
31
|
+
inputSchema: { event_id: z.string().describe('Numeric event id') },
|
|
32
|
+
}, async ({ event_id }) => {
|
|
33
|
+
const data = await client.request('GET', `/events/${encodeURIComponent(event_id)}/ticket_classes/`);
|
|
34
|
+
return textResult(data);
|
|
35
|
+
});
|
|
36
|
+
server.registerTool('eb_event_description', {
|
|
37
|
+
description: "Get an event's full HTML description.",
|
|
38
|
+
annotations: { readOnlyHint: true },
|
|
39
|
+
inputSchema: { event_id: z.string().describe('Numeric event id') },
|
|
40
|
+
}, async ({ event_id }) => {
|
|
41
|
+
const data = await client.request('GET', `/events/${encodeURIComponent(event_id)}/description/`);
|
|
42
|
+
return textResult(data);
|
|
43
|
+
});
|
|
44
|
+
server.registerTool('eb_reference', {
|
|
45
|
+
description: 'List Eventbrite reference data: categories (103=Music, 101=Business, 110=Food & Drink, …), subcategories, formats, timezones, countries or regions. Category/subcategory/format ids feed eb_search_events filters.',
|
|
46
|
+
annotations: { readOnlyHint: true },
|
|
47
|
+
inputSchema: {
|
|
48
|
+
kind: z
|
|
49
|
+
.enum(['categories', 'subcategories', 'formats', 'timezones', 'countries', 'regions'])
|
|
50
|
+
.describe('Which list to fetch'),
|
|
51
|
+
},
|
|
52
|
+
}, async ({ kind }) => {
|
|
53
|
+
// timezones/countries/regions hang off /system/; the bare /countries/ and
|
|
54
|
+
// /regions/ paths 404 (verified live 2026-07-30). The taxonomy lists
|
|
55
|
+
// (categories, subcategories, formats) sit at the root.
|
|
56
|
+
const path = SYSTEM_LISTS.has(kind) ? `/system/${kind}/` : `/${kind}/`;
|
|
57
|
+
const data = await client.request('GET', path);
|
|
58
|
+
return textResult(data);
|
|
59
|
+
});
|
|
60
|
+
server.registerTool('eb_event_attendees', {
|
|
61
|
+
description: "List a single event's attendees (organizer-side; requires access to that event). Use changed_since to poll incrementally instead of re-reading the whole list.",
|
|
62
|
+
annotations: { readOnlyHint: true },
|
|
63
|
+
inputSchema: {
|
|
64
|
+
event_id: z.string().describe('Numeric event id'),
|
|
65
|
+
status: z.enum(['attending', 'not_attending', 'unpaid']).optional(),
|
|
66
|
+
changed_since: z
|
|
67
|
+
.string()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('ISO 8601 UTC timestamp — only attendees changed since then'),
|
|
70
|
+
continuation: schemaContinuation,
|
|
71
|
+
},
|
|
72
|
+
}, async ({ event_id, status, changed_since, continuation }) => {
|
|
73
|
+
const params = new URLSearchParams();
|
|
74
|
+
if (status)
|
|
75
|
+
params.set('status', status);
|
|
76
|
+
if (changed_since)
|
|
77
|
+
params.set('changed_since', changed_since);
|
|
78
|
+
if (continuation)
|
|
79
|
+
params.set('continuation', continuation);
|
|
80
|
+
return textResult(await client.request('GET', `/events/${enc(event_id)}/attendees/${qs(params)}`));
|
|
81
|
+
});
|
|
82
|
+
server.registerTool('eb_event_attendee', {
|
|
83
|
+
description: "Get one attendee of an event by id (barcode, profile answers, check-in state).",
|
|
84
|
+
annotations: { readOnlyHint: true },
|
|
85
|
+
inputSchema: {
|
|
86
|
+
event_id: z.string().describe('Numeric event id'),
|
|
87
|
+
attendee_id: z.string().describe('Numeric attendee id'),
|
|
88
|
+
},
|
|
89
|
+
}, async ({ event_id, attendee_id }) => textResult(await client.request('GET', `/events/${enc(event_id)}/attendees/${enc(attendee_id)}/`)));
|
|
90
|
+
server.registerTool('eb_event_orders', {
|
|
91
|
+
description: "List a single event's orders (organizer-side; requires access to that event).",
|
|
92
|
+
annotations: { readOnlyHint: true },
|
|
93
|
+
inputSchema: {
|
|
94
|
+
event_id: z.string().describe('Numeric event id'),
|
|
95
|
+
status: z.enum(['all', 'placed', 'refunded']).optional(),
|
|
96
|
+
changed_since: z
|
|
97
|
+
.string()
|
|
98
|
+
.optional()
|
|
99
|
+
.describe('ISO 8601 UTC timestamp — only orders changed since then'),
|
|
100
|
+
continuation: schemaContinuation,
|
|
101
|
+
},
|
|
102
|
+
}, async ({ event_id, status, changed_since, continuation }) => {
|
|
103
|
+
const params = new URLSearchParams();
|
|
104
|
+
if (status)
|
|
105
|
+
params.set('status', status);
|
|
106
|
+
if (changed_since)
|
|
107
|
+
params.set('changed_since', changed_since);
|
|
108
|
+
if (continuation)
|
|
109
|
+
params.set('continuation', continuation);
|
|
110
|
+
return textResult(await client.request('GET', `/events/${enc(event_id)}/orders/${qs(params)}`));
|
|
111
|
+
});
|
|
112
|
+
server.registerTool('eb_ticket_class', {
|
|
113
|
+
description: 'Get one ticket class of an event by id. Use eb_ticket_classes to list them first.',
|
|
114
|
+
annotations: { readOnlyHint: true },
|
|
115
|
+
inputSchema: {
|
|
116
|
+
event_id: z.string().describe('Numeric event id'),
|
|
117
|
+
ticket_class_id: z.string().describe('Numeric ticket class id'),
|
|
118
|
+
},
|
|
119
|
+
}, async ({ event_id, ticket_class_id }) => textResult(await client.request('GET', `/events/${enc(event_id)}/ticket_classes/${enc(ticket_class_id)}/`)));
|
|
120
|
+
server.registerTool('eb_event_questions', {
|
|
121
|
+
description: "List the registration questions an event asks its buyers. Set canned=true for Eventbrite's standard question bank instead of the event's custom ones.",
|
|
122
|
+
annotations: { readOnlyHint: true },
|
|
123
|
+
inputSchema: {
|
|
124
|
+
event_id: z.string().describe('Numeric event id'),
|
|
125
|
+
canned: z
|
|
126
|
+
.boolean()
|
|
127
|
+
.optional()
|
|
128
|
+
.describe("Fetch the standard question bank instead of the event's custom questions"),
|
|
129
|
+
},
|
|
130
|
+
}, async ({ event_id, canned }) => textResult(await client.request('GET', `/events/${enc(event_id)}/${canned ? 'canned_questions' : 'questions'}/`)));
|
|
131
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { textResult } from '@chrischall/mcp-utils';
|
|
3
|
+
import { enc, qs, schemaContinuation, schemaEventStatus } from './params.js';
|
|
4
|
+
/**
|
|
5
|
+
* Single-object lookups on the documented API: resolve an id that appears
|
|
6
|
+
* inside another response (an order on a ticket, the venue/organizer/series a
|
|
7
|
+
* search result points at) into the full record.
|
|
8
|
+
*
|
|
9
|
+
* All read-only. Every id is percent-encoded — ids arrive from model output and
|
|
10
|
+
* must not be able to traverse out of their path segment.
|
|
11
|
+
*/
|
|
12
|
+
export function registerLookupTools(server, deps) {
|
|
13
|
+
const { client } = deps;
|
|
14
|
+
server.registerTool('eb_order', {
|
|
15
|
+
description: 'Get a single order by id (the buyer-side record behind a ticket). Order ids appear in eb_my_orders / eb_event_orders results.',
|
|
16
|
+
annotations: { readOnlyHint: true },
|
|
17
|
+
inputSchema: {
|
|
18
|
+
order_id: z.string().describe('Numeric order id'),
|
|
19
|
+
expand: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Comma-separated expansions, e.g. 'attendees,event'"),
|
|
23
|
+
},
|
|
24
|
+
}, async ({ order_id, expand }) => {
|
|
25
|
+
const params = new URLSearchParams();
|
|
26
|
+
if (expand)
|
|
27
|
+
params.set('expand', expand);
|
|
28
|
+
return textResult(await client.request('GET', `/orders/${enc(order_id)}/${qs(params)}`));
|
|
29
|
+
});
|
|
30
|
+
server.registerTool('eb_venue', {
|
|
31
|
+
description: 'Get a venue by id (name, address, geo). Venue ids appear on expanded events and in eb_org_venues.',
|
|
32
|
+
annotations: { readOnlyHint: true },
|
|
33
|
+
inputSchema: { venue_id: z.string().describe('Numeric venue id') },
|
|
34
|
+
}, async ({ venue_id }) => textResult(await client.request('GET', `/venues/${enc(venue_id)}/`)));
|
|
35
|
+
server.registerTool('eb_venue_events', {
|
|
36
|
+
description: 'List the events held at a venue.',
|
|
37
|
+
annotations: { readOnlyHint: true },
|
|
38
|
+
inputSchema: {
|
|
39
|
+
venue_id: z.string().describe('Numeric venue id'),
|
|
40
|
+
status: schemaEventStatus,
|
|
41
|
+
continuation: schemaContinuation,
|
|
42
|
+
},
|
|
43
|
+
}, async ({ venue_id, status, continuation }) => {
|
|
44
|
+
const params = new URLSearchParams();
|
|
45
|
+
if (status)
|
|
46
|
+
params.set('status', status);
|
|
47
|
+
if (continuation)
|
|
48
|
+
params.set('continuation', continuation);
|
|
49
|
+
return textResult(await client.request('GET', `/venues/${enc(venue_id)}/events/${qs(params)}`));
|
|
50
|
+
});
|
|
51
|
+
server.registerTool('eb_organizer', {
|
|
52
|
+
description: "Get an organizer's public profile by id (name, description, logo, social links).",
|
|
53
|
+
annotations: { readOnlyHint: true },
|
|
54
|
+
inputSchema: { organizer_id: z.string().describe('Numeric organizer id') },
|
|
55
|
+
}, async ({ organizer_id }) => textResult(await client.request('GET', `/organizers/${enc(organizer_id)}/`)));
|
|
56
|
+
server.registerTool('eb_organizer_events', {
|
|
57
|
+
description: "List an organizer's events — the public way to see everything one organizer is running.",
|
|
58
|
+
annotations: { readOnlyHint: true },
|
|
59
|
+
inputSchema: {
|
|
60
|
+
organizer_id: z.string().describe('Numeric organizer id'),
|
|
61
|
+
status: schemaEventStatus,
|
|
62
|
+
order_by: z.enum(['start_asc', 'start_desc', 'created_asc', 'created_desc']).optional(),
|
|
63
|
+
continuation: schemaContinuation,
|
|
64
|
+
},
|
|
65
|
+
}, async ({ organizer_id, status, order_by, continuation }) => {
|
|
66
|
+
const params = new URLSearchParams();
|
|
67
|
+
if (status)
|
|
68
|
+
params.set('status', status);
|
|
69
|
+
if (order_by)
|
|
70
|
+
params.set('order_by', order_by);
|
|
71
|
+
if (continuation)
|
|
72
|
+
params.set('continuation', continuation);
|
|
73
|
+
return textResult(await client.request('GET', `/organizers/${enc(organizer_id)}/events/${qs(params)}`));
|
|
74
|
+
});
|
|
75
|
+
server.registerTool('eb_series_events', {
|
|
76
|
+
description: 'List the occurrences of a recurring event series. Search results and events carry a series_id when they belong to one.',
|
|
77
|
+
annotations: { readOnlyHint: true },
|
|
78
|
+
inputSchema: {
|
|
79
|
+
series_id: z.string().describe('Numeric series id (from an event/search result)'),
|
|
80
|
+
status: schemaEventStatus,
|
|
81
|
+
continuation: schemaContinuation,
|
|
82
|
+
},
|
|
83
|
+
}, async ({ series_id, status, continuation }) => {
|
|
84
|
+
const params = new URLSearchParams();
|
|
85
|
+
if (status)
|
|
86
|
+
params.set('status', status);
|
|
87
|
+
if (continuation)
|
|
88
|
+
params.set('continuation', continuation);
|
|
89
|
+
return textResult(await client.request('GET', `/series/${enc(series_id)}/events/${qs(params)}`));
|
|
90
|
+
});
|
|
91
|
+
server.registerTool('eb_user', {
|
|
92
|
+
description: "Get a public user profile by id. Use eb_me for the authenticated user (that call also returns private fields like emails).",
|
|
93
|
+
annotations: { readOnlyHint: true },
|
|
94
|
+
inputSchema: { user_id: z.string().describe('Numeric user id') },
|
|
95
|
+
}, async ({ user_id }) => textResult(await client.request('GET', `/users/${enc(user_id)}/`)));
|
|
96
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { McpToolError } from '@chrischall/mcp-utils';
|
|
3
|
+
/**
|
|
4
|
+
* Shared request-building helpers for the documented-API tool modules.
|
|
5
|
+
* Keeping these in one place means every tool paginates, filters and encodes
|
|
6
|
+
* ids the same way.
|
|
7
|
+
*/
|
|
8
|
+
export const schemaContinuation = z
|
|
9
|
+
.string()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe('Pagination continuation token from a previous response');
|
|
12
|
+
export const schemaEventStatus = z
|
|
13
|
+
.enum(['all', 'live', 'draft', 'started', 'ended', 'completed', 'canceled'])
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('Event status filter (default all)');
|
|
16
|
+
/**
|
|
17
|
+
* Render a query string, or '' when nothing is set. Eventbrite 301s on a bare
|
|
18
|
+
* trailing '?', which would silently drop the Authorization header on the
|
|
19
|
+
* redirect — so never emit one.
|
|
20
|
+
*/
|
|
21
|
+
export function qs(params) {
|
|
22
|
+
return params.size > 0 ? `?${params}` : '';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Percent-encode a path segment, rejecting ids that would traverse.
|
|
26
|
+
*
|
|
27
|
+
* `encodeURIComponent` alone is NOT sufficient: `.` is an unreserved character,
|
|
28
|
+
* so `encodeURIComponent('..') === '..'` and `/orders/../` climbs a segment
|
|
29
|
+
* before the request is ever sent. Slashes do encode, so only dot-only ids are
|
|
30
|
+
* dangerous — reject those outright rather than trying to sanitise them.
|
|
31
|
+
*/
|
|
32
|
+
export function enc(id) {
|
|
33
|
+
if (/^\.+$/.test(id)) {
|
|
34
|
+
throw new McpToolError(`Invalid id '${id}'.`, {
|
|
35
|
+
hint: 'Ids must be Eventbrite object ids (normally digits), not path segments.',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return encodeURIComponent(id);
|
|
39
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// EventbriteTransport backed by the shared fetchproxy factory: every request
|
|
2
|
+
// runs as a same-origin fetch inside the user's signed-in eventbrite.com tab
|
|
3
|
+
// (Transporter extension), which is what clears the WAF that blocks any
|
|
4
|
+
// server-side client. The `csrftoken` cookie scope is declared up front so the
|
|
5
|
+
// ONE pairing approval covers both fetch and the cookie read — fpx 1.4.0
|
|
6
|
+
// cannot widen scope after the first pair (see the fleet skill's gotcha).
|
|
7
|
+
import { createBootstrapOpts, createFetchproxyTransport, } from '@chrischall/mcp-utils/fetchproxy';
|
|
8
|
+
// Re-export typed errors so callers importing from this module keep working.
|
|
9
|
+
export { FetchproxyBridgeDownError, FetchproxyTimeoutError, } from '@chrischall/mcp-utils/fetchproxy';
|
|
10
|
+
const DEFAULT_PORT = 37_149; // fleet-wide concentrator port — do NOT change
|
|
11
|
+
export class FetchproxyTransport {
|
|
12
|
+
inner;
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
const port = opts.port ?? DEFAULT_PORT;
|
|
15
|
+
this.inner = createFetchproxyTransport({
|
|
16
|
+
port,
|
|
17
|
+
serverName: opts.server ?? 'eventbrite-mcp',
|
|
18
|
+
version: opts.version,
|
|
19
|
+
// Declares domains + the csrftoken cookie scope, and derives the
|
|
20
|
+
// `read_cookies` capability so the pair prompt covers it from day one.
|
|
21
|
+
...createBootstrapOpts({
|
|
22
|
+
domains: ['eventbrite.com'],
|
|
23
|
+
bootstrap: { cookieKeys: ['csrftoken'] },
|
|
24
|
+
}),
|
|
25
|
+
// Every eventbrite.com request targets www unless a caller overrides.
|
|
26
|
+
defaultSubdomain: 'www',
|
|
27
|
+
logListening: true,
|
|
28
|
+
debugEnvVar: 'EVENTBRITE_DEBUG',
|
|
29
|
+
...(opts.fetchTimeoutMs !== undefined ? { fetchTimeoutMs: opts.fetchTimeoutMs } : {}),
|
|
30
|
+
...(opts.bridgeReviveDelayMs !== undefined
|
|
31
|
+
? { bridgeReviveDelayMs: opts.bridgeReviveDelayMs }
|
|
32
|
+
: {}),
|
|
33
|
+
...(opts.createServer ? { createServer: opts.createServer } : {}),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
start() {
|
|
37
|
+
return this.inner.start();
|
|
38
|
+
}
|
|
39
|
+
close() {
|
|
40
|
+
return this.inner.close();
|
|
41
|
+
}
|
|
42
|
+
status() {
|
|
43
|
+
return this.inner.status();
|
|
44
|
+
}
|
|
45
|
+
fetch(init) {
|
|
46
|
+
return this.inner.fetch(init);
|
|
47
|
+
}
|
|
48
|
+
requestJson(method, path, init) {
|
|
49
|
+
return this.inner.requestJson(method, path, init);
|
|
50
|
+
}
|
|
51
|
+
async readCookies(keys) {
|
|
52
|
+
return this.inner.server.readCookies({ domain: 'eventbrite.com', subdomain: 'www', keys });
|
|
53
|
+
}
|
|
54
|
+
runProbe(fetchFn, probePath) {
|
|
55
|
+
return this.inner.runProbe(fetchFn, probePath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Transport contract for the WAF-walled consumer surface
|
|
2
|
+
// (`www.eventbrite.com/api/v3/destination/…` and the SSR `/d/…` pages).
|
|
3
|
+
// The production implementation is `transport-fetchproxy.ts` (requests ride
|
|
4
|
+
// the user's signed-in browser tab); tests inject a mock.
|
|
5
|
+
export {};
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VERSION = '0.0.0'; // x-release-please-version
|