@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/dist/server.js ADDED
@@ -0,0 +1,464 @@
1
+ import { BaseAccessServer, handleApiError } from "@access-mcp/shared";
2
+ import axios from "axios";
3
+ export class EventsServer extends BaseAccessServer {
4
+ _eventsHttpClient;
5
+ constructor() {
6
+ super("access-mcp-events", "0.1.0", "https://support.access-ci.org");
7
+ }
8
+ get httpClient() {
9
+ if (!this._eventsHttpClient) {
10
+ const headers = {
11
+ "User-Agent": `${this.serverName}/${this.version}`,
12
+ };
13
+ // Add authentication if API key is provided
14
+ const apiKey = process.env.ACCESS_CI_API_KEY;
15
+ if (apiKey) {
16
+ headers["Authorization"] = `Bearer ${apiKey}`;
17
+ }
18
+ this._eventsHttpClient = axios.create({
19
+ baseURL: this.baseURL,
20
+ timeout: 10000, // 10 seconds for events API (can be slower)
21
+ headers,
22
+ validateStatus: () => true, // Don't throw on HTTP errors
23
+ });
24
+ }
25
+ return this._eventsHttpClient;
26
+ }
27
+ getTools() {
28
+ return [
29
+ {
30
+ name: "get_events",
31
+ description: "Get ACCESS-CI events with comprehensive filtering capabilities",
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 (today, +1week, -1month, etc.)",
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 (today, +1week, -1month, etc.)",
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",
70
+ },
71
+ end_date: {
72
+ type: "string",
73
+ description: "End date in YYYY-MM-DD or YYYY-MM-DD HH:MM:SS format",
74
+ },
75
+ // Faceted search filters
76
+ event_type: {
77
+ type: "string",
78
+ description: "Filter by event type (workshop, webinar, etc.)",
79
+ },
80
+ event_affiliation: {
81
+ type: "string",
82
+ description: "Filter by organizational affiliation (Community, ACCESS, etc.)",
83
+ },
84
+ skill_level: {
85
+ type: "string",
86
+ description: "Filter by required skill level (beginner, intermediate, advanced)",
87
+ enum: ["beginner", "intermediate", "advanced"],
88
+ },
89
+ event_tags: {
90
+ type: "string",
91
+ description: "Filter by event tags (python, big-data, machine-learning, etc.)",
92
+ },
93
+ limit: {
94
+ type: "number",
95
+ description: "Maximum number of events to return (default: 100)",
96
+ minimum: 1,
97
+ maximum: 1000,
98
+ },
99
+ },
100
+ required: [],
101
+ },
102
+ },
103
+ {
104
+ name: "get_upcoming_events",
105
+ description: "Get upcoming ACCESS-CI events (today onward)",
106
+ inputSchema: {
107
+ type: "object",
108
+ properties: {
109
+ limit: {
110
+ type: "number",
111
+ description: "Maximum number of events to return (default: 50)",
112
+ minimum: 1,
113
+ maximum: 100,
114
+ },
115
+ event_type: {
116
+ type: "string",
117
+ description: "Filter by event type (workshop, webinar, etc.)",
118
+ },
119
+ },
120
+ required: [],
121
+ },
122
+ },
123
+ {
124
+ name: "search_events",
125
+ description: "Search events by keywords in title and description",
126
+ inputSchema: {
127
+ type: "object",
128
+ properties: {
129
+ query: {
130
+ type: "string",
131
+ description: "Search query for event titles and descriptions",
132
+ },
133
+ beginning_date_relative: {
134
+ type: "string",
135
+ description: "Start date using relative values (default: today)",
136
+ default: "today",
137
+ },
138
+ limit: {
139
+ type: "number",
140
+ description: "Maximum number of events to return (default: 25)",
141
+ minimum: 1,
142
+ maximum: 100,
143
+ },
144
+ },
145
+ required: ["query"],
146
+ },
147
+ },
148
+ {
149
+ name: "get_events_by_tag",
150
+ description: "Get events filtered by specific tags",
151
+ inputSchema: {
152
+ type: "object",
153
+ properties: {
154
+ tag: {
155
+ type: "string",
156
+ description: "Event tag to filter by (e.g., python, machine-learning, gpu)",
157
+ },
158
+ time_range: {
159
+ type: "string",
160
+ description: "Time range for events",
161
+ enum: ["upcoming", "this_week", "this_month", "all"],
162
+ default: "upcoming",
163
+ },
164
+ limit: {
165
+ type: "number",
166
+ description: "Maximum number of events to return (default: 25)",
167
+ minimum: 1,
168
+ maximum: 100,
169
+ },
170
+ },
171
+ required: ["tag"],
172
+ },
173
+ },
174
+ ];
175
+ }
176
+ getResources() {
177
+ return [
178
+ {
179
+ uri: "accessci://events",
180
+ name: "ACCESS-CI Events",
181
+ description: "Comprehensive events data including workshops, webinars, and training",
182
+ mimeType: "application/json",
183
+ },
184
+ {
185
+ uri: "accessci://events/upcoming",
186
+ name: "Upcoming Events",
187
+ description: "Events scheduled for today and beyond",
188
+ mimeType: "application/json",
189
+ },
190
+ {
191
+ uri: "accessci://events/workshops",
192
+ name: "Workshops",
193
+ description: "Workshop events only",
194
+ mimeType: "application/json",
195
+ },
196
+ {
197
+ uri: "accessci://events/webinars",
198
+ name: "Webinars",
199
+ description: "Webinar events only",
200
+ mimeType: "application/json",
201
+ },
202
+ ];
203
+ }
204
+ async handleToolCall(request) {
205
+ const { name, arguments: args = {} } = request.params;
206
+ try {
207
+ switch (name) {
208
+ case "get_events":
209
+ return await this.getEvents(args);
210
+ case "get_upcoming_events":
211
+ return await this.getUpcomingEvents(args);
212
+ case "search_events":
213
+ return await this.searchEvents(args);
214
+ case "get_events_by_tag":
215
+ return await this.getEventsByTag(args);
216
+ default:
217
+ throw new Error(`Unknown tool: ${name}`);
218
+ }
219
+ }
220
+ catch (error) {
221
+ return {
222
+ content: [
223
+ {
224
+ type: "text",
225
+ text: `Error: ${handleApiError(error)}`,
226
+ },
227
+ ],
228
+ };
229
+ }
230
+ }
231
+ async handleResourceRead(request) {
232
+ const { uri } = request.params;
233
+ switch (uri) {
234
+ case "accessci://events":
235
+ const allEvents = await this.getEvents({});
236
+ return {
237
+ contents: [
238
+ {
239
+ uri,
240
+ mimeType: "application/json",
241
+ text: allEvents.content[0].text,
242
+ },
243
+ ],
244
+ };
245
+ case "accessci://events/upcoming":
246
+ const upcomingEvents = await this.getUpcomingEvents({});
247
+ return {
248
+ contents: [
249
+ {
250
+ uri,
251
+ mimeType: "application/json",
252
+ text: upcomingEvents.content[0].text,
253
+ },
254
+ ],
255
+ };
256
+ case "accessci://events/workshops":
257
+ const workshops = await this.getEvents({ event_type: "workshop" });
258
+ return {
259
+ contents: [
260
+ {
261
+ uri,
262
+ mimeType: "application/json",
263
+ text: workshops.content[0].text,
264
+ },
265
+ ],
266
+ };
267
+ case "accessci://events/webinars":
268
+ const webinars = await this.getEvents({ event_type: "webinar" });
269
+ return {
270
+ contents: [
271
+ {
272
+ uri,
273
+ mimeType: "application/json",
274
+ text: webinars.content[0].text,
275
+ },
276
+ ],
277
+ };
278
+ default:
279
+ throw new Error(`Unknown resource: ${uri}`);
280
+ }
281
+ }
282
+ buildEventsUrl(params) {
283
+ const url = new URL("/api/2.0/events", this.baseURL);
284
+ // Add date filtering parameters
285
+ if (params.beginning_date_relative) {
286
+ url.searchParams.set("beginning_date_relative", params.beginning_date_relative);
287
+ }
288
+ if (params.end_date_relative) {
289
+ url.searchParams.set("end_date_relative", params.end_date_relative);
290
+ }
291
+ if (params.beginning_date) {
292
+ url.searchParams.set("beginning_date", params.beginning_date);
293
+ }
294
+ if (params.end_date) {
295
+ url.searchParams.set("end_date", params.end_date);
296
+ }
297
+ // Add faceted search filters
298
+ let filterIndex = 0;
299
+ if (params.event_type) {
300
+ url.searchParams.set(`f[${filterIndex}]`, `custom_event_type:${params.event_type}`);
301
+ filterIndex++;
302
+ }
303
+ if (params.event_affiliation) {
304
+ url.searchParams.set(`f[${filterIndex}]`, `custom_event_affiliation:${params.event_affiliation}`);
305
+ filterIndex++;
306
+ }
307
+ if (params.skill_level) {
308
+ url.searchParams.set(`f[${filterIndex}]`, `skill_level:${params.skill_level}`);
309
+ filterIndex++;
310
+ }
311
+ if (params.event_tags) {
312
+ url.searchParams.set(`f[${filterIndex}]`, `custom_event_tags:${params.event_tags}`);
313
+ filterIndex++;
314
+ }
315
+ return url.toString();
316
+ }
317
+ async getEvents(params) {
318
+ const url = this.buildEventsUrl(params);
319
+ const response = await this.httpClient.get(url);
320
+ if (response.status !== 200) {
321
+ throw new Error(`Events API returned ${response.status}: ${response.statusText}`);
322
+ }
323
+ let events = response.data || [];
324
+ // Apply limit if specified
325
+ if (params.limit && events.length > params.limit) {
326
+ events = events.slice(0, params.limit);
327
+ }
328
+ // Enhance events with additional metadata
329
+ const enhancedEvents = events.map((event) => ({
330
+ ...event,
331
+ // Parse dates for better handling
332
+ start_date: new Date(event.date),
333
+ end_date: event.date_1 ? new Date(event.date_1) : null,
334
+ // Split tags into array
335
+ tags: event.custom_event_tags
336
+ ? event.custom_event_tags.split(",").map((tag) => tag.trim())
337
+ : [],
338
+ // Calculate duration if both dates present
339
+ duration_hours: event.date_1
340
+ ? Math.round((new Date(event.date_1).getTime() -
341
+ new Date(event.date).getTime()) /
342
+ (1000 * 60 * 60))
343
+ : null,
344
+ // Relative timing
345
+ starts_in_hours: Math.max(0, Math.round((new Date(event.date).getTime() - Date.now()) / (1000 * 60 * 60))),
346
+ }));
347
+ const summary = {
348
+ total_events: enhancedEvents.length,
349
+ upcoming_events: enhancedEvents.filter((e) => e.starts_in_hours >= 0)
350
+ .length,
351
+ events_this_week: enhancedEvents.filter((e) => e.starts_in_hours <= 168 && e.starts_in_hours >= 0).length,
352
+ event_types: [
353
+ ...new Set(enhancedEvents.map((e) => e.event_type).filter(Boolean)),
354
+ ],
355
+ affiliations: [
356
+ ...new Set(enhancedEvents.map((e) => e.event_affiliation).filter(Boolean)),
357
+ ],
358
+ skill_levels: [
359
+ ...new Set(enhancedEvents.map((e) => e.skill_level).filter(Boolean)),
360
+ ],
361
+ popular_tags: this.getPopularTags(enhancedEvents),
362
+ events: enhancedEvents,
363
+ };
364
+ return {
365
+ content: [
366
+ {
367
+ type: "text",
368
+ text: JSON.stringify(summary, null, 2),
369
+ },
370
+ ],
371
+ };
372
+ }
373
+ async getUpcomingEvents(params) {
374
+ const upcomingParams = {
375
+ ...params,
376
+ beginning_date_relative: "today",
377
+ limit: params.limit || 50,
378
+ };
379
+ return this.getEvents(upcomingParams);
380
+ }
381
+ async searchEvents(params) {
382
+ const searchParams = {
383
+ beginning_date_relative: params.beginning_date_relative || "today",
384
+ limit: params.limit || 25,
385
+ };
386
+ // Get events and filter by search query
387
+ const eventsResponse = await this.getEvents(searchParams);
388
+ const eventsData = JSON.parse(eventsResponse.content[0].text);
389
+ const query = params.query.toLowerCase();
390
+ const filteredEvents = eventsData.events.filter((event) => event.title?.toLowerCase().includes(query) ||
391
+ event.description?.toLowerCase().includes(query) ||
392
+ event.speakers?.toLowerCase().includes(query) ||
393
+ event.tags?.some((tag) => tag.toLowerCase().includes(query)));
394
+ const summary = {
395
+ search_query: params.query,
396
+ total_matches: filteredEvents.length,
397
+ searched_in: eventsData.total_events,
398
+ events: filteredEvents,
399
+ };
400
+ return {
401
+ content: [
402
+ {
403
+ type: "text",
404
+ text: JSON.stringify(summary, null, 2),
405
+ },
406
+ ],
407
+ };
408
+ }
409
+ async getEventsByTag(params) {
410
+ const { tag, time_range = "upcoming", limit = 25 } = params;
411
+ let dateParams = {};
412
+ switch (time_range) {
413
+ case "upcoming":
414
+ dateParams.beginning_date_relative = "today";
415
+ break;
416
+ case "this_week":
417
+ dateParams.beginning_date_relative = "today";
418
+ dateParams.end_date_relative = "+1week";
419
+ break;
420
+ case "this_month":
421
+ dateParams.beginning_date_relative = "today";
422
+ dateParams.end_date_relative = "+1month";
423
+ break;
424
+ case "all":
425
+ // No date restrictions
426
+ break;
427
+ }
428
+ const taggedParams = {
429
+ ...dateParams,
430
+ event_tags: tag,
431
+ limit,
432
+ };
433
+ const eventsResponse = await this.getEvents(taggedParams);
434
+ const eventsData = JSON.parse(eventsResponse.content[0].text);
435
+ const summary = {
436
+ tag: tag,
437
+ time_range: time_range,
438
+ total_events: eventsData.events.length,
439
+ events: eventsData.events,
440
+ };
441
+ return {
442
+ content: [
443
+ {
444
+ type: "text",
445
+ text: JSON.stringify(summary, null, 2),
446
+ },
447
+ ],
448
+ };
449
+ }
450
+ getPopularTags(events) {
451
+ const tagCounts = {};
452
+ events.forEach((event) => {
453
+ if (event.tags) {
454
+ event.tags.forEach((tag) => {
455
+ tagCounts[tag] = (tagCounts[tag] || 0) + 1;
456
+ });
457
+ }
458
+ });
459
+ return Object.entries(tagCounts)
460
+ .sort(([, a], [, b]) => b - a)
461
+ .slice(0, 10)
462
+ .map(([tag]) => tag);
463
+ }
464
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@access-mcp/events",
3
+ "version": "0.1.0",
4
+ "description": "ACCESS-CI Events MCP Server - Get information about workshops, webinars, and training events",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "access-mcp-events": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "dev": "npm run build && npm start",
14
+ "test": "vitest run --exclude '**/**.integration.test.ts'",
15
+ "test:integration": "vitest run src/**/*.integration.test.ts",
16
+ "test:all": "vitest run",
17
+ "test:watch": "vitest",
18
+ "test:ui": "vitest --ui",
19
+ "test:coverage": "vitest run --coverage --exclude '**/**.integration.test.ts'"
20
+ },
21
+ "dependencies": {
22
+ "@access-mcp/shared": "file:../shared",
23
+ "@modelcontextprotocol/sdk": "^0.5.0",
24
+ "axios": "^1.7.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22.0.0",
28
+ "@vitest/ui": "^2.1.9",
29
+ "c8": "^10.1.3",
30
+ "typescript": "^5.0.0",
31
+ "vitest": "^2.1.9"
32
+ },
33
+ "keywords": [
34
+ "mcp",
35
+ "access-ci",
36
+ "events",
37
+ "workshops",
38
+ "training"
39
+ ],
40
+ "author": "ACCESS-CI",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/access-ci-org/access-mcp-servers.git",
45
+ "directory": "packages/events"
46
+ }
47
+ }