@mpowr/nexus-mcp 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/release.yml +57 -0
- package/README.md +30 -5
- package/dist/nexus-api.d.ts +5 -0
- package/dist/nexus-api.d.ts.map +1 -1
- package/dist/nexus-api.js +8 -0
- package/dist/nexus-api.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +7 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/list-documents.d.ts +33 -0
- package/dist/tools/list-documents.d.ts.map +1 -0
- package/dist/tools/list-documents.js +46 -0
- package/dist/tools/list-documents.js.map +1 -0
- package/dist/tools/list-tasks.d.ts +39 -0
- package/dist/tools/list-tasks.d.ts.map +1 -0
- package/dist/tools/list-tasks.js +46 -0
- package/dist/tools/list-tasks.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/e2e-integration.test.ts +443 -0
- package/src/__tests__/layer2-coordination.test.ts +67 -0
- package/src/__tests__/server.test.ts +14 -2
- package/src/__tests__/task-list-and-doc-list.test.ts +226 -0
- package/src/nexus-api.ts +9 -0
- package/src/server.ts +22 -1
- package/src/tools/list-documents.ts +57 -0
- package/src/tools/list-tasks.ts +57 -0
|
@@ -0,0 +1,226 @@
|
|
|
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/nexus-api.ts
CHANGED
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
let _baseUrl: string | null = null
|
|
10
10
|
let _token: string | null = null
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Reset the cached API configuration. Used by E2E tests to switch
|
|
14
|
+
* from mocked env vars to real credentials.
|
|
15
|
+
*/
|
|
16
|
+
export function resetApiConfig(): void {
|
|
17
|
+
_baseUrl = null
|
|
18
|
+
_token = null
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
function getConfig(): { baseUrl: string; token: string } {
|
|
13
22
|
if (_baseUrl && _token) return { baseUrl: _baseUrl, token: _token }
|
|
14
23
|
|
package/src/server.ts
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* - task_create: Create a task in a project
|
|
28
28
|
* - task_update: Update task status, priority, or assignee
|
|
29
29
|
* - task_note: Append a note to a task (append-only)
|
|
30
|
+
* - task_list: List tasks for a project with filters
|
|
30
31
|
* - dc_add: Add a comment to an ADR (append-only)
|
|
31
32
|
* - dc_list: List comments for an ADR
|
|
32
33
|
* - sk_list: List skills for the current tenant
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
* - sk_unassign: Remove a skill assignment from a project
|
|
39
40
|
* - sk_export: Export all skill assignments for a project
|
|
40
41
|
* - doc_ingest: Push text/markdown content into project knowledge base
|
|
42
|
+
* - doc_list: List ingested documents for a project
|
|
41
43
|
* - session_append: Append to a session (write-isolated)
|
|
42
44
|
* - session_create: Start a new work session
|
|
43
45
|
* - session_close: End a session with summary and next entry point
|
|
@@ -114,6 +116,11 @@ import {
|
|
|
114
116
|
replyLetter,
|
|
115
117
|
replyLetterSchema,
|
|
116
118
|
} from './tools/letters.js'
|
|
119
|
+
import {
|
|
120
|
+
listDocuments,
|
|
121
|
+
listDocumentsSchema,
|
|
122
|
+
} from './tools/list-documents.js'
|
|
123
|
+
import { listTasks, listTasksSchema } from './tools/list-tasks.js'
|
|
117
124
|
import {
|
|
118
125
|
closeSession,
|
|
119
126
|
closeSessionSchema,
|
|
@@ -195,7 +202,7 @@ function withIdentity(handler: (args: any) => Promise<any>) {
|
|
|
195
202
|
const server = new McpServer(
|
|
196
203
|
{
|
|
197
204
|
name: 'nexus-mcp',
|
|
198
|
-
version: '0.
|
|
205
|
+
version: '0.6.0',
|
|
199
206
|
},
|
|
200
207
|
{
|
|
201
208
|
capabilities: {
|
|
@@ -303,6 +310,13 @@ server.tool(
|
|
|
303
310
|
withIdentity(addTaskNote),
|
|
304
311
|
)
|
|
305
312
|
|
|
313
|
+
server.tool(
|
|
314
|
+
'task_list',
|
|
315
|
+
'List tasks for a project with optional status filtering. Returns tasks ordered by creation date (newest first).',
|
|
316
|
+
listTasksSchema,
|
|
317
|
+
withIdentity(listTasks),
|
|
318
|
+
)
|
|
319
|
+
|
|
306
320
|
server.tool(
|
|
307
321
|
'doc_ingest',
|
|
308
322
|
'Push text or markdown content into a project knowledge base. Creates an ingest item that can later be classified. Useful for agents to persist research results, generated documents, or extracted knowledge.',
|
|
@@ -310,6 +324,13 @@ server.tool(
|
|
|
310
324
|
withIdentity(ingestDocument),
|
|
311
325
|
)
|
|
312
326
|
|
|
327
|
+
server.tool(
|
|
328
|
+
'doc_list',
|
|
329
|
+
'List ingested documents for a project with optional source filtering. Returns documents ordered by creation date (newest first).',
|
|
330
|
+
listDocumentsSchema,
|
|
331
|
+
withIdentity(listDocuments),
|
|
332
|
+
)
|
|
333
|
+
|
|
313
334
|
server.tool(
|
|
314
335
|
'session_append',
|
|
315
336
|
'Append an entry to an existing session. Enforces session write isolation: only the session creator can append entries.',
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doc_list -- Layer 2 Coordination
|
|
3
|
+
*
|
|
4
|
+
* List ingested documents for a project with optional source filtering.
|
|
5
|
+
* Delegates to POST /api/mcp/documents (action: doc_list).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
import { nexusPost } from '../nexus-api.js'
|
|
10
|
+
|
|
11
|
+
export const listDocumentsSchema = {
|
|
12
|
+
project_id: z.string().uuid().describe('Project UUID'),
|
|
13
|
+
source: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Filter by source (e.g., "mcp-agent", "research", "session-extract")'),
|
|
17
|
+
limit: z
|
|
18
|
+
.number()
|
|
19
|
+
.min(1)
|
|
20
|
+
.max(100)
|
|
21
|
+
.default(50)
|
|
22
|
+
.describe('Maximum number of documents to return (default 50)'),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type ListDocumentsArgs = {
|
|
26
|
+
project_id: string
|
|
27
|
+
source?: string
|
|
28
|
+
limit?: number
|
|
29
|
+
user_id: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function listDocuments(args: ListDocumentsArgs) {
|
|
33
|
+
const result = await nexusPost('/api/mcp/documents', {
|
|
34
|
+
action: 'doc_list',
|
|
35
|
+
project_id: args.project_id,
|
|
36
|
+
source: args.source,
|
|
37
|
+
limit: args.limit ?? 50,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (!result.ok) {
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: 'text' as const,
|
|
45
|
+
text: JSON.stringify({ error: result.error }, null, 2),
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
isError: true,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
content: [
|
|
54
|
+
{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) },
|
|
55
|
+
],
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task_list -- Layer 2 Coordination
|
|
3
|
+
*
|
|
4
|
+
* List tasks for a project with optional status filtering.
|
|
5
|
+
* Delegates to POST /api/mcp/tasks (action: task_list).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
import { nexusPost } from '../nexus-api.js'
|
|
10
|
+
|
|
11
|
+
export const listTasksSchema = {
|
|
12
|
+
project_id: z.string().uuid().describe('Project UUID'),
|
|
13
|
+
status_filter: z
|
|
14
|
+
.array(z.enum(['open', 'in_progress', 'blocked', 'done', 'cancelled']))
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Filter by task status (default: all statuses)'),
|
|
17
|
+
limit: z
|
|
18
|
+
.number()
|
|
19
|
+
.min(1)
|
|
20
|
+
.max(100)
|
|
21
|
+
.default(50)
|
|
22
|
+
.describe('Maximum number of tasks to return (default 50)'),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type ListTasksArgs = {
|
|
26
|
+
project_id: string
|
|
27
|
+
status_filter?: string[]
|
|
28
|
+
limit?: number
|
|
29
|
+
user_id: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function listTasks(args: ListTasksArgs) {
|
|
33
|
+
const result = await nexusPost('/api/mcp/tasks', {
|
|
34
|
+
action: 'task_list',
|
|
35
|
+
project_id: args.project_id,
|
|
36
|
+
status_filter: args.status_filter,
|
|
37
|
+
limit: args.limit ?? 50,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (!result.ok) {
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: 'text' as const,
|
|
45
|
+
text: JSON.stringify({ error: result.error }, null, 2),
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
isError: true,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
content: [
|
|
54
|
+
{ type: 'text' as const, text: JSON.stringify(result.data, null, 2) },
|
|
55
|
+
],
|
|
56
|
+
}
|
|
57
|
+
}
|