@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/README.md +99 -75
- package/dist/index.js +2 -1
- package/dist/server.d.ts +9 -148
- package/dist/server.js +87 -373
- package/package.json +2 -2
- package/src/__tests__/server.integration.test.ts +60 -145
- package/src/__tests__/server.test.ts +108 -466
- package/src/index.ts +2 -1
- package/src/server.ts +94 -433
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
45
|
+
type: {
|
|
165
46
|
type: "string",
|
|
166
|
-
description: "
|
|
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
|
-
|
|
53
|
+
date: {
|
|
191
54
|
type: "string",
|
|
192
|
-
description: "
|
|
193
|
-
enum: ["upcoming", "
|
|
194
|
-
default: "upcoming",
|
|
55
|
+
description: "Filter by time period",
|
|
56
|
+
enum: ["today", "upcoming", "past", "this_week", "this_month"]
|
|
195
57
|
},
|
|
196
|
-
|
|
58
|
+
skill: {
|
|
197
59
|
type: "string",
|
|
198
|
-
description: "
|
|
199
|
-
|
|
60
|
+
description: "Skill level filter",
|
|
61
|
+
enum: ["beginner", "intermediate", "advanced"]
|
|
200
62
|
},
|
|
201
63
|
limit: {
|
|
202
64
|
type: "number",
|
|
203
|
-
description: "
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
url.searchParams.set("
|
|
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
|
-
//
|
|
173
|
+
// Faceted filters
|
|
356
174
|
let filterIndex = 0;
|
|
357
|
-
if (params.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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
|
-
|
|
468
|
-
|
|
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
|
-
//
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
|
580
|
-
.sort(([, a], [, b]) => b - a)
|
|
581
|
-
.slice(0, 10)
|
|
582
|
-
.map(([tag]) => tag);
|
|
243
|
+
return await this.getEvents(params);
|
|
583
244
|
}
|
|
584
245
|
}
|