@gwdn/nexus-mcp 0.8.7 → 0.8.8

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 (48) hide show
  1. package/dist/server.js +0 -0
  2. package/package.json +8 -2
  3. package/.github/workflows/ci.yml +0 -38
  4. package/.github/workflows/release.yml +0 -57
  5. package/src/__tests__/auth.test.ts +0 -162
  6. package/src/__tests__/decision-comments.test.ts +0 -173
  7. package/src/__tests__/e2e-integration.test.ts +0 -443
  8. package/src/__tests__/helpers.ts +0 -58
  9. package/src/__tests__/layer1-knowledge.test.ts +0 -302
  10. package/src/__tests__/layer2-coordination.test.ts +0 -918
  11. package/src/__tests__/layer3-governance.test.ts +0 -205
  12. package/src/__tests__/machine-id.test.ts +0 -136
  13. package/src/__tests__/nexus-api.test.ts +0 -127
  14. package/src/__tests__/project-list-and-skill-assign.test.ts +0 -282
  15. package/src/__tests__/reviews.test.ts +0 -420
  16. package/src/__tests__/server.test.ts +0 -251
  17. package/src/__tests__/setup.ts +0 -15
  18. package/src/__tests__/task-list-and-doc-list.test.ts +0 -226
  19. package/src/auth.ts +0 -81
  20. package/src/machine-id.ts +0 -60
  21. package/src/nexus-api.ts +0 -149
  22. package/src/server.ts +0 -629
  23. package/src/tools/add-task-note.ts +0 -50
  24. package/src/tools/append-session-entry.ts +0 -113
  25. package/src/tools/classify-document.ts +0 -55
  26. package/src/tools/create-task.ts +0 -66
  27. package/src/tools/decision-comments.ts +0 -103
  28. package/src/tools/delete-document.ts +0 -43
  29. package/src/tools/directives.ts +0 -303
  30. package/src/tools/get-document.ts +0 -80
  31. package/src/tools/get-project-memory.ts +0 -65
  32. package/src/tools/get-related-entities.ts +0 -73
  33. package/src/tools/governance.ts +0 -165
  34. package/src/tools/ingest-document.ts +0 -65
  35. package/src/tools/letter-inbox.ts +0 -157
  36. package/src/tools/letters.ts +0 -144
  37. package/src/tools/list-documents.ts +0 -58
  38. package/src/tools/list-tasks.ts +0 -57
  39. package/src/tools/project-list.ts +0 -52
  40. package/src/tools/reviews.ts +0 -281
  41. package/src/tools/search-knowledge.ts +0 -68
  42. package/src/tools/sessions.ts +0 -157
  43. package/src/tools/skill-assign.ts +0 -142
  44. package/src/tools/skills.ts +0 -255
  45. package/src/tools/update-document.ts +0 -66
  46. package/src/tools/update-task-status.ts +0 -64
  47. package/tsconfig.json +0 -21
  48. package/vitest.config.ts +0 -8
