@mpowr/nexus-mcp 0.5.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.
Files changed (119) hide show
  1. package/README.md +59 -0
  2. package/dist/auth.d.ts +39 -0
  3. package/dist/auth.d.ts.map +1 -0
  4. package/dist/auth.js +47 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/nexus-api.d.ts +29 -0
  7. package/dist/nexus-api.d.ts.map +1 -0
  8. package/dist/nexus-api.js +76 -0
  9. package/dist/nexus-api.js.map +1 -0
  10. package/dist/server.d.ts +65 -0
  11. package/dist/server.d.ts.map +1 -0
  12. package/dist/server.js +183 -0
  13. package/dist/server.js.map +1 -0
  14. package/dist/tools/add-task-note.d.ts +34 -0
  15. package/dist/tools/add-task-note.d.ts.map +1 -0
  16. package/dist/tools/add-task-note.js +39 -0
  17. package/dist/tools/add-task-note.js.map +1 -0
  18. package/dist/tools/append-session-entry.d.ts +53 -0
  19. package/dist/tools/append-session-entry.d.ts.map +1 -0
  20. package/dist/tools/append-session-entry.js +67 -0
  21. package/dist/tools/append-session-entry.js.map +1 -0
  22. package/dist/tools/create-task.d.ts +52 -0
  23. package/dist/tools/create-task.d.ts.map +1 -0
  24. package/dist/tools/create-task.js +51 -0
  25. package/dist/tools/create-task.js.map +1 -0
  26. package/dist/tools/decision-comments.d.ts +54 -0
  27. package/dist/tools/decision-comments.d.ts.map +1 -0
  28. package/dist/tools/decision-comments.js +80 -0
  29. package/dist/tools/decision-comments.js.map +1 -0
  30. package/dist/tools/get-document.d.ts +47 -0
  31. package/dist/tools/get-document.d.ts.map +1 -0
  32. package/dist/tools/get-document.js +68 -0
  33. package/dist/tools/get-document.js.map +1 -0
  34. package/dist/tools/get-project-memory.d.ts +47 -0
  35. package/dist/tools/get-project-memory.d.ts.map +1 -0
  36. package/dist/tools/get-project-memory.js +53 -0
  37. package/dist/tools/get-project-memory.js.map +1 -0
  38. package/dist/tools/get-related-entities.d.ts +44 -0
  39. package/dist/tools/get-related-entities.d.ts.map +1 -0
  40. package/dist/tools/get-related-entities.js +60 -0
  41. package/dist/tools/get-related-entities.js.map +1 -0
  42. package/dist/tools/governance.d.ts +90 -0
  43. package/dist/tools/governance.d.ts.map +1 -0
  44. package/dist/tools/governance.js +124 -0
  45. package/dist/tools/governance.js.map +1 -0
  46. package/dist/tools/ingest-document.d.ts +40 -0
  47. package/dist/tools/ingest-document.d.ts.map +1 -0
  48. package/dist/tools/ingest-document.js +48 -0
  49. package/dist/tools/ingest-document.js.map +1 -0
  50. package/dist/tools/letter-inbox.d.ts +80 -0
  51. package/dist/tools/letter-inbox.d.ts.map +1 -0
  52. package/dist/tools/letter-inbox.js +118 -0
  53. package/dist/tools/letter-inbox.js.map +1 -0
  54. package/dist/tools/letters.d.ts +91 -0
  55. package/dist/tools/letters.d.ts.map +1 -0
  56. package/dist/tools/letters.js +112 -0
  57. package/dist/tools/letters.js.map +1 -0
  58. package/dist/tools/project-list.d.ts +28 -0
  59. package/dist/tools/project-list.d.ts.map +1 -0
  60. package/dist/tools/project-list.js +43 -0
  61. package/dist/tools/project-list.js.map +1 -0
  62. package/dist/tools/reviews.d.ts +145 -0
  63. package/dist/tools/reviews.d.ts.map +1 -0
  64. package/dist/tools/reviews.js +216 -0
  65. package/dist/tools/reviews.js.map +1 -0
  66. package/dist/tools/search-knowledge.d.ts +48 -0
  67. package/dist/tools/search-knowledge.d.ts.map +1 -0
  68. package/dist/tools/search-knowledge.js +54 -0
  69. package/dist/tools/search-knowledge.js.map +1 -0
  70. package/dist/tools/sessions.d.ts +81 -0
  71. package/dist/tools/sessions.d.ts.map +1 -0
  72. package/dist/tools/sessions.js +120 -0
  73. package/dist/tools/sessions.js.map +1 -0
  74. package/dist/tools/skill-assign.d.ts +77 -0
  75. package/dist/tools/skill-assign.d.ts.map +1 -0
  76. package/dist/tools/skill-assign.js +108 -0
  77. package/dist/tools/skill-assign.js.map +1 -0
  78. package/dist/tools/skills.d.ts +138 -0
  79. package/dist/tools/skills.d.ts.map +1 -0
  80. package/dist/tools/skills.js +192 -0
  81. package/dist/tools/skills.js.map +1 -0
  82. package/dist/tools/update-task-status.d.ts +48 -0
  83. package/dist/tools/update-task-status.d.ts.map +1 -0
  84. package/dist/tools/update-task-status.js +51 -0
  85. package/dist/tools/update-task-status.js.map +1 -0
  86. package/package.json +30 -0
  87. package/src/__tests__/auth.test.ts +162 -0
  88. package/src/__tests__/decision-comments.test.ts +173 -0
  89. package/src/__tests__/helpers.ts +58 -0
  90. package/src/__tests__/layer1-knowledge.test.ts +302 -0
  91. package/src/__tests__/layer2-coordination.test.ts +775 -0
  92. package/src/__tests__/layer3-governance.test.ts +205 -0
  93. package/src/__tests__/project-list-and-skill-assign.test.ts +282 -0
  94. package/src/__tests__/reviews.test.ts +420 -0
  95. package/src/__tests__/server.test.ts +238 -0
  96. package/src/__tests__/setup.ts +15 -0
  97. package/src/auth.ts +81 -0
  98. package/src/nexus-api.ts +110 -0
  99. package/src/server.ts +499 -0
  100. package/src/tools/add-task-note.ts +50 -0
  101. package/src/tools/append-session-entry.ts +83 -0
  102. package/src/tools/create-task.ts +66 -0
  103. package/src/tools/decision-comments.ts +102 -0
  104. package/src/tools/get-document.ts +80 -0
  105. package/src/tools/get-project-memory.ts +65 -0
  106. package/src/tools/get-related-entities.ts +73 -0
  107. package/src/tools/governance.ts +162 -0
  108. package/src/tools/ingest-document.ts +64 -0
  109. package/src/tools/letter-inbox.ts +157 -0
  110. package/src/tools/letters.ts +144 -0
  111. package/src/tools/project-list.ts +52 -0
  112. package/src/tools/reviews.ts +277 -0
  113. package/src/tools/search-knowledge.ts +68 -0
  114. package/src/tools/sessions.ts +154 -0
  115. package/src/tools/skill-assign.ts +142 -0
  116. package/src/tools/skills.ts +252 -0
  117. package/src/tools/update-task-status.ts +64 -0
  118. package/tsconfig.json +20 -0
  119. package/vitest.config.ts +8 -0
