@chrischall/eventbrite-mcp 0.2.0 → 0.3.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.
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { textResult } from '@chrischall/mcp-utils';
2
+ import { viewArg, viewResponse } from '../view.js';
3
3
  import { enc, qs, schemaContinuation } from './params.js';
4
4
  /**
5
5
  * Account-side tools on the documented API (`eventbriteapi.com/v3`, bearer
@@ -10,45 +10,52 @@ export function registerAccountTools(server, deps) {
10
10
  const { client } = deps;
11
11
  server.registerTool('eb_me', {
12
12
  description: "Get the authenticated Eventbrite user's profile (id, name, primary email).",
13
+ inputSchema: {
14
+ view: viewArg(),
15
+ },
13
16
  annotations: { readOnlyHint: true },
14
- }, async () => {
17
+ }, async ({ view }) => {
15
18
  const data = await client.request('GET', '/users/me/');
16
- return textResult(data);
19
+ return viewResponse(view, data);
17
20
  });
18
21
  server.registerTool('eb_my_orders', {
19
22
  description: "List the authenticated user's ticket orders (attendee side), with the event expanded. time_filter narrows to upcoming or past events.",
20
23
  annotations: { readOnlyHint: true },
21
24
  inputSchema: {
25
+ view: viewArg(),
22
26
  time_filter: z
23
27
  .enum(['all', 'current_future', 'past'])
24
28
  .optional()
25
29
  .describe('Filter orders by event time (default all)'),
26
30
  continuation: schemaContinuation,
27
31
  },
28
- }, async ({ time_filter, continuation }) => {
32
+ }, async ({ time_filter, continuation, view }) => {
29
33
  const params = new URLSearchParams({ expand: 'event' });
30
34
  if (time_filter)
31
35
  params.set('time_filter', time_filter);
32
36
  if (continuation)
33
37
  params.set('continuation', continuation);
34
38
  const data = await client.request('GET', `/users/me/orders/?${params}`);
35
- return textResult(data);
39
+ return viewResponse(view, data);
36
40
  });
37
41
  server.registerTool('eb_my_organizations', {
38
42
  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
43
  annotations: { readOnlyHint: true },
40
- inputSchema: { continuation: schemaContinuation },
41
- }, async ({ continuation }) => {
44
+ inputSchema: {
45
+ view: viewArg(), continuation: schemaContinuation
46
+ },
47
+ }, async ({ continuation, view }) => {
42
48
  const params = new URLSearchParams();
43
49
  if (continuation)
44
50
  params.set('continuation', continuation);
45
51
  const data = await client.request('GET', `/users/me/organizations/${qs(params)}`);
46
- return textResult(data);
52
+ return viewResponse(view, data);
47
53
  });
48
54
  server.registerTool('eb_org_events', {
49
55
  description: "List an organization's events (as organizer), optionally filtered by status.",
50
56
  annotations: { readOnlyHint: true },
51
57
  inputSchema: {
58
+ view: viewArg(),
52
59
  org_id: z.string().describe('Organization id (from eb_my_organizations)'),
53
60
  status: z
54
61
  .enum(['all', 'live', 'draft', 'started', 'ended', 'completed', 'canceled'])
@@ -59,7 +66,7 @@ export function registerAccountTools(server, deps) {
59
66
  .optional(),
60
67
  continuation: schemaContinuation,
61
68
  },
62
- }, async ({ org_id, status, order_by, continuation }) => {
69
+ }, async ({ org_id, status, order_by, continuation, view }) => {
63
70
  const params = new URLSearchParams();
64
71
  if (status)
65
72
  params.set('status', status);
@@ -68,12 +75,13 @@ export function registerAccountTools(server, deps) {
68
75
  if (continuation)
69
76
  params.set('continuation', continuation);
70
77
  const data = await client.request('GET', `/organizations/${enc(org_id)}/events/${qs(params)}`);
71
- return textResult(data);
78
+ return viewResponse(view, data);
72
79
  });
73
80
  server.registerTool('eb_org_attendees', {
74
81
  description: "List attendees across an organization's events (organizer side).",
75
82
  annotations: { readOnlyHint: true },
76
83
  inputSchema: {
84
+ view: viewArg(),
77
85
  org_id: z.string().describe('Organization id (from eb_my_organizations)'),
78
86
  status: z
79
87
  .enum(['attending', 'not_attending', 'unpaid'])
@@ -81,28 +89,29 @@ export function registerAccountTools(server, deps) {
81
89
  .describe('Attendee status filter'),
82
90
  continuation: schemaContinuation,
83
91
  },
84
- }, async ({ org_id, status, continuation }) => {
92
+ }, async ({ org_id, status, continuation, view }) => {
85
93
  const params = new URLSearchParams();
86
94
  if (status)
87
95
  params.set('status', status);
88
96
  if (continuation)
89
97
  params.set('continuation', continuation);
90
98
  const data = await client.request('GET', `/organizations/${enc(org_id)}/attendees/${qs(params)}`);
91
- return textResult(data);
99
+ return viewResponse(view, data);
92
100
  });
93
101
  server.registerTool('eb_org_orders', {
94
102
  description: "List orders across an organization's events (organizer side).",
95
103
  annotations: { readOnlyHint: true },
96
104
  inputSchema: {
105
+ view: viewArg(),
97
106
  org_id: z.string().describe('Organization id (from eb_my_organizations)'),
98
107
  continuation: schemaContinuation,
99
108
  },
100
- }, async ({ org_id, continuation }) => {
109
+ }, async ({ org_id, continuation, view }) => {
101
110
  const params = new URLSearchParams();
102
111
  if (continuation)
103
112
  params.set('continuation', continuation);
104
113
  const data = await client.request('GET', `/organizations/${enc(org_id)}/orders/${qs(params)}`);
105
- return textResult(data);
114
+ return viewResponse(view, data);
106
115
  });
107
116
  // Simple org-scoped collections: same shape, same pagination, different noun.
108
117
  const orgCollections = [
@@ -130,12 +139,13 @@ export function registerAccountTools(server, deps) {
130
139
  inputSchema: {
131
140
  org_id: z.string().describe('Organization id (from eb_my_organizations)'),
132
141
  continuation: schemaContinuation,
142
+ view: viewArg(),
133
143
  },
134
- }, async ({ org_id, continuation }) => {
144
+ }, async ({ org_id, continuation, view }) => {
135
145
  const params = new URLSearchParams();
136
146
  if (continuation)
137
147
  params.set('continuation', continuation);
138
- return textResult(await client.request('GET', `/organizations/${enc(org_id)}/${noun}/${qs(params)}`));
148
+ return viewResponse(view, await client.request('GET', `/organizations/${enc(org_id)}/${noun}/${qs(params)}`));
139
149
  });
140
150
  }
141
151
  server.registerTool('eb_org_report', {
@@ -152,8 +162,9 @@ export function registerAccountTools(server, deps) {
152
162
  .optional()
153
163
  .describe("Grouping dimension, e.g. 'event', 'day', 'ticket_class'"),
154
164
  continuation: schemaContinuation,
165
+ view: viewArg(),
155
166
  },
156
- }, async ({ org_id, kind, start_date, end_date, event_status, group_by, continuation }) => {
167
+ }, async ({ org_id, kind, start_date, end_date, event_status, group_by, continuation, view }) => {
157
168
  const params = new URLSearchParams();
158
169
  if (start_date)
159
170
  params.set('start_date', start_date);
@@ -165,6 +176,6 @@ export function registerAccountTools(server, deps) {
165
176
  params.set('group_by', group_by);
166
177
  if (continuation)
167
178
  params.set('continuation', continuation);
168
- return textResult(await client.request('GET', `/organizations/${enc(org_id)}/reports/${kind}/${qs(params)}`));
179
+ return viewResponse(view, await client.request('GET', `/organizations/${enc(org_id)}/reports/${kind}/${qs(params)}`));
169
180
  });
170
181
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { textResult } from '@chrischall/mcp-utils';
2
+ import { resolveView } from '@chrischall/mcp-utils';
3
+ import { EB_VIEWS, viewArg, viewResponse } from '../view.js';
3
4
  import { toCompactEvent } from '../discovery.js';
4
5
  /**
5
6
  * Public event discovery. Verified live 2026-07-30: the documented host serves
@@ -8,23 +9,31 @@ import { toCompactEvent } from '../discovery.js';
8
9
  * ARE registered without a bridge. The fetchproxy bridge remains a
9
10
  * fallback on the stdio path.
10
11
  */
