@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.
- package/build/index.js +576 -256
- package/package.json +1 -1
- 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
|
-
|
|
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
|
-
//
|
|
26
|
-
|
|
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
|
|
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
|
-
//
|
|
207
|
+
// ============================================================================
|
|
208
|
+
// Tools
|
|
209
|
+
// ============================================================================
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get Boltz
|
|
213
|
+
*/
|
|
51
214
|
server.tool(
|
|
52
215
|
"get_boltz",
|
|
53
|
-
"Fetch all boltz for
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
278
|
+
* Get Task Details
|
|
157
279
|
*/
|
|
158
280
|
server.tool(
|
|
159
281
|
"get_task_details",
|
|
160
|
-
"Get a task
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Get Task Images
|
|
376
|
+
*/
|
|
223
377
|
server.tool(
|
|
224
378
|
"get_task_images",
|
|
225
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
408
|
+
/**
|
|
409
|
+
* Work on Task
|
|
410
|
+
*/
|
|
253
411
|
server.tool(
|
|
254
412
|
"work_on_task",
|
|
255
|
-
"
|
|
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
|
-
|
|
423
|
+
|
|
424
|
+
const task = taskData.data as Task;
|
|
425
|
+
|
|
266
426
|
// Step 2: Check for blockers
|
|
267
|
-
if (Array.isArray(
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
489
|
+
/**
|
|
490
|
+
* Move Task
|
|
491
|
+
*/
|
|
343
492
|
server.tool(
|
|
344
493
|
"move_task",
|
|
345
|
-
"Move a task to
|
|
494
|
+
"Move a task to a different board/status. Use this to update task progress through the workflow: backlog → priority → in-progress → reviewing → 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
|
-
|
|
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
|
-
|
|
525
|
+
/**
|
|
526
|
+
* Create Task
|
|
527
|
+
*/
|
|
375
528
|
server.tool(
|
|
376
529
|
"create_task",
|
|
377
|
-
"Create a new task or subtask
|
|
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.
|
|
384
|
-
github_file_path: z.string().optional()
|
|
385
|
-
github_repo_name: z.string().optional()
|
|
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
|
-
|
|
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
|
-
|
|
586
|
+
/**
|
|
587
|
+
* Update Task
|
|
588
|
+
*/
|
|
419
589
|
server.tool(
|
|
420
590
|
"update_task",
|
|
421
|
-
"Update an existing task
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|