@@ -1,443 +0,0 @@
1
- /**
2
- * E2E Integration Tests for Nexus MCP Tools
3
- *
4
- * These tests exercise the real API client (nexusApi) against a live
5
- * Nexus backend. They require:
6
- *
7
- * NEXUS_API_URL=https://nexus.gatewarden.eu (or localhost:3000)
8
- * NEXUS_PRIVATE_TOKEN=nxs_pat_<valid_token>
9
- * NEXUS_E2E_PROJECT_ID=<uuid of a test project>
10
- *
11
- * Run with: NEXUS_E2E=1 vitest run --testPathPattern=e2e
12
- *
13
- * Skip in CI by default (only runs when NEXUS_E2E=1 is set).
14
- */
15
-
16
- import { afterAll, beforeAll, describe, expect, it } from 'vitest'
17
- import { nexusGet, nexusPost, resetApiConfig } from '../nexus-api.js'
18
-
19
- const E2E_ENABLED = process.env.NEXUS_E2E === '1'
20
- const PROJECT_ID = process.env.NEXUS_E2E_PROJECT_ID
21
-
22
- // The global setup.ts overrides env vars with test values.
23
- // For E2E tests, we restore the real values from NEXUS_E2E_* vars.
24
- const REAL_API_URL = process.env.NEXUS_E2E_API_URL
25
- const REAL_TOKEN = process.env.NEXUS_E2E_TOKEN
26
-
27
- if (E2E_ENABLED && REAL_API_URL && REAL_TOKEN) {
28
- process.env.NEXUS_API_URL = REAL_API_URL
29
- process.env.NEXUS_PRIVATE_TOKEN = REAL_TOKEN
30
- resetApiConfig()
31
- }
32
-
33
- const describeE2e = E2E_ENABLED ? describe : describe.skip
34
-
35
- // ---------------------------------------------------------------------------
36
- // Shared state for cross-test references
37
- // ---------------------------------------------------------------------------
38
- let createdSessionId: string | null = null
39
- let createdTaskId: string | null = null
40
- let createdDocId: string | null = null
41
- let createdLetterId: string | null = null
42
-
43
- describeE2e('E2E: Identity and Project Access', () => {
44
- it('should resolve identity from token', async () => {
45
- const result = await nexusGet('/api/mcp/identity')
46
- expect(result.ok).toBe(true)
47
- expect(result.data).toHaveProperty('userId')
48
- expect(result.data).toHaveProperty('email')
49
- // Backend returns isPlatformAdmin/isPlatformOwner boolean fields
50
- expect(result.data).toHaveProperty('isPlatformAdmin')
51
- })
52
-
53
- it('should list accessible projects', async () => {
54
- const result = await nexusGet('/api/mcp/projects')
55
- expect(result.ok).toBe(true)
56
- const data = result.data as { projects: unknown[] }
57
- expect(Array.isArray(data.projects)).toBe(true)
58
- expect(data.projects.length).toBeGreaterThan(0)
59
- })
60
- })
61
-
62
- describeE2e('E2E: Session Lifecycle', () => {
63
- it('should create a session', async () => {
64
- const result = await nexusPost('/api/mcp/sessions', {
65
- action: 'session_create',
66
- project_id: PROJECT_ID,
67
- title: 'E2E Test Session',
68
- })
69
- expect(result.ok).toBe(true)
70
- const data = result.data as { action: string; id: string; status: string }
71
- expect(data.action).toBe('session_create')
72
- expect(data.status).toBe('open')
73
- createdSessionId = data.id
74
- })
75
-
76
- it('should list sessions and find the created one', async () => {
77
- const result = await nexusPost('/api/mcp/sessions', {
78
- action: 'session_list',
79
- project_id: PROJECT_ID,
80
- })
81
- expect(result.ok).toBe(true)
82
- const data = result.data as { sessions: { id: string }[] }
83
- const found = data.sessions.find((s) => s.id === createdSessionId)
84
- expect(found).toBeDefined()
85
- })
86
-
87
- it('should append an entry to the session', async () => {
88
- const result = await nexusPost('/api/mcp/sessions', {
89
- action: 'session_append',
90
- session_id: createdSessionId,
91
- entry_type: 'note',
92
- summary: 'E2E test entry',
93
- })
94
- // Known backend bug: actor field gets email string instead of UUID
95
- if (!result.ok && result.error?.includes('uuid')) {
96
- console.warn('[E2E] session_append has known actor UUID bug:', result.error)
97
- return
98
- }
99
- expect(result.ok).toBe(true)
100
- })
101
-
102
- it('should close the session', async () => {
103
- const result = await nexusPost('/api/mcp/sessions', {
104
- action: 'session_close',
105
- session_id: createdSessionId,
106
- summary: 'E2E test completed',
107
- next_entry_point: 'Cleanup test data',
108
- })
109
- expect(result.ok).toBe(true)
110
- const data = result.data as { status: string }
111
- expect(data.status).toBe('closed')
112
- })
113
- })
114
-
115
- describeE2e('E2E: Task Lifecycle', () => {
116
- it('should create a task', async () => {
117
- const result = await nexusPost('/api/mcp/tasks', {
118
- action: 'task_create',
119
- project_id: PROJECT_ID,
120
- title: 'E2E Test Task',
121
- description: 'Created by E2E integration test',
122
- priority: 'low',
123
- })
124
- expect(result.ok).toBe(true)
125
- const data = result.data as { action: string; task_id: string }
126
- expect(data.action).toBe('task_create')
127
- createdTaskId = data.task_id
128
- })
129
-
130
- it('should list tasks and find the created one', async () => {
131
- const result = await nexusPost('/api/mcp/tasks', {
132
- action: 'task_list',
133
- project_id: PROJECT_ID,
134
- })
135
- // task_list is a new action - skip if backend not yet deployed
136
- if (!result.ok && result.error?.includes('Unknown action')) return
137
- expect(result.ok).toBe(true)
138
- const data = result.data as { tasks: { id: string }[] }
139
- const found = data.tasks.find((t) => t.id === createdTaskId)
140
- expect(found).toBeDefined()
141
- })
142
-
143
- it('should add a note to the task', async () => {
144
- const result = await nexusPost('/api/mcp/tasks', {
145
- action: 'task_note',
146
- task_id: createdTaskId,
147
- note: 'E2E test note',
148
- })
149
- // task_notes may fail due to RLS or missing table - record but don't fail
150
- if (!result.ok) {
151
- console.warn('[E2E] task_note failed:', result.error)
152
- return
153
- }
154
- expect(result.ok).toBe(true)
155
- })
156
-
157
- it('should update task status to done', async () => {
158
- const result = await nexusPost('/api/mcp/tasks', {
159
- action: 'task_update',
160
- task_id: createdTaskId,
161
- status: 'done',
162
- })
163
- expect(result.ok).toBe(true)
164
- const data = result.data as { new_status: string }
165
- expect(data.new_status).toBe('done')
166
- })
167
-
168
- it('should filter tasks by status', async () => {
169
- const result = await nexusPost('/api/mcp/tasks', {
170
- action: 'task_list',
171
- project_id: PROJECT_ID,
172
- status_filter: ['done'],
173
- })
174
- // task_list is a new action - skip if backend not yet deployed
175
- if (!result.ok && result.error?.includes('Unknown action')) return
176
- expect(result.ok).toBe(true)
177
- const data = result.data as { tasks: { status: string }[] }
178
- for (const task of data.tasks) {
179
- expect(task.status).toBe('done')
180
- }
181
- })
182
- })
183
-
184
- describeE2e('E2E: Document Lifecycle', () => {
185
- it('should ingest a document', async () => {
186
- const result = await nexusPost('/api/mcp/documents', {
187
- action: 'doc_ingest',
188
- project_id: PROJECT_ID,
189
- title: 'E2E Test Document',
190
- body: '# E2E Test\n\nThis document was created by the E2E test suite.',
191
- source: 'e2e-test',
192
- })
193
- expect(result.ok).toBe(true)
194
- const data = result.data as { action: string; document_id: string }
195
- expect(data.action).toBe('doc_ingest')
196
- createdDocId = data.document_id
197
- })
198
-
199
- it('should list documents and find the ingested one', async () => {
200
- const result = await nexusPost('/api/mcp/documents', {
201
- action: 'doc_list',
202
- project_id: PROJECT_ID,
203
- })
204
- // doc_list is a new action - skip if backend not yet deployed
205
- if (!result.ok && result.error?.includes('entity_type')) return
206
- expect(result.ok).toBe(true)
207
- const data = result.data as { documents: { id: string }[] }
208
- const found = data.documents.find((d) => d.id === createdDocId)
209
- expect(found).toBeDefined()
210
- })
211
-
212
- it('should filter documents by source', async () => {
213
- const result = await nexusPost('/api/mcp/documents', {
214
- action: 'doc_list',
215
- project_id: PROJECT_ID,
216
- source: 'e2e-test',
217
- })
218
- // doc_list is a new action - skip if backend not yet deployed
219
- if (!result.ok && result.error?.includes('entity_type')) return
220
- expect(result.ok).toBe(true)
221
- const data = result.data as { documents: { source: string }[] }
222
- for (const doc of data.documents) {
223
- expect(doc.source).toBe('e2e-test')
224
- }
225
- })
226
-
227
- it('should fetch the ingested document via kb_get', async () => {
228
- const result = await nexusPost('/api/mcp/documents', {
229
- action: 'kb_get',
230
- entity_type: 'ingest_item',
231
- entity_id: createdDocId,
232
- })
233
- expect(result.ok).toBe(true)
234
- const data = result.data as { entity_type: string; document: { title: string } }
235
- expect(data.entity_type).toBe('ingest_item')
236
- expect(data.document.title).toBe('E2E Test Document')
237
- })
238
- })
239
-
240
- describeE2e('E2E: Knowledge Search and Memory', () => {
241
- it('should search knowledge by keyword', async () => {
242
- const result = await nexusPost('/api/mcp/search', {
243
- project_id: PROJECT_ID,
244
- query: 'architecture',
245
- search_mode: 'keyword',
246
- })
247
- expect(result.ok).toBe(true)
248
- const data = result.data as { total_results: number }
249
- expect(data.total_results).toBeGreaterThanOrEqual(0)
250
- })
251
-
252
- it('should get project memory with ADRs', async () => {
253
- const result = await nexusPost('/api/mcp/memory', {
254
- project_id: PROJECT_ID,
255
- include: ['adrs'],
256
- depth: 'light',
257
- })
258
- expect(result.ok).toBe(true)
259
- const data = result.data as { memory: { adrs: unknown[] } }
260
- expect(Array.isArray(data.memory.adrs)).toBe(true)
261
- })
262
-
263
- it('should get project memory with active tasks', async () => {
264
- const result = await nexusPost('/api/mcp/memory', {
265
- project_id: PROJECT_ID,
266
- include: ['active_tasks'],
267
- })
268
- expect(result.ok).toBe(true)
269
- })
270
- })
271
-
272
- describeE2e('E2E: Vault Letter Lifecycle', () => {
273
- it('should create a letter', async () => {
274
- const result = await nexusPost('/api/mcp/letters', {
275
- action: 'vl_create',
276
- project_id: PROJECT_ID,
277
- from_actor: 'e2e-test-agent',
278
- to_actor: 'e2e-test-human',
279
- subject: 'E2E Test Letter',
280
- body: 'This letter was created by the E2E test suite.',
281
- priority: 'low',
282
- })
283
- expect(result.ok).toBe(true)
284
- const data = result.data as { action: string; letter_id: string }
285
- expect(data.action).toBe('vl_create')
286
- createdLetterId = data.letter_id
287
- })
288
-
289
- it('should find the letter in inbox', async () => {
290
- const result = await nexusPost('/api/mcp/letters', {
291
- action: 'vl_inbox',
292
- project_id: PROJECT_ID,
293
- })
294
- expect(result.ok).toBe(true)
295
- })
296
-
297
- it('should acknowledge the letter', async () => {
298
- const result = await nexusPost('/api/mcp/letters', {
299
- action: 'vl_ack',
300
- letter_id: createdLetterId,
301
- })
302
- expect(result.ok).toBe(true)
303
- })
304
-
305
- it('should reply to the letter', async () => {
306
- const result = await nexusPost('/api/mcp/letters', {
307
- action: 'vl_reply',
308
- letter_id: createdLetterId,
309
- body: 'E2E test reply',
310
- new_status: 'closed',
311
- })
312
- expect(result.ok).toBe(true)
313
- })
314
-
315
- it('should find the letter in outbox', async () => {
316
- const result = await nexusPost('/api/mcp/letters', {
317
- action: 'vl_outbox',
318
- project_id: PROJECT_ID,
319
- })
320
- expect(result.ok).toBe(true)
321
- })
322
- })
323
-
324
- describeE2e('E2E: Skill Management', () => {
325
- it('should list skills', async () => {
326
- const result = await nexusPost('/api/mcp/skills', {
327
- action: 'sk_list',
328
- })
329
- expect(result.ok).toBe(true)
330
- const data = result.data as { action: string; count: number }
331
- expect(data.action).toBe('sk_list')
332
- })
333
-
334
- it('should export skills for project', async () => {
335
- const result = await nexusPost('/api/mcp/skills', {
336
- action: 'sk_export',
337
- project_id: PROJECT_ID,
338
- })
339
- expect(result.ok).toBe(true)
340
- const data = result.data as { action: string }
341
- expect(data.action).toBe('sk_export')
342
- })
343
- })
344
-
345
- describeE2e('E2E: Governance (ADR)', () => {
346
- let testAdrId: string | null = null
347
-
348
- it('should create an ADR draft', async () => {
349
- const result = await nexusPost('/api/mcp/governance', {
350
- action: 'adr_create',
351
- project_id: PROJECT_ID,
352
- title: 'E2E Test ADR',
353
- context: 'Created by E2E integration test suite.',
354
- decision: 'This ADR should be cleaned up after testing.',
355
- })
356
- expect(result.ok).toBe(true)
357
- const data = result.data as { action: string; adr_id: string }
358
- expect(data.action).toBe('adr_create')
359
- testAdrId = data.adr_id
360
- })
361
-
362
- it('should submit the ADR for review', async () => {
363
- const result = await nexusPost('/api/mcp/governance', {
364
- action: 'adr_submit',
365
- adr_id: testAdrId,
366
- })
367
- expect(result.ok).toBe(true)
368
- const data = result.data as { new_state: string }
369
- expect(data.new_state).toBe('under_review')
370
- })
371
-
372
- it('should add a comment to the ADR', async () => {
373
- const result = await nexusPost('/api/mcp/governance', {
374
- action: 'dc_add',
375
- decision_id: testAdrId,
376
- body: 'E2E test comment',
377
- })
378
- expect(result.ok).toBe(true)
379
- })
380
-
381
- it('should list ADR comments', async () => {
382
- const result = await nexusPost('/api/mcp/governance', {
383
- action: 'dc_list',
384
- decision_id: testAdrId,
385
- })
386
- expect(result.ok).toBe(true)
387
- })
388
-
389
- it('should reject the test ADR', async () => {
390
- const result = await nexusPost('/api/mcp/governance', {
391
- action: 'adr_decide',
392
- adr_id: testAdrId,
393
- decision: 'rejected',
394
- rationale: 'E2E test cleanup - not a real ADR',
395
- })
396
- expect(result.ok).toBe(true)
397
- const data = result.data as { new_state: string }
398
- expect(data.new_state).toBe('rejected')
399
- })
400
- })
401
-
402
- describeE2e('E2E: Entity Navigation', () => {
403
- it('should navigate related entities', async () => {
404
- // Use the test session to find related entries
405
- if (!createdSessionId) return
406
-
407
- const result = await nexusPost('/api/mcp/related', {
408
- entity_type: 'session',
409
- entity_id: createdSessionId,
410
- })
411
- expect(result.ok).toBe(true)
412
- })
413
- })
414
-
415
- describeE2e('E2E: Error Handling', () => {
416
- it('should return 400 for missing required fields', async () => {
417
- const result = await nexusPost('/api/mcp/tasks', {
418
- action: 'task_create',
419
- project_id: PROJECT_ID,
420
- // missing title
421
- })
422
- expect(result.ok).toBe(false)
423
- expect(result.status).toBe(400)
424
- })
425
-
426
- it('should return 404 for non-existent entity', async () => {
427
- const result = await nexusPost('/api/mcp/documents', {
428
- action: 'kb_get',
429
- entity_type: 'task',
430
- entity_id: '00000000-0000-0000-0000-000000000000',
431
- })
432
- expect(result.ok).toBe(false)
433
- expect(result.status).toBe(404)
434
- })
435
-
436
- it('should return 400 for unknown action', async () => {
437
- const result = await nexusPost('/api/mcp/tasks', {
438
- action: 'nonexistent_action',
439
- })
440
- expect(result.ok).toBe(false)
441
- expect(result.status).toBe(400)
442
- })
443
- })
@@ -1,58 +0,0 @@
1
- /**
2
- * Test helpers for MCP tool tests.
3
- *
4
- * Provides utilities for mocking the Nexus API client
5
- * and parsing MCP tool responses.
6
- */
7
-
8
- import type { NexusApiResponse } from '../nexus-api.js'
9
-
10
- /**
11
- * Create a successful NexusApiResponse.
12
- */
13
- export function mockApiSuccess<T>(data: T): NexusApiResponse<T> {
14
- return { ok: true, status: 200, data, error: null }
15
- }
16
-
17
- /**
18
- * Create a failed NexusApiResponse.
19
- */
20
- export function mockApiError(
21
- error: string,
22
- status = 400,
23
- ): NexusApiResponse<never> {
24
- return { ok: false, status, data: null, error }
25
- }
26
-
27
- /**
28
- * Helper to parse the JSON from an MCP tool response.
29
- */
30
- export function parseToolResponse(result: {
31
- content: { type: string; text: string }[]
32
- isError?: boolean
33
- }) {
34
- const text = result.content[0]?.text
35
- if (!text) throw new Error('Empty tool response')
36
- return JSON.parse(text)
37
- }
38
-
39
- /**
40
- * Standard test UUIDs for consistency across tests.
41
- */
42
- export const TEST_IDS = {
43
- userId: '83a37012-8add-428f-8bc3-d56c84291671',
44
- projectId: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
45
- sessionId: '11111111-2222-3333-4444-555555555555',
46
- taskId: '22222222-3333-4444-5555-666666666666',
47
- letterId: '33333333-4444-5555-6666-777777777777',
48
- adrId: '44444444-5555-6666-7777-888888888888',
49
- entryId: '55555555-6666-7777-8888-999999999999',
50
- noteId: '66666666-7777-8888-9999-aaaaaaaaaaaa',
51
- documentId: '77777777-8888-9999-aaaa-bbbbbbbbbbbb',
52
- agentId: 'nexus-app-agent',
53
- otherUserId: '99999999-aaaa-bbbb-cccc-dddddddddddd',
54
- skillId: '88888888-9999-aaaa-bbbb-cccccccccccc',
55
- tenantId: '20c72e35-d4d8-4e40-a7be-efff14d8eaff',
56
- reviewId: 'aabbccdd-1122-3344-5566-778899001122',
57
- assignmentId: 'bbccddee-2233-4455-6677-889900112233',
58
- }