@access-mcp/system-status 0.5.0 → 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/server.js CHANGED
@@ -1,7 +1,30 @@
1
- import { BaseAccessServer, handleApiError } from "@access-mcp/shared";
1
+ import { BaseAccessServer, handleApiError, resolveResourceId, } from "@access-mcp/shared";
2
+ import { createRequire } from "module";
3
+ const require = createRequire(import.meta.url);
4
+ const { version } = require("../package.json");
2
5
  export class SystemStatusServer extends BaseAccessServer {
3
6
  constructor() {
4
- super("access-mcp-system-status", "0.4.0", "https://operations-api.access-ci.org");
7
+ super("access-mcp-system-status", version, "https://operations-api.access-ci.org");
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
+ }
5
28
  }
6
29
  getTools() {
7
30
  return [
@@ -13,30 +36,30 @@ export class SystemStatusServer extends BaseAccessServer {
13
36
  properties: {
14
37
  query: {
15
38
  type: "string",
16
- description: "Filter by resource name (e.g., 'delta', 'bridges2')"
39
+ description: "Filter by resource name (e.g., 'delta', 'bridges2')",
17
40
  },
18
41
  time: {
19
42
  type: "string",
20
43
  enum: ["current", "scheduled", "past", "all"],
21
- description: "Period: current (active), scheduled (future), past, all",
22
- default: "current"
44
+ description: "Time filter. Values: 'current' for active outages, 'scheduled' for future/planned, 'past' for historical, 'all' for everything",
45
+ default: "current",
23
46
  },
24
47
  ids: {
25
48
  type: "array",
26
49
  items: { type: "string" },
27
- description: "Check status for specific resource IDs"
50
+ description: "Check status for specific resources. Accepts names (e.g., 'Anvil', 'Delta') or full IDs (e.g., 'anvil.purdue.access-ci.org')",
28
51
  },
29
52
  limit: {
30
53
  type: "number",
31
54
  description: "Max results (default: 50)",
32
- default: 50
55
+ default: 50,
33
56
  },
34
57
  use_group_api: {
35
58
  type: "boolean",
36
59
  description: "Use group API for status (with ids only)",
37
- default: false
38
- }
39
- }
60
+ default: false,
61
+ },
62
+ },
40
63
  },
41
64
  },
42
65
  ];
@@ -71,15 +94,16 @@ export class SystemStatusServer extends BaseAccessServer {
71
94
  }