@@ -0,0 +1,420 @@
1
+ /**
2
+ * Tests for review lifecycle tools:
3
+ * - rv_list
4
+ * - rv_get
5
+ * - rv_create
6
+ * - rv_decide
7
+ * - rv_comment
8
+ *
9
+ * All tools delegate to the Nexus API via nexusPost().
10
+ */
11
+
12
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
13
+ import { mockApiError, mockApiSuccess, parseToolResponse, TEST_IDS } from './helpers'
14
+
15
+ vi.mock('../nexus-api.js', () => ({
16
+ nexusPost: vi.fn(),
17
+ }))
18
+
19
+ import { nexusPost } from '../nexus-api.js'
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // rv_list
23
+ // ---------------------------------------------------------------------------
24
+
25
+ describe('rv_list', () => {
26
+ beforeEach(() => {
27
+ vi.restoreAllMocks()
28
+ })
29
+
30
+ it('should return a list of reviews', async () => {
31
+ vi.mocked(nexusPost).mockResolvedValue(
32
+ mockApiSuccess({
33
+ action: 'rv_list',
34
+ count: 2,
35
+ reviews: [
36
+ {
37
+ id: TEST_IDS.reviewId,
38
+ entity_type: 'skill',
39
+ entity_id: TEST_IDS.skillId,
40
+ status: 'pending',
41
+ created_at: '2026-04-12T00:00:00Z',
42
+ },
43
+ {
44
+ id: '22222222-1111-1111-1111-111111111111',
45
+ entity_type: 'agent',
46
+ entity_id: TEST_IDS.agentId,
47
+ status: 'accepted',
48
+ created_at: '2026-04-11T00:00:00Z',
49
+ },
50
+ ],
51
+ }),
52
+ )
53
+
54
+ const { rvList } = await import('../tools/reviews.js')
55
+ const result = await rvList({ user_id: TEST_IDS.userId })
56
+
57
+ expect(result.isError).toBeUndefined()
58
+ const parsed = parseToolResponse(result)
59
+ expect(parsed.action).toBe('rv_list')
60
+ expect(parsed.count).toBe(2)
61
+ expect(parsed.reviews).toHaveLength(2)
62
+ expect(parsed.reviews[0].id).toBe(TEST_IDS.reviewId)
63
+ })
64
+
65
+ it('should filter by entity_type', async () => {
66
+ vi.mocked(nexusPost).mockResolvedValue(
67
+ mockApiSuccess({
68
+ action: 'rv_list',
69
+ count: 1,
70
+ reviews: [
71
+ {
72
+ id: TEST_IDS.reviewId,
73
+ entity_type: 'skill',
74
+ entity_id: TEST_IDS.skillId,
75
+ status: 'pending',
76
+ },
77
+ ],
78
+ }),
79
+ )
80
+
81
+ const { rvList } = await import('../tools/reviews.js')
82
+ const result = await rvList({
83
+ entity_type: 'skill',
84
+ user_id: TEST_IDS.userId,
85
+ })
86
+
87
+ expect(result.isError).toBeUndefined()
88
+ const parsed = parseToolResponse(result)
89
+ expect(parsed.count).toBe(1)
90
+ expect(parsed.reviews[0].entity_type).toBe('skill')
91
+
92
+ expect(nexusPost).toHaveBeenCalledWith('/api/mcp/reviews', {
93
+ action: 'rv_list',
94
+ entity_type: 'skill',
95
+ status: undefined,
96
+ limit: undefined,
97
+ })
98
+ })
99
+
100
+ it('should return error on API failure', async () => {
101
+ vi.mocked(nexusPost).mockResolvedValue(mockApiError('Internal server error', 500))
102
+
103
+ const { rvList } = await import('../tools/reviews.js')
104
+ const result = await rvList({ user_id: TEST_IDS.userId })
105
+
106
+ expect(result.isError).toBe(true)
107
+ })
108
+ })
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // rv_get
112
+ // ---------------------------------------------------------------------------
113
+
114
+ describe('rv_get', () => {
115
+ beforeEach(() => {
116
+ vi.restoreAllMocks()
117
+ })
118
+
119
+ it('should get a review by review_id', async () => {
120
+ vi.mocked(nexusPost).mockResolvedValue(
121
+ mockApiSuccess({
122
+ action: 'rv_get',
123
+ review: {
124
+ id: TEST_IDS.reviewId,
125
+ entity_type: 'skill',
126
+ entity_id: TEST_IDS.skillId,
127
+ status: 'pending',
128
+ created_at: '2026-04-12T00:00:00Z',
129
+ comments: [],
130
+ },
131
+ }),
132
+ )
133
+
134
+ const { rvGet } = await import('../tools/reviews.js')
135
+ const result = await rvGet({
136
+ review_id: TEST_IDS.reviewId,
137
+ user_id: TEST_IDS.userId,
138
+ })
139
+
140
+ expect(result.isError).toBeUndefined()
141
+ const parsed = parseToolResponse(result)
142
+ expect(parsed.action).toBe('rv_get')
143
+ expect(parsed.review.id).toBe(TEST_IDS.reviewId)
144
+ expect(parsed.review.entity_type).toBe('skill')
145
+ })
146
+
147
+ it('should get a review by entity_type and entity_id', async () => {
148
+ vi.mocked(nexusPost).mockResolvedValue(
149
+ mockApiSuccess({
150
+ action: 'rv_get',
151
+ review: {
152
+ id: TEST_IDS.reviewId,
153
+ entity_type: 'skill',
154
+ entity_id: TEST_IDS.skillId,
155
+ status: 'pending',
156
+ },
157
+ }),
158
+ )
159
+
160
+ const { rvGet } = await import('../tools/reviews.js')
161
+ const result = await rvGet({
162
+ entity_type: 'skill',
163
+ entity_id: TEST_IDS.skillId,
164
+ user_id: TEST_IDS.userId,
165
+ })
166
+
167
+ expect(result.isError).toBeUndefined()
168
+ const parsed = parseToolResponse(result)
169
+ expect(parsed.review.entity_type).toBe('skill')
170
+ expect(parsed.review.entity_id).toBe(TEST_IDS.skillId)
171
+
172
+ expect(nexusPost).toHaveBeenCalledWith('/api/mcp/reviews', {
173
+ action: 'rv_get',
174
+ review_id: undefined,
175
+ entity_type: 'skill',
176
+ entity_id: TEST_IDS.skillId,
177
+ })
178
+ })
179
+
180
+ it('should return error on API failure', async () => {
181
+ vi.mocked(nexusPost).mockResolvedValue(mockApiError('Review not found', 404))
182
+
183
+ const { rvGet } = await import('../tools/reviews.js')
184
+ const result = await rvGet({
185
+ review_id: TEST_IDS.reviewId,
186
+ user_id: TEST_IDS.userId,
187
+ })
188
+
189
+ expect(result.isError).toBe(true)
190
+ })
191
+ })
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // rv_create
195
+ // ---------------------------------------------------------------------------
196
+
197
+ describe('rv_create', () => {
198
+ beforeEach(() => {
199
+ vi.restoreAllMocks()
200
+ })
201
+
202
+ it('should create a review for an entity', async () => {
203
+ vi.mocked(nexusPost).mockResolvedValue(
204
+ mockApiSuccess({
205
+ action: 'rv_create',
206
+ review_id: TEST_IDS.reviewId,
207
+ entity_type: 'skill',
208
+ entity_id: TEST_IDS.skillId,
209
+ status: 'pending',
210
+ }),
211
+ )
212
+
213
+ const { rvCreate } = await import('../tools/reviews.js')
214
+ const result = await rvCreate({
215
+ entity_type: 'skill',
216
+ entity_id: TEST_IDS.skillId,
217
+ user_id: TEST_IDS.userId,
218
+ })
219
+
220
+ expect(result.isError).toBeUndefined()
221
+ const parsed = parseToolResponse(result)
222
+ expect(parsed.action).toBe('rv_create')
223
+ expect(parsed.review_id).toBe(TEST_IDS.reviewId)
224
+ expect(parsed.entity_type).toBe('skill')
225
+ expect(parsed.status).toBe('pending')
226
+ })
227
+
228
+ it('should return error on duplicate review (409)', async () => {
229
+ vi.mocked(nexusPost).mockResolvedValue(
230
+ mockApiError('Review already exists for this entity', 409),
231
+ )
232
+
233
+ const { rvCreate } = await import('../tools/reviews.js')
234
+ const result = await rvCreate({
235
+ entity_type: 'skill',
236
+ entity_id: TEST_IDS.skillId,
237
+ user_id: TEST_IDS.userId,
238
+ })
239
+
240
+ expect(result.isError).toBe(true)
241
+ const parsed = parseToolResponse(result)
242
+ expect(parsed.error).toBe('Review already exists for this entity')
243
+ })
244
+
245
+ it('should return error on API failure', async () => {
246
+ vi.mocked(nexusPost).mockResolvedValue(mockApiError('Entity not found', 404))
247
+
248
+ const { rvCreate } = await import('../tools/reviews.js')
249
+ const result = await rvCreate({
250
+ entity_type: 'skill',
251
+ entity_id: TEST_IDS.skillId,
252
+ user_id: TEST_IDS.userId,
253
+ })
254
+
255
+ expect(result.isError).toBe(true)
256
+ })
257
+ })
258
+
259
+ // ---------------------------------------------------------------------------
260
+ // rv_decide
261
+ // ---------------------------------------------------------------------------
262
+
263
+ describe('rv_decide', () => {
264
+ beforeEach(() => {
265
+ vi.restoreAllMocks()
266
+ })
267
+
268
+ it('should accept a review', async () => {
269
+ vi.mocked(nexusPost).mockResolvedValue(
270
+ mockApiSuccess({
271
+ action: 'rv_decide',
272
+ review_id: TEST_IDS.reviewId,
273
+ transition: 'accept',
274
+ previous_status: 'pending',
275
+ new_status: 'accepted',
276
+ }),
277
+ )
278
+
279
+ const { rvDecide } = await import('../tools/reviews.js')
280
+ const result = await rvDecide({
281
+ review_id: TEST_IDS.reviewId,
282
+ transition: 'accept',
283
+ user_id: TEST_IDS.userId,
284
+ })
285
+
286
+ expect(result.isError).toBeUndefined()
287
+ const parsed = parseToolResponse(result)
288
+ expect(parsed.action).toBe('rv_decide')
289
+ expect(parsed.transition).toBe('accept')
290
+ expect(parsed.new_status).toBe('accepted')
291
+ })
292
+
293
+ it('should reject a review with rationale', async () => {
294
+ vi.mocked(nexusPost).mockResolvedValue(
295
+ mockApiSuccess({
296
+ action: 'rv_decide',
297
+ review_id: TEST_IDS.reviewId,
298
+ transition: 'reject',
299
+ previous_status: 'pending',
300
+ new_status: 'rejected',
301
+ }),
302
+ )
303
+
304
+ const { rvDecide } = await import('../tools/reviews.js')
305
+ const result = await rvDecide({
306
+ review_id: TEST_IDS.reviewId,
307
+ transition: 'reject',
308
+ rationale: 'Does not meet quality standards',
309
+ user_id: TEST_IDS.userId,
310
+ })
311
+
312
+ expect(result.isError).toBeUndefined()
313
+ const parsed = parseToolResponse(result)
314
+ expect(parsed.transition).toBe('reject')
315
+ expect(parsed.new_status).toBe('rejected')
316
+
317
+ expect(nexusPost).toHaveBeenCalledWith('/api/mcp/reviews', {
318
+ action: 'rv_decide',
319
+ review_id: TEST_IDS.reviewId,
320
+ transition: 'reject',
321
+ rationale: 'Does not meet quality standards',
322
+ })
323
+ })
324
+
325
+ it('should return error on API failure', async () => {
326
+ vi.mocked(nexusPost).mockResolvedValue(
327
+ mockApiError('Review must be in pending state', 409),
328
+ )
329
+
330
+ const { rvDecide } = await import('../tools/reviews.js')
331
+ const result = await rvDecide({
332
+ review_id: TEST_IDS.reviewId,
333
+ transition: 'accept',
334
+ user_id: TEST_IDS.userId,
335
+ })
336
+
337
+ expect(result.isError).toBe(true)
338
+ })
339
+ })
340
+
341
+ // ---------------------------------------------------------------------------
342
+ // rv_comment
343
+ // ---------------------------------------------------------------------------
344
+
345
+ describe('rv_comment', () => {
346
+ beforeEach(() => {
347
+ vi.restoreAllMocks()
348
+ })
349
+
350
+ it('should add a comment to a review', async () => {
351
+ vi.mocked(nexusPost).mockResolvedValue(
352
+ mockApiSuccess({
353
+ action: 'rv_comment',
354
+ comment_id: 'comment-rv-1',
355
+ review_id: TEST_IDS.reviewId,
356
+ }),
357
+ )
358
+
359
+ const { rvComment } = await import('../tools/reviews.js')
360
+ const result = await rvComment({
361
+ review_id: TEST_IDS.reviewId,
362
+ body: 'Looks good overall, minor nit on line 42.',
363
+ user_id: TEST_IDS.userId,
364
+ })
365
+
366
+ expect(result.isError).toBeUndefined()
367
+ const parsed = parseToolResponse(result)
368
+ expect(parsed.action).toBe('rv_comment')
369
+ expect(parsed.comment_id).toBe('comment-rv-1')
370
+ expect(parsed.review_id).toBe(TEST_IDS.reviewId)
371
+ })
372
+
373
+ it('should support line range for inline comments', async () => {
374
+ vi.mocked(nexusPost).mockResolvedValue(
375
+ mockApiSuccess({
376
+ action: 'rv_comment',
377
+ comment_id: 'comment-rv-2',
378
+ review_id: TEST_IDS.reviewId,
379
+ line_start: 10,
380
+ line_end: 15,
381
+ }),
382
+ )
383
+
384
+ const { rvComment } = await import('../tools/reviews.js')
385
+ const result = await rvComment({
386
+ review_id: TEST_IDS.reviewId,
387
+ body: 'This block could be simplified.',
388
+ line_start: 10,
389
+ line_end: 15,
390
+ user_id: TEST_IDS.userId,
391
+ })
392
+
393
+ expect(result.isError).toBeUndefined()
394
+ const parsed = parseToolResponse(result)
395
+ expect(parsed.line_start).toBe(10)
396
+ expect(parsed.line_end).toBe(15)
397
+
398
+ expect(nexusPost).toHaveBeenCalledWith('/api/mcp/reviews', {
399
+ action: 'rv_comment',
400
+ review_id: TEST_IDS.reviewId,
401
+ body: 'This block could be simplified.',
402
+ agent_id: undefined,
403
+ line_start: 10,
404
+ line_end: 15,
405
+ })
406
+ })
407
+
408
+ it('should return error on API failure', async () => {
409
+ vi.mocked(nexusPost).mockResolvedValue(mockApiError('Review not found', 404))
410
+
411
+ const { rvComment } = await import('../tools/reviews.js')
412
+ const result = await rvComment({
413
+ review_id: TEST_IDS.reviewId,
414
+ body: 'Comment on missing review',
415
+ user_id: TEST_IDS.userId,
416
+ })
417
+
418
+ expect(result.isError).toBe(true)
419
+ })
420
+ })
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Tests for MCP server.ts integration:
3
+ * - withIdentity wrapper
4
+ * - Tool registration count (regression guard)
5
+ * - Schema export verification
6
+ */
7
+
8
+ import { readFileSync } from 'node:fs'
9
+ import { resolve } from 'node:path'
10
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
11
+
12
+ // We test the withIdentity pattern and schema exports without
13
+ // starting the full server (which needs stdio transport).
14
+
15
+ describe('MCP Server: withIdentity wrapper', () => {
16
+ beforeEach(() => {
17
+ vi.resetModules()
18
+ vi.restoreAllMocks()
19
+ })
20
+
21
+ it('should inject user_id from identity into handler args', async () => {
22
+ // Recreate the withIdentity function inline (same logic as server.ts)
23
+ // with a simple mock identity
24
+ const mockIdentity = {
25
+ userId: 'injected-user-id',
26
+ email: 'test@example.com',
27
+ displayName: 'Test',
28
+ isPlatformAdmin: true,
29
+ }
30
+
31
+ function withIdentity(
32
+ handler: (args: Record<string, unknown>) => Promise<unknown>,
33
+ ) {
34
+ return async (args: Record<string, unknown>) => {
35
+ return handler({ ...args, user_id: mockIdentity.userId })
36
+ }
37
+ }
38
+
39
+ const mockHandler = vi.fn().mockResolvedValue({ content: [] })
40
+ const wrapped = withIdentity(mockHandler)
41
+
42
+ await wrapped({ project_id: 'test-project', title: 'Test' })
43
+
44
+ expect(mockHandler).toHaveBeenCalledWith({
45
+ project_id: 'test-project',
46
+ title: 'Test',
47
+ user_id: 'injected-user-id',
48
+ })
49
+ })
50
+
51
+ it('should override any user_id passed by the caller', async () => {
52
+ const mockIdentity = {
53
+ userId: 'real-user-id',
54
+ email: 'real@example.com',
55
+ displayName: 'Real User',
56
+ isPlatformAdmin: false,
57
+ }
58
+
59
+ function withIdentity(
60
+ handler: (args: Record<string, unknown>) => Promise<unknown>,
61
+ ) {
62
+ return async (args: Record<string, unknown>) => {
63
+ return handler({ ...args, user_id: mockIdentity.userId })
64
+ }
65
+ }
66
+
67
+ const mockHandler = vi.fn().mockResolvedValue({ content: [] })
68
+ const wrapped = withIdentity(mockHandler)
69
+
70
+ // Caller tries to inject a different user_id
71
+ await wrapped({ project_id: 'test', user_id: 'attacker-user-id' })
72
+
73
+ // The real identity should override the attacker's
74
+ expect(mockHandler).toHaveBeenCalledWith(
75
+ expect.objectContaining({ user_id: 'real-user-id' }),
76
+ )
77
+ })
78
+ })
79
+
80
+ describe('MCP Server: Schema exports', () => {
81
+ const EXPECTED_TOOL_COUNT = 36
82
+
83
+ it('should register exactly 36 tools in server.ts', () => {
84
+ // Static analysis: count server.tool( calls in server.ts as a regression guard.
85
+ // If you add or remove a tool, update EXPECTED_TOOL_COUNT above.
86
+ const serverSource = readFileSync(
87
+ resolve(__dirname, '../server.ts'),
88
+ 'utf-8',
89
+ )
90
+ const matches = serverSource.match(/server\.tool\(/g) || []
91
+ expect(matches.length).toBe(EXPECTED_TOOL_COUNT)
92
+ })
93
+
94
+ it('should export valid schemas from all tool modules', async () => {
95
+ // Verify each tool module exports the expected schema + handler
96
+
97
+ const searchKnowledge = await import('../tools/search-knowledge.js')
98
+ expect(searchKnowledge.searchKnowledgeSchema).toBeDefined()
99
+ expect(searchKnowledge.searchKnowledgeSchema.query).toBeDefined()
100
+ expect(searchKnowledge.searchKnowledgeSchema.project_id).toBeDefined()
101
+ expect(typeof searchKnowledge.searchKnowledge).toBe('function')
102
+
103
+ const getProjectMemory = await import('../tools/get-project-memory.js')
104
+ expect(getProjectMemory.getProjectMemorySchema).toBeDefined()
105
+ expect(typeof getProjectMemory.getProjectMemory).toBe('function')
106
+
107
+ const getDocument = await import('../tools/get-document.js')
108
+ expect(getDocument.getDocumentSchema).toBeDefined()
109
+ expect(typeof getDocument.getDocument).toBe('function')
110
+
111
+ const getRelated = await import('../tools/get-related-entities.js')
112
+ expect(getRelated.getRelatedEntitiesSchema).toBeDefined()
113
+ expect(typeof getRelated.getRelatedEntities).toBe('function')
114
+
115
+ const createTask = await import('../tools/create-task.js')
116
+ expect(createTask.createTaskSchema).toBeDefined()
117
+ expect(createTask.createTaskSchema.project_id).toBeDefined()
118
+ expect(createTask.createTaskSchema.title).toBeDefined()
119
+ expect(typeof createTask.createTask).toBe('function')
120
+
121
+ const updateTaskStatus = await import('../tools/update-task-status.js')
122
+ expect(updateTaskStatus.updateTaskStatusSchema).toBeDefined()
123
+ expect(updateTaskStatus.updateTaskStatusSchema.task_id).toBeDefined()
124
+ expect(updateTaskStatus.updateTaskStatusSchema.status).toBeDefined()
125
+ expect(typeof updateTaskStatus.updateTaskStatus).toBe('function')
126
+
127
+ const addTaskNote = await import('../tools/add-task-note.js')
128
+ expect(addTaskNote.addTaskNoteSchema).toBeDefined()
129
+ expect(addTaskNote.addTaskNoteSchema.task_id).toBeDefined()
130
+ expect(addTaskNote.addTaskNoteSchema.note).toBeDefined()
131
+ expect(typeof addTaskNote.addTaskNote).toBe('function')
132
+
133
+ const ingestDoc = await import('../tools/ingest-document.js')
134
+ expect(ingestDoc.ingestDocumentSchema).toBeDefined()
135
+ expect(ingestDoc.ingestDocumentSchema.project_id).toBeDefined()
136
+ expect(ingestDoc.ingestDocumentSchema.body).toBeDefined()
137
+ expect(typeof ingestDoc.ingestDocument).toBe('function')
138
+
139
+ const sessions = await import('../tools/sessions.js')
140
+ expect(sessions.createSessionSchema).toBeDefined()
141
+ expect(sessions.closeSessionSchema).toBeDefined()
142
+ expect(sessions.listOpenSessionsSchema).toBeDefined()
143
+ expect(typeof sessions.createSession).toBe('function')
144
+ expect(typeof sessions.closeSession).toBe('function')
145
+ expect(typeof sessions.listOpenSessions).toBe('function')
146
+
147
+ const appendEntry = await import('../tools/append-session-entry.js')
148
+ expect(appendEntry.appendSessionEntrySchema).toBeDefined()
149
+ expect(typeof appendEntry.appendSessionEntry).toBe('function')
150
+
151
+ const letters = await import('../tools/letters.js')
152
+ expect(letters.createLetterSchema).toBeDefined()
153
+ expect(letters.replyLetterSchema).toBeDefined()
154
+ expect(typeof letters.createLetter).toBe('function')
155
+ expect(typeof letters.replyLetter).toBe('function')
156
+
157
+ const letterInbox = await import('../tools/letter-inbox.js')
158
+ expect(letterInbox.listInboxSchema).toBeDefined()
159
+ expect(letterInbox.listInboxSchema.project_id).toBeDefined()
160
+ expect(letterInbox.listOutboxSchema).toBeDefined()
161
+ expect(letterInbox.listOutboxSchema.project_id).toBeDefined()
162
+ expect(letterInbox.acknowledgeLetterSchema).toBeDefined()
163
+ expect(letterInbox.acknowledgeLetterSchema.letter_id).toBeDefined()
164
+ expect(typeof letterInbox.listInbox).toBe('function')
165
+ expect(typeof letterInbox.listOutbox).toBe('function')
166
+ expect(typeof letterInbox.acknowledgeLetter).toBe('function')
167
+
168
+ const comments = await import('../tools/decision-comments.js')
169
+ expect(comments.addDecisionCommentSchema).toBeDefined()
170
+ expect(comments.listDecisionCommentsSchema).toBeDefined()
171
+ expect(typeof comments.addDecisionComment).toBe('function')
172
+ expect(typeof comments.listDecisionComments).toBe('function')
173
+
174
+ const governance = await import('../tools/governance.js')
175
+ expect(governance.createAdrDraftSchema).toBeDefined()
176
+ expect(governance.submitAdrReviewSchema).toBeDefined()
177
+ expect(governance.recordAdrDecisionSchema).toBeDefined()
178
+ expect(typeof governance.createAdrDraft).toBe('function')
179
+ expect(typeof governance.submitAdrReview).toBe('function')
180
+ expect(typeof governance.recordAdrDecision).toBe('function')
181
+
182
+ const skills = await import('../tools/skills.js')
183
+ expect(skills.skListSchema).toBeDefined()
184
+ expect(skills.skGetSchema).toBeDefined()
185
+ expect(skills.skGetSchema.skill_id).toBeDefined()
186
+ expect(skills.skCreateSchema).toBeDefined()
187
+ expect(skills.skCreateSchema.skill_id).toBeDefined()
188
+ expect(skills.skCreateSchema.name).toBeDefined()
189
+ expect(skills.skCreateSchema.body).toBeDefined()
190
+ expect(skills.skUpdateSchema).toBeDefined()
191
+ expect(skills.skUpdateSchema.skill_id).toBeDefined()
192
+ expect(skills.skActivateSchema).toBeDefined()
193
+ expect(skills.skActivateSchema.skill_id).toBeDefined()
194
+ expect(skills.skActivateSchema.status).toBeDefined()
195
+ expect(typeof skills.skList).toBe('function')
196
+ expect(typeof skills.skGet).toBe('function')
197
+ expect(typeof skills.skCreate).toBe('function')
198
+ expect(typeof skills.skUpdate).toBe('function')
199
+ expect(typeof skills.skActivate).toBe('function')
200
+
201
+ // project-list module
202
+ const projectList = await import('../tools/project-list.js')
203
+ expect(projectList.projectListSchema).toBeDefined()
204
+ expect(typeof projectList.projectList).toBe('function')
205
+
206
+ // skill-assign module (sk_assign, sk_unassign, sk_export)
207
+ const skillAssign = await import('../tools/skill-assign.js')
208
+ expect(skillAssign.skAssignSchema).toBeDefined()
209
+ expect(skillAssign.skAssignSchema.project_id).toBeDefined()
210
+ expect(skillAssign.skAssignSchema.skill_id).toBeDefined()
211
+ expect(skillAssign.skUnassignSchema).toBeDefined()
212
+ expect(skillAssign.skUnassignSchema.project_id).toBeDefined()
213
+ expect(skillAssign.skExportSchema).toBeDefined()
214
+ expect(skillAssign.skExportSchema.project_id).toBeDefined()
215
+ expect(typeof skillAssign.skAssign).toBe('function')
216
+ expect(typeof skillAssign.skUnassign).toBe('function')
217
+ expect(typeof skillAssign.skExport).toBe('function')
218
+
219
+ // reviews module (rv_list, rv_get, rv_create, rv_decide, rv_comment)
220
+ const reviews = await import('../tools/reviews.js')
221
+ expect(reviews.rvListSchema).toBeDefined()
222
+ expect(reviews.rvGetSchema).toBeDefined()
223
+ expect(reviews.rvCreateSchema).toBeDefined()
224
+ expect(reviews.rvCreateSchema.entity_type).toBeDefined()
225
+ expect(reviews.rvCreateSchema.entity_id).toBeDefined()
226
+ expect(reviews.rvDecideSchema).toBeDefined()
227
+ expect(reviews.rvDecideSchema.review_id).toBeDefined()
228
+ expect(reviews.rvDecideSchema.transition).toBeDefined()
229
+ expect(reviews.rvCommentSchema).toBeDefined()
230
+ expect(reviews.rvCommentSchema.review_id).toBeDefined()
231
+ expect(reviews.rvCommentSchema.body).toBeDefined()
232
+ expect(typeof reviews.rvList).toBe('function')
233
+ expect(typeof reviews.rvGet).toBe('function')
234
+ expect(typeof reviews.rvCreate).toBe('function')
235
+ expect(typeof reviews.rvDecide).toBe('function')
236
+ expect(typeof reviews.rvComment).toBe('function')
237
+ })
238
+ })
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Global test setup for MCP server tests.
3
+ *
4
+ * Sets environment variables needed for the Nexus API client.
5
+ */
6
+
7
+ import { vi } from 'vitest'
8
+
9
+ // Mock environment variables for Nexus API client
10
+ process.env.NEXUS_API_URL = 'https://test.nexus.example.com'
11
+ process.env.NEXUS_PRIVATE_TOKEN =
12
+ 'nxs_pat_test-token-for-unit-tests-1234567890abc'
13
+
14
+ // Suppress console.error from MCP tools during tests
15
+ vi.spyOn(console, 'error').mockImplementation(() => {})