@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/dist/server.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { BaseAccessServer } from "@access-mcp/shared";
|
|
2
|
+
export class AnnouncementsServer extends BaseAccessServer {
|
|
3
|
+
constructor() {
|
|
4
|
+
super("access-announcements", "0.1.0", "https://support.access-ci.org");
|
|
5
|
+
}
|
|
6
|
+
getTools() {
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
name: "get_announcements",
|
|
10
|
+
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.",
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
tags: {
|
|
15
|
+
type: "string",
|
|
16
|
+
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'"
|
|
17
|
+
},
|
|
18
|
+
ag: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Filter by system/community affinity group. Real examples: 'ACCESS Support', 'DELTA', 'Anvil', 'Bridges-2', 'CSSN (Computational Science Support Network)', 'Open OnDemand', 'Pegasus'"
|
|
21
|
+
},
|
|
22
|
+
affiliation: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Filter by organization affiliation (e.g., 'ACCESS')"
|
|
25
|
+
},
|
|
26
|
+
relative_start_date: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "Filter announcements from a relative date. Examples: 'today', '-1 week', '-1 month', '-3 months', '-1 year'. Use negative values for past dates"
|
|
29
|
+
},
|
|
30
|
+
relative_end_date: {
|
|
31
|
+
type: "string",
|
|
32
|
+
description: "Filter announcements up to a relative date. Examples: 'now', 'today', '-1 week' (past), '+1 week' (future)"
|
|
33
|
+
},
|
|
34
|
+
start_date: {
|
|
35
|
+
type: "string",
|
|
36
|
+
description: "Filter announcements from exact date onwards (YYYY-MM-DD). Use when users specify dates like 'since January 1st, 2024' or 'from March 15th'"
|
|
37
|
+
},
|
|
38
|
+
end_date: {
|
|
39
|
+
type: "string",
|
|
40
|
+
description: "Filter announcements up to exact date (YYYY-MM-DD). Use when users specify dates like 'until December 31st, 2024' or 'before April 1st'"
|
|
41
|
+
},
|
|
42
|
+
limit: {
|
|
43
|
+
type: "number",
|
|
44
|
+
description: "Maximum number of announcements to return. Use smaller values (5-10) for quick overviews, larger values (20-50) for comprehensive searches",
|
|
45
|
+
default: 20
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "get_announcements_by_tags",
|
|
52
|
+
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'.",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {
|
|
56
|
+
tags: {
|
|
57
|
+
type: "string",
|
|
58
|
+
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'",
|
|
59
|
+
required: true
|
|
60
|
+
},
|
|
61
|
+
limit: {
|
|
62
|
+
type: "number",
|
|
63
|
+
description: "Maximum number of announcements to return",
|
|
64
|
+
default: 10
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
required: ["tags"]
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "get_announcements_by_affinity_group",
|
|
72
|
+
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.",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
ag: {
|
|
77
|
+
type: "string",
|
|
78
|
+
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)'",
|
|
79
|
+
required: true
|
|
80
|
+
},
|
|
81
|
+
limit: {
|
|
82
|
+
type: "number",
|
|
83
|
+
description: "Maximum number of announcements to return",
|
|
84
|
+
default: 10
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
required: ["ag"]
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "get_recent_announcements",
|
|
92
|
+
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.",
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: "object",
|
|
95
|
+
properties: {
|
|
96
|
+
period: {
|
|
97
|
+
type: "string",
|
|
98
|
+
description: "How far back to look for announcements. Examples: '1 week', '2 weeks', '1 month', '3 months', '6 months'. Default is '1 month'",
|
|
99
|
+
default: "1 month"
|
|
100
|
+
},
|
|
101
|
+
limit: {
|
|
102
|
+
type: "number",
|
|
103
|
+
description: "Maximum number of announcements to return",
|
|
104
|
+
default: 10
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
getResources() {
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
114
|
+
uri: "accessci://announcements",
|
|
115
|
+
name: "ACCESS Support Announcements",
|
|
116
|
+
description: "Recent announcements and notifications from ACCESS support",
|
|
117
|
+
mimeType: "application/json"
|
|
118
|
+
}
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
async handleToolCall(request) {
|
|
122
|
+
const { name, arguments: args } = request.params;
|
|
123
|
+
try {
|
|
124
|
+
switch (name) {
|
|
125
|
+
case "get_announcements":
|
|
126
|
+
return await this.getAnnouncements(args);
|
|
127
|
+
case "get_announcements_by_tags":
|
|
128
|
+
return await this.getAnnouncementsByTags(args.tags, args.limit);
|
|
129
|
+
case "get_announcements_by_affinity_group":
|
|
130
|
+
return await this.getAnnouncementsByAffinityGroup(args.ag, args.limit);
|
|
131
|
+
case "get_recent_announcements":
|
|
132
|
+
return await this.getRecentAnnouncements(args.period, args.limit);
|
|
133
|
+
default:
|
|
134
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
return {
|
|
139
|
+
content: [
|
|
140
|
+
{
|
|
141
|
+
type: "text",
|
|
142
|
+
text: `Error: ${error.message}`
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async handleResourceRead(request) {
|
|
149
|
+
const { uri } = request.params;
|
|
150
|
+
if (uri === "accessci://announcements") {
|
|
151
|
+
try {
|
|
152
|
+
const announcements = await this.fetchAnnouncements({ limit: 10 });
|
|
153
|
+
return {
|
|
154
|
+
contents: [
|
|
155
|
+
{
|
|
156
|
+
uri,
|
|
157
|
+
mimeType: "application/json",
|
|
158
|
+
text: JSON.stringify(announcements, null, 2)
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return {
|
|
165
|
+
contents: [
|
|
166
|
+
{
|
|
167
|
+
uri,
|
|
168
|
+
mimeType: "text/plain",
|
|
169
|
+
text: `Error loading announcements: ${error.message}`
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
176
|
+
}
|
|
177
|
+
buildAnnouncementsUrl(filters) {
|
|
178
|
+
const params = new URLSearchParams();
|
|
179
|
+
if (filters.tags) {
|
|
180
|
+
params.append("tags", filters.tags);
|
|
181
|
+
}
|
|
182
|
+
if (filters.ag) {
|
|
183
|
+
params.append("ag", filters.ag);
|
|
184
|
+
}
|
|
185
|
+
if (filters.affiliation) {
|
|
186
|
+
params.append("affiliation", filters.affiliation);
|
|
187
|
+
}
|
|
188
|
+
if (filters.relative_start_date) {
|
|
189
|
+
params.append("relative_start_date", filters.relative_start_date);
|
|
190
|
+
}
|
|
191
|
+
if (filters.relative_end_date) {
|
|
192
|
+
params.append("relative_end_date", filters.relative_end_date);
|
|
193
|
+
}
|
|
194
|
+
if (filters.start_date) {
|
|
195
|
+
params.append("start_date", filters.start_date);
|
|
196
|
+
}
|
|
197
|
+
if (filters.end_date) {
|
|
198
|
+
params.append("end_date", filters.end_date);
|
|
199
|
+
}
|
|
200
|
+
return `/api/2.1/announcements?${params.toString()}`;
|
|
201
|
+
}
|
|
202
|
+
async fetchAnnouncements(filters) {
|
|
203
|
+
const url = this.buildAnnouncementsUrl(filters);
|
|
204
|
+
const response = await this.httpClient.get(url);
|
|
205
|
+
if (response.status !== 200) {
|
|
206
|
+
throw new Error(`API Error ${response.status}: ${response.statusText}`);
|
|
207
|
+
}
|
|
208
|
+
const announcements = response.data || [];
|
|
209
|
+
return this.enhanceAnnouncements(announcements);
|
|
210
|
+
}
|
|
211
|
+
enhanceAnnouncements(rawAnnouncements) {
|
|
212
|
+
return rawAnnouncements.map(announcement => ({
|
|
213
|
+
...announcement,
|
|
214
|
+
date: announcement.field_published_date,
|
|
215
|
+
tags: announcement.custom_announcement_tags
|
|
216
|
+
? announcement.custom_announcement_tags.split(',').map((t) => t.trim()).filter((t) => t)
|
|
217
|
+
: [],
|
|
218
|
+
formatted_date: announcement.field_published_date
|
|
219
|
+
? new Date(announcement.field_published_date).toLocaleDateString('en-US', {
|
|
220
|
+
year: 'numeric',
|
|
221
|
+
month: 'long',
|
|
222
|
+
day: 'numeric'
|
|
223
|
+
})
|
|
224
|
+
: '',
|
|
225
|
+
body_preview: announcement.body
|
|
226
|
+
? announcement.body.replace(/<[^>]*>/g, '').substring(0, 200) + '...'
|
|
227
|
+
: '',
|
|
228
|
+
affinity_groups: announcement.custom_announcement_ag
|
|
229
|
+
? announcement.custom_announcement_ag.split(',').map((g) => g.trim()).filter((g) => g)
|
|
230
|
+
: []
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
async getAnnouncements(filters) {
|
|
234
|
+
const announcements = await this.fetchAnnouncements(filters);
|
|
235
|
+
const limited = filters.limit ? announcements.slice(0, filters.limit) : announcements;
|
|
236
|
+
// Build filters_applied object
|
|
237
|
+
const filters_applied = {};
|
|
238
|
+
if (filters.tags)
|
|
239
|
+
filters_applied.tags = filters.tags;
|
|
240
|
+
if (filters.ag)
|
|
241
|
+
filters_applied.affinity_group = filters.ag;
|
|
242
|
+
if (filters.relative_start_date || filters.relative_end_date) {
|
|
243
|
+
filters_applied.date_range = `${filters.relative_start_date || 'any'} to ${filters.relative_end_date || 'now'}`;
|
|
244
|
+
}
|
|
245
|
+
if (filters.start_date || filters.end_date) {
|
|
246
|
+
filters_applied.date_range = `${filters.start_date || 'any'} to ${filters.end_date || 'now'}`;
|
|
247
|
+
}
|
|
248
|
+
if (filters.limit)
|
|
249
|
+
filters_applied.limit = filters.limit;
|
|
250
|
+
const result = {
|
|
251
|
+
total_announcements: announcements.length,
|
|
252
|
+
filtered_announcements: limited.length,
|
|
253
|
+
announcements: limited,
|
|
254
|
+
filters_applied,
|
|
255
|
+
popular_tags: this.getPopularTags(limited),
|
|
256
|
+
message: limited.length === 0 ? "No announcements found matching the filters" : undefined
|
|
257
|
+
};
|
|
258
|
+
return {
|
|
259
|
+
content: [
|
|
260
|
+
{
|
|
261
|
+
type: "text",
|
|
262
|
+
text: JSON.stringify(result, null, 2)
|
|
263
|
+
}
|
|
264
|
+
]
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async getAnnouncementsByTags(tags, limit = 10) {
|
|
268
|
+
return await this.getAnnouncements({ tags, limit });
|
|
269
|
+
}
|
|
270
|
+
async getAnnouncementsByAffinityGroup(ag, limit = 10) {
|
|
271
|
+
return await this.getAnnouncements({ ag, limit });
|
|
272
|
+
}
|
|
273
|
+
async getRecentAnnouncements(period = "1 month", limit = 10) {
|
|
274
|
+
const relativeStart = `-${period}`;
|
|
275
|
+
return await this.getAnnouncements({
|
|
276
|
+
relative_start_date: relativeStart,
|
|
277
|
+
relative_end_date: "now",
|
|
278
|
+
limit
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
getPopularTags(announcements) {
|
|
282
|
+
const tagCounts = {};
|
|
283
|
+
announcements.forEach(announcement => {
|
|
284
|
+
if (announcement.tags) {
|
|
285
|
+
announcement.tags.forEach((tag) => {
|
|
286
|
+
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
return Object.entries(tagCounts)
|
|
291
|
+
.sort(([, a], [, b]) => b - a)
|
|
292
|
+
.slice(0, 10)
|
|
293
|
+
.map(([tag]) => tag);
|
|
294
|
+
}
|
|
295
|
+
getAffinityGroups(announcements) {
|
|
296
|
+
const groups = new Set();
|
|
297
|
+
announcements.forEach(announcement => {
|
|
298
|
+
if (announcement.affinity_groups && Array.isArray(announcement.affinity_groups)) {
|
|
299
|
+
announcement.affinity_groups.forEach((group) => {
|
|
300
|
+
if (group)
|
|
301
|
+
groups.add(group);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
return Array.from(groups);
|
|
306
|
+
}
|
|
307
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@access-mcp/announcements",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for ACCESS Support Announcements API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"access-mcp-announcements": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
17
|
+
"test:all": "vitest run && vitest run --config vitest.integration.config.ts",
|
|
18
|
+
"test:coverage": "vitest run --coverage"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"access-ci",
|
|
23
|
+
"announcements",
|
|
24
|
+
"support"
|
|
25
|
+
],
|
|
26
|
+
"author": "ACCESS-CI",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@access-mcp/shared": "file:../shared",
|
|
30
|
+
"@modelcontextprotocol/sdk": "^0.5.0",
|
|
31
|
+
"axios": "^1.7.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^20.0.0",
|
|
35
|
+
"typescript": "^5.0.0",
|
|
36
|
+
"vitest": "^1.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { AnnouncementsServer } from "./server.js";
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
// Check if we should run as HTTP server (for deployment)
|
|
7
|
+
const port = process.env.PORT;
|
|
8
|
+
|
|
9
|
+
const server = new AnnouncementsServer();
|
|
10
|
+
|
|
11
|
+
if (port) {
|
|
12
|
+
// Running in HTTP mode (deployment)
|
|
13
|
+
await server.start({ httpPort: parseInt(port) });
|
|
14
|
+
// Keep the process running in HTTP mode
|
|
15
|
+
process.on('SIGINT', () => {
|
|
16
|
+
console.log('Shutting down server...');
|
|
17
|
+
process.exit(0);
|
|
18
|
+
});
|
|
19
|
+
// Keep the event loop alive
|
|
20
|
+
setInterval(() => {}, 1000 * 60 * 60); // Heartbeat every hour
|
|
21
|
+
} else {
|
|
22
|
+
// Running in MCP mode (stdio)
|
|
23
|
+
await server.start();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
main().catch((error) => {
|
|
28
|
+
// Log errors to stderr and exit
|
|
29
|
+
console.error("Server error:", error);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { AnnouncementsServer } from "./server.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Integration tests for AnnouncementsServer
|
|
6
|
+
* These tests make actual API calls to the ACCESS Support API
|
|
7
|
+
*/
|
|
8
|
+
describe("AnnouncementsServer Integration Tests", () => {
|
|
9
|
+
let server: AnnouncementsServer;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
server = new AnnouncementsServer();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("get_announcements", () => {
|
|
16
|
+
it("should fetch real announcements from API", async () => {
|
|
17
|
+
const result = await server["handleToolCall"]({
|
|
18
|
+
params: {
|
|
19
|
+
name: "get_announcements",
|
|
20
|
+
arguments: {
|
|
21
|
+
limit: 5,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
} as any);
|
|
25
|
+
|
|
26
|
+
expect(result.isError).toBeFalsy();
|
|
27
|
+
const responseData = JSON.parse(result.content[0].text);
|
|
28
|
+
|
|
29
|
+
// Check structure
|
|
30
|
+
expect(responseData).toHaveProperty("total_announcements");
|
|
31
|
+
expect(responseData).toHaveProperty("announcements");
|
|
32
|
+
expect(responseData).toHaveProperty("popular_tags");
|
|
33
|
+
expect(responseData).toHaveProperty("filters_applied");
|
|
34
|
+
|
|
35
|
+
// Announcements should be an array
|
|
36
|
+
expect(Array.isArray(responseData.announcements)).toBe(true);
|
|
37
|
+
|
|
38
|
+
// If there are announcements, check their structure
|
|
39
|
+
if (responseData.announcements.length > 0) {
|
|
40
|
+
const firstAnnouncement = responseData.announcements[0];
|
|
41
|
+
expect(firstAnnouncement).toHaveProperty("title");
|
|
42
|
+
expect(firstAnnouncement).toHaveProperty("body");
|
|
43
|
+
expect(firstAnnouncement).toHaveProperty("date");
|
|
44
|
+
expect(firstAnnouncement).toHaveProperty("formatted_date");
|
|
45
|
+
expect(firstAnnouncement).toHaveProperty("tags");
|
|
46
|
+
expect(Array.isArray(firstAnnouncement.tags)).toBe(true);
|
|
47
|
+
}
|
|
48
|
+
}, 10000);
|
|
49
|
+
|
|
50
|
+
it("should filter by tags", async () => {
|
|
51
|
+
const result = await server["handleToolCall"]({
|
|
52
|
+
params: {
|
|
53
|
+
name: "get_announcements_by_tags",
|
|
54
|
+
arguments: {
|
|
55
|
+
tags: "maintenance",
|
|
56
|
+
limit: 3,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
} as any);
|
|
60
|
+
|
|
61
|
+
expect(result.isError).toBeFalsy();
|
|
62
|
+
const responseData = JSON.parse(result.content[0].text);
|
|
63
|
+
|
|
64
|
+
expect(responseData).toHaveProperty("announcements");
|
|
65
|
+
expect(Array.isArray(responseData.announcements)).toBe(true);
|
|
66
|
+
|
|
67
|
+
// If maintenance announcements exist, they should contain the tag
|
|
68
|
+
if (responseData.announcements.length > 0) {
|
|
69
|
+
const hasMaintenanceTag = responseData.announcements.some((ann: any) =>
|
|
70
|
+
ann.tags && ann.tags.some((tag: string) =>
|
|
71
|
+
tag.toLowerCase().includes("maintenance")
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
// This might not always be true if the API doesn't have maintenance announcements
|
|
75
|
+
console.log("Found maintenance announcements:", hasMaintenanceTag);
|
|
76
|
+
}
|
|
77
|
+
}, 10000);
|
|
78
|
+
|
|
79
|
+
it("should handle date range filters", async () => {
|
|
80
|
+
const result = await server["handleToolCall"]({
|
|
81
|
+
params: {
|
|
82
|
+
name: "get_announcements",
|
|
83
|
+
arguments: {
|
|
84
|
+
relative_start_date: "-1month",
|
|
85
|
+
relative_end_date: "today",
|
|
86
|
+
limit: 10,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
} as any);
|
|
90
|
+
|
|
91
|
+
expect(result.isError).toBeFalsy();
|
|
92
|
+
const responseData = JSON.parse(result.content[0].text);
|
|
93
|
+
|
|
94
|
+
expect(responseData).toHaveProperty("announcements");
|
|
95
|
+
expect(responseData.filters_applied).toHaveProperty("date_range");
|
|
96
|
+
|
|
97
|
+
// All announcements should be within the last month
|
|
98
|
+
if (responseData.announcements.length > 0) {
|
|
99
|
+
const oneMonthAgo = new Date();
|
|
100
|
+
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
|
|
101
|
+
|
|
102
|
+
responseData.announcements.forEach((ann: any) => {
|
|
103
|
+
const annDate = new Date(ann.date);
|
|
104
|
+
expect(annDate.getTime()).toBeGreaterThanOrEqual(oneMonthAgo.getTime());
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}, 10000);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("get_recent_announcements", () => {
|
|
111
|
+
it("should fetch announcements from the past week", async () => {
|
|
112
|
+
const result = await server["handleToolCall"]({
|
|
113
|
+
params: {
|
|
114
|
+
name: "get_recent_announcements",
|
|
115
|
+
arguments: {
|
|
116
|
+
period: "1 week",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
} as any);
|
|
120
|
+
|
|
121
|
+
expect(result.isError).toBeFalsy();
|
|
122
|
+
const responseData = JSON.parse(result.content[0].text);
|
|
123
|
+
|
|
124
|
+
expect(responseData).toHaveProperty("announcements");
|
|
125
|
+
expect(responseData.filters_applied.date_range).toContain("week");
|
|
126
|
+
|
|
127
|
+
// Check that announcements are from the past week if any exist
|
|
128
|
+
if (responseData.announcements.length > 0) {
|
|
129
|
+
const oneWeekAgo = new Date();
|
|
130
|
+
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
|
131
|
+
|
|
132
|
+
responseData.announcements.forEach((ann: any) => {
|
|
133
|
+
const annDate = new Date(ann.date);
|
|
134
|
+
expect(annDate.getTime()).toBeGreaterThanOrEqual(oneWeekAgo.getTime());
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}, 10000);
|
|
138
|
+
|
|
139
|
+
it("should handle today filter", async () => {
|
|
140
|
+
const result = await server["handleToolCall"]({
|
|
141
|
+
params: {
|
|
142
|
+
name: "get_recent_announcements",
|
|
143
|
+
arguments: {
|
|
144
|
+
period: "0 days",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
} as any);
|
|
148
|
+
|
|
149
|
+
expect(result.isError).toBeFalsy();
|
|
150
|
+
const responseData = JSON.parse(result.content[0].text);
|
|
151
|
+
|
|
152
|
+
expect(responseData).toHaveProperty("announcements");
|
|
153
|
+
expect(responseData.filters_applied.date_range).toContain("0 days");
|
|
154
|
+
|
|
155
|
+
// Today's announcements might be empty, that's okay
|
|
156
|
+
expect(Array.isArray(responseData.announcements)).toBe(true);
|
|
157
|
+
}, 10000);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("get_announcements_by_affinity_group", () => {
|
|
161
|
+
it("should handle affinity group filtering", async () => {
|
|
162
|
+
// We don't know specific affinity group IDs, so test with a made-up one
|
|
163
|
+
const result = await server["handleToolCall"]({
|
|
164
|
+
params: {
|
|
165
|
+
name: "get_announcements_by_affinity_group",
|
|
166
|
+
arguments: {
|
|
167
|
+
ag: "test-group-123",
|
|
168
|
+
limit: 5,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
} as any);
|
|
172
|
+
|
|
173
|
+
expect(result.isError).toBeFalsy();
|
|
174
|
+
const responseData = JSON.parse(result.content[0].text);
|
|
175
|
+
|
|
176
|
+
// Should return a valid response even if no announcements match
|
|
177
|
+
expect(responseData).toHaveProperty("announcements");
|
|
178
|
+
expect(Array.isArray(responseData.announcements)).toBe(true);
|
|
179
|
+
expect(responseData.filters_applied).toHaveProperty("affinity_group");
|
|
180
|
+
expect(responseData.filters_applied.affinity_group).toBe("test-group-123");
|
|
181
|
+
}, 10000);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("API Error Handling", () => {
|
|
185
|
+
it("should handle invalid parameters gracefully", async () => {
|
|
186
|
+
const result = await server["handleToolCall"]({
|
|
187
|
+
params: {
|
|
188
|
+
name: "get_announcements",
|
|
189
|
+
arguments: {
|
|
190
|
+
beginning_date: "invalid-date",
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
} as any);
|
|
194
|
+
|
|
195
|
+
// The API might accept this or reject it
|
|
196
|
+
// Just verify we get a response
|
|
197
|
+
expect(result).toBeDefined();
|
|
198
|
+
expect(result.content).toBeDefined();
|
|
199
|
+
}, 10000);
|
|
200
|
+
|
|
201
|
+
it("should handle empty results", async () => {
|
|
202
|
+
const result = await server["handleToolCall"]({
|
|
203
|
+
params: {
|
|
204
|
+
name: "get_announcements",
|
|
205
|
+
arguments: {
|
|
206
|
+
tags: "nonexistent-tag-xyz-123-456",
|
|
207
|
+
exact_match: true,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
} as any);
|
|
211
|
+
|
|
212
|
+
expect(result.isError).toBeFalsy();
|
|
213
|
+
const responseData = JSON.parse(result.content[0].text);
|
|
214
|
+
|
|
215
|
+
expect(responseData.total_announcements).toBe(0);
|
|
216
|
+
expect(responseData.announcements).toEqual([]);
|
|
217
|
+
expect(responseData.message).toContain("No announcements found");
|
|
218
|
+
}, 10000);
|
|
219
|
+
});
|
|
220
|
+
});
|