12
+ /**
13
+ * `eb_search_events` is the ONE tool here whose compact rung is a real field
14
+ * projection (`toCompactEvent`) rather than the server-wide media strip, so it
15
+ * names what it keeps instead of borrowing the generic note.
16
+ */
17
+ const SEARCH_NOTE = 'compact returns { id, name, start date/time, timezone, venue, city, online flag, free/sold-out flags, organizer, summary, url } per event; ' +
18
+ '"full" returns Eventbrite\'s whole search envelope, every field included.';
11
19
  export async function registerDiscoveryTools(server, deps) {
12
20
  const { discovery, transport } = deps;
13
21
  server.registerTool('eb_resolve_place', {
14
22
  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
23
  annotations: { readOnlyHint: true, openWorldHint: true },
16
24
  inputSchema: {
25
+ view: viewArg(),
17
26
  location: z
18
27
  .string()
19
28
  .min(2)
20
29
  .describe("Location, e.g. 'Charlotte, NC', 'Berlin, Germany', or the slug 'nc--charlotte'"),
21
30
  },
22
- }, async ({ location }) => {
31
+ }, async ({ location, view }) => {
23
32
  const place = await discovery.resolveLocation(location);
24
- return textResult(place);
33
+ return viewResponse(view, place);
25
34
  });
26
35
  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.',
36
+ 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. Answers with the slim per-event projection by default; pass view:"full" for Eventbrite\'s whole search envelope.',
28
37
  annotations: { readOnlyHint: true, openWorldHint: true },
29
38
  inputSchema: {
30
39
  q: z.string().optional().describe('Keyword query'),
@@ -49,10 +58,7 @@ export async function registerDiscoveryTools(server, deps) {
49
58
  .array(z.enum(['places_borough', 'places_neighborhood']))
50
59
  .optional()
51
60
  .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)'),
61
+ view: viewArg(SEARCH_NOTE),
56
62
  },
57
63
  }, async (args) => {
58
64
  const data = await discovery.search({
@@ -70,34 +76,36 @@ export async function registerDiscoveryTools(server, deps) {
70
76
  pageSize: args.page_size,
71
77
  aggs: args.aggs,
72
78
  });
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
- });
79
+ if (resolveView(args.view, EB_VIEWS) === 'full')
80
+ return viewResponse('full', data);
81
+ const results = data.events?.results;
82
+ // Drift fallback: if the envelope isn't the shape we know, hand back the
83
+ // raw response rather than an empty or half-filled projection — the same
84
+ // rule `projectOrRaw` applies, because a record with holes in it is
85
+ // indistinguishable from "there was nothing there".
86
+ if (!Array.isArray(results)) {
87
+ console.error('[eventbrite-mcp] destination search response missing events.results — returning raw response');
88
+ return viewResponse('full', data);
85
89
  }
