@access-mcp/system-status 0.6.0 → 0.7.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 +17 -6
- package/dist/__tests__/server.integration.test.js +12 -22
- package/dist/__tests__/server.test.js +87 -41
- package/dist/server.d.ts +0 -1
- package/dist/server.js +79 -108
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -29,11 +29,11 @@ Get infrastructure status, outages, and maintenance information for ACCESS-CI re
|
|
|
29
29
|
**Parameters:**
|
|
30
30
|
| Parameter | Type | Description |
|
|
31
31
|
|-----------|------|-------------|
|
|
32
|
-
| `resource` | string | Filter by resource name (e.g., "delta", "bridges2") |
|
|
33
32
|
| `time` | enum | Time period: `current`, `scheduled`, `past`, `all` (default: "current") |
|
|
34
|
-
| `
|
|
33
|
+
| `resource` | string | Filter by resource name (e.g., "delta", "bridges2", "anvil") |
|
|
34
|
+
| `outage_type` | enum | Filter by type: `Full`, `Partial`, `Degraded`, `Reconfiguration` |
|
|
35
|
+
| `ids` | array | Check operational status for specific resources by name or ID |
|
|
35
36
|
| `limit` | number | Max results (default: 50 for "all", 100 for "past") |
|
|
36
|
-
| `use_group_api` | boolean | Use resource group API for status checking (default: false) |
|
|
37
37
|
|
|
38
38
|
**Examples:**
|
|
39
39
|
```javascript
|
|
@@ -46,12 +46,20 @@ get_infrastructure_news({ time: "scheduled" })
|
|
|
46
46
|
// Get comprehensive overview
|
|
47
47
|
get_infrastructure_news({ time: "all" })
|
|
48
48
|
|
|
49
|
-
//
|
|
49
|
+
// Filter to a specific resource
|
|
50
50
|
get_infrastructure_news({ resource: "delta", time: "current" })
|
|
51
51
|
|
|
52
|
-
//
|
|
52
|
+
// Filter by outage type
|
|
53
|
+
get_infrastructure_news({ outage_type: "Full" })
|
|
54
|
+
|
|
55
|
+
// Check operational status of specific resources (by name or ID)
|
|
56
|
+
get_infrastructure_news({
|
|
57
|
+
ids: ["Anvil", "Delta", "Bridges-2"]
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Or using full resource IDs
|
|
53
61
|
get_infrastructure_news({
|
|
54
|
-
|
|
62
|
+
ids: ["delta.ncsa.access-ci.org", "bridges2.psc.access-ci.org"]
|
|
55
63
|
})
|
|
56
64
|
|
|
57
65
|
// Get past outages with limit
|
|
@@ -80,3 +88,6 @@ npm install -g @access-mcp/system-status
|
|
|
80
88
|
## Resources
|
|
81
89
|
|
|
82
90
|
- `accessci://system-status` - Current operational status of all ACCESS-CI resources
|
|
91
|
+
- `accessci://outages/current` - Currently active outages
|
|
92
|
+
- `accessci://outages/scheduled` - Upcoming scheduled maintenance
|
|
93
|
+
- `accessci://outages/past` - Historical outages
|
|
@@ -106,15 +106,15 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
106
106
|
expect(["current", "scheduled", "recent_past"]).toContain(announcement.category);
|
|
107
107
|
}
|
|
108
108
|
}, 15000);
|
|
109
|
-
it("should check resource status
|
|
109
|
+
it("should check resource status using group API", async () => {
|
|
110
110
|
// Test with human-readable names - these get resolved to full IDs
|
|
111
|
+
// The group API is used automatically for efficient per-resource queries
|
|
111
112
|
const result = await server["handleToolCall"]({
|
|
112
113
|
method: "tools/call",
|
|
113
114
|
params: {
|
|
114
115
|
name: "get_infrastructure_news",
|
|
115
116
|
arguments: {
|
|
116
117
|
ids: ["Anvil", "Delta", "Expanse"],
|
|
117
|
-
use_group_api: false,
|
|
118
118
|
},
|
|
119
119
|
},
|
|
120
120
|
});
|
|
@@ -124,7 +124,6 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
124
124
|
expect(responseData).toHaveProperty("resources_checked", 3);
|
|
125
125
|
expect(responseData).toHaveProperty("operational");
|
|
126
126
|
expect(responseData).toHaveProperty("affected");
|
|
127
|
-
expect(responseData).toHaveProperty("api_method", "direct_outages_check");
|
|
128
127
|
expect(Array.isArray(responseData.resource_status)).toBe(true);
|
|
129
128
|
expect(responseData.resource_status).toHaveLength(3);
|
|
130
129
|
// Check resource status structure - IDs should be resolved to full format
|
|
@@ -132,50 +131,40 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
132
131
|
expect(resource).toHaveProperty("resource_id");
|
|
133
132
|
expect(resource.resource_id).toContain(".access-ci.org"); // Resolved to full ID
|
|
134
133
|
expect(resource).toHaveProperty("status");
|
|
135
|
-
expect(["operational", "affected"]).toContain(resource.status);
|
|
134
|
+
expect(["operational", "affected", "unknown"]).toContain(resource.status);
|
|
136
135
|
expect(resource).toHaveProperty("active_outages");
|
|
137
136
|
expect(Array.isArray(resource.outage_details)).toBe(true);
|
|
138
137
|
});
|
|
139
138
|
}, 15000);
|
|
140
|
-
it("should
|
|
141
|
-
// Test
|
|
139
|
+
it("should check single resource status", async () => {
|
|
140
|
+
// Test with a single human-readable name
|
|
142
141
|
const result = await server["handleToolCall"]({
|
|
143
142
|
method: "tools/call",
|
|
144
143
|
params: {
|
|
145
144
|
name: "get_infrastructure_news",
|
|
146
145
|
arguments: {
|
|
147
146
|
ids: ["Anvil"],
|
|
148
|
-
use_group_api: true,
|
|
149
147
|
},
|
|
150
148
|
},
|
|
151
149
|
});
|
|
152
150
|
const content = result.content[0];
|
|
153
151
|
const responseData = JSON.parse(content.text);
|
|
154
|
-
expect(responseData).toHaveProperty("api_method", "resource_group_api");
|
|
155
152
|
expect(responseData).toHaveProperty("resources_checked", 1);
|
|
156
153
|
expect(responseData.resource_status).toHaveLength(1);
|
|
157
154
|
const resourceStatus = responseData.resource_status[0];
|
|
158
155
|
expect(resourceStatus.resource_id).toContain("anvil"); // Resolved ID contains anvil
|
|
159
156
|
expect(resourceStatus.resource_id).toContain(".access-ci.org");
|
|
160
|
-
expect(resourceStatus).toHaveProperty("
|
|
161
|
-
expect(["
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
expect(resourceStatus).toHaveProperty("status");
|
|
165
|
-
expect(["operational", "affected"]).toContain(resourceStatus.status);
|
|
166
|
-
}
|
|
167
|
-
// If it failed, check error handling
|
|
168
|
-
if (resourceStatus.api_method === "group_specific_failed") {
|
|
169
|
-
expect(resourceStatus.status).toBe("unknown");
|
|
170
|
-
expect(resourceStatus).toHaveProperty("error");
|
|
171
|
-
}
|
|
157
|
+
expect(resourceStatus).toHaveProperty("status");
|
|
158
|
+
expect(["operational", "affected", "unknown"]).toContain(resourceStatus.status);
|
|
159
|
+
expect(resourceStatus).toHaveProperty("active_outages");
|
|
160
|
+
expect(resourceStatus).toHaveProperty("outage_details");
|
|
172
161
|
}, 15000);
|
|
173
162
|
it("should filter outages by resource correctly", async () => {
|
|
174
163
|
const result = await server["handleToolCall"]({
|
|
175
164
|
method: "tools/call",
|
|
176
165
|
params: {
|
|
177
166
|
name: "get_infrastructure_news",
|
|
178
|
-
arguments: { time: "current",
|
|
167
|
+
arguments: { time: "current", resource: "anvil" },
|
|
179
168
|
},
|
|
180
169
|
});
|
|
181
170
|
const content = result.content[0];
|
|
@@ -220,11 +209,12 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
220
209
|
describe("Edge Cases and Error Handling", () => {
|
|
221
210
|
it("should handle empty API responses", async () => {
|
|
222
211
|
// This tests the robustness of our logic with potentially empty responses
|
|
212
|
+
// Using a resource filter that won't match anything
|
|
223
213
|
const result = await server["handleToolCall"]({
|
|
224
214
|
method: "tools/call",
|
|
225
215
|
params: {
|
|
226
216
|
name: "get_infrastructure_news",
|
|
227
|
-
arguments: { time: "current",
|
|
217
|
+
arguments: { time: "current", resource: "nonexistent-resource-xyz-12345" },
|
|
228
218
|
},
|
|
229
219
|
});
|
|
230
220
|
const content = result.content[0];
|
|
@@ -15,6 +15,7 @@ describe("SystemStatusServer", () => {
|
|
|
15
15
|
Content: "Critical issue requiring immediate attention",
|
|
16
16
|
OutageStart: "2024-08-27T10:00:00Z",
|
|
17
17
|
OutageEnd: "2024-08-27T11:00:00Z",
|
|
18
|
+
OutageType: "Full",
|
|
18
19
|
AffectedResources: [{ ResourceName: "Anvil", ResourceID: "anvil-1.purdue.access-ci.org" }],
|
|
19
20
|
},
|
|
20
21
|
{
|
|
@@ -23,6 +24,7 @@ describe("SystemStatusServer", () => {
|
|
|
23
24
|
Content: "Regular maintenance window",
|
|
24
25
|
OutageStart: "2024-08-27T08:00:00Z",
|
|
25
26
|
OutageEnd: "2024-08-27T08:30:00Z",
|
|
27
|
+
OutageType: "Partial",
|
|
26
28
|
AffectedResources: [{ ResourceName: "Bridges-2", ResourceID: "bridges2.psc.access-ci.org" }],
|
|
27
29
|
},
|
|
28
30
|
];
|
|
@@ -113,7 +115,7 @@ describe("SystemStatusServer", () => {
|
|
|
113
115
|
method: "tools/call",
|
|
114
116
|
params: {
|
|
115
117
|
name: "get_infrastructure_news",
|
|
116
|
-
arguments: {
|
|
118
|
+
arguments: { resource: "Anvil", time: "current" },
|
|
117
119
|
},
|
|
118
120
|
});
|
|
119
121
|
const content = result.content[0];
|
|
@@ -121,6 +123,59 @@ describe("SystemStatusServer", () => {
|
|
|
121
123
|
expect(response.total_outages).toBe(1);
|
|
122
124
|
expect(response.outages[0].Subject).toContain("Anvil");
|
|
123
125
|
});
|
|
126
|
+
it("should filter outages by outage_type", async () => {
|
|
127
|
+
mockHttpClient.get.mockResolvedValue({
|
|
128
|
+
status: 200,
|
|
129
|
+
data: { results: mockCurrentOutagesData },
|
|
130
|
+
});
|
|
131
|
+
const result = await server["handleToolCall"]({
|
|
132
|
+
method: "tools/call",
|
|
133
|
+
params: {
|
|
134
|
+
name: "get_infrastructure_news",
|
|
135
|
+
arguments: { outage_type: "Full", time: "current" },
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
const content = result.content[0];
|
|
139
|
+
const response = JSON.parse(content.text);
|
|
140
|
+
expect(response.total_outages).toBe(1);
|
|
141
|
+
expect(response.outages[0].OutageType).toBe("Full");
|
|
142
|
+
expect(response.outages[0].Subject).toContain("Anvil");
|
|
143
|
+
});
|
|
144
|
+
it("should filter by both resource and outage_type", async () => {
|
|
145
|
+
mockHttpClient.get.mockResolvedValue({
|
|
146
|
+
status: 200,
|
|
147
|
+
data: { results: mockCurrentOutagesData },
|
|
148
|
+
});
|
|
149
|
+
// Filter for Partial outages on Bridges-2
|
|
150
|
+
const result = await server["handleToolCall"]({
|
|
151
|
+
method: "tools/call",
|
|
152
|
+
params: {
|
|
153
|
+
name: "get_infrastructure_news",
|
|
154
|
+
arguments: { resource: "Bridges", outage_type: "Partial", time: "current" },
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
const content = result.content[0];
|
|
158
|
+
const response = JSON.parse(content.text);
|
|
159
|
+
expect(response.total_outages).toBe(1);
|
|
160
|
+
expect(response.outages[0].OutageType).toBe("Partial");
|
|
161
|
+
});
|
|
162
|
+
it("should return empty when outage_type filter matches nothing", async () => {
|
|
163
|
+
mockHttpClient.get.mockResolvedValue({
|
|
164
|
+
status: 200,
|
|
165
|
+
data: { results: mockCurrentOutagesData },
|
|
166
|
+
});
|
|
167
|
+
const result = await server["handleToolCall"]({
|
|
168
|
+
method: "tools/call",
|
|
169
|
+
params: {
|
|
170
|
+
name: "get_infrastructure_news",
|
|
171
|
+
arguments: { outage_type: "Degraded", time: "current" },
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
const content = result.content[0];
|
|
175
|
+
const response = JSON.parse(content.text);
|
|
176
|
+
expect(response.total_outages).toBe(0);
|
|
177
|
+
expect(response.outages).toHaveLength(0);
|
|
178
|
+
});
|
|
124
179
|
it("should categorize severity correctly", async () => {
|
|
125
180
|
mockHttpClient.get.mockResolvedValue({
|
|
126
181
|
status: 200,
|
|
@@ -282,75 +337,62 @@ describe("SystemStatusServer", () => {
|
|
|
282
337
|
});
|
|
283
338
|
});
|
|
284
339
|
describe("checkResourceStatus", () => {
|
|
285
|
-
it("should check resource status
|
|
286
|
-
|
|
340
|
+
it("should check resource status using group API", async () => {
|
|
341
|
+
// Mock group API responses for each resource
|
|
342
|
+
const now = new Date();
|
|
343
|
+
const activeOutage = {
|
|
344
|
+
Subject: "Current outage on Anvil",
|
|
345
|
+
OutageStart: new Date(now.getTime() - 3600000).toISOString(), // 1 hour ago
|
|
346
|
+
OutageEnd: null, // Still active
|
|
347
|
+
OutageType: "Full",
|
|
348
|
+
};
|
|
349
|
+
mockHttpClient.get
|
|
350
|
+
.mockResolvedValueOnce({
|
|
287
351
|
status: 200,
|
|
288
|
-
data: { results:
|
|
352
|
+
data: { results: [activeOutage] }, // anvil has active outage
|
|
353
|
+
})
|
|
354
|
+
.mockResolvedValueOnce({
|
|
355
|
+
status: 200,
|
|
356
|
+
data: { results: [] }, // unknown resource has no outages
|
|
289
357
|
});
|
|
290
358
|
// Use full IDs with dots to skip resolution lookup
|
|
291
359
|
const result = await server["handleToolCall"]({
|
|
292
360
|
method: "tools/call",
|
|
293
361
|
params: {
|
|
294
362
|
name: "get_infrastructure_news",
|
|
295
|
-
arguments: { ids: ["anvil
|
|
363
|
+
arguments: { ids: ["anvil.purdue.access-ci.org", "unknown.resource.org"] },
|
|
296
364
|
},
|
|
297
365
|
});
|
|
366
|
+
expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/news/v1/info_groupid/anvil.purdue.access-ci.org/");
|
|
367
|
+
expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/news/v1/info_groupid/unknown.resource.org/");
|
|
298
368
|
const content = result.content[0];
|
|
299
369
|
const response = JSON.parse(content.text);
|
|
300
|
-
expect(response.api_method).toBe("direct_outages_check");
|
|
301
370
|
expect(response.resources_checked).toBe(2);
|
|
302
371
|
expect(response.operational).toBe(1); // unknown.resource.org
|
|
303
|
-
expect(response.affected).toBe(1); // anvil
|
|
304
|
-
const anvilStatus = response.resource_status.find((r) => r.resource_id === "anvil
|
|
372
|
+
expect(response.affected).toBe(1); // anvil.purdue.access-ci.org
|
|
373
|
+
const anvilStatus = response.resource_status.find((r) => r.resource_id === "anvil.purdue.access-ci.org");
|
|
305
374
|
expect(anvilStatus.status).toBe("affected");
|
|
306
|
-
expect(anvilStatus.severity).toBe("high"); //
|
|
307
|
-
|
|
308
|
-
it("should use group API when requested", async () => {
|
|
309
|
-
mockHttpClient.get.mockResolvedValue({
|
|
310
|
-
status: 200,
|
|
311
|
-
data: { results: [] }, // No outages for this group
|
|
312
|
-
});
|
|
313
|
-
// Use full ID with dots to skip resolution lookup
|
|
314
|
-
const result = await server["handleToolCall"]({
|
|
315
|
-
method: "tools/call",
|
|
316
|
-
params: {
|
|
317
|
-
name: "get_infrastructure_news",
|
|
318
|
-
arguments: {
|
|
319
|
-
ids: ["anvil.purdue.access-ci.org"],
|
|
320
|
-
use_group_api: true,
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
|
-
});
|
|
324
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/news/v1/info_groupid/anvil.purdue.access-ci.org/");
|
|
325
|
-
const content = result.content[0];
|
|
326
|
-
const response = JSON.parse(content.text);
|
|
327
|
-
expect(response.api_method).toBe("resource_group_api");
|
|
328
|
-
expect(response.resource_status[0].status).toBe("operational");
|
|
329
|
-
expect(response.resource_status[0].api_method).toBe("group_specific");
|
|
375
|
+
expect(anvilStatus.severity).toBe("high"); // Full outage = high
|
|
376
|
+
expect(anvilStatus.outage_details[0].outage_type).toBe("Full");
|
|
330
377
|
});
|
|
331
378
|
it("should handle group API failures gracefully", async () => {
|
|
332
379
|
mockHttpClient.get.mockRejectedValue(new Error("API Error"));
|
|
333
|
-
// Use full ID with dots to skip resolution lookup
|
|
334
380
|
const result = await server["handleToolCall"]({
|
|
335
381
|
method: "tools/call",
|
|
336
382
|
params: {
|
|
337
383
|
name: "get_infrastructure_news",
|
|
338
|
-
arguments: {
|
|
339
|
-
ids: ["invalid.resource.org"],
|
|
340
|
-
use_group_api: true,
|
|
341
|
-
},
|
|
384
|
+
arguments: { ids: ["failing.resource.org"] },
|
|
342
385
|
},
|
|
343
386
|
});
|
|
344
387
|
const content = result.content[0];
|
|
345
388
|
const response = JSON.parse(content.text);
|
|
346
|
-
expect(response).
|
|
389
|
+
expect(response.unknown).toBe(1);
|
|
347
390
|
expect(response.resource_status[0].status).toBe("unknown");
|
|
348
|
-
expect(response.resource_status[0].api_method).toBe("group_specific_failed");
|
|
349
391
|
expect(response.resource_status[0]).toHaveProperty("error");
|
|
350
392
|
});
|
|
351
393
|
it("should resolve human-readable name to resource ID", async () => {
|
|
352
394
|
// First call: resource search for name resolution
|
|
353
|
-
// Second call:
|
|
395
|
+
// Second call: group API for the resolved resource ID
|
|
354
396
|
mockHttpClient.get
|
|
355
397
|
.mockResolvedValueOnce({
|
|
356
398
|
status: 200,
|
|
@@ -364,7 +406,7 @@ describe("SystemStatusServer", () => {
|
|
|
364
406
|
})
|
|
365
407
|
.mockResolvedValueOnce({
|
|
366
408
|
status: 200,
|
|
367
|
-
data: { results: [] }, // No outages
|
|
409
|
+
data: { results: [] }, // No outages from group API
|
|
368
410
|
});
|
|
369
411
|
const result = await server["handleToolCall"]({
|
|
370
412
|
method: "tools/call",
|
|
@@ -373,10 +415,14 @@ describe("SystemStatusServer", () => {
|
|
|
373
415
|
arguments: { ids: ["Anvil"] },
|
|
374
416
|
},
|
|
375
417
|
});
|
|
418
|
+
// Should have called resource search first, then group API
|
|
419
|
+
expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/cider/v1/access-active-groups/type/resource-catalog.access-ci.org/");
|
|
420
|
+
expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/news/v1/info_groupid/anvil.purdue.access-ci.org/");
|
|
376
421
|
const content = result.content[0];
|
|
377
422
|
const response = JSON.parse(content.text);
|
|
378
423
|
expect(response.resources_checked).toBe(1);
|
|
379
424
|
expect(response.resource_status[0].resource_id).toBe("anvil.purdue.access-ci.org");
|
|
425
|
+
expect(response.resource_status[0].status).toBe("operational"); // No current outages
|
|
380
426
|
});
|
|
381
427
|
it("should return error when resource name is ambiguous", async () => {
|
|
382
428
|
mockHttpClient.get.mockResolvedValueOnce({
|
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -30,35 +30,35 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
30
30
|
return [
|
|
31
31
|
{
|
|
32
32
|
name: "get_infrastructure_news",
|
|
33
|
-
description: "Get ACCESS-CI infrastructure status
|
|
33
|
+
description: "Get ACCESS-CI infrastructure status including outages, maintenance, and incidents. With no parameters, returns current active outages. Use 'time' to get scheduled/past outages, 'resource' to filter to a specific system, or 'ids' to check status of specific resources.",
|
|
34
34
|
inputSchema: {
|
|
35
35
|
type: "object",
|
|
36
36
|
properties: {
|
|
37
|
-
query: {
|
|
38
|
-
type: "string",
|
|
39
|
-
description: "Filter by resource name (e.g., 'delta', 'bridges2')",
|
|
40
|
-
},
|
|
41
37
|
time: {
|
|
42
38
|
type: "string",
|
|
43
39
|
enum: ["current", "scheduled", "past", "all"],
|
|
44
|
-
description: "Time
|
|
40
|
+
description: "Time period: 'current' (active now), 'scheduled' (planned/future), 'past' (historical), 'all' (combined view)",
|
|
45
41
|
default: "current",
|
|
46
42
|
},
|
|
43
|
+
resource: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "Filter to a specific resource by name (e.g., 'delta', 'bridges2', 'anvil')",
|
|
46
|
+
},
|
|
47
|
+
outage_type: {
|
|
48
|
+
type: "string",
|
|
49
|
+
enum: ["Full", "Partial", "Degraded", "Reconfiguration"],
|
|
50
|
+
description: "Filter by severity: 'Full' (complete outage), 'Partial' (some services affected), 'Degraded' (reduced performance), 'Reconfiguration' (system changes)",
|
|
51
|
+
},
|
|
47
52
|
ids: {
|
|
48
53
|
type: "array",
|
|
49
54
|
items: { type: "string" },
|
|
50
|
-
description: "Check status for specific resources. Accepts names (
|
|
55
|
+
description: "Check operational status for specific resources. Returns 'operational' or 'affected' for each. Accepts names ('Anvil') or IDs ('anvil.purdue.access-ci.org')",
|
|
51
56
|
},
|
|
52
57
|
limit: {
|
|
53
58
|
type: "number",
|
|
54
|
-
description: "Max results
|
|
59
|
+
description: "Max results to return",
|
|
55
60
|
default: 50,
|
|
56
61
|
},
|
|
57
|
-
use_group_api: {
|
|
58
|
-
type: "boolean",
|
|
59
|
-
description: "Use group API for status (with ids only)",
|
|
60
|
-
default: false,
|
|
61
|
-
},
|
|
62
62
|
},
|
|
63
63
|
},
|
|
64
64
|
},
|
|
@@ -99,11 +99,11 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
99
99
|
switch (name) {
|
|
100
100
|
case "get_infrastructure_news":
|
|
101
101
|
return await this.getInfrastructureNewsRouter({
|
|
102
|
-
resource: typedArgs.
|
|
102
|
+
resource: typedArgs.resource,
|
|
103
103
|
time: typedArgs.time,
|
|
104
|
+
outage_type: typedArgs.outage_type,
|
|
104
105
|
resource_ids: typedArgs.ids,
|
|
105
106
|
limit: typedArgs.limit,
|
|
106
|
-
use_group_api: typedArgs.use_group_api,
|
|
107
107
|
});
|
|
108
108
|
default:
|
|
109
109
|
return this.errorResponse(`Unknown tool: ${name}`);
|
|
@@ -118,21 +118,21 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
118
118
|
* Routes to appropriate handler based on parameters
|
|
119
119
|
*/
|
|
120
120
|
async getInfrastructureNewsRouter(args) {
|
|
121
|
-
const { resource, time = "current", resource_ids, limit
|
|
121
|
+
const { resource, time = "current", outage_type, resource_ids, limit } = args;
|
|
122
122
|
// Check resource status (returns operational/affected) - only if IDs provided
|
|
123
123
|
if (resource_ids && Array.isArray(resource_ids) && resource_ids.length > 0) {
|
|
124
|
-
return await this.checkResourceStatus(resource_ids
|
|
124
|
+
return await this.checkResourceStatus(resource_ids);
|
|
125
125
|
}
|
|
126
126
|
// Time-based routing
|
|
127
127
|
switch (time) {
|
|
128
128
|
case "current":
|
|
129
|
-
return await this.getCurrentOutages(resource);
|
|
129
|
+
return await this.getCurrentOutages(resource, outage_type);
|
|
130
130
|
case "scheduled":
|
|
131
|
-
return await this.getScheduledMaintenance(resource);
|
|
131
|
+
return await this.getScheduledMaintenance(resource, outage_type);
|
|
132
132
|
case "past":
|
|
133
|
-
return await this.getPastOutages(resource, limit || 100);
|
|
133
|
+
return await this.getPastOutages(resource, outage_type, limit || 100);
|
|
134
134
|
case "all":
|
|
135
|
-
return await this.getSystemAnnouncements(limit || 50);
|
|
135
|
+
return await this.getSystemAnnouncements(outage_type, limit || 50);
|
|
136
136
|
default:
|
|
137
137
|
throw new Error(`Invalid time parameter: ${time}. Must be one of: current, scheduled, past, all`);
|
|
138
138
|
}
|
|
@@ -197,9 +197,13 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
197
197
|
throw new Error(`Unknown resource: ${uri}`);
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
|
-
async getCurrentOutages(resourceFilter) {
|
|
200
|
+
async getCurrentOutages(resourceFilter, outageTypeFilter) {
|
|
201
201
|
const response = await this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/current_outages/");
|
|
202
202
|
let outages = response.data.results || [];
|
|
203
|
+
// Filter by outage type if specified
|
|
204
|
+
if (outageTypeFilter) {
|
|
205
|
+
outages = outages.filter((outage) => outage.OutageType?.toLowerCase() === outageTypeFilter.toLowerCase());
|
|
206
|
+
}
|
|
203
207
|
// Filter by resource if specified
|
|
204
208
|
if (resourceFilter) {
|
|
205
209
|
const filter = resourceFilter.toLowerCase();
|
|
@@ -251,9 +255,13 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
251
255
|
],
|
|
252
256
|
};
|
|
253
257
|
}
|
|
254
|
-
async getScheduledMaintenance(resourceFilter) {
|
|
258
|
+
async getScheduledMaintenance(resourceFilter, outageTypeFilter) {
|
|
255
259
|
const response = await this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/future_outages/");
|
|
256
260
|
let maintenance = response.data.results || [];
|
|
261
|
+
// Filter by outage type if specified
|
|
262
|
+
if (outageTypeFilter) {
|
|
263
|
+
maintenance = maintenance.filter((item) => item.OutageType?.toLowerCase() === outageTypeFilter.toLowerCase());
|
|
264
|
+
}
|
|
257
265
|
// Filter by resource if specified
|
|
258
266
|
if (resourceFilter) {
|
|
259
267
|
const filter = resourceFilter.toLowerCase();
|
|
@@ -315,9 +323,13 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
315
323
|
],
|
|
316
324
|
};
|
|
317
325
|
}
|
|
318
|
-
async getPastOutages(resourceFilter, limit = 100) {
|
|
326
|
+
async getPastOutages(resourceFilter, outageTypeFilter, limit = 100) {
|
|
319
327
|
const response = await this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/past_outages/");
|
|
320
328
|
let pastOutages = response.data.results || [];
|
|
329
|
+
// Filter by outage type if specified
|
|
330
|
+
if (outageTypeFilter) {
|
|
331
|
+
pastOutages = pastOutages.filter((outage) => outage.OutageType?.toLowerCase() === outageTypeFilter.toLowerCase());
|
|
332
|
+
}
|
|
321
333
|
// Filter by resource if specified
|
|
322
334
|
if (resourceFilter) {
|
|
323
335
|
const filter = resourceFilter.toLowerCase();
|
|
@@ -392,16 +404,23 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
392
404
|
],
|
|
393
405
|
};
|
|
394
406
|
}
|
|
395
|
-
async getSystemAnnouncements(limit = 50) {
|
|
407
|
+
async getSystemAnnouncements(outageTypeFilter, limit = 50) {
|
|
396
408
|
// Get current, future, and recent past announcements for comprehensive view
|
|
397
409
|
const [currentResponse, futureResponse, pastResponse] = await Promise.all([
|
|
398
410
|
this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/current_outages/"),
|
|
399
411
|
this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/future_outages/"),
|
|
400
412
|
this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/past_outages/"),
|
|
401
413
|
]);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
414
|
+
let currentOutages = currentResponse.data.results || [];
|
|
415
|
+
let futureOutages = futureResponse.data.results || [];
|
|
416
|
+
let pastOutagesData = pastResponse.data.results || [];
|
|
417
|
+
// Filter by outage type if specified
|
|
418
|
+
if (outageTypeFilter) {
|
|
419
|
+
const typeFilter = outageTypeFilter.toLowerCase();
|
|
420
|
+
currentOutages = currentOutages.filter((o) => o.OutageType?.toLowerCase() === typeFilter);
|
|
421
|
+
futureOutages = futureOutages.filter((o) => o.OutageType?.toLowerCase() === typeFilter);
|
|
422
|
+
pastOutagesData = pastOutagesData.filter((o) => o.OutageType?.toLowerCase() === typeFilter);
|
|
423
|
+
}
|
|
405
424
|
// Filter recent past outages (last 30 days) for announcements
|
|
406
425
|
const recentPastOutages = pastOutagesData.filter((outage) => {
|
|
407
426
|
const endTime = new Date(outage.OutageEnd || "");
|
|
@@ -449,7 +468,7 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
449
468
|
],
|
|
450
469
|
};
|
|
451
470
|
}
|
|
452
|
-
async checkResourceStatus(resourceIds
|
|
471
|
+
async checkResourceStatus(resourceIds) {
|
|
453
472
|
if (!resourceIds || !Array.isArray(resourceIds) || resourceIds.length === 0) {
|
|
454
473
|
throw new Error("resource_ids parameter is required and must be a non-empty array of resource IDs");
|
|
455
474
|
}
|
|
@@ -483,97 +502,50 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
483
502
|
],
|
|
484
503
|
};
|
|
485
504
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
// Efficient approach: fetch raw current outages data once
|
|
490
|
-
const response = await this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/current_outages/");
|
|
491
|
-
const rawOutages = response.data.results || [];
|
|
492
|
-
const resourceStatus = resolvedIds.map((resourceId) => {
|
|
493
|
-
const affectedOutages = rawOutages.filter((outage) => outage.AffectedResources?.some((resource) => resource.ResourceID?.toString() === resourceId ||
|
|
494
|
-
resource.ResourceName?.toLowerCase().includes(resourceId.toLowerCase())));
|
|
495
|
-
let status = "operational";
|
|
496
|
-
let severity = null;
|
|
497
|
-
if (affectedOutages.length > 0) {
|
|
498
|
-
status = "affected";
|
|
499
|
-
// Get highest severity using same logic as getCurrentOutages
|
|
500
|
-
const severities = affectedOutages.map((outage) => {
|
|
501
|
-
const subject = outage.Subject?.toLowerCase() || "";
|
|
502
|
-
if (subject.includes("emergency") || subject.includes("critical")) {
|
|
503
|
-
return "high";
|
|
504
|
-
}
|
|
505
|
-
else if (subject.includes("maintenance") || subject.includes("scheduled")) {
|
|
506
|
-
return "low";
|
|
507
|
-
}
|
|
508
|
-
else {
|
|
509
|
-
return "medium";
|
|
510
|
-
}
|
|
511
|
-
});
|
|
512
|
-
if (severities.includes("high"))
|
|
513
|
-
severity = "high";
|
|
514
|
-
else if (severities.includes("medium"))
|
|
515
|
-
severity = "medium";
|
|
516
|
-
else
|
|
517
|
-
severity = "low";
|
|
518
|
-
}
|
|
519
|
-
return {
|
|
520
|
-
resource_id: resourceId,
|
|
521
|
-
status,
|
|
522
|
-
severity,
|
|
523
|
-
active_outages: affectedOutages.length,
|
|
524
|
-
outage_details: affectedOutages.map((outage) => ({
|
|
525
|
-
subject: outage.Subject,
|
|
526
|
-
severity,
|
|
527
|
-
})),
|
|
528
|
-
};
|
|
529
|
-
});
|
|
530
|
-
return {
|
|
531
|
-
content: [
|
|
532
|
-
{
|
|
533
|
-
type: "text",
|
|
534
|
-
text: JSON.stringify({
|
|
535
|
-
checked_at: new Date().toISOString(),
|
|
536
|
-
resources_checked: resolvedIds.length,
|
|
537
|
-
operational: resourceStatus.filter((r) => r.status === "operational").length,
|
|
538
|
-
affected: resourceStatus.filter((r) => r.status === "affected").length,
|
|
539
|
-
api_method: "direct_outages_check",
|
|
540
|
-
resource_status: resourceStatus,
|
|
541
|
-
}, null, 2),
|
|
542
|
-
},
|
|
543
|
-
],
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
async checkResourceStatusViaGroups(resourceIds) {
|
|
547
|
-
if (!resourceIds || !Array.isArray(resourceIds) || resourceIds.length === 0) {
|
|
548
|
-
throw new Error("resource_ids parameter is required and must be a non-empty array of resource IDs");
|
|
549
|
-
}
|
|
550
|
-
// Try to use the more efficient group-based API
|
|
551
|
-
const statusPromises = resourceIds.map(async (resourceId) => {
|
|
505
|
+
// Use group API for efficient per-resource queries
|
|
506
|
+
const now = new Date();
|
|
507
|
+
const statusPromises = resolvedIds.map(async (resourceId) => {
|
|
552
508
|
try {
|
|
553
509
|
const response = await this.httpClient.get(`/wh2/news/v1/info_groupid/${resourceId}/`);
|
|
554
|
-
const
|
|
555
|
-
|
|
510
|
+
const allOutages = response.data.results || [];
|
|
511
|
+
// Filter to current outages only (started and not ended)
|
|
512
|
+
const currentOutages = allOutages.filter((outage) => {
|
|
513
|
+
const start = outage.OutageStart ? new Date(outage.OutageStart) : null;
|
|
514
|
+
const end = outage.OutageEnd ? new Date(outage.OutageEnd) : null;
|
|
515
|
+
return start && start <= now && (!end || end > now);
|
|
516
|
+
});
|
|
517
|
+
let status = "operational";
|
|
518
|
+
let severity = null;
|
|
519
|
+
if (currentOutages.length > 0) {
|
|
520
|
+
status = "affected";
|
|
521
|
+
// Determine severity from OutageType
|
|
522
|
+
const types = currentOutages.map((o) => o.OutageType?.toLowerCase());
|
|
523
|
+
if (types.includes("full"))
|
|
524
|
+
severity = "high";
|
|
525
|
+
else if (types.includes("degraded") || types.includes("partial"))
|
|
526
|
+
severity = "medium";
|
|
527
|
+
else
|
|
528
|
+
severity = "low";
|
|
529
|
+
}
|
|
556
530
|
return {
|
|
557
531
|
resource_id: resourceId,
|
|
558
|
-
status
|
|
559
|
-
severity
|
|
560
|
-
active_outages:
|
|
561
|
-
outage_details:
|
|
532
|
+
status,
|
|
533
|
+
severity,
|
|
534
|
+
active_outages: currentOutages.length,
|
|
535
|
+
outage_details: currentOutages.map((outage) => ({
|
|
562
536
|
subject: outage.Subject,
|
|
537
|
+
outage_type: outage.OutageType,
|
|
563
538
|
})),
|
|
564
|
-
api_method: "group_specific",
|
|
565
539
|
};
|
|
566
540
|
}
|
|
567
541
|
catch {
|
|
568
|
-
// Fallback to general check if group API fails
|
|
569
542
|
return {
|
|
570
543
|
resource_id: resourceId,
|
|
571
544
|
status: "unknown",
|
|
572
545
|
severity: null,
|
|
573
546
|
active_outages: 0,
|
|
574
547
|
outage_details: [],
|
|
575
|
-
error: `
|
|
576
|
-
api_method: "group_specific_failed",
|
|
548
|
+
error: `Failed to fetch status for ${resourceId}`,
|
|
577
549
|
};
|
|
578
550
|
}
|
|
579
551
|
});
|
|
@@ -583,12 +555,11 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
583
555
|
{
|
|
584
556
|
type: "text",
|
|
585
557
|
text: JSON.stringify({
|
|
586
|
-
checked_at:
|
|
587
|
-
resources_checked:
|
|
558
|
+
checked_at: now.toISOString(),
|
|
559
|
+
resources_checked: resolvedIds.length,
|
|
588
560
|
operational: resourceStatus.filter((r) => r.status === "operational").length,
|
|
589
561
|
affected: resourceStatus.filter((r) => r.status === "affected").length,
|
|
590
562
|
unknown: resourceStatus.filter((r) => r.status === "unknown").length,
|
|
591
|
-
api_method: "resource_group_api",
|
|
592
563
|
resource_status: resourceStatus,
|
|
593
564
|
}, null, 2),
|
|
594
565
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@access-mcp/system-status",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "MCP server for ACCESS-CI System Status and Outages API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
"node": ">=18.0.0"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@access-mcp/shared": "
|
|
47
|
+
"@access-mcp/shared": "*",
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.16.0",
|
|
48
49
|
"express": "^4.18.0"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|