@access-mcp/events 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/server.ts ADDED
@@ -0,0 +1,546 @@
1
+ import { BaseAccessServer, handleApiError } from "@access-mcp/shared";
2
+ import axios, { AxiosInstance } from "axios";
3
+
4
+ export class EventsServer extends BaseAccessServer {
5
+ private _eventsHttpClient?: AxiosInstance;
6
+
7
+ constructor() {
8
+ super("access-mcp-events", "0.1.0", "https://support.access-ci.org");
9
+ }
10
+
11
+ protected get httpClient(): AxiosInstance {
12
+ if (!this._eventsHttpClient) {
13
+ const headers: any = {
14
+ "User-Agent": `${this.serverName}/${this.version}`,
15
+ };
16
+
17
+ // Add authentication if API key is provided
18
+ const apiKey = process.env.ACCESS_CI_API_KEY;
19
+ if (apiKey) {
20
+ headers["Authorization"] = `Bearer ${apiKey}`;
21
+ }
22
+
23
+ this._eventsHttpClient = axios.create({
24
+ baseURL: this.baseURL,
25
+ timeout: 10000, // 10 seconds for events API (can be slower)
26
+ headers,
27
+ validateStatus: () => true, // Don't throw on HTTP errors
28
+ });
29
+ }
30
+ return this._eventsHttpClient;
31
+ }
32
+
33
+ protected getTools() {
34
+ return [
35
+ {
36
+ name: "get_events",
37
+ description:
38
+ "Get ACCESS-CI events with comprehensive filtering capabilities",
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 (today, +1week, -1month, etc.)",
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 (today, +1week, -1month, etc.)",
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",
80
+ },
81
+ end_date: {
82
+ type: "string",
83
+ description:
84
+ "End date in YYYY-MM-DD or YYYY-MM-DD HH:MM:SS format",
85
+ },
86
+ // Faceted search filters
87
+ event_type: {
88
+ type: "string",
89
+ description: "Filter by event type (workshop, webinar, etc.)",
90
+ },
91
+ event_affiliation: {
92
+ type: "string",
93
+ description:
94
+ "Filter by organizational affiliation (Community, ACCESS, etc.)",
95
+ },
96
+ skill_level: {
97
+ type: "string",
98
+ description:
99
+ "Filter by required skill level (beginner, intermediate, advanced)",
100
+ enum: ["beginner", "intermediate", "advanced"],
101
+ },
102
+ event_tags: {
103
+ type: "string",
104
+ description:
105
+ "Filter by event tags (python, big-data, machine-learning, etc.)",
106
+ },
107
+ limit: {
108
+ type: "number",
109
+ description: "Maximum number of events to return (default: 100)",
110
+ minimum: 1,
111
+ maximum: 1000,
112
+ },
113
+ },
114
+ required: [],
115
+ },
116
+ },
117
+ {
118
+ name: "get_upcoming_events",
119
+ description: "Get upcoming ACCESS-CI events (today onward)",
120
+ inputSchema: {
121
+ type: "object",
122
+ properties: {
123
+ limit: {
124
+ type: "number",
125
+ description: "Maximum number of events to return (default: 50)",
126
+ minimum: 1,
127
+ maximum: 100,
128
+ },
129
+ event_type: {
130
+ type: "string",
131
+ description: "Filter by event type (workshop, webinar, etc.)",
132
+ },
133
+ },
134
+ required: [],
135
+ },
136
+ },
137
+ {
138
+ name: "search_events",
139
+ description: "Search events by keywords in title and description",
140
+ inputSchema: {
141
+ type: "object",
142
+ properties: {
143
+ query: {
144
+ type: "string",
145
+ description: "Search query for event titles and descriptions",
146
+ },
147
+ beginning_date_relative: {
148
+ type: "string",
149
+ description: "Start date using relative values (default: today)",
150
+ default: "today",
151
+ },
152
+ limit: {
153
+ type: "number",
154
+ description: "Maximum number of events to return (default: 25)",
155
+ minimum: 1,
156
+ maximum: 100,
157
+ },
158
+ },
159
+ required: ["query"],
160
+ },
161
+ },
162
+ {
163
+ name: "get_events_by_tag",
164
+ description: "Get events filtered by specific tags",
165
+ inputSchema: {
166
+ type: "object",
167
+ properties: {
168
+ tag: {
169
+ type: "string",
170
+ description:
171
+ "Event tag to filter by (e.g., python, machine-learning, gpu)",
172
+ },
173
+ time_range: {
174
+ type: "string",
175
+ description: "Time range for events",
176
+ enum: ["upcoming", "this_week", "this_month", "all"],
177
+ default: "upcoming",
178
+ },
179
+ limit: {
180
+ type: "number",
181
+ description: "Maximum number of events to return (default: 25)",
182
+ minimum: 1,
183
+ maximum: 100,
184
+ },
185
+ },
186
+ required: ["tag"],
187
+ },
188
+ },
189
+ ];
190
+ }
191
+
192
+ protected getResources() {
193
+ return [
194
+ {
195
+ uri: "accessci://events",
196
+ name: "ACCESS-CI Events",
197
+ description:
198
+ "Comprehensive events data including workshops, webinars, and training",
199
+ mimeType: "application/json",
200
+ },
201
+ {
202
+ uri: "accessci://events/upcoming",
203
+ name: "Upcoming Events",
204
+ description: "Events scheduled for today and beyond",
205
+ mimeType: "application/json",
206
+ },
207
+ {
208
+ uri: "accessci://events/workshops",
209
+ name: "Workshops",
210
+ description: "Workshop events only",
211
+ mimeType: "application/json",
212
+ },
213
+ {
214
+ uri: "accessci://events/webinars",
215
+ name: "Webinars",
216
+ description: "Webinar events only",
217
+ mimeType: "application/json",
218
+ },
219
+ ];
220
+ }
221
+
222
+ async handleToolCall(request: any) {
223
+ const { name, arguments: args = {} } = request.params;
224
+
225
+ try {
226
+ switch (name) {
227
+ case "get_events":
228
+ return await this.getEvents(args);
229
+ case "get_upcoming_events":
230
+ return await this.getUpcomingEvents(args);
231
+ case "search_events":
232
+ return await this.searchEvents(args);
233
+ case "get_events_by_tag":
234
+ return await this.getEventsByTag(args);
235
+ default:
236
+ throw new Error(`Unknown tool: ${name}`);
237
+ }
238
+ } catch (error) {
239
+ return {
240
+ content: [
241
+ {
242
+ type: "text",
243
+ text: `Error: ${handleApiError(error)}`,
244
+ },
245
+ ],
246
+ };
247
+ }
248
+ }
249
+
250
+ async handleResourceRead(request: any) {
251
+ const { uri } = request.params;
252
+
253
+ switch (uri) {
254
+ case "accessci://events":
255
+ const allEvents = await this.getEvents({});
256
+ return {
257
+ contents: [
258
+ {
259
+ uri,
260
+ mimeType: "application/json",
261
+ text: allEvents.content[0].text,
262
+ },
263
+ ],
264
+ };
265
+ case "accessci://events/upcoming":
266
+ const upcomingEvents = await this.getUpcomingEvents({});
267
+ return {
268
+ contents: [
269
+ {
270
+ uri,
271
+ mimeType: "application/json",
272
+ text: upcomingEvents.content[0].text,
273
+ },
274
+ ],
275
+ };
276
+ case "accessci://events/workshops":
277
+ const workshops = await this.getEvents({ event_type: "workshop" });
278
+ return {
279
+ contents: [
280
+ {
281
+ uri,
282
+ mimeType: "application/json",
283
+ text: workshops.content[0].text,
284
+ },
285
+ ],
286
+ };
287
+ case "accessci://events/webinars":
288
+ const webinars = await this.getEvents({ event_type: "webinar" });
289
+ return {
290
+ contents: [
291
+ {
292
+ uri,
293
+ mimeType: "application/json",
294
+ text: webinars.content[0].text,
295
+ },
296
+ ],
297
+ };
298
+ default:
299
+ throw new Error(`Unknown resource: ${uri}`);
300
+ }
301
+ }
302
+
303
+ private buildEventsUrl(params: any): string {
304
+ const url = new URL("/api/2.0/events", this.baseURL);
305
+
306
+ // Add date filtering parameters
307
+ if (params.beginning_date_relative) {
308
+ url.searchParams.set(
309
+ "beginning_date_relative",
310
+ params.beginning_date_relative,
311
+ );
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
+
323
+ // Add faceted search filters
324
+ let filterIndex = 0;
325
+ if (params.event_type) {
326
+ url.searchParams.set(
327
+ `f[${filterIndex}]`,
328
+ `custom_event_type:${params.event_type}`,
329
+ );
330
+ filterIndex++;
331
+ }
332
+ if (params.event_affiliation) {
333
+ url.searchParams.set(
334
+ `f[${filterIndex}]`,
335
+ `custom_event_affiliation:${params.event_affiliation}`,
336
+ );
337
+ filterIndex++;
338
+ }
339
+ if (params.skill_level) {
340
+ url.searchParams.set(
341
+ `f[${filterIndex}]`,
342
+ `skill_level:${params.skill_level}`,
343
+ );
344
+ filterIndex++;
345
+ }
346
+ if (params.event_tags) {
347
+ url.searchParams.set(
348
+ `f[${filterIndex}]`,
349
+ `custom_event_tags:${params.event_tags}`,
350
+ );
351
+ filterIndex++;
352
+ }
353
+
354
+ return url.toString();
355
+ }
356
+
357
+ private async getEvents(params: any) {
358
+ const url = this.buildEventsUrl(params);
359
+ const response = await this.httpClient.get(url);
360
+
361
+ if (response.status !== 200) {
362
+ throw new Error(
363
+ `Events API returned ${response.status}: ${response.statusText}`,
364
+ );
365
+ }
366
+
367
+ let events = response.data || [];
368
+
369
+ // Apply limit if specified
370
+ if (params.limit && events.length > params.limit) {
371
+ events = events.slice(0, params.limit);
372
+ }
373
+
374
+ // Enhance events with additional metadata
375
+ const enhancedEvents = events.map((event: any) => ({
376
+ ...event,
377
+ // Parse dates for better handling
378
+ start_date: new Date(event.date),
379
+ end_date: event.date_1 ? new Date(event.date_1) : null,
380
+ // Split tags into array
381
+ tags: event.custom_event_tags
382
+ ? event.custom_event_tags.split(",").map((tag: string) => tag.trim())
383
+ : [],
384
+ // Calculate duration if both dates present
385
+ duration_hours: event.date_1
386
+ ? Math.round(
387
+ (new Date(event.date_1).getTime() -
388
+ new Date(event.date).getTime()) /
389
+ (1000 * 60 * 60),
390
+ )
391
+ : null,
392
+ // Relative timing
393
+ starts_in_hours: Math.max(
394
+ 0,
395
+ Math.round(
396
+ (new Date(event.date).getTime() - Date.now()) / (1000 * 60 * 60),
397
+ ),
398
+ ),
399
+ }));
400
+
401
+ const summary = {
402
+ total_events: enhancedEvents.length,
403
+ upcoming_events: enhancedEvents.filter((e: any) => e.starts_in_hours >= 0)
404
+ .length,
405
+ events_this_week: enhancedEvents.filter(
406
+ (e: any) => e.starts_in_hours <= 168 && e.starts_in_hours >= 0,
407
+ ).length,
408
+ event_types: [
409
+ ...new Set(
410
+ enhancedEvents.map((e: any) => e.event_type).filter(Boolean),
411
+ ),
412
+ ],
413
+ affiliations: [
414
+ ...new Set(
415
+ enhancedEvents.map((e: any) => e.event_affiliation).filter(Boolean),
416
+ ),
417
+ ],
418
+ skill_levels: [
419
+ ...new Set(
420
+ enhancedEvents.map((e: any) => e.skill_level).filter(Boolean),
421
+ ),
422
+ ],
423
+ popular_tags: this.getPopularTags(enhancedEvents),
424
+ events: enhancedEvents,
425
+ };
426
+
427
+ return {
428
+ content: [
429
+ {
430
+ type: "text",
431
+ text: JSON.stringify(summary, null, 2),
432
+ },
433
+ ],
434
+ };
435
+ }
436
+
437
+ private async getUpcomingEvents(params: any) {
438
+ const upcomingParams = {
439
+ ...params,
440
+ beginning_date_relative: "today",
441
+ limit: params.limit || 50,
442
+ };
443
+
444
+ return this.getEvents(upcomingParams);
445
+ }
446
+
447
+ private async searchEvents(params: any) {
448
+ const searchParams = {
449
+ beginning_date_relative: params.beginning_date_relative || "today",
450
+ limit: params.limit || 25,
451
+ };
452
+
453
+ // Get events and filter by search query
454
+ const eventsResponse = await this.getEvents(searchParams);
455
+ const eventsData = JSON.parse(eventsResponse.content[0].text);
456
+
457
+ const query = params.query.toLowerCase();
458
+ const filteredEvents = eventsData.events.filter(
459
+ (event: any) =>
460
+ event.title?.toLowerCase().includes(query) ||
461
+ event.description?.toLowerCase().includes(query) ||
462
+ event.speakers?.toLowerCase().includes(query) ||
463
+ event.tags?.some((tag: string) => tag.toLowerCase().includes(query)),
464
+ );
465
+
466
+ const summary = {
467
+ search_query: params.query,
468
+ total_matches: filteredEvents.length,
469
+ searched_in: eventsData.total_events,
470
+ events: filteredEvents,
471
+ };
472
+
473
+ return {
474
+ content: [
475
+ {
476
+ type: "text",
477
+ text: JSON.stringify(summary, null, 2),
478
+ },
479
+ ],
480
+ };
481
+ }
482
+
483
+ private async getEventsByTag(params: any) {
484
+ const { tag, time_range = "upcoming", limit = 25 } = params;
485
+
486
+ let dateParams: any = {};
487
+ switch (time_range) {
488
+ case "upcoming":
489
+ dateParams.beginning_date_relative = "today";
490
+ break;
491
+ case "this_week":
492
+ dateParams.beginning_date_relative = "today";
493
+ dateParams.end_date_relative = "+1week";
494
+ break;
495
+ case "this_month":
496
+ dateParams.beginning_date_relative = "today";
497
+ dateParams.end_date_relative = "+1month";
498
+ break;
499
+ case "all":
500
+ // No date restrictions
501
+ break;
502
+ }
503
+
504
+ const taggedParams = {
505
+ ...dateParams,
506
+ event_tags: tag,
507
+ limit,
508
+ };
509
+
510
+ const eventsResponse = await this.getEvents(taggedParams);
511
+ const eventsData = JSON.parse(eventsResponse.content[0].text);
512
+
513
+ const summary = {
514
+ tag: tag,
515
+ time_range: time_range,
516
+ total_events: eventsData.events.length,
517
+ events: eventsData.events,
518
+ };
519
+
520
+ return {
521
+ content: [
522
+ {
523
+ type: "text",
524
+ text: JSON.stringify(summary, null, 2),
525
+ },
526
+ ],
527
+ };
528
+ }
529
+
530
+ private getPopularTags(events: any[]): string[] {
531
+ const tagCounts: { [key: string]: number } = {};
532
+
533
+ events.forEach((event) => {
534
+ if (event.tags) {
535
+ event.tags.forEach((tag: string) => {
536
+ tagCounts[tag] = (tagCounts[tag] || 0) + 1;
537
+ });
538
+ }
539
+ });
540
+
541
+ return Object.entries(tagCounts)
542
+ .sort(([, a], [, b]) => b - a)
543
+ .slice(0, 10)
544
+ .map(([tag]) => tag);
545
+ }
546
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["src/**/*.test.ts"],
9
+ "references": [{ "path": "../shared" }]
10
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import path from "path";
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ globals: true,
7
+ environment: "node",
8
+ coverage: {
9
+ reporter: ["text", "json", "html"],
10
+ exclude: ["node_modules/", "dist/", "*.config.ts", "src/index.ts"],
11
+ },
12
+ },
13
+ resolve: {
14
+ alias: {
15
+ "@": path.resolve(__dirname, "./src"),
16
+ },
17
+ },
18
+ });