86
- return textResult(data);
90
+ return viewResponse('full', {
91
+ pagination: data.events?.pagination,
92
+ results: results.map(toCompactEvent),
93
+ });
87
94
  });
88
95
  server.registerTool('eb_event_details', {
89
96
  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
97
  annotations: { readOnlyHint: true, openWorldHint: true },
91
98
  inputSchema: {
99
+ view: viewArg(),
92
100
  event_ids: z.array(z.string()).min(1).max(20).describe('Numeric event ids'),
93
101
  expand: z
94
102
  .string()
95
103
  .optional()
96
104
  .describe('Comma-separated expansions (default primary_venue,image,ticket_availability,event_sales_status,primary_organizer)'),
97
105
  },
98
- }, async ({ event_ids, expand }) => {
106
+ }, async ({ event_ids, expand, view }) => {
99
107
  const data = await discovery.eventsByIds(event_ids, expand ? expand.split(',').map((s) => s.trim()) : undefined);
100
- return textResult(data);
108
+ return viewResponse(view, data);
101
109
  });
102
110
  // eb_healthcheck diagnoses the BRIDGE. With no bridge
103
111
  // there is nothing for it to report on, so it is not registered at all —
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { textResult } from '@chrischall/mcp-utils';
2
+ import { viewArg, viewResponse } from '../view.js';
3
3
  import { enc, qs, schemaContinuation } from './params.js';
4
4
  /** Reference lists served under /system/ rather than the API root. */
5
5
  const SYSTEM_LISTS = new Set(['timezones', 'countries', 'regions']);
