@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 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
- | `resource_ids` | array | Check status for specific resource IDs |
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
- // Check current status for specific resource
49
+ // Filter to a specific resource
50
50
  get_infrastructure_news({ resource: "delta", time: "current" })
51
51
 
52
- // Check operational status of specific resources
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
- resource_ids: ["delta.ncsa.access-ci.org", "bridges2.psc.access-ci.org"]
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 with direct method", async () => {
110
- // Test with common resource names that might exist
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: ["anvil", "bridges", "jetstream"],
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
- }, 10000);
139
- it("should test group API functionality", async () => {
140
- // Test group API with a resource that might have a group ID
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: ["anvil"],
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).toHaveProperty("resource_id", "anvil");
158
- expect(resourceStatus).toHaveProperty("api_method");
159
- expect(["group_specific", "group_specific_failed"]).toContain(resourceStatus.api_method);
160
- // If it succeeded, check structure
161
- if (resourceStatus.api_method === "group_specific") {
162
- expect(resourceStatus).toHaveProperty("status");
163
- expect(["operational", "affected"]).toContain(resourceStatus.status);
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", query: "anvil" },
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
- expect(() => JSON.parse(result.contents[0].text)).not.toThrow();
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", query: "nonexistent-resource-xyz-12345" },
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
- AffectedResources: [{ ResourceName: "Anvil", ResourceID: "anvil-1" }],
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
- AffectedResources: [{ ResourceName: "Bridges-2", ResourceID: "bridges2-1" }],
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: { query: "Anvil", time: "current" },
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 efficiently (direct method)", async () => {
286
- mockHttpClient.get.mockResolvedValue({
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: mockCurrentOutagesData },
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-1", "unknown-resource"] },
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-resource
302
- expect(response.affected).toBe(1); // anvil-1
303
- const anvilStatus = response.resource_status.find((r) => r.resource_id === "anvil-1");
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"); // Emergency maintenance
375
+ expect(anvilStatus.severity).toBe("high"); // Full outage = high
376
+ expect(anvilStatus.outage_details[0].outage_type).toBe("Full");
306
377
  });
307
- it("should use group API when requested", async () => {
308
- mockHttpClient.get.mockResolvedValue({
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: { results: [] }, // No outages for this group
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
- expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/news/v1/info_groupid/anvil/");
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.api_method).toBe("resource_group_api");
326
- expect(response.resource_status[0].status).toBe("operational");
327
- expect(response.resource_status[0].api_method).toBe("group_specific");
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 handle group API failures gracefully", async () => {
330
- mockHttpClient.get.mockRejectedValue(new Error("API Error"));
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).toHaveProperty("unknown", 1);
344
- expect(response.resource_status[0].status).toBe("unknown");
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 (outages, maintenance, incidents). Returns {total, items}.",
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: "Period: current (active), scheduled (future), past, all",
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 resource IDs",
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 (default: 50)",
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.query,
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, use_group_api = false } = args;
102
- // Check resource status (returns operational/affected)
103
- if (resource_ids && Array.isArray(resource_ids)) {
104
- return await this.checkResourceStatus(resource_ids, use_group_api);
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
- const currentOutages = currentResponse.data.results || [];
383
- const futureOutages = futureResponse.data.results || [];
384
- const pastOutagesData = pastResponse.data.results || [];
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, useGroupApi = false) {
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
- if (useGroupApi) {
437
- return await this.checkResourceStatusViaGroups(resourceIds);
438
- }
439
- // Efficient approach: fetch raw current outages data once
440
- const response = await this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/current_outages/");
441
- const rawOutages = response.data.results || [];
442
- const resourceStatus = resourceIds.map((resourceId) => {
443
- const affectedOutages = rawOutages.filter((outage) => outage.AffectedResources?.some((resource) => resource.ResourceID?.toString() === resourceId ||
444
- resource.ResourceName?.toLowerCase().includes(resourceId.toLowerCase())));
445
- let status = "operational";
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
- resource_id: resourceId,
471
- status,
472
- severity,
473
- active_outages: affectedOutages.length,
474
- outage_details: affectedOutages.map((outage) => ({
475
- subject: outage.Subject,
476
- severity,
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
- // Try to use the more efficient group-based API
501
- 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) => {
502
508
  try {
503
509
  const response = await this.httpClient.get(`/wh2/news/v1/info_groupid/${resourceId}/`);
504
- const groupData = response.data.results || [];
505
- const hasOutages = groupData.length > 0;
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: hasOutages ? "affected" : "operational",
509
- severity: hasOutages ? "medium" : null,
510
- active_outages: groupData.length,
511
- outage_details: groupData.map((outage) => ({
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: `Group API failed for ${resourceId}`,
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: new Date().toISOString(),
537
- resources_checked: resourceIds.length,
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.5.1",
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": "^0.6.0",
47
+ "@access-mcp/shared": "*",
48
+ "@modelcontextprotocol/sdk": "^1.16.0",
48
49
  "express": "^4.18.0"
49
50
  },
50
51
  "devDependencies": {