@access-mcp/events 0.3.0 → 0.3.1
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 +42 -234
- package/dist/server.d.ts +22 -56
- package/dist/server.js +163 -42
- package/package.json +2 -2
- package/src/__tests__/server.integration.test.ts +23 -8
- package/src/__tests__/server.test.ts +29 -22
- package/src/server.ts +251 -63
package/README.md
CHANGED
|
@@ -1,266 +1,74 @@
|
|
|
1
1
|
# ACCESS-CI Events MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
### 🔧 Tools
|
|
8
|
-
|
|
9
|
-
- **`search_events`** - Comprehensive event search and filtering with native full-text search, date filters, topic/tag filtering, and more
|
|
10
|
-
|
|
11
|
-
### 📊 Resources
|
|
12
|
-
|
|
13
|
-
- **`accessci://events`** - All events data
|
|
14
|
-
- **`accessci://events/upcoming`** - Upcoming events only
|
|
15
|
-
- **`accessci://events/workshops`** - Workshop events only
|
|
16
|
-
- **`accessci://events/webinars`** - Webinar events only
|
|
17
|
-
|
|
18
|
-
## Installation
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
npm install -g @access-mcp/events
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Usage
|
|
25
|
-
|
|
26
|
-
### Claude Desktop Configuration
|
|
27
|
-
|
|
28
|
-
Add to your Claude Desktop configuration:
|
|
29
|
-
|
|
30
|
-
```json
|
|
31
|
-
{
|
|
32
|
-
"mcpServers": {
|
|
33
|
-
"access-events": {
|
|
34
|
-
"command": "npx",
|
|
35
|
-
"args": ["@access-mcp/events"]
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
```
|
|
3
|
+
MCP server for ACCESS-CI events including workshops, webinars, and training sessions.
|
|
40
4
|
|
|
41
5
|
## Usage Examples
|
|
42
6
|
|
|
43
|
-
###
|
|
44
|
-
|
|
7
|
+
### Search & Discovery
|
|
45
8
|
```
|
|
46
|
-
"Upcoming events
|
|
9
|
+
"Upcoming ACCESS events"
|
|
47
10
|
"Python workshops"
|
|
48
|
-
"Machine learning
|
|
11
|
+
"Machine learning training"
|
|
49
12
|
"Office hours this week"
|
|
50
13
|
```
|
|
51
14
|
|
|
52
|
-
###
|
|
53
|
-
|
|
15
|
+
### Filtering
|
|
54
16
|
```
|
|
55
|
-
"Beginner
|
|
56
|
-
"GPU computing
|
|
57
|
-
"Webinars in December"
|
|
17
|
+
"Beginner events this month"
|
|
18
|
+
"GPU computing workshops"
|
|
58
19
|
"Advanced training sessions"
|
|
59
20
|
```
|
|
60
21
|
|
|
61
|
-
##
|
|
62
|
-
|
|
63
|
-
### 🚀 Enhanced Search (v2.1)
|
|
64
|
-
|
|
65
|
-
**Native API Full-Text Search:**
|
|
66
|
-
- Searches across titles, descriptions, speakers, tags, location, and event type
|
|
67
|
-
- Supports multi-word queries (e.g., "machine learning", "office hours")
|
|
68
|
-
- Much more comprehensive results than previous tag-only filtering
|
|
69
|
-
- Server-side indexing for better performance
|
|
70
|
-
|
|
71
|
-
**Search Examples:**
|
|
72
|
-
- `"python"` - Find Python programming events
|
|
73
|
-
- `"machine learning"` - Find ML-related content in any field
|
|
74
|
-
- `"gpu computing"` - Find GPU-related events
|
|
75
|
-
- `"office hours"` - Find all office hours sessions
|
|
76
|
-
|
|
77
|
-
### 🌍 Timezone Support (v2.1)
|
|
78
|
-
|
|
79
|
-
**Smart Timezone Handling:**
|
|
80
|
-
- All timestamps returned in UTC (ISO 8601 format with Z suffix)
|
|
81
|
-
- Timezone parameter controls relative date calculations
|
|
82
|
-
- Common timezone examples: `America/New_York`, `Europe/London`, `Asia/Tokyo`
|
|
83
|
-
- Default: UTC calculations
|
|
84
|
-
|
|
85
|
-
## Filtering Capabilities
|
|
86
|
-
|
|
87
|
-
### Date Filtering
|
|
88
|
-
|
|
89
|
-
**Relative Dates (Dynamic):**
|
|
90
|
-
|
|
91
|
-
- `today` - Current date
|
|
92
|
-
- `+1week`, `+2week` - Future weeks
|
|
93
|
-
- `+1month`, `+2month` - Future months
|
|
94
|
-
- `+1year` - Future year
|
|
95
|
-
- `-1week`, `-1month` - Past periods
|
|
96
|
-
|
|
97
|
-
**Absolute Dates (Fixed):**
|
|
98
|
-
|
|
99
|
-
- `YYYY-MM-DD` format (e.g., "2024-08-30")
|
|
100
|
-
- `YYYY-MM-DD HH:MM:SS` format with time
|
|
101
|
-
|
|
102
|
-
**Mixed Filtering:**
|
|
103
|
-
You can combine relative and absolute dates in the same query.
|
|
104
|
-
|
|
105
|
-
### Faceted Search Filters
|
|
22
|
+
## Tools
|
|
106
23
|
|
|
107
|
-
|
|
108
|
-
- **Event Affiliation:** Community, ACCESS, etc.
|
|
109
|
-
- **Skill Level:** beginner, intermediate, advanced
|
|
110
|
-
- **Event Tags:** python, big-data, machine-learning, gpu, etc.
|
|
24
|
+
### `search_events`
|
|
111
25
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
### Event Object Structure
|
|
115
|
-
|
|
116
|
-
Each event contains:
|
|
117
|
-
|
|
118
|
-
- `id` - Unique identifier
|
|
119
|
-
- `title` - Event title
|
|
120
|
-
- `description` - Event description
|
|
121
|
-
- `date` - Start date/time (ISO 8601)
|
|
122
|
-
- `date_1` - End date/time (ISO 8601)
|
|
123
|
-
- `location` - Event location
|
|
124
|
-
- `event_type` - Type of event
|
|
125
|
-
- `event_affiliation` - Organizational affiliation
|
|
126
|
-
- `custom_event_tags` - Comma-separated tags
|
|
127
|
-
- `skill_level` - Required skill level
|
|
128
|
-
- `speakers` - Event speakers
|
|
129
|
-
- `contact` - Contact information
|
|
130
|
-
- `registration` - Registration URL/info
|
|
131
|
-
- `created` - Creation timestamp
|
|
132
|
-
- `changed` - Last modified timestamp
|
|
133
|
-
|
|
134
|
-
### Enhanced Fields
|
|
135
|
-
|
|
136
|
-
The server adds these computed fields:
|
|
137
|
-
|
|
138
|
-
- `start_date` - Parsed start date object
|
|
139
|
-
- `end_date` - Parsed end date object
|
|
140
|
-
- `tags` - Tags split into array
|
|
141
|
-
- `duration_hours` - Calculated event duration
|
|
142
|
-
- `starts_in_hours` - Hours until event starts
|
|
143
|
-
|
|
144
|
-
## Tool Reference
|
|
145
|
-
|
|
146
|
-
### search_events
|
|
147
|
-
|
|
148
|
-
Comprehensive event search and filtering with native full-text search, date filters, and topic/tag filtering.
|
|
26
|
+
Search and filter ACCESS-CI events.
|
|
149
27
|
|
|
150
28
|
**Parameters:**
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
29
|
+
| Parameter | Type | Description |
|
|
30
|
+
|-----------|------|-------------|
|
|
31
|
+
| `query` | string | Search titles, descriptions, speakers, tags |
|
|
32
|
+
| `type` | string | Filter: `workshop`, `webinar`, `training` |
|
|
33
|
+
| `tags` | string | Filter: `python`, `gpu`, `hpc`, `ml` |
|
|
34
|
+
| `date` | enum | Time period: `today`, `upcoming`, `past`, `this_week`, `this_month` |
|
|
35
|
+
| `skill` | enum | Skill level: `beginner`, `intermediate`, `advanced` |
|
|
36
|
+
| `limit` | number | Max results (default: 50) |
|
|
37
|
+
|
|
38
|
+
**Examples:**
|
|
162
39
|
```javascript
|
|
163
40
|
// Upcoming Python events
|
|
164
|
-
search_events({
|
|
165
|
-
query: "python",
|
|
166
|
-
date: "upcoming",
|
|
167
|
-
limit: 10
|
|
168
|
-
})
|
|
41
|
+
search_events({ query: "python", date: "upcoming", limit: 10 })
|
|
169
42
|
|
|
170
43
|
// Machine learning workshops this month
|
|
171
|
-
search_events({
|
|
172
|
-
query: "machine learning",
|
|
173
|
-
date: "this_month",
|
|
174
|
-
type: "workshop",
|
|
175
|
-
limit: 25
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
// Past office hours
|
|
179
|
-
search_events({
|
|
180
|
-
query: "office hours",
|
|
181
|
-
date: "past",
|
|
182
|
-
limit: 30
|
|
183
|
-
})
|
|
184
|
-
```
|
|
44
|
+
search_events({ query: "machine learning", date: "this_month", type: "workshop" })
|
|
185
45
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
```javascript
|
|
189
|
-
// All upcoming GPU events
|
|
190
|
-
search_events({
|
|
191
|
-
tags: "gpu",
|
|
192
|
-
date: "upcoming",
|
|
193
|
-
limit: 20
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
// Beginner training sessions
|
|
197
|
-
search_events({
|
|
198
|
-
type: "training",
|
|
199
|
-
skill: "beginner",
|
|
200
|
-
date: "upcoming",
|
|
201
|
-
limit: 15
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
// Python workshops for beginners
|
|
205
|
-
search_events({
|
|
206
|
-
date: "this_month",
|
|
207
|
-
type: "workshop",
|
|
208
|
-
skill: "beginner",
|
|
209
|
-
tags: "python",
|
|
210
|
-
limit: 25
|
|
211
|
-
})
|
|
46
|
+
// Beginner GPU training
|
|
47
|
+
search_events({ tags: "gpu", skill: "beginner", date: "upcoming" })
|
|
212
48
|
```
|
|
213
49
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
```javascript
|
|
217
|
-
// Get events in New York timezone
|
|
218
|
-
search_events({
|
|
219
|
-
beginning_date_relative: "today",
|
|
220
|
-
end_date_relative: "+1week",
|
|
221
|
-
timezone: "America/New_York",
|
|
222
|
-
limit: 30
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
// Get events in Europe/London timezone
|
|
226
|
-
search_events({
|
|
227
|
-
query: "workshop",
|
|
228
|
-
beginning_date_relative: "today",
|
|
229
|
-
timezone: "Europe/London",
|
|
230
|
-
limit: 25
|
|
231
|
-
})
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
## Development
|
|
50
|
+
## Installation
|
|
235
51
|
|
|
236
52
|
```bash
|
|
237
|
-
|
|
238
|
-
npm run build
|
|
239
|
-
|
|
240
|
-
# Run in development
|
|
241
|
-
npm run dev
|
|
242
|
-
|
|
243
|
-
# Test the server
|
|
244
|
-
npm test
|
|
53
|
+
npm install -g @access-mcp/events
|
|
245
54
|
```
|
|
246
55
|
|
|
247
|
-
##
|
|
56
|
+
## Configuration
|
|
248
57
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"mcpServers": {
|
|
61
|
+
"access-events": {
|
|
62
|
+
"command": "npx",
|
|
63
|
+
"args": ["@access-mcp/events"]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
252
68
|
|
|
253
|
-
|
|
254
|
-
- **UTC timestamps**: All dates returned in UTC with Z suffix (e.g., `2024-08-30T13:00:00Z`)
|
|
255
|
-
- **Native search**: Uses `search_api_fulltext` parameter for comprehensive searching
|
|
256
|
-
- **Timezone support**: Relative dates calculated using specified timezone
|
|
257
|
-
- **Pagination**: API accepts specific page sizes (25, 50, 75, or 100). Requested limits are automatically rounded to nearest valid size.
|
|
258
|
-
- **Enhanced metadata**: Responses include API version and timezone info
|
|
69
|
+
## Resources
|
|
259
70
|
|
|
260
|
-
|
|
261
|
-
-
|
|
262
|
-
-
|
|
263
|
-
-
|
|
264
|
-
- Response times are typically under 5 seconds
|
|
265
|
-
- No pagination - all matching events are returned
|
|
266
|
-
- URL encoding is handled automatically for special characters
|
|
71
|
+
- `accessci://events` - All events data
|
|
72
|
+
- `accessci://events/upcoming` - Upcoming events
|
|
73
|
+
- `accessci://events/workshops` - Workshop events
|
|
74
|
+
- `accessci://events/webinars` - Webinar events
|
package/dist/server.d.ts
CHANGED
|
@@ -1,65 +1,31 @@
|
|
|
1
|
-
import { BaseAccessServer } from "@access-mcp/shared";
|
|
1
|
+
import { BaseAccessServer, Tool, Resource, CallToolResult } from "@access-mcp/shared";
|
|
2
|
+
import { CallToolRequest, ReadResourceRequest, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
3
|
import { AxiosInstance } from "axios";
|
|
3
4
|
export declare class EventsServer extends BaseAccessServer {
|
|
4
5
|
private _eventsHttpClient?;
|
|
6
|
+
private drupalAuth?;
|
|
5
7
|
constructor();
|
|
8
|
+
/**
|
|
9
|
+
* Get or create the Drupal auth provider for authenticated operations.
|
|
10
|
+
* Requires DRUPAL_API_URL, DRUPAL_USERNAME, and DRUPAL_PASSWORD env vars.
|
|
11
|
+
*/
|
|
12
|
+
private getDrupalAuth;
|
|
13
|
+
/**
|
|
14
|
+
* Get the acting user's ACCESS ID for filtering.
|
|
15
|
+
*/
|
|
16
|
+
private getActingUserAccessId;
|
|
6
17
|
protected get httpClient(): AxiosInstance;
|
|
7
|
-
protected getTools():
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
type: string;
|
|
12
|
-
properties: {
|
|
13
|
-
query: {
|
|
14
|
-
type: string;
|
|
15
|
-
description: string;
|
|
16
|
-
};
|
|
17
|
-
type: {
|
|
18
|
-
type: string;
|
|
19
|
-
description: string;
|
|
20
|
-
};
|
|
21
|
-
tags: {
|
|
22
|
-
type: string;
|
|
23
|
-
description: string;
|
|
24
|
-
};
|
|
25
|
-
date: {
|
|
26
|
-
type: string;
|
|
27
|
-
description: string;
|
|
28
|
-
enum: string[];
|
|
29
|
-
};
|
|
30
|
-
skill: {
|
|
31
|
-
type: string;
|
|
32
|
-
description: string;
|
|
33
|
-
enum: string[];
|
|
34
|
-
};
|
|
35
|
-
limit: {
|
|
36
|
-
type: string;
|
|
37
|
-
description: string;
|
|
38
|
-
default: number;
|
|
39
|
-
};
|
|
40
|
-
};
|
|
41
|
-
};
|
|
42
|
-
}[];
|
|
43
|
-
protected getResources(): {
|
|
44
|
-
uri: string;
|
|
45
|
-
name: string;
|
|
46
|
-
description: string;
|
|
47
|
-
mimeType: string;
|
|
48
|
-
}[];
|
|
49
|
-
handleToolCall(request: any): Promise<{
|
|
50
|
-
content: {
|
|
51
|
-
type: string;
|
|
52
|
-
text: string;
|
|
53
|
-
}[];
|
|
54
|
-
}>;
|
|
55
|
-
handleResourceRead(request: any): Promise<{
|
|
56
|
-
contents: {
|
|
57
|
-
uri: string;
|
|
58
|
-
mimeType: string;
|
|
59
|
-
text: string;
|
|
60
|
-
}[];
|
|
61
|
-
}>;
|
|
18
|
+
protected getTools(): Tool[];
|
|
19
|
+
protected getResources(): Resource[];
|
|
20
|
+
protected handleToolCall(request: CallToolRequest): Promise<CallToolResult>;
|
|
21
|
+
protected handleResourceRead(request: ReadResourceRequest): Promise<ReadResourceResult>;
|
|
62
22
|
private buildEventsUrl;
|
|
63
23
|
private getEvents;
|
|
64
24
|
private searchEvents;
|
|
25
|
+
/**
|
|
26
|
+
* Get events for the authenticated user via the unified Drupal view.
|
|
27
|
+
* Uses the /jsonapi/views/event_instance_mine/my_events_page endpoint
|
|
28
|
+
* which filters by X-Acting-User header.
|
|
29
|
+
*/
|
|
30
|
+
private getMyEvents;
|
|
65
31
|
}
|
package/dist/server.js
CHANGED
|
@@ -1,9 +1,53 @@
|
|
|
1
|
-
import { BaseAccessServer, handleApiError } from "@access-mcp/shared";
|
|
1
|
+
import { BaseAccessServer, handleApiError, DrupalAuthProvider, getRequestContext, } from "@access-mcp/shared";
|
|
2
2
|
import axios from "axios";
|
|
3
|
+
import { createRequire } from "module";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const { version } = require("../package.json");
|
|
3
6
|
export class EventsServer extends BaseAccessServer {
|
|
4
7
|
_eventsHttpClient;
|
|
8
|
+
drupalAuth;
|
|
5
9
|
constructor() {
|
|
6
|
-
super("access-mcp-events",
|
|
10
|
+
super("access-mcp-events", version, "https://support.access-ci.org", {
|
|
11
|
+
requireApiKey: true,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get or create the Drupal auth provider for authenticated operations.
|
|
16
|
+
* Requires DRUPAL_API_URL, DRUPAL_USERNAME, and DRUPAL_PASSWORD env vars.
|
|
17
|
+
*/
|
|
18
|
+
getDrupalAuth() {
|
|
19
|
+
if (!this.drupalAuth) {
|
|
20
|
+
const baseUrl = process.env.DRUPAL_API_URL;
|
|
21
|
+
const username = process.env.DRUPAL_USERNAME;
|
|
22
|
+
const password = process.env.DRUPAL_PASSWORD;
|
|
23
|
+
if (!baseUrl || !username || !password) {
|
|
24
|
+
throw new Error("Authenticated operations require DRUPAL_API_URL, DRUPAL_USERNAME, and DRUPAL_PASSWORD environment variables");
|
|
25
|
+
}
|
|
26
|
+
this.drupalAuth = new DrupalAuthProvider(baseUrl, username, password);
|
|
27
|
+
}
|
|
28
|
+
// Update acting user from request context or env var
|
|
29
|
+
const context = getRequestContext();
|
|
30
|
+
const actingUser = context?.actingUser || process.env.ACTING_USER;
|
|
31
|
+
if (actingUser) {
|
|
32
|
+
this.drupalAuth.setActingUser(actingUser);
|
|
33
|
+
}
|
|
34
|
+
return this.drupalAuth;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get the acting user's ACCESS ID for filtering.
|
|
38
|
+
*/
|
|
39
|
+
getActingUserAccessId() {
|
|
40
|
+
const context = getRequestContext();
|
|
41
|
+
if (context?.actingUser) {
|
|
42
|
+
return context.actingUser;
|
|
43
|
+
}
|
|
44
|
+
const envUser = process.env.ACTING_USER;
|
|
45
|
+
if (envUser) {
|
|
46
|
+
return envUser;
|
|
47
|
+
}
|
|
48
|
+
throw new Error("Cannot get user events: No acting user specified.\n\n" +
|
|
49
|
+
"Either set the X-Acting-User header or the ACTING_USER environment variable " +
|
|
50
|
+
"to the ACCESS ID (e.g., username@access-ci.org).");
|
|
7
51
|
}
|
|
8
52
|
get httpClient() {
|
|
9
53
|
if (!this._eventsHttpClient) {
|
|
@@ -34,34 +78,53 @@ export class EventsServer extends BaseAccessServer {
|
|
|
34
78
|
properties: {
|
|
35
79
|
query: {
|
|
36
80
|
type: "string",
|
|
37
|
-
description: "Search titles, descriptions, speakers, tags"
|
|
81
|
+
description: "Search titles, descriptions, speakers, tags",
|
|
38
82
|
},
|
|
39
83
|
type: {
|
|
40
84
|
type: "string",
|
|
41
|
-
description: "Filter: workshop, webinar, training"
|
|
85
|
+
description: "Filter: workshop, webinar, training",
|
|
42
86
|
},
|
|
43
87
|
tags: {
|
|
44
88
|
type: "string",
|
|
45
|
-
description: "Filter: python, gpu, hpc, ml"
|
|
89
|
+
description: "Filter: python, gpu, hpc, ml",
|
|
46
90
|
},
|
|
47
91
|
date: {
|
|
48
92
|
type: "string",
|
|
49
93
|
description: "Filter by time period",
|
|
50
|
-
enum: ["today", "upcoming", "past", "this_week", "this_month"]
|
|
94
|
+
enum: ["today", "upcoming", "past", "this_week", "this_month"],
|
|
51
95
|
},
|
|
52
96
|
skill: {
|
|
53
97
|
type: "string",
|
|
54
98
|
description: "Skill level filter",
|
|
55
|
-
enum: ["beginner", "intermediate", "advanced"]
|
|
99
|
+
enum: ["beginner", "intermediate", "advanced"],
|
|
56
100
|
},
|
|
57
101
|
limit: {
|
|
58
102
|
type: "number",
|
|
59
103
|
description: "Max results (default: 50)",
|
|
60
|
-
default: 50
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
104
|
+
default: 50,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "get_my_events",
|
|
111
|
+
description: `Get events created by or associated with the authenticated user.
|
|
112
|
+
|
|
113
|
+
Returns events the user has created or is associated with, including unpublished/draft events.
|
|
114
|
+
Requires authentication via X-Acting-User header or ACTING_USER environment variable.
|
|
115
|
+
|
|
116
|
+
Returns: {total, items: [{title, start_date, end_date, status, ...}]}`,
|
|
117
|
+
inputSchema: {
|
|
118
|
+
type: "object",
|
|
119
|
+
properties: {
|
|
120
|
+
limit: {
|
|
121
|
+
type: "number",
|
|
122
|
+
description: "Max results (default: 50)",
|
|
123
|
+
default: 50,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
65
128
|
];
|
|
66
129
|
}
|
|
67
130
|
getResources() {
|
|
@@ -98,6 +161,8 @@ export class EventsServer extends BaseAccessServer {
|
|
|
98
161
|
switch (name) {
|
|
99
162
|
case "search_events":
|
|
100
163
|
return await this.searchEvents(args);
|
|
164
|
+
case "get_my_events":
|
|
165
|
+
return await this.getMyEvents(args);
|
|
101
166
|
default:
|
|
102
167
|
return this.errorResponse(`Unknown tool: ${name}`);
|
|
103
168
|
}
|
|
@@ -109,18 +174,30 @@ export class EventsServer extends BaseAccessServer {
|
|
|
109
174
|
async handleResourceRead(request) {
|
|
110
175
|
const { uri } = request.params;
|
|
111
176
|
switch (uri) {
|
|
112
|
-
case "accessci://events":
|
|
177
|
+
case "accessci://events": {
|
|
113
178
|
const allEvents = await this.searchEvents({});
|
|
114
|
-
|
|
115
|
-
|
|
179
|
+
const content = allEvents.content[0];
|
|
180
|
+
const text = content.type === "text" ? content.text : "";
|
|
181
|
+
return this.createJsonResource(uri, JSON.parse(text));
|
|
182
|
+
}
|
|
183
|
+
case "accessci://events/upcoming": {
|
|
116
184
|
const upcomingEvents = await this.searchEvents({ date: "upcoming" });
|
|
117
|
-
|
|
118
|
-
|
|
185
|
+
const content = upcomingEvents.content[0];
|
|
186
|
+
const text = content.type === "text" ? content.text : "";
|
|
187
|
+
return this.createJsonResource(uri, JSON.parse(text));
|
|
188
|
+
}
|
|
189
|
+
case "accessci://events/workshops": {
|
|
119
190
|
const workshops = await this.searchEvents({ type: "workshop" });
|
|
120
|
-
|
|
121
|
-
|
|
191
|
+
const content = workshops.content[0];
|
|
192
|
+
const text = content.type === "text" ? content.text : "";
|
|
193
|
+
return this.createJsonResource(uri, JSON.parse(text));
|
|
194
|
+
}
|
|
195
|
+
case "accessci://events/webinars": {
|
|
122
196
|
const webinars = await this.searchEvents({ type: "webinar" });
|
|
123
|
-
|
|
197
|
+
const content = webinars.content[0];
|
|
198
|
+
const text = content.type === "text" ? content.text : "";
|
|
199
|
+
return this.createJsonResource(uri, JSON.parse(text));
|
|
200
|
+
}
|
|
124
201
|
default:
|
|
125
202
|
throw new Error(`Unknown resource: ${uri}`);
|
|
126
203
|
}
|
|
@@ -149,7 +226,7 @@ export class EventsServer extends BaseAccessServer {
|
|
|
149
226
|
upcoming: { start: "today" },
|
|
150
227
|
past: { start: "-1year", end: "today" },
|
|
151
228
|
this_week: { start: "today", end: "+1week" },
|
|
152
|
-
this_month: { start: "today", end: "+1month" }
|
|
229
|
+
this_month: { start: "today", end: "+1month" },
|
|
153
230
|
};
|
|
154
231
|
if (params.date && dateMap[params.date]) {
|
|
155
232
|
const dateMapping = dateMap[params.date];
|
|
@@ -158,20 +235,9 @@ export class EventsServer extends BaseAccessServer {
|
|
|
158
235
|
url.searchParams.set("end_date_relative", dateMapping.end);
|
|
159
236
|
}
|
|
160
237
|
}
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
url.searchParams.set(`f[${filterIndex}]`, `custom_event_type:${params.type}`);
|
|
165
|
-
filterIndex++;
|
|
166
|
-
}
|
|
167
|
-
if (params.tags) {
|
|
168
|
-
url.searchParams.set(`f[${filterIndex}]`, `custom_event_tags:${params.tags}`);
|
|
169
|
-
filterIndex++;
|
|
170
|
-
}
|
|
171
|
-
if (params.skill) {
|
|
172
|
-
url.searchParams.set(`f[${filterIndex}]`, `skill_level:${params.skill}`);
|
|
173
|
-
filterIndex++;
|
|
174
|
-
}
|
|
238
|
+
// Note: faceted filters (f[0]=field:value) are not supported on the
|
|
239
|
+
// data_export API display — they're bound to the page display in Drupal.
|
|
240
|
+
// Type, tags, and skill filters are applied client-side in getEvents().
|
|
175
241
|
return url.toString();
|
|
176
242
|
}
|
|
177
243
|
async getEvents(params) {
|
|
@@ -180,7 +246,24 @@ export class EventsServer extends BaseAccessServer {
|
|
|
180
246
|
if (response.status !== 200) {
|
|
181
247
|
throw new Error(`API error ${response.status}`);
|
|
182
248
|
}
|
|
183
|
-
|
|
249
|
+
// Ensure response is an array (non-array means unexpected response like 403 HTML)
|
|
250
|
+
let events = Array.isArray(response.data) ? response.data : [];
|
|
251
|
+
// Client-side filters (faceted search not available on data_export display)
|
|
252
|
+
if (params.type) {
|
|
253
|
+
const typeFilter = params.type.toLowerCase();
|
|
254
|
+
events = events.filter((e) => (e.event_type || "").toLowerCase() === typeFilter);
|
|
255
|
+
}
|
|
256
|
+
if (params.tags) {
|
|
257
|
+
const tagFilter = params.tags.toLowerCase();
|
|
258
|
+
events = events.filter((e) => {
|
|
259
|
+
const tags = typeof e.tags === "string" ? e.tags : Array.isArray(e.tags) ? e.tags.join(",") : "";
|
|
260
|
+
return tags.toLowerCase().includes(tagFilter);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (params.skill) {
|
|
264
|
+
const skillFilter = params.skill.toLowerCase();
|
|
265
|
+
events = events.filter((e) => (e.skill_level || "").toLowerCase() === skillFilter);
|
|
266
|
+
}
|
|
184
267
|
if (params.limit && events.length > params.limit) {
|
|
185
268
|
events = events.slice(0, params.limit);
|
|
186
269
|
}
|
|
@@ -188,18 +271,21 @@ export class EventsServer extends BaseAccessServer {
|
|
|
188
271
|
...event,
|
|
189
272
|
tags: Array.isArray(event.tags) ? event.tags : [],
|
|
190
273
|
duration_hours: event.end_date
|
|
191
|
-
? Math.round((new Date(event.end_date).getTime() - new Date(event.start_date).getTime()) /
|
|
274
|
+
? Math.round((new Date(event.end_date).getTime() - new Date(event.start_date || "").getTime()) /
|
|
275
|
+
3600000)
|
|
192
276
|
: null,
|
|
193
|
-
starts_in_hours: Math.max(0, Math.round((new Date(event.start_date).getTime() - Date.now()) / 3600000))
|
|
277
|
+
starts_in_hours: Math.max(0, Math.round((new Date(event.start_date || "").getTime() - Date.now()) / 3600000)),
|
|
194
278
|
}));
|
|
195
279
|
return {
|
|
196
|
-
content: [
|
|
280
|
+
content: [
|
|
281
|
+
{
|
|
197
282
|
type: "text",
|
|
198
283
|
text: JSON.stringify({
|
|
199
284
|
total: enhancedEvents.length,
|
|
200
|
-
items: enhancedEvents
|
|
201
|
-
})
|
|
202
|
-
}
|
|
285
|
+
items: enhancedEvents,
|
|
286
|
+
}),
|
|
287
|
+
},
|
|
288
|
+
],
|
|
203
289
|
};
|
|
204
290
|
}
|
|
205
291
|
async searchEvents(params) {
|
|
@@ -214,4 +300,39 @@ export class EventsServer extends BaseAccessServer {
|
|
|
214
300
|
}
|
|
215
301
|
return await this.getEvents(params);
|
|
216
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* Get events for the authenticated user via the unified Drupal view.
|
|
305
|
+
* Uses the /jsonapi/views/event_instance_mine/my_events_page endpoint
|
|
306
|
+
* which filters by X-Acting-User header.
|
|
307
|
+
*/
|
|
308
|
+
async getMyEvents(params) {
|
|
309
|
+
const auth = this.getDrupalAuth();
|
|
310
|
+
// Ensure we have an acting user
|
|
311
|
+
this.getActingUserAccessId();
|
|
312
|
+
const limit = params.limit || 50;
|
|
313
|
+
// Use the unified view endpoint - it respects X-Acting-User header
|
|
314
|
+
const result = await auth.get(`/jsonapi/views/event_instance_mine/my_events_page?page[limit]=${limit}`);
|
|
315
|
+
// JSON:API views return data in a different format
|
|
316
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- JSON:API response shape is dynamic
|
|
317
|
+
const events = (result.data || []).map((item) => ({
|
|
318
|
+
id: item.id,
|
|
319
|
+
type: item.type,
|
|
320
|
+
title: item.attributes?.title,
|
|
321
|
+
start_date: item.attributes?.field_start_date || item.attributes?.start_date,
|
|
322
|
+
end_date: item.attributes?.field_end_date || item.attributes?.end_date,
|
|
323
|
+
status: item.attributes?.status ? "published" : "draft",
|
|
324
|
+
...item.attributes,
|
|
325
|
+
}));
|
|
326
|
+
return {
|
|
327
|
+
content: [
|
|
328
|
+
{
|
|
329
|
+
type: "text",
|
|
330
|
+
text: JSON.stringify({
|
|
331
|
+
total: events.length,
|
|
332
|
+
items: events,
|
|
333
|
+
}),
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
217
338
|
}
|