@access-mcp/announcements 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/README.md +276 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +27 -0
- package/dist/server.d.ts +156 -0
- package/dist/server.js +307 -0
- package/package.json +38 -0
- package/src/index.ts +31 -0
- package/src/server.integration.test.ts +220 -0
- package/src/server.test.ts +367 -0
- package/src/server.ts +347 -0
- package/tsconfig.json +12 -0
- package/vitest.integration.config.ts +11 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { BaseAccessServer } from "@access-mcp/shared";
|
|
2
|
+
|
|
3
|
+
interface AnnouncementFilters {
|
|
4
|
+
tags?: string;
|
|
5
|
+
ag?: string;
|
|
6
|
+
affiliation?: string;
|
|
7
|
+
relative_start_date?: string;
|
|
8
|
+
relative_end_date?: string;
|
|
9
|
+
start_date?: string;
|
|
10
|
+
end_date?: string;
|
|
11
|
+
limit?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Announcement {
|
|
15
|
+
title: string;
|
|
16
|
+
body: string;
|
|
17
|
+
field_published_date: string;
|
|
18
|
+
custom_announcement_ag: string;
|
|
19
|
+
custom_announcement_tags: string;
|
|
20
|
+
field_affiliation: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class AnnouncementsServer extends BaseAccessServer {
|
|
24
|
+
constructor() {
|
|
25
|
+
super("access-announcements", "0.1.0", "https://support.access-ci.org");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected getTools() {
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
name: "get_announcements",
|
|
32
|
+
description: "Search ACCESS support announcements about system maintenance, service updates, outages, and community news. Use this when users ask about recent announcements, service status, or need to filter by dates, tags, or affinity groups.",
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
tags: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "Filter by specific topics (comma-separated). Real examples: 'gpu,nvidia', 'machine-learning,ai', 'maintenance,downtime', 'training,workshop', 'allocation-users', 'data-science', 'cloud-computing', 'bridges-2,anvil'"
|
|
39
|
+
},
|
|
40
|
+
ag: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "Filter by system/community affinity group. Real examples: 'ACCESS Support', 'DELTA', 'Anvil', 'Bridges-2', 'CSSN (Computational Science Support Network)', 'Open OnDemand', 'Pegasus'"
|
|
43
|
+
},
|
|
44
|
+
affiliation: {
|
|
45
|
+
type: "string",
|
|
46
|
+
description: "Filter by organization affiliation (e.g., 'ACCESS')"
|
|
47
|
+
},
|
|
48
|
+
relative_start_date: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "Filter announcements from a relative date. Examples: 'today', '-1 week', '-1 month', '-3 months', '-1 year'. Use negative values for past dates"
|
|
51
|
+
},
|
|
52
|
+
relative_end_date: {
|
|
53
|
+
type: "string",
|
|
54
|
+
description: "Filter announcements up to a relative date. Examples: 'now', 'today', '-1 week' (past), '+1 week' (future)"
|
|
55
|
+
},
|
|
56
|
+
start_date: {
|
|
57
|
+
type: "string",
|
|
58
|
+
description: "Filter announcements from exact date onwards (YYYY-MM-DD). Use when users specify dates like 'since January 1st, 2024' or 'from March 15th'"
|
|
59
|
+
},
|
|
60
|
+
end_date: {
|
|
61
|
+
type: "string",
|
|
62
|
+
description: "Filter announcements up to exact date (YYYY-MM-DD). Use when users specify dates like 'until December 31st, 2024' or 'before April 1st'"
|
|
63
|
+
},
|
|
64
|
+
limit: {
|
|
65
|
+
type: "number",
|
|
66
|
+
description: "Maximum number of announcements to return. Use smaller values (5-10) for quick overviews, larger values (20-50) for comprehensive searches",
|
|
67
|
+
default: 20
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "get_announcements_by_tags",
|
|
74
|
+
description: "Find announcements about specific topics or systems. Use when users ask about particular subjects like 'GPU maintenance', 'machine learning resources', 'training workshops', or 'cloud computing updates'.",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {
|
|
78
|
+
tags: {
|
|
79
|
+
type: "string",
|
|
80
|
+
description: "Specific topics to search for (comma-separated). Popular examples: 'gpu,nvidia', 'machine-learning,ai', 'training,professional-development', 'data-science,python', 'cloud-computing', 'allocations-proposal'",
|
|
81
|
+
required: true
|
|
82
|
+
},
|
|
83
|
+
limit: {
|
|
84
|
+
type: "number",
|
|
85
|
+
description: "Maximum number of announcements to return",
|
|
86
|
+
default: 10
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
required: ["tags"]
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "get_announcements_by_affinity_group",
|
|
94
|
+
description: "Get announcements for a specific ACCESS system or community group. Use when users ask about updates for particular resources like DELTA, Anvil, or Bridges-2, or community groups like CSSN.",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
ag: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: "The system or community group name (not the technical API ID). Examples: 'DELTA', 'Anvil', 'Bridges-2', 'ACCESS Support', 'Open OnDemand', 'Pegasus', 'CSSN (Computational Science Support Network)'",
|
|
101
|
+
required: true
|
|
102
|
+
},
|
|
103
|
+
limit: {
|
|
104
|
+
type: "number",
|
|
105
|
+
description: "Maximum number of announcements to return",
|
|
106
|
+
default: 10
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
required: ["ag"]
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "get_recent_announcements",
|
|
114
|
+
description: "Get the latest ACCESS support announcements from a recent time period. Use this when users ask 'what's new?', 'recent updates', 'latest announcements', or want a general overview of current activity.",
|
|
115
|
+
inputSchema: {
|
|
116
|
+
type: "object",
|
|
117
|
+
properties: {
|
|
118
|
+
period: {
|
|
119
|
+
type: "string",
|
|
120
|
+
description: "How far back to look for announcements. Examples: '1 week', '2 weeks', '1 month', '3 months', '6 months'. Default is '1 month'",
|
|
121
|
+
default: "1 month"
|
|
122
|
+
},
|
|
123
|
+
limit: {
|
|
124
|
+
type: "number",
|
|
125
|
+
description: "Maximum number of announcements to return",
|
|
126
|
+
default: 10
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
protected getResources() {
|
|
135
|
+
return [
|
|
136
|
+
{
|
|
137
|
+
uri: "accessci://announcements",
|
|
138
|
+
name: "ACCESS Support Announcements",
|
|
139
|
+
description: "Recent announcements and notifications from ACCESS support",
|
|
140
|
+
mimeType: "application/json"
|
|
141
|
+
}
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async handleToolCall(request: any) {
|
|
146
|
+
const { name, arguments: args } = request.params;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
switch (name) {
|
|
150
|
+
case "get_announcements":
|
|
151
|
+
return await this.getAnnouncements(args);
|
|
152
|
+
case "get_announcements_by_tags":
|
|
153
|
+
return await this.getAnnouncementsByTags(args.tags, args.limit);
|
|
154
|
+
case "get_announcements_by_affinity_group":
|
|
155
|
+
return await this.getAnnouncementsByAffinityGroup(args.ag, args.limit);
|
|
156
|
+
case "get_recent_announcements":
|
|
157
|
+
return await this.getRecentAnnouncements(args.period, args.limit);
|
|
158
|
+
default:
|
|
159
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
160
|
+
}
|
|
161
|
+
} catch (error: any) {
|
|
162
|
+
return {
|
|
163
|
+
content: [
|
|
164
|
+
{
|
|
165
|
+
type: "text",
|
|
166
|
+
text: `Error: ${error.message}`
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async handleResourceRead(request: any) {
|
|
174
|
+
const { uri } = request.params;
|
|
175
|
+
|
|
176
|
+
if (uri === "accessci://announcements") {
|
|
177
|
+
try {
|
|
178
|
+
const announcements = await this.fetchAnnouncements({ limit: 10 });
|
|
179
|
+
return {
|
|
180
|
+
contents: [
|
|
181
|
+
{
|
|
182
|
+
uri,
|
|
183
|
+
mimeType: "application/json",
|
|
184
|
+
text: JSON.stringify(announcements, null, 2)
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
};
|
|
188
|
+
} catch (error: any) {
|
|
189
|
+
return {
|
|
190
|
+
contents: [
|
|
191
|
+
{
|
|
192
|
+
uri,
|
|
193
|
+
mimeType: "text/plain",
|
|
194
|
+
text: `Error loading announcements: ${error.message}`
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private buildAnnouncementsUrl(filters: AnnouncementFilters): string {
|
|
205
|
+
const params = new URLSearchParams();
|
|
206
|
+
|
|
207
|
+
if (filters.tags) {
|
|
208
|
+
params.append("tags", filters.tags);
|
|
209
|
+
}
|
|
210
|
+
if (filters.ag) {
|
|
211
|
+
params.append("ag", filters.ag);
|
|
212
|
+
}
|
|
213
|
+
if (filters.affiliation) {
|
|
214
|
+
params.append("affiliation", filters.affiliation);
|
|
215
|
+
}
|
|
216
|
+
if (filters.relative_start_date) {
|
|
217
|
+
params.append("relative_start_date", filters.relative_start_date);
|
|
218
|
+
}
|
|
219
|
+
if (filters.relative_end_date) {
|
|
220
|
+
params.append("relative_end_date", filters.relative_end_date);
|
|
221
|
+
}
|
|
222
|
+
if (filters.start_date) {
|
|
223
|
+
params.append("start_date", filters.start_date);
|
|
224
|
+
}
|
|
225
|
+
if (filters.end_date) {
|
|
226
|
+
params.append("end_date", filters.end_date);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return `/api/2.1/announcements?${params.toString()}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private async fetchAnnouncements(filters: AnnouncementFilters): Promise<Announcement[]> {
|
|
233
|
+
const url = this.buildAnnouncementsUrl(filters);
|
|
234
|
+
const response = await this.httpClient.get(url);
|
|
235
|
+
|
|
236
|
+
if (response.status !== 200) {
|
|
237
|
+
throw new Error(`API Error ${response.status}: ${response.statusText}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const announcements = response.data || [];
|
|
241
|
+
return this.enhanceAnnouncements(announcements);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private enhanceAnnouncements(rawAnnouncements: any[]): Announcement[] {
|
|
245
|
+
return rawAnnouncements.map(announcement => ({
|
|
246
|
+
...announcement,
|
|
247
|
+
date: announcement.field_published_date,
|
|
248
|
+
tags: announcement.custom_announcement_tags
|
|
249
|
+
? announcement.custom_announcement_tags.split(',').map((t: string) => t.trim()).filter((t: string) => t)
|
|
250
|
+
: [],
|
|
251
|
+
formatted_date: announcement.field_published_date
|
|
252
|
+
? new Date(announcement.field_published_date).toLocaleDateString('en-US', {
|
|
253
|
+
year: 'numeric',
|
|
254
|
+
month: 'long',
|
|
255
|
+
day: 'numeric'
|
|
256
|
+
})
|
|
257
|
+
: '',
|
|
258
|
+
body_preview: announcement.body
|
|
259
|
+
? announcement.body.replace(/<[^>]*>/g, '').substring(0, 200) + '...'
|
|
260
|
+
: '',
|
|
261
|
+
affinity_groups: announcement.custom_announcement_ag
|
|
262
|
+
? announcement.custom_announcement_ag.split(',').map((g: string) => g.trim()).filter((g: string) => g)
|
|
263
|
+
: []
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private async getAnnouncements(filters: AnnouncementFilters) {
|
|
268
|
+
const announcements = await this.fetchAnnouncements(filters);
|
|
269
|
+
const limited = filters.limit ? announcements.slice(0, filters.limit) : announcements;
|
|
270
|
+
|
|
271
|
+
// Build filters_applied object
|
|
272
|
+
const filters_applied: any = {};
|
|
273
|
+
if (filters.tags) filters_applied.tags = filters.tags;
|
|
274
|
+
if (filters.ag) filters_applied.affinity_group = filters.ag;
|
|
275
|
+
if (filters.relative_start_date || filters.relative_end_date) {
|
|
276
|
+
filters_applied.date_range = `${filters.relative_start_date || 'any'} to ${filters.relative_end_date || 'now'}`;
|
|
277
|
+
}
|
|
278
|
+
if (filters.start_date || filters.end_date) {
|
|
279
|
+
filters_applied.date_range = `${filters.start_date || 'any'} to ${filters.end_date || 'now'}`;
|
|
280
|
+
}
|
|
281
|
+
if (filters.limit) filters_applied.limit = filters.limit;
|
|
282
|
+
|
|
283
|
+
const result = {
|
|
284
|
+
total_announcements: announcements.length,
|
|
285
|
+
filtered_announcements: limited.length,
|
|
286
|
+
announcements: limited,
|
|
287
|
+
filters_applied,
|
|
288
|
+
popular_tags: this.getPopularTags(limited),
|
|
289
|
+
message: limited.length === 0 ? "No announcements found matching the filters" : undefined
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
content: [
|
|
294
|
+
{
|
|
295
|
+
type: "text",
|
|
296
|
+
text: JSON.stringify(result, null, 2)
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private async getAnnouncementsByTags(tags: string, limit: number = 10) {
|
|
303
|
+
return await this.getAnnouncements({ tags, limit });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private async getAnnouncementsByAffinityGroup(ag: string, limit: number = 10) {
|
|
307
|
+
return await this.getAnnouncements({ ag, limit });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private async getRecentAnnouncements(period: string = "1 month", limit: number = 10) {
|
|
311
|
+
const relativeStart = `-${period}`;
|
|
312
|
+
return await this.getAnnouncements({
|
|
313
|
+
relative_start_date: relativeStart,
|
|
314
|
+
relative_end_date: "now",
|
|
315
|
+
limit
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private getPopularTags(announcements: any[]): string[] {
|
|
320
|
+
const tagCounts: { [key: string]: number } = {};
|
|
321
|
+
|
|
322
|
+
announcements.forEach(announcement => {
|
|
323
|
+
if (announcement.tags) {
|
|
324
|
+
announcement.tags.forEach((tag: string) => {
|
|
325
|
+
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return Object.entries(tagCounts)
|
|
331
|
+
.sort(([,a], [,b]) => b - a)
|
|
332
|
+
.slice(0, 10)
|
|
333
|
+
.map(([tag]) => tag);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private getAffinityGroups(announcements: any[]): string[] {
|
|
337
|
+
const groups = new Set<string>();
|
|
338
|
+
announcements.forEach(announcement => {
|
|
339
|
+
if (announcement.affinity_groups && Array.isArray(announcement.affinity_groups)) {
|
|
340
|
+
announcement.affinity_groups.forEach((group: string) => {
|
|
341
|
+
if (group) groups.add(group);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
return Array.from(groups);
|
|
346
|
+
}
|
|
347
|
+
}
|
package/tsconfig.json
ADDED