@access-mcp/system-status 0.3.0 → 0.4.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
@@ -16,6 +16,11 @@ Get current system outages and issues affecting ACCESS-CI resources.
16
16
 
17
17
  - `resource_filter` (string, optional): Filter by specific resource name or ID
18
18
 
19
+ **Returns:**
20
+ - Total outages count and severity breakdown
21
+ - Affected resources list
22
+ - Enhanced outage details with severity levels
23
+
19
24
  ### get_scheduled_maintenance
20
25
 
21
26
  Get scheduled maintenance and future outages for ACCESS-CI resources.
@@ -24,21 +29,51 @@ Get scheduled maintenance and future outages for ACCESS-CI resources.
24
29
 
25
30
  - `resource_filter` (string, optional): Filter by specific resource name or ID
26
31
 
32
+ **Returns:**
33
+ - Scheduled maintenance sorted by start time
34
+ - Time until maintenance starts
35
+ - Duration calculations for planned windows
36
+
37
+ ### get_past_outages
38
+
39
+ Get historical outages and past incidents affecting ACCESS-CI resources.
40
+
41
+ **Parameters:**
42
+
43
+ - `resource_filter` (string, optional): Filter by specific resource name or ID
44
+ - `limit` (number, optional): Maximum number of past outages to return (default: 100)
45
+
46
+ **Returns:**
47
+ - Historical outage data with duration analysis
48
+ - Recent outages (last 30 days) summary
49
+ - Outage type categorization
50
+
27
51
  ### get_system_announcements
28
52
 
29
- Get all system announcements (current and scheduled).
53
+ Get comprehensive system announcements combining current, scheduled, and recent past outages.
30
54
 
31
55
  **Parameters:**
32
56
 
33
57
  - `limit` (number, optional): Maximum number of announcements to return (default: 50)
34
58
 
35
- ### get_resource_status
59
+ **Returns:**
60
+ - Unified view of current outages, scheduled maintenance, and recent past incidents
61
+ - Categorized announcements for better organization
62
+ - Timeline-based sorting
63
+
64
+ ### check_resource_status
36
65
 
37
- Get the current operational status of a specific resource.
66
+ Check the operational status of specific ACCESS-CI resources.
38
67
 
39
68
  **Parameters:**
40
69
 
41
- - `resource_id` (string): The resource ID to check status for
70
+ - `resource_ids` (array): List of resource IDs or names to check status for
71
+ - `use_group_api` (boolean, optional): Use resource group API for more efficient querying (default: false)
72
+
73
+ **Returns:**
74
+ - Operational status for each requested resource
75
+ - Active outage details with severity levels
76
+ - API method used (direct vs group-specific)
42
77
 
43
78
  ## Resources
44
79
 
@@ -98,11 +133,13 @@ Add to your Claude Desktop configuration:
98
133
  **Natural Language**: "Are there any systems down right now?"
99
134
 
100
135
  **Tool Call**:
136
+
101
137
  ```typescript
102
138
  const outages = await get_current_outages();
103
139
  ```
104
140
 
105
141
  **Returns**: List of active outages with:
142
+
106
143
  - Affected resources
107
144
  - Start time and expected resolution
108
145
  - Impact description
@@ -113,13 +150,15 @@ const outages = await get_current_outages();
113
150
  **Natural Language**: "When is Delta scheduled for maintenance?"
114
151
 
115
152
  **Tool Call**:
153
+
116
154
  ```typescript
117
155
  const maintenance = await get_scheduled_maintenance({
118
- resource_filter: "delta"
156
+ resource_filter: "delta",
119
157
  });
120
158
  ```
121
159
 
122
160
  **Returns**: Upcoming maintenance windows including:
161
+
123
162
  - Scheduled start and end times
124
163
  - Systems affected
125
164
  - Type of maintenance
