@access-mcp/system-status 0.5.1 → 0.6.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/dist/__tests__/server.integration.test.js +14 -9
- package/dist/__tests__/server.test.js +73 -10
- package/dist/server.d.ts +5 -0
- package/dist/server.js +58 -8
- package/package.json +1 -1
|
@@ -107,13 +107,13 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
107
107
|
}
|
|
108
108
|
}, 15000);
|
|
109
109
|
it("should check resource status with direct method", async () => {
|
|
110
|
-
// Test with
|
|
110
|
+
// Test with human-readable names - these get resolved to full IDs
|
|
111
111
|
const result = await server["handleToolCall"]({
|
|
112
112
|
method: "tools/call",
|
|
113
113
|
params: {
|
|
114
114
|
name: "get_infrastructure_news",
|
|
115
115
|
arguments: {
|
|
116
|
-
ids: ["
|
|
116
|
+
ids: ["Anvil", "Delta", "Expanse"],
|
|
117
117
|
use_group_api: false,
|
|
118
118
|
},
|
|
119
119
|
},
|
|
@@ -127,23 +127,24 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
127
127
|
expect(responseData).toHaveProperty("api_method", "direct_outages_check");
|
|
128
128
|
expect(Array.isArray(responseData.resource_status)).toBe(true);
|
|
129
129
|
expect(responseData.resource_status).toHaveLength(3);
|
|
130
|
-
// Check resource status structure
|
|
130
|
+
// Check resource status structure - IDs should be resolved to full format
|
|
131
131
|
responseData.resource_status.forEach((resource) => {
|
|
132
132
|
expect(resource).toHaveProperty("resource_id");
|
|
133
|
+
expect(resource.resource_id).toContain(".access-ci.org"); // Resolved to full ID
|
|
133
134
|
expect(resource).toHaveProperty("status");
|
|
134
135
|
expect(["operational", "affected"]).toContain(resource.status);
|
|
135
136
|
expect(resource).toHaveProperty("active_outages");
|
|
136
137
|
expect(Array.isArray(resource.outage_details)).toBe(true);
|
|
137
138
|
});
|
|
138
|
-
},
|
|
139
|
+
}, 15000);
|
|
139
140
|
it("should test group API functionality", async () => {
|
|
140
|
-
// Test group API with a
|
|
141
|
+
// Test group API with a human-readable name
|
|
141
142
|
const result = await server["handleToolCall"]({
|
|
142
143
|
method: "tools/call",
|
|
143
144
|
params: {
|
|
144
145
|
name: "get_infrastructure_news",
|
|
145
146
|
arguments: {
|
|
146
|
-
ids: ["
|
|
147
|
+
ids: ["Anvil"],
|
|
147
148
|
use_group_api: true,
|
|
148
149
|
},
|
|
149
150
|
},
|
|
@@ -154,7 +155,8 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
154
155
|
expect(responseData).toHaveProperty("resources_checked", 1);
|
|
155
156
|
expect(responseData.resource_status).toHaveLength(1);
|
|
156
157
|
const resourceStatus = responseData.resource_status[0];
|
|
157
|
-
expect(resourceStatus).
|
|
158
|
+
expect(resourceStatus.resource_id).toContain("anvil"); // Resolved ID contains anvil
|
|
159
|
+
expect(resourceStatus.resource_id).toContain(".access-ci.org");
|
|
158
160
|
expect(resourceStatus).toHaveProperty("api_method");
|
|
159
161
|
expect(["group_specific", "group_specific_failed"]).toContain(resourceStatus.api_method);
|
|
160
162
|
// If it succeeded, check structure
|
|
@@ -167,7 +169,7 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
167
169
|
expect(resourceStatus.status).toBe("unknown");
|
|
168
170
|
expect(resourceStatus).toHaveProperty("error");
|
|
169
171
|
}
|
|
170
|
-
},
|
|
172
|
+
}, 15000);
|
|
171
173
|
it("should filter outages by resource correctly", async () => {
|
|
172
174
|
const result = await server["handleToolCall"]({
|
|
173
175
|
method: "tools/call",
|
|
@@ -207,7 +209,10 @@ describe("SystemStatusServer Integration Tests", () => {
|
|
|
207
209
|
expect(result.contents[0]).toHaveProperty("text");
|
|
208
210
|
if (uri !== "accessci://system-status") {
|
|
209
211
|
// JSON resources should have valid JSON
|
|
210
|
-
|
|
212
|
+
const content = result.contents[0];
|
|
213
|
+
if ("text" in content) {
|
|
214
|
+
expect(() => JSON.parse(content.text)).not.toThrow();
|
|
215
|
+
}
|
|
211
216
|
}
|
|
212
217
|
}
|
|
213
218
|
}, 15000);
|
|
@@ -15,7 +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
|
-
AffectedResources: [{ ResourceName: "Anvil", ResourceID: "anvil-1" }],
|
|
18
|
+
AffectedResources: [{ ResourceName: "Anvil", ResourceID: "anvil-1.purdue.access-ci.org" }],
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
id: "2",
|
|
@@ -23,7 +23,7 @@ describe("SystemStatusServer", () => {
|
|
|
23
23
|
Content: "Regular maintenance window",
|
|
24
24
|
OutageStart: "2024-08-27T08:00:00Z",
|
|
25
25
|
OutageEnd: "2024-08-27T08:30:00Z",
|
|
26
|
-
AffectedResources: [{ ResourceName: "Bridges-2", ResourceID: "bridges2-
|
|
26
|
+
AffectedResources: [{ ResourceName: "Bridges-2", ResourceID: "bridges2.psc.access-ci.org" }],
|
|
27
27
|
},
|
|
28
28
|
];
|
|
29
29
|
const mockFutureOutagesData = [
|
|
@@ -287,20 +287,21 @@ describe("SystemStatusServer", () => {
|
|
|
287
287
|
status: 200,
|
|
288
288
|
data: { results: mockCurrentOutagesData },
|
|
289
289
|
});
|
|
290
|
+
// Use full IDs with dots to skip resolution lookup
|
|
290
291
|
const result = await server["handleToolCall"]({
|
|
291
292
|
method: "tools/call",
|
|
292
293
|
params: {
|
|
293
294
|
name: "get_infrastructure_news",
|
|
294
|
-
arguments: { ids: ["anvil-1", "unknown
|
|
295
|
+
arguments: { ids: ["anvil-1.purdue.access-ci.org", "unknown.resource.org"] },
|
|
295
296
|
},
|
|
296
297
|
});
|
|
297
298
|
const content = result.content[0];
|
|
298
299
|
const response = JSON.parse(content.text);
|
|
299
300
|
expect(response.api_method).toBe("direct_outages_check");
|
|
300
301
|
expect(response.resources_checked).toBe(2);
|
|
301
|
-
expect(response.operational).toBe(1); // unknown
|
|
302
|
-
expect(response.affected).toBe(1); // anvil-1
|
|
303
|
-
const anvilStatus = response.resource_status.find((r) => r.resource_id === "anvil-1");
|
|
302
|
+
expect(response.operational).toBe(1); // unknown.resource.org
|
|
303
|
+
expect(response.affected).toBe(1); // anvil-1.purdue.access-ci.org
|
|
304
|
+
const anvilStatus = response.resource_status.find((r) => r.resource_id === "anvil-1.purdue.access-ci.org");
|
|
304
305
|
expect(anvilStatus.status).toBe("affected");
|
|
305
306
|
expect(anvilStatus.severity).toBe("high"); // Emergency maintenance
|
|
306
307
|
});
|
|
@@ -309,17 +310,18 @@ describe("SystemStatusServer", () => {
|
|
|
309
310
|
status: 200,
|
|
310
311
|
data: { results: [] }, // No outages for this group
|
|
311
312
|
});
|
|
313
|
+
// Use full ID with dots to skip resolution lookup
|
|
312
314
|
const result = await server["handleToolCall"]({
|
|
313
315
|
method: "tools/call",
|
|
314
316
|
params: {
|
|
315
317
|
name: "get_infrastructure_news",
|
|
316
318
|
arguments: {
|
|
317
|
-
ids: ["anvil"],
|
|
319
|
+
ids: ["anvil.purdue.access-ci.org"],
|
|
318
320
|
use_group_api: true,
|
|
319
321
|
},
|
|
320
322
|
},
|
|
321
323
|
});
|
|
322
|
-
expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/news/v1/info_groupid/anvil/");
|
|
324
|
+
expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/news/v1/info_groupid/anvil.purdue.access-ci.org/");
|
|
323
325
|
const content = result.content[0];
|
|
324
326
|
const response = JSON.parse(content.text);
|
|
325
327
|
expect(response.api_method).toBe("resource_group_api");
|
|
@@ -328,12 +330,13 @@ describe("SystemStatusServer", () => {
|
|
|
328
330
|
});
|
|
329
331
|
it("should handle group API failures gracefully", async () => {
|
|
330
332
|
mockHttpClient.get.mockRejectedValue(new Error("API Error"));
|
|
333
|
+
// Use full ID with dots to skip resolution lookup
|
|
331
334
|
const result = await server["handleToolCall"]({
|
|
332
335
|
method: "tools/call",
|
|
333
336
|
params: {
|
|
334
337
|
name: "get_infrastructure_news",
|
|
335
338
|
arguments: {
|
|
336
|
-
ids: ["invalid
|
|
339
|
+
ids: ["invalid.resource.org"],
|
|
337
340
|
use_group_api: true,
|
|
338
341
|
},
|
|
339
342
|
},
|
|
@@ -345,6 +348,66 @@ describe("SystemStatusServer", () => {
|
|
|
345
348
|
expect(response.resource_status[0].api_method).toBe("group_specific_failed");
|
|
346
349
|
expect(response.resource_status[0]).toHaveProperty("error");
|
|
347
350
|
});
|
|
351
|
+
it("should resolve human-readable name to resource ID", async () => {
|
|
352
|
+
// First call: resource search for name resolution
|
|
353
|
+
// Second call: current outages
|
|
354
|
+
mockHttpClient.get
|
|
355
|
+
.mockResolvedValueOnce({
|
|
356
|
+
status: 200,
|
|
357
|
+
data: {
|
|
358
|
+
results: {
|
|
359
|
+
active_groups: [
|
|
360
|
+
{ info_groupid: "anvil.purdue.access-ci.org", group_descriptive_name: "Anvil" },
|
|
361
|
+
],
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
})
|
|
365
|
+
.mockResolvedValueOnce({
|
|
366
|
+
status: 200,
|
|
367
|
+
data: { results: [] }, // No outages
|
|
368
|
+
});
|
|
369
|
+
const result = await server["handleToolCall"]({
|
|
370
|
+
method: "tools/call",
|
|
371
|
+
params: {
|
|
372
|
+
name: "get_infrastructure_news",
|
|
373
|
+
arguments: { ids: ["Anvil"] },
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
const content = result.content[0];
|
|
377
|
+
const response = JSON.parse(content.text);
|
|
378
|
+
expect(response.resources_checked).toBe(1);
|
|
379
|
+
expect(response.resource_status[0].resource_id).toBe("anvil.purdue.access-ci.org");
|
|
380
|
+
});
|
|
381
|
+
it("should return error when resource name is ambiguous", async () => {
|
|
382
|
+
mockHttpClient.get.mockResolvedValueOnce({
|
|
383
|
+
status: 200,
|
|
384
|
+
data: {
|
|
385
|
+
results: {
|
|
386
|
+
active_groups: [
|
|
387
|
+
{
|
|
388
|
+
info_groupid: "stampede2.tacc.access-ci.org",
|
|
389
|
+
group_descriptive_name: "Stampede 2",
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
info_groupid: "stampede3.tacc.access-ci.org",
|
|
393
|
+
group_descriptive_name: "Stampede 3",
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
const result = await server["handleToolCall"]({
|
|
400
|
+
method: "tools/call",
|
|
401
|
+
params: {
|
|
402
|
+
name: "get_infrastructure_news",
|
|
403
|
+
arguments: { ids: ["Stampede"] },
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
const content = result.content[0];
|
|
407
|
+
const response = JSON.parse(content.text);
|
|
408
|
+
expect(response.error).toContain("Could not resolve");
|
|
409
|
+
expect(response.resolution_errors[0].error).toContain("Multiple resources match");
|
|
410
|
+
});
|
|
348
411
|
});
|
|
349
412
|
describe("Error Handling", () => {
|
|
350
413
|
it("should handle API errors gracefully", async () => {
|
|
@@ -391,7 +454,7 @@ describe("SystemStatusServer", () => {
|
|
|
391
454
|
params: { uri: "accessci://outages/current" },
|
|
392
455
|
});
|
|
393
456
|
expect(result.contents[0].mimeType).toBe("application/json");
|
|
394
|
-
expect(result.contents[0].text).toBeDefined();
|
|
457
|
+
expect("text" in result.contents[0] && result.contents[0].text).toBeDefined();
|
|
395
458
|
});
|
|
396
459
|
it("should handle unknown resources", async () => {
|
|
397
460
|
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>;
|
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,6 +6,26 @@ 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
|
{
|
|
@@ -21,13 +41,13 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
21
41
|
time: {
|
|
22
42
|
type: "string",
|
|
23
43
|
enum: ["current", "scheduled", "past", "all"],
|
|
24
|
-
description: "
|
|
44
|
+
description: "Time filter. Values: 'current' for active outages, 'scheduled' for future/planned, 'past' for historical, 'all' for everything",
|
|
25
45
|
default: "current",
|
|
26
46
|
},
|
|
27
47
|
ids: {
|
|
28
48
|
type: "array",
|
|
29
49
|
items: { type: "string" },
|
|
30
|
-
description: "Check status for specific
|
|
50
|
+
description: "Check status for specific resources. Accepts names (e.g., 'Anvil', 'Delta') or full IDs (e.g., 'anvil.purdue.access-ci.org')",
|
|
31
51
|
},
|
|
32
52
|
limit: {
|
|
33
53
|
type: "number",
|
|
@@ -99,8 +119,8 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
99
119
|
*/
|
|
100
120
|
async getInfrastructureNewsRouter(args) {
|
|
101
121
|
const { resource, time = "current", resource_ids, limit, use_group_api = false } = args;
|
|
102
|
-
// Check resource status (returns operational/affected)
|
|
103
|
-
if (resource_ids && Array.isArray(resource_ids)) {
|
|
122
|
+
// Check resource status (returns operational/affected) - only if IDs provided
|
|
123
|
+
if (resource_ids && Array.isArray(resource_ids) && resource_ids.length > 0) {
|
|
104
124
|
return await this.checkResourceStatus(resource_ids, use_group_api);
|
|
105
125
|
}
|
|
106
126
|
// Time-based routing
|
|
@@ -433,13 +453,43 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
433
453
|
if (!resourceIds || !Array.isArray(resourceIds) || resourceIds.length === 0) {
|
|
434
454
|
throw new Error("resource_ids parameter is required and must be a non-empty array of resource IDs");
|
|
435
455
|
}
|
|
456
|
+
// Resolve all resource names to IDs first
|
|
457
|
+
const resolvedIds = [];
|
|
458
|
+
const resolutionErrors = [];
|
|
459
|
+
for (const inputId of resourceIds) {
|
|
460
|
+
const resolved = await resolveResourceId(inputId, (query) => this.searchResourcesByName(query));
|
|
461
|
+
if (resolved.success) {
|
|
462
|
+
resolvedIds.push(resolved.id);
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
resolutionErrors.push({ input: inputId, error: resolved.error });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// If any resolutions failed, return errors
|
|
469
|
+
if (resolutionErrors.length > 0) {
|
|
470
|
+
return {
|
|
471
|
+
content: [
|
|
472
|
+
{
|
|
473
|
+
type: "text",
|
|
474
|
+
text: JSON.stringify({
|
|
475
|
+
error: "Could not resolve some resource names",
|
|
476
|
+
resolution_errors: resolutionErrors,
|
|
477
|
+
suggestions: [
|
|
478
|
+
"Use full resource IDs (e.g., 'anvil.purdue.access-ci.org')",
|
|
479
|
+
"Or use exact resource names (e.g., 'Anvil', 'Delta')",
|
|
480
|
+
],
|
|
481
|
+
}, null, 2),
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
};
|
|
485
|
+
}
|
|
436
486
|
if (useGroupApi) {
|
|
437
|
-
return await this.checkResourceStatusViaGroups(
|
|
487
|
+
return await this.checkResourceStatusViaGroups(resolvedIds);
|
|
438
488
|
}
|
|
439
489
|
// Efficient approach: fetch raw current outages data once
|
|
440
490
|
const response = await this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/current_outages/");
|
|
441
491
|
const rawOutages = response.data.results || [];
|
|
442
|
-
const resourceStatus =
|
|
492
|
+
const resourceStatus = resolvedIds.map((resourceId) => {
|
|
443
493
|
const affectedOutages = rawOutages.filter((outage) => outage.AffectedResources?.some((resource) => resource.ResourceID?.toString() === resourceId ||
|
|
444
494
|
resource.ResourceName?.toLowerCase().includes(resourceId.toLowerCase())));
|
|
445
495
|
let status = "operational";
|
|
@@ -483,7 +533,7 @@ export class SystemStatusServer extends BaseAccessServer {
|
|
|
483
533
|
type: "text",
|
|
484
534
|
text: JSON.stringify({
|
|
485
535
|
checked_at: new Date().toISOString(),
|
|
486
|
-
resources_checked:
|
|
536
|
+
resources_checked: resolvedIds.length,
|
|
487
537
|
operational: resourceStatus.filter((r) => r.status === "operational").length,
|
|
488
538
|
affected: resourceStatus.filter((r) => r.status === "affected").length,
|
|
489
539
|
api_method: "direct_outages_check",
|