@access-mcp/system-status 0.2.3 → 0.4.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.
@@ -0,0 +1,380 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2
+ import { SystemStatusServer } from "../server.js";
3
+ // Mock axios
4
+ vi.mock("axios");
5
+ describe("SystemStatusServer", () => {
6
+ let server;
7
+ let mockHttpClient;
8
+ const mockCurrentOutagesData = [
9
+ {
10
+ id: "1",
11
+ Subject: "Emergency maintenance on Anvil",
12
+ Content: "Critical issue requiring immediate attention",
13
+ CreationTime: "2024-08-27T10:00:00Z",
14
+ LastModificationTime: "2024-08-27T11:00:00Z",
15
+ AffectedResources: [
16
+ { ResourceName: "Anvil", ResourceID: "anvil-1" }
17
+ ]
18
+ },
19
+ {
20
+ id: "2",
21
+ Subject: "Scheduled maintenance on Bridges-2",
22
+ Content: "Regular maintenance window",
23
+ CreationTime: "2024-08-27T08:00:00Z",
24
+ LastModificationTime: "2024-08-27T08:30:00Z",
25
+ AffectedResources: [
26
+ { ResourceName: "Bridges-2", ResourceID: "bridges2-1" }
27
+ ]
28
+ }
29
+ ];
30
+ const mockFutureOutagesData = [
31
+ {
32
+ id: "3",
33
+ Subject: "Scheduled Jetstream maintenance",
34
+ Content: "Planned maintenance",
35
+ CreationTime: "2024-08-27T09:00:00Z",
36
+ LastModificationTime: "2024-08-27T09:00:00Z",
37
+ OutageStartDateTime: "2024-08-30T10:00:00Z",
38
+ OutageEndDateTime: "2024-08-30T14:00:00Z",
39
+ AffectedResources: [
40
+ { ResourceName: "Jetstream", ResourceID: "jetstream-1" }
41
+ ]
42
+ }
43
+ ];
44
+ const mockPastOutagesData = [
45
+ {
46
+ id: "4",
47
+ Subject: "Past maintenance on Stampede3",
48
+ Content: "Completed maintenance",
49
+ CreationTime: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), // 5 days ago
50
+ LastModificationTime: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), // 2 days ago
51
+ OutageStartDateTime: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days ago
52
+ OutageEndDateTime: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000 + 6 * 60 * 60 * 1000).toISOString(), // 3 days ago + 6 hours
53
+ OutageType: "Full",
54
+ AffectedResources: [
55
+ { ResourceName: "Stampede3", ResourceID: "stampede3-1" }
56
+ ]
57
+ }
58
+ ];
59
+ beforeEach(() => {
60
+ server = new SystemStatusServer();
61
+ // Set up mock HTTP client
62
+ mockHttpClient = {
63
+ get: vi.fn(),
64
+ };
65
+ // Override the httpClient getter
66
+ Object.defineProperty(server, "httpClient", {
67
+ get: () => mockHttpClient,
68
+ configurable: true,
69
+ });
70
+ });
71
+ afterEach(() => {
72
+ vi.clearAllMocks();
73
+ });
74
+ describe("Server Initialization", () => {
75
+ it("should initialize with correct server info", () => {
76
+ expect(server).toBeDefined();
77
+ expect(server["serverName"]).toBe("access-mcp-system-status");
78
+ expect(server["version"]).toBe("0.4.0");
79
+ expect(server["baseURL"]).toBe("https://operations-api.access-ci.org");
80
+ });
81
+ it("should provide correct tools", () => {
82
+ const tools = server["getTools"]();
83
+ expect(tools).toHaveLength(5);
84
+ expect(tools.map(t => t.name)).toEqual([
85
+ "get_current_outages",
86
+ "get_scheduled_maintenance",
87
+ "get_past_outages",
88
+ "get_system_announcements",
89
+ "check_resource_status"
90
+ ]);
91
+ });
92
+ it("should provide correct resources", () => {
93
+ const resources = server["getResources"]();
94
+ expect(resources).toHaveLength(4);
95
+ expect(resources.map(r => r.uri)).toEqual([
96
+ "accessci://system-status",
97
+ "accessci://outages/current",
98
+ "accessci://outages/scheduled",
99
+ "accessci://outages/past"
100
+ ]);
101
+ });
102
+ });
103
+ describe("getCurrentOutages", () => {
104
+ it("should fetch and enhance current outages", async () => {
105
+ mockHttpClient.get.mockResolvedValue({
106
+ status: 200,
107
+ data: { results: mockCurrentOutagesData }
108
+ });
109
+ const result = await server["handleToolCall"]({
110
+ params: { name: "get_current_outages", arguments: {} }
111
+ });
112
+ expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/news/v1/affiliation/access-ci.org/current_outages/");
113
+ const response = JSON.parse(result.content[0].text);
114
+ expect(response.total_outages).toBe(2);
115
+ expect(response.affected_resources).toEqual(["Anvil", "Bridges-2"]);
116
+ expect(response.severity_counts).toHaveProperty("high", 1); // Emergency
117
+ expect(response.severity_counts).toHaveProperty("low", 1); // Scheduled maintenance
118
+ expect(response.outages[0]).toHaveProperty("severity");
119
+ expect(response.outages[0]).toHaveProperty("posted_time");
120
+ });
121
+ it("should filter outages by resource", async () => {
122
+ mockHttpClient.get.mockResolvedValue({
123
+ status: 200,
124
+ data: { results: mockCurrentOutagesData }
125
+ });
126
+ const result = await server["handleToolCall"]({
127
+ params: {
128
+ name: "get_current_outages",
129
+ arguments: { resource_filter: "Anvil" }
130
+ }
131
+ });
132
+ const response = JSON.parse(result.content[0].text);
133
+ expect(response.total_outages).toBe(1);
134
+ expect(response.outages[0].Subject).toContain("Anvil");
135
+ });
136
+ it("should categorize severity correctly", async () => {
137
+ mockHttpClient.get.mockResolvedValue({
138
+ status: 200,
139
+ data: { results: mockCurrentOutagesData }
140
+ });
141
+ const result = await server["handleToolCall"]({
142
+ params: { name: "get_current_outages", arguments: {} }
143
+ });
144
+ const response = JSON.parse(result.content[0].text);
145
+ const emergencyOutage = response.outages.find((o) => o.Subject.includes("Emergency"));
146
+ const maintenanceOutage = response.outages.find((o) => o.Subject.includes("Scheduled"));
147
+ expect(emergencyOutage.severity).toBe("high");
148
+ expect(maintenanceOutage.severity).toBe("low");
149
+ });
150
+ });
151
+ describe("getScheduledMaintenance", () => {
152
+ it("should fetch and enhance scheduled maintenance", async () => {
153
+ mockHttpClient.get.mockResolvedValue({
154
+ status: 200,
155
+ data: { results: mockFutureOutagesData }
156
+ });
157
+ const result = await server["handleToolCall"]({
158
+ params: { name: "get_scheduled_maintenance", arguments: {} }
159
+ });
160
+ const response = JSON.parse(result.content[0].text);
161
+ expect(response.total_scheduled).toBe(1);
162
+ expect(response.affected_resources).toEqual(["Jetstream"]);
163
+ expect(response.maintenance[0]).toHaveProperty("hours_until_start");
164
+ expect(response.maintenance[0]).toHaveProperty("duration_hours", 4); // 10am to 2pm = 4 hours
165
+ expect(response.maintenance[0]).toHaveProperty("has_scheduled_time", true);
166
+ });
167
+ it("should handle missing scheduled times", async () => {
168
+ const dataWithoutSchedule = [{
169
+ ...mockFutureOutagesData[0],
170
+ OutageStartDateTime: null,
171
+ OutageEndDateTime: null
172
+ }];
173
+ mockHttpClient.get.mockResolvedValue({
174
+ status: 200,
175
+ data: { results: dataWithoutSchedule }
176
+ });
177
+ const result = await server["handleToolCall"]({
178
+ params: { name: "get_scheduled_maintenance", arguments: {} }
179
+ });
180
+ const response = JSON.parse(result.content[0].text);
181
+ expect(response.maintenance[0].has_scheduled_time).toBe(false);
182
+ expect(response.maintenance[0].duration_hours).toBe(null);
183
+ });
184
+ it("should sort by scheduled start time", async () => {
185
+ const multipleMaintenanceData = [
186
+ {
187
+ ...mockFutureOutagesData[0],
188
+ OutageStartDateTime: "2024-08-31T10:00:00Z", // Later
189
+ Subject: "Later maintenance"
190
+ },
191
+ {
192
+ ...mockFutureOutagesData[0],
193
+ OutageStartDateTime: "2024-08-30T10:00:00Z", // Earlier
194
+ Subject: "Earlier maintenance"
195
+ }
196
+ ];
197
+ mockHttpClient.get.mockResolvedValue({
198
+ status: 200,
199
+ data: { results: multipleMaintenanceData }
200
+ });
201
+ const result = await server["handleToolCall"]({
202
+ params: { name: "get_scheduled_maintenance", arguments: {} }
203
+ });
204
+ const response = JSON.parse(result.content[0].text);
205
+ expect(response.maintenance[0].Subject).toBe("Earlier maintenance");
206
+ expect(response.maintenance[1].Subject).toBe("Later maintenance");
207
+ });
208
+ });
209
+ describe("getPastOutages", () => {
210
+ it("should fetch and enhance past outages", async () => {
211
+ mockHttpClient.get.mockResolvedValue({
212
+ status: 200,
213
+ data: { results: mockPastOutagesData }
214
+ });
215
+ const result = await server["handleToolCall"]({
216
+ params: { name: "get_past_outages", arguments: {} }
217
+ });
218
+ const response = JSON.parse(result.content[0].text);
219
+ expect(response.total_past_outages).toBe(1);
220
+ expect(response.outage_types).toEqual(["Full"]);
221
+ expect(response.average_duration_hours).toBe(6); // 6 hour duration
222
+ expect(response.outages[0]).toHaveProperty("duration_hours", 6);
223
+ expect(response.outages[0]).toHaveProperty("days_ago");
224
+ });
225
+ it("should apply limit correctly", async () => {
226
+ const manyOutages = Array(50).fill(0).map((_, i) => ({
227
+ ...mockPastOutagesData[0],
228
+ id: `past-${i}`,
229
+ Subject: `Past outage ${i}`
230
+ }));
231
+ mockHttpClient.get.mockResolvedValue({
232
+ status: 200,
233
+ data: { results: manyOutages }
234
+ });
235
+ const result = await server["handleToolCall"]({
236
+ params: {
237
+ name: "get_past_outages",
238
+ arguments: { limit: 10 }
239
+ }
240
+ });
241
+ const response = JSON.parse(result.content[0].text);
242
+ expect(response.outages).toHaveLength(10);
243
+ });
244
+ });
245
+ describe("getSystemAnnouncements", () => {
246
+ it("should combine current, future, and recent past outages", async () => {
247
+ mockHttpClient.get
248
+ .mockResolvedValueOnce({ status: 200, data: { results: mockCurrentOutagesData } })
249
+ .mockResolvedValueOnce({ status: 200, data: { results: mockFutureOutagesData } })
250
+ .mockResolvedValueOnce({ status: 200, data: { results: mockPastOutagesData } });
251
+ const result = await server["handleToolCall"]({
252
+ params: { name: "get_system_announcements", arguments: {} }
253
+ });
254
+ expect(mockHttpClient.get).toHaveBeenCalledTimes(3);
255
+ const response = JSON.parse(result.content[0].text);
256
+ expect(response.current_outages).toBe(2);
257
+ expect(response.scheduled_maintenance).toBe(1);
258
+ expect(response.recent_past_outages).toBe(1); // Within 30 days
259
+ expect(response.categories).toHaveProperty("current");
260
+ expect(response.categories).toHaveProperty("scheduled");
261
+ expect(response.categories).toHaveProperty("recent_past");
262
+ });
263
+ it("should prioritize current outages in sorting", async () => {
264
+ mockHttpClient.get
265
+ .mockResolvedValueOnce({ status: 200, data: { results: mockCurrentOutagesData } })
266
+ .mockResolvedValueOnce({ status: 200, data: { results: mockFutureOutagesData } })
267
+ .mockResolvedValueOnce({ status: 200, data: { results: mockPastOutagesData } });
268
+ const result = await server["handleToolCall"]({
269
+ params: { name: "get_system_announcements", arguments: {} }
270
+ });
271
+ const response = JSON.parse(result.content[0].text);
272
+ const firstAnnouncement = response.announcements[0];
273
+ expect(firstAnnouncement.category).toBe("current");
274
+ });
275
+ });
276
+ describe("checkResourceStatus", () => {
277
+ it("should check resource status efficiently (direct method)", async () => {
278
+ mockHttpClient.get.mockResolvedValue({
279
+ status: 200,
280
+ data: { results: mockCurrentOutagesData }
281
+ });
282
+ const result = await server["handleToolCall"]({
283
+ params: {
284
+ name: "check_resource_status",
285
+ arguments: { resource_ids: ["anvil-1", "unknown-resource"] }
286
+ }
287
+ });
288
+ const response = JSON.parse(result.content[0].text);
289
+ expect(response.api_method).toBe("direct_outages_check");
290
+ expect(response.resources_checked).toBe(2);
291
+ expect(response.operational).toBe(1); // unknown-resource
292
+ expect(response.affected).toBe(1); // anvil-1
293
+ const anvilStatus = response.resource_status.find((r) => r.resource_id === "anvil-1");
294
+ expect(anvilStatus.status).toBe("affected");
295
+ expect(anvilStatus.severity).toBe("high"); // Emergency maintenance
296
+ });
297
+ it("should use group API when requested", async () => {
298
+ mockHttpClient.get.mockResolvedValue({
299
+ status: 200,
300
+ data: { results: [] } // No outages for this group
301
+ });
302
+ const result = await server["handleToolCall"]({
303
+ params: {
304
+ name: "check_resource_status",
305
+ arguments: {
306
+ resource_ids: ["anvil"],
307
+ use_group_api: true
308
+ }
309
+ }
310
+ });
311
+ expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/news/v1/info_groupid/anvil/");
312
+ const response = JSON.parse(result.content[0].text);
313
+ expect(response.api_method).toBe("resource_group_api");
314
+ expect(response.resource_status[0].status).toBe("operational");
315
+ expect(response.resource_status[0].api_method).toBe("group_specific");
316
+ });
317
+ it("should handle group API failures gracefully", async () => {
318
+ mockHttpClient.get.mockRejectedValue(new Error("API Error"));
319
+ const result = await server["handleToolCall"]({
320
+ params: {
321
+ name: "check_resource_status",
322
+ arguments: {
323
+ resource_ids: ["invalid-resource"],
324
+ use_group_api: true
325
+ }
326
+ }
327
+ });
328
+ const response = JSON.parse(result.content[0].text);
329
+ expect(response.unknown).toBe(1);
330
+ expect(response.resource_status[0].status).toBe("unknown");
331
+ expect(response.resource_status[0].api_method).toBe("group_specific_failed");
332
+ expect(response.resource_status[0]).toHaveProperty("error");
333
+ });
334
+ });
335
+ describe("Error Handling", () => {
336
+ it("should handle API errors gracefully", async () => {
337
+ mockHttpClient.get.mockResolvedValue({
338
+ status: 500,
339
+ statusText: "Internal Server Error"
340
+ });
341
+ const result = await server["handleToolCall"]({
342
+ params: { name: "get_current_outages", arguments: {} }
343
+ });
344
+ expect(result.content[0].text).toContain("Error");
345
+ });
346
+ it("should handle network errors", async () => {
347
+ mockHttpClient.get.mockRejectedValue(new Error("Network error"));
348
+ const result = await server["handleToolCall"]({
349
+ params: { name: "get_current_outages", arguments: {} }
350
+ });
351
+ expect(result.content[0].text).toContain("Error");
352
+ });
353
+ it("should handle unknown tools", async () => {
354
+ const result = await server["handleToolCall"]({
355
+ params: { name: "unknown_tool", arguments: {} }
356
+ });
357
+ expect(result.content[0].text).toContain("Unknown tool");
358
+ });
359
+ });
360
+ describe("Resource Handling", () => {
361
+ it("should handle resource reads correctly", async () => {
362
+ mockHttpClient.get.mockResolvedValue({
363
+ status: 200,
364
+ data: { results: mockCurrentOutagesData }
365
+ });
366
+ const result = await server["handleResourceRead"]({
367
+ params: { uri: "accessci://outages/current" }
368
+ });
369
+ expect(result.contents[0].mimeType).toBe("application/json");
370
+ expect(result.contents[0].text).toBeDefined();
371
+ });
372
+ it("should handle unknown resources", async () => {
373
+ await expect(async () => {
374
+ await server["handleResourceRead"]({
375
+ params: { uri: "accessci://unknown" }
376
+ });
377
+ }).rejects.toThrow("Unknown resource");
378
+ });
379
+ });
380
+ });
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { SystemStatusServer } from './server.js';
3
- import { startWebServer } from './web-server.js';
2
+ import { SystemStatusServer } from "./server.js";
3
+ import { startWebServer } from "./web-server.js";
4
4
  async function main() {
5
5
  // Check if we should run as web server (for deployment)
6
6
  const port = process.env.PORT;
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BaseAccessServer } from '@access-mcp/shared';
1
+ import { BaseAccessServer } from "@access-mcp/shared";
2
2
  export declare class SystemStatusServer extends BaseAccessServer {
3
3
  constructor();
4
4
  protected getTools(): ({
@@ -13,6 +13,26 @@ export declare class SystemStatusServer extends BaseAccessServer {
13
13
  };
14
14
  limit?: undefined;
15
15
  resource_ids?: undefined;
16
+ use_group_api?: undefined;
17
+ };
18
+ required: never[];
19
+ };
20
+ } | {
21
+ name: string;
22
+ description: string;
23
+ inputSchema: {
24
+ type: string;
25
+ properties: {
26
+ resource_filter: {
27
+ type: string;
28
+ description: string;
29
+ };
30
+ limit: {
31
+ type: string;
32
+ description: string;
33
+ };
34
+ resource_ids?: undefined;
35
+ use_group_api?: undefined;
16
36
  };
17
37
  required: never[];
18
38
  };
@@ -28,6 +48,7 @@ export declare class SystemStatusServer extends BaseAccessServer {
28
48
  };
29
49
  resource_filter?: undefined;
30
50
  resource_ids?: undefined;
51
+ use_group_api?: undefined;
31
52
  };
32
53
  required: never[];
33
54
  };
@@ -44,6 +65,11 @@ export declare class SystemStatusServer extends BaseAccessServer {
44
65
  };
45
66
  description: string;
46
67
  };
68
+ use_group_api: {
69
+ type: string;
70
+ description: string;
71
+ default: boolean;
72
+ };
47
73
  resource_filter?: undefined;
48
74
  limit?: undefined;
49
75
  };
@@ -71,6 +97,8 @@ export declare class SystemStatusServer extends BaseAccessServer {
71
97
  }>;
72
98
  private getCurrentOutages;
73
99
  private getScheduledMaintenance;
100
+ private getPastOutages;
74
101
  private getSystemAnnouncements;
75
102
  private checkResourceStatus;
103
+ private checkResourceStatusViaGroups;
76
104
  }