@gwdn/nexus-mcp 0.8.6 → 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.
- package/README.md +3 -4
- package/dist/nexus-api.d.ts.map +1 -1
- package/dist/nexus-api.js +10 -1
- package/dist/nexus-api.js.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +9 -2
- package/.github/workflows/ci.yml +0 -38
- package/.github/workflows/release.yml +0 -57
- package/src/__tests__/auth.test.ts +0 -162
- package/src/__tests__/decision-comments.test.ts +0 -173
- package/src/__tests__/e2e-integration.test.ts +0 -443
- package/src/__tests__/helpers.ts +0 -58
- package/src/__tests__/layer1-knowledge.test.ts +0 -302
- package/src/__tests__/layer2-coordination.test.ts +0 -918
- package/src/__tests__/layer3-governance.test.ts +0 -205
- package/src/__tests__/machine-id.test.ts +0 -136
- package/src/__tests__/nexus-api.test.ts +0 -127
- package/src/__tests__/project-list-and-skill-assign.test.ts +0 -282
- package/src/__tests__/reviews.test.ts +0 -420
- package/src/__tests__/server.test.ts +0 -251
- package/src/__tests__/setup.ts +0 -15
- package/src/__tests__/task-list-and-doc-list.test.ts +0 -226
- package/src/auth.ts +0 -81
- package/src/machine-id.ts +0 -60
- package/src/nexus-api.ts +0 -141
- package/src/server.ts +0 -629
- package/src/tools/add-task-note.ts +0 -50
- package/src/tools/append-session-entry.ts +0 -113
- package/src/tools/classify-document.ts +0 -55
- package/src/tools/create-task.ts +0 -66
- package/src/tools/decision-comments.ts +0 -103
- package/src/tools/delete-document.ts +0 -43
- package/src/tools/directives.ts +0 -303
- package/src/tools/get-document.ts +0 -80
- package/src/tools/get-project-memory.ts +0 -65
- package/src/tools/get-related-entities.ts +0 -73
- package/src/tools/governance.ts +0 -165
- package/src/tools/ingest-document.ts +0 -65
- package/src/tools/letter-inbox.ts +0 -157
- package/src/tools/letters.ts +0 -144
- package/src/tools/list-documents.ts +0 -58
- package/src/tools/list-tasks.ts +0 -57
- package/src/tools/project-list.ts +0 -52
- package/src/tools/reviews.ts +0 -281
- package/src/tools/search-knowledge.ts +0 -68
- package/src/tools/sessions.ts +0 -157
- package/src/tools/skill-assign.ts +0 -142
- package/src/tools/skills.ts +0 -255
- package/src/tools/update-document.ts +0 -66
- package/src/tools/update-task-status.ts +0 -64
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -8
|
@@ -1,251 +0,0 @@
|
|
|
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 = 48
|
|
82
|
-
|
|
83
|
-
it('should register exactly 48 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(appendEntry.appendSessionEntrySchema.metadata).toBeDefined()
|
|
150
|
-
expect(typeof appendEntry.appendSessionEntry).toBe('function')
|
|
151
|
-
|
|
152
|
-
const letters = await import('../tools/letters.js')
|
|
153
|
-
expect(letters.createLetterSchema).toBeDefined()
|
|
154
|
-
expect(letters.replyLetterSchema).toBeDefined()
|
|
155
|
-
expect(typeof letters.createLetter).toBe('function')
|
|
156
|
-
expect(typeof letters.replyLetter).toBe('function')
|
|
157
|
-
|
|
158
|
-
const letterInbox = await import('../tools/letter-inbox.js')
|
|
159
|
-
expect(letterInbox.listInboxSchema).toBeDefined()
|
|
160
|
-
expect(letterInbox.listInboxSchema.project_id).toBeDefined()
|
|
161
|
-
expect(letterInbox.listOutboxSchema).toBeDefined()
|
|
162
|
-
expect(letterInbox.listOutboxSchema.project_id).toBeDefined()
|
|
163
|
-
expect(letterInbox.acknowledgeLetterSchema).toBeDefined()
|
|
164
|
-
expect(letterInbox.acknowledgeLetterSchema.letter_id).toBeDefined()
|
|
165
|
-
expect(typeof letterInbox.listInbox).toBe('function')
|
|
166
|
-
expect(typeof letterInbox.listOutbox).toBe('function')
|
|
167
|
-
expect(typeof letterInbox.acknowledgeLetter).toBe('function')
|
|
168
|
-
|
|
169
|
-
const comments = await import('../tools/decision-comments.js')
|
|
170
|
-
expect(comments.addDecisionCommentSchema).toBeDefined()
|
|
171
|
-
expect(comments.listDecisionCommentsSchema).toBeDefined()
|
|
172
|
-
expect(typeof comments.addDecisionComment).toBe('function')
|
|
173
|
-
expect(typeof comments.listDecisionComments).toBe('function')
|
|
174
|
-
|
|
175
|
-
const governance = await import('../tools/governance.js')
|
|
176
|
-
expect(governance.createAdrDraftSchema).toBeDefined()
|
|
177
|
-
expect(governance.submitAdrReviewSchema).toBeDefined()
|
|
178
|
-
expect(governance.recordAdrDecisionSchema).toBeDefined()
|
|
179
|
-
expect(typeof governance.createAdrDraft).toBe('function')
|
|
180
|
-
expect(typeof governance.submitAdrReview).toBe('function')
|
|
181
|
-
expect(typeof governance.recordAdrDecision).toBe('function')
|
|
182
|
-
|
|
183
|
-
const skills = await import('../tools/skills.js')
|
|
184
|
-
expect(skills.skListSchema).toBeDefined()
|
|
185
|
-
expect(skills.skGetSchema).toBeDefined()
|
|
186
|
-
expect(skills.skGetSchema.skill_id).toBeDefined()
|
|
187
|
-
expect(skills.skCreateSchema).toBeDefined()
|
|
188
|
-
expect(skills.skCreateSchema.skill_id).toBeDefined()
|
|
189
|
-
expect(skills.skCreateSchema.name).toBeDefined()
|
|
190
|
-
expect(skills.skCreateSchema.body).toBeDefined()
|
|
191
|
-
expect(skills.skUpdateSchema).toBeDefined()
|
|
192
|
-
expect(skills.skUpdateSchema.skill_id).toBeDefined()
|
|
193
|
-
expect(skills.skActivateSchema).toBeDefined()
|
|
194
|
-
expect(skills.skActivateSchema.skill_id).toBeDefined()
|
|
195
|
-
expect(skills.skActivateSchema.status).toBeDefined()
|
|
196
|
-
expect(typeof skills.skList).toBe('function')
|
|
197
|
-
expect(typeof skills.skGet).toBe('function')
|
|
198
|
-
expect(typeof skills.skCreate).toBe('function')
|
|
199
|
-
expect(typeof skills.skUpdate).toBe('function')
|
|
200
|
-
expect(typeof skills.skActivate).toBe('function')
|
|
201
|
-
|
|
202
|
-
// project-list module
|
|
203
|
-
const projectList = await import('../tools/project-list.js')
|
|
204
|
-
expect(projectList.projectListSchema).toBeDefined()
|
|
205
|
-
expect(typeof projectList.projectList).toBe('function')
|
|
206
|
-
|
|
207
|
-
// skill-assign module (sk_assign, sk_unassign, sk_export)
|
|
208
|
-
const skillAssign = await import('../tools/skill-assign.js')
|
|
209
|
-
expect(skillAssign.skAssignSchema).toBeDefined()
|
|
210
|
-
expect(skillAssign.skAssignSchema.project_id).toBeDefined()
|
|
211
|
-
expect(skillAssign.skAssignSchema.skill_id).toBeDefined()
|
|
212
|
-
expect(skillAssign.skUnassignSchema).toBeDefined()
|
|
213
|
-
expect(skillAssign.skUnassignSchema.project_id).toBeDefined()
|
|
214
|
-
expect(skillAssign.skExportSchema).toBeDefined()
|
|
215
|
-
expect(skillAssign.skExportSchema.project_id).toBeDefined()
|
|
216
|
-
expect(typeof skillAssign.skAssign).toBe('function')
|
|
217
|
-
expect(typeof skillAssign.skUnassign).toBe('function')
|
|
218
|
-
expect(typeof skillAssign.skExport).toBe('function')
|
|
219
|
-
|
|
220
|
-
// reviews module (rv_list, rv_get, rv_create, rv_decide, rv_comment)
|
|
221
|
-
const reviews = await import('../tools/reviews.js')
|
|
222
|
-
expect(reviews.rvListSchema).toBeDefined()
|
|
223
|
-
expect(reviews.rvGetSchema).toBeDefined()
|
|
224
|
-
expect(reviews.rvCreateSchema).toBeDefined()
|
|
225
|
-
expect(reviews.rvCreateSchema.entity_type).toBeDefined()
|
|
226
|
-
expect(reviews.rvCreateSchema.entity_id).toBeDefined()
|
|
227
|
-
expect(reviews.rvDecideSchema).toBeDefined()
|
|
228
|
-
expect(reviews.rvDecideSchema.review_id).toBeDefined()
|
|
229
|
-
expect(reviews.rvDecideSchema.transition).toBeDefined()
|
|
230
|
-
expect(reviews.rvCommentSchema).toBeDefined()
|
|
231
|
-
expect(reviews.rvCommentSchema.review_id).toBeDefined()
|
|
232
|
-
expect(reviews.rvCommentSchema.body).toBeDefined()
|
|
233
|
-
expect(typeof reviews.rvList).toBe('function')
|
|
234
|
-
expect(typeof reviews.rvGet).toBe('function')
|
|
235
|
-
expect(typeof reviews.rvCreate).toBe('function')
|
|
236
|
-
expect(typeof reviews.rvDecide).toBe('function')
|
|
237
|
-
expect(typeof reviews.rvComment).toBe('function')
|
|
238
|
-
|
|
239
|
-
// list-tasks module (task_list)
|
|
240
|
-
const listTasks = await import('../tools/list-tasks.js')
|
|
241
|
-
expect(listTasks.listTasksSchema).toBeDefined()
|
|
242
|
-
expect(listTasks.listTasksSchema.project_id).toBeDefined()
|
|
243
|
-
expect(typeof listTasks.listTasks).toBe('function')
|
|
244
|
-
|
|
245
|
-
// list-documents module (doc_list)
|
|
246
|
-
const listDocuments = await import('../tools/list-documents.js')
|
|
247
|
-
expect(listDocuments.listDocumentsSchema).toBeDefined()
|
|
248
|
-
expect(listDocuments.listDocumentsSchema.project_id).toBeDefined()
|
|
249
|
-
expect(typeof listDocuments.listDocuments).toBe('function')
|
|
250
|
-
})
|
|
251
|
-
})
|
package/src/__tests__/setup.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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(() => {})
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for task_list and doc_list tools.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
-
import { mockApiError, mockApiSuccess, parseToolResponse, TEST_IDS } from './helpers'
|
|
7
|
-
|
|
8
|
-
vi.mock('../nexus-api.js', () => ({
|
|
9
|
-
nexusPost: vi.fn(),
|
|
10
|
-
}))
|
|
11
|
-
|
|
12
|
-
import { nexusPost } from '../nexus-api.js'
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// task_list
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
describe('task_list', () => {
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
vi.restoreAllMocks()
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('should list tasks for a project', async () => {
|
|
24
|
-
vi.mocked(nexusPost).mockResolvedValue(
|
|
25
|
-
mockApiSuccess({
|
|
26
|
-
action: 'task_list',
|
|
27
|
-
project_id: TEST_IDS.projectId,
|
|
28
|
-
count: 2,
|
|
29
|
-
tasks: [
|
|
30
|
-
{
|
|
31
|
-
id: TEST_IDS.taskId,
|
|
32
|
-
title: 'Implement feature',
|
|
33
|
-
status: 'open',
|
|
34
|
-
priority: 'high',
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
id: TEST_IDS.noteId,
|
|
38
|
-
title: 'Fix bug',
|
|
39
|
-
status: 'in_progress',
|
|
40
|
-
priority: 'normal',
|
|
41
|
-
},
|
|
42
|
-
],
|
|
43
|
-
}),
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
const { listTasks } = await import('../tools/list-tasks.js')
|
|
47
|
-
const result = await listTasks({
|
|
48
|
-
project_id: TEST_IDS.projectId,
|
|
49
|
-
user_id: TEST_IDS.userId,
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
expect(result.isError).toBeUndefined()
|
|
53
|
-
const parsed = parseToolResponse(result)
|
|
54
|
-
expect(parsed.action).toBe('task_list')
|
|
55
|
-
expect(parsed.count).toBe(2)
|
|
56
|
-
expect(parsed.tasks).toHaveLength(2)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('should pass status_filter to API', async () => {
|
|
60
|
-
vi.mocked(nexusPost).mockResolvedValue(
|
|
61
|
-
mockApiSuccess({
|
|
62
|
-
action: 'task_list',
|
|
63
|
-
project_id: TEST_IDS.projectId,
|
|
64
|
-
count: 1,
|
|
65
|
-
tasks: [
|
|
66
|
-
{ id: TEST_IDS.taskId, title: 'Open task', status: 'open' },
|
|
67
|
-
],
|
|
68
|
-
}),
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
const { listTasks } = await import('../tools/list-tasks.js')
|
|
72
|
-
await listTasks({
|
|
73
|
-
project_id: TEST_IDS.projectId,
|
|
74
|
-
status_filter: ['open', 'in_progress'],
|
|
75
|
-
user_id: TEST_IDS.userId,
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
expect(vi.mocked(nexusPost)).toHaveBeenCalledWith('/api/mcp/tasks', {
|
|
79
|
-
action: 'task_list',
|
|
80
|
-
project_id: TEST_IDS.projectId,
|
|
81
|
-
status_filter: ['open', 'in_progress'],
|
|
82
|
-
limit: 50,
|
|
83
|
-
})
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('should return error on API failure', async () => {
|
|
87
|
-
vi.mocked(nexusPost).mockResolvedValue(mockApiError('Project not found', 404))
|
|
88
|
-
|
|
89
|
-
const { listTasks } = await import('../tools/list-tasks.js')
|
|
90
|
-
const result = await listTasks({
|
|
91
|
-
project_id: TEST_IDS.projectId,
|
|
92
|
-
user_id: TEST_IDS.userId,
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
expect(result.isError).toBe(true)
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('should handle empty task list', async () => {
|
|
99
|
-
vi.mocked(nexusPost).mockResolvedValue(
|
|
100
|
-
mockApiSuccess({
|
|
101
|
-
action: 'task_list',
|
|
102
|
-
project_id: TEST_IDS.projectId,
|
|
103
|
-
count: 0,
|
|
104
|
-
tasks: [],
|
|
105
|
-
}),
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
const { listTasks } = await import('../tools/list-tasks.js')
|
|
109
|
-
const result = await listTasks({
|
|
110
|
-
project_id: TEST_IDS.projectId,
|
|
111
|
-
user_id: TEST_IDS.userId,
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
expect(result.isError).toBeUndefined()
|
|
115
|
-
const parsed = parseToolResponse(result)
|
|
116
|
-
expect(parsed.count).toBe(0)
|
|
117
|
-
expect(parsed.tasks).toHaveLength(0)
|
|
118
|
-
})
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
// ---------------------------------------------------------------------------
|
|
122
|
-
// doc_list
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
|
|
125
|
-
describe('doc_list', () => {
|
|
126
|
-
beforeEach(() => {
|
|
127
|
-
vi.restoreAllMocks()
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
it('should list documents for a project', async () => {
|
|
131
|
-
vi.mocked(nexusPost).mockResolvedValue(
|
|
132
|
-
mockApiSuccess({
|
|
133
|
-
action: 'doc_list',
|
|
134
|
-
project_id: TEST_IDS.projectId,
|
|
135
|
-
count: 2,
|
|
136
|
-
documents: [
|
|
137
|
-
{
|
|
138
|
-
id: TEST_IDS.documentId,
|
|
139
|
-
title: 'Research Findings',
|
|
140
|
-
source: 'agent:app-agent',
|
|
141
|
-
classification: 'unclassified',
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
id: TEST_IDS.noteId,
|
|
145
|
-
title: 'E2E Test Plan',
|
|
146
|
-
source: 'mcp',
|
|
147
|
-
classification: 'unclassified',
|
|
148
|
-
},
|
|
149
|
-
],
|
|
150
|
-
}),
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
const { listDocuments } = await import('../tools/list-documents.js')
|
|
154
|
-
const result = await listDocuments({
|
|
155
|
-
project_id: TEST_IDS.projectId,
|
|
156
|
-
user_id: TEST_IDS.userId,
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
expect(result.isError).toBeUndefined()
|
|
160
|
-
const parsed = parseToolResponse(result)
|
|
161
|
-
expect(parsed.action).toBe('doc_list')
|
|
162
|
-
expect(parsed.count).toBe(2)
|
|
163
|
-
expect(parsed.documents).toHaveLength(2)
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
it('should pass source filter to API', async () => {
|
|
167
|
-
vi.mocked(nexusPost).mockResolvedValue(
|
|
168
|
-
mockApiSuccess({
|
|
169
|
-
action: 'doc_list',
|
|
170
|
-
project_id: TEST_IDS.projectId,
|
|
171
|
-
count: 1,
|
|
172
|
-
documents: [
|
|
173
|
-
{ id: TEST_IDS.documentId, title: 'Agent doc', source: 'agent:app-agent' },
|
|
174
|
-
],
|
|
175
|
-
}),
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
const { listDocuments } = await import('../tools/list-documents.js')
|
|
179
|
-
await listDocuments({
|
|
180
|
-
project_id: TEST_IDS.projectId,
|
|
181
|
-
source: 'agent:app-agent',
|
|
182
|
-
user_id: TEST_IDS.userId,
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
expect(vi.mocked(nexusPost)).toHaveBeenCalledWith('/api/mcp/documents', {
|
|
186
|
-
action: 'doc_list',
|
|
187
|
-
project_id: TEST_IDS.projectId,
|
|
188
|
-
source: 'agent:app-agent',
|
|
189
|
-
limit: 50,
|
|
190
|
-
})
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
it('should return error on API failure', async () => {
|
|
194
|
-
vi.mocked(nexusPost).mockResolvedValue(mockApiError('No access to this project', 403))
|
|
195
|
-
|
|
196
|
-
const { listDocuments } = await import('../tools/list-documents.js')
|
|
197
|
-
const result = await listDocuments({
|
|
198
|
-
project_id: TEST_IDS.projectId,
|
|
199
|
-
user_id: TEST_IDS.userId,
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
expect(result.isError).toBe(true)
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
it('should handle empty document list', async () => {
|
|
206
|
-
vi.mocked(nexusPost).mockResolvedValue(
|
|
207
|
-
mockApiSuccess({
|
|
208
|
-
action: 'doc_list',
|
|
209
|
-
project_id: TEST_IDS.projectId,
|
|
210
|
-
count: 0,
|
|
211
|
-
documents: [],
|
|
212
|
-
}),
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
const { listDocuments } = await import('../tools/list-documents.js')
|
|
216
|
-
const result = await listDocuments({
|
|
217
|
-
project_id: TEST_IDS.projectId,
|
|
218
|
-
user_id: TEST_IDS.userId,
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
expect(result.isError).toBeUndefined()
|
|
222
|
-
const parsed = parseToolResponse(result)
|
|
223
|
-
expect(parsed.count).toBe(0)
|
|
224
|
-
expect(parsed.documents).toHaveLength(0)
|
|
225
|
-
})
|
|
226
|
-
})
|
package/src/auth.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP Authentication Layer
|
|
3
|
-
*
|
|
4
|
-
* Resolves the NEXUS_PRIVATE_TOKEN environment variable at server startup
|
|
5
|
-
* to a verified user identity by calling the Nexus API identity endpoint.
|
|
6
|
-
*
|
|
7
|
-
* Flow:
|
|
8
|
-
* 1. Read NEXUS_PRIVATE_TOKEN from process.env
|
|
9
|
-
* 2. Call GET /api/mcp/identity with Bearer token
|
|
10
|
-
* 3. Cache resolved identity for the lifetime of the MCP process
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { nexusGet } from './nexus-api.js'
|
|
14
|
-
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
// Types
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
|
|
19
|
-
export interface McpIdentity {
|
|
20
|
-
userId: string
|
|
21
|
-
email: string | null
|
|
22
|
-
displayName: string | null
|
|
23
|
-
isPlatformAdmin: boolean
|
|
24
|
-
isPlatformOwner: boolean
|
|
25
|
-
tenantId: string | null
|
|
26
|
-
memberships: Array<{ project_id: string; role: string }>
|
|
27
|
-
agentAssignments: Array<{
|
|
28
|
-
project_id: string
|
|
29
|
-
agent_id: string
|
|
30
|
-
agent_owner: string
|
|
31
|
-
}>
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Module-level cache: resolved once at startup
|
|
35
|
-
let _identity: McpIdentity | null = null
|
|
36
|
-
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
// Public API
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Resolve the MCP server identity from NEXUS_PRIVATE_TOKEN.
|
|
43
|
-
* Called once at server startup. Caches the result.
|
|
44
|
-
* Throws if no valid token is available.
|
|
45
|
-
*/
|
|
46
|
-
export async function initIdentity(): Promise<McpIdentity> {
|
|
47
|
-
if (_identity) return _identity
|
|
48
|
-
|
|
49
|
-
const rawToken = process.env.NEXUS_PRIVATE_TOKEN
|
|
50
|
-
if (!rawToken) {
|
|
51
|
-
throw new Error(
|
|
52
|
-
'MCP: NEXUS_PRIVATE_TOKEN is not set. The MCP server requires a valid nxs_pat_* token.',
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const result = await nexusGet<McpIdentity>('/api/mcp/identity')
|
|
57
|
-
|
|
58
|
-
if (!result.ok || !result.data) {
|
|
59
|
-
throw new Error(
|
|
60
|
-
`MCP: Identity resolution failed: ${result.error ?? 'Unknown error'}. Check that the token is valid, not expired, and not revoked.`,
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
_identity = result.data
|
|
65
|
-
|
|
66
|
-
console.error(
|
|
67
|
-
`[nexus-mcp] Identity resolved: ${_identity.displayName ?? _identity.email ?? _identity.userId} (admin=${_identity.isPlatformAdmin}, owner=${_identity.isPlatformOwner})`,
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
return _identity
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Get the cached MCP identity. Must call initIdentity() first.
|
|
75
|
-
*/
|
|
76
|
-
export function getIdentity(): McpIdentity {
|
|
77
|
-
if (!_identity) {
|
|
78
|
-
throw new Error('MCP: Identity not initialized. Call initIdentity() first.')
|
|
79
|
-
}
|
|
80
|
-
return _identity
|
|
81
|
-
}
|
package/src/machine-id.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Machine ID module for Nexus MCP server.
|
|
3
|
-
*
|
|
4
|
-
* Reads or generates a persistent machine identifier stored at
|
|
5
|
-
* ~/.config/nexus/machine.toml. This ID is sent as X-Nexus-Machine-Id
|
|
6
|
-
* header with every API request so session entries can be attributed
|
|
7
|
-
* to a specific machine.
|
|
8
|
-
*
|
|
9
|
-
* Format of machine.toml:
|
|
10
|
-
* machine_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
14
|
-
import { homedir } from 'node:os'
|
|
15
|
-
import { join } from 'node:path'
|
|
16
|
-
import { randomUUID } from 'node:crypto'
|
|
17
|
-
|
|
18
|
-
const CONFIG_DIR = join(homedir(), '.config', 'nexus')
|
|
19
|
-
const MACHINE_TOML = join(CONFIG_DIR, 'machine.toml')
|
|
20
|
-
|
|
21
|
-
let _cachedId: string | null = null
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Get the persistent machine ID, creating one if it doesn't exist.
|
|
25
|
-
* Returns null only if file I/O fails (e.g. read-only filesystem).
|
|
26
|
-
*/
|
|
27
|
-
export function getMachineId(): string | null {
|
|
28
|
-
if (_cachedId) return _cachedId
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
// Try to read existing
|
|
32
|
-
if (existsSync(MACHINE_TOML)) {
|
|
33
|
-
const content = readFileSync(MACHINE_TOML, 'utf-8')
|
|
34
|
-
const match = content.match(/machine_id\s*=\s*"([^"]+)"/)
|
|
35
|
-
if (match?.[1]) {
|
|
36
|
-
_cachedId = match[1]
|
|
37
|
-
return _cachedId
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Generate new machine ID
|
|
42
|
-
const id = randomUUID()
|
|
43
|
-
mkdirSync(CONFIG_DIR, { recursive: true })
|
|
44
|
-
writeFileSync(
|
|
45
|
-
MACHINE_TOML,
|
|
46
|
-
`# Nexus persistent machine identifier\n# Generated automatically — do not edit\nmachine_id = "${id}"\n`,
|
|
47
|
-
'utf-8',
|
|
48
|
-
)
|
|
49
|
-
_cachedId = id
|
|
50
|
-
return _cachedId
|
|
51
|
-
} catch {
|
|
52
|
-
// Silently fail — machine_id is optional enrichment
|
|
53
|
-
return null
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Reset cached machine ID (for testing). */
|
|
58
|
-
export function resetMachineId(): void {
|
|
59
|
-
_cachedId = null
|
|
60
|
-
}
|