@lil2good/nubis-mcp-server 1.0.51 → 1.0.53

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 (3) hide show
  1. package/build/index.js +576 -256
  2. package/package.json +1 -1
  3. package/src/index.ts +793 -277
package/src/index.ts CHANGED
@@ -4,7 +4,6 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import { z } from "zod";
6
6
  import dotenv from 'dotenv';
7
- import { json } from "stream/consumers";
8
7
 
9
8
  dotenv.config();
10
9
 
@@ -12,7 +11,7 @@ dotenv.config();
12
11
  const cliWorkspaceID: string | undefined = process.env.NUBIS_WORKSPACE_ID;
13
12
  const cliApiKey: string | undefined = process.env.NUBIS_API_KEY;
14
13
 
15
- // Create server instance
14
+ // Create server instance
16
15
  const server = new McpServer({
17
16
  name: "nubis-mcp-server",
18
17
  version: "1.0.0",
@@ -22,8 +21,167 @@ const server = new McpServer({
22
21
  },
23
22
  });
24
23
 
25
- // Helper to get results from middleware
26
- async function getResultsFromMiddleware({endpoint, schema}: {endpoint: string, schema: any}) {
24
+ // ============================================================================
25
+ // Types
26
+ // ============================================================================
27
+
28
+ type Task = {
29
+ readonly id: string;
30
+ readonly title: string;
31
+ readonly task_number: number;
32
+ readonly board: string;
33
+ readonly bolt?: { id: string; name: string } | null;
34
+ readonly description?: string;
35
+ readonly context?: string;
36
+ readonly github_item_type?: string;
37
+ readonly github_file_path?: string;
38
+ readonly github_repo_name?: string;
39
+ readonly parent_task_id?: string;
40
+ readonly pm_task_blockers?: readonly { blocker_task_id: string }[];
41
+ readonly images?: readonly { url: string }[];
42
+ readonly subtasks?: readonly Task[];
43
+ readonly comments?: readonly { id: string; content: string; created_at: string }[];
44
+ };
45
+
46
+ type Bolt = {
47
+ readonly id: string;
48
+ readonly name: string;
49
+ readonly description?: string;
50
+ };
51
+
52
+ type ApiUsage = {
53
+ count: string;
54
+ remaining_calls: number;
55
+ total_limit: number;
56
+ };
57
+
58
+ type McpContentItem =
59
+ | { type: "text"; text: string }
60
+ | { type: "image"; data: string; mimeType: string }
61
+ | { type: "audio"; data: string; mimeType: string }
62
+ | { type: "resource"; resource: { text: string; uri: string; mimeType?: string } | { uri: string; blob: string; mimeType?: string } };
63
+
64
+ // ============================================================================
65
+ // Formatting Helpers
66
+ // ============================================================================
67
+
68
+ const BOARD_LABELS: Record<string, string> = {
69
+ 'backlog': '📋 Backlog',
70
+ 'bugs': '🐛 Bugs',
71
+ 'priority': '⚡ Priority',
72
+ 'in-progress': '🔄 In Progress',
73
+ 'reviewing': '👀 Reviewing',
74
+ 'completed': '✅ Completed'
75
+ };
76
+
77
+ function formatBoard(board: string): string {
78
+ return BOARD_LABELS[board] || board;
79
+ }
80
+
81
+ function formatApiUsage(usage: ApiUsage): string {
82
+ return `📊 **API Usage:** ${usage.remaining_calls}/${usage.total_limit} calls remaining`;
83
+ }
84
+
85
+ function formatTask(task: Task, options: { detailed?: boolean } = {}): string {
86
+ const lines: string[] = [
87
+ `## ${task.title}`,
88
+ '',
89
+ `| Property | Value |`,
90
+ `|----------|-------|`,
91
+ `| **ID** | \`${task.id}\` |`,
92
+ `| **Number** | #${task.task_number} |`,
93
+ `| **Board** | ${formatBoard(task.board)} |`,
94
+ `| **Bolt** | ${task.bolt?.name || '_Unassigned_'} |`,
95
+ ];
96
+
97
+ if (task.parent_task_id) {
98
+ lines.push(`| **Parent Task** | \`${task.parent_task_id}\` |`);
99
+ }
100
+
101
+ // Description
102
+ lines.push('', '### Description', task.description || '_No description provided_');
103
+
104
+ // GitHub info
105
+ if (task.github_file_path || task.github_repo_name) {
106
+ lines.push(
107
+ '',
108
+ '### GitHub',
109
+ `- **Repo:** ${task.github_repo_name || '_Not set_'}`,
110
+ `- **Path:** \`${task.github_file_path || '_Not set_'}\``,
111
+ `- **Type:** ${task.github_item_type || '_Not set_'}`
112
+ );
113
+ }
114
+
115
+ // Blockers
116
+ if (task.pm_task_blockers && task.pm_task_blockers.length > 0) {
117
+ lines.push(
118
+ '',
119
+ '### ⚠️ Blockers',
120
+ ...task.pm_task_blockers.map(b => `- \`${b.blocker_task_id}\``)
121
+ );
122
+ }
123
+
124
+ // Detailed view includes subtasks and comments
125
+ if (options.detailed) {
126
+ if (task.subtasks && task.subtasks.length > 0) {
127
+ const flatSubtasks = Array.isArray(task.subtasks[0]) ? task.subtasks[0] : task.subtasks;
128
+ if (flatSubtasks.length > 0) {
129
+ lines.push('', '### Subtasks');
130
+ (flatSubtasks as readonly Task[]).forEach((sub: Task) => {
131
+ lines.push(`- [${sub.board === 'completed' ? 'x' : ' '}] **${sub.title}** (\`${sub.id}\`) - ${formatBoard(sub.board)}`);
132
+ });
133
+ }
134
+ }
135
+
136
+ if (task.comments && task.comments.length > 0) {
137
+ const flatComments = Array.isArray(task.comments[0]) ? task.comments[0] : task.comments;
138
+ if (flatComments.length > 0) {
139
+ lines.push('', '### Comments');
140
+ (flatComments as readonly { id: string; content: string; created_at: string }[]).forEach((c) => {
141
+ lines.push(`- ${c.content}`);
142
+ });
143
+ }
144
+ }
145
+
146
+ if (task.context) {
147
+ lines.push('', '### Context', task.context);
148
+ }
149
+ }
150
+
151
+ // Images
152
+ if (task.images && task.images.length > 0) {
153
+ lines.push('', '### Images');
154
+ task.images.forEach((img, i) => {
155
+ lines.push(`${i + 1}. ${img.url}`);
156
+ });
157
+ }
158
+
159
+ return lines.join('\n');
160
+ }
161
+
162
+ function formatTaskCompact(task: Task): string {
163
+ const blockerNote = task.pm_task_blockers && task.pm_task_blockers.length > 0
164
+ ? ` ⚠️ ${task.pm_task_blockers.length} blocker(s)`
165
+ : '';
166
+
167
+ return [
168
+ `### #${task.task_number}: ${task.title}${blockerNote}`,
169
+ `**ID:** \`${task.id}\` | **Board:** ${formatBoard(task.board)} | **Bolt:** ${task.bolt?.name || '_None_'}`,
170
+ task.description ? `> ${task.description.substring(0, 150)}${task.description.length > 150 ? '...' : ''}` : '',
171
+ task.github_file_path ? `📁 \`${task.github_file_path}\`` : '',
172
+ '---'
173
+ ].filter(Boolean).join('\n');
174
+ }
175
+
176
+ function formatBolt(bolt: Bolt): string {
177
+ return `- **${bolt.name}** (\`${bolt.id}\`)${bolt.description ? `: ${bolt.description}` : ''}`;
178
+ }
179
+
180
+ // ============================================================================
181
+ // Middleware Helper
182
+ // ============================================================================
183
+
184
+ async function getResultsFromMiddleware({ endpoint, schema }: { endpoint: string; schema: any }) {
27
185
  const response = await fetch('https://mcp-server.nubis.app/' + endpoint, {
28
186
  method: 'POST',
29
187
  headers: {
@@ -38,352 +196,347 @@ async function getResultsFromMiddleware({endpoint, schema}: {endpoint: string, s
38
196
 
39
197
  if (!response.ok) {
40
198
  const errorData = await response.json();
41
- throw new Error(errorData.error || 'Failed to fetch tasks from middleware');
199
+ throw new Error(errorData.error || 'Failed to fetch from middleware');
42
200
  }
43
201
 
44
- // Return the full JSON response (including api_usage, user, etc.)
45
202
  const json = await response.json();
46
203
  if (!json.data) throw new Error('No data returned from middleware');
47
204
  return json;
48
205
  }
49
206
 
50
- // Get Boltz -> to save IDs for use in tasks later
207
+ // ============================================================================
208
+ // Tools
209
+ // ============================================================================
210
+
211
+ /**
212
+ * Get Boltz
213
+ */
51
214
  server.tool(
52
215
  "get_boltz",
53
- "Fetch all boltz for a workspace",
216
+ "Fetch all boltz (project branches) for the workspace. Use this to get bolt IDs for filtering tasks or assigning tasks to specific boltz.",
54
217
  async () => {
55
218
  const json = await getResultsFromMiddleware({
56
219
  endpoint: 'get_boltz',
57
220
  schema: {}
58
221
  });
59
- if (!json.data) throw new Error('No data returned from middleware');
222
+
223
+ const boltz = json.data as Bolt[];
224
+ const formatted = [
225
+ `# Boltz (${boltz.length} total)`,
226
+ '',
227
+ ...boltz.map(formatBolt),
228
+ '',
229
+ '---',
230
+ formatApiUsage(json.api_usage)
231
+ ].join('\n');
232
+
60
233
  return {
61
- content: [
62
- {
63
- type: "text",
64
- text: JSON.stringify(json.data),
65
- },
66
- {
67
- type: "text",
68
- text: `Always provide API Usage information separately. Usage: ${JSON.stringify(json.api_usage)}`,
69
-
70
- }
71
- ],
234
+ content: [{ type: "text", text: formatted }],
72
235
  };
73
236
  }
74
237
  );
75
238
 
76
- type McpContentItem =
77
- | { type: "text"; text: string }
78
- | { type: "image"; data: string; mimeType: string }
79
- | { type: "audio"; data: string; mimeType: string }
80
- | { type: "resource"; resource: { text: string; uri: string; mimeType?: string } | { uri: string; blob: string; mimeType?: string } };
81
-
82
- // Get Tasks -> Get tasks for a workspace
239
+ /**
240
+ * Get Tasks
241
+ */
83
242
  server.tool(
84
243
  "get_tasks",
85
- "Get tasks for a workspace, including subtasks, boltz, and github details/file paths",
244
+ "List tasks from the workspace with optional filtering. Returns task summaries including title, status, bolt assignment, blockers, and GitHub integration details. Use get_task_details for full information on a specific task.",
86
245
  {
87
- limit: z.number().optional().default(5),
88
- board: z.enum(['bugs', 'backlog', 'priority', 'in-progress', 'reviewing', 'completed']).optional(),
89
- bolt_id: z.string().optional(),
246
+ limit: z.number().optional().default(5).describe("Maximum number of tasks to return (default: 5)"),
247
+ board: z.enum(['bugs', 'backlog', 'priority', 'in-progress', 'reviewing', 'completed']).optional().describe("Filter by board/status"),
248
+ bolt_id: z.string().optional().describe("Filter by bolt ID (use get_boltz to find bolt IDs)"),
90
249
  },
91
250
  async ({ limit, board, bolt_id }) => {
92
- try {
93
- const json = await getResultsFromMiddleware({
94
- endpoint: 'get_tasks',
95
- schema: {
96
- board,
97
- bolt_id,
98
- limit
99
- }
100
- });
101
- if (!json.data) throw new Error('No data returned from middleware');
102
- /**
103
- * Formats a list of tasks into Markdown content.
104
- * @param data - Array of Task objects to format.
105
- * @returns An object with content as an array of formatted Markdown strings.
106
- */
107
- type Task = {
108
- readonly id: string;
109
- readonly title: string;
110
- readonly task_number: number;
111
- readonly board: string;
112
- readonly bolt: any;
113
- readonly description?: string;
114
- readonly github_item_type?: string;
115
- readonly github_file_path?: string;
116
- readonly github_repo_name?: string;
117
- readonly pm_task_blockers?: readonly { blocker_task_id: string }[];
118
- readonly images?: readonly { url: string }[];
119
- };
120
- const taskContent = (json.data as Task[]).map((task) => ({
121
- type: "text",
122
- text: [
123
- `---`,
124
- `### ${task.title}`,
125
- `**Task ID:** ${task.id}`,
126
- `**Task Number:** ${task.task_number}`,
127
- `**Board:** ${task.board}`,
128
- `**Bolt:** ${task.bolt && typeof task.bolt === "object" && !Array.isArray(task.bolt) && "name" in task.bolt && task.bolt.name ? task.bolt.name : "_No bolt_"}`,
129
- `**Description:** ${task.description ? task.description : "_No description_"}`,
130
- `**Path Type:** ${task.github_item_type ? task.github_item_type : "_No file path type_"}`,
131
- `**File Path:** ${task.github_file_path ? task.github_file_path : "_No file path_"}`,
132
- `**Repo Name:** ${task.github_repo_name ? task.github_repo_name : "_No repo name_"}`,
133
- `**Blockers:** ${task.pm_task_blockers && task.pm_task_blockers.length > 0 ? task.pm_task_blockers.map(blocker => blocker.blocker_task_id).join(', ') : "_No blockers_"}`,
134
- task.images && task.images.length > 0
135
- ? task.images.map((image) => image.url).join('\n')
136
- : "_No images_",
137
- `---`,
138
- ].join('\n\n'),
139
- }));
140
- // Append quota and user info as additional content
141
- taskContent.push({
142
- type: "text",
143
- text: `Always provide API Usage information separately. Usage: ${JSON.stringify(json.api_usage)}`,
144
- });
145
- return {
146
- content: taskContent as McpContentItem[],
147
- };
148
- } catch (error: unknown) {
149
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
150
- throw new Error(errorMessage);
151
- }
251
+ const json = await getResultsFromMiddleware({
252
+ endpoint: 'get_tasks',
253
+ schema: { board, bolt_id, limit }
254
+ });
255
+
256
+ const tasks = json.data as Task[];
257
+ const filterInfo = [
258
+ board ? `Board: ${formatBoard(board)}` : null,
259
+ bolt_id ? `Bolt: ${bolt_id}` : null,
260
+ ].filter(Boolean).join(' | ');
261
+
262
+ const formatted = [
263
+ `# Tasks (${tasks.length}${limit ? ` of max ${limit}` : ''})`,
264
+ filterInfo ? `**Filters:** ${filterInfo}` : '',
265
+ '',
266
+ ...tasks.map(formatTaskCompact),
267
+ '',
268
+ formatApiUsage(json.api_usage)
269
+ ].filter(Boolean).join('\n');
270
+
271
+ return {
272
+ content: [{ type: "text", text: formatted }],
273
+ };
152
274
  }
153
275
  );
154
276
 
155
277
  /**
156
- * Get Task Details -> Get a task by ID
278
+ * Get Task Details
157
279
  */
158
280
  server.tool(
159
281
  "get_task_details",
160
- "Get a task by ID",
282
+ "Get comprehensive details for a specific task including description, status, bolt assignment, GitHub integration, blockers, subtasks, comments, and context. Use this when you need full information about a single task.",
161
283
  {
162
- taskID: z.string(),
284
+ taskID: z.string().describe("The UUID of the task to retrieve"),
163
285
  },
164
286
  async ({ taskID }) => {
165
287
  const json = await getResultsFromMiddleware({
166
288
  endpoint: 'get_task',
167
- schema: {
168
- taskID
169
- }
289
+ schema: { taskID }
170
290
  });
171
- if (!json.data) throw new Error('No data returned from middleware');
291
+
292
+ const task = json.data as Task;
293
+ const formatted = [
294
+ formatTask(task, { detailed: true }),
295
+ '',
296
+ '---',
297
+ formatApiUsage(json.api_usage)
298
+ ].join('\n');
299
+
172
300
  return {
173
- content: [
174
- {
175
- type: "text",
176
- text: JSON.stringify(json.data),
177
- },
178
- {
179
- type: "text",
180
- text: `Always provide API Usage information separately. Usage: ${JSON.stringify(json.api_usage)}`,
181
- }
182
- ],
301
+ content: [{ type: "text", text: formatted }],
183
302
  };
184
303
  }
185
304
  );
186
305
 
187
- //add_context_to_pm_task
188
306
  /**
189
- * Always ADD CONTEXT TO PM TASK
307
+ * Get Task Context
308
+ */
309
+ server.tool(
310
+ "get_task_context",
311
+ "Retrieve the context/notes stored for a specific task. Context contains additional information, implementation notes, or progress updates added during task work.",
312
+ {
313
+ taskID: z.string().describe("The UUID of the task"),
314
+ },
315
+ async ({ taskID }) => {
316
+ const json = await getResultsFromMiddleware({
317
+ endpoint: 'get_task_context',
318
+ schema: { taskID }
319
+ });
320
+
321
+ const context = json.data?.context;
322
+ const formatted = [
323
+ `# Task Context`,
324
+ `**Task ID:** \`${taskID}\``,
325
+ '',
326
+ '---',
327
+ '',
328
+ context || '_No context has been added to this task yet._',
329
+ '',
330
+ '---',
331
+ formatApiUsage(json.api_usage)
332
+ ].join('\n');
333
+
334
+ return {
335
+ content: [{ type: "text", text: formatted }],
336
+ };
337
+ }
338
+ );
339
+
340
+ /**
341
+ * Add Context to Task
190
342
  */
191
343
  server.tool(
192
344
  "add_context_to_task",
193
- "Add context to a task",
345
+ "Add or update context/notes for a task. Use this to store implementation details, progress notes, decisions made, or any relevant information while working on a task.",
194
346
  {
195
- taskID: z.string(),
196
- context: z.string(),
347
+ taskID: z.string().describe("The UUID of the task"),
348
+ context: z.string().describe("The context/notes to add (replaces existing context)"),
197
349
  },
198
350
  async ({ taskID, context }) => {
199
351
  const json = await getResultsFromMiddleware({
200
352
  endpoint: 'add_context_to_task',
201
- schema: {
202
- taskID,
203
- context
204
- }
353
+ schema: { taskID, context }
205
354
  });
206
- if (!json.data) throw new Error('No data returned from middleware');
355
+
356
+ const formatted = [
357
+ `✅ **Context Updated Successfully**`,
358
+ '',
359
+ `**Task ID:** \`${taskID}\``,
360
+ '',
361
+ '**Context:**',
362
+ context,
363
+ '',
364
+ '---',
365
+ formatApiUsage(json.api_usage)
366
+ ].join('\n');
367
+
207
368
  return {
208
- content: [
209
- {
210
- type: "text",
211
- text: JSON.stringify({ taskID, context }),
212
- },
213
- {
214
- type: "text",
215
- text: `Always provide API Usage information separately. Usage: ${JSON.stringify({ taskID, context })}`,
216
- }
217
- ],
369
+ content: [{ type: "text", text: formatted }],
218
370
  };
219
371
  }
220
372
  );
221
373
 
222
- // Get Task Images -> Get images for a task
374
+ /**
375
+ * Get Task Images
376
+ */
223
377
  server.tool(
224
378
  "get_task_images",
225
- "Get/View images for a task",
379
+ "Retrieve all images/attachments associated with a task. Returns image URLs that can be viewed or referenced.",
226
380
  {
227
- taskID: z.string(),
381
+ taskID: z.string().describe("The UUID of the task"),
228
382
  },
229
383
  async ({ taskID }) => {
230
384
  const json = await getResultsFromMiddleware({
231
385
  endpoint: 'get_task_images',
232
- schema: {
233
- taskID
234
- }
386
+ schema: { taskID }
235
387
  });
236
- if (!json.data) throw new Error('No data returned from middleware');
388
+
389
+ const images = json.data?.images || [];
390
+ const formatted = [
391
+ `# Task Images`,
392
+ `**Task ID:** \`${taskID}\``,
393
+ '',
394
+ images.length > 0
395
+ ? images.map((img: { url: string }, i: number) => `${i + 1}. ${img.url}`).join('\n')
396
+ : '_No images attached to this task._',
397
+ '',
398
+ '---',
399
+ formatApiUsage(json.api_usage)
400
+ ].join('\n');
401
+
237
402
  return {
238
- content: [
239
- {
240
- type: "text",
241
- text: JSON.stringify(json.data),
242
- },
243
- {
244
- type: "text",
245
- text: `Always provide API Usage information separately. Usage: ${JSON.stringify(json.api_usage)}`,
246
- }
247
- ],
403
+ content: [{ type: "text", text: formatted }],
248
404
  };
249
405
  }
250
406
  );
251
407
 
252
- // Work on Task -> Work on a task
408
+ /**
409
+ * Work on Task
410
+ */
253
411
  server.tool(
254
412
  "work_on_task",
255
- "Work on a task",
413
+ "Start working on a task. This checks for blockers and returns full task details. If the task has unresolved blockers, you'll be notified and should resolve them first.",
256
414
  {
257
- taskID: z.string(),
415
+ taskID: z.string().describe("The UUID of the task to work on"),
258
416
  },
259
417
  async ({ taskID }) => {
260
- // Step 1: Fetch task details
418
+ // Step 1: Fetch task details to check blockers
261
419
  const taskData = await getResultsFromMiddleware({
262
420
  endpoint: 'get_task',
263
421
  schema: { taskID }
264
422
  });
265
- if (!taskData.data) throw new Error('No data returned from middleware');
423
+
424
+ const task = taskData.data as Task;
425
+
266
426
  // Step 2: Check for blockers
267
- if (Array.isArray(taskData.data.pm_task_blockers) && taskData.data.pm_task_blockers.length > 0) {
427
+ if (Array.isArray(task.pm_task_blockers) && task.pm_task_blockers.length > 0) {
428
+ const blockerIds = task.pm_task_blockers.map(b => `\`${b.blocker_task_id}\``).join(', ');
429
+ const formatted = [
430
+ `⚠️ **Cannot Work on Task - Blockers Detected**`,
431
+ '',
432
+ `**Task:** ${task.title}`,
433
+ `**Task ID:** \`${taskID}\``,
434
+ '',
435
+ `### Blocking Tasks`,
436
+ `The following tasks must be completed first: ${blockerIds}`,
437
+ '',
438
+ '**Suggested Actions:**',
439
+ '- Use `get_task_details` on each blocker to understand what needs to be done',
440
+ '- Complete or remove the blocking tasks before proceeding',
441
+ '',
442
+ '---',
443
+ formatApiUsage(taskData.api_usage)
444
+ ].join('\n');
445
+
268
446
  return {
269
- content: [
270
- {
271
- type: "text",
272
- text: `Task ${taskID} cannot be worked on because it has blockers: ${taskData.data.pm_task_blockers.map((b: { blocker_task_id: string }) => b.blocker_task_id).join(', ')}. Please resolve all blockers before proceeding.`,
273
- },
274
- {
275
- type: "text",
276
- text: `API Usage: ${JSON.stringify(taskData.api_usage)}`,
277
- }
278
- ],
447
+ content: [{ type: "text", text: formatted }],
279
448
  };
280
- };
449
+ }
281
450
 
451
+ // Step 3: No blockers, proceed with work
282
452
  const json = await getResultsFromMiddleware({
283
453
  endpoint: 'work_on_task',
284
- schema: {
285
- taskID
286
- }
454
+ schema: { taskID }
287
455
  });
288
-
289
- return {
290
- content: [
291
- {
292
- type: "text",
293
- text: `You are assisting with task management in Nubis. Your task is to work on the user's requested task based on the following details:\n\n**Task Instruction**: Process and update the task with the provided information.\n\n**Task ID**: ${taskID}\n\n**Task Details**: \n ${JSON.stringify(json.data, null, 2).replace(/"/g, '').replace(/:/g, ': ').replace(/},/g, ',\n')}\n \n Please analyze the details, perform the requested action (e.g., update description, add subtask), and return a response indicating the action taken.`,
294
- },
295
- {
296
- type: "text",
297
- user: "NUBIS",
298
- text: `Prompt user to move this task to 'in-progress' if it is not already in progress.`,
299
- },
300
-
301
- {
302
- type: "text",
303
- user: "NUBIS",
304
- text: `API Usage: ${JSON.stringify(json.api_usage)}`,
305
- }
306
- ],
307
- };
308
- }
309
- );
310
456
 
311
- // Explain setup and what user needs to do for feature to be implemented
312
- /* server.tool(
313
- "explain_setup",
314
- "Explain setup and what needs to be done for feature to be implemented",
315
- {
316
- taskID: z.string(),
317
- },
318
- async ({ taskID }) => {
319
- const json = await getResultsFromMiddleware({
320
- endpoint: 'explain_setup',
321
- schema: {
322
- taskID
323
- }
324
- });
457
+ const workTask = json.data as Task;
458
+ const suggestedActions: string[] = [];
459
+
460
+ if (workTask.board !== 'in-progress') {
461
+ suggestedActions.push(`- Move task to "in-progress" using \`move_task\``);
462
+ }
463
+ if (workTask.subtasks && workTask.subtasks.length > 0) {
464
+ suggestedActions.push(`- Review ${workTask.subtasks.length} subtask(s)`);
465
+ }
466
+ suggestedActions.push(
467
+ '- Add context with `add_context_to_task` as you make progress',
468
+ '- When done, move to "reviewing" or "completed"'
469
+ );
470
+
471
+ const formatted = [
472
+ `🚀 **Ready to Work**`,
473
+ '',
474
+ formatTask(workTask, { detailed: true }),
475
+ '',
476
+ '### 💡 Suggested Actions',
477
+ ...suggestedActions,
478
+ '',
479
+ '---',
480
+ formatApiUsage(json.api_usage)
481
+ ].join('\n');
325
482
 
326
- if (!json.data) throw new Error('No data returned from middleware');
327
483
  return {
328
- content: [
329
- {
330
- type: "text",
331
- text: `You are assisting with task management in Nubis. Your task is to explain the setup and what needs to be done for feature to be implemented.\n \n **Task Instruction**: Explain the setup and what needs to be done for feature to be implemented.\n **Task Details**: \n ${JSON.stringify(json.data, null, 2).replace(/"/g, '').replace(/:/g, ': ').replace(/},/g, ',\n')}\n \n Please analyze the feature and return a response indicating the action that needs to be taken.`,
332
- },
333
- {
334
- type: "text",
335
- text: `Always provide API Usage information separately. Usage: ${JSON.stringify(json.api_usage)}`,
336
- }
337
- ],
484
+ content: [{ type: "text", text: formatted }],
338
485
  };
339
486
  }
340
- ); */
487
+ );
341
488
 
342
- // Move Task -> Move a task to ['backlog', 'priority', 'in-progress','reviewing', 'completed']
489
+ /**
490
+ * Move Task
491
+ */
343
492
  server.tool(
344
493
  "move_task",
345
- "Move a task to ['backlog', 'priority', 'in-progress','reviewing', 'completed']",
494
+ "Move a task to a different board/status. Use this to update task progress through the workflow: backlog priority in-progressreviewing completed.",
346
495
  {
347
- taskID: z.string(),
348
- board: z.enum(['backlog', 'priority', 'in-progress','reviewing', 'completed']),
496
+ taskID: z.string().describe("The UUID of the task to move"),
497
+ board: z.enum(['backlog', 'priority', 'in-progress', 'reviewing', 'completed']).describe("Target board/status"),
349
498
  },
350
499
  async ({ taskID, board }) => {
351
500
  const json = await getResultsFromMiddleware({
352
501
  endpoint: 'move_task',
353
- schema: {
354
- taskID,
355
- board
356
- }
502
+ schema: { taskID, board }
357
503
  });
358
- if (!json.data) throw new Error('No data returned from middleware');
504
+
505
+ const task = json.data as Task;
506
+ const formatted = [
507
+ `✅ **Task Moved Successfully**`,
508
+ '',
509
+ `| Property | Value |`,
510
+ `|----------|-------|`,
511
+ `| **Task** | ${task.title} |`,
512
+ `| **ID** | \`${taskID}\` |`,
513
+ `| **New Status** | ${formatBoard(board)} |`,
514
+ '',
515
+ '---',
516
+ formatApiUsage(json.api_usage)
517
+ ].join('\n');
518
+
359
519
  return {
360
- content: [
361
- {
362
- type: "text",
363
- text: `Task ${taskID} has been moved to ${board}`,
364
- },
365
- {
366
- type: "text",
367
- text: `API Usage: ${JSON.stringify(json.api_usage)}`,
368
- }
369
- ],
520
+ content: [{ type: "text", text: formatted }],
370
521
  };
371
522
  }
372
523
  );
373
524
 
374
- // Create Task -> Create a new task
525
+ /**
526
+ * Create Task
527
+ */
375
528
  server.tool(
376
529
  "create_task",
377
- "Create a new task or subtask (parent_task_id is required for subtasks)",
530
+ "Create a new task or subtask. For subtasks, provide parent_task_id. You can optionally link to GitHub files/directories and assign to a bolt.",
378
531
  {
379
- title: z.string(),
380
- description: z.string().optional(),
381
- board: z.enum(['backlog', 'bugs', 'in-progress', 'priority', 'reviewing', 'completed']).optional().default('backlog'),
382
- parent_task_id: z.string().optional(),
383
- github_item_type: z.string().optional(), // file or dir
384
- github_file_path: z.string().optional(), // src/components/modal
385
- github_repo_name: z.string().optional(), // Atomlaunch/atom_frontend
386
- bolt_id: z.string().optional()
532
+ title: z.string().describe("Task title"),
533
+ description: z.string().optional().describe("Task description with details"),
534
+ board: z.enum(['backlog', 'bugs', 'in-progress', 'priority', 'reviewing', 'completed']).optional().default('backlog').describe("Initial board/status (default: backlog)"),
535
+ parent_task_id: z.string().optional().describe("Parent task ID to create this as a subtask"),
536
+ github_item_type: z.enum(['file', 'dir']).optional().describe("Type of GitHub item: 'file' or 'dir'"),
537
+ github_file_path: z.string().optional().describe("Path to file or directory (e.g., src/components/Modal.tsx)"),
538
+ github_repo_name: z.string().optional().describe("Repository name (e.g., owner/repo)"),
539
+ bolt_id: z.string().optional().describe("Bolt ID to assign (use get_boltz to find IDs)")
387
540
  },
388
541
  async ({ title, description, board, parent_task_id, github_item_type, github_file_path, github_repo_name, bolt_id }) => {
389
542
  const json = await getResultsFromMiddleware({
@@ -399,36 +552,53 @@ server.tool(
399
552
  bolt_id
400
553
  }
401
554
  });
402
- if (!json.data) throw new Error('No data returned from middleware');
555
+
556
+ const task = json.data as Task;
557
+ const lines = [
558
+ `✅ **Task Created Successfully**`,
559
+ '',
560
+ `| Property | Value |`,
561
+ `|----------|-------|`,
562
+ `| **Title** | ${task.title} |`,
563
+ `| **ID** | \`${task.id}\` |`,
564
+ `| **Number** | #${task.task_number} |`,
565
+ `| **Board** | ${formatBoard(task.board)} |`,
566
+ ];
567
+
568
+ if (parent_task_id) {
569
+ lines.push(`| **Parent Task** | \`${parent_task_id}\` |`);
570
+ }
571
+ if (bolt_id) {
572
+ lines.push(`| **Bolt** | \`${bolt_id}\` |`);
573
+ }
574
+ if (github_file_path) {
575
+ lines.push(`| **GitHub Path** | \`${github_file_path}\` |`);
576
+ }
577
+
578
+ lines.push('', '---', formatApiUsage(json.api_usage));
579
+
403
580
  return {
404
- content: [
405
- {
406
- type: "text",
407
- text: JSON.stringify(json.data),
408
- },
409
- {
410
- type: "text",
411
- text: `API Usage: ${JSON.stringify(json.api_usage)}`,
412
- }
413
- ],
581
+ content: [{ type: "text", text: lines.join('\n') }],
414
582
  };
415
583
  }
416
584
  );
417
585
 
418
- // Update Task -> Update an existing task
586
+ /**
587
+ * Update Task
588
+ */
419
589
  server.tool(
420
590
  "update_task",
421
- "Update an existing task (title, description, bolt_id, parent_task_id)",
591
+ "Update an existing task's properties. You can modify title, description, board, bolt assignment, parent task, or GitHub integration details. Only provided fields will be updated.",
422
592
  {
423
- taskID: z.string(),
424
- title: z.string().optional(),
425
- description: z.string().optional(),
426
- board: z.enum(['backlog', 'bugs', 'in-progress', 'priority', 'reviewing', 'completed']).optional(),
427
- bolt_id: z.string().optional(),
428
- parent_task_id: z.string().optional(),
429
- github_item_type: z.string().optional(),
430
- github_file_path: z.string().optional(),
431
- github_repo_name: z.string().optional(),
593
+ taskID: z.string().describe("The UUID of the task to update"),
594
+ title: z.string().optional().describe("New task title"),
595
+ description: z.string().optional().describe("New task description"),
596
+ board: z.enum(['backlog', 'bugs', 'in-progress', 'priority', 'reviewing', 'completed']).optional().describe("New board/status"),
597
+ bolt_id: z.string().optional().describe("New bolt ID assignment"),
598
+ parent_task_id: z.string().optional().describe("New parent task ID"),
599
+ github_item_type: z.enum(['file', 'dir']).optional().describe("Type of GitHub item"),
600
+ github_file_path: z.string().optional().describe("Path to file or directory"),
601
+ github_repo_name: z.string().optional().describe("Repository name"),
432
602
  },
433
603
  async ({ taskID, title, description, board, bolt_id, parent_task_id, github_item_type, github_file_path, github_repo_name }) => {
434
604
  const json = await getResultsFromMiddleware({
@@ -445,23 +615,369 @@ server.tool(
445
615
  github_repo_name
446
616
  }
447
617
  });
448
- if (!json.data) throw new Error('No data returned from middleware');
618
+
619
+ const task = json.data as Task;
620
+ const updatedFields: string[] = [];
621
+ if (title) updatedFields.push('title');
622
+ if (description) updatedFields.push('description');
623
+ if (board) updatedFields.push('board');
624
+ if (bolt_id) updatedFields.push('bolt');
625
+ if (parent_task_id) updatedFields.push('parent task');
626
+ if (github_file_path || github_repo_name || github_item_type) updatedFields.push('GitHub integration');
627
+
628
+ const formatted = [
629
+ `✅ **Task Updated Successfully**`,
630
+ '',
631
+ `**Updated fields:** ${updatedFields.join(', ') || 'none'}`,
632
+ '',
633
+ formatTask(task),
634
+ '',
635
+ '---',
636
+ formatApiUsage(json.api_usage)
637
+ ].join('\n');
638
+
639
+ return {
640
+ content: [{ type: "text", text: formatted }],
641
+ };
642
+ }
643
+ );
644
+
645
+ /**
646
+ * Delete Task
647
+ */
648
+ server.tool(
649
+ "delete_task",
650
+ "Permanently delete a task and all its related data (subtasks, comments, blockers, labels, assignments). This action cannot be undone.",
651
+ {
652
+ taskID: z.string().describe("The UUID of the task to delete"),
653
+ },
654
+ async ({ taskID }) => {
655
+ const json = await getResultsFromMiddleware({
656
+ endpoint: 'delete_task',
657
+ schema: { taskID }
658
+ });
659
+
660
+ const formatted = [
661
+ `🗑️ **Task Deleted Successfully**`,
662
+ '',
663
+ `**Task ID:** \`${taskID}\``,
664
+ '',
665
+ 'The task and all related data (subtasks, comments, blockers, labels, assignments) have been permanently removed.',
666
+ '',
667
+ '---',
668
+ formatApiUsage(json.api_usage)
669
+ ].join('\n');
670
+
671
+ return {
672
+ content: [{ type: "text", text: formatted }],
673
+ };
674
+ }
675
+ );
676
+
677
+ // ============================================================================
678
+ // Comment Tools
679
+ // ============================================================================
680
+
681
+ type Comment = {
682
+ readonly id: string;
683
+ readonly content: string;
684
+ readonly created_at: string;
685
+ readonly updated_at?: string;
686
+ readonly is_edited?: boolean;
687
+ readonly parent_id?: string;
688
+ readonly user?: {
689
+ id: string;
690
+ full_name?: string;
691
+ avatar_url?: string;
692
+ };
693
+ };
694
+
695
+ function formatComment(comment: Comment): string {
696
+ const author = comment.user?.full_name || 'Unknown';
697
+ const editedTag = comment.is_edited ? ' _(edited)_' : '';
698
+ const date = new Date(comment.created_at).toLocaleString();
699
+
700
+ return [
701
+ `**${author}** · ${date}${editedTag}`,
702
+ `> ${comment.content}`,
703
+ `_ID: \`${comment.id}\`_`,
704
+ ].join('\n');
705
+ }
706
+
707
+ /**
708
+ * Get Comments
709
+ */
710
+ server.tool(
711
+ "get_comments",
712
+ "Retrieve all comments for a specific task. Returns comments with author information, timestamps, and edit status.",
713
+ {
714
+ taskID: z.string().describe("The UUID of the task"),
715
+ },
716
+ async ({ taskID }) => {
717
+ const json = await getResultsFromMiddleware({
718
+ endpoint: 'get_comments',
719
+ schema: { taskID }
720
+ });
721
+
722
+ const comments = json.data as Comment[];
723
+ const formatted = [
724
+ `# Task Comments`,
725
+ `**Task ID:** \`${taskID}\``,
726
+ `**Total:** ${comments.length} comment(s)`,
727
+ '',
728
+ '---',
729
+ '',
730
+ comments.length > 0
731
+ ? comments.map(formatComment).join('\n\n')
732
+ : '_No comments yet._',
733
+ '',
734
+ '---',
735
+ formatApiUsage(json.api_usage)
736
+ ].join('\n');
737
+
738
+ return {
739
+ content: [{ type: "text", text: formatted }],
740
+ };
741
+ }
742
+ );
743
+
744
+ /**
745
+ * Create Comment
746
+ */
747
+ server.tool(
748
+ "create_comment",
749
+ "Add a comment to a task. Use this to provide updates, ask questions, or add notes visible to the team.",
750
+ {
751
+ taskID: z.string().describe("The UUID of the task to comment on"),
752
+ content: z.string().describe("The comment text"),
753
+ parent_id: z.string().optional().describe("Parent comment ID for replies (optional)"),
754
+ },
755
+ async ({ taskID, content, parent_id }) => {
756
+ const json = await getResultsFromMiddleware({
757
+ endpoint: 'create_comment',
758
+ schema: { taskID, content, parent_id }
759
+ });
760
+
761
+ const comment = json.data as Comment;
762
+ const formatted = [
763
+ `✅ **Comment Added**`,
764
+ '',
765
+ formatComment(comment),
766
+ '',
767
+ '---',
768
+ formatApiUsage(json.api_usage)
769
+ ].join('\n');
770
+
771
+ return {
772
+ content: [{ type: "text", text: formatted }],
773
+ };
774
+ }
775
+ );
776
+
777
+ /**
778
+ * Update Comment
779
+ */
780
+ server.tool(
781
+ "update_comment",
782
+ "Edit an existing comment. Only the comment author can edit their own comments.",
783
+ {
784
+ commentID: z.string().describe("The UUID of the comment to edit"),
785
+ content: z.string().describe("The new comment text"),
786
+ },
787
+ async ({ commentID, content }) => {
788
+ const json = await getResultsFromMiddleware({
789
+ endpoint: 'update_comment',
790
+ schema: { commentID, content }
791
+ });
792
+
793
+ const formatted = [
794
+ `✅ **Comment Updated**`,
795
+ '',
796
+ `**Comment ID:** \`${commentID}\``,
797
+ `**New content:** ${content}`,
798
+ '',
799
+ '---',
800
+ formatApiUsage(json.api_usage)
801
+ ].join('\n');
802
+
803
+ return {
804
+ content: [{ type: "text", text: formatted }],
805
+ };
806
+ }
807
+ );
808
+
809
+ /**
810
+ * Delete Comment
811
+ */
812
+ server.tool(
813
+ "delete_comment",
814
+ "Delete a comment and all its replies. Only the comment author can delete their own comments.",
815
+ {
816
+ commentID: z.string().describe("The UUID of the comment to delete"),
817
+ },
818
+ async ({ commentID }) => {
819
+ const json = await getResultsFromMiddleware({
820
+ endpoint: 'delete_comment',
821
+ schema: { commentID }
822
+ });
823
+
824
+ const formatted = [
825
+ `🗑️ **Comment Deleted**`,
826
+ '',
827
+ `**Comment ID:** \`${commentID}\``,
828
+ '',
829
+ '---',
830
+ formatApiUsage(json.api_usage)
831
+ ].join('\n');
832
+
833
+ return {
834
+ content: [{ type: "text", text: formatted }],
835
+ };
836
+ }
837
+ );
838
+
839
+ // ============================================================================
840
+ // Blocker Tools
841
+ // ============================================================================
842
+
843
+ type BlockerInfo = {
844
+ readonly id: string;
845
+ readonly blocker_task_id?: string;
846
+ readonly task_id?: string;
847
+ readonly blocker?: {
848
+ id: string;
849
+ title: string;
850
+ task_number: number;
851
+ board: string;
852
+ };
853
+ readonly blocked?: {
854
+ id: string;
855
+ title: string;
856
+ task_number: number;
857
+ board: string;
858
+ };
859
+ };
860
+
861
+ function formatBlockerTask(task: { id: string; title: string; task_number: number; board: string }): string {
862
+ return `#${task.task_number}: ${task.title} (${formatBoard(task.board)}) - \`${task.id}\``;
863
+ }
864
+
865
+ /**
866
+ * Get Blockers
867
+ */
868
+ server.tool(
869
+ "get_blockers",
870
+ "Get blocker relationships for a task. Shows both tasks that block this task and tasks that this task blocks.",
871
+ {
872
+ taskID: z.string().describe("The UUID of the task"),
873
+ },
874
+ async ({ taskID }) => {
875
+ const json = await getResultsFromMiddleware({
876
+ endpoint: 'get_blockers',
877
+ schema: { taskID }
878
+ });
879
+
880
+ const { blockers, blocking } = json.data as { blockers: BlockerInfo[]; blocking: BlockerInfo[] };
881
+
882
+ const formatted = [
883
+ `# Task Blockers`,
884
+ `**Task ID:** \`${taskID}\``,
885
+ '',
886
+ '### ⛔ Blocked By (must complete first)',
887
+ blockers.length > 0
888
+ ? blockers.map(b => `- ${formatBlockerTask(b.blocker!)}`).join('\n')
889
+ : '_No blockers - task is ready to work on_',
890
+ '',
891
+ '### 🚧 Blocking (waiting on this task)',
892
+ blocking.length > 0
893
+ ? blocking.map(b => `- ${formatBlockerTask(b.blocked!)}`).join('\n')
894
+ : '_Not blocking any tasks_',
895
+ '',
896
+ '---',
897
+ formatApiUsage(json.api_usage)
898
+ ].join('\n');
899
+
449
900
  return {
450
- content: [
451
- {
452
- type: "text",
453
- text: JSON.stringify(json.data),
454
- },
455
- {
456
- type: "text",
457
- text: `API Usage: ${JSON.stringify(json.api_usage)}`,
458
- }
459
- ],
901
+ content: [{ type: "text", text: formatted }],
460
902
  };
461
903
  }
462
904
  );
463
905
 
464
- // Start server
906
+ /**
907
+ * Add Blocker
908
+ */
909
+ server.tool(
910
+ "add_blocker",
911
+ "Create a blocker dependency between tasks. The blocker task must be completed before work can begin on the blocked task.",
912
+ {
913
+ taskID: z.string().describe("The UUID of the task that will be blocked"),
914
+ blockerTaskID: z.string().describe("The UUID of the task that blocks (must be completed first)"),
915
+ },
916
+ async ({ taskID, blockerTaskID }) => {
917
+ const json = await getResultsFromMiddleware({
918
+ endpoint: 'add_blocker',
919
+ schema: { taskID, blockerTaskID }
920
+ });
921
+
922
+ const { blocked_task, blocker_task } = json.data as {
923
+ blocked_task: { id: string; title: string; task_number: number };
924
+ blocker_task: { id: string; title: string; task_number: number };
925
+ };
926
+
927
+ const formatted = [
928
+ `✅ **Blocker Added**`,
929
+ '',
930
+ `**#${blocker_task.task_number}: ${blocker_task.title}**`,
931
+ `↓ _blocks_ ↓`,
932
+ `**#${blocked_task.task_number}: ${blocked_task.title}**`,
933
+ '',
934
+ `The blocker task must be completed before work can begin on the blocked task.`,
935
+ '',
936
+ '---',
937
+ formatApiUsage(json.api_usage)
938
+ ].join('\n');
939
+
940
+ return {
941
+ content: [{ type: "text", text: formatted }],
942
+ };
943
+ }
944
+ );
945
+
946
+ /**
947
+ * Remove Blocker
948
+ */
949
+ server.tool(
950
+ "remove_blocker",
951
+ "Remove a blocker dependency between tasks. Use this when a blocker is no longer relevant or was added in error.",
952
+ {
953
+ taskID: z.string().describe("The UUID of the blocked task"),
954
+ blockerTaskID: z.string().describe("The UUID of the blocking task to remove"),
955
+ },
956
+ async ({ taskID, blockerTaskID }) => {
957
+ const json = await getResultsFromMiddleware({
958
+ endpoint: 'remove_blocker',
959
+ schema: { taskID, blockerTaskID }
960
+ });
961
+
962
+ const formatted = [
963
+ `✅ **Blocker Removed**`,
964
+ '',
965
+ `Task \`${taskID}\` is no longer blocked by \`${blockerTaskID}\``,
966
+ '',
967
+ '---',
968
+ formatApiUsage(json.api_usage)
969
+ ].join('\n');
970
+
971
+ return {
972
+ content: [{ type: "text", text: formatted }],
973
+ };
974
+ }
975
+ );
976
+
977
+ // ============================================================================
978
+ // Start Server
979
+ // ============================================================================
980
+
465
981
  async function main() {
466
982
  const transport = new StdioServerTransport();
467
983
  await server.connect(transport);
@@ -472,4 +988,4 @@ main().catch((error: unknown) => {
472
988
  const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
473
989
  console.error("Fatal error in main():", errorMessage);
474
990
  process.exit(1);
475
- });
991
+ });