@@ -130,13 +169,15 @@ const maintenance = await get_scheduled_maintenance({
130
169
  **Natural Language**: "What are the latest announcements?"
131
170
 
132
171
  **Tool Call**:
172
+
133
173
  ```typescript
134
174
  const announcements = await get_system_announcements({
135
- limit: 10
175
+ limit: 10,
136
176
  });
137
177
  ```
138
178
 
139
179
  **Returns**: Recent announcements about:
180
+
140
181
  - Policy changes
141
182
  - New features or services
142
183
  - Important deadlines
@@ -147,13 +188,15 @@ const announcements = await get_system_announcements({
147
188
  **Natural Language**: "Is Expanse available?"
148
189
 
149
190
  **Tool Call**:
191
+
150
192
  ```typescript
151
193
  const status = await get_resource_status({
152
- resource_id: "expanse.sdsc.xsede.org"
194
+ resource_id: "expanse.sdsc.xsede.org",
153
195
  });
154
196
  ```
155
197
 
156
198
  **Returns**: Current operational status:
199
+
157
200
  - Overall system health
158
201
  - Service availability
159
202
  - Performance metrics
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,225 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { SystemStatusServer } from "../server.js";
3
+ describe("SystemStatusServer Integration Tests", () => {
4
+ let server;
5
+ beforeEach(() => {
6
+ server = new SystemStatusServer();
7
+ });
8
+ describe("Real API Integration", () => {
9
+ it("should fetch current outages from real API", async () => {
10
+ const result = await server["handleToolCall"]({
11
+ params: {
12
+ name: "get_current_outages",
13
+ arguments: { limit: 5 }
14
+ }
15
+ });
16
+ const responseData = JSON.parse(result.content[0].text);
17
+ expect(responseData).toHaveProperty("total_outages");
18
+ expect(responseData).toHaveProperty("affected_resources");
19
+ expect(responseData).toHaveProperty("severity_counts");
20
+ expect(responseData.severity_counts).toHaveProperty("high");
21
+ expect(responseData.severity_counts).toHaveProperty("medium");
22
+ expect(responseData.severity_counts).toHaveProperty("low");
23
+ expect(responseData.severity_counts).toHaveProperty("unknown");
24
+ expect(Array.isArray(responseData.outages)).toBe(true);
25
+ // Check enhanced fields
26
+ if (responseData.outages.length > 0) {
27
+ const outage = responseData.outages[0];
28
+ expect(outage).toHaveProperty("severity");
29
+ expect(outage).toHaveProperty("posted_time");
30
+ expect(outage).toHaveProperty("last_updated");
31
+ }
32
+ }, 10000);
33
+ it("should fetch scheduled maintenance from real API", async () => {
34
+ const result = await server["handleToolCall"]({
35
+ params: {
36
+ name: "get_scheduled_maintenance",
37
+ arguments: { limit: 5 }
38
+ }
39
+ });
40
+ const responseData = JSON.parse(result.content[0].text);
41
+ expect(responseData).toHaveProperty("total_scheduled");
42
+ expect(responseData).toHaveProperty("upcoming_24h");
43
+ expect(responseData).toHaveProperty("upcoming_week");
44
+ expect(responseData).toHaveProperty("affected_resources");
45
+ expect(Array.isArray(responseData.maintenance)).toBe(true);
46
+ // Check enhanced fields
47
+ if (responseData.maintenance.length > 0) {
48
+ const maintenance = responseData.maintenance[0];
49
+ expect(maintenance).toHaveProperty("hours_until_start");
50
+ expect(maintenance).toHaveProperty("has_scheduled_time");
51
+ expect(maintenance.hours_until_start).toSatisfy((val) => val === null || typeof val === "number");
52
+ }
53
+ }, 10000);
54
+ it("should fetch past outages from real API", async () => {
55
+ const result = await server["handleToolCall"]({
56
+ params: {
57
+ name: "get_past_outages",
58
+ arguments: { limit: 10 }
59
+ }
60
+ });
61
+ const responseData = JSON.parse(result.content[0].text);
62
+ expect(responseData).toHaveProperty("total_past_outages");
63
+ expect(responseData).toHaveProperty("recent_outages_30_days");
64
+ expect(responseData).toHaveProperty("affected_resources");
65
+ expect(responseData).toHaveProperty("outage_types");
66
+ expect(responseData).toHaveProperty("average_duration_hours");
67
+ expect(Array.isArray(responseData.outages)).toBe(true);
68
+ // Check enhanced fields
69
+ if (responseData.outages.length > 0) {
70
+ const outage = responseData.outages[0];
71
+ expect(outage).toHaveProperty("duration_hours");
72
+ expect(outage).toHaveProperty("days_ago");
73
+ expect(outage).toHaveProperty("outage_type");
74
+ expect(outage.days_ago).toSatisfy((val) => val === null || typeof val === "number");
75
+ }
76
+ }, 10000);
77
+ it("should get comprehensive system announcements", async () => {
78
+ const result = await server["handleToolCall"]({
79
+ params: {
80
+ name: "get_system_announcements",
81
+ arguments: { limit: 20 }
82
+ }
83
+ });
84
+ const responseData = JSON.parse(result.content[0].text);
85
+ expect(responseData).toHaveProperty("total_announcements");
86
+ expect(responseData).toHaveProperty("current_outages");
87
+ expect(responseData).toHaveProperty("scheduled_maintenance");
88
+ expect(responseData).toHaveProperty("recent_past_outages");
89
+ expect(responseData).toHaveProperty("categories");
90
+ expect(responseData.categories).toHaveProperty("current");
91
+ expect(responseData.categories).toHaveProperty("scheduled");
92
+ expect(responseData.categories).toHaveProperty("recent_past");
93
+ expect(Array.isArray(responseData.announcements)).toBe(true);
94
+ // Check categorization
95
+ if (responseData.announcements.length > 0) {
96
+ const announcement = responseData.announcements[0];
97
+ expect(announcement).toHaveProperty("category");
98
+ expect(["current", "scheduled", "recent_past"]).toContain(announcement.category);
99
+ }
100
+ }, 15000);
101
+ it("should check resource status with direct method", async () => {
102
+ // Test with common resource names that might exist
103
+ const result = await server["handleToolCall"]({
104
+ params: {
105
+ name: "check_resource_status",
106
+ arguments: {
107
+ resource_ids: ["anvil", "bridges", "jetstream"],
108
+ use_group_api: false
109
+ }
110
+ }
111
+ });
112
+ const responseData = JSON.parse(result.content[0].text);
113
+ expect(responseData).toHaveProperty("checked_at");
114
+ expect(responseData).toHaveProperty("resources_checked", 3);
115
+ expect(responseData).toHaveProperty("operational");
116
+ expect(responseData).toHaveProperty("affected");
117
+ expect(responseData).toHaveProperty("api_method", "direct_outages_check");
118
+ expect(Array.isArray(responseData.resource_status)).toBe(true);
119
+ expect(responseData.resource_status).toHaveLength(3);
120
+ // Check resource status structure
121
+ responseData.resource_status.forEach((resource) => {
122
+ expect(resource).toHaveProperty("resource_id");
123
+ expect(resource).toHaveProperty("status");
124
+ expect(["operational", "affected"]).toContain(resource.status);
125
+ expect(resource).toHaveProperty("active_outages");
126
+ expect(Array.isArray(resource.outage_details)).toBe(true);
127
+ });
128
+ }, 10000);
129
+ it("should test group API functionality", async () => {
130
+ // Test group API with a resource that might have a group ID
131
+ const result = await server["handleToolCall"]({
132
+ params: {
133
+ name: "check_resource_status",
134
+ arguments: {
135
+ resource_ids: ["anvil"],
136
+ use_group_api: true
137
+ }
138
+ }
139
+ });
140
+ const responseData = JSON.parse(result.content[0].text);
141
+ expect(responseData).toHaveProperty("api_method", "resource_group_api");
142
+ expect(responseData).toHaveProperty("resources_checked", 1);
143
+ expect(responseData.resource_status).toHaveLength(1);
144
+ const resourceStatus = responseData.resource_status[0];
145
+ expect(resourceStatus).toHaveProperty("resource_id", "anvil");
146
+ expect(resourceStatus).toHaveProperty("api_method");
147
+ expect(["group_specific", "group_specific_failed"]).toContain(resourceStatus.api_method);
148
+ // If it succeeded, check structure
149
+ if (resourceStatus.api_method === "group_specific") {
150
+ expect(resourceStatus).toHaveProperty("status");
151
+ expect(["operational", "affected"]).toContain(resourceStatus.status);
152
+ }
153
+ // If it failed, check error handling
154
+ if (resourceStatus.api_method === "group_specific_failed") {
155
+ expect(resourceStatus.status).toBe("unknown");
156
+ expect(resourceStatus).toHaveProperty("error");
157
+ }
158
+ }, 10000);
159
+ it("should filter outages by resource correctly", async () => {
160
+ const result = await server["handleToolCall"]({
161
+ params: {
162
+ name: "get_current_outages",
163
+ arguments: { resource_filter: "anvil" }
164
+ }
165
+ });
166
+ const responseData = JSON.parse(result.content[0].text);
167
+ expect(responseData).toHaveProperty("total_outages");
168
+ // If there are any results, they should match the filter
169
+ responseData.outages.forEach((outage) => {
170
+ const matchesFilter = outage.Subject?.toLowerCase().includes("anvil") ||
171
+ outage.AffectedResources?.some((resource) => resource.ResourceName?.toLowerCase().includes("anvil") ||
172
+ resource.ResourceID?.toString().includes("anvil"));
173
+ expect(matchesFilter).toBe(true);
174
+ });
175
+ }, 10000);
176
+ it("should handle resource reads for all endpoints", async () => {
177
+ const resources = [
178
+ "accessci://system-status",
179
+ "accessci://outages/current",
180
+ "accessci://outages/scheduled",
181
+ "accessci://outages/past"
182
+ ];
183
+ for (const uri of resources) {
184
+ const result = await server["handleResourceRead"]({
185
+ params: { uri }
186
+ });
187
+ expect(result.contents).toHaveLength(1);
188
+ expect(result.contents[0]).toHaveProperty("uri", uri);
189
+ expect(result.contents[0]).toHaveProperty("mimeType");
190
+ expect(result.contents[0]).toHaveProperty("text");
191
+ if (uri !== "accessci://system-status") {
192
+ // JSON resources should have valid JSON
193
+ expect(() => JSON.parse(result.contents[0].text)).not.toThrow();
194
+ }
195
+ }
196
+ }, 15000);
197
+ });
198
+ describe("Edge Cases and Error Handling", () => {
199
+ it("should handle empty API responses", async () => {
200
+ // This tests the robustness of our logic with potentially empty responses
201
+ const result = await server["handleToolCall"]({
202
+ params: {
203
+ name: "get_current_outages",
204
+ arguments: { resource_filter: "nonexistent-resource-xyz" }
205
+ }
206
+ });
207
+ const responseData = JSON.parse(result.content[0].text);
208
+ expect(responseData).toHaveProperty("total_outages", 0);
209
+ expect(responseData.outages).toHaveLength(0);
210
+ expect(responseData.affected_resources).toHaveLength(0);
211
+ }, 10000);
212
+ it("should handle large limit values gracefully", async () => {
213
+ const result = await server["handleToolCall"]({
214
+ params: {
215
+ name: "get_past_outages",
216
+ arguments: { limit: 1000 }
217
+ }
218
+ });
219
+ const responseData = JSON.parse(result.content[0].text);
220
+ expect(responseData).toHaveProperty("total_past_outages");
221
+ // Should not crash or timeout
222
+ expect(responseData.outages.length).toBeLessThanOrEqual(1000);
223
+ }, 15000);
224
+ });
225
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,380 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2
+ import { SystemStatusServer } from "../server.js";
3
+ // Mock axios
4
+ vi.mock("axios");
5
+ describe("SystemStatusServer", () => {
6
+ let server;
7
+ let mockHttpClient;
8
+ const mockCurrentOutagesData = [
9
+ {
10
+ id: "1",
11
+ Subject: "Emergency maintenance on Anvil",
12
+ Content: "Critical issue requiring immediate attention",
13
+ CreationTime: "2024-08-27T10:00:00Z",
14
+ LastModificationTime: "2024-08-27T11:00:00Z",
15
+ AffectedResources: [
16
+ { ResourceName: "Anvil", ResourceID: "anvil-1" }
17
+ ]
18
+ },
19
+ {
20
+ id: "2",
21
+ Subject: "Scheduled maintenance on Bridges-2",
22
+ Content: "Regular maintenance window",
23
+ CreationTime: "2024-08-27T08:00:00Z",
24
+ LastModificationTime: "2024-08-27T08:30:00Z",
25
+ AffectedResources: [
26
+ { ResourceName: "Bridges-2", ResourceID: "bridges2-1" }
27
+ ]
28
+ }
29
+ ];
30
+ const mockFutureOutagesData = [
31
+ {
32
+ id: "3",
33
+ Subject: "Scheduled Jetstream maintenance",
34
+ Content: "Planned maintenance",
35
+ CreationTime: "2024-08-27T09:00:00Z",
36
+ LastModificationTime: "2024-08-27T09:00:00Z",
37
+ OutageStartDateTime: "2024-08-30T10:00:00Z",
38
+ OutageEndDateTime: "2024-08-30T14:00:00Z",
39
+ AffectedResources: [
40
+ { ResourceName: "Jetstream", ResourceID: "jetstream-1" }
41
+ ]
42
+ }
43
+ ];
44
+ const mockPastOutagesData = [
45
+ {
46
+ id: "4",
47
+ Subject: "Past maintenance on Stampede3",
48
+ Content: "Completed maintenance",
49
+ CreationTime: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), // 5 days ago
50
+ LastModificationTime: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), // 2 days ago
51
+ OutageStartDateTime: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days ago
52
+ OutageEndDateTime: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000 + 6 * 60 * 60 * 1000).toISOString(), // 3 days ago + 6 hours
53
+ OutageType: "Full",
54
+ AffectedResources: [
55
+ { ResourceName: "Stampede3", ResourceID: "stampede3-1" }
56
+ ]
57
+ }
58
+ ];
59
+ beforeEach(() => {
60
+ server = new SystemStatusServer();
61
+ // Set up mock HTTP client
62
+ mockHttpClient = {
63
+ get: vi.fn(),
64
+ };
65
+ // Override the httpClient getter
66
+ Object.defineProperty(server, "httpClient", {
67
+ get: () => mockHttpClient,
68
+ configurable: true,
69
+ });
70
+ });
71
+ afterEach(() => {
72
+ vi.clearAllMocks();
73
+ });
74
+ describe("Server Initialization", () => {
75
+ it("should initialize with correct server info", () => {
76
+ expect(server).toBeDefined();
77
+ expect(server["serverName"]).toBe("access-mcp-system-status");
78
+ expect(server["version"]).toBe("0.4.0");
79
+ expect(server["baseURL"]).toBe("https://operations-api.access-ci.org");
80
+ });
81
+ it("should provide correct tools", () => {
82
+ const tools = server["getTools"]();
83
+ expect(tools).toHaveLength(5);
84
+ expect(tools.map(t => t.name)).toEqual([
85
+ "get_current_outages",
86
+ "get_scheduled_maintenance",
87
+ "get_past_outages",
88
+ "get_system_announcements",
89
+ "check_resource_status"
90
+ ]);
91
+ });
92
+ it("should provide correct resources", () => {
93
+ const resources = server["getResources"]();
94
+ expect(resources).toHaveLength(4);
95
+ expect(resources.map(r => r.uri)).toEqual([
96
+ "accessci://system-status",
97
+ "accessci://outages/current",
98
+ "accessci://outages/scheduled",
99
+ "accessci://outages/past"
100
+ ]);
101
+ });
102
+ });
103
+ describe("getCurrentOutages", () => {
104
+ it("should fetch and enhance current outages", async () => {
105
+ mockHttpClient.get.mockResolvedValue({
106
+ status: 200,
107
+ data: { results: mockCurrentOutagesData }
108
+ });
109
+ const result = await server["handleToolCall"]({
110
+ params: { name: "get_current_outages", arguments: {} }
111
+ });
112
+ expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/news/v1/affiliation/access-ci.org/current_outages/");
113
+ const response = JSON.parse(result.content[0].text);
114
+ expect(response.total_outages).toBe(2);
115
+ expect(response.affected_resources).toEqual(["Anvil", "Bridges-2"]);
116
+ expect(response.severity_counts).toHaveProperty("high", 1); // Emergency
117
+ expect(response.severity_counts).toHaveProperty("low", 1); // Scheduled maintenance
118
+ expect(response.outages[0]).toHaveProperty("severity");
119
+ expect(response.outages[0]).toHaveProperty("posted_time");
120
+ });
121
+ it("should filter outages by resource", async () => {
122
+ mockHttpClient.get.mockResolvedValue({
123
+ status: 200,
124
+ data: { results: mockCurrentOutagesData }
125
+ });
126
+ const result = await server["handleToolCall"]({
127
+ params: {
128
+ name: "get_current_outages",
129
+ arguments: { resource_filter: "Anvil" }
130
+ }
131
+ });
132
+ const response = JSON.parse(result.content[0].text);
133
+ expect(response.total_outages).toBe(1);
134
+ expect(response.outages[0].Subject).toContain("Anvil");
135
+ });
136
+ it("should categorize severity correctly", async () => {
137
+ mockHttpClient.get.mockResolvedValue({
138
+ status: 200,
139
+ data: { results: mockCurrentOutagesData }
140
+ });
141
+ const result = await server["handleToolCall"]({
142
+ params: { name: "get_current_outages", arguments: {} }
143
+ });
144
+ const response = JSON.parse(result.content[0].text);
145
+ const emergencyOutage = response.outages.find((o) => o.Subject.includes("Emergency"));
146
+ const maintenanceOutage = response.outages.find((o) => o.Subject.includes("Scheduled"));
147
+ expect(emergencyOutage.severity).toBe("high");
148
+ expect(maintenanceOutage.severity).toBe("low");
149
+ });
150
+ });
151
+ describe("getScheduledMaintenance", () => {
152
+ it("should fetch and enhance scheduled maintenance", async () => {
153
+ mockHttpClient.get.mockResolvedValue({
154
+ status: 200,
155
+ data: { results: mockFutureOutagesData }
156
+ });
157
+ const result = await server["handleToolCall"]({
158
+ params: { name: "get_scheduled_maintenance", arguments: {} }
159
+ });
160
+ const response = JSON.parse(result.content[0].text);
161
+ expect(response.total_scheduled).toBe(1);
162
+ expect(response.affected_resources).toEqual(["Jetstream"]);
163
+ expect(response.maintenance[0]).toHaveProperty("hours_until_start");
164
+ expect(response.maintenance[0]).toHaveProperty("duration_hours", 4); // 10am to 2pm = 4 hours
165
+ expect(response.maintenance[0]).toHaveProperty("has_scheduled_time", true);
166
+ });
167
+ it("should handle missing scheduled times", async () => {
168
+ const dataWithoutSchedule = [{
169
+ ...mockFutureOutagesData[0],
170
+ OutageStartDateTime: null,
171
+ OutageEndDateTime: null
172
+ }];
173
+ mockHttpClient.get.mockResolvedValue({
174
+ status: 200,
175
+ data: { results: dataWithoutSchedule }
176
+ });
177
+ const result = await server["handleToolCall"]({
178
+ params: { name: "get_scheduled_maintenance", arguments: {} }
179
+ });
180
+ const response = JSON.parse(result.content[0].text);
181
+ expect(response.maintenance[0].has_scheduled_time).toBe(false);
182
+ expect(response.maintenance[0].duration_hours).toBe(null);
183
+ });
184
+ it("should sort by scheduled start time", async () => {
185
+ const multipleMaintenanceData = [
186
+ {
187
+ ...mockFutureOutagesData[0],
188
+ OutageStartDateTime: "2024-08-31T10:00:00Z", // Later
189
+ Subject: "Later maintenance"
190
+ },
191
+ {
192
+ ...mockFutureOutagesData[0],
193
+ OutageStartDateTime: "2024-08-30T10:00:00Z", // Earlier
194
+ Subject: "Earlier maintenance"
195
+ }
196
+ ];
197
+ mockHttpClient.get.mockResolvedValue({
198
+ status: 200,
199
+ data: { results: multipleMaintenanceData }
200
+ });
201
+ const result = await server["handleToolCall"]({
202
+ params: { name: "get_scheduled_maintenance", arguments: {} }
203
+ });
204
+ const response = JSON.parse(result.content[0].text);
205
+ expect(response.maintenance[0].Subject).toBe("Earlier maintenance");
206
+ expect(response.maintenance[1].Subject).toBe("Later maintenance");
207
+ });
208
+ });
209
+ describe("getPastOutages", () => {
210
+ it("should fetch and enhance past outages", async () => {
211
+ mockHttpClient.get.mockResolvedValue({
212
+ status: 200,
213
+ data: { results: mockPastOutagesData }
214
+ });
215
+ const result = await server["handleToolCall"]({
216
+ params: { name: "get_past_outages", arguments: {} }
217
+ });
218
+ const response = JSON.parse(result.content[0].text);
219
+ expect(response.total_past_outages).toBe(1);
220
+ expect(response.outage_types).toEqual(["Full"]);
221
+ expect(response.average_duration_hours).toBe(6); // 6 hour duration
222
+ expect(response.outages[0]).toHaveProperty("duration_hours", 6);
223
+ expect(response.outages[0]).toHaveProperty("days_ago");
224
+ });
225
+ it("should apply limit correctly", async () => {
226
+ const manyOutages = Array(50).fill(0).map((_, i) => ({
227
+ ...mockPastOutagesData[0],
228
+ id: `past-${i}`,
229
+ Subject: `Past outage ${i}`
230
+ }));
231
+ mockHttpClient.get.mockResolvedValue({
232
+ status: 200,
233
+ data: { results: manyOutages }
234
+ });
235
+ const result = await server["handleToolCall"]({
236
+ params: {
237
+ name: "get_past_outages",
238
+ arguments: { limit: 10 }
239
+ }
240
+ });
241
+ const response = JSON.parse(result.content[0].text);
242
+ expect(response.outages).toHaveLength(10);
243
+ });
244
+ });
245
+ describe("getSystemAnnouncements", () => {
246
+ it("should combine current, future, and recent past outages", async () => {
247
+ mockHttpClient.get
248
+ .mockResolvedValueOnce({ status: 200, data: { results: mockCurrentOutagesData } })
249
+ .mockResolvedValueOnce({ status: 200, data: { results: mockFutureOutagesData } })
250
+ .mockResolvedValueOnce({ status: 200, data: { results: mockPastOutagesData } });
251
+ const result = await server["handleToolCall"]({
252
+ params: { name: "get_system_announcements", arguments: {} }
253
+ });
254
+ expect(mockHttpClient.get).toHaveBeenCalledTimes(3);
255
+ const response = JSON.parse(result.content[0].text);
256
+ expect(response.current_outages).toBe(2);
257
+ expect(response.scheduled_maintenance).toBe(1);
258
+ expect(response.recent_past_outages).toBe(1); // Within 30 days
259
+ expect(response.categories).toHaveProperty("current");
260
+ expect(response.categories).toHaveProperty("scheduled");
261
+ expect(response.categories).toHaveProperty("recent_past");
262
+ });
263
+ it("should prioritize current outages in sorting", async () => {
264
+ mockHttpClient.get
265
+ .mockResolvedValueOnce({ status: 200, data: { results: mockCurrentOutagesData } })
266
+ .mockResolvedValueOnce({ status: 200, data: { results: mockFutureOutagesData } })
267
+ .mockResolvedValueOnce({ status: 200, data: { results: mockPastOutagesData } });
268
+ const result = await server["handleToolCall"]({
269
+ params: { name: "get_system_announcements", arguments: {} }
270
+ });
271
+ const response = JSON.parse(result.content[0].text);
272
+ const firstAnnouncement = response.announcements[0];
273
+ expect(firstAnnouncement.category).toBe("current");
274
+ });
275
+ });
276
+ describe("checkResourceStatus", () => {
277
+ it("should check resource status efficiently (direct method)", async () => {
278
+ mockHttpClient.get.mockResolvedValue({
279
+ status: 200,
280
+ data: { results: mockCurrentOutagesData }
281
+ });
282
+ const result = await server["handleToolCall"]({
283
+ params: {
284
+ name: "check_resource_status",
285
+ arguments: { resource_ids: ["anvil-1", "unknown-resource"] }
286
+ }
287
+ });
288
+ const response = JSON.parse(result.content[0].text);
289
+ expect(response.api_method).toBe("direct_outages_check");
290
+ expect(response.resources_checked).toBe(2);
291
+ expect(response.operational).toBe(1); // unknown-resource
292
+ expect(response.affected).toBe(1); // anvil-1
293
+ const anvilStatus = response.resource_status.find((r) => r.resource_id === "anvil-1");
294
+ expect(anvilStatus.status).toBe("affected");
295
+ expect(anvilStatus.severity).toBe("high"); // Emergency maintenance
296
+ });
297
+ it("should use group API when requested", async () => {
298
+ mockHttpClient.get.mockResolvedValue({
299
+ status: 200,
300
+ data: { results: [] } // No outages for this group
301
+ });
302
+ const result = await server["handleToolCall"]({
303
+ params: {
304
+ name: "check_resource_status",
305
+ arguments: {
306
+ resource_ids: ["anvil"],
307
+ use_group_api: true
308
+ }
309
+ }
310
+ });
311
+ expect(mockHttpClient.get).toHaveBeenCalledWith("/wh2/news/v1/info_groupid/anvil/");
312
+ const response = JSON.parse(result.content[0].text);
313
+ expect(response.api_method).toBe("resource_group_api");
314
+ expect(response.resource_status[0].status).toBe("operational");
315
+ expect(response.resource_status[0].api_method).toBe("group_specific");
316
+ });
317
+ it("should handle group API failures gracefully", async () => {
318
+ mockHttpClient.get.mockRejectedValue(new Error("API Error"));
319
+ const result = await server["handleToolCall"]({
320
+ params: {
321
+ name: "check_resource_status",
322
+ arguments: {
323
+ resource_ids: ["invalid-resource"],
324
+ use_group_api: true
325
+ }
326
+ }
327
+ });
328
+ const response = JSON.parse(result.content[0].text);
329
+ expect(response.unknown).toBe(1);
330
+ expect(response.resource_status[0].status).toBe("unknown");
331
+ expect(response.resource_status[0].api_method).toBe("group_specific_failed");
332
+ expect(response.resource_status[0]).toHaveProperty("error");
333
+ });
334
+ });
335
+ describe("Error Handling", () => {
336
+ it("should handle API errors gracefully", async () => {
337
+ mockHttpClient.get.mockResolvedValue({
338
+ status: 500,
339
+ statusText: "Internal Server Error"
340
+ });
341
+ const result = await server["handleToolCall"]({
342
+ params: { name: "get_current_outages", arguments: {} }
343
+ });
344
+ expect(result.content[0].text).toContain("Error");
345
+ });
346
+ it("should handle network errors", async () => {
347
+ mockHttpClient.get.mockRejectedValue(new Error("Network error"));
348
+ const result = await server["handleToolCall"]({
349
+ params: { name: "get_current_outages", arguments: {} }
350
+ });
351
+ expect(result.content[0].text).toContain("Error");
352
+ });
353
+ it("should handle unknown tools", async () => {
354
+ const result = await server["handleToolCall"]({
355
+ params: { name: "unknown_tool", arguments: {} }
356
+ });
357
+ expect(result.content[0].text).toContain("Unknown tool");
358
+ });
359
+ });
360
+ describe("Resource Handling", () => {
361
+ it("should handle resource reads correctly", async () => {
362
+ mockHttpClient.get.mockResolvedValue({
363
+ status: 200,
364
+ data: { results: mockCurrentOutagesData }
365
+ });
366
+ const result = await server["handleResourceRead"]({
367
+ params: { uri: "accessci://outages/current" }
368
+ });
369
+ expect(result.contents[0].mimeType).toBe("application/json");
370
+ expect(result.contents[0].text).toBeDefined();
371
+ });
372
+ it("should handle unknown resources", async () => {
373
+ await expect(async () => {
374
+ await server["handleResourceRead"]({
375
+ params: { uri: "accessci://unknown" }
376
+ });
377
+ }).rejects.toThrow("Unknown resource");
378
+ });
379
+ });
380
+ });
package/dist/server.d.ts CHANGED
@@ -13,6 +13,26 @@ export declare class SystemStatusServer extends BaseAccessServer {
13
13
  };
14
14
  limit?: undefined;
15
15
  resource_ids?: undefined;
16
+ use_group_api?: undefined;
17
+ };
18
+ required: never[];
19
+ };
20
+ } | {
21
+ name: string;
22
+ description: string;
23
+ inputSchema: {
24
+ type: string;
25
+ properties: {
26
+ resource_filter: {
27
+ type: string;
28
+ description: string;
29
+ };
30
+ limit: {
31
+ type: string;
32
+ description: string;
33
+ };
34
+ resource_ids?: undefined;
35
+ use_group_api?: undefined;
16
36
  };
17
37
  required: never[];
18
38
  };
@@ -28,6 +48,7 @@ export declare class SystemStatusServer extends BaseAccessServer {
28
48
  };
29
49
  resource_filter?: undefined;
30
50
  resource_ids?: undefined;
51
+ use_group_api?: undefined;
31
52
  };
32
53
  required: never[];
33
54
  };
@@ -44,6 +65,11 @@ export declare class SystemStatusServer extends BaseAccessServer {
44
65
  };
45
66
  description: string;
46
67
  };
68
+ use_group_api: {
69
+ type: string;
70
+ description: string;
71
+ default: boolean;
72
+ };
47
73
  resource_filter?: undefined;
48
74
  limit?: undefined;
49
75
  };
@@ -71,6 +97,8 @@ export declare class SystemStatusServer extends BaseAccessServer {
71
97
  }>;
72
98
  private getCurrentOutages;
73
99
  private getScheduledMaintenance;
100
+ private getPastOutages;
74
101
  private getSystemAnnouncements;
75
102
  private checkResourceStatus;
103
+ private checkResourceStatusViaGroups;
76
104
  }
package/dist/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BaseAccessServer, handleApiError } from "@access-mcp/shared";
2
2
  export class SystemStatusServer extends BaseAccessServer {
3
3
  constructor() {
4
- super("access-mcp-system-status", "0.3.0", "https://operations-api.access-ci.org");
4
+ super("access-mcp-system-status", "0.4.0", "https://operations-api.access-ci.org");
5
5
  }
6
6
  getTools() {
7
7
  return [
@@ -33,6 +33,24 @@ export class SystemStatusServer extends BaseAccessServer {
33
33
  required: [],
34
34
  },
35
35
  },
36
+ {
37
+ name: "get_past_outages",
38
+ description: "Get historical outages and past incidents affecting ACCESS-CI resources",
39
+ inputSchema: {
40
+ type: "object",
41
+ properties: {
42
+ resource_filter: {
43
+ type: "string",
44
+ description: "Optional: filter by specific resource name or ID",
45
+ },
46
+ limit: {
47
+ type: "number",
48
+ description: "Maximum number of past outages to return (default: 100)",
49
+ },
50
+ },
51
+ required: [],
52
+ },
53
+ },
36
54
  {
37
55
  name: "get_system_announcements",
38
56
  description: "Get all system announcements (current and scheduled)",
@@ -56,7 +74,12 @@ export class SystemStatusServer extends BaseAccessServer {
56
74
  resource_ids: {
57
75
  type: "array",
58
76
  items: { type: "string" },
59
- description: "List of resource IDs to check status for",
77
+ description: "List of resource IDs or names to check status for",
78
+ },
79
+ use_group_api: {
80
+ type: "boolean",
81
+ description: "Use resource group API for more efficient querying (default: false)",
82
+ default: false,
60
83
  },
61
84
  },
62
85
  required: ["resource_ids"],
@@ -84,6 +107,12 @@ export class SystemStatusServer extends BaseAccessServer {
84
107
  description: "Upcoming scheduled maintenance and planned outages",
85
108
  mimeType: "application/json",
86
109
  },
110
+ {
111
+ uri: "accessci://outages/past",
112
+ name: "Past Outages",
113
+ description: "Historical outages and past incidents",
114
+ mimeType: "application/json",
115
+ },
87
116
  ];
88
117
  }
89
118
  async handleToolCall(request) {
@@ -94,10 +123,12 @@ export class SystemStatusServer extends BaseAccessServer {
94
123
  return await this.getCurrentOutages(args.resource_filter);
95
124
  case "get_scheduled_maintenance":
96
125
  return await this.getScheduledMaintenance(args.resource_filter);
126
+ case "get_past_outages":
127
+ return await this.getPastOutages(args.resource_filter, args.limit);
97
128
  case "get_system_announcements":
98
129
  return await this.getSystemAnnouncements(args.limit);
99
130
  case "check_resource_status":
100
- return await this.checkResourceStatus(args.resource_ids);
131
+ return await this.checkResourceStatus(args.resource_ids, args.use_group_api);
101
132
  default:
102
133
  throw new Error(`Unknown tool: ${name}`);
103
134
  }
@@ -148,6 +179,17 @@ export class SystemStatusServer extends BaseAccessServer {
148
179
  },
149
180
  ],
150
181
  };
182
+ case "accessci://outages/past":
183
+ const pastOutages = await this.getPastOutages();
184
+ return {
185
+ contents: [
186
+ {
187
+ uri,
188
+ mimeType: "application/json",
189
+ text: pastOutages.content[0].text,
190
+ },
191
+ ],
192
+ };
151
193
  default:
152
194
  throw new Error(`Unknown resource: ${uri}`);
153
195
  }
@@ -202,10 +244,7 @@ export class SystemStatusServer extends BaseAccessServer {
202
244
  content: [
203
245
  {
204
246
  type: "text",
205
- text: JSON.stringify({
206
- ...summary,
207
- affected_resources: summary.affected_resources,
208
- }, null, 2),
247
+ text: JSON.stringify(summary, null, 2),
209
248
  },
210
249
  ],
211
250
  };
@@ -226,92 +265,211 @@ export class SystemStatusServer extends BaseAccessServer {
226
265
  const dateB = new Date(b.OutageStartDateTime || b.CreationTime);
227
266
  return dateA.getTime() - dateB.getTime();
228
267
  });
268
+ // Initialize tracking variables
269
+ const affectedResources = new Set();
270
+ let upcoming24h = 0;
271
+ let upcomingWeek = 0;
272
+ const enhancedMaintenance = maintenance.map((item) => {
273
+ // Track affected resources
274
+ item.AffectedResources?.forEach((resource) => {
275
+ affectedResources.add(resource.ResourceName);
276
+ });
277
+ // Check timing - only use OutageStartDateTime for scheduling, fallback shows warning
278
+ const hasScheduledTime = !!item.OutageStartDateTime;
279
+ const startTime = new Date(item.OutageStartDateTime || item.CreationTime);
280
+ const now = new Date();
281
+ const hoursUntil = (startTime.getTime() - now.getTime()) / (1000 * 60 * 60);
282
+ if (hoursUntil <= 24)
283
+ upcoming24h++;
284
+ if (hoursUntil <= 168)
285
+ upcomingWeek++; // 7 days * 24 hours
286
+ return {
287
+ ...item,
288
+ scheduled_start: item.OutageStartDateTime,
289
+ scheduled_end: item.OutageEndDateTime,
290
+ hours_until_start: Math.max(0, Math.round(hoursUntil)),
291
+ duration_hours: item.OutageEndDateTime && item.OutageStartDateTime
292
+ ? Math.round((new Date(item.OutageEndDateTime).getTime() -
293
+ new Date(item.OutageStartDateTime).getTime()) /
294
+ (1000 * 60 * 60))
295
+ : null,
296
+ has_scheduled_time: hasScheduledTime,
297
+ };
298
+ });
229
299
  const summary = {
230
300
  total_scheduled: maintenance.length,
231
- upcoming_24h: 0,
232
- upcoming_week: 0,
233
- affected_resources: new Set(),
234
- maintenance: maintenance.map((item) => {
235
- // Track affected resources
236
- item.AffectedResources?.forEach((resource) => {
237
- summary.affected_resources.add(resource.ResourceName);
238
- });
239
- // Check timing
240
- const startTime = new Date(item.OutageStartDateTime || item.CreationTime);
241
- const now = new Date();
242
- const hoursUntil = (startTime.getTime() - now.getTime()) / (1000 * 60 * 60);
243
- if (hoursUntil <= 24)
244
- summary.upcoming_24h++;
245
- if (hoursUntil <= 168)
246
- summary.upcoming_week++; // 7 days * 24 hours
247
- return {
248
- ...item,
249
- scheduled_start: item.OutageStartDateTime,
250
- scheduled_end: item.OutageEndDateTime,
251
- hours_until_start: Math.max(0, Math.round(hoursUntil)),
252
- duration_hours: item.OutageEndDateTime && item.OutageStartDateTime
253
- ? Math.round((new Date(item.OutageEndDateTime).getTime() -
254
- new Date(item.OutageStartDateTime).getTime()) /
255
- (1000 * 60 * 60))
256
- : null,
257
- };
258
- }),
301
+ upcoming_24h: upcoming24h,
302
+ upcoming_week: upcomingWeek,
303
+ affected_resources: Array.from(affectedResources),
304
+ maintenance: enhancedMaintenance,
259
305
  };