@@ -14,48 +14,54 @@ export function registerEventTools(server, deps) {
14
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
15
  annotations: { readOnlyHint: true },
16
16
  inputSchema: {
17
+ view: viewArg(),
17
18
  event_id: z.string().describe('Numeric event id'),
18
19
  expand: z
19
20
  .string()
20
21
  .optional()
21
22
  .describe('Comma-separated expansions (default venue,organizer,ticket_availability)'),
22
23
  },
23
- }, async ({ event_id, expand }) => {
24
+ }, async ({ event_id, expand, view }) => {
24
25
  const exp = expand ?? 'venue,organizer,ticket_availability';
25
26
  const data = await client.request('GET', `/events/${encodeURIComponent(event_id)}/?expand=${encodeURIComponent(exp)}`);
26
- return textResult(data);
27
+ return viewResponse(view, data);
27
28
  });
28
29
  server.registerTool('eb_ticket_classes', {
29
30
  description: "List an event's ticket classes (name, cost, free/paid, on-sale status).",
30
31
  annotations: { readOnlyHint: true },
31
- inputSchema: { event_id: z.string().describe('Numeric event id') },
32
- }, async ({ event_id }) => {
32
+ inputSchema: {
33
+ view: viewArg(), event_id: z.string().describe('Numeric event id')
34
+ },
35
+ }, async ({ event_id, view }) => {
33
36
  const data = await client.request('GET', `/events/${encodeURIComponent(event_id)}/ticket_classes/`);
34
- return textResult(data);
37
+ return viewResponse(view, data);
35
38
  });
36
39
  server.registerTool('eb_event_description', {
37
40
  description: "Get an event's full HTML description.",
38
41
  annotations: { readOnlyHint: true },
39
- inputSchema: { event_id: z.string().describe('Numeric event id') },
40
- }, async ({ event_id }) => {
42
+ inputSchema: {
43
+ view: viewArg(), event_id: z.string().describe('Numeric event id')
44
+ },
45
+ }, async ({ event_id, view }) => {
41
46
  const data = await client.request('GET', `/events/${encodeURIComponent(event_id)}/description/`);
42
- return textResult(data);
47
+ return viewResponse(view, data);
43
48
  });
44
49
  server.registerTool('eb_reference', {
45
50
  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
51
  annotations: { readOnlyHint: true },
47
52
  inputSchema: {
53
+ view: viewArg(),
48
54
  kind: z
49
55
  .enum(['categories', 'subcategories', 'formats', 'timezones', 'countries', 'regions'])
50
56
  .describe('Which list to fetch'),
51
57
  },
52
- }, async ({ kind }) => {
58
+ }, async ({ kind, view }) => {
53
59
  // timezones/countries/regions hang off /system/; the bare /countries/ and
54
60
  // /regions/ paths 404 (verified live 2026-07-30). The taxonomy lists
55
61
  // (categories, subcategories, formats) sit at the root.
56
62
  const path = SYSTEM_LISTS.has(kind) ? `/system/${kind}/` : `/${kind}/`;
57
63
  const data = await client.request('GET', path);
58
- return textResult(data);
64
+ return viewResponse(view, data);
59
65
  });
