@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.
@@ -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 common resource names that might exist
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: ["anvil", "bridges", "jetstream"],
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
- }, 10000);
139
+ }, 15000);
139
140
  it("should test group API functionality", async () => {
140
- // Test group API with a resource that might have a group ID
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: ["anvil"],
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).toHaveProperty("resource_id", "anvil");
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
- }, 10000);
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
- expect(() => JSON.parse(result.contents[0].text)).not.toThrow();
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-1" }],
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-resource"] },
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-resource
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-resource"],
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: "Period: current (active), scheduled (future), past, all",
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 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')",
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(resourceIds);
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 = resourceIds.map((resourceId) => {
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: resourceIds.length,
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",
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.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",