@access-mcp/system-status 0.5.1 → 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 +25 -30
- package/dist/__tests__/server.test.js +143 -34
- package/dist/server.d.ts +5 -1
- package/dist/server.js +128 -107
- 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
|
|
110
|
-
// Test with
|
|
109
|
+
it("should check resource status using group API", async () => {
|
|
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
|
-
ids: ["
|
|
117
|
-
use_group_api: false,
|
|
117
|
+
ids: ["Anvil", "Delta", "Expanse"],
|
|
118
118
|
},
|
|
119
119
|
},
|
|
120
120
|
});
|
|
@@ -124,56 +124,47 @@ 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
|
-
// Check resource status structure
|
|
129
|
+
// Check resource status structure - IDs should be resolved to full format
|
|
131
130
|
responseData.resource_status.forEach((resource) => {
|
|
132
131
|
expect(resource).toHaveProperty("resource_id");
|
|
132
|
+
expect(resource.resource_id).toContain(".access-ci.org"); // Resolved to full ID
|
|
133
133
|
expect(resource).toHaveProperty("status");
|
|
134
|
-
expect(["operational", "affected"]).toContain(resource.status);
|
|
134
|
+
expect(["operational", "affected", "unknown"]).toContain(resource.status);
|
|
135
135
|
expect(resource).toHaveProperty("active_outages");
|
|
136
136
|
expect(Array.isArray(resource.outage_details)).toBe(true);
|
|
137
137
|
});
|
|
138
|
-
},
|
|
139
|
-
it("should
|
|
140
|
-
// Test
|
|
138
|
+
}, 15000);
|
|
139
|
+
it("should check single resource status", async () => {
|
|
140
|
+
// Test with a single human-readable name
|
|
141
141
|
const result = await server["handleToolCall"]({
|
|
142
142
|
method: "tools/call",
|
|
143
143
|
params: {
|
|
144
144
|
name: "get_infrastructure_news",
|
|
145
145
|
arguments: {
|
|
146
|
-
ids: ["
|
|
147
|
-
use_group_api: true,
|
|
146
|
+
ids: ["Anvil"],
|
|
148
147
|
},
|
|
149
148
|
},
|
|
150
149
|
});
|
|
151
150
|
const content = result.content[0];
|
|
152
151
|
const responseData = JSON.parse(content.text);
|
|
153
|
-
expect(responseData).toHaveProperty("api_method", "resource_group_api");
|
|
154
152
|
expect(responseData).toHaveProperty("resources_checked", 1);
|
|
155
153
|
expect(responseData.resource_status).toHaveLength(1);
|
|
156
154
|
const resourceStatus = responseData.resource_status[0];
|
|
157
|
-
expect(resourceStatus).
|
|
158
|
-
expect(resourceStatus).
|
|
159
|
-
expect(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
// If it failed, check error handling
|
|
166
|
-
if (resourceStatus.api_method === "group_specific_failed") {
|
|
167
|
-
expect(resourceStatus.status).toBe("unknown");
|
|
168
|
-
expect(resourceStatus).toHaveProperty("error");
|
|
169
|
-
}
|
|
170
|
-
}, 10000);
|
|
155
|
+
expect(resourceStatus.resource_id).toContain("anvil"); // Resolved ID contains anvil
|
|
156
|
+
expect(resourceStatus.resource_id).toContain(".access-ci.org");
|
|
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");
|
|
161
|
+
}, 15000);
|
|
171
162
|
it("should filter outages by resource correctly", async () => {
|
|
172
163
|
const result = await server["handleToolCall"]({
|
|
173
164
|
method: "tools/call",
|
|
174
165
|
params: {
|
|
175
166
|
name: "get_infrastructure_news",
|
|
176
|
-
arguments: { time: "current",
|
|
167
|
+
arguments: { time: "current", resource: "anvil" },
|
|
177
168
|
},
|
|
178
169
|
});
|
|
179
170
|
const content = result.content[0];
|
|
@@ -207,7 +198,10 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
207
198
|
expect(result.contents[0]).toHaveProperty("text");
|
|
208
199
|
if (uri !== "accessci://system-status") {
|
|
209
200
|
// JSON resources should have valid JSON
|
|
210
|
-
|
|
201
|
+
const content = result.contents[0];
|
|
202
|
+
if ("text" in content) {
|
|
203
|
+
expect(() => JSON.parse(content.text)).not.toThrow();
|
|
204
|
+
}
|
|
211
205
|
}
|
|
212
206
|
}
|
|
213
207
|
}, 15000);
|
|
@@ -215,11 +209,12 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
215
209
|
describe("Edge Cases and Error Handling", () => {
|
|
216
210
|
it("should handle empty API responses", async () => {
|
|
217
211
|
// This tests the robustness of our logic with potentially empty responses
|
|
212
|
+
// Using a resource filter that won't match anything
|
|
218
213
|
const result = await server["handleToolCall"]({
|
|
219
214
|
method: "tools/call",
|
|
220
215
|
params: {
|
|
221
216
|
name: "get_infrastructure_news",
|
|
222
|
-
arguments: { time: "current",
|
|
217
|
+
arguments: { time: "current", resource: "nonexistent-resource-xyz-12345" },
|
|
223
218
|
},
|
|
224
219
|
});
|
|
225
220
|
const content = result.content[0];
|
|
@@ -15,7 +15,8 @@ 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
|
-
|
|
18
|
+
OutageType: "Full",
|
|
19
|
+
AffectedResources: [{ ResourceName: "Anvil", ResourceID: "anvil-1.purdue.access-ci.org" }],
|
|
19
20
|
},
|
|
20
21
|
{
|
|
21
22
|
id: "2",
|
|
@@ -23,7 +24,8 @@ describe("SystemStatusServer", () => {
|
|
|
23
24
|
Content: "Regular maintenance window",
|
|
24
25
|
OutageStart: "2024-08-27T08:00:00Z",
|
|
25
26
|
OutageEnd: "2024-08-27T08:30:00Z",
|
|
26
|
-
|
|
27
|
+
OutageType: "Partial",
|
|
28
|
+
AffectedResources: [{ ResourceName: "Bridges-2", ResourceID: "bridges2.psc.access-ci.org" }],
|
|
27
29
|
},
|
|
28
30
|
];
|
|
29
31
|
const mockFutureOutagesData = [
|
|
@@ -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,68 +337,122 @@ 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
|
});
|
|
358
|
+
// Use full IDs with dots to skip resolution lookup
|
|
290
359
|
const result = await server["handleToolCall"]({
|
|
291
360
|
method: "tools/call",
|
|
292
361
|
params: {
|
|
293
362
|
name: "get_infrastructure_news",
|
|
294
|
-
arguments: { ids: ["anvil-
|
|
363
|
+
arguments: { ids: ["anvil.purdue.access-ci.org", "unknown.resource.org"] },
|
|
295
364
|
},
|
|
296
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/");
|
|
297
368
|
const content = result.content[0];
|
|
298
369
|
const response = JSON.parse(content.text);
|
|
299
|
-
expect(response.api_method).toBe("direct_outages_check");
|
|
300
370
|
expect(response.resources_checked).toBe(2);
|
|
301
|
-
expect(response.operational).toBe(1); // unknown
|
|
302
|
-
expect(response.affected).toBe(1); // anvil-
|
|
303
|
-
const anvilStatus = response.resource_status.find((r) => r.resource_id === "anvil-
|
|
371
|
+
expect(response.operational).toBe(1); // unknown.resource.org
|
|
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");
|
|
304
374
|
expect(anvilStatus.status).toBe("affected");
|
|
305
|
-
expect(anvilStatus.severity).toBe("high"); //
|
|
375
|
+
expect(anvilStatus.severity).toBe("high"); // Full outage = high
|
|
376
|
+
expect(anvilStatus.outage_details[0].outage_type).toBe("Full");
|
|
306
377
|
});
|
|
307
|
-
it("should
|
|
308
|
-
mockHttpClient.get.
|
|
378
|
+
it("should handle group API failures gracefully", async () => {
|
|
379
|
+
mockHttpClient.get.mockRejectedValue(new Error("API Error"));
|
|
380
|
+
const result = await server["handleToolCall"]({
|
|
381
|
+
method: "tools/call",
|
|
382
|
+
params: {
|
|
383
|
+
name: "get_infrastructure_news",
|
|
384
|
+
arguments: { ids: ["failing.resource.org"] },
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
const content = result.content[0];
|
|
388
|
+
const response = JSON.parse(content.text);
|
|
389
|
+
expect(response.unknown).toBe(1);
|
|
390
|
+
expect(response.resource_status[0].status).toBe("unknown");
|
|
391
|
+
expect(response.resource_status[0]).toHaveProperty("error");
|
|
392
|
+
});
|
|
393
|
+
it("should resolve human-readable name to resource ID", async () => {
|
|
394
|
+
// First call: resource search for name resolution
|
|
395
|
+
// Second call: group API for the resolved resource ID
|
|
396
|
+
mockHttpClient.get
|
|
397
|
+
.mockResolvedValueOnce({
|
|
309
398
|
status: 200,
|
|
310
|
-
data: {
|
|
399
|
+
data: {
|
|
400
|
+
results: {
|
|
401
|
+
active_groups: [
|
|
402
|
+
{ info_groupid: "anvil.purdue.access-ci.org", group_descriptive_name: "Anvil" },
|
|
403
|
+
],
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
})
|
|
407
|
+
.mockResolvedValueOnce({
|
|
408
|
+
status: 200,
|
|
409
|
+
data: { results: [] }, // No outages from group API
|
|
311
410
|
});
|
|
312
411
|
const result = await server["handleToolCall"]({
|
|
313
412
|
method: "tools/call",
|
|
314
413
|
params: {
|
|
315
414
|
name: "get_infrastructure_news",
|
|
316
|
-
arguments: {
|
|
317
|
-
ids: ["anvil"],
|
|
318
|
-
use_group_api: true,
|
|
319
|
-
},
|
|
415
|
+
arguments: { ids: ["Anvil"] },
|
|
320
416
|
},
|
|
321
417
|
});
|
|
322
|
-
|
|
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/");
|
|
323
421
|
const content = result.content[0];
|
|
324
422
|
const response = JSON.parse(content.text);
|
|
325
|
-
expect(response.
|
|
326
|
-
expect(response.resource_status[0].
|
|
327
|
-
expect(response.resource_status[0].
|
|
423
|
+
expect(response.resources_checked).toBe(1);
|
|
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
|
|
328
426
|
});
|
|
329
|
-
it("should
|
|
330
|
-
mockHttpClient.get.
|
|
427
|
+
it("should return error when resource name is ambiguous", async () => {
|
|
428
|
+
mockHttpClient.get.mockResolvedValueOnce({
|
|
429
|
+
status: 200,
|
|
430
|
+
data: {
|
|
431
|
+
results: {
|
|
432
|
+
active_groups: [
|
|
433
|
+
{
|
|
434
|
+
info_groupid: "stampede2.tacc.access-ci.org",
|
|
435
|
+
group_descriptive_name: "Stampede 2",
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
info_groupid: "stampede3.tacc.access-ci.org",
|
|
439
|
+
group_descriptive_name: "Stampede 3",
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
});
|
|
331
445
|
const result = await server["handleToolCall"]({
|
|
332
446
|
method: "tools/call",
|
|
333
447
|
params: {
|
|
334
448
|
name: "get_infrastructure_news",
|
|
335
|
-
arguments: {
|
|
336
|
-
ids: ["invalid-resource"],
|
|
337
|
-
use_group_api: true,
|
|
338
|
-
},
|
|
449
|
+
arguments: { ids: ["Stampede"] },
|
|
339
450
|
},
|
|
340
451
|
});
|
|
341
452
|
const content = result.content[0];
|
|
342
453
|
const response = JSON.parse(content.text);
|
|
343
|
-
expect(response).
|
|
344
|
-
expect(response.
|
|
345
|
-
expect(response.resource_status[0].api_method).toBe("group_specific_failed");
|
|
346
|
-
expect(response.resource_status[0]).toHaveProperty("error");
|
|
454
|
+
expect(response.error).toContain("Could not resolve");
|
|
455
|
+
expect(response.resolution_errors[0].error).toContain("Multiple resources match");
|
|
347
456
|
});
|
|
348
457
|
});
|
|
349
458
|
describe("Error Handling", () => {
|
|
@@ -391,7 +500,7 @@ describe("SystemStatusServer", () => {
|
|
|
391
500
|
params: { uri: "accessci://outages/current" },
|
|
392
501
|
});
|
|
393
502
|
expect(result.contents[0].mimeType).toBe("application/json");
|
|
394
|
-
expect(result.contents[0].text).toBeDefined();
|
|
503
|
+
expect("text" in result.contents[0] && result.contents[0].text).toBeDefined();
|
|
395
504
|
});
|
|
396
505
|
it("should handle unknown resources", async () => {
|
|
397
506
|
await expect(async () => {
|
package/dist/server.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { BaseAccessServer, Tool, Resource, CallToolResult } from "@access-mcp/sh
|
|
|
2
2
|
import { CallToolRequest, ReadResourceRequest, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
export declare class SystemStatusServer extends BaseAccessServer {
|
|
4
4
|
constructor();
|
|
5
|
+
/**
|
|
6
|
+
* Search for resources by name to resolve human-readable names to full IDs.
|
|
7
|
+
* Used by resolveResourceId callback.
|
|
8
|
+
*/
|
|
9
|
+
private searchResourcesByName;
|
|
5
10
|
protected getTools(): Tool[];
|
|
6
11
|
protected getResources(): Resource[];
|
|
7
12
|
protected handleToolCall(request: CallToolRequest): Promise<CallToolResult>;
|
|
@@ -16,5 +21,4 @@ export declare class SystemStatusServer extends BaseAccessServer {
|
|
|
16
21
|
private getPastOutages;
|
|
17
22
|
private getSystemAnnouncements;
|
|
18
23
|
private checkResourceStatus;
|
|
19
|
-
private checkResourceStatusViaGroups;
|
|
20
24
|
}
|
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseAccessServer, handleApiError, } from "@access-mcp/shared";
|
|
1
|
+
import { BaseAccessServer, handleApiError, resolveResourceId, } from "@access-mcp/shared";
|
|
2
2
|
import { createRequire } from "module";
|
|
3
3
|
const require = createRequire(import.meta.url);
|
|
4
4
|
const { version } = require("../package.json");
|
|
@@ -6,39 +6,59 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
6
6
|
constructor() {
|
|
7
7
|
super("access-mcp-system-status", version, "https://operations-api.access-ci.org");
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Search for resources by name to resolve human-readable names to full IDs.
|
|
11
|
+
* Used by resolveResourceId callback.
|
|
12
|
+
*/
|
|
13
|
+
async searchResourcesByName(query) {
|
|
14
|
+
try {
|
|
15
|
+
const response = await this.httpClient.get("/wh2/cider/v1/access-active-groups/type/resource-catalog.access-ci.org/");
|
|
16
|
+
const groups = response.data.results?.active_groups || [];
|
|
17
|
+
const queryLower = query.toLowerCase();
|
|
18
|
+
return groups
|
|
19
|
+
.filter((g) => g.group_descriptive_name?.toLowerCase().includes(queryLower))
|
|
20
|
+
.map((g) => ({
|
|
21
|
+
id: g.info_groupid || "",
|
|
22
|
+
name: g.group_descriptive_name || "",
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
9
29
|
getTools() {
|
|
10
30
|
return [
|
|
11
31
|
{
|
|
12
32
|
name: "get_infrastructure_news",
|
|
13
|
-
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.",
|
|
14
34
|
inputSchema: {
|
|
15
35
|
type: "object",
|
|
16
36
|
properties: {
|
|
17
|
-
query: {
|
|
18
|
-
type: "string",
|
|
19
|
-
description: "Filter by resource name (e.g., 'delta', 'bridges2')",
|
|
20
|
-
},
|
|
21
37
|
time: {
|
|
22
38
|
type: "string",
|
|
23
39
|
enum: ["current", "scheduled", "past", "all"],
|
|
24
|
-
description: "
|
|
40
|
+
description: "Time period: 'current' (active now), 'scheduled' (planned/future), 'past' (historical), 'all' (combined view)",
|
|
25
41
|
default: "current",
|
|
26
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
|
+
},
|
|
27
52
|
ids: {
|
|
28
53
|
type: "array",
|
|
29
54
|
items: { type: "string" },
|
|
30
|
-
description: "Check status for specific
|
|
55
|
+
description: "Check operational status for specific resources. Returns 'operational' or 'affected' for each. Accepts names ('Anvil') or IDs ('anvil.purdue.access-ci.org')",
|
|
31
56
|
},
|
|
32
57
|
limit: {
|
|
33
58
|
type: "number",
|
|
34
|
-
description: "Max results
|
|
59
|
+
description: "Max results to return",
|
|
35
60
|
default: 50,
|
|
36
61
|
},
|
|
37
|
-
use_group_api: {
|
|
38
|
-
type: "boolean",
|
|
39
|
-
description: "Use group API for status (with ids only)",
|
|
40
|
-
default: false,
|
|
41
|
-
},
|
|
42
62
|
},
|
|
43
63
|
},
|
|
44
64
|
},
|
|
@@ -79,11 +99,11 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
79
99
|
switch (name) {
|
|
80
100
|
case "get_infrastructure_news":
|
|
81
101
|
return await this.getInfrastructureNewsRouter({
|
|
82
|
-
resource: typedArgs.
|
|
102
|
+
resource: typedArgs.resource,
|
|
83
103
|
time: typedArgs.time,
|
|
104
|
+
outage_type: typedArgs.outage_type,
|
|
84
105
|
resource_ids: typedArgs.ids,
|
|
85
106
|
limit: typedArgs.limit,
|
|
86
|
-
use_group_api: typedArgs.use_group_api,
|
|
87
107
|
});
|
|
88
108
|
default:
|
|
89
109
|
return this.errorResponse(`Unknown tool: ${name}`);
|
|
@@ -98,21 +118,21 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
98
118
|
* Routes to appropriate handler based on parameters
|
|
99
119
|
*/
|
|
100
120
|
async getInfrastructureNewsRouter(args) {
|
|
101
|
-
const { resource, time = "current", resource_ids, limit
|
|
102
|
-
// Check resource status (returns operational/affected)
|
|
103
|
-
if (resource_ids && Array.isArray(resource_ids)) {
|
|
104
|
-
return await this.checkResourceStatus(resource_ids
|
|
121
|
+
const { resource, time = "current", outage_type, resource_ids, limit } = args;
|
|
122
|
+
// Check resource status (returns operational/affected) - only if IDs provided
|
|
123
|
+
if (resource_ids && Array.isArray(resource_ids) && resource_ids.length > 0) {
|
|
124
|
+
return await this.checkResourceStatus(resource_ids);
|
|
105
125
|
}
|
|
106
126
|
// Time-based routing
|
|
107
127
|
switch (time) {
|
|
108
128
|
case "current":
|
|
109
|
-
return await this.getCurrentOutages(resource);
|
|
129
|
+
return await this.getCurrentOutages(resource, outage_type);
|
|
110
130
|
case "scheduled":
|
|
111
|
-
return await this.getScheduledMaintenance(resource);
|
|
131
|
+
return await this.getScheduledMaintenance(resource, outage_type);
|
|
112
132
|
case "past":
|
|
113
|
-
return await this.getPastOutages(resource, limit || 100);
|
|
133
|
+
return await this.getPastOutages(resource, outage_type, limit || 100);
|
|
114
134
|
case "all":
|
|
115
|
-
return await this.getSystemAnnouncements(limit || 50);
|
|
135
|
+
return await this.getSystemAnnouncements(outage_type, limit || 50);
|
|
116
136
|
default:
|
|
117
137
|
throw new Error(`Invalid time parameter: ${time}. Must be one of: current, scheduled, past, all`);
|
|
118
138
|
}
|
|
@@ -177,9 +197,13 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
177
197
|
throw new Error(`Unknown resource: ${uri}`);
|
|
178
198
|
}
|
|
179
199
|
}
|
|
180
|
-
async getCurrentOutages(resourceFilter) {
|
|
200
|
+
async getCurrentOutages(resourceFilter, outageTypeFilter) {
|
|
181
201
|
const response = await this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/current_outages/");
|
|
182
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
|
+
}
|
|
183
207
|
// Filter by resource if specified
|
|
184
208
|
if (resourceFilter) {
|
|
185
209
|
const filter = resourceFilter.toLowerCase();
|
|
@@ -231,9 +255,13 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
231
255
|
],
|
|
232
256
|
};
|
|
233
257
|
}
|
|
234
|
-
async getScheduledMaintenance(resourceFilter) {
|
|
258
|
+
async getScheduledMaintenance(resourceFilter, outageTypeFilter) {
|
|
235
259
|
const response = await this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/future_outages/");
|
|
236
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
|
+
}
|
|
237
265
|
// Filter by resource if specified
|
|
238
266
|
if (resourceFilter) {
|
|
239
267
|
const filter = resourceFilter.toLowerCase();
|
|
@@ -295,9 +323,13 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
295
323
|
],
|
|
296
324
|
};
|
|
297
325
|
}
|
|
298
|
-
async getPastOutages(resourceFilter, limit = 100) {
|
|
326
|
+
async getPastOutages(resourceFilter, outageTypeFilter, limit = 100) {
|
|
299
327
|
const response = await this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/past_outages/");
|
|
300
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
|
+
}
|
|
301
333
|
// Filter by resource if specified
|
|
302
334
|
if (resourceFilter) {
|
|
303
335
|
const filter = resourceFilter.toLowerCase();
|
|
@@ -372,16 +404,23 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
372
404
|
],
|
|
373
405
|
};
|
|
374
406
|
}
|
|
375
|
-
async getSystemAnnouncements(limit = 50) {
|
|
407
|
+
async getSystemAnnouncements(outageTypeFilter, limit = 50) {
|
|
376
408
|
// Get current, future, and recent past announcements for comprehensive view
|
|
377
409
|
const [currentResponse, futureResponse, pastResponse] = await Promise.all([
|
|
378
410
|
this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/current_outages/"),
|
|
379
411
|
this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/future_outages/"),
|
|
380
412
|
this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/past_outages/"),
|
|
381
413
|
]);
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
+
}
|
|
385
424
|
// Filter recent past outages (last 30 days) for announcements
|
|
386
425
|
const recentPastOutages = pastOutagesData.filter((outage) => {
|
|
387
426
|
const endTime = new Date(outage.OutageEnd || "");
|
|
@@ -429,101 +468,84 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
429
468
|
],
|
|
430
469
|
};
|
|
431
470
|
}
|
|
432
|
-
async checkResourceStatus(resourceIds
|
|
471
|
+
async checkResourceStatus(resourceIds) {
|
|
433
472
|
if (!resourceIds || !Array.isArray(resourceIds) || resourceIds.length === 0) {
|
|
434
473
|
throw new Error("resource_ids parameter is required and must be a non-empty array of resource IDs");
|
|
435
474
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
let severity = null;
|
|
447
|
-
if (affectedOutages.length > 0) {
|
|
448
|
-
status = "affected";
|
|
449
|
-
// Get highest severity using same logic as getCurrentOutages
|
|
450
|
-
const severities = affectedOutages.map((outage) => {
|
|
451
|
-
const subject = outage.Subject?.toLowerCase() || "";
|
|
452
|
-
if (subject.includes("emergency") || subject.includes("critical")) {
|
|
453
|
-
return "high";
|
|
454
|
-
}
|
|
455
|
-
else if (subject.includes("maintenance") || subject.includes("scheduled")) {
|
|
456
|
-
return "low";
|
|
457
|
-
}
|
|
458
|
-
else {
|
|
459
|
-
return "medium";
|
|
460
|
-
}
|
|
461
|
-
});
|
|
462
|
-
if (severities.includes("high"))
|
|
463
|
-
severity = "high";
|
|
464
|
-
else if (severities.includes("medium"))
|
|
465
|
-
severity = "medium";
|
|
466
|
-
else
|
|
467
|
-
severity = "low";
|
|
475
|
+
// Resolve all resource names to IDs first
|
|
476
|
+
const resolvedIds = [];
|
|
477
|
+
const resolutionErrors = [];
|
|
478
|
+
for (const inputId of resourceIds) {
|
|
479
|
+
const resolved = await resolveResourceId(inputId, (query) => this.searchResourcesByName(query));
|
|
480
|
+
if (resolved.success) {
|
|
481
|
+
resolvedIds.push(resolved.id);
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
resolutionErrors.push({ input: inputId, error: resolved.error });
|
|
468
485
|
}
|
|
486
|
+
}
|
|
487
|
+
// If any resolutions failed, return errors
|
|
488
|
+
if (resolutionErrors.length > 0) {
|
|
469
489
|
return {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
490
|
+
content: [
|
|
491
|
+
{
|
|
492
|
+
type: "text",
|
|
493
|
+
text: JSON.stringify({
|
|
494
|
+
error: "Could not resolve some resource names",
|
|
495
|
+
resolution_errors: resolutionErrors,
|
|
496
|
+
suggestions: [
|
|
497
|
+
"Use full resource IDs (e.g., 'anvil.purdue.access-ci.org')",
|
|
498
|
+
"Or use exact resource names (e.g., 'Anvil', 'Delta')",
|
|
499
|
+
],
|
|
500
|
+
}, null, 2),
|
|
501
|
+
},
|
|
502
|
+
],
|
|
478
503
|
};
|
|
479
|
-
});
|
|
480
|
-
return {
|
|
481
|
-
content: [
|
|
482
|
-
{
|
|
483
|
-
type: "text",
|
|
484
|
-
text: JSON.stringify({
|
|
485
|
-
checked_at: new Date().toISOString(),
|
|
486
|
-
resources_checked: resourceIds.length,
|
|
487
|
-
operational: resourceStatus.filter((r) => r.status === "operational").length,
|
|
488
|
-
affected: resourceStatus.filter((r) => r.status === "affected").length,
|
|
489
|
-
api_method: "direct_outages_check",
|
|
490
|
-
resource_status: resourceStatus,
|
|
491
|
-
}, null, 2),
|
|
492
|
-
},
|
|
493
|
-
],
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
async checkResourceStatusViaGroups(resourceIds) {
|
|
497
|
-
if (!resourceIds || !Array.isArray(resourceIds) || resourceIds.length === 0) {
|
|
498
|
-
throw new Error("resource_ids parameter is required and must be a non-empty array of resource IDs");
|
|
499
504
|
}
|
|
500
|
-
//
|
|
501
|
-
const
|
|
505
|
+
// Use group API for efficient per-resource queries
|
|
506
|
+
const now = new Date();
|
|
507
|
+
const statusPromises = resolvedIds.map(async (resourceId) => {
|
|
502
508
|
try {
|
|
503
509
|
const response = await this.httpClient.get(`/wh2/news/v1/info_groupid/${resourceId}/`);
|
|
504
|
-
const
|
|
505
|
-
|
|
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
|
+
}
|
|
506
530
|
return {
|
|
507
531
|
resource_id: resourceId,
|
|
508
|
-
status
|
|
509
|
-
severity
|
|
510
|
-
active_outages:
|
|
511
|
-
outage_details:
|
|
532
|
+
status,
|
|
533
|
+
severity,
|
|
534
|
+
active_outages: currentOutages.length,
|
|
535
|
+
outage_details: currentOutages.map((outage) => ({
|
|
512
536
|
subject: outage.Subject,
|
|
537
|
+
outage_type: outage.OutageType,
|
|
513
538
|
})),
|
|
514
|
-
api_method: "group_specific",
|
|
515
539
|
};
|
|
516
540
|
}
|
|
517
541
|
catch {
|
|
518
|
-
// Fallback to general check if group API fails
|
|
519
542
|
return {
|
|
520
543
|
resource_id: resourceId,
|
|
521
544
|
status: "unknown",
|
|
522
545
|
severity: null,
|
|
523
546
|
active_outages: 0,
|
|
524
547
|
outage_details: [],
|
|
525
|
-
error: `
|
|
526
|
-
api_method: "group_specific_failed",
|
|
548
|
+
error: `Failed to fetch status for ${resourceId}`,
|
|
527
549
|
};
|
|
528
550
|
}
|
|
529
551
|
});
|
|
@@ -533,12 +555,11 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
533
555
|
{
|
|
534
556
|
type: "text",
|
|
535
557
|
text: JSON.stringify({
|
|
536
|
-
checked_at:
|
|
537
|
-
resources_checked:
|
|
558
|
+
checked_at: now.toISOString(),
|
|
559
|
+
resources_checked: resolvedIds.length,
|
|
538
560
|
operational: resourceStatus.filter((r) => r.status === "operational").length,
|
|
539
561
|
affected: resourceStatus.filter((r) => r.status === "affected").length,
|
|
540
562
|
unknown: resourceStatus.filter((r) => r.status === "unknown").length,
|
|
541
|
-
api_method: "resource_group_api",
|
|
542
563
|
resource_status: resourceStatus,
|
|
543
564
|
}, null, 2),
|
|
544
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": {
|