@mpowr/nexus-mcp 0.5.0 → 0.6.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.
@@ -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.5.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
+ }