@access-mcp/announcements 0.1.0 → 0.2.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 +46 -121
- package/dist/index.js +2 -17
- package/dist/server.d.ts +28 -85
- package/dist/server.js +70 -101
- package/package.json +3 -3
- package/src/index.ts +2 -18
- package/src/server.integration.test.ts +63 -72
- package/src/server.test.ts +69 -81
- package/src/server.ts +56 -217
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@access-mcp/announcements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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": "
|
|
29
|
+
"@access-mcp/shared": "^0.3.3",
|
|
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
|
-
|
|
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) => {
|
|
@@ -16,7 +16,7 @@ describe("AnnouncementsServer Integration Tests", () => {
|
|
|
16
16
|
it("should fetch real announcements from API", async () => {
|
|
17
17
|
const result = await server["handleToolCall"]({
|
|
18
18
|
params: {
|
|
19
|
-
name: "
|
|
19
|
+
name: "search_announcements",
|
|
20
20
|
arguments: {
|
|
21
21
|
limit: 5,
|
|
22
22
|
},
|
|
@@ -27,21 +27,18 @@ describe("AnnouncementsServer Integration Tests", () => {
|
|
|
27
27
|
const responseData = JSON.parse(result.content[0].text);
|
|
28
28
|
|
|
29
29
|
// Check structure
|
|
30
|
-
expect(responseData).toHaveProperty("
|
|
31
|
-
expect(responseData).toHaveProperty("
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// Announcements should be an array
|
|
36
|
-
expect(Array.isArray(responseData.announcements)).toBe(true);
|
|
30
|
+
expect(responseData).toHaveProperty("total");
|
|
31
|
+
expect(responseData).toHaveProperty("items");
|
|
32
|
+
|
|
33
|
+
// Items should be an array
|
|
34
|
+
expect(Array.isArray(responseData.items)).toBe(true);
|
|
37
35
|
|
|
38
36
|
// If there are announcements, check their structure
|
|
39
|
-
if (responseData.
|
|
40
|
-
const firstAnnouncement = responseData.
|
|
37
|
+
if (responseData.items.length > 0) {
|
|
38
|
+
const firstAnnouncement = responseData.items[0];
|
|
41
39
|
expect(firstAnnouncement).toHaveProperty("title");
|
|
42
40
|
expect(firstAnnouncement).toHaveProperty("body");
|
|
43
|
-
expect(firstAnnouncement).toHaveProperty("
|
|
44
|
-
expect(firstAnnouncement).toHaveProperty("formatted_date");
|
|
41
|
+
expect(firstAnnouncement).toHaveProperty("published_date");
|
|
45
42
|
expect(firstAnnouncement).toHaveProperty("tags");
|
|
46
43
|
expect(Array.isArray(firstAnnouncement.tags)).toBe(true);
|
|
47
44
|
}
|
|
@@ -50,7 +47,7 @@ describe("AnnouncementsServer Integration Tests", () => {
|
|
|
50
47
|
it("should filter by tags", async () => {
|
|
51
48
|
const result = await server["handleToolCall"]({
|
|
52
49
|
params: {
|
|
53
|
-
name: "
|
|
50
|
+
name: "search_announcements",
|
|
54
51
|
arguments: {
|
|
55
52
|
tags: "maintenance",
|
|
56
53
|
limit: 3,
|
|
@@ -60,14 +57,14 @@ describe("AnnouncementsServer Integration Tests", () => {
|
|
|
60
57
|
|
|
61
58
|
expect(result.isError).toBeFalsy();
|
|
62
59
|
const responseData = JSON.parse(result.content[0].text);
|
|
63
|
-
|
|
64
|
-
expect(responseData).toHaveProperty("
|
|
65
|
-
expect(Array.isArray(responseData.
|
|
66
|
-
|
|
60
|
+
|
|
61
|
+
expect(responseData).toHaveProperty("items");
|
|
62
|
+
expect(Array.isArray(responseData.items)).toBe(true);
|
|
63
|
+
|
|
67
64
|
// If maintenance announcements exist, they should contain the tag
|
|
68
|
-
if (responseData.
|
|
69
|
-
const hasMaintenanceTag = responseData.
|
|
70
|
-
ann.tags && ann.tags.some((tag: string) =>
|
|
65
|
+
if (responseData.items.length > 0) {
|
|
66
|
+
const hasMaintenanceTag = responseData.items.some((ann: any) =>
|
|
67
|
+
ann.tags && ann.tags.some((tag: string) =>
|
|
71
68
|
tag.toLowerCase().includes("maintenance")
|
|
72
69
|
)
|
|
73
70
|
);
|
|
@@ -79,10 +76,9 @@ describe("AnnouncementsServer Integration Tests", () => {
|
|
|
79
76
|
it("should handle date range filters", async () => {
|
|
80
77
|
const result = await server["handleToolCall"]({
|
|
81
78
|
params: {
|
|
82
|
-
name: "
|
|
79
|
+
name: "search_announcements",
|
|
83
80
|
arguments: {
|
|
84
|
-
|
|
85
|
-
relative_end_date: "today",
|
|
81
|
+
date: "this_month",
|
|
86
82
|
limit: 10,
|
|
87
83
|
},
|
|
88
84
|
},
|
|
@@ -90,17 +86,16 @@ describe("AnnouncementsServer Integration Tests", () => {
|
|
|
90
86
|
|
|
91
87
|
expect(result.isError).toBeFalsy();
|
|
92
88
|
const responseData = JSON.parse(result.content[0].text);
|
|
93
|
-
|
|
94
|
-
expect(responseData).toHaveProperty("
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
|
|
90
|
+
expect(responseData).toHaveProperty("items");
|
|
91
|
+
|
|
97
92
|
// All announcements should be within the last month
|
|
98
|
-
if (responseData.
|
|
93
|
+
if (responseData.items.length > 0) {
|
|
99
94
|
const oneMonthAgo = new Date();
|
|
100
95
|
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
|
|
101
|
-
|
|
102
|
-
responseData.
|
|
103
|
-
const annDate = new Date(ann.
|
|
96
|
+
|
|
97
|
+
responseData.items.forEach((ann: any) => {
|
|
98
|
+
const annDate = new Date(ann.published_date);
|
|
104
99
|
expect(annDate.getTime()).toBeGreaterThanOrEqual(oneMonthAgo.getTime());
|
|
105
100
|
});
|
|
106
101
|
}
|
|
@@ -111,26 +106,25 @@ describe("AnnouncementsServer Integration Tests", () => {
|
|
|
111
106
|
it("should fetch announcements from the past week", async () => {
|
|
112
107
|
const result = await server["handleToolCall"]({
|
|
113
108
|
params: {
|
|
114
|
-
name: "
|
|
109
|
+
name: "search_announcements",
|
|
115
110
|
arguments: {
|
|
116
|
-
|
|
111
|
+
date: "this_week",
|
|
117
112
|
},
|
|
118
113
|
},
|
|
119
114
|
} as any);
|
|
120
115
|
|
|
121
116
|
expect(result.isError).toBeFalsy();
|
|
122
117
|
const responseData = JSON.parse(result.content[0].text);
|
|
123
|
-
|
|
124
|
-
expect(responseData).toHaveProperty("
|
|
125
|
-
|
|
126
|
-
|
|
118
|
+
|
|
119
|
+
expect(responseData).toHaveProperty("items");
|
|
120
|
+
|
|
127
121
|
// Check that announcements are from the past week if any exist
|
|
128
|
-
if (responseData.
|
|
122
|
+
if (responseData.items.length > 0) {
|
|
129
123
|
const oneWeekAgo = new Date();
|
|
130
124
|
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
|
131
|
-
|
|
132
|
-
responseData.
|
|
133
|
-
const annDate = new Date(ann.
|
|
125
|
+
|
|
126
|
+
responseData.items.forEach((ann: any) => {
|
|
127
|
+
const annDate = new Date(ann.published_date);
|
|
134
128
|
expect(annDate.getTime()).toBeGreaterThanOrEqual(oneWeekAgo.getTime());
|
|
135
129
|
});
|
|
136
130
|
}
|
|
@@ -139,32 +133,29 @@ describe("AnnouncementsServer Integration Tests", () => {
|
|
|
139
133
|
it("should handle today filter", async () => {
|
|
140
134
|
const result = await server["handleToolCall"]({
|
|
141
135
|
params: {
|
|
142
|
-
name: "
|
|
136
|
+
name: "search_announcements",
|
|
143
137
|
arguments: {
|
|
144
|
-
|
|
138
|
+
date: "today",
|
|
145
139
|
},
|
|
146
140
|
},
|
|
147
141
|
} as any);
|
|
148
142
|
|
|
149
143
|
expect(result.isError).toBeFalsy();
|
|
150
144
|
const responseData = JSON.parse(result.content[0].text);
|
|
151
|
-
|
|
152
|
-
expect(responseData).toHaveProperty("
|
|
153
|
-
|
|
154
|
-
|
|
145
|
+
|
|
146
|
+
expect(responseData).toHaveProperty("items");
|
|
147
|
+
|
|
155
148
|
// Today's announcements might be empty, that's okay
|
|
156
|
-
expect(Array.isArray(responseData.
|
|
149
|
+
expect(Array.isArray(responseData.items)).toBe(true);
|
|
157
150
|
}, 10000);
|
|
158
151
|
});
|
|
159
152
|
|
|
160
|
-
describe("
|
|
161
|
-
it("should
|
|
162
|
-
// We don't know specific affinity group IDs, so test with a made-up one
|
|
153
|
+
describe("get_announcements with limit", () => {
|
|
154
|
+
it("should respect limit parameter", async () => {
|
|
163
155
|
const result = await server["handleToolCall"]({
|
|
164
156
|
params: {
|
|
165
|
-
name: "
|
|
157
|
+
name: "search_announcements",
|
|
166
158
|
arguments: {
|
|
167
|
-
ag: "test-group-123",
|
|
168
159
|
limit: 5,
|
|
169
160
|
},
|
|
170
161
|
},
|
|
@@ -172,49 +163,49 @@ describe("AnnouncementsServer Integration Tests", () => {
|
|
|
172
163
|
|
|
173
164
|
expect(result.isError).toBeFalsy();
|
|
174
165
|
const responseData = JSON.parse(result.content[0].text);
|
|
175
|
-
|
|
176
|
-
// Should return a valid response
|
|
177
|
-
expect(responseData).toHaveProperty("
|
|
178
|
-
expect(Array.isArray(responseData.
|
|
179
|
-
|
|
180
|
-
|
|
166
|
+
|
|
167
|
+
// Should return a valid response
|
|
168
|
+
expect(responseData).toHaveProperty("items");
|
|
169
|
+
expect(Array.isArray(responseData.items)).toBe(true);
|
|
170
|
+
if (responseData.items.length > 0) {
|
|
171
|
+
expect(responseData.items.length).toBeLessThanOrEqual(5);
|
|
172
|
+
}
|
|
181
173
|
}, 10000);
|
|
182
174
|
});
|
|
183
175
|
|
|
184
176
|
describe("API Error Handling", () => {
|
|
185
|
-
it("should handle
|
|
177
|
+
it("should handle search with no parameters", async () => {
|
|
186
178
|
const result = await server["handleToolCall"]({
|
|
187
179
|
params: {
|
|
188
|
-
name: "
|
|
189
|
-
arguments: {
|
|
190
|
-
beginning_date: "invalid-date",
|
|
191
|
-
},
|
|
180
|
+
name: "search_announcements",
|
|
181
|
+
arguments: {},
|
|
192
182
|
},
|
|
193
183
|
} as any);
|
|
194
184
|
|
|
195
|
-
//
|
|
196
|
-
// Just verify we get a response
|
|
185
|
+
// Should return default results
|
|
197
186
|
expect(result).toBeDefined();
|
|
198
187
|
expect(result.content).toBeDefined();
|
|
188
|
+
const responseData = JSON.parse(result.content[0].text);
|
|
189
|
+
expect(responseData).toHaveProperty("items");
|
|
199
190
|
}, 10000);
|
|
200
191
|
|
|
201
192
|
it("should handle empty results", async () => {
|
|
202
193
|
const result = await server["handleToolCall"]({
|
|
203
194
|
params: {
|
|
204
|
-
name: "
|
|
195
|
+
name: "search_announcements",
|
|
205
196
|
arguments: {
|
|
206
197
|
tags: "nonexistent-tag-xyz-123-456",
|
|
207
|
-
exact_match: true,
|
|
208
198
|
},
|
|
209
199
|
},
|
|
210
200
|
} as any);
|
|
211
201
|
|
|
212
202
|
expect(result.isError).toBeFalsy();
|
|
213
203
|
const responseData = JSON.parse(result.content[0].text);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
expect(responseData
|
|
217
|
-
expect(responseData
|
|
204
|
+
|
|
205
|
+
// May have 0 or few results
|
|
206
|
+
expect(responseData).toHaveProperty("total");
|
|
207
|
+
expect(responseData).toHaveProperty("items");
|
|
208
|
+
expect(Array.isArray(responseData.items)).toBe(true);
|
|
218
209
|
}, 10000);
|
|
219
210
|
});
|
|
220
211
|
});
|