60
66
  server.registerTool('eb_event_attendees', {
61
67
  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.",
@@ -68,8 +74,9 @@ export function registerEventTools(server, deps) {
68
74
  .optional()
69
75
  .describe('ISO 8601 UTC timestamp — only attendees changed since then'),
70
76
  continuation: schemaContinuation,
77
+ view: viewArg(),
71
78
  },
72
- }, async ({ event_id, status, changed_since, continuation }) => {
79
+ }, async ({ event_id, status, changed_since, continuation, view }) => {
73
80
  const params = new URLSearchParams();
74
81
  if (status)
75
82
  params.set('status', status);
@@ -77,7 +84,7 @@ export function registerEventTools(server, deps) {
77
84
  params.set('changed_since', changed_since);
78
85
  if (continuation)
79
86
  params.set('continuation', continuation);
80
- return textResult(await client.request('GET', `/events/${enc(event_id)}/attendees/${qs(params)}`));
87
+ return viewResponse(view, await client.request('GET', `/events/${enc(event_id)}/attendees/${qs(params)}`));
81
88
  });
82
89
  server.registerTool('eb_event_attendee', {
83
90
  description: "Get one attendee of an event by id (barcode, profile answers, check-in state).",
@@ -85,8 +92,9 @@ export function registerEventTools(server, deps) {
85
92
  inputSchema: {
86
93
  event_id: z.string().describe('Numeric event id'),
87
94
  attendee_id: z.string().describe('Numeric attendee id'),
95
+ view: viewArg(),
88
96
  },
89
- }, async ({ event_id, attendee_id }) => textResult(await client.request('GET', `/events/${enc(event_id)}/attendees/${enc(attendee_id)}/`)));
97
+ }, async ({ event_id, attendee_id, view }) => viewResponse(view, await client.request('GET', `/events/${enc(event_id)}/attendees/${enc(attendee_id)}/`)));
90
98
  server.registerTool('eb_event_orders', {
91
99
  description: "List a single event's orders (organizer-side; requires access to that event).",
92
100
  annotations: { readOnlyHint: true },
@@ -98,8 +106,9 @@ export function registerEventTools(server, deps) {
98
106
  .optional()
99
107
  .describe('ISO 8601 UTC timestamp — only orders changed since then'),
100
108
  continuation: schemaContinuation,
109
+ view: viewArg(),
101
110
  },
102
- }, async ({ event_id, status, changed_since, continuation }) => {
111
+ }, async ({ event_id, status, changed_since, continuation, view }) => {
103
112
  const params = new URLSearchParams();
104
113
  if (status)
105
114
  params.set('status', status);
@@ -107,7 +116,7 @@ export function registerEventTools(server, deps) {
107
116
  params.set('changed_since', changed_since);
108
117
  if (continuation)
109
118
  params.set('continuation', continuation);
110
- return textResult(await client.request('GET', `/events/${enc(event_id)}/orders/${qs(params)}`));
119
+ return viewResponse(view, await client.request('GET', `/events/${enc(event_id)}/orders/${qs(params)}`));
111
120
  });
112
121
  server.registerTool('eb_ticket_class', {
113
122
  description: 'Get one ticket class of an event by id. Use eb_ticket_classes to list them first.',
@@ -115,8 +124,9 @@ export function registerEventTools(server, deps) {
115
124
  inputSchema: {
116
125
  event_id: z.string().describe('Numeric event id'),
117
126
  ticket_class_id: z.string().describe('Numeric ticket class id'),
127
+ view: viewArg(),
118
128
  },
119
- }, async ({ event_id, ticket_class_id }) => textResult(await client.request('GET', `/events/${enc(event_id)}/ticket_classes/${enc(ticket_class_id)}/`)));
129
+ }, async ({ event_id, ticket_class_id, view }) => viewResponse(view, await client.request('GET', `/events/${enc(event_id)}/ticket_classes/${enc(ticket_class_id)}/`)));
120
130
  server.registerTool('eb_event_questions', {
121
131
  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
132
  annotations: { readOnlyHint: true },
@@ -126,6 +136,7 @@ export function registerEventTools(server, deps) {
126
136
  .boolean()
127
137
  .optional()
128
138
  .describe("Fetch the standard question bank instead of the event's custom questions"),
139
+ view: viewArg(),
129
140
  },
130
- }, async ({ event_id, canned }) => textResult(await client.request('GET', `/events/${enc(event_id)}/${canned ? 'canned_questions' : 'questions'}/`)));
141
+ }, async ({ event_id, canned, view }) => viewResponse(view, await client.request('GET', `/events/${enc(event_id)}/${canned ? 'canned_questions' : 'questions'}/`)));
131
142
  }
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { textResult } from '@chrischall/mcp-utils';
2
+ import { viewArg, viewResponse } from '../view.js';
3
3
  import { enc, qs, schemaContinuation, schemaEventStatus } from './params.js';
