@access-mcp/events 0.1.0 → 0.2.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 CHANGED
@@ -8,7 +8,7 @@ A Model Context Protocol server providing access to ACCESS-CI events data includ
8
8
 
9
9
  - **`get_events`** - Get ACCESS-CI events with comprehensive filtering
10
10
  - **`get_upcoming_events`** - Get upcoming events (today onward)
11
- - **`search_events`** - Search events by keywords in title/description
11
+ - **`search_events`** - **Enhanced!** Search events using API's native full-text search across all content
12
12
  - **`get_events_by_tag`** - Get events filtered by specific tags
13
13
 
14
14
  ### 📊 Resources
@@ -49,10 +49,12 @@ Add to your Claude Desktop configuration:
49
49
  What events are coming up in the next week?
50
50
  ```
51
51
 
52
- **Search for specific topics:**
52
+ **Search for specific topics (now much more powerful!):**
53
53
 
54
54
  ```
55
55
  Find upcoming Python workshops
56
+ Show me machine learning events
57
+ Search for "office hours" this week
56
58
  ```
57
59
 
58
60
  **Filter by skill level:**
@@ -64,9 +66,33 @@ Show me beginner-level events this month
64
66
  **Get events by tags:**
65
67
 
66
68
  ```
67
- Find all machine learning events
69
+ Find all GPU computing events
68
70
  ```
69
71
 
72
+ ## Key Improvements
73
+
74
+ ### 🚀 Enhanced Search (v2.1)
75
+
76
+ **Native API Full-Text Search:**
77
+ - Searches across titles, descriptions, speakers, tags, location, and event type
78
+ - Supports multi-word queries (e.g., "machine learning", "office hours")
79
+ - Much more comprehensive results than previous tag-only filtering
80
+ - Server-side indexing for better performance
81
+
82
+ **Search Examples:**
83
+ - `"python"` - Find Python programming events
84
+ - `"machine learning"` - Find ML-related content in any field
85
+ - `"gpu computing"` - Find GPU-related events
86
+ - `"office hours"` - Find all office hours sessions
87
+
88
+ ### 🌍 Timezone Support (v2.1)
89
+
90
+ **Smart Timezone Handling:**
91
+ - All timestamps returned in UTC (ISO 8601 format with Z suffix)
92
+ - Timezone parameter controls relative date calculations
93
+ - Common timezone examples: `America/New_York`, `Europe/London`, `Asia/Tokyo`
94
+ - Default: UTC calculations
95
+
70
96
  ## Filtering Capabilities
71
97
 
72
98
  ### Date Filtering
@@ -144,18 +170,28 @@ The server adds these computed fields:
144
170
  }
145
171
  ```
146
172
 
147
- ### Search Events
173
+ ### Search Events (Enhanced API Search)
148
174
 
149
175
  ```typescript
150
- // Search for GPU-related events
176
+ // Search for GPU-related events using native API search
151
177
  {
152
178
  "tool": "search_events",
153
179
  "arguments": {
154
180
  "query": "GPU computing",
155
181
  "beginning_date_relative": "today",
182
+ "timezone": "America/New_York",
156
183
  "limit": 10
157
184
  }
158
185
  }
186
+
187
+ // Multi-word search examples
188
+ {
189
+ "tool": "search_events",
190
+ "arguments": {
191
+ "query": "machine learning",
192
+ "beginning_date_relative": "-1month"
193
+ }
194
+ }
159
195
  ```
160
196
 
161
197
  ### Get Events by Tag
@@ -187,10 +223,17 @@ npm test
187
223
 
188
224
  ## Base URL
189
225
 
190
- The server connects to: `https://support.access-ci.org/api/2.0/events`
226
+ The server connects to: `https://support.access-ci.org/api/2.1/events` (v2.1 with UTC timestamps and enhanced search)
191
227
 
192
228
  ## Technical Notes
193
229
 
230
+ ### API Version 2.1 Features
231
+ - **UTC timestamps**: All dates returned in UTC with Z suffix (e.g., `2024-08-30T13:00:00Z`)
232
+ - **Native search**: Uses `search_api_fulltext` parameter for comprehensive searching
233
+ - **Timezone support**: Relative dates calculated using specified timezone
234
+ - **Enhanced metadata**: Responses include API version and timezone info
235
+
236
+ ### General
194
237
  - All date comparisons use the event's start date (`date` field)
195
238
  - Results include both upcoming and past events unless date filtered
196
239
  - Faceted search filters use AND logic when combined
package/dist/server.d.ts CHANGED
@@ -29,6 +29,11 @@ export declare class EventsServer extends BaseAccessServer {
29
29
  type: string;
30
30
  description: string;
31
31
  };
