@lil2good/nubis-mcp-server 1.0.52 → 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 +567 -274
  2. package/package.json +1 -1
  3. package/src/index.ts +778 -294
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,384 +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: `API Usage: ${JSON.stringify(json.api_usage)}`,
181
- }
182
- ],
301
+ content: [{ type: "text", text: formatted }],
183
302
  };
184
303
  }
185
304
  );
186
305
 
187
306
  /**
188
- * Get Task Context -> Get context for a task
307
+ * Get Task Context
189
308
  */
190
309
  server.tool(
191
310
  "get_task_context",
192
- "Get context for a task",
311
+ "Retrieve the context/notes stored for a specific task. Context contains additional information, implementation notes, or progress updates added during task work.",
193
312
  {
194
- taskID: z.string(),
313
+ taskID: z.string().describe("The UUID of the task"),
195
314
  },
196
315
  async ({ taskID }) => {
197
316
  const json = await getResultsFromMiddleware({
198
317
  endpoint: 'get_task_context',
199
- schema: {
200
- taskID
201
- }
318
+ schema: { taskID }
202
319
  });
203
- if (!json.data) throw new Error('No data returned from middleware');
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
+
204
334
  return {
205
- content: [
206
- {
207
- type: "text",
208
- text: JSON.stringify(json.data),
209
- },
210
- {
211
- type: "text",
212
- text: `API Usage: ${JSON.stringify(json.api_usage)}`,
213
- }
214
- ],
335
+ content: [{ type: "text", text: formatted }],
215
336
  };
216
337
  }
217
338
  );
218
339
 
219
- //add_context_to_pm_task
220
340
  /**
221
- * Always ADD CONTEXT TO PM TASK
341
+ * Add Context to Task
222
342
  */
223
343
  server.tool(
224
344
  "add_context_to_task",
225
- "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.",
226
346
  {
227
- taskID: z.string(),
228
- 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)"),
229
349
  },
230
350
  async ({ taskID, context }) => {
231
351
  const json = await getResultsFromMiddleware({
232
352
  endpoint: 'add_context_to_task',
233
- schema: {
234
- taskID,
235
- context
236
- }
353
+ schema: { taskID, context }
237
354
  });
238
- 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
+
239
368
  return {
240
- content: [
241
- {
242
- type: "text",
243
- text: JSON.stringify({ taskID, context }),
244
- },
245
- {
246
- type: "text",
247
- text: `API Usage: ${JSON.stringify({ taskID, context })}`,
248
- }
249
- ],
369
+ content: [{ type: "text", text: formatted }],
250
370
  };
251
371
  }
252
372
  );
253
373
 
254
- // Get Task Images -> Get images for a task
374
+ /**
375
+ * Get Task Images
376
+ */
255
377
  server.tool(
256
378
  "get_task_images",
257
- "Get/View images for a task",
379
+ "Retrieve all images/attachments associated with a task. Returns image URLs that can be viewed or referenced.",
258
380
  {
259
- taskID: z.string(),
381
+ taskID: z.string().describe("The UUID of the task"),
260
382
  },
261
383
  async ({ taskID }) => {
262
384
  const json = await getResultsFromMiddleware({
263
385
  endpoint: 'get_task_images',
264
- schema: {
265
- taskID
266
- }
386
+ schema: { taskID }
267
387
  });
268
- 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
+
269
402
  return {
270
- content: [
271
- {
272
- type: "text",
273
- text: JSON.stringify(json.data),
274
- },
275
- {
276
- type: "text",
277
- text: `API Usage: ${JSON.stringify(json.api_usage)}`,
278
- }
279
- ],
403
+ content: [{ type: "text", text: formatted }],
280
404
  };
281
405
  }
282
406
  );
283
407
 
284
- // Work on Task -> Work on a task
408
+ /**
409
+ * Work on Task
410
+ */
285
411
  server.tool(
286
412
  "work_on_task",
287
- "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.",
288
414
  {
289
- taskID: z.string(),
415
+ taskID: z.string().describe("The UUID of the task to work on"),
290
416
  },
291
417
  async ({ taskID }) => {
292
- // Step 1: Fetch task details
418
+ // Step 1: Fetch task details to check blockers
293
419
  const taskData = await getResultsFromMiddleware({
294
420
  endpoint: 'get_task',
295
421
  schema: { taskID }
296
422
  });
297
- if (!taskData.data) throw new Error('No data returned from middleware');
423
+
424
+ const task = taskData.data as Task;
425
+
298
426
  // Step 2: Check for blockers
299
- 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
+
300
446
  return {
301
- content: [
302
- {
303
- type: "text",
304
- 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.`,
305
- },
306
- {
307
- type: "text",
308
- text: `API Usage: ${JSON.stringify(taskData.api_usage)}`,
309
- }
310
- ],
447
+ content: [{ type: "text", text: formatted }],
311
448
  };
312
- };
449
+ }
313
450
 
451
+ // Step 3: No blockers, proceed with work
314
452
  const json = await getResultsFromMiddleware({
315
453
  endpoint: 'work_on_task',
316
- schema: {
317
- taskID
318
- }
454
+ schema: { taskID }
319
455
  });
320
-
321
- return {
322
- content: [
323
- {
324
- type: "text",
325
- 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.`,
326
- },
327
- {
328
- type: "text",
329
- user: "NUBIS",
330
- text: `Prompt user to move this task to 'in-progress' if it is not already in progress.`,
331
- },
332
-
333
- {
334
- type: "text",
335
- user: "NUBIS",
336
- text: `API Usage: ${JSON.stringify(json.api_usage)}`,
337
- }
338
- ],
339
- };
340
- }
341
- );
342
456
 