72
95
  async handleToolCall(request) {
73
96
  const { name, arguments: args = {} } = request.params;
97
+ const typedArgs = args;
74
98
  try {
75
99
  switch (name) {
76
100
  case "get_infrastructure_news":
77
101
  return await this.getInfrastructureNewsRouter({
78
- resource: args.query,
79
- time: args.time,
80
- resource_ids: args.ids,
81
- limit: args.limit,
82
- use_group_api: args.use_group_api
102
+ resource: typedArgs.query,
103
+ time: typedArgs.time,
104
+ resource_ids: typedArgs.ids,
105
+ limit: typedArgs.limit,
106
+ use_group_api: typedArgs.use_group_api,
83
107
  });
84
108
  default:
85
109
  return this.errorResponse(`Unknown tool: ${name}`);
@@ -95,8 +119,8 @@ export class SystemStatusServer extends BaseAccessServer {
95
119
  */
96
120
  async getInfrastructureNewsRouter(args) {
97
121
  const { resource, time = "current", resource_ids, limit, use_group_api = false } = args;
98
- // Check resource status (returns operational/affected)
99
- 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) {
100
124
  return await this.checkResourceStatus(resource_ids, use_group_api);
101
125
  }
102
126
  // Time-based routing
@@ -116,7 +140,7 @@ export class SystemStatusServer extends BaseAccessServer {
116
140
  async handleResourceRead(request) {
117
141
  const { uri } = request.params;
118
142
  switch (uri) {
119
- case "accessci://system-status":
143
+ case "accessci://system-status": {
120
144
  return {
121
145
  contents: [
122
146
  {
@@ -126,39 +150,49 @@ export class SystemStatusServer extends BaseAccessServer {
126
150
  },
127
151
  ],
128
152
  };
129
- case "accessci://outages/current":
153
+ }
154
+ case "accessci://outages/current": {
130
155
  const currentOutages = await this.getCurrentOutages();
156
+ const content = currentOutages.content[0];
157
+ const text = content.type === "text" ? content.text : "";
131
158
  return {
132
159
  contents: [
133
160
  {
134
161
  uri,
135
162
  mimeType: "application/json",
136
- text: currentOutages.content[0].text,
163
+ text,
137
164
  },
138
165
  ],
139
166
  };
140
- case "accessci://outages/scheduled":
167
+ }
168
+ case "accessci://outages/scheduled": {
141
169
  const scheduledMaintenance = await this.getScheduledMaintenance();
170
+ const content = scheduledMaintenance.content[0];
171
+ const text = content.type === "text" ? content.text : "";
142
172
  return {
143
173
  contents: [
144
174
  {
145
175
  uri,
146
176
  mimeType: "application/json",
147
- text: scheduledMaintenance.content[0].text,
177
+ text,
148
178
  },
149
179
  ],
150
180
  };
151
- case "accessci://outages/past":
181
+ }
182
+ case "accessci://outages/past": {
152
183
  const pastOutages = await this.getPastOutages();
184
+ const content = pastOutages.content[0];
185
+ const text = content.type === "text" ? content.text : "";
153
186
  return {
154
187
  contents: [
155
188
  {
156
189
  uri,
157
190
  mimeType: "application/json",
158
- text: pastOutages.content[0].text,
191
+ text,
159
192
  },
160
193
  ],
161
194
  };
195
+ }
162
196
  default:
163
197
  throw new Error(`Unknown resource: ${uri}`);
164
198
  }
@@ -178,9 +212,11 @@ export class SystemStatusServer extends BaseAccessServer {
178
212
  const severityCounts = { high: 0, medium: 0, low: 0, unknown: 0 };
179
213
  // Enhance outages with status summary
180
214
  const enhancedOutages = outages.map((outage) => {
181
- // Track affected resources
215
+ // Track affected resources (use ResourceID as fallback if ResourceName is missing)
182
216
  outage.AffectedResources?.forEach((resource) => {
183
- affectedResources.add(resource.ResourceName);
217
+ const resourceIdentifier = resource.ResourceName || resource.ResourceID;
218
+ if (resourceIdentifier)
219
+ affectedResources.add(String(resourceIdentifier));
184
220
  });
185
221
  // Categorize severity (basic heuristic)
186
222
  const subject = outage.Subject?.toLowerCase() || "";
@@ -188,8 +224,7 @@ export class SystemStatusServer extends BaseAccessServer {
188
224
  if (subject.includes("emergency") || subject.includes("critical")) {
189
225
  severity = "high";
190
226
  }
191
- else if (subject.includes("maintenance") ||
192
- subject.includes("scheduled")) {
227
+ else if (subject.includes("maintenance") || subject.includes("scheduled")) {
193
228
  severity = "low";
194
229
  }
195
230
  else {
@@ -228,8 +263,8 @@ export class SystemStatusServer extends BaseAccessServer {
228
263
  }
229
264
  // Sort by scheduled start time
230
265
  maintenance.sort((a, b) => {
231
- const dateA = new Date(a.OutageStart);
232
- const dateB = new Date(b.OutageStart);
266
+ const dateA = new Date(a.OutageStart || "");
267
+ const dateB = new Date(b.OutageStart || "");
233
268
  return dateA.getTime() - dateB.getTime();
234
269
  });
235
270
  // Initialize tracking variables
@@ -239,11 +274,13 @@ export class SystemStatusServer extends BaseAccessServer {
239
274
  const enhancedMaintenance = maintenance.map((item) => {
240
275
  // Track affected resources
241
276
  item.AffectedResources?.forEach((resource) => {
242
- affectedResources.add(resource.ResourceName);
277
+ const resourceIdentifier = resource.ResourceName || resource.ResourceID;
278
+ if (resourceIdentifier)
279
+ affectedResources.add(String(resourceIdentifier));
243
280
  });
244
281
  // Check timing - use OutageStart for scheduling
245
282
  const hasScheduledTime = !!item.OutageStart;
246
- const startTime = new Date(item.OutageStart);
283
+ const startTime = new Date(item.OutageStart || "");
247
284
  const now = new Date();
248
285
  const hoursUntil = (startTime.getTime() - now.getTime()) / (1000 * 60 * 60);
249
286
  if (hoursUntil <= 24)
@@ -256,8 +293,7 @@ export class SystemStatusServer extends BaseAccessServer {
256
293
  scheduled_end: item.OutageEnd,
257
294
  hours_until_start: Math.max(0, Math.round(hoursUntil)),
258
295
  duration_hours: item.OutageEnd && item.OutageStart
259
- ? Math.round((new Date(item.OutageEnd).getTime() -
260
- new Date(item.OutageStart).getTime()) /
296
+ ? Math.round((new Date(item.OutageEnd).getTime() - new Date(item.OutageStart).getTime()) /
261
297
  (1000 * 60 * 60))
262
298
  : null,
263
299
  has_scheduled_time: hasScheduledTime,
@@ -291,8 +327,8 @@ export class SystemStatusServer extends BaseAccessServer {
291
327
  }
292
328
  // Sort by outage end time (most recent first)
293
329
  pastOutages.sort((a, b) => {
294
- const dateA = new Date(a.OutageEnd);
295
- const dateB = new Date(b.OutageEnd);
330
+ const dateA = new Date(a.OutageEnd || "");
331
+ const dateB = new Date(b.OutageEnd || "");
296
332
  return dateB.getTime() - dateA.getTime();
297
333
  });
298
334
  // Apply limit
@@ -303,23 +339,25 @@ export class SystemStatusServer extends BaseAccessServer {
303
339
  const affectedResources = new Set();
304
340
  const outageTypes = new Set();
305
341
  const recentOutages = pastOutages.filter((outage) => {
306
- const endTime = new Date(outage.OutageEnd);
342
+ const endTime = new Date(outage.OutageEnd || "");
307
343
  const daysAgo = (Date.now() - endTime.getTime()) / (1000 * 60 * 60 * 24);
308
344
  return daysAgo <= 30; // Last 30 days
309
345
  });
310
346
  // Enhance outages with calculated fields
311
347
  const enhancedOutages = pastOutages.map((outage) => {
312
- // Track affected resources
348
+ // Track affected resources (use ResourceID as fallback if ResourceName is missing)
313
349
  outage.AffectedResources?.forEach((resource) => {
314
- affectedResources.add(resource.ResourceName);
350
+ const resourceIdentifier = resource.ResourceName || resource.ResourceID;
351
+ if (resourceIdentifier)
352
+ affectedResources.add(String(resourceIdentifier));
315
353
  });
316
354
  // Track outage types
317
355
  if (outage.OutageType) {
318
356
  outageTypes.add(outage.OutageType);
319
357
  }
320
358
  // Calculate duration
321
- const startTime = new Date(outage.OutageStart);
322
- const endTime = new Date(outage.OutageEnd);
359
+ const startTime = new Date(outage.OutageStart || "");
360
+ const endTime = new Date(outage.OutageEnd || "");
323
361
  const durationHours = Math.round((endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60));
324
362
  // Calculate how long ago it ended
325
363
  const daysAgo = Math.round((Date.now() - endTime.getTime()) / (1000 * 60 * 60 * 24));
@@ -339,9 +377,9 @@ export class SystemStatusServer extends BaseAccessServer {
339
377
  outage_types: Array.from(outageTypes),
340
378
  average_duration_hours: enhancedOutages.length > 0
341
379
  ? Math.round(enhancedOutages
342
- .filter((o) => o.duration_hours > 0)
343
- .reduce((sum, o) => sum + o.duration_hours, 0) /
344
- enhancedOutages.filter((o) => o.duration_hours > 0).length)
380
+ .filter((o) => o.duration_hours && o.duration_hours > 0)
381
+ .reduce((sum, o) => sum + (o.duration_hours || 0), 0) /
382
+ enhancedOutages.filter((o) => o.duration_hours && o.duration_hours > 0).length)
345
383
  : 0,
346
384
  outages: enhancedOutages,
347
385
  };
@@ -363,27 +401,30 @@ export class SystemStatusServer extends BaseAccessServer {
363
401
  ]);
364
402
  const currentOutages = currentResponse.data.results || [];
365
403
  const futureOutages = futureResponse.data.results || [];
366
- const pastOutages = pastResponse.data.results || [];
404
+ const pastOutagesData = pastResponse.data.results || [];
367
405
  // Filter recent past outages (last 30 days) for announcements
368
- const recentPastOutages = pastOutages.filter((outage) => {
369
- const endTime = new Date(outage.OutageEnd);
406
+ const recentPastOutages = pastOutagesData.filter((outage) => {
407
+ const endTime = new Date(outage.OutageEnd || "");
370
408
  const daysAgo = (Date.now() - endTime.getTime()) / (1000 * 60 * 60 * 24);
371
409
  return daysAgo <= 30;
372
410
  });
373
411
  // Combine all announcements and sort by most relevant date
374
412
  const allAnnouncements = [
375
- ...currentOutages.map((item) => ({ ...item, category: 'current' })),
376
- ...futureOutages.map((item) => ({ ...item, category: 'scheduled' })),
377
- ...recentPastOutages.map((item) => ({ ...item, category: 'recent_past' })),
413
+ ...currentOutages.map((item) => ({ ...item, category: "current" })),
414
+ ...futureOutages.map((item) => ({ ...item, category: "scheduled" })),
415
+ ...recentPastOutages.map((item) => ({
416
+ ...item,
417
+ category: "recent_past",
418
+ })),
378
419
  ]
379
420
  .sort((a, b) => {
380
421
  // Sort by most relevant date: current first, then future by start time, then past by end time
381
- if (a.category === 'current' && b.category !== 'current')
422
+ if (a.category === "current" && b.category !== "current")
382
423
  return -1;
383
- if (b.category === 'current' && a.category !== 'current')
424
+ if (b.category === "current" && a.category !== "current")
384
425
  return 1;
385
- const dateA = new Date(a.OutageStart);
386
- const dateB = new Date(b.OutageStart);
426
+ const dateA = new Date(a.OutageStart || "");
427
+ const dateB = new Date(b.OutageStart || "");
387
428
  return dateB.getTime() - dateA.getTime(); // Most recent first
388
429
  })
389
430
  .slice(0, limit);
@@ -393,9 +434,9 @@ export class SystemStatusServer extends BaseAccessServer {
393
434
  scheduled_maintenance: futureOutages.length,
394
435
  recent_past_outages: recentPastOutages.length,
395
436
  categories: {
396
- current: allAnnouncements.filter(a => a.category === 'current').length,
397
- scheduled: allAnnouncements.filter(a => a.category === 'scheduled').length,
398
- recent_past: allAnnouncements.filter(a => a.category === 'recent_past').length,
437
+ current: allAnnouncements.filter((a) => a.category === "current").length,
438
+ scheduled: allAnnouncements.filter((a) => a.category === "scheduled").length,
439
+ recent_past: allAnnouncements.filter((a) => a.category === "recent_past").length,
399
440
  },
400
441
  announcements: allAnnouncements,
401
442
  };
@@ -412,13 +453,43 @@ export class SystemStatusServer extends BaseAccessServer {
412
453
  if (!resourceIds || !Array.isArray(resourceIds) || resourceIds.length === 0) {
413
454
  throw new Error("resource_ids parameter is required and must be a non-empty array of resource IDs");
414
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
+ }
415
486
  if (useGroupApi) {
416
- return await this.checkResourceStatusViaGroups(resourceIds);
487
+ return await this.checkResourceStatusViaGroups(resolvedIds);
417
488
  }
418
489
  // Efficient approach: fetch raw current outages data once
419
490
  const response = await this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/current_outages/");
420
491
  const rawOutages = response.data.results || [];
421
- const resourceStatus = resourceIds.map((resourceId) => {
492
+ const resourceStatus = resolvedIds.map((resourceId) => {
422
493
  const affectedOutages = rawOutages.filter((outage) => outage.AffectedResources?.some((resource) => resource.ResourceID?.toString() === resourceId ||
423
494
  resource.ResourceName?.toLowerCase().includes(resourceId.toLowerCase())));
424
495
  let status = "operational";
@@ -462,10 +533,9 @@ export class SystemStatusServer extends BaseAccessServer {
462
533
  type: "text",
463
534
  text: JSON.stringify({
464
535
  checked_at: new Date().toISOString(),
465
- resources_checked: resourceIds.length,
536
+ resources_checked: resolvedIds.length,
466
537
  operational: resourceStatus.filter((r) => r.status === "operational").length,
467
- affected: resourceStatus.filter((r) => r.status === "affected")
468
- .length,
538
+ affected: resourceStatus.filter((r) => r.status === "affected").length,
469
539
  api_method: "direct_outages_check",
470
540
  resource_status: resourceStatus,
471
541
  }, null, 2),
@@ -494,7 +564,7 @@ export class SystemStatusServer extends BaseAccessServer {
494
564
  api_method: "group_specific",
495
565
  };
496
566
  }
497
- catch (error) {
567
+ catch {
498
568
  // Fallback to general check if group API fails
499
569
  return {
500
570
  resource_id: resourceId,
@@ -516,10 +586,8 @@ export class SystemStatusServer extends BaseAccessServer {
516
586
  checked_at: new Date().toISOString(),
517
587
  resources_checked: resourceIds.length,
518
588
  operational: resourceStatus.filter((r) => r.status === "operational").length,
519
- affected: resourceStatus.filter((r) => r.status === "affected")
520
- .length,
521
- unknown: resourceStatus.filter((r) => r.status === "unknown")
522
- .length,
589
+ affected: resourceStatus.filter((r) => r.status === "affected").length,
590
+ unknown: resourceStatus.filter((r) => r.status === "unknown").length,
523
591
  api_method: "resource_group_api",
524
592
  resource_status: resourceStatus,
525
593
  }, null, 2),
@@ -3,7 +3,7 @@ import path from "path";
3
3
  import { SystemStatusServer } from "./server.js";
4
4
  export function startWebServer(port = 3000) {
5
5
  const app = express();
6
- const server = new SystemStatusServer();
6
+ new SystemStatusServer();
7
7
  // Serve static files from public directory
8
8
  const publicDir = path.join(__dirname, "../../../public");
9
9
  app.use(express.static(publicDir));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@access-mcp/system-status",
3
- "version": "0.5.0",
3
+ "version": "0.6.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,7 @@
44
44
  "node": ">=18.0.0"
45
45
  },
46
46
  "dependencies": {
47
- "@access-mcp/shared": "^0.3.3",
47
+ "@access-mcp/shared": "^0.6.0",
48
48
  "express": "^4.18.0"
49
49
  },
50
50
  "devDependencies": {