4
4
  /**
5
5
  * Single-object lookups on the documented API: resolve an id that appears
@@ -20,18 +20,21 @@ export function registerLookupTools(server, deps) {
20
20
  .string()
21
21
  .optional()
22
22
  .describe("Comma-separated expansions, e.g. 'attendees,event'"),
23
+ view: viewArg(),
23
24
  },
24
- }, async ({ order_id, expand }) => {
25
+ }, async ({ order_id, expand, view }) => {
25
26
  const params = new URLSearchParams();
26
27
  if (expand)
27
28
  params.set('expand', expand);
28
- return textResult(await client.request('GET', `/orders/${enc(order_id)}/${qs(params)}`));
29
+ return viewResponse(view, await client.request('GET', `/orders/${enc(order_id)}/${qs(params)}`));
29
30
  });
30
31
  server.registerTool('eb_venue', {
31
32
  description: 'Get a venue by id (name, address, geo). Venue ids appear on expanded events and in eb_org_venues.',
32
33
  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)}/`)));
34
+ inputSchema: { venue_id: z.string().describe('Numeric venue id'),
35
+ view: viewArg(),
36
+ },
37
+ }, async ({ venue_id, view }) => viewResponse(view, await client.request('GET', `/venues/${enc(venue_id)}/`)));
35
38
  server.registerTool('eb_venue_events', {
36
39
  description: 'List the events held at a venue.',
37
40
  annotations: { readOnlyHint: true },
@@ -39,20 +42,23 @@ export function registerLookupTools(server, deps) {
39
42
  venue_id: z.string().describe('Numeric venue id'),
40
43
  status: schemaEventStatus,
41
44
  continuation: schemaContinuation,
45
+ view: viewArg(),
42
46
  },
43
- }, async ({ venue_id, status, continuation }) => {
47
+ }, async ({ venue_id, status, continuation, view }) => {
44
48
  const params = new URLSearchParams();
45
49
  if (status)
46
50
  params.set('status', status);
47
51
  if (continuation)
48
52
  params.set('continuation', continuation);
49
- return textResult(await client.request('GET', `/venues/${enc(venue_id)}/events/${qs(params)}`));
53
+ return viewResponse(view, await client.request('GET', `/venues/${enc(venue_id)}/events/${qs(params)}`));
50
54
  });
51
55
  server.registerTool('eb_organizer', {
52
56
  description: "Get an organizer's public profile by id (name, description, logo, social links).",
53
57
  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)}/`)));
58
+ inputSchema: { organizer_id: z.string().describe('Numeric organizer id'),
59
+ view: viewArg(),
60
+ },
61
+ }, async ({ organizer_id, view }) => viewResponse(view, await client.request('GET', `/organizers/${enc(organizer_id)}/`)));
56
62
  server.registerTool('eb_organizer_events', {
57
63
  description: "List an organizer's events — the public way to see everything one organizer is running.",
58
64
  annotations: { readOnlyHint: true },
@@ -61,8 +67,9 @@ export function registerLookupTools(server, deps) {
61
67
  status: schemaEventStatus,
62
68
  order_by: z.enum(['start_asc', 'start_desc', 'created_asc', 'created_desc']).optional(),
63
69
  continuation: schemaContinuation,
70
+ view: viewArg(),
64
71
  },
65
- }, async ({ organizer_id, status, order_by, continuation }) => {
72
+ }, async ({ organizer_id, status, order_by, continuation, view }) => {
66
73
  const params = new URLSearchParams();
67
74
  if (status)
68
75
  params.set('status', status);
@@ -70,7 +77,7 @@ export function registerLookupTools(server, deps) {
70
77
  params.set('order_by', order_by);
71
78
  if (continuation)
72
79
  params.set('continuation', continuation);
73
- return textResult(await client.request('GET', `/organizers/${enc(organizer_id)}/events/${qs(params)}`));
80
+ return viewResponse(view, await client.request('GET', `/organizers/${enc(organizer_id)}/events/${qs(params)}`));
74
81
  });
75
82
  server.registerTool('eb_series_events', {
76
83
  description: 'List the occurrences of a recurring event series. Search results and events carry a series_id when they belong to one.',
@@ -79,18 +86,21 @@ export function registerLookupTools(server, deps) {
79
86
  series_id: z.string().describe('Numeric series id (from an event/search result)'),
80
87
  status: schemaEventStatus,
81
88
  continuation: schemaContinuation,
89
+ view: viewArg(),
82
90
  },
83
- }, async ({ series_id, status, continuation }) => {
91
+ }, async ({ series_id, status, continuation, view }) => {
84
92
  const params = new URLSearchParams();
85
93
  if (status)
86
94
  params.set('status', status);
87
95
  if (continuation)
88
96
  params.set('continuation', continuation);
89
- return textResult(await client.request('GET', `/series/${enc(series_id)}/events/${qs(params)}`));
97
+ return viewResponse(view, await client.request('GET', `/series/${enc(series_id)}/events/${qs(params)}`));
90
98
  });
91
99
  server.registerTool('eb_user', {
92
100
  description: "Get a public user profile by id. Use eb_me for the authenticated user (that call also returns private fields like emails).",
93
101
  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)}/`)));
102
+ inputSchema: { user_id: z.string().describe('Numeric user id'),
103
+ view: viewArg(),
104
+ },
105
+ }, async ({ user_id, view }) => viewResponse(view, await client.request('GET', `/users/${enc(user_id)}/`)));
96
106
  }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.0'; // x-release-please-version
1
+ export const VERSION = '0.3.0'; // x-release-please-version
package/dist/view.js ADDED
@@ -0,0 +1,55 @@
1
+ import { minifiedResult, resolveView, stripMediaUrls, viewParam } from '@chrischall/mcp-utils';
2
+ /**
3
+ * The rungs this server honours (`@chrischall/mcp-utils`' `view` vocabulary;
4
+ * `chrischall/workflows` `docs/fleet-conventions.md`, "Response shape").
5
+ *
6
+ * **This server is in two tiers at once, and the note per tool says which.**
7
+ *
8
+ * Most read tools hand back Eventbrite's documented-API payload close to
9
+ * verbatim, and nothing here has a verified record of which of those fields
10
+ * matter. For those, compact does the one projection that needs no such
11
+ * knowledge: it strips image and avatar URLs. That is SUBTRACTIVE, so it
12
+ * cannot lose a field nobody knew about — the failure an invented field list
13
+ * would risk, where a record comes back with holes in it and reads like a
14
+ * verified answer.
15
+ *
16
+ * `eb_search_events` is the exception and always was: `toCompactEvent`
17
+ * (`src/discovery.ts`) is a real, hand-written field projection over the
18
+ * consumer search payload. The rollout that added this file described the
19
+ * whole server as un-grounded and did not notice it — the same mistake made in
20
+ * `viator-mcp` (#69, corrected in #72). A hand-written projection is grounded
21
+ * knowledge and is NOT then media-stripped: an un-grounded rule must never
22
+ * overrule a grounded one.
23
+ *
24
+ * So `viewArg` takes a per-tool note. A copy-pasted note is worse than a
25
+ * generic one — it tells a caller to expect a field that was never going to be
26
+ * there.
27
+ */
28
+ export const EB_VIEWS = ['compact', 'full'];
29
+ /** The note for the media-strip tier: what compact does when nothing here knows the shape. */
30
+ const MEDIA_STRIP_NOTE = 'compact strips image/avatar URLs from the response; "full" returns Eventbrite\'s payload untouched. ' +
31
+ 'No field projection on this tool: this server has no verified record of which Eventbrite fields matter here, ' +
32
+ 'and inventing one would risk dropping a field a caller needs.';
33
+ /**
34
+ * The `view` parameter, taken by EVERY read tool in this server.
35
+ *
36
+ * Every one, deliberately — 12 of the 26 took it and 14 did not, which makes a
37
+ * caller memorise a map of which tools accept the vocabulary. An inconsistent
38
+ * surface is its own defect, and worse than a rung that occasionally has
39
+ * nothing to strip.
40
+ */
41
+ export const viewArg = (note = MEDIA_STRIP_NOTE) => viewParam(EB_VIEWS, { note });
42
+ /**
43
+ * Answer in the requested rung.
44
+ *
45
+ * Only ever called from a READ tool. A write's response is a receipt — an id,
46
+ * a status — with nothing to strip and everything to keep.
47
+ *
48
+ * Callers that have ALREADY projected pass `'full'`: the value is theirs to
49
+ * hand back whole, and media-stripping a hand-written projection on the way
50
+ * out is exactly the un-grounded-overrules-grounded error.
51
+ */
52
+ export function viewResponse(view, data) {
53
+ const rung = resolveView(view, EB_VIEWS);
54
+ return minifiedResult(rung === 'compact' ? stripMediaUrls(data) : data);
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/eventbrite-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "mcpName": "io.github.chrischall/eventbrite-mcp",
5
5
  "description": "Eventbrite MCP server for Claude — developed and maintained by AI (Claude Code)",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -45,7 +45,7 @@
45
45
  "typecheck": "tsc -p tsconfig.json --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@chrischall/mcp-utils": "^0.15.0",
48
+ "@chrischall/mcp-utils": "^0.23.2",
49
49
  "@fetchproxy/server": "^2.2.0",
50
50
  "@modelcontextprotocol/sdk": "^1.29.0",
51
51
  "dotenv": "^17.4.0",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/eventbrite-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.2.0",
9
+ "version": "0.3.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@chrischall/eventbrite-mcp",
14
- "version": "0.2.0",
14
+ "version": "0.3.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },