@access-mcp/announcements 0.1.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@access-mcp/announcements",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for ACCESS Support Announcements API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,7 +26,7 @@
26
26
  "author": "ACCESS-CI",
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
- "@access-mcp/shared": "file:../shared",
29
+ "@access-mcp/shared": "^0.5.0",
30
30
  "@modelcontextprotocol/sdk": "^0.5.0",
31
31
  "axios": "^1.7.0"
32
32
  },
@@ -35,4 +35,4 @@
35
35
  "typescript": "^5.0.0",
36
36
  "vitest": "^1.0.0"
37
37
  }
38
- }
38
+ }
package/src/index.ts CHANGED
@@ -3,25 +3,9 @@
3
3
  import { AnnouncementsServer } from "./server.js";
4
4
 
5
5
  async function main() {
6
- // Check if we should run as HTTP server (for deployment)
7
- const port = process.env.PORT;
8
-
9
6
  const server = new AnnouncementsServer();
10
-
11
- if (port) {
12
- // Running in HTTP mode (deployment)
13
- await server.start({ httpPort: parseInt(port) });
14
- // Keep the process running in HTTP mode
15
- process.on('SIGINT', () => {
16
- console.log('Shutting down server...');
17
- process.exit(0);
18
- });
19
- // Keep the event loop alive
20
- setInterval(() => {}, 1000 * 60 * 60); // Heartbeat every hour
21
- } else {
22
- // Running in MCP mode (stdio)
23
- await server.start();
24
- }
7
+ const port = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
8
+ await server.start(port ? { httpPort: port } : undefined);
25
9
  }
26
10
 
27
11
  main().catch((error) => {
@@ -1,6 +1,18 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
2
  import { AnnouncementsServer } from "./server.js";
3
3
 
4
+ interface TextContent {
5
+ type: "text";
6
+ text: string;
7
+ }
8
+
9
+ interface AnnouncementItem {
10
+ title: string;
11
+ body: string;
12
+ published_date: string;
13
+ tags: string[];
14
+ }
15
+
4
16
  /**
5
17
  * Integration tests for AnnouncementsServer
6
18
  * These tests make actual API calls to the ACCESS Support API
@@ -15,33 +27,31 @@ describe("AnnouncementsServer Integration Tests", () => {
15
27
  describe("get_announcements", () => {
16
28
  it("should fetch real announcements from API", async () => {
17
29
  const result = await server["handleToolCall"]({
30
+ method: "tools/call",
18
31
  params: {
19
- name: "get_announcements",
32
+ name: "search_announcements",
20
33
  arguments: {
21
34
  limit: 5,
22
35
  },
23
36
  },
24
- } as any);
37
+ });
25
38
 
26
39
  expect(result.isError).toBeFalsy();
27
- const responseData = JSON.parse(result.content[0].text);
40
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
28
41
 
29
42
  // Check structure
30
- expect(responseData).toHaveProperty("total_announcements");
31
- expect(responseData).toHaveProperty("announcements");
32
- expect(responseData).toHaveProperty("popular_tags");
33
- expect(responseData).toHaveProperty("filters_applied");
34
-
35
- // Announcements should be an array
36
- expect(Array.isArray(responseData.announcements)).toBe(true);
43
+ expect(responseData).toHaveProperty("total");
44
+ expect(responseData).toHaveProperty("items");
45
+
46
+ // Items should be an array
47
+ expect(Array.isArray(responseData.items)).toBe(true);
37
48
 
38
49
  // If there are announcements, check their structure
39
- if (responseData.announcements.length > 0) {
40
- const firstAnnouncement = responseData.announcements[0];
50
+ if (responseData.items.length > 0) {
51
+ const firstAnnouncement = responseData.items[0];
41
52
  expect(firstAnnouncement).toHaveProperty("title");
42
53
  expect(firstAnnouncement).toHaveProperty("body");
43
- expect(firstAnnouncement).toHaveProperty("date");
44
- expect(firstAnnouncement).toHaveProperty("formatted_date");
54
+ expect(firstAnnouncement).toHaveProperty("published_date");
45
55
  expect(firstAnnouncement).toHaveProperty("tags");
46
56
  expect(Array.isArray(firstAnnouncement.tags)).toBe(true);
47
57
  }
@@ -49,25 +59,26 @@ describe("AnnouncementsServer Integration Tests", () => {
49
59
 
50
60
  it("should filter by tags", async () => {
51
61
  const result = await server["handleToolCall"]({
62
+ method: "tools/call",
52
63
  params: {
53
- name: "get_announcements_by_tags",
64
+ name: "search_announcements",
54
65
  arguments: {
55
66
  tags: "maintenance",
56
67
  limit: 3,
57
68
  },
58
69
  },
59
- } as any);
70
+ });
60
71
 
61
72
  expect(result.isError).toBeFalsy();
62
- const responseData = JSON.parse(result.content[0].text);
63
-
64
- expect(responseData).toHaveProperty("announcements");
65
- expect(Array.isArray(responseData.announcements)).toBe(true);
66
-
73
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
74
+
75
+ expect(responseData).toHaveProperty("items");
76
+ expect(Array.isArray(responseData.items)).toBe(true);
77
+
67
78
  // If maintenance announcements exist, they should contain the tag
68
- if (responseData.announcements.length > 0) {
69
- const hasMaintenanceTag = responseData.announcements.some((ann: any) =>
70
- ann.tags && ann.tags.some((tag: string) =>
79
+ if (responseData.items.length > 0) {
80
+ const hasMaintenanceTag = responseData.items.some((ann: AnnouncementItem) =>
81
+ ann.tags && ann.tags.some((tag: string) =>
71
82
  tag.toLowerCase().includes("maintenance")
72
83
  )
73
84
  );
@@ -78,29 +89,28 @@ describe("AnnouncementsServer Integration Tests", () => {
78
89
 
79
90
  it("should handle date range filters", async () => {
80
91
  const result = await server["handleToolCall"]({
92
+ method: "tools/call",
81
93
  params: {
82
- name: "get_announcements",
94
+ name: "search_announcements",
83
95
  arguments: {
84
- relative_start_date: "-1month",
85
- relative_end_date: "today",
96
+ date: "this_month",
86
97
  limit: 10,
87
98
  },
88
99
  },
89
- } as any);
100
+ });
90
101
 
91
102
  expect(result.isError).toBeFalsy();
92
- const responseData = JSON.parse(result.content[0].text);
93
-
94
- expect(responseData).toHaveProperty("announcements");
95
- expect(responseData.filters_applied).toHaveProperty("date_range");
96
-
103
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
104
+
105
+ expect(responseData).toHaveProperty("items");
106
+
97
107
  // All announcements should be within the last month
98
- if (responseData.announcements.length > 0) {
108
+ if (responseData.items.length > 0) {
99
109
  const oneMonthAgo = new Date();
100
110
  oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
101
-
102
- responseData.announcements.forEach((ann: any) => {
103
- const annDate = new Date(ann.date);
111
+
112
+ responseData.items.forEach((ann: AnnouncementItem) => {
113
+ const annDate = new Date(ann.published_date);
104
114
  expect(annDate.getTime()).toBeGreaterThanOrEqual(oneMonthAgo.getTime());
105
115
  });
106
116
  }
@@ -110,27 +120,27 @@ describe("AnnouncementsServer Integration Tests", () => {
110
120
  describe("get_recent_announcements", () => {
111
121
  it("should fetch announcements from the past week", async () => {
112
122
  const result = await server["handleToolCall"]({
123
+ method: "tools/call",
113
124
  params: {
114
- name: "get_recent_announcements",
125
+ name: "search_announcements",
115
126
  arguments: {
116
- period: "1 week",
127
+ date: "this_week",
117
128
  },
118
129
  },
119
- } as any);
130
+ });
120
131
 
121
132
  expect(result.isError).toBeFalsy();
122
- const responseData = JSON.parse(result.content[0].text);
123
-
124
- expect(responseData).toHaveProperty("announcements");
125
- expect(responseData.filters_applied.date_range).toContain("week");
126
-
133
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
134
+
135
+ expect(responseData).toHaveProperty("items");
136
+
127
137
  // Check that announcements are from the past week if any exist
128
- if (responseData.announcements.length > 0) {
138
+ if (responseData.items.length > 0) {
129
139
  const oneWeekAgo = new Date();
130
140
  oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
131
-
132
- responseData.announcements.forEach((ann: any) => {
133
- const annDate = new Date(ann.date);
141
+
142
+ responseData.items.forEach((ann: AnnouncementItem) => {
143
+ const annDate = new Date(ann.published_date);
134
144
  expect(annDate.getTime()).toBeGreaterThanOrEqual(oneWeekAgo.getTime());
135
145
  });
136
146
  }
@@ -138,83 +148,105 @@ describe("AnnouncementsServer Integration Tests", () => {
138
148
 
139
149
  it("should handle today filter", async () => {
140
150
  const result = await server["handleToolCall"]({
151
+ method: "tools/call",
141
152
  params: {
142
- name: "get_recent_announcements",
153
+ name: "search_announcements",
143
154
  arguments: {
144
- period: "0 days",
155
+ date: "today",
145
156
  },
146
157
  },
147
- } as any);
158
+ });
148
159
 
149
160
  expect(result.isError).toBeFalsy();
150
- const responseData = JSON.parse(result.content[0].text);
151
-
152
- expect(responseData).toHaveProperty("announcements");
153
- expect(responseData.filters_applied.date_range).toContain("0 days");
154
-
161
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
162
+
163
+ expect(responseData).toHaveProperty("items");
164
+
155
165
  // Today's announcements might be empty, that's okay
156
- expect(Array.isArray(responseData.announcements)).toBe(true);
166
+ expect(Array.isArray(responseData.items)).toBe(true);
157
167
  }, 10000);
158
168
  });
159
169
 
160
- describe("get_announcements_by_affinity_group", () => {
161
- it("should handle affinity group filtering", async () => {
162
- // We don't know specific affinity group IDs, so test with a made-up one
170
+ describe("get_announcements with limit", () => {
171
+ it("should respect limit parameter", async () => {
163
172
  const result = await server["handleToolCall"]({
173
+ method: "tools/call",
164
174
  params: {
165
- name: "get_announcements_by_affinity_group",
175
+ name: "search_announcements",
166
176
  arguments: {
167
- ag: "test-group-123",
168
177
  limit: 5,
169
178
  },
170
179
  },
171
- } as any);
180
+ });
172
181
 
173
182
  expect(result.isError).toBeFalsy();
174
- const responseData = JSON.parse(result.content[0].text);
175
-
176
- // Should return a valid response even if no announcements match
177
- expect(responseData).toHaveProperty("announcements");
178
- expect(Array.isArray(responseData.announcements)).toBe(true);
179
- expect(responseData.filters_applied).toHaveProperty("affinity_group");
180
- expect(responseData.filters_applied.affinity_group).toBe("test-group-123");
183
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
184
+
185
+ // Should return a valid response
186
+ expect(responseData).toHaveProperty("items");
187
+ expect(Array.isArray(responseData.items)).toBe(true);
188
+ if (responseData.items.length > 0) {
189
+ expect(responseData.items.length).toBeLessThanOrEqual(5);
190
+ }
181
191
  }, 10000);
182
192
  });
183
193
 
184
- describe("API Error Handling", () => {
185
- it("should handle invalid parameters gracefully", async () => {
194
+ describe("search with query parameter", () => {
195
+ it("should perform full-text search", async () => {
186
196
  const result = await server["handleToolCall"]({
197
+ method: "tools/call",
187
198
  params: {
188
- name: "get_announcements",
199
+ name: "search_announcements",
189
200
  arguments: {
190
- beginning_date: "invalid-date",
201
+ query: "ACCESS",
202
+ limit: 5,
191
203
  },
192
204
  },
193
- } as any);
205
+ });
194
206
 
195
- // The API might accept this or reject it
196
- // Just verify we get a response
207
+ expect(result.isError).toBeFalsy();
208
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
209
+
210
+ expect(responseData).toHaveProperty("items");
211
+ expect(Array.isArray(responseData.items)).toBe(true);
212
+ }, 10000);
213
+ });
214
+
215
+ describe("API Error Handling", () => {
216
+ it("should handle search with no parameters", async () => {
217
+ const result = await server["handleToolCall"]({
218
+ method: "tools/call",
219
+ params: {
220
+ name: "search_announcements",
221
+ arguments: {},
222
+ },
223
+ });
224
+
225
+ // Should return default results
197
226
  expect(result).toBeDefined();
198
227
  expect(result.content).toBeDefined();
228
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
229
+ expect(responseData).toHaveProperty("items");
199
230
  }, 10000);
200
231
 
201
232
  it("should handle empty results", async () => {
202
233
  const result = await server["handleToolCall"]({
234
+ method: "tools/call",
203
235
  params: {
204
- name: "get_announcements",
236
+ name: "search_announcements",
205
237
  arguments: {
206
238
  tags: "nonexistent-tag-xyz-123-456",
207
- exact_match: true,
208
239
  },
209
240
  },
210
- } as any);
241
+ });
211
242
 
212
243
  expect(result.isError).toBeFalsy();
213
- const responseData = JSON.parse(result.content[0].text);
214
-
215
- expect(responseData.total_announcements).toBe(0);
216
- expect(responseData.announcements).toEqual([]);
217
- expect(responseData.message).toContain("No announcements found");
244
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
245
+
246
+ // May have 0 or few results
247
+ expect(responseData).toHaveProperty("total");
248
+ expect(responseData).toHaveProperty("items");
249
+ expect(Array.isArray(responseData.items)).toBe(true);
218
250
  }, 10000);
219
251
  });
220
252
  });