@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,58 @@
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
+ }
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Tests for Layer 1 Knowledge Access tools:
3
+ * - kb_search
4
+ * - kb_memory
5
+ * - kb_get
6
+ * - kb_related
7
+ *
8
+ * All tools now delegate to the Nexus API via nexusPost().
9
+ * Tests mock the nexus-api module.
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
+ // kb_search
23
+ // ---------------------------------------------------------------------------
24
+
25
+ describe('Layer 1: kb_search', () => {
26
+ beforeEach(() => {
27
+ vi.restoreAllMocks()
28
+ })
29
+
30
+ it('should return matching results for keyword search', async () => {
31
+ vi.mocked(nexusPost).mockResolvedValue(
32
+ mockApiSuccess({
33
+ total_results: 1,
34
+ results: [
35
+ {
36
+ entity_type: 'task',
37
+ entity_id: TEST_IDS.taskId,
38
+ title: 'Fix authentication bug',
39
+ relevance: 'exact',
40
+ },
41
+ ],
42
+ }),
43
+ )
44
+
45
+ const { searchKnowledge } = await import('../tools/search-knowledge.js')
46
+ const result = await searchKnowledge({
47
+ query: 'authentication bug',
48
+ project_id: TEST_IDS.projectId,
49
+ entity_types: ['task'],
50
+ })
51
+
52
+ expect(result.isError).toBeUndefined()
53
+ const parsed = parseToolResponse(result)
54
+ expect(parsed.total_results).toBe(1)
55
+ expect(parsed.results[0].entity_type).toBe('task')
56
+ expect(parsed.results[0].entity_id).toBe(TEST_IDS.taskId)
57
+ })
58
+
59
+ it('should return empty results when no matches', async () => {
60
+ vi.mocked(nexusPost).mockResolvedValue(
61
+ mockApiSuccess({ total_results: 0, results: [] }),
62
+ )
63
+
64
+ const { searchKnowledge } = await import('../tools/search-knowledge.js')
65
+ const result = await searchKnowledge({
66
+ query: 'nonexistent term xyz123',
67
+ project_id: TEST_IDS.projectId,
68
+ })
69
+
70
+ const parsed = parseToolResponse(result)
71
+ expect(parsed.total_results).toBe(0)
72
+ expect(parsed.results).toEqual([])
73
+ })
74
+
75
+ it('should pass correct parameters to the API', async () => {
76
+ vi.mocked(nexusPost).mockResolvedValue(
77
+ mockApiSuccess({ total_results: 0, results: [] }),
78
+ )
79
+
80
+ const { searchKnowledge } = await import('../tools/search-knowledge.js')
81
+ await searchKnowledge({
82
+ query: 'test',
83
+ project_id: TEST_IDS.projectId,
84
+ entity_types: ['task', 'decision'],
85
+ search_mode: 'hybrid',
86
+ limit: 5,
87
+ })
88
+
89
+ expect(nexusPost).toHaveBeenCalledWith('/api/mcp/search', {
90
+ project_id: TEST_IDS.projectId,
91
+ query: 'test',
92
+ entity_types: ['task', 'decision'],
93
+ search_mode: 'hybrid',
94
+ limit: 5,
95
+ })
96
+ })
97
+
98
+ it('should handle API errors gracefully', async () => {
99
+ vi.mocked(nexusPost).mockResolvedValue(mockApiError('connection lost'))
100
+
101
+ const { searchKnowledge } = await import('../tools/search-knowledge.js')
102
+ const result = await searchKnowledge({
103
+ query: 'test',
104
+ project_id: TEST_IDS.projectId,
105
+ })
106
+
107
+ expect(result.isError).toBe(true)
108
+ const parsed = parseToolResponse(result)
109
+ expect(parsed.error).toBe('connection lost')
110
+ })
111
+ })
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // kb_memory
115
+ // ---------------------------------------------------------------------------
116
+
117
+ describe('Layer 1: kb_memory', () => {
118
+ beforeEach(() => {
119
+ vi.restoreAllMocks()
120
+ })
121
+
122
+ it('should return curated project context', async () => {
123
+ vi.mocked(nexusPost).mockResolvedValue(
124
+ mockApiSuccess({
125
+ project_id: TEST_IDS.projectId,
126
+ depth: 'standard',
127
+ categories_included: ['adrs', 'active_tasks'],
128
+ memory: {
129
+ project: { id: TEST_IDS.projectId, name: 'Test Project' },
130
+ adrs: [{ id: TEST_IDS.adrId, title: 'ADR-001', status: 'accepted' }],
131
+ active_tasks: [{ id: TEST_IDS.taskId, title: 'Open task', status: 'open' }],
132
+ },
133
+ }),
134
+ )
135
+
136
+ const { getProjectMemory } = await import('../tools/get-project-memory.js')
137
+ const result = await getProjectMemory({
138
+ project_id: TEST_IDS.projectId,
139
+ include: ['adrs', 'active_tasks'],
140
+ })
141
+
142
+ expect(result.isError).toBeUndefined()
143
+ const parsed = parseToolResponse(result)
144
+ expect(parsed.project_id).toBe(TEST_IDS.projectId)
145
+ expect(parsed.memory.adrs).toHaveLength(1)
146
+ expect(parsed.memory.active_tasks).toHaveLength(1)
147
+ })
148
+
149
+ it('should pass depth parameter to the API', async () => {
150
+ vi.mocked(nexusPost).mockResolvedValue(
151
+ mockApiSuccess({ project_id: TEST_IDS.projectId, depth: 'light', memory: {} }),
152
+ )
153
+
154
+ const { getProjectMemory } = await import('../tools/get-project-memory.js')
155
+ await getProjectMemory({
156
+ project_id: TEST_IDS.projectId,
157
+ include: ['adrs'],
158
+ depth: 'light',
159
+ })
160
+
161
+ expect(nexusPost).toHaveBeenCalledWith('/api/mcp/memory', {
162
+ project_id: TEST_IDS.projectId,
163
+ include: ['adrs'],
164
+ depth: 'light',
165
+ })
166
+ })
167
+
168
+ it('should handle API errors', async () => {
169
+ vi.mocked(nexusPost).mockResolvedValue(mockApiError('Project not found'))
170
+
171
+ const { getProjectMemory } = await import('../tools/get-project-memory.js')
172
+ const result = await getProjectMemory({
173
+ project_id: TEST_IDS.projectId,
174
+ include: ['adrs'],
175
+ })
176
+
177
+ expect(result.isError).toBe(true)
178
+ })
179
+ })
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // kb_get
183
+ // ---------------------------------------------------------------------------
184
+
185
+ describe('Layer 1: kb_get', () => {
186
+ beforeEach(() => {
187
+ vi.restoreAllMocks()
188
+ })
189
+
190
+ it('should fetch a document in structured mode', async () => {
191
+ vi.mocked(nexusPost).mockResolvedValue(
192
+ mockApiSuccess({
193
+ entity_type: 'session',
194
+ entity_id: TEST_IDS.sessionId,
195
+ document: { id: TEST_IDS.sessionId, title: 'Test Session', status: 'open' },
196
+ entries: [{ id: TEST_IDS.entryId, entry_type: 'note', summary: 'A note' }],
197
+ }),
198
+ )
199
+
200
+ const { getDocument } = await import('../tools/get-document.js')
201
+ const result = await getDocument({
202
+ entity_type: 'session',
203
+ entity_id: TEST_IDS.sessionId,
204
+ })
205
+
206
+ expect(result.isError).toBeUndefined()
207
+ const parsed = parseToolResponse(result)
208
+ expect(parsed.entity_type).toBe('session')
209
+ expect(parsed.entity_id).toBe(TEST_IDS.sessionId)
210
+ })
211
+
212
+ it('should fetch a document in markdown mode', async () => {
213
+ vi.mocked(nexusPost).mockResolvedValue(
214
+ mockApiSuccess({
215
+ entity_type: 'decision',
216
+ entity_id: TEST_IDS.adrId,
217
+ format: 'markdown',
218
+ content: '# ADR-001\n\nSome content',
219
+ }),
220
+ )
221
+
222
+ const { getDocument } = await import('../tools/get-document.js')
223
+ const result = await getDocument({
224
+ entity_type: 'decision',
225
+ entity_id: TEST_IDS.adrId,
226
+ render_mode: 'markdown',
227
+ })
228
+
229
+ expect(result.isError).toBeUndefined()
230
+ expect(result.content[0].text).toContain('# ADR-001')
231
+ })
232
+
233
+ it('should return error for not found entity', async () => {
234
+ vi.mocked(nexusPost).mockResolvedValue(mockApiError('Document not found', 404))
235
+
236
+ const { getDocument } = await import('../tools/get-document.js')
237
+ const result = await getDocument({
238
+ entity_type: 'session',
239
+ entity_id: TEST_IDS.sessionId,
240
+ })
241
+
242
+ expect(result.isError).toBe(true)
243
+ })
244
+ })
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // kb_related
248
+ // ---------------------------------------------------------------------------
249
+
250
+ describe('Layer 1: kb_related', () => {
251
+ beforeEach(() => {
252
+ vi.restoreAllMocks()
253
+ })
254
+
255
+ it('should return related entities for a session', async () => {
256
+ vi.mocked(nexusPost).mockResolvedValue(
257
+ mockApiSuccess({
258
+ source: {
259
+ entity_type: 'session',
260
+ entity_id: TEST_IDS.sessionId,
261
+ project_id: TEST_IDS.projectId,
262
+ },
263
+ total_related: 1,
264
+ related: [
265
+ {
266
+ relation: 'task_in_project',
267
+ entity_type: 'task',
268
+ entity_id: TEST_IDS.taskId,
269
+ title: 'Related task',
270
+ status: 'open',
271
+ created_at: '2026-04-12',
272
+ },
273
+ ],
274
+ }),
275
+ )
276
+
277
+ const { getRelatedEntities } = await import('../tools/get-related-entities.js')
278
+ const result = await getRelatedEntities({
279
+ entity_type: 'session',
280
+ entity_id: TEST_IDS.sessionId,
281
+ })
282
+
283
+ expect(result.isError).toBeUndefined()
284
+ const parsed = parseToolResponse(result)
285
+ expect(parsed.source.entity_type).toBe('session')
286
+ expect(parsed.source.entity_id).toBe(TEST_IDS.sessionId)
287
+ expect(parsed.total_related).toBe(1)
288
+ expect(parsed.related[0].entity_type).toBe('task')
289
+ })
290
+
291
+ it('should handle API errors', async () => {
292
+ vi.mocked(nexusPost).mockResolvedValue(mockApiError('Entity not found', 404))
293
+
294
+ const { getRelatedEntities } = await import('../tools/get-related-entities.js')
295
+ const result = await getRelatedEntities({
296
+ entity_type: 'session',
297
+ entity_id: TEST_IDS.sessionId,
298
+ })
299
+
300
+ expect(result.isError).toBe(true)
301
+ })
302
+ })