@access-mcp/announcements 0.3.0 → 0.3.1

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.3.0",
3
+ "version": "0.3.1",
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": "^0.5.0",
29
+ "@access-mcp/shared": "*",
30
30
  "@modelcontextprotocol/sdk": "^0.5.0",
31
31
  "axios": "^1.7.0"
32
32
  },
package/src/index.ts CHANGED
@@ -12,4 +12,4 @@ main().catch((error) => {
12
12
  // Log errors to stderr and exit
13
13
  console.error("Server error:", error);
14
14
  process.exit(1);
15
- });
15
+ });
@@ -1,5 +1,6 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
1
+ import { describe, it, expect, beforeEach, afterAll } from "vitest";
2
2
  import { AnnouncementsServer } from "./server.js";
3
+ import { requestContextStorage, RequestContext } from "@access-mcp/shared";
3
4
 
4
5
  interface TextContent {
5
6
  type: "text";
@@ -13,6 +14,17 @@ interface AnnouncementItem {
13
14
  tags: string[];
14
15
  }
15
16
 
17
+ interface MyAnnouncementItem {
18
+ uuid: string;
19
+ nid: number;
20
+ title: string;
21
+ status: string;
22
+ created: string;
23
+ published_date: string;
24
+ summary: string;
25
+ edit_url: string;
26
+ }
27
+
16
28
  /**
17
29
  * Integration tests for AnnouncementsServer
18
30
  * These tests make actual API calls to the ACCESS Support API
@@ -38,14 +50,14 @@ describe("AnnouncementsServer Integration Tests", () => {
38
50
 
39
51
  expect(result.isError).toBeFalsy();
40
52
  const responseData = JSON.parse((result.content[0] as TextContent).text);
41
-
53
+
42
54
  // Check structure
43
55
  expect(responseData).toHaveProperty("total");
44
56
  expect(responseData).toHaveProperty("items");
45
57
 
46
58
  // Items should be an array
47
59
  expect(Array.isArray(responseData.items)).toBe(true);
48
-
60
+
49
61
  // If there are announcements, check their structure
50
62
  if (responseData.items.length > 0) {
51
63
  const firstAnnouncement = responseData.items[0];
@@ -77,10 +89,9 @@ describe("AnnouncementsServer Integration Tests", () => {
77
89
 
78
90
  // If maintenance announcements exist, they should contain the tag
79
91
  if (responseData.items.length > 0) {
80
- const hasMaintenanceTag = responseData.items.some((ann: AnnouncementItem) =>
81
- ann.tags && ann.tags.some((tag: string) =>
82
- tag.toLowerCase().includes("maintenance")
83
- )
92
+ const hasMaintenanceTag = responseData.items.some(
93
+ (ann: AnnouncementItem) =>
94
+ ann.tags && ann.tags.some((tag: string) => tag.toLowerCase().includes("maintenance"))
84
95
  );
85
96
  // This might not always be true if the API doesn't have maintenance announcements
86
97
  console.log("Found maintenance announcements:", hasMaintenanceTag);
@@ -249,4 +260,281 @@ describe("AnnouncementsServer Integration Tests", () => {
249
260
  expect(Array.isArray(responseData.items)).toBe(true);
250
261
  }, 10000);
251
262
  });
252
- });
263
+ });
264
+
265
+ /**
266
+ * E2E tests for authenticated CRUD operations.
267
+ *
268
+ * Requires env vars: DRUPAL_API_URL, DRUPAL_USERNAME, DRUPAL_PASSWORD, ACTING_USER
269
+ * Tests run against a live Drupal instance (e.g., accessmatch.ddev.site).
270
+ * Skipped if DRUPAL_USERNAME is not set.
271
+ */
272
+ const hasDrupalCreds = !!process.env.DRUPAL_USERNAME && !!process.env.DRUPAL_PASSWORD;
273
+
274
+ describe.skipIf(!hasDrupalCreds)("Authenticated CRUD E2E Tests", () => {
275
+ let server: AnnouncementsServer;
276
+ const createdUuids: string[] = [];
277
+
278
+ beforeEach(() => {
279
+ server = new AnnouncementsServer();
280
+ });
281
+
282
+ // Clean up any announcements created during tests
283
+ afterAll(async () => {
284
+ if (createdUuids.length === 0) return;
285
+ const cleanupServer = new AnnouncementsServer();
286
+ for (const uuid of createdUuids) {
287
+ try {
288
+ await cleanupServer["handleToolCall"]({
289
+ method: "tools/call",
290
+ params: {
291
+ name: "delete_announcement",
292
+ arguments: { uuid, confirmed: true },
293
+ },
294
+ });
295
+ } catch {
296
+ // Best effort cleanup
297
+ }
298
+ }
299
+ });
300
+
301
+ describe("get_announcement_context", () => {
302
+ it("should return tags and coordinator status via views endpoint", async () => {
303
+ const result = await server["handleToolCall"]({
304
+ method: "tools/call",
305
+ params: {
306
+ name: "get_announcement_context",
307
+ arguments: {},
308
+ },
309
+ });
310
+
311
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
312
+
313
+ // Should have tags array
314
+ expect(responseData).toHaveProperty("tags");
315
+ expect(Array.isArray(responseData.tags)).toBe(true);
316
+ if (responseData.tags.length > 0) {
317
+ expect(responseData.tags[0]).toHaveProperty("name");
318
+ expect(responseData.tags[0]).toHaveProperty("uuid");
319
+ }
320
+
321
+ // Should have affinity groups (may be empty if user is not a coordinator)
322
+ expect(responseData).toHaveProperty("affinity_groups");
323
+ expect(Array.isArray(responseData.affinity_groups)).toBe(true);
324
+
325
+ // Should have is_coordinator boolean
326
+ expect(responseData).toHaveProperty("is_coordinator");
327
+ expect(typeof responseData.is_coordinator).toBe("boolean");
328
+ expect(responseData.is_coordinator).toBe(responseData.affinity_groups.length > 0);
329
+
330
+ // Should have static options
331
+ expect(responseData.affiliations).toContain("ACCESS Collaboration");
332
+ expect(responseData.affiliations).toContain("Community");
333
+ expect(responseData.where_to_share_options).toHaveLength(4);
334
+ }, 15000);
335
+
336
+ it("should work with request context acting user", async () => {
337
+ const savedActingUser = process.env.ACTING_USER;
338
+ delete process.env.ACTING_USER;
339
+
340
+ try {
341
+ const context: RequestContext = {
342
+ actingUser: savedActingUser || "apasquale@access-ci.org",
343
+ };
344
+
345
+ const result = await requestContextStorage.run(context, async () => {
346
+ return server["handleToolCall"]({
347
+ method: "tools/call",
348
+ params: {
349
+ name: "get_announcement_context",
350
+ arguments: {},
351
+ },
352
+ });
353
+ });
354
+
355
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
356
+ expect(responseData).toHaveProperty("tags");
357
+ expect(responseData).toHaveProperty("is_coordinator");
358
+ } finally {
359
+ if (savedActingUser) process.env.ACTING_USER = savedActingUser;
360
+ }
361
+ }, 15000);
362
+ });
363
+
364
+ describe("create, get_my, update, delete announcement", () => {
365
+ it("should create an announcement and find it via get_my_announcements", async () => {
366
+ // 1. Create announcement
367
+ const createResult = await server["handleToolCall"]({
368
+ method: "tools/call",
369
+ params: {
370
+ name: "create_announcement",
371
+ arguments: {
372
+ title: "E2E Test Announcement",
373
+ body: "<p>This is an automated e2e test announcement.</p>",
374
+ summary: "E2E test summary",
375
+ },
376
+ },
377
+ });
378
+
379
+ const createData = JSON.parse((createResult.content[0] as TextContent).text);
380
+ expect(createData.success).toBe(true);
381
+ expect(createData.uuid).toBeTruthy();
382
+ expect(createData.title).toBe("E2E Test Announcement");
383
+ expect(createData.edit_url).toContain("/node/");
384
+ expect(createData.edit_url).toContain("/edit");
385
+
386
+ createdUuids.push(createData.uuid);
387
+
388
+ // 2. Verify it appears in get_my_announcements
389
+ const myResult = await server["handleToolCall"]({
390
+ method: "tools/call",
391
+ params: {
392
+ name: "get_my_announcements",
393
+ arguments: { limit: 50 },
394
+ },
395
+ });
396
+
397
+ const myData = JSON.parse((myResult.content[0] as TextContent).text);
398
+ expect(myData.total).toBeGreaterThanOrEqual(1);
399
+
400
+ const found = myData.items.find(
401
+ (item: MyAnnouncementItem) => item.uuid === createData.uuid
402
+ );
403
+ expect(found).toBeTruthy();
404
+ expect(found.title).toBe("E2E Test Announcement");
405
+ expect(found.status).toBe("draft");
406
+ expect(found.summary).toBe("E2E test summary");
407
+ expect(found.uuid).toBe(createData.uuid);
408
+ expect(found.edit_url).toContain("/edit");
409
+ }, 30000);
410
+
411
+ it("should update an existing announcement", async () => {
412
+ // Create one to update
413
+ const createResult = await server["handleToolCall"]({
414
+ method: "tools/call",
415
+ params: {
416
+ name: "create_announcement",
417
+ arguments: {
418
+ title: "E2E Update Test",
419
+ body: "<p>Original body</p>",
420
+ summary: "Original summary",
421
+ },
422
+ },
423
+ });
424
+
425
+ const createData = JSON.parse((createResult.content[0] as TextContent).text);
426
+ expect(createData.success).toBe(true);
427
+ createdUuids.push(createData.uuid);
428
+
429
+ // Update it
430
+ const updateResult = await server["handleToolCall"]({
431
+ method: "tools/call",
432
+ params: {
433
+ name: "update_announcement",
434
+ arguments: {
435
+ uuid: createData.uuid,
436
+ title: "E2E Update Test - Modified",
437
+ },
438
+ },
439
+ });
440
+
441
+ const updateData = JSON.parse((updateResult.content[0] as TextContent).text);
442
+ expect(updateData.success).toBe(true);
443
+ expect(updateData.title).toBe("E2E Update Test - Modified");
444
+ }, 30000);
445
+
446
+ it("should delete an announcement", async () => {
447
+ // Create one to delete
448
+ const createResult = await server["handleToolCall"]({
449
+ method: "tools/call",
450
+ params: {
451
+ name: "create_announcement",
452
+ arguments: {
453
+ title: "E2E Delete Test",
454
+ body: "<p>To be deleted</p>",
455
+ summary: "Will be deleted",
456
+ },
457
+ },
458
+ });
459
+
460
+ const createData = JSON.parse((createResult.content[0] as TextContent).text);
461
+ expect(createData.success).toBe(true);
462
+ const uuid = createData.uuid;
463
+
464
+ // Delete it
465
+ const deleteResult = await server["handleToolCall"]({
466
+ method: "tools/call",
467
+ params: {
468
+ name: "delete_announcement",
469
+ arguments: { uuid, confirmed: true },
470
+ },
471
+ });
472
+
473
+ const deleteData = JSON.parse((deleteResult.content[0] as TextContent).text);
474
+ expect(deleteData.success).toBe(true);
475
+ expect(deleteData.uuid).toBe(uuid);
476
+
477
+ // Remove from cleanup list since we already deleted it
478
+ const idx = createdUuids.indexOf(uuid);
479
+ if (idx !== -1) createdUuids.splice(idx, 1);
480
+
481
+ // Verify it's gone from get_my_announcements
482
+ const myResult = await server["handleToolCall"]({
483
+ method: "tools/call",
484
+ params: {
485
+ name: "get_my_announcements",
486
+ arguments: {},
487
+ },
488
+ });
489
+
490
+ const myData = JSON.parse((myResult.content[0] as TextContent).text);
491
+ const found = myData.items.find(
492
+ (item: MyAnnouncementItem) => item.uuid === uuid
493
+ );
494
+ expect(found).toBeUndefined();
495
+ }, 30000);
496
+
497
+ it("should reject delete without confirmation", async () => {
498
+ const result = await server["handleToolCall"]({
499
+ method: "tools/call",
500
+ params: {
501
+ name: "delete_announcement",
502
+ arguments: {
503
+ uuid: "some-uuid",
504
+ confirmed: false,
505
+ },
506
+ },
507
+ });
508
+
509
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
510
+ expect(responseData.error).toContain("explicit confirmation");
511
+ }, 10000);
512
+ });
513
+
514
+ describe("get_my_announcements", () => {
515
+ it("should return proper structure from views endpoint", async () => {
516
+ const result = await server["handleToolCall"]({
517
+ method: "tools/call",
518
+ params: {
519
+ name: "get_my_announcements",
520
+ arguments: {},
521
+ },
522
+ });
523
+
524
+ const responseData = JSON.parse((result.content[0] as TextContent).text);
525
+ expect(responseData).toHaveProperty("total");
526
+ expect(responseData).toHaveProperty("items");
527
+ expect(Array.isArray(responseData.items)).toBe(true);
528
+
529
+ // If there are items, check structure
530
+ if (responseData.items.length > 0) {
531
+ const item = responseData.items[0];
532
+ expect(item).toHaveProperty("uuid");
533
+ expect(item).toHaveProperty("title");
534
+ expect(item).toHaveProperty("status");
535
+ expect(["draft", "published"]).toContain(item.status);
536
+ expect(item).toHaveProperty("edit_url");
537
+ }
538
+ }, 15000);
539
+ });
540
+ });