@access-mcp/events 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.
package/src/index.ts CHANGED
@@ -4,7 +4,8 @@ import { EventsServer } from "./server.js";
4
4
 
5
5
  async function main() {
6
6
  const server = new EventsServer();
7
- await server.start();
7
+ const port = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
8
+ await server.start(port ? { httpPort: port } : undefined);
8
9
  }
9
10
 
10
11
  if (import.meta.url === `file://${process.argv[1]}`) {
package/src/server.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { BaseAccessServer, handleApiError } from "@access-mcp/shared";
1
+ import { BaseAccessServer, handleApiError, UniversalSearchParams, UniversalResponse } from "@access-mcp/shared";
2
2
  import axios, { AxiosInstance } from "axios";
3
3
 
4
4
  export class EventsServer extends BaseAccessServer {
5
5
  private _eventsHttpClient?: AxiosInstance;
6
6
 
7
7
  constructor() {
8
- super("access-mcp-events", "0.2.0", "https://support.access-ci.org");
8
+ super("access-mcp-events", "0.3.0", "https://support.access-ci.org");
9
9
  }
10
10
 
11
11
  protected get httpClient(): AxiosInstance {
@@ -32,182 +32,42 @@ export class EventsServer extends BaseAccessServer {
32
32
 
33
33
  protected getTools() {
34
34
  return [
35
- {
36
- name: "get_events",
37
- description:
38
- "Get ACCESS-CI events with comprehensive filtering capabilities. Returns events in UTC timezone with enhanced metadata.",
39
- inputSchema: {
40
- type: "object",
41
- properties: {
42
- // Relative date filtering
43
- beginning_date_relative: {
44
- type: "string",
45
- description:
46
- "Start date using relative values. Calculated in UTC by default, or use 'timezone' parameter for local calculations.",
47
- enum: [
48
- "today",
49
- "+1week",
50
- "+2week",
51
- "+1month",
52
- "+2month",
53
- "+1year",
54
- "-1week",
55
- "-1month",
56
- "-1year",
57
- ],
58
- },
59
- end_date_relative: {
60
- type: "string",
61
- description:
62
- "End date using relative values. Calculated in UTC by default, or use 'timezone' parameter for local calculations.",
63
- enum: [
64
- "today",
65
- "+1week",
66
- "+2week",
67
- "+1month",
68
- "+2month",
69
- "+1year",
70
- "-1week",
71
- "-1month",
72
- "-1year",
73
- ],
74
- },
75
- // Absolute date filtering
76
- beginning_date: {
77
- type: "string",
78
- description:
79
- "Start date in YYYY-MM-DD or YYYY-MM-DD HH:MM:SS format. Always interpreted as provided (no timezone conversion).",
80
- },
81
- end_date: {
82
- type: "string",
83
- description:
84
- "End date in YYYY-MM-DD or YYYY-MM-DD HH:MM:SS format. Always interpreted as provided (no timezone conversion).",
85
- },
86
- // Timezone parameter for relative date calculations
87
- timezone: {
88
- type: "string",
89
- description:
90
- "Timezone for relative date calculations (default: UTC). Common values: UTC, America/New_York (Eastern), America/Chicago (Central), America/Denver (Mountain), America/Los_Angeles (Pacific), Europe/London (British), Europe/Berlin (CET). Only affects relative dates, not absolute dates. Invalid timezones default to UTC.",
91
- default: "UTC",
92
- },
93
- // Faceted search filters
94
- event_type: {
95
- type: "string",
96
- description: "Filter by event type (workshop, webinar, etc.)",
97
- },
98
- event_affiliation: {
99
- type: "string",
100
- description:
101
- "Filter by organizational affiliation (Community, ACCESS, etc.)",
102
- },
103
- skill_level: {
104
- type: "string",
105
- description:
106
- "Filter by required skill level (beginner, intermediate, advanced)",
107
- enum: ["beginner", "intermediate", "advanced"],
108
- },
109
- event_tags: {
110
- type: "string",
111
- description:
112
- "Filter by event tags (python, big-data, machine-learning, etc.)",
113
- },
114
- limit: {
115
- type: "number",
116
- description: "Maximum number of events to return (default: 100)",
117
- minimum: 1,
118
- maximum: 1000,
119
- },
120
- },
121
- required: [],
122
- },
123
- },
124
- {
125
- name: "get_upcoming_events",
126
- description: "Get upcoming ACCESS-CI events (from today onward in UTC). Convenient shortcut for get_events with beginning_date_relative=today.",
127
- inputSchema: {
128
- type: "object",
129
- properties: {
130
- limit: {
131
- type: "number",
132
- description: "Maximum number of events to return (default: 50)",
133
- minimum: 1,
134
- maximum: 100,
135
- },
136
- event_type: {
137
- type: "string",
138
- description: "Filter by event type (workshop, webinar, Office Hours, Training, etc.)",
139
- },
140
- timezone: {
141
- type: "string",
142
- description: "Timezone for 'today' calculation (default: UTC). Use user's local timezone for better relevance.",
143
- default: "UTC",
144
- },
145
- },
146
- required: [],
147
- },
148
- },
149
35
  {
150
36
  name: "search_events",
151
- description: "Search events using API's native full-text search. Searches across titles, descriptions, speakers, tags, location, and event type. Much more powerful than tag filtering.",
37
+ description: "Search ACCESS-CI events (workshops, webinars, training). Returns {total, items}.",
152
38
  inputSchema: {
153
39
  type: "object",
154
40
  properties: {
155
41
  query: {
156
42
  type: "string",
157
- description: "Search query (case-insensitive). Use spaces for multiple words (e.g., 'machine learning', 'office hours'). Searches across all event content including descriptions.",
158
- },
159
- beginning_date_relative: {
160
- type: "string",
161
- description: "Start date using relative values (default: today). Use '-1month' or '-1year' to search past events, or omit for all-time search.",
162
- default: "today",
43
+ description: "Search titles, descriptions, speakers, tags"
163
44
  },
164
- timezone: {
45
+ type: {
165
46
  type: "string",
166
- description: "Timezone for relative date calculation (default: UTC)",
167
- default: "UTC",
168
- },
169
- limit: {
170
- type: "number",
171
- description: "Maximum number of events to return (default: 25)",
172
- minimum: 1,
173
- maximum: 100,
47
+ description: "Filter: workshop, webinar, training"
174
48
  },
175
- },
176
- required: ["query"],
177
- },
178
- },
179
- {
180
- name: "get_events_by_tag",
181
- description: "Get events filtered by specific tags. Useful for finding events on topics like 'python', 'ai', 'machine-learning', 'gpu', etc.",
182
- inputSchema: {
183
- type: "object",
184
- properties: {
185
- tag: {
49
+ tags: {
186
50
  type: "string",
187
- description:
188
- "Event tag to filter by. Common tags: python, ai, machine-learning, gpu, deep-learning, neural-networks, big-data, hpc, jetstream, neocortex",
51
+ description: "Filter: python, gpu, hpc, ml"
189
52
  },
190
- time_range: {
53
+ date: {
191
54
  type: "string",
192
- description: "Time range for events (upcoming=today onward, this_week=next 7 days, this_month=next 30 days, all=no date filter)",
193
- enum: ["upcoming", "this_week", "this_month", "all"],
194
- default: "upcoming",
55
+ description: "Filter by time period",
56
+ enum: ["today", "upcoming", "past", "this_week", "this_month"]
195
57
  },
196
- timezone: {
58
+ skill: {
197
59
  type: "string",
198
- description: "Timezone for time_range calculations (default: UTC)",
199
- default: "UTC",
60
+ description: "Skill level filter",
61
+ enum: ["beginner", "intermediate", "advanced"]
200
62
  },
201
63
  limit: {
202
64
  type: "number",
203
- description: "Maximum number of events to return (default: 25)",
204
- minimum: 1,
205
- maximum: 100,
206
- },
207
- },
208
- required: ["tag"],
209
- },
210
- },
65
+ description: "Max results (default: 50)",
66
+ default: 50
67
+ }
68
+ }
69
+ }
70
+ }
211
71
  ];
212
72
  }
213
73
 
@@ -246,26 +106,13 @@ export class EventsServer extends BaseAccessServer {
246
106
 
247
107
  try {
248
108
  switch (name) {
249
- case "get_events":
250
- return await this.getEvents(args);
251
- case "get_upcoming_events":
252
- return await this.getUpcomingEvents(args);
253
109
  case "search_events":
254
110
  return await this.searchEvents(args);
255
- case "get_events_by_tag":
256
- return await this.getEventsByTag(args);
257
111
  default:
258
- throw new Error(`Unknown tool: ${name}`);
112
+ return this.errorResponse(`Unknown tool: ${name}`);
259
113
  }
260
114
  } catch (error) {
261
- return {
262
- content: [
263
- {
264
- type: "text",
265
- text: `Error: ${handleApiError(error)}`,
266
- },
267
- ],
268
- };
115
+ return this.errorResponse(handleApiError(error));
269
116
  }
270
117
  }
271
118
 
@@ -274,112 +121,67 @@ export class EventsServer extends BaseAccessServer {
274
121
 
275
122
  switch (uri) {
276
123
  case "accessci://events":
277
- const allEvents = await this.getEvents({});
278
- return {
279
- contents: [
280
- {
281
- uri,
282
- mimeType: "application/json",
283
- text: allEvents.content[0].text,
284
- },
285
- ],
286
- };
124
+ const allEvents = await this.searchEvents({});
125
+ return this.createJsonResource(uri, JSON.parse(allEvents.content[0].text));
287
126
  case "accessci://events/upcoming":
288
- const upcomingEvents = await this.getUpcomingEvents({});
289
- return {
290
- contents: [
291
- {
292
- uri,
293
- mimeType: "application/json",
294
- text: upcomingEvents.content[0].text,
295
- },
296
- ],
297
- };
127
+ const upcomingEvents = await this.searchEvents({ date: "upcoming" });
128
+ return this.createJsonResource(uri, JSON.parse(upcomingEvents.content[0].text));
298
129
  case "accessci://events/workshops":
299
- const workshops = await this.getEvents({ event_type: "workshop" });
300
- return {
301
- contents: [
302
- {
303
- uri,
304
- mimeType: "application/json",
305
- text: workshops.content[0].text,
306
- },
307
- ],
308
- };
130
+ const workshops = await this.searchEvents({ type: "workshop" });
131
+ return this.createJsonResource(uri, JSON.parse(workshops.content[0].text));
309
132
  case "accessci://events/webinars":
310
- const webinars = await this.getEvents({ event_type: "webinar" });
311
- return {
312
- contents: [
313
- {
314
- uri,
315
- mimeType: "application/json",
316
- text: webinars.content[0].text,
317
- },
318
- ],
319
- };
133
+ const webinars = await this.searchEvents({ type: "webinar" });
134
+ return this.createJsonResource(uri, JSON.parse(webinars.content[0].text));
320
135
  default:
321
136
  throw new Error(`Unknown resource: ${uri}`);
322
137
  }
323
138
  }
324
139
 
325
140
  private buildEventsUrl(params: any): string {
326
- const url = new URL("/api/2.1/events", this.baseURL);
327
-
328
- // Add full-text search parameter (API native search)
329
- if (params.search_api_fulltext) {
330
- url.searchParams.set("search_api_fulltext", params.search_api_fulltext);
331
- }
332
-
333
- // Add date filtering parameters
334
- if (params.beginning_date_relative) {
335
- url.searchParams.set(
336
- "beginning_date_relative",
337
- params.beginning_date_relative,
338
- );
339
- }
340
- if (params.end_date_relative) {
341
- url.searchParams.set("end_date_relative", params.end_date_relative);
342
- }
343
- if (params.beginning_date) {
344
- url.searchParams.set("beginning_date", params.beginning_date);
345
- }
346
- if (params.end_date) {
347
- url.searchParams.set("end_date", params.end_date);
348
- }
141
+ const url = new URL("/api/2.2/events", this.baseURL);
142
+
143
+ const limit = params.limit || 50;
144
+ let itemsPerPage = 50;
145
+ if (limit >= 100) itemsPerPage = 100;
146
+ else if (limit >= 75) itemsPerPage = 75;
147
+ else if (limit >= 50) itemsPerPage = 50;
148
+ else if (limit >= 25) itemsPerPage = 25;
149
+ else itemsPerPage = 25;
150
+ url.searchParams.set("items_per_page", String(itemsPerPage));
151
+
152
+ if (params.query) {
153
+ url.searchParams.set("search_api_fulltext", params.query);
154
+ }
155
+
156
+ // Map universal 'date' to API params
157
+ const dateMap: Record<string, {start: string, end?: string}> = {
158
+ today: {start: "today"},
159
+ upcoming: {start: "today"},
160
+ past: {start: "-1year", end: "today"},
161
+ this_week: {start: "today", end: "+1week"},
162
+ this_month: {start: "today", end: "+1month"}
163
+ };
349
164
 
350
- // Add timezone parameter for relative date calculations
351
- if (params.timezone) {
352
- url.searchParams.set("timezone", params.timezone);
165
+ if (params.date && dateMap[params.date]) {
166
+ const dateMapping = dateMap[params.date];
167
+ url.searchParams.set("beginning_date_relative", dateMapping.start);
168
+ if (dateMapping.end) {
169
+ url.searchParams.set("end_date_relative", dateMapping.end);
170
+ }
353
171
  }
354
172
 
355
- // Add faceted search filters
173
+ // Faceted filters
356
174
  let filterIndex = 0;
357
- if (params.event_type) {
358
- url.searchParams.set(
359
- `f[${filterIndex}]`,
360
- `custom_event_type:${params.event_type}`,
361
- );
175
+ if (params.type) {
176
+ url.searchParams.set(`f[${filterIndex}]`, `custom_event_type:${params.type}`);
362
177
  filterIndex++;
363
178
  }
364
- if (params.event_affiliation) {
365
- url.searchParams.set(
366
- `f[${filterIndex}]`,
367
- `custom_event_affiliation:${params.event_affiliation}`,
368
- );
179
+ if (params.tags) {
180
+ url.searchParams.set(`f[${filterIndex}]`, `custom_event_tags:${params.tags}`);
369
181
  filterIndex++;
370
182
  }
371
- if (params.skill_level) {
372
- url.searchParams.set(
373
- `f[${filterIndex}]`,
374
- `skill_level:${params.skill_level}`,
375
- );
376
- filterIndex++;
377
- }
378
- if (params.event_tags) {
379
- url.searchParams.set(
380
- `f[${filterIndex}]`,
381
- `custom_event_tags:${params.event_tags}`,
382
- );
183
+ if (params.skill) {
184
+ url.searchParams.set(`f[${filterIndex}]`, `skill_level:${params.skill}`);
383
185
  filterIndex++;
384
186
  }
385
187
 
@@ -391,194 +193,53 @@ export class EventsServer extends BaseAccessServer {
391
193
  const response = await this.httpClient.get(url);
392
194
 
393
195
  if (response.status !== 200) {
394
- throw new Error(
395
- `Events API returned ${response.status}: ${response.statusText}`,
396
- );
196
+ throw new Error(`API error ${response.status}`);
397
197
  }
398
198
 
399
199
  let events = response.data || [];
400
-
401
- // Apply limit if specified
402
200
  if (params.limit && events.length > params.limit) {
403
201
  events = events.slice(0, params.limit);
404
202
  }
405
203
 
406
- // Enhance events with additional metadata
407
204
  const enhancedEvents = events.map((event: any) => ({
408
205
  ...event,
409
- // Parse dates for better handling
410
- start_date: new Date(event.date),
411
- end_date: event.date_1 ? new Date(event.date_1) : null,
412
- // Split tags into array
413
- tags: event.custom_event_tags
414
- ? event.custom_event_tags.split(",").map((tag: string) => tag.trim())
415
- : [],
416
- // Calculate duration if both dates present
417
- duration_hours: event.date_1
418
- ? Math.round(
419
- (new Date(event.date_1).getTime() -
420
- new Date(event.date).getTime()) /
421
- (1000 * 60 * 60),
422
- )
206
+ tags: Array.isArray(event.tags) ? event.tags : [],
207
+ duration_hours: event.end_date
208
+ ? Math.round((new Date(event.end_date).getTime() - new Date(event.start_date).getTime()) / 3600000)
423
209
  : null,
424
- // Relative timing
425
- starts_in_hours: Math.max(
426
- 0,
427
- Math.round(
428
- (new Date(event.date).getTime() - Date.now()) / (1000 * 60 * 60),
429
- ),
430
- ),
210
+ starts_in_hours: Math.max(0, Math.round((new Date(event.start_date).getTime() - Date.now()) / 3600000))
431
211
  }));
432
212
 
433
- const summary = {
434
- total_events: enhancedEvents.length,
435
- upcoming_events: enhancedEvents.filter((e: any) => e.starts_in_hours >= 0)
436
- .length,
437
- events_this_week: enhancedEvents.filter(
438
- (e: any) => e.starts_in_hours <= 168 && e.starts_in_hours >= 0,
439
- ).length,
440
- api_info: {
441
- endpoint_version: "2.1",
442
- timezone_handling: "All timestamps in UTC (Z suffix). Relative dates calculated using timezone parameter (default: UTC).",
443
- timezone_used: params.timezone || "UTC",
444
- },
445
- event_types: [
446
- ...new Set(
447
- enhancedEvents.map((e: any) => e.event_type).filter(Boolean),
448
- ),
449
- ],
450
- affiliations: [
451
- ...new Set(
452
- enhancedEvents.map((e: any) => e.event_affiliation).filter(Boolean),
453
- ),
454
- ],
455
- skill_levels: [
456
- ...new Set(
457
- enhancedEvents.map((e: any) => e.skill_level).filter(Boolean),
458
- ),
459
- ],
460
- popular_tags: this.getPopularTags(enhancedEvents),
461
- events: enhancedEvents,
462
- };
463
-
464
213
  return {
465
- content: [
466
- {
467
- type: "text",
468
- text: JSON.stringify(summary, null, 2),
469
- },
470
- ],
214
+ content: [{
215
+ type: "text",
216
+ text: JSON.stringify({
217
+ total: enhancedEvents.length,
218
+ items: enhancedEvents
219
+ })
220
+ }]
471
221
  };
472
222
  }
473
223
 
474
- private async getUpcomingEvents(params: any) {
475
- const upcomingParams = {
476
- ...params,
477
- beginning_date_relative: "today",
478
- limit: params.limit || 50,
479
- // Pass through timezone if provided
480
- ...(params.timezone && { timezone: params.timezone }),
481
- };
482
-
483
- return this.getEvents(upcomingParams);
484
- }
485
-
486
224
  private async searchEvents(params: any) {
487
- // Use API's native full-text search instead of client-side filtering
488
- const searchParams = {
489
- search_api_fulltext: params.query,
490
- beginning_date_relative: params.beginning_date_relative || "today",
491
- limit: params.limit || 25,
492
- // Pass through timezone if provided
493
- ...(params.timezone && { timezone: params.timezone }),
494
- };
495
-
496
- // Use the API's native search capabilities
497
- const eventsResponse = await this.getEvents(searchParams);
498
- const eventsData = JSON.parse(eventsResponse.content[0].text);
499
-
500
- // API returns already filtered results, no need for client-side filtering
501
- const summary = {
502
- search_query: params.query,
503
- total_matches: eventsData.total_events,
504
- search_method: "API native full-text search",
505
- search_scope: "titles, descriptions, speakers, tags, location, event type",
506
- events: eventsData.events,
507
- };
508
-
509
- return {
510
- content: [
511
- {
512
- type: "text",
513
- text: JSON.stringify(summary, null, 2),
514
- },
515
- ],
516
- };
517
- }
518
-
519
- private async getEventsByTag(params: any) {
520
- const { tag, time_range = "upcoming", limit = 25, timezone } = params;
521
-
522
- let dateParams: any = {};
523
- switch (time_range) {
524
- case "upcoming":
525
- dateParams.beginning_date_relative = "today";
526
- break;
527
- case "this_week":
528
- dateParams.beginning_date_relative = "today";
529
- dateParams.end_date_relative = "+1week";
530
- break;
531
- case "this_month":
532
- dateParams.beginning_date_relative = "today";
533
- dateParams.end_date_relative = "+1month";
534
- break;
535
- case "all":
536
- // No date restrictions
537
- break;
225
+ // Validate enum parameters
226
+ const validDateValues = ["today", "upcoming", "past", "this_week", "this_month"];
227
+ const validSkillValues = ["beginner", "intermediate", "advanced"];
228
+
229
+ if (params.date && !validDateValues.includes(params.date)) {
230
+ return this.errorResponse(
231
+ `Invalid date value: '${params.date}'`,
232
+ `Valid values are: ${validDateValues.join(", ")}`
233
+ );
538
234
  }
539
235
 
540
- const taggedParams = {
541
- ...dateParams,
542
- event_tags: tag,
543
- limit,
544
- // Pass through timezone if provided
545
- ...(timezone && { timezone }),
546
- };
547
-
548
- const eventsResponse = await this.getEvents(taggedParams);
549
- const eventsData = JSON.parse(eventsResponse.content[0].text);
550
-
551
- const summary = {
552
- tag: tag,
553
- time_range: time_range,
554
- total_events: eventsData.events.length,
555
- events: eventsData.events,
556
- };
557
-
558
- return {
559
- content: [
560
- {
561
- type: "text",
562
- text: JSON.stringify(summary, null, 2),
563
- },
564
- ],
565
- };
566
- }
567
-
568
- private getPopularTags(events: any[]): string[] {
569
- const tagCounts: { [key: string]: number } = {};
570
-
571
- events.forEach((event) => {
572
- if (event.tags) {
573
- event.tags.forEach((tag: string) => {
574
- tagCounts[tag] = (tagCounts[tag] || 0) + 1;
575
- });
576
- }
577
- });
236
+ if (params.skill && !validSkillValues.includes(params.skill)) {
237
+ return this.errorResponse(
238
+ `Invalid skill value: '${params.skill}'`,
239
+ `Valid values are: ${validSkillValues.join(", ")}`
240
+ );
241
+ }
578
242
 
579
- return Object.entries(tagCounts)
580
- .sort(([, a], [, b]) => b - a)
581
- .slice(0, 10)
582
- .map(([tag]) => tag);
243
+ return await this.getEvents(params);
583
244
  }
584
245
  }