@access-mcp/system-status 0.6.0 → 0.7.0

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