260
306
  return {
261
307
  content: [
262
308
  {
263
309
  type: "text",
264
- text: JSON.stringify({
265
- ...summary,
266
- affected_resources: summary.affected_resources,
267
- }, null, 2),
310
+ text: JSON.stringify(summary, null, 2),
311
+ },
312
+ ],
313
+ };
314
+ }
315
+ async getPastOutages(resourceFilter, limit = 100) {
316
+ const response = await this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/past_outages/");
317
+ let pastOutages = response.data.results || [];
318
+ // Filter by resource if specified
319
+ if (resourceFilter) {
320
+ const filter = resourceFilter.toLowerCase();
321
+ pastOutages = pastOutages.filter((outage) => outage.Subject?.toLowerCase().includes(filter) ||
322
+ outage.AffectedResources?.some((resource) => resource.ResourceName?.toLowerCase().includes(filter) ||
323
+ resource.ResourceID?.toString().includes(filter)));
324
+ }
325
+ // Sort by outage end time (most recent first)
326
+ pastOutages.sort((a, b) => {
327
+ const dateA = new Date(a.OutageEndDateTime || a.LastModificationTime);
328
+ const dateB = new Date(b.OutageEndDateTime || b.LastModificationTime);
329
+ return dateB.getTime() - dateA.getTime();
330
+ });
331
+ // Apply limit
332
+ if (limit && pastOutages.length > limit) {
333
+ pastOutages = pastOutages.slice(0, limit);
334
+ }
335
+ // Initialize tracking variables
336
+ const affectedResources = new Set();
337
+ const outageTypes = new Set();
338
+ const recentOutages = pastOutages.filter((outage) => {
339
+ const endTime = new Date(outage.OutageEndDateTime || outage.LastModificationTime);
340
+ const daysAgo = (Date.now() - endTime.getTime()) / (1000 * 60 * 60 * 24);
341
+ return daysAgo <= 30; // Last 30 days
342
+ });
343
+ // Enhance outages with calculated fields
344
+ const enhancedOutages = pastOutages.map((outage) => {
345
+ // Track affected resources
346
+ outage.AffectedResources?.forEach((resource) => {
347
+ affectedResources.add(resource.ResourceName);
348
+ });
349
+ // Track outage types
350
+ if (outage.OutageType) {
351
+ outageTypes.add(outage.OutageType);
352
+ }
353
+ // Calculate duration
354
+ const startTime = new Date(outage.OutageStartDateTime);
355
+ const endTime = new Date(outage.OutageEndDateTime);
356
+ const durationHours = Math.round((endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60));
357
+ // Calculate how long ago it ended
358
+ const daysAgo = Math.round((Date.now() - endTime.getTime()) / (1000 * 60 * 60 * 24));
359
+ return {
360
+ ...outage,
361
+ outage_start: outage.OutageStartDateTime,
362
+ outage_end: outage.OutageEndDateTime,
363
+ duration_hours: durationHours,
364
+ days_ago: daysAgo,
365
+ outage_type: outage.OutageType,
366
+ posted_time: outage.CreationTime,
367
+ last_updated: outage.LastModificationTime,
368
+ };
369
+ });
370
+ const summary = {
371
+ total_past_outages: enhancedOutages.length,
372
+ recent_outages_30_days: recentOutages.length,
373
+ affected_resources: Array.from(affectedResources),
374
+ outage_types: Array.from(outageTypes),
375
+ average_duration_hours: enhancedOutages.length > 0
376
+ ? Math.round(enhancedOutages
377
+ .filter((o) => o.duration_hours > 0)
378
+ .reduce((sum, o) => sum + o.duration_hours, 0) /
379
+ enhancedOutages.filter((o) => o.duration_hours > 0).length)
380
+ : 0,
381
+ outages: enhancedOutages,
382
+ };
383
+ return {
384
+ content: [
385
+ {
386
+ type: "text",
387
+ text: JSON.stringify(summary, null, 2),
268
388
  },
269
389
  ],
270
390
  };
271
391
  }