343
- // Explain setup and what user needs to do for feature to be implemented
344
- /* server.tool(
345
- "explain_setup",
346
- "Explain setup and what needs to be done for feature to be implemented",
347
- {
348
- taskID: z.string(),
349
- },
350
- async ({ taskID }) => {
351
- const json = await getResultsFromMiddleware({
352
- endpoint: 'explain_setup',
353
- schema: {
354
- taskID
355
- }
356
- });
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');
357
482
 
358
- if (!json.data) throw new Error('No data returned from middleware');
359
483
  return {
360
- content: [
361
- {
362
- type: "text",
363
- 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.`,
364
- },
365
- {
366
- type: "text",
367
- text: `Always provide API Usage information separately. Usage: ${JSON.stringify(json.api_usage)}`,
368
- }
369
- ],
484
+ content: [{ type: "text", text: formatted }],
370
485
  };
371
486
  }
372
- ); */
487
+ );
373
488
 
374
- // Move Task -> Move a task to ['backlog', 'priority', 'in-progress','reviewing', 'completed']
489
+ /**
490
+ * Move Task
491
+ */
375
492
  server.tool(
376
493
  "move_task",
377
- "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.",
378
495
  {
379
- taskID: z.string(),
380
- 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"),
381
498
  },
382
499
  async ({ taskID, board }) => {
383
500
  const json = await getResultsFromMiddleware({
384
501
  endpoint: 'move_task',
385
- schema: {
386
- taskID,
387
- board
388
- }
502
+ schema: { taskID, board }
389
503
  });
390
- 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
+
391
519
  return {
392
- content: [
393
- {
394
- type: "text",
395
- text: `Task ${taskID} has been moved to ${board}`,
396
- },
397
- {
398
- type: "text",
399
- text: `API Usage: ${JSON.stringify(json.api_usage)}`,
400
- }
401
- ],
520
+ content: [{ type: "text", text: formatted }],
402
521
  };
403
522
  }
404
523
  );
405
524
 
406
- // Create Task -> Create a new task
525
+ /**
526
+ * Create Task
527
+ */
407
528
  server.tool(
408
529
  "create_task",
409
- "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.",
410
531
  {
411
- title: z.string(),
412
- description: z.string().optional(),
413
- board: z.enum(['backlog', 'bugs', 'in-progress', 'priority', 'reviewing', 'completed']).optional().default('backlog'),
414
- parent_task_id: z.string().optional(),
415
- github_item_type: z.string().optional(), // file or dir
416
- github_file_path: z.string().optional(), // src/components/modal
417
- github_repo_name: z.string().optional(), // Atomlaunch/atom_frontend
418
- 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)")
419
540
  },
420
541
  async ({ title, description, board, parent_task_id, github_item_type, github_file_path, github_repo_name, bolt_id }) => {
421
542
  const json = await getResultsFromMiddleware({
@@ -431,36 +552,53 @@ server.tool(
431
552
  bolt_id
432
553
  }
433
554
  });
434
- 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
+
435
580
  return {
436
- content: [
437
- {
438
- type: "text",
439
- text: JSON.stringify(json.data),
440
- },
441
- {
442
- type: "text",
443
- text: `API Usage: ${JSON.stringify(json.api_usage)}`,
444
- }
445
- ],
581
+ content: [{ type: "text", text: lines.join('\n') }],
446
582
  };
447
583
  }
448
584
  );
449
585
 
450
- // Update Task -> Update an existing task
586
+ /**
587
+ * Update Task
588
+ */
451
589
  server.tool(
452
590
  "update_task",
453
- "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.",
454
592
  {
455
- taskID: z.string(),
456
- title: z.string().optional(),
457
- description: z.string().optional(),
458
- board: z.enum(['backlog', 'bugs', 'in-progress', 'priority', 'reviewing', 'completed']).optional(),
459
- bolt_id: z.string().optional(),
460
- parent_task_id: z.string().optional(),
461
- github_item_type: z.string().optional(),
462
- github_file_path: z.string().optional(),
463
- 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"),
464
602
  },
465
603
  async ({ taskID, title, description, board, bolt_id, parent_task_id, github_item_type, github_file_path, github_repo_name }) => {
466
604
  const json = await getResultsFromMiddleware({
@@ -477,23 +615,369 @@ server.tool(
477
615
  github_repo_name
478
616
  }
479
617
  });
480
- 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
+
481
803
  return {
482
- content: [
483
- {
484
- type: "text",
485
- text: JSON.stringify(json.data),
486
- },
487
- {
488
- type: "text",
489
- text: `API Usage: ${JSON.stringify(json.api_usage)}`,
490
- }
491
- ],
804
+ content: [{ type: "text", text: formatted }],
492
805
  };
493
806
  }
494
807
  );
495
808
 
496
- // Start server
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
+
900
+ return {
901
+ content: [{ type: "text", text: formatted }],
902
+ };
903
+ }
904
+ );
905
+
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
+
497
981
  async function main() {
498
982
  const transport = new StdioServerTransport();
499
983
  await server.connect(transport);
@@ -504,4 +988,4 @@ main().catch((error: unknown) => {
504
988
  const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
505
989
  console.error("Fatal error in main():", errorMessage);
506
990
  process.exit(1);
507
- });
991
+ });