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