272
392
  async getSystemAnnouncements(limit = 50) {
273
- // Get both current and future announcements
274
- const [currentResponse, futureResponse] = await Promise.all([
393
+ // Get current, future, and recent past announcements for comprehensive view
394
+ const [currentResponse, futureResponse, pastResponse] = await Promise.all([
275
395
  this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/current_outages/"),
276
396
  this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/future_outages/"),
397
+ this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/past_outages/"),
277
398
  ]);
278
399
  const currentOutages = currentResponse.data.results || [];
279
400
  const futureOutages = futureResponse.data.results || [];
280
- // Combine and sort by creation time
281
- const allAnnouncements = [...currentOutages, ...futureOutages]
401
+ const pastOutages = pastResponse.data.results || [];
402
+ // Filter recent past outages (last 30 days) for announcements
403
+ const recentPastOutages = pastOutages.filter((outage) => {
404
+ const endTime = new Date(outage.OutageEndDateTime || outage.LastModificationTime);
405
+ const daysAgo = (Date.now() - endTime.getTime()) / (1000 * 60 * 60 * 24);
406
+ return daysAgo <= 30;
407
+ });
408
+ // Combine all announcements and sort by most relevant date
409
+ const allAnnouncements = [
410
+ ...currentOutages.map((item) => ({ ...item, category: 'current' })),
411
+ ...futureOutages.map((item) => ({ ...item, category: 'scheduled' })),
412
+ ...recentPastOutages.map((item) => ({ ...item, category: 'recent_past' })),
413
+ ]
282
414
  .sort((a, b) => {
283
- const dateA = new Date(a.CreationTime);
284
- const dateB = new Date(b.CreationTime);
415
+ // Sort by most relevant date: current first, then future by start time, then past by end time
416
+ if (a.category === 'current' && b.category !== 'current')
417
+ return -1;
418
+ if (b.category === 'current' && a.category !== 'current')
419
+ return 1;
420
+ const dateA = new Date(a.OutageStartDateTime || a.CreationTime);
421
+ const dateB = new Date(b.OutageStartDateTime || b.CreationTime);
285
422
  return dateB.getTime() - dateA.getTime(); // Most recent first
286
423
  })
287
424
  .slice(0, limit);
425
+ const summary = {
426
+ total_announcements: allAnnouncements.length,
427
+ current_outages: currentOutages.length,
428
+ scheduled_maintenance: futureOutages.length,
429
+ recent_past_outages: recentPastOutages.length,
430
+ categories: {
431
+ current: allAnnouncements.filter(a => a.category === 'current').length,
432
+ scheduled: allAnnouncements.filter(a => a.category === 'scheduled').length,
433
+ recent_past: allAnnouncements.filter(a => a.category === 'recent_past').length,
434
+ },
435
+ announcements: allAnnouncements,
436
+ };
288
437
  return {
289
438
  content: [
290
439
  {
291
440
  type: "text",
292
- text: JSON.stringify({
293
- total_announcements: allAnnouncements.length,
294
- current_outages: currentOutages.length,
295
- scheduled_maintenance: futureOutages.length,
296
- announcements: allAnnouncements,
297
- }, null, 2),
441
+ text: JSON.stringify(summary, null, 2),
298
442
  },
299
443
  ],
300
444
  };
301
445
  }
302
- async checkResourceStatus(resourceIds) {
303
- // Get current outages to check against resource IDs
304
- const currentOutages = await this.getCurrentOutages();
305
- const outageData = JSON.parse(currentOutages.content[0].text);
446
+ async checkResourceStatus(resourceIds, useGroupApi = false) {
447
+ if (useGroupApi) {
448
+ return await this.checkResourceStatusViaGroups(resourceIds);
449
+ }
450
+ // Efficient approach: fetch raw current outages data once
451
+ const response = await this.httpClient.get("/wh2/news/v1/affiliation/access-ci.org/current_outages/");
452
+ const rawOutages = response.data.results || [];
306
453
  const resourceStatus = resourceIds.map((resourceId) => {
307
- const affectedOutages = outageData.outages.filter((outage) => outage.AffectedResources?.some((resource) => resource.ResourceID?.toString() === resourceId ||
454
+ const affectedOutages = rawOutages.filter((outage) => outage.AffectedResources?.some((resource) => resource.ResourceID?.toString() === resourceId ||
308
455
  resource.ResourceName?.toLowerCase().includes(resourceId.toLowerCase())));
309
456
  let status = "operational";
310
457
  let severity = null;
311
458
  if (affectedOutages.length > 0) {
312
459
  status = "affected";
313
- // Get highest severity
314
- const severities = affectedOutages.map((o) => o.severity);
460
+ // Get highest severity using same logic as getCurrentOutages
461
+ const severities = affectedOutages.map((outage) => {
462
+ const subject = outage.Subject?.toLowerCase() || "";
463
+ if (subject.includes("emergency") || subject.includes("critical")) {
464
+ return "high";
465
+ }
466
+ else if (subject.includes("maintenance") || subject.includes("scheduled")) {
467
+ return "low";
468
+ }
469
+ else {
470
+ return "medium";
471
+ }
472
+ });
315
473
  if (severities.includes("high"))
316
474
  severity = "high";
317
475
  else if (severities.includes("medium"))
@@ -326,8 +484,9 @@ export class SystemStatusServer extends BaseAccessServer {
326
484
  active_outages: affectedOutages.length,
327
485
  outage_details: affectedOutages.map((outage) => ({
328
486
  subject: outage.Subject,
329
- severity: outage.severity,
330
- posted: outage.posted_time,
487
+ severity,
488
+ posted: outage.CreationTime,
489
+ last_updated: outage.LastModificationTime,
331
490
  })),
332
491
  };
333
492
  });
@@ -341,6 +500,60 @@ export class SystemStatusServer extends BaseAccessServer {
341
500
  operational: resourceStatus.filter((r) => r.status === "operational").length,
342
501
  affected: resourceStatus.filter((r) => r.status === "affected")
343
502
  .length,
503
+ api_method: "direct_outages_check",
504
+ resource_status: resourceStatus,
505
+ }, null, 2),
506
+ },
507
+ ],
508
+ };
509
+ }
510
+ async checkResourceStatusViaGroups(resourceIds) {
511
+ // Try to use the more efficient group-based API
512
+ const statusPromises = resourceIds.map(async (resourceId) => {
513
+ try {
514
+ const response = await this.httpClient.get(`/wh2/news/v1/info_groupid/${resourceId}/`);
515
+ const groupData = response.data.results || [];
516
+ const hasOutages = groupData.length > 0;
517
+ return {
518
+ resource_id: resourceId,
519
+ status: hasOutages ? "affected" : "operational",
520
+ severity: hasOutages ? "medium" : null,
521
+ active_outages: groupData.length,
522
+ outage_details: groupData.map((outage) => ({
523
+ subject: outage.Subject,
524
+ posted: outage.CreationTime,
525
+ last_updated: outage.LastModificationTime,
526
+ })),
527
+ api_method: "group_specific",
528
+ };
529
+ }
530
+ catch (error) {
531
+ // Fallback to general check if group API fails
532
+ return {
533
+ resource_id: resourceId,
534
+ status: "unknown",
535
+ severity: null,
536
+ active_outages: 0,
537
+ outage_details: [],
538
+ error: `Group API failed for ${resourceId}`,
539
+ api_method: "group_specific_failed",
540
+ };
541
+ }
542
+ });
543
+ const resourceStatus = await Promise.all(statusPromises);
544
+ return {
545
+ content: [
546
+ {
547
+ type: "text",
548
+ text: JSON.stringify({
549
+ checked_at: new Date().toISOString(),
550
+ resources_checked: resourceIds.length,
551
+ operational: resourceStatus.filter((r) => r.status === "operational").length,
552
+ affected: resourceStatus.filter((r) => r.status === "affected")
553
+ .length,
554
+ unknown: resourceStatus.filter((r) => r.status === "unknown")
555
+ .length,
556
+ api_method: "resource_group_api",
344
557
  resource_status: resourceStatus,
345
558
  }, null, 2),
346
559
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@access-mcp/system-status",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "MCP server for ACCESS-CI System Status and Outages API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -13,6 +13,14 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "build": "tsc",
16
+ "start": "node dist/index.js",
17
+ "dev": "npm run build && npm start",
18
+ "test": "vitest run --exclude '**/**.integration.test.ts'",
19
+ "test:integration": "vitest run --config vitest.integration.config.ts",
20
+ "test:all": "vitest run",
21
+ "test:watch": "vitest",
22
+ "test:ui": "vitest --ui",
23
+ "test:coverage": "vitest run --coverage --exclude '**/**.integration.test.ts'",
16
24
  "prepublishOnly": "npm run build"
17
25
  },
18
26
  "keywords": [
@@ -38,5 +46,12 @@
38
46
  "dependencies": {
39
47
  "@access-mcp/shared": "^0.3.0",
40
48
  "express": "^4.18.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^22.0.0",
52
+ "@vitest/ui": "^2.1.9",
53
+ "c8": "^10.1.3",
54
+ "typescript": "^5.0.0",
55
+ "vitest": "^2.1.9"
41
56
  }
42
57
  }