32
+ timezone: {
33
+ type: string;
34
+ description: string;
35
+ default: string;
36
+ };
32
37
  event_type: {
33
38
  type: string;
34
39
  description: string;
@@ -74,6 +79,11 @@ export declare class EventsServer extends BaseAccessServer {
74
79
  type: string;
75
80
  description: string;
76
81
  };
82
+ timezone: {
83
+ type: string;
84
+ description: string;
85
+ default: string;
86
+ };
77
87
  beginning_date_relative?: undefined;
78
88
  end_date_relative?: undefined;
79
89
  beginning_date?: undefined;
@@ -103,6 +113,11 @@ export declare class EventsServer extends BaseAccessServer {
103
113
  default: string;
104
114
  enum?: undefined;
105
115
  };
116
+ timezone: {
117
+ type: string;
118
+ description: string;
119
+ default: string;
120
+ };
106
121
  limit: {
107
122
  type: string;
108
123
  description: string;
@@ -137,6 +152,11 @@ export declare class EventsServer extends BaseAccessServer {
137
152
  enum: string[];
138
153
  default: string;
139
154
  };
155
+ timezone: {
156
+ type: string;
157
+ description: string;
158
+ default: string;
159
+ };
140
160
  limit: {
141
161
  type: string;
142
162
  description: string;
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.1.0", "https://support.access-ci.org");
6
+ super("access-mcp-events", "0.2.0", "https://support.access-ci.org");
7
7
  }
8
8
  get httpClient() {
9
9
  if (!this._eventsHttpClient) {
@@ -28,14 +28,14 @@ export class EventsServer extends BaseAccessServer {
28
28
  return [
29
29
  {
30
30
  name: "get_events",
31
- description: "Get ACCESS-CI events with comprehensive filtering capabilities",
31
+ description: "Get ACCESS-CI events with comprehensive filtering capabilities. Returns events in UTC timezone with enhanced metadata.",
32
32
  inputSchema: {
33
33
  type: "object",
34
34
  properties: {
35
35
  // Relative date filtering
36
36
  beginning_date_relative: {
37
37
  type: "string",
38
- description: "Start date using relative values (today, +1week, -1month, etc.)",
38
+ description: "Start date using relative values. Calculated in UTC by default, or use 'timezone' parameter for local calculations.",
39
39
  enum: [
40
40
  "today",
41
41
  "+1week",
@@ -50,7 +50,7 @@ export class EventsServer extends BaseAccessServer {
50
50
  },
51
51
  end_date_relative: {
52
52
  type: "string",
53
- description: "End date using relative values (today, +1week, -1month, etc.)",
53
+ description: "End date using relative values. Calculated in UTC by default, or use 'timezone' parameter for local calculations.",
54
54
  enum: [
55
55
  "today",
56
56
  "+1week",
@@ -66,11 +66,17 @@ export class EventsServer extends BaseAccessServer {
66
66
  // Absolute date filtering
67
67
  beginning_date: {
68
68
  type: "string",
69
- description: "Start date in YYYY-MM-DD or YYYY-MM-DD HH:MM:SS format",
69
+ description: "Start date in YYYY-MM-DD or YYYY-MM-DD HH:MM:SS format. Always interpreted as provided (no timezone conversion).",
70
70
  },
71
71
  end_date: {
72
72
  type: "string",
73
- description: "End date in YYYY-MM-DD or YYYY-MM-DD HH:MM:SS format",
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",
74
80
  },
75
81
  // Faceted search filters
76
82
  event_type: {
@@ -102,7 +108,7 @@ export class EventsServer extends BaseAccessServer {
102
108
  },
103
109
  {
104
110
  name: "get_upcoming_events",
105
- description: "Get upcoming ACCESS-CI events (today onward)",
111
+ description: "Get upcoming ACCESS-CI events (from today onward in UTC). Convenient shortcut for get_events with beginning_date_relative=today.",
106
112
  inputSchema: {
107
113
  type: "object",
108
114
  properties: {
@@ -114,7 +120,12 @@ export class EventsServer extends BaseAccessServer {
114
120
  },
115
121
  event_type: {
116
122
  type: "string",
117
- description: "Filter by event type (workshop, webinar, etc.)",
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",
118
129
  },
119
130
  },
120
131
  required: [],
@@ -122,19 +133,24 @@ export class EventsServer extends BaseAccessServer {
122
133
  },
123
134
  {
124
135
  name: "search_events",
125
- description: "Search events by keywords in title and description",
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.",
126
137
  inputSchema: {
127
138
  type: "object",
128
139
  properties: {
129
140
  query: {
130
141
  type: "string",
131
- description: "Search query for event titles and descriptions",
142
+ description: "Search query (case-insensitive). Use spaces for multiple words (e.g., 'machine learning', 'office hours'). Searches across all event content including descriptions.",
132
143
  },
133
144
  beginning_date_relative: {
134
145
  type: "string",
135
- description: "Start date using relative values (default: today)",
146
+ description: "Start date using relative values (default: today). Use '-1month' or '-1year' to search past events, or omit for all-time search.",
136
147
  default: "today",
137
148
  },
149
+ timezone: {
150
+ type: "string",
151
+ description: "Timezone for relative date calculation (default: UTC)",
152
+ default: "UTC",
153
+ },
138
154
  limit: {
139
155
  type: "number",
140
156
  description: "Maximum number of events to return (default: 25)",
@@ -147,20 +163,25 @@ export class EventsServer extends BaseAccessServer {
147
163
  },
148
164
  {
149
165
  name: "get_events_by_tag",
150
- description: "Get events filtered by specific tags",
166
+ description: "Get events filtered by specific tags. Useful for finding events on topics like 'python', 'ai', 'machine-learning', 'gpu', etc.",
151
167
  inputSchema: {
152
168
  type: "object",
153
169
  properties: {
154
170
  tag: {
155
171
  type: "string",
156
- description: "Event tag to filter by (e.g., python, machine-learning, gpu)",
172
+ description: "Event tag to filter by. Common tags: python, ai, machine-learning, gpu, deep-learning, neural-networks, big-data, hpc, jetstream, neocortex",
157
173
  },
158
174
  time_range: {
159
175
  type: "string",
160
- description: "Time range for events",
176
+ description: "Time range for events (upcoming=today onward, this_week=next 7 days, this_month=next 30 days, all=no date filter)",
161
177
  enum: ["upcoming", "this_week", "this_month", "all"],
162
178
  default: "upcoming",
163
179
  },
180
+ timezone: {
181
+ type: "string",
182
+ description: "Timezone for time_range calculations (default: UTC)",
183
+ default: "UTC",
184
+ },
164
185
  limit: {
165
186
  type: "number",
166
187
  description: "Maximum number of events to return (default: 25)",
@@ -280,7 +301,11 @@ export class EventsServer extends BaseAccessServer {
280
301
  }
281
302
  }
282
303
  buildEventsUrl(params) {
283
- const url = new URL("/api/2.0/events", this.baseURL);
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
+ }
284
309
  // Add date filtering parameters
285
310
  if (params.beginning_date_relative) {
286
311
  url.searchParams.set("beginning_date_relative", params.beginning_date_relative);
@@ -294,6 +319,10 @@ export class EventsServer extends BaseAccessServer {
294
319
  if (params.end_date) {
295
320
  url.searchParams.set("end_date", params.end_date);
296
321
  }
322
+ // Add timezone parameter for relative date calculations
323
+ if (params.timezone) {
324
+ url.searchParams.set("timezone", params.timezone);
325
+ }
297
326
  // Add faceted search filters
298
327
  let filterIndex = 0;
299
328
  if (params.event_type) {
@@ -349,6 +378,11 @@ export class EventsServer extends BaseAccessServer {
349
378
  upcoming_events: enhancedEvents.filter((e) => e.starts_in_hours >= 0)
350
379
  .length,
351
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
+ },
352
386
  event_types: [
353
387
  ...new Set(enhancedEvents.map((e) => e.event_type).filter(Boolean)),
354
388
  ],
@@ -375,27 +409,30 @@ export class EventsServer extends BaseAccessServer {
375
409
  ...params,
376
410
  beginning_date_relative: "today",
377
411
  limit: params.limit || 50,
412
+ // Pass through timezone if provided
413
+ ...(params.timezone && { timezone: params.timezone }),
378
414
  };
379
415
  return this.getEvents(upcomingParams);
380
416
  }
381
417
  async searchEvents(params) {
418
+ // Use API's native full-text search instead of client-side filtering
382
419
  const searchParams = {
420
+ search_api_fulltext: params.query,
383
421
  beginning_date_relative: params.beginning_date_relative || "today",
384
422
  limit: params.limit || 25,
423
+ // Pass through timezone if provided
424
+ ...(params.timezone && { timezone: params.timezone }),
385
425
  };
386
- // Get events and filter by search query
426
+ // Use the API's native search capabilities
387
427
  const eventsResponse = await this.getEvents(searchParams);
388
428
  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)));
429
+ // API returns already filtered results, no need for client-side filtering
394
430
  const summary = {
395
431
  search_query: params.query,
396
- total_matches: filteredEvents.length,
397
- searched_in: eventsData.total_events,
398
- events: filteredEvents,
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,
399
436
  };
400
437
  return {
401
438
  content: [
@@ -407,7 +444,7 @@ export class EventsServer extends BaseAccessServer {
407
444
  };
408
445
  }
409
446
  async getEventsByTag(params) {
410
- const { tag, time_range = "upcoming", limit = 25 } = params;
447
+ const { tag, time_range = "upcoming", limit = 25, timezone } = params;
411
448
  let dateParams = {};
412
449
  switch (time_range) {
413
450
  case "upcoming":
@@ -429,6 +466,8 @@ export class EventsServer extends BaseAccessServer {
429
466
  ...dateParams,
430
467
  event_tags: tag,
431
468
  limit,
469
+ // Pass through timezone if provided
470
+ ...(timezone && { timezone }),
432
471
  };
433
472
  const eventsResponse = await this.getEvents(taggedParams);
434
473
  const eventsData = JSON.parse(eventsResponse.content[0].text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@access-mcp/events",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
@@ -164,6 +164,88 @@ describe("EventsServer Integration Tests", () => {
164
164
  });
165
165
  }
166
166
  }, 10000);
167
+
168
+ it("should handle timezone parameter with relative dates", async () => {
169
+ const result = await server["handleToolCall"]({
170
+ params: {
171
+ name: "get_events",
172
+ arguments: {
173
+ beginning_date_relative: "today",
174
+ timezone: "America/New_York",
175
+ limit: 5,
176
+ },
177
+ },
178
+ });
179
+
180
+ const responseData = JSON.parse(result.content[0].text);
181
+
182
+ expect(responseData).toHaveProperty("events");
183
+ expect(responseData).toHaveProperty("total_events");
184
+ expect(responseData).toHaveProperty("api_info");
185
+
186
+ // Should successfully handle timezone parameter (v2.1 API feature)
187
+ expect(typeof responseData.total_events).toBe("number");
188
+ expect(responseData.api_info.timezone_used).toBe("America/New_York");
189
+ expect(responseData.api_info.endpoint_version).toBe("2.1");
190
+ }, 10000);
191
+
192
+ it("should handle upcoming events with timezone", async () => {
193
+ const result = await server["handleToolCall"]({
194
+ params: {
195
+ name: "get_upcoming_events",
196
+ arguments: {
197
+ timezone: "Europe/London",
198
+ limit: 3,
199
+ },
200
+ },
201
+ });
202
+
203
+ const responseData = JSON.parse(result.content[0].text);
204
+
205
+ expect(responseData).toHaveProperty("events");
206
+ expect(responseData).toHaveProperty("api_info");
207
+ expect(responseData.api_info.timezone_used).toBe("Europe/London");
208
+ }, 10000);
209
+
210
+ it("should handle search with Pacific timezone", async () => {
211
+ const result = await server["handleToolCall"]({
212
+ params: {
213
+ name: "search_events",
214
+ arguments: {
215
+ query: "office",
216
+ timezone: "America/Los_Angeles",
217
+ limit: 2,
218
+ },
219
+ },
220
+ });
221
+
222
+ const responseData = JSON.parse(result.content[0].text);
223
+
224
+ expect(responseData).toHaveProperty("search_query");
225
+ expect(responseData.search_query).toBe("office");
226
+ expect(responseData).toHaveProperty("total_matches");
227
+ }, 10000);
228
+
229
+ it("should handle events by tag with timezone", async () => {
230
+ const result = await server["handleToolCall"]({
231
+ params: {
232
+ name: "get_events_by_tag",
233
+ arguments: {
234
+ tag: "ai",
235
+ time_range: "this_week",
236
+ timezone: "Asia/Tokyo",
237
+ limit: 3,
238
+ },
239
+ },
240
+ });
241
+
242
+ const responseData = JSON.parse(result.content[0].text);
243
+
244
+ expect(responseData).toHaveProperty("tag");
245
+ expect(responseData.tag).toBe("ai");
246
+ expect(responseData).toHaveProperty("time_range");
247
+ expect(responseData.time_range).toBe("this_week");
248
+ }, 10000);
167
249
  });
168
250
 
169
251
  describe("Error Handling with Real API", () => {
@@ -32,7 +32,7 @@ describe("EventsServer", () => {
32
32
  it("should initialize with correct server name and version", () => {
33
33
  expect(server).toBeDefined();
34
34
  expect(server["serverName"]).toBe("access-mcp-events");
35
- expect(server["version"]).toBe("0.1.0");
35
+ expect(server["version"]).toBe("0.2.0");
36
36
  expect(server["baseURL"]).toBe("https://support.access-ci.org");
37
37
  });
38
38
 
@@ -64,6 +64,11 @@ describe("EventsServer", () => {
64
64
  });
65
65
 
66
66
  describe("URL Building", () => {
67
+ it("should build correct URLs with v2.1 endpoint", () => {
68
+ const url = server["buildEventsUrl"]({});
69
+ expect(url).toContain("/api/2.1/events");
70
+ });
71
+
67
72
  it("should build correct URLs with relative date filters", () => {
68
73
  const url = server["buildEventsUrl"]({
69
74
  beginning_date_relative: "today",
@@ -74,6 +79,16 @@ describe("EventsServer", () => {
74
79
  expect(url).toContain("end_date_relative=%2B1week");
75
80
  });
76
81
 
82
+ it("should build correct URLs with timezone parameter", () => {
83
+ const url = server["buildEventsUrl"]({
84
+ beginning_date_relative: "today",
85
+ timezone: "America/New_York",
86
+ });
87
+
88
+ expect(url).toContain("beginning_date_relative=today");
89
+ expect(url).toContain("timezone=America%2FNew_York");
90
+ });
91
+
77
92
  it("should build correct URLs with absolute date filters", () => {
78
93
  const url = server["buildEventsUrl"]({
79
94
  beginning_date: "2024-01-01",
@@ -111,6 +126,66 @@ describe("EventsServer", () => {
111
126
  expect(url).toContain("f%5B0%5D=custom_event_type%3Awebinar");
112
127
  expect(url).toContain("f%5B1%5D=skill_level%3Aintermediate");
113
128
  });
129
+
130
+ it("should build correct URLs with timezone and mixed parameters", () => {
131
+ const url = server["buildEventsUrl"]({
132
+ beginning_date_relative: "today",
133
+ end_date_relative: "+1month",
134
+ timezone: "Europe/Berlin",
135
+ event_type: "workshop",
136
+ });
137
+
138
+ expect(url).toContain("beginning_date_relative=today");
139
+ expect(url).toContain("end_date_relative=%2B1month");
140
+ expect(url).toContain("timezone=Europe%2FBerlin");
141
+ expect(url).toContain("f%5B0%5D=custom_event_type%3Aworkshop");
142
+ });
143
+
144
+ it("should not include timezone parameter when not provided", () => {
145
+ const url = server["buildEventsUrl"]({
146
+ beginning_date_relative: "today",
147
+ });
148
+
149
+ expect(url).toContain("beginning_date_relative=today");
150
+ expect(url).not.toContain("timezone=");
151
+ });
152
+
153
+ it("should handle various timezone formats", () => {
154
+ const timezones = [
155
+ "UTC",
156
+ "America/New_York",
157
+ "America/Los_Angeles",
158
+ "Europe/London",
159
+ "Asia/Tokyo"
160
+ ];
161
+
162
+ timezones.forEach(tz => {
163
+ const url = server["buildEventsUrl"]({
164
+ beginning_date_relative: "today",
165
+ timezone: tz,
166
+ });
167
+ expect(url).toContain(`timezone=${encodeURIComponent(tz)}`);
168
+ });
169
+ });
170
+
171
+ it("should include search_api_fulltext parameter", () => {
172
+ const url = server["buildEventsUrl"]({
173
+ search_api_fulltext: "python machine learning",
174
+ beginning_date_relative: "today",
175
+ });
176
+
177
+ expect(url).toContain("search_api_fulltext=python+machine+learning");
178
+ expect(url).toContain("beginning_date_relative=today");
179
+ });
180
+
181
+ it("should not include search parameter when not provided", () => {
182
+ const url = server["buildEventsUrl"]({
183
+ beginning_date_relative: "today",
184
+ });
185
+
186
+ expect(url).not.toContain("search_api_fulltext");
187
+ expect(url).toContain("beginning_date_relative=today");
188
+ });
114
189
  });
115
190
 
116
191
  describe("Tool Methods", () => {
@@ -273,6 +348,68 @@ describe("EventsServer", () => {
273
348
  expect(responseData.popular_tags).toContain("python");
274
349
  expect(responseData.popular_tags).toContain("machine-learning");
275
350
  });
351
+
352
+ it("should include timezone parameter in URL when provided", async () => {
353
+ mockHttpClient.get.mockResolvedValue({
354
+ status: 200,
355
+ data: mockEventsData,
356
+ });
357
+
358
+ const result = await server["handleToolCall"]({
359
+ params: {
360
+ name: "get_events",
361
+ arguments: {
362
+ beginning_date_relative: "today",
363
+ timezone: "America/New_York",
364
+ },
365
+ },
366
+ });
367
+
368
+ const calledUrl = mockHttpClient.get.mock.calls[0][0];
369
+ expect(calledUrl).toContain("timezone=America%2FNew_York");
370
+ expect(calledUrl).toContain("beginning_date_relative=today");
371
+ });
372
+
373
+ it("should include API info with timezone used", async () => {
374
+ mockHttpClient.get.mockResolvedValue({
375
+ status: 200,
376
+ data: mockEventsData,
377
+ });
378
+
379
+ const result = await server["handleToolCall"]({
380
+ params: {
381
+ name: "get_events",
382
+ arguments: {
383
+ beginning_date_relative: "today",
384
+ timezone: "Europe/London",
385
+ },
386
+ },
387
+ });
388
+
389
+ const responseData = JSON.parse(result.content[0].text);
390
+ expect(responseData.api_info).toBeDefined();
391
+ expect(responseData.api_info.endpoint_version).toBe("2.1");
392
+ expect(responseData.api_info.timezone_used).toBe("Europe/London");
393
+ });
394
+
395
+ it("should default to UTC timezone when not specified", async () => {
396
+ mockHttpClient.get.mockResolvedValue({
397
+ status: 200,
398
+ data: mockEventsData,
399
+ });
400
+
401
+ const result = await server["handleToolCall"]({
402
+ params: {
403
+ name: "get_events",
404
+ arguments: {
405
+ beginning_date_relative: "today",
406
+ },
407
+ },
408
+ });
409
+
410
+ const responseData = JSON.parse(result.content[0].text);
411
+ expect(responseData.api_info.timezone_used).toBe("UTC");
412
+ });
276
413
  });
277
414
 
278
415
  describe("get_upcoming_events", () => {
@@ -314,6 +451,27 @@ describe("EventsServer", () => {
314
451
  const calledUrl = mockHttpClient.get.mock.calls[0][0];
315
452
  expect(calledUrl).toContain("custom_event_type%3Awebinar");
316
453
  });
454
+
455
+ it("should pass timezone parameter to get_events", async () => {
456
+ mockHttpClient.get.mockResolvedValue({
457
+ status: 200,
458
+ data: mockEventsData,
459
+ });
460
+
461
+ const result = await server["handleToolCall"]({
462
+ params: {
463
+ name: "get_upcoming_events",
464
+ arguments: {
465
+ timezone: "America/Chicago",
466
+ limit: 5,
467
+ },
468
+ },
469
+ });
470
+
471
+ const calledUrl = mockHttpClient.get.mock.calls[0][0];
472
+ expect(calledUrl).toContain("beginning_date_relative=today");
473
+ expect(calledUrl).toContain("timezone=America%2FChicago");
474
+ });
317
475
  });
318
476
 
319
477
  describe("search_events", () => {
@@ -338,7 +496,7 @@ describe("EventsServer", () => {
338
496
  expect(responseData.events[0].title).toContain("Python");
339
497
  });
340
498
 
341
- it("should search events by query in description", async () => {
499
+ it("should use API native search instead of client-side filtering", async () => {
342
500
  mockHttpClient.get.mockResolvedValue({
343
501
  status: 200,
344
502
  data: mockEventsData,
@@ -353,12 +511,16 @@ describe("EventsServer", () => {
353
511
  },
354
512
  });
355
513
 
514
+ const calledUrl = mockHttpClient.get.mock.calls[0][0];
515
+ expect(calledUrl).toContain("search_api_fulltext=ML");
516
+
356
517
  const responseData = JSON.parse(result.content[0].text);
357
- expect(responseData.total_matches).toBe(1);
358
- expect(responseData.events[0].description).toContain("ML");
518
+ expect(responseData.search_query).toBe("ML");
519
+ expect(responseData.search_method).toBe("API native full-text search");
520
+ expect(responseData.total_matches).toBe(mockEventsData.length); // API returns raw count
359
521
  });
360
522
 
361
- it("should search events by query in tags", async () => {
523
+ it("should include search scope information", async () => {
362
524
  mockHttpClient.get.mockResolvedValue({
363
525
  status: 200,
364
526
  data: mockEventsData,
@@ -373,8 +535,11 @@ describe("EventsServer", () => {
373
535
  },
374
536
  });
375
537
 
538
+ const calledUrl = mockHttpClient.get.mock.calls[0][0];
539
+ expect(calledUrl).toContain("search_api_fulltext=machine-learning");
540
+
376
541
  const responseData = JSON.parse(result.content[0].text);
377
- expect(responseData.total_matches).toBe(1);
542
+ expect(responseData.search_scope).toBe("titles, descriptions, speakers, tags, location, event type");
378
543
  });
379
544
 
380
545
  it("should search with custom date range", async () => {
@@ -397,6 +562,27 @@ describe("EventsServer", () => {
397
562
  const calledUrl = mockHttpClient.get.mock.calls[0][0];
398
563
  expect(calledUrl).toContain("beginning_date_relative=-1month");
399
564
  });
565
+
566
+ it("should include timezone parameter in search", async () => {
567
+ mockHttpClient.get.mockResolvedValue({
568
+ status: 200,
569
+ data: mockEventsData,
570
+ });
571
+
572
+ const result = await server["handleToolCall"]({
573
+ params: {
574
+ name: "search_events",
575
+ arguments: {
576
+ query: "python",
577
+ timezone: "Asia/Tokyo",
578
+ },
579
+ },
580
+ });
581
+
582
+ const calledUrl = mockHttpClient.get.mock.calls[0][0];
583
+ expect(calledUrl).toContain("timezone=Asia%2FTokyo");
584
+ expect(calledUrl).toContain("beginning_date_relative=today");
585
+ });
400
586
  });
401
587
 
402
588
  describe("get_events_by_tag", () => {
@@ -487,6 +673,30 @@ describe("EventsServer", () => {
487
673
  expect(calledUrl).not.toContain("beginning_date_relative");
488
674
  expect(calledUrl).not.toContain("end_date_relative");
489
675
  });
676
+
677
+ it("should include timezone parameter for time-based ranges", async () => {
678
+ mockHttpClient.get.mockResolvedValue({
679
+ status: 200,
680
+ data: mockEventsData,
681
+ });
682
+
683
+ const result = await server["handleToolCall"]({
684
+ params: {
685
+ name: "get_events_by_tag",
686
+ arguments: {
687
+ tag: "ai",
688
+ time_range: "this_week",
689
+ timezone: "Australia/Sydney",
690
+ },
691
+ },
692
+ });
693
+
694
+ const calledUrl = mockHttpClient.get.mock.calls[0][0];
695
+ expect(calledUrl).toContain("custom_event_tags%3Aai");
696
+ expect(calledUrl).toContain("beginning_date_relative=today");
697
+ expect(calledUrl).toContain("end_date_relative=%2B1week");
698
+ expect(calledUrl).toContain("timezone=Australia%2FSydney");
699
+ });
490
700
  });
491
701
 
492
702
  describe("Error Handling", () => {
package/src/server.ts CHANGED
@@ -5,7 +5,7 @@ export class EventsServer extends BaseAccessServer {
5
5
  private _eventsHttpClient?: AxiosInstance;
6
6
 
7
7
  constructor() {
8
- super("access-mcp-events", "0.1.0", "https://support.access-ci.org");
8
+ super("access-mcp-events", "0.2.0", "https://support.access-ci.org");
9
9
  }
10
10
 
11
11
  protected get httpClient(): AxiosInstance {
@@ -35,7 +35,7 @@ export class EventsServer extends BaseAccessServer {
35
35
  {
36
36
  name: "get_events",
37
37
  description:
38
- "Get ACCESS-CI events with comprehensive filtering capabilities",
38
+ "Get ACCESS-CI events with comprehensive filtering capabilities. Returns events in UTC timezone with enhanced metadata.",
39
39
  inputSchema: {
40
40
  type: "object",
41
41
  properties: {
@@ -43,7 +43,7 @@ export class EventsServer extends BaseAccessServer {
43
43
  beginning_date_relative: {
44
44
  type: "string",
45
45
  description:
46
- "Start date using relative values (today, +1week, -1month, etc.)",
46
+ "Start date using relative values. Calculated in UTC by default, or use 'timezone' parameter for local calculations.",
47
47
  enum: [
48
48
  "today",
49
49
  "+1week",
@@ -59,7 +59,7 @@ export class EventsServer extends BaseAccessServer {
59
59
  end_date_relative: {
60
60
  type: "string",
61
61
  description:
62
- "End date using relative values (today, +1week, -1month, etc.)",
62
+ "End date using relative values. Calculated in UTC by default, or use 'timezone' parameter for local calculations.",
63
63
  enum: [
64
64
  "today",
65
65
  "+1week",
@@ -76,12 +76,19 @@ export class EventsServer extends BaseAccessServer {
76
76
  beginning_date: {
77
77
  type: "string",
78
78
  description:
79
- "Start date in YYYY-MM-DD or YYYY-MM-DD HH:MM:SS format",
79
+ "Start date in YYYY-MM-DD or YYYY-MM-DD HH:MM:SS format. Always interpreted as provided (no timezone conversion).",
80
80
  },
81
81
  end_date: {
82
82
  type: "string",
83
83
  description:
84
- "End date in YYYY-MM-DD or YYYY-MM-DD HH:MM:SS format",
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",
85
92
  },
86
93
  // Faceted search filters
87
94
  event_type: {
@@ -116,7 +123,7 @@ export class EventsServer extends BaseAccessServer {
116
123
  },
117
124
  {
118
125
  name: "get_upcoming_events",
119
- description: "Get upcoming ACCESS-CI events (today onward)",
126
+ description: "Get upcoming ACCESS-CI events (from today onward in UTC). Convenient shortcut for get_events with beginning_date_relative=today.",
120
127
  inputSchema: {
121
128
  type: "object",
122
129
  properties: {
@@ -128,7 +135,12 @@ export class EventsServer extends BaseAccessServer {
128
135
  },
129
136
  event_type: {
130
137
  type: "string",
131
- description: "Filter by event type (workshop, webinar, etc.)",
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",
132
144
  },
133
145
  },
134
146
  required: [],
@@ -136,19 +148,24 @@ export class EventsServer extends BaseAccessServer {
136
148
  },
137
149
  {
138
150
  name: "search_events",
139
- description: "Search events by keywords in title and description",
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.",
140
152
  inputSchema: {
141
153
  type: "object",
142
154
  properties: {
143
155
  query: {
144
156
  type: "string",
145
- description: "Search query for event titles and descriptions",
157
+ description: "Search query (case-insensitive). Use spaces for multiple words (e.g., 'machine learning', 'office hours'). Searches across all event content including descriptions.",
146
158
  },
147
159
  beginning_date_relative: {
148
160
  type: "string",
149
- description: "Start date using relative values (default: today)",
161
+ description: "Start date using relative values (default: today). Use '-1month' or '-1year' to search past events, or omit for all-time search.",
150
162
  default: "today",
151
163
  },
164
+ timezone: {
165
+ type: "string",
166
+ description: "Timezone for relative date calculation (default: UTC)",
167
+ default: "UTC",
168
+ },
152
169
  limit: {
153
170
  type: "number",
154
171
  description: "Maximum number of events to return (default: 25)",
@@ -161,21 +178,26 @@ export class EventsServer extends BaseAccessServer {
161
178
  },
162
179
  {
163
180
  name: "get_events_by_tag",
164
- description: "Get events filtered by specific tags",
181
+ description: "Get events filtered by specific tags. Useful for finding events on topics like 'python', 'ai', 'machine-learning', 'gpu', etc.",
165
182
  inputSchema: {
166
183
  type: "object",
167
184
  properties: {
168
185
  tag: {
169
186
  type: "string",
170
187
  description:
171
- "Event tag to filter by (e.g., python, machine-learning, gpu)",
188
+ "Event tag to filter by. Common tags: python, ai, machine-learning, gpu, deep-learning, neural-networks, big-data, hpc, jetstream, neocortex",
172
189
  },
173
190
  time_range: {
174
191
  type: "string",
175
- description: "Time range for events",
192
+ description: "Time range for events (upcoming=today onward, this_week=next 7 days, this_month=next 30 days, all=no date filter)",
176
193
  enum: ["upcoming", "this_week", "this_month", "all"],
177
194
  default: "upcoming",
178
195
  },
196
+ timezone: {
197
+ type: "string",
198
+ description: "Timezone for time_range calculations (default: UTC)",
199
+ default: "UTC",
200
+ },
179
201
  limit: {
180
202
  type: "number",
181
203
  description: "Maximum number of events to return (default: 25)",
@@ -301,7 +323,12 @@ export class EventsServer extends BaseAccessServer {
301
323
  }
302
324
 
303
325
  private buildEventsUrl(params: any): string {
304
- const url = new URL("/api/2.0/events", this.baseURL);
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
+ }
305
332
 
306
333
  // Add date filtering parameters
307
334
  if (params.beginning_date_relative) {
@@ -320,6 +347,11 @@ export class EventsServer extends BaseAccessServer {
320
347
  url.searchParams.set("end_date", params.end_date);
321
348
  }
322
349
 
350
+ // Add timezone parameter for relative date calculations
351
+ if (params.timezone) {
352
+ url.searchParams.set("timezone", params.timezone);
353
+ }
354
+
323
355
  // Add faceted search filters
324
356
  let filterIndex = 0;
325
357
  if (params.event_type) {
@@ -405,6 +437,11 @@ export class EventsServer extends BaseAccessServer {
405
437
  events_this_week: enhancedEvents.filter(
406
438
  (e: any) => e.starts_in_hours <= 168 && e.starts_in_hours >= 0,
407
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
+ },
408
445
  event_types: [
409
446
  ...new Set(
410
447
  enhancedEvents.map((e: any) => e.event_type).filter(Boolean),
@@ -439,35 +476,34 @@ export class EventsServer extends BaseAccessServer {
439
476
  ...params,
440
477
  beginning_date_relative: "today",
441
478
  limit: params.limit || 50,
479
+ // Pass through timezone if provided
480
+ ...(params.timezone && { timezone: params.timezone }),
442
481
  };
443
482
 
444
483
  return this.getEvents(upcomingParams);
445
484
  }
446
485
 
447
486
  private async searchEvents(params: any) {
487
+ // Use API's native full-text search instead of client-side filtering
448
488
  const searchParams = {
489
+ search_api_fulltext: params.query,
449
490
  beginning_date_relative: params.beginning_date_relative || "today",
450
491
  limit: params.limit || 25,
492
+ // Pass through timezone if provided
493
+ ...(params.timezone && { timezone: params.timezone }),
451
494
  };
452
495
 
453
- // Get events and filter by search query
496
+ // Use the API's native search capabilities
454
497
  const eventsResponse = await this.getEvents(searchParams);
455
498
  const eventsData = JSON.parse(eventsResponse.content[0].text);
456
499
 
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
-
500
+ // API returns already filtered results, no need for client-side filtering
466
501
  const summary = {
467
502
  search_query: params.query,
468
- total_matches: filteredEvents.length,
469
- searched_in: eventsData.total_events,
470
- events: filteredEvents,
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,
471
507
  };
472
508
 
473
509
  return {
@@ -481,7 +517,7 @@ export class EventsServer extends BaseAccessServer {
481
517
  }
482
518
 
483
519
  private async getEventsByTag(params: any) {
484
- const { tag, time_range = "upcoming", limit = 25 } = params;
520
+ const { tag, time_range = "upcoming", limit = 25, timezone } = params;
485
521
 
486
522
  let dateParams: any = {};
487
523
  switch (time_range) {
@@ -505,6 +541,8 @@ export class EventsServer extends BaseAccessServer {
505
541
  ...dateParams,
506
542
  event_tags: tag,
507
543
  limit,
544
+ // Pass through timezone if provided
545
+ ...(timezone && { timezone }),
508
546
  };
509
547
 
510
548
  const eventsResponse = await this.getEvents(taggedParams);