internaltool-mcp 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +805 -15
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -264,14 +264,30 @@ function registerTaskTools(server, { isAdmin, scopedProjectId }) {
|
|
|
264
264
|
|
|
265
265
|
server.tool(
|
|
266
266
|
'move_task',
|
|
267
|
-
'Move a task to a different column. Requires approved README to move from planning to execution columns.',
|
|
267
|
+
'Move a task to a different column. Requires approved README to move from planning to execution columns. Set confirmed=false first to preview.',
|
|
268
268
|
{
|
|
269
|
-
taskId:
|
|
270
|
-
column:
|
|
271
|
-
toIndex:
|
|
269
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
270
|
+
column: z.enum(['backlog', 'todo', 'in_progress', 'in_review', 'done']),
|
|
271
|
+
toIndex: z.number().int().min(0).default(0).describe('Position in column (0 = top)'),
|
|
272
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to execute after reviewing the preview'),
|
|
272
273
|
},
|
|
273
|
-
async ({ taskId, column, toIndex }) =>
|
|
274
|
-
|
|
274
|
+
async ({ taskId, column, toIndex, confirmed = false }) => {
|
|
275
|
+
if (!confirmed) {
|
|
276
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
277
|
+
const task = taskRes?.data?.task
|
|
278
|
+
return text({
|
|
279
|
+
preview: {
|
|
280
|
+
action: 'move_task',
|
|
281
|
+
task: task ? { key: task.key, title: task.title, currentColumn: task.column } : { taskId },
|
|
282
|
+
moveTo: column,
|
|
283
|
+
atPosition: toIndex,
|
|
284
|
+
},
|
|
285
|
+
requiresConfirmation: true,
|
|
286
|
+
message: `This will move the task to "${column}". Call move_task again with confirmed=true to proceed.`,
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
return call(() => api.post(`/api/tasks/${taskId}/move`, { column, toIndex }))
|
|
290
|
+
}
|
|
275
291
|
)
|
|
276
292
|
|
|
277
293
|
server.tool(
|
|
@@ -283,22 +299,328 @@ function registerTaskTools(server, { isAdmin, scopedProjectId }) {
|
|
|
283
299
|
|
|
284
300
|
server.tool(
|
|
285
301
|
'park_task',
|
|
286
|
-
|
|
302
|
+
`Pause a task and save your current work context.
|
|
303
|
+
IMPORTANT: Before calling this, run "git diff HEAD" and "git status" in the terminal to see what has changed.
|
|
304
|
+
Use that output to write precise summary/remaining/blockers fields — not generic text.
|
|
305
|
+
This is the developer's saved mental state; make it detailed enough to resume cold tomorrow.
|
|
306
|
+
|
|
307
|
+
Set confirmed=false first to preview, then confirmed=true to actually save.`,
|
|
287
308
|
{
|
|
288
309
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
289
|
-
summary: z.string().optional().describe('
|
|
290
|
-
remaining: z.string().optional().describe('
|
|
291
|
-
blockers: z.string().optional().describe('
|
|
310
|
+
summary: z.string().optional().describe('What was built/changed — include file names and what was done'),
|
|
311
|
+
remaining: z.string().optional().describe('Specific next steps to complete this task'),
|
|
312
|
+
blockers: z.string().optional().describe('Anything blocking progress'),
|
|
313
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to execute after reviewing the preview'),
|
|
292
314
|
},
|
|
293
|
-
async ({ taskId, summary = '', remaining = '', blockers = '' }) =>
|
|
294
|
-
|
|
315
|
+
async ({ taskId, summary = '', remaining = '', blockers = '', confirmed = false }) => {
|
|
316
|
+
if (!confirmed) {
|
|
317
|
+
return text({
|
|
318
|
+
preview: {
|
|
319
|
+
action: 'park_task',
|
|
320
|
+
taskId,
|
|
321
|
+
willSave: { summary: summary || '(empty)', remaining: remaining || '(empty)', blockers: blockers || '(empty)' },
|
|
322
|
+
},
|
|
323
|
+
requiresConfirmation: true,
|
|
324
|
+
message: 'Review the park note above. Call park_task again with confirmed=true to save it.',
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
return call(() => api.patch(`/api/tasks/${taskId}/park`, { summary, remaining, blockers }))
|
|
328
|
+
}
|
|
295
329
|
)
|
|
296
330
|
|
|
297
331
|
server.tool(
|
|
298
332
|
'unpark_task',
|
|
299
|
-
'Resume a parked task.',
|
|
300
|
-
{
|
|
301
|
-
|
|
333
|
+
'Resume a parked task. Set confirmed=false first to preview, then confirmed=true to execute.',
|
|
334
|
+
{
|
|
335
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
336
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to execute after reviewing the preview'),
|
|
337
|
+
},
|
|
338
|
+
async ({ taskId, confirmed = false }) => {
|
|
339
|
+
if (!confirmed) {
|
|
340
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
341
|
+
const task = taskRes?.data?.task
|
|
342
|
+
return text({
|
|
343
|
+
preview: {
|
|
344
|
+
action: 'unpark_task',
|
|
345
|
+
taskId,
|
|
346
|
+
task: task ? { key: task.key, title: task.title, parkNote: task.parkNote } : null,
|
|
347
|
+
willDo: 'Clear park note and mark task as active again',
|
|
348
|
+
},
|
|
349
|
+
requiresConfirmation: true,
|
|
350
|
+
message: 'Call unpark_task again with confirmed=true to resume this task.',
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
return call(() => api.patch(`/api/tasks/${taskId}/unpark`, {}))
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function registerProductivityTools(server) {
|
|
359
|
+
server.tool(
|
|
360
|
+
'generate_standup',
|
|
361
|
+
`Generate a daily standup for the current user.
|
|
362
|
+
Fetches all assigned tasks and their recent activity, then formats:
|
|
363
|
+
- What was done yesterday (based on activity + park notes)
|
|
364
|
+
- What is planned today (in_progress / todo tasks)
|
|
365
|
+
- Any blockers
|
|
366
|
+
|
|
367
|
+
Call this when the developer says "prepare my standup", "what did I do yesterday", or opens Cursor in the morning.`,
|
|
368
|
+
{},
|
|
369
|
+
async () => {
|
|
370
|
+
// Fetch all my tasks
|
|
371
|
+
const tasksRes = await api.get('/api/users/me/tasks')
|
|
372
|
+
if (!tasksRes?.success) return errorText('Could not fetch tasks')
|
|
373
|
+
|
|
374
|
+
const tasks = tasksRes.data.tasks
|
|
375
|
+
if (!tasks.length) return text({ standup: 'No assigned tasks found.', tasks: [] })
|
|
376
|
+
|
|
377
|
+
// Fetch activity for each task (parallel)
|
|
378
|
+
const withActivity = await Promise.all(
|
|
379
|
+
tasks.map(async (t) => {
|
|
380
|
+
try {
|
|
381
|
+
const actRes = await api.get(`/api/tasks/${t._id}/activity`)
|
|
382
|
+
return { ...t, activity: actRes?.data?.activity || [] }
|
|
383
|
+
} catch {
|
|
384
|
+
return { ...t, activity: [] }
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
// Summarise for standup
|
|
390
|
+
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
|
391
|
+
const standup = withActivity.map((t) => {
|
|
392
|
+
const recentActivity = t.activity.filter(
|
|
393
|
+
(a) => new Date(a.createdAt) > yesterday
|
|
394
|
+
)
|
|
395
|
+
return {
|
|
396
|
+
taskId: t._id,
|
|
397
|
+
key: t.key,
|
|
398
|
+
title: t.title,
|
|
399
|
+
column: t.column,
|
|
400
|
+
priority: t.priority,
|
|
401
|
+
project: t.project?.name,
|
|
402
|
+
parkNote: t.parkNote,
|
|
403
|
+
recentActivity: recentActivity.map((a) => a.message || a.type),
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
return text({ standup })
|
|
408
|
+
}
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
server.tool(
|
|
412
|
+
'end_of_day',
|
|
413
|
+
`End-of-day wrap-up. For every task currently in_progress:
|
|
414
|
+
1. Parks it with the provided notes (run "git diff HEAD" first to fill these accurately)
|
|
415
|
+
2. Returns a summary of what was wrapped
|
|
416
|
+
|
|
417
|
+
After this tool runs, post a short comment on each parked task via add_task_comment summarising the day.
|
|
418
|
+
Use this when the developer says "wrap up", "end of day", or "I'm done for today".`,
|
|
419
|
+
{
|
|
420
|
+
taskNotes: z.array(z.object({
|
|
421
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
422
|
+
summary: z.string().describe('What was done today — be specific, include file names'),
|
|
423
|
+
remaining: z.string().describe('What is left to do next session'),
|
|
424
|
+
blockers: z.string().optional().describe('Anything blocking'),
|
|
425
|
+
})).describe('Park notes for each in_progress task. Call get_my_tasks(column="in_progress") first to get the list.'),
|
|
426
|
+
},
|
|
427
|
+
async ({ taskNotes }) => {
|
|
428
|
+
const results = []
|
|
429
|
+
for (const note of taskNotes) {
|
|
430
|
+
try {
|
|
431
|
+
const res = await api.patch(`/api/tasks/${note.taskId}/park`, {
|
|
432
|
+
summary: note.summary,
|
|
433
|
+
remaining: note.remaining,
|
|
434
|
+
blockers: note.blockers || '',
|
|
435
|
+
})
|
|
436
|
+
results.push({
|
|
437
|
+
taskId: note.taskId,
|
|
438
|
+
success: res?.success ?? false,
|
|
439
|
+
message: res?.success ? 'Parked' : (res?.message || 'Failed'),
|
|
440
|
+
})
|
|
441
|
+
} catch (e) {
|
|
442
|
+
results.push({ taskId: note.taskId, success: false, message: e.message })
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return text({ wrapped: results })
|
|
446
|
+
}
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
server.tool(
|
|
450
|
+
'kickoff_task',
|
|
451
|
+
`Kick off a task — brief the developer and move it to in_progress.
|
|
452
|
+
Fetches the full task details plus recent project commits for context.
|
|
453
|
+
Returns everything needed to start: task plan, what recently changed in the repo, and the git branch command to run.
|
|
454
|
+
Use this when a developer says "start task", "brief me on", or "what do I need to do for TASK-X".
|
|
455
|
+
|
|
456
|
+
Set confirmed=false first to preview what will happen, then confirmed=true to actually move the task to in_progress.`,
|
|
457
|
+
{
|
|
458
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
459
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to move the task to in_progress'),
|
|
460
|
+
},
|
|
461
|
+
async ({ taskId, confirmed = false }) => {
|
|
462
|
+
// Get full task
|
|
463
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
464
|
+
if (!taskRes?.success) return errorText('Task not found')
|
|
465
|
+
const task = taskRes.data.task
|
|
466
|
+
|
|
467
|
+
// Get recent commits from the project
|
|
468
|
+
let recentCommits = []
|
|
469
|
+
try {
|
|
470
|
+
const commitsRes = await api.get(
|
|
471
|
+
`/api/projects/${task.project}/github/commits?per_page=10`
|
|
472
|
+
)
|
|
473
|
+
if (commitsRes?.success) recentCommits = commitsRes.data.commits || []
|
|
474
|
+
} catch { /* GitHub may not be linked */ }
|
|
475
|
+
|
|
476
|
+
const suggestedBranch = `feature/${task.key?.toLowerCase()}-${task.title
|
|
477
|
+
.toLowerCase()
|
|
478
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
479
|
+
.slice(0, 40)}`
|
|
480
|
+
|
|
481
|
+
// Preview mode — show what will happen without touching the task
|
|
482
|
+
if (!confirmed) {
|
|
483
|
+
return text({
|
|
484
|
+
preview: {
|
|
485
|
+
action: 'kickoff_task',
|
|
486
|
+
task: { key: task.key, title: task.title, currentColumn: task.column, priority: task.priority },
|
|
487
|
+
willMoveTo: 'in_progress',
|
|
488
|
+
suggestedBranch,
|
|
489
|
+
},
|
|
490
|
+
requiresConfirmation: true,
|
|
491
|
+
message: `This will move "${task.key}: ${task.title}" to in_progress. Call kickoff_task again with confirmed=true to proceed.`,
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Move to in_progress
|
|
496
|
+
let moved = false
|
|
497
|
+
try {
|
|
498
|
+
const moveRes = await api.post(`/api/tasks/${taskId}/move`, {
|
|
499
|
+
column: 'in_progress',
|
|
500
|
+
toIndex: 0,
|
|
501
|
+
})
|
|
502
|
+
moved = moveRes?.success ?? false
|
|
503
|
+
} catch { /* might already be in_progress */ }
|
|
504
|
+
|
|
505
|
+
return text({
|
|
506
|
+
task: {
|
|
507
|
+
id: task._id,
|
|
508
|
+
key: task.key,
|
|
509
|
+
title: task.title,
|
|
510
|
+
description: task.description,
|
|
511
|
+
priority: task.priority,
|
|
512
|
+
column: moved ? 'in_progress' : task.column,
|
|
513
|
+
readme: task.readmeMarkdown,
|
|
514
|
+
subtasks: task.subtasks,
|
|
515
|
+
parkNote: task.parkNote,
|
|
516
|
+
approval: task.approval,
|
|
517
|
+
},
|
|
518
|
+
recentCommits: recentCommits.slice(0, 5).map((c) => ({
|
|
519
|
+
sha: c.sha?.slice(0, 7),
|
|
520
|
+
message: c.commit?.message?.split('\n')[0],
|
|
521
|
+
author: c.commit?.author?.name,
|
|
522
|
+
date: c.commit?.author?.date,
|
|
523
|
+
})),
|
|
524
|
+
movedToInProgress: moved,
|
|
525
|
+
suggestedBranch,
|
|
526
|
+
nextStep: `Branch created? Run: git checkout -b ${suggestedBranch}\nOr use the create_branch tool to create it on GitHub automatically.`,
|
|
527
|
+
})
|
|
528
|
+
}
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
server.tool(
|
|
532
|
+
'check_stale_tasks',
|
|
533
|
+
`Find tasks assigned to you that have had no activity for N days.
|
|
534
|
+
Use this to surface forgotten or blocked work.
|
|
535
|
+
Call this when the developer asks "what have I been neglecting" or "any stale tasks".`,
|
|
536
|
+
{
|
|
537
|
+
staleDays: z.number().int().min(1).default(3).describe('Number of days without activity to consider stale'),
|
|
538
|
+
},
|
|
539
|
+
async ({ staleDays = 3 }) => {
|
|
540
|
+
const tasksRes = await api.get('/api/users/me/tasks')
|
|
541
|
+
if (!tasksRes?.success) return errorText('Could not fetch tasks')
|
|
542
|
+
|
|
543
|
+
const activeTasks = tasksRes.data.tasks.filter(
|
|
544
|
+
(t) => t.column === 'in_progress' || t.column === 'in_review'
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
const threshold = new Date(Date.now() - staleDays * 24 * 60 * 60 * 1000)
|
|
548
|
+
|
|
549
|
+
const stale = []
|
|
550
|
+
for (const task of activeTasks) {
|
|
551
|
+
try {
|
|
552
|
+
const actRes = await api.get(`/api/tasks/${task._id}/activity`)
|
|
553
|
+
const activity = actRes?.data?.activity || []
|
|
554
|
+
const lastActivity = activity.length
|
|
555
|
+
? new Date(activity[activity.length - 1].createdAt)
|
|
556
|
+
: new Date(task.updatedAt || 0)
|
|
557
|
+
|
|
558
|
+
if (lastActivity < threshold) {
|
|
559
|
+
stale.push({
|
|
560
|
+
taskId: task._id,
|
|
561
|
+
key: task.key,
|
|
562
|
+
title: task.title,
|
|
563
|
+
column: task.column,
|
|
564
|
+
project: task.project?.name,
|
|
565
|
+
daysSilent: Math.floor((Date.now() - lastActivity) / (24 * 60 * 60 * 1000)),
|
|
566
|
+
lastActivity: lastActivity.toISOString(),
|
|
567
|
+
parkNote: task.parkNote,
|
|
568
|
+
})
|
|
569
|
+
}
|
|
570
|
+
} catch { /* skip if activity fetch fails */ }
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
stale.sort((a, b) => b.daysSilent - a.daysSilent)
|
|
574
|
+
return text({ staleTasks: stale, checkedTasks: activeTasks.length })
|
|
575
|
+
}
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
server.tool(
|
|
579
|
+
'link_pr_to_task',
|
|
580
|
+
`Link a pull request to a task, post a comment, and move the task to in_review.
|
|
581
|
+
Use this when a developer pushes a branch and opens a PR.
|
|
582
|
+
Keeps the board in sync with git automatically.
|
|
583
|
+
|
|
584
|
+
Set confirmed=false first to preview, then confirmed=true to execute.`,
|
|
585
|
+
{
|
|
586
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
587
|
+
prUrl: z.string().describe('Full GitHub PR URL'),
|
|
588
|
+
prTitle: z.string().optional().describe('PR title'),
|
|
589
|
+
branch: z.string().optional().describe('Branch name'),
|
|
590
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to execute after reviewing the preview'),
|
|
591
|
+
},
|
|
592
|
+
async ({ taskId, prUrl, prTitle, branch, confirmed = false }) => {
|
|
593
|
+
const commentBody = [
|
|
594
|
+
`## Pull Request Opened`,
|
|
595
|
+
`**PR:** [${prTitle || prUrl}](${prUrl})`,
|
|
596
|
+
branch ? `**Branch:** \`${branch}\`` : '',
|
|
597
|
+
`**Status:** Ready for review`,
|
|
598
|
+
].filter(Boolean).join('\n')
|
|
599
|
+
|
|
600
|
+
if (!confirmed) {
|
|
601
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
602
|
+
const task = taskRes?.data?.task
|
|
603
|
+
return text({
|
|
604
|
+
preview: {
|
|
605
|
+
action: 'link_pr_to_task',
|
|
606
|
+
task: task ? { key: task.key, title: task.title } : { taskId },
|
|
607
|
+
willPost: commentBody,
|
|
608
|
+
willMoveTo: 'in_review',
|
|
609
|
+
},
|
|
610
|
+
requiresConfirmation: true,
|
|
611
|
+
message: 'This will post the comment above and move the task to in_review. Call link_pr_to_task again with confirmed=true to proceed.',
|
|
612
|
+
})
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const commentRes = await api.post(`/api/tasks/${taskId}/comments`, { body: commentBody })
|
|
616
|
+
const moveRes = await api.post(`/api/tasks/${taskId}/move`, { column: 'in_review', toIndex: 0 })
|
|
617
|
+
|
|
618
|
+
return text({
|
|
619
|
+
commented: commentRes?.success ?? false,
|
|
620
|
+
movedToReview: moveRes?.success ?? false,
|
|
621
|
+
message: 'Task linked to PR and moved to in_review',
|
|
622
|
+
})
|
|
623
|
+
}
|
|
302
624
|
)
|
|
303
625
|
}
|
|
304
626
|
|
|
@@ -435,6 +757,472 @@ function registerGithubTools(server, { scopedProjectId }) {
|
|
|
435
757
|
)
|
|
436
758
|
}
|
|
437
759
|
|
|
760
|
+
// ── Git Workflow Tools ─────────────────────────────────────────────────────────
|
|
761
|
+
// All write operations require confirmed=true after showing a preview.
|
|
762
|
+
// This gives developers full visibility before anything is executed.
|
|
763
|
+
|
|
764
|
+
function registerGitWorkflowTools(server, { scopedProjectId } = {}) {
|
|
765
|
+
|
|
766
|
+
// ── list_my_tasks ────────────────────────────────────────────────────────────
|
|
767
|
+
server.tool(
|
|
768
|
+
'list_my_tasks',
|
|
769
|
+
`List your assigned tasks sorted by priority (critical → high → medium → low).
|
|
770
|
+
Each task includes a suggested next git action so Claude can guide you step by step.
|
|
771
|
+
Use this at the start of a session or when switching tasks.`,
|
|
772
|
+
{
|
|
773
|
+
includeColumns: z.array(
|
|
774
|
+
z.enum(['backlog', 'todo', 'in_progress', 'in_review', 'done'])
|
|
775
|
+
).optional().default(['todo', 'in_progress', 'in_review'])
|
|
776
|
+
.describe('Which columns to include. Default excludes backlog and done.'),
|
|
777
|
+
},
|
|
778
|
+
async ({ includeColumns = ['todo', 'in_progress', 'in_review'] } = {}) => {
|
|
779
|
+
const res = await api.get('/api/users/me/tasks')
|
|
780
|
+
if (!res?.success) return errorText('Could not fetch tasks')
|
|
781
|
+
|
|
782
|
+
const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 }
|
|
783
|
+
const tasks = (res.data.tasks || [])
|
|
784
|
+
.filter(t => includeColumns.includes(t.column))
|
|
785
|
+
.sort((a, b) => (PRIORITY_ORDER[a.priority] ?? 99) - (PRIORITY_ORDER[b.priority] ?? 99))
|
|
786
|
+
.map(t => {
|
|
787
|
+
let suggestedAction = null
|
|
788
|
+
if (t.column === 'todo') {
|
|
789
|
+
suggestedAction = 'Use kickoff_task to start — it will brief you and move the task to in_progress'
|
|
790
|
+
} else if (t.column === 'in_progress' && !t.github?.headBranch) {
|
|
791
|
+
suggestedAction = 'No branch yet — use create_branch to create one before coding'
|
|
792
|
+
} else if (t.column === 'in_progress' && t.github?.headBranch) {
|
|
793
|
+
suggestedAction = `Branch exists (${t.github.headBranch}). Pushed commits? Use raise_pr to open a PR`
|
|
794
|
+
} else if (t.column === 'in_review') {
|
|
795
|
+
suggestedAction = `PR #${t.github?.prNumber || '?'} is open — waiting for reviewer feedback`
|
|
796
|
+
}
|
|
797
|
+
return {
|
|
798
|
+
id: t._id,
|
|
799
|
+
key: t.key,
|
|
800
|
+
title: t.title,
|
|
801
|
+
column: t.column,
|
|
802
|
+
priority: t.priority,
|
|
803
|
+
project: t.project?.name || t.project,
|
|
804
|
+
github: t.github ? { headBranch: t.github.headBranch, prNumber: t.github.prNumber, mergedAt: t.github.mergedAt } : null,
|
|
805
|
+
parked: !!t.parkNote?.parkedAt,
|
|
806
|
+
suggestedAction,
|
|
807
|
+
}
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
return text({ tasks, count: tasks.length })
|
|
811
|
+
}
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
// ── get_task_context ─────────────────────────────────────────────────────────
|
|
815
|
+
server.tool(
|
|
816
|
+
'get_task_context',
|
|
817
|
+
`Get everything needed to resume a task: full details, park notes, recent activity, and git state.
|
|
818
|
+
Use this when returning to a task after a break, or when Claude needs the full picture before suggesting next steps.`,
|
|
819
|
+
{
|
|
820
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
821
|
+
},
|
|
822
|
+
async ({ taskId }) => {
|
|
823
|
+
const [taskRes, activityRes] = await Promise.all([
|
|
824
|
+
api.get(`/api/tasks/${taskId}`),
|
|
825
|
+
api.get(`/api/tasks/${taskId}/activity`).catch(() => null),
|
|
826
|
+
])
|
|
827
|
+
if (!taskRes?.success) return errorText('Task not found')
|
|
828
|
+
const task = taskRes.data.task
|
|
829
|
+
const recentActivity = (activityRes?.data?.activity || []).slice(-10)
|
|
830
|
+
|
|
831
|
+
let coachingPrompt = null
|
|
832
|
+
if (task.column === 'in_progress' && !task.github?.headBranch) {
|
|
833
|
+
coachingPrompt = 'No branch created yet. Use create_branch before writing code.'
|
|
834
|
+
} else if (task.column === 'in_progress' && task.github?.headBranch) {
|
|
835
|
+
coachingPrompt = `Branch: ${task.github.headBranch}. When commits are pushed, use raise_pr to open a PR.`
|
|
836
|
+
} else if (task.column === 'in_review') {
|
|
837
|
+
coachingPrompt = `PR #${task.github?.prNumber} is open. Waiting for reviewer feedback.`
|
|
838
|
+
} else if (task.parkNote?.parkedAt) {
|
|
839
|
+
coachingPrompt = `Task was parked. Park note: "${task.parkNote.summary}". Use pop_stash if you stashed changes.`
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return text({
|
|
843
|
+
task: {
|
|
844
|
+
id: task._id,
|
|
845
|
+
key: task.key,
|
|
846
|
+
title: task.title,
|
|
847
|
+
description: task.description,
|
|
848
|
+
priority: task.priority,
|
|
849
|
+
column: task.column,
|
|
850
|
+
assignees: (task.assignees || []).map(a => ({ id: a._id, name: a.name })),
|
|
851
|
+
reviewer: task.reviewer ? { id: task.reviewer._id, name: task.reviewer.name } : null,
|
|
852
|
+
parkNote: task.parkNote,
|
|
853
|
+
github: task.github,
|
|
854
|
+
subtasks: task.subtasks,
|
|
855
|
+
readmeMarkdown: task.readmeMarkdown,
|
|
856
|
+
},
|
|
857
|
+
recentActivity: recentActivity.map(a => ({ action: a.action, createdAt: a.createdAt, meta: a.meta })),
|
|
858
|
+
coachingPrompt,
|
|
859
|
+
})
|
|
860
|
+
}
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
// ── create_branch ────────────────────────────────────────────────────────────
|
|
864
|
+
server.tool(
|
|
865
|
+
'create_branch',
|
|
866
|
+
`Create a Git branch on GitHub for a task using the naming convention:
|
|
867
|
+
feature/TASK-XXX-short-description (for new features)
|
|
868
|
+
fix/TASK-XXX-short-description (for bug fixes / patches)
|
|
869
|
+
|
|
870
|
+
WHEN TO USE: Immediately after a task moves to in_progress.
|
|
871
|
+
Claude will suggest this automatically after kickoff_task.
|
|
872
|
+
|
|
873
|
+
Set confirmed=false first to preview the branch name, then confirmed=true to create it.
|
|
874
|
+
After creation, run locally:
|
|
875
|
+
git fetch origin
|
|
876
|
+
git checkout <branchName>`,
|
|
877
|
+
{
|
|
878
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
879
|
+
projectId: z.string().describe("Project's MongoDB ObjectId"),
|
|
880
|
+
fromRef: z.string().optional().describe("Base ref to branch from (default: project's default branch)"),
|
|
881
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to create the branch after reviewing the preview'),
|
|
882
|
+
},
|
|
883
|
+
async ({ taskId, projectId, fromRef, confirmed = false }) => {
|
|
884
|
+
if (scopedProjectId && projectId !== scopedProjectId) {
|
|
885
|
+
return errorText(`Access denied: session is scoped to project ${scopedProjectId}`)
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
889
|
+
if (!taskRes?.success) return errorText('Task not found')
|
|
890
|
+
const task = taskRes.data.task
|
|
891
|
+
|
|
892
|
+
const isFix = /\b(fix|bug|hotfix|patch)\b/i.test(task.title + ' ' + (task.description || ''))
|
|
893
|
+
const prefix = isFix ? 'fix' : 'feature'
|
|
894
|
+
const slug = task.title
|
|
895
|
+
.toLowerCase()
|
|
896
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
897
|
+
.replace(/^-|-$/g, '')
|
|
898
|
+
.slice(0, 35)
|
|
899
|
+
const branchName = `${prefix}/${task.key.toLowerCase()}-${slug}`
|
|
900
|
+
|
|
901
|
+
// Preview
|
|
902
|
+
if (!confirmed) {
|
|
903
|
+
return text({
|
|
904
|
+
preview: {
|
|
905
|
+
action: 'create_branch',
|
|
906
|
+
branchName,
|
|
907
|
+
fromRef: fromRef || '(project default branch)',
|
|
908
|
+
repo: '(resolved from project)',
|
|
909
|
+
task: { key: task.key, title: task.title },
|
|
910
|
+
},
|
|
911
|
+
requiresConfirmation: true,
|
|
912
|
+
message: `Will create branch "${branchName}" on GitHub. Call create_branch again with confirmed=true to proceed.`,
|
|
913
|
+
})
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
try {
|
|
917
|
+
const res = await api.post(`/api/projects/${projectId}/github/branches`, { branchName, fromRef })
|
|
918
|
+
if (!res?.success) return errorText(res?.message || 'Could not create branch')
|
|
919
|
+
return text({
|
|
920
|
+
branchName,
|
|
921
|
+
url: res.data.url,
|
|
922
|
+
message: 'Branch created on GitHub.',
|
|
923
|
+
gitSteps: [
|
|
924
|
+
'git fetch origin',
|
|
925
|
+
`git checkout ${branchName}`,
|
|
926
|
+
],
|
|
927
|
+
nextStep: 'Start coding! When your commits are ready to review, use raise_pr to open a pull request.',
|
|
928
|
+
})
|
|
929
|
+
} catch (e) {
|
|
930
|
+
return errorText(e.message)
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
// ── stash_changes ────────────────────────────────────────────────────────────
|
|
936
|
+
server.tool(
|
|
937
|
+
'stash_changes',
|
|
938
|
+
`Save your current uncommitted work before switching to a higher-priority task.
|
|
939
|
+
|
|
940
|
+
WHEN TO USE: When an urgent task arrives while you are mid-work on another task.
|
|
941
|
+
Claude suggests this automatically when you say "I need to switch to TASK-XYZ".
|
|
942
|
+
|
|
943
|
+
This tool parks the current task in InternalTool (saves your context) and returns the
|
|
944
|
+
exact git stash command to run in your terminal — MCP cannot run git commands directly.
|
|
945
|
+
|
|
946
|
+
Set confirmed=false first to preview, then confirmed=true to park the task.`,
|
|
947
|
+
{
|
|
948
|
+
taskId: z.string().describe("Task's MongoDB ObjectId of the task you are pausing"),
|
|
949
|
+
summary: z.string().optional().describe('What you were doing — include file names and what changed'),
|
|
950
|
+
reason: z.string().optional().describe('Why you are switching (e.g. "urgent fix for TASK-042")'),
|
|
951
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to park the task after reviewing the preview'),
|
|
952
|
+
},
|
|
953
|
+
async ({ taskId, summary = '', reason = '', confirmed = false }) => {
|
|
954
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
955
|
+
const task = taskRes?.data?.task
|
|
956
|
+
|
|
957
|
+
const stashMsg = summary
|
|
958
|
+
? `wip: ${summary.slice(0, 60).replace(/"/g, "'")}`
|
|
959
|
+
: 'wip: pausing for priority switch'
|
|
960
|
+
|
|
961
|
+
const parkNote = {
|
|
962
|
+
summary: summary || 'Work in progress — stashing before switching tasks',
|
|
963
|
+
remaining: reason ? `Switched to: ${reason}` : 'Resume after priority task is done',
|
|
964
|
+
blockers: '',
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (!confirmed) {
|
|
968
|
+
return text({
|
|
969
|
+
preview: {
|
|
970
|
+
action: 'stash_changes',
|
|
971
|
+
task: task ? { key: task.key, title: task.title } : { taskId },
|
|
972
|
+
willPark: parkNote,
|
|
973
|
+
gitCommand: `git stash push -m "${stashMsg}"`,
|
|
974
|
+
},
|
|
975
|
+
requiresConfirmation: true,
|
|
976
|
+
message: 'This will park the task in InternalTool. Run the git stash command yourself in the terminal. Call stash_changes again with confirmed=true to save the park note.',
|
|
977
|
+
})
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
try {
|
|
981
|
+
await api.patch(`/api/tasks/${taskId}/park`, parkNote)
|
|
982
|
+
} catch (e) {
|
|
983
|
+
return errorText(`Failed to park task: ${e.message}`)
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return text({
|
|
987
|
+
taskParked: true,
|
|
988
|
+
gitCommand: `git stash push -m "${stashMsg}"`,
|
|
989
|
+
message: 'Task parked. Run the git command above in your terminal to stash your changes.',
|
|
990
|
+
nextStep: 'Use kickoff_task or create_branch to start the priority task. Use pop_stash to return here.',
|
|
991
|
+
})
|
|
992
|
+
}
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
// ── pop_stash ────────────────────────────────────────────────────────────────
|
|
996
|
+
server.tool(
|
|
997
|
+
'pop_stash',
|
|
998
|
+
`Restore stashed work when returning to a previously paused task.
|
|
999
|
+
|
|
1000
|
+
WHEN TO USE: When returning to a task you paused with stash_changes.
|
|
1001
|
+
Claude suggests this automatically when you resume a parked task.
|
|
1002
|
+
|
|
1003
|
+
Returns the git commands to run in your terminal and unparks the task in InternalTool.
|
|
1004
|
+
|
|
1005
|
+
Set confirmed=false first to review the park note, then confirmed=true to unpark.`,
|
|
1006
|
+
{
|
|
1007
|
+
taskId: z.string().describe("Task's MongoDB ObjectId of the task you are resuming"),
|
|
1008
|
+
stashIndex: z.number().int().min(0).optional().default(0)
|
|
1009
|
+
.describe("Stash index to pop (0 = most recent). Run 'git stash list' to see all."),
|
|
1010
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to unpark the task after reviewing'),
|
|
1011
|
+
},
|
|
1012
|
+
async ({ taskId, stashIndex = 0, confirmed = false }) => {
|
|
1013
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
1014
|
+
if (!taskRes?.success) return errorText('Task not found')
|
|
1015
|
+
const task = taskRes.data.task
|
|
1016
|
+
|
|
1017
|
+
const stashCmd = stashIndex === 0 ? 'git stash pop' : `git stash pop stash@{${stashIndex}}`
|
|
1018
|
+
const gitSteps = ['git fetch origin', 'git rebase origin/main', stashCmd]
|
|
1019
|
+
|
|
1020
|
+
if (!confirmed) {
|
|
1021
|
+
return text({
|
|
1022
|
+
preview: {
|
|
1023
|
+
action: 'pop_stash',
|
|
1024
|
+
task: { key: task.key, title: task.title, column: task.column },
|
|
1025
|
+
parkNote: task.parkNote || null,
|
|
1026
|
+
gitCommands: gitSteps,
|
|
1027
|
+
willDo: 'Clear park note and mark task active in InternalTool',
|
|
1028
|
+
},
|
|
1029
|
+
requiresConfirmation: true,
|
|
1030
|
+
message: 'Review your park note above. Call pop_stash again with confirmed=true to unpark and get the git commands.',
|
|
1031
|
+
})
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
try {
|
|
1035
|
+
await api.patch(`/api/tasks/${taskId}/unpark`, {})
|
|
1036
|
+
} catch (e) {
|
|
1037
|
+
return errorText(`Failed to unpark task: ${e.message}`)
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
return text({
|
|
1041
|
+
taskUnparked: true,
|
|
1042
|
+
parkNote: task.parkNote || null,
|
|
1043
|
+
gitCommands: gitSteps,
|
|
1044
|
+
message: 'Task unparked. Run the git commands above in your terminal to restore your work.',
|
|
1045
|
+
nextStep: 'Review your park note for context, then continue where you left off.',
|
|
1046
|
+
})
|
|
1047
|
+
}
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
// ── fix_pr_feedback ──────────────────────────────────────────────────────────
|
|
1051
|
+
server.tool(
|
|
1052
|
+
'fix_pr_feedback',
|
|
1053
|
+
`Guide the developer through fixing a PR that has "changes requested" or was closed,
|
|
1054
|
+
while they are mid-work on a different task.
|
|
1055
|
+
|
|
1056
|
+
WHEN TO USE: When a notification says "PR needs changes" on Task A while working on Task B.
|
|
1057
|
+
Claude will suggest this automatically when it detects a task with github.changesRequestedAt set.
|
|
1058
|
+
|
|
1059
|
+
The tool shows exactly what to do — stash Task B, switch to Task A's branch, fix, push, return.
|
|
1060
|
+
All git commands are returned for the developer to run in their terminal.
|
|
1061
|
+
|
|
1062
|
+
Set confirmed=false first to preview the full plan, then confirmed=true to save the stash note.`,
|
|
1063
|
+
{
|
|
1064
|
+
taskAId: z.string().describe("Task's MongoDB ObjectId of the task whose PR needs fixing (Task A)"),
|
|
1065
|
+
taskBId: z.string().optional().describe("Task's MongoDB ObjectId of the task currently being worked on (Task B), if any"),
|
|
1066
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to park Task B and get the full command sequence'),
|
|
1067
|
+
},
|
|
1068
|
+
async ({ taskAId, taskBId, confirmed = false }) => {
|
|
1069
|
+
const taskARes = await api.get(`/api/tasks/${taskAId}`)
|
|
1070
|
+
if (!taskARes?.success) return errorText('Task A not found')
|
|
1071
|
+
const taskA = taskARes.data.task
|
|
1072
|
+
|
|
1073
|
+
let taskB = null
|
|
1074
|
+
if (taskBId) {
|
|
1075
|
+
const taskBRes = await api.get(`/api/tasks/${taskBId}`)
|
|
1076
|
+
taskB = taskBRes?.data?.task || null
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const taskABranch = taskA.github?.headBranch || `feature/${taskA.key?.toLowerCase()}-${taskA.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 30)}`
|
|
1080
|
+
const taskBBranch = taskB?.github?.headBranch || (taskB ? `feature/${taskB.key?.toLowerCase()}` : null)
|
|
1081
|
+
|
|
1082
|
+
const plan = {
|
|
1083
|
+
situation: taskB
|
|
1084
|
+
? `PR for Task A (${taskA.key}) needs changes while you're working on Task B (${taskB.key}).`
|
|
1085
|
+
: `PR for Task A (${taskA.key}) needs changes.`,
|
|
1086
|
+
taskA: { key: taskA.key, title: taskA.title, branch: taskABranch, prNumber: taskA.github?.prNumber, prUrl: taskA.github?.prUrl },
|
|
1087
|
+
taskB: taskB ? { key: taskB.key, title: taskB.title, branch: taskBBranch } : null,
|
|
1088
|
+
steps: [
|
|
1089
|
+
taskB ? `1. Stash Task B work: git stash push -m "wip: ${taskBBranch} — switching to fix ${taskA.key} PR"` : null,
|
|
1090
|
+
`${taskB ? 2 : 1}. Switch to Task A branch: git checkout ${taskABranch}`,
|
|
1091
|
+
`${taskB ? 3 : 2}. Fetch and rebase: git fetch origin && git rebase origin/main`,
|
|
1092
|
+
`${taskB ? 4 : 3}. Read reviewer feedback on GitHub: ${taskA.github?.prUrl || 'open the PR'}`,
|
|
1093
|
+
`${taskB ? 5 : 4}. Fix issues, commit: git add . && git commit -m "fix(${taskA.key?.toLowerCase()}): address review feedback"`,
|
|
1094
|
+
`${taskB ? 6 : 5}. Push to update PR: git push origin ${taskABranch}`,
|
|
1095
|
+
taskB ? `7. Return to Task B: git checkout ${taskBBranch} && git stash pop` : null,
|
|
1096
|
+
].filter(Boolean),
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
if (!confirmed) {
|
|
1100
|
+
return text({
|
|
1101
|
+
preview: plan,
|
|
1102
|
+
requiresConfirmation: true,
|
|
1103
|
+
message: taskB
|
|
1104
|
+
? `This will park Task B and guide you to fix the PR for Task A. Call fix_pr_feedback again with confirmed=true to proceed.`
|
|
1105
|
+
: `Here's the plan to fix the PR. Call fix_pr_feedback again with confirmed=true to proceed.`,
|
|
1106
|
+
})
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Park Task B if provided
|
|
1110
|
+
if (taskB && taskBId) {
|
|
1111
|
+
try {
|
|
1112
|
+
await api.patch(`/api/tasks/${taskBId}/park`, {
|
|
1113
|
+
summary: `Working on fix for ${taskA.key} PR review`,
|
|
1114
|
+
remaining: `Resume ${taskB.key} after PR fix is pushed`,
|
|
1115
|
+
blockers: '',
|
|
1116
|
+
})
|
|
1117
|
+
} catch { /* non-fatal — developer can park manually */ }
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
return text({
|
|
1121
|
+
plan,
|
|
1122
|
+
taskBParked: !!(taskB && taskBId),
|
|
1123
|
+
gitCommands: [
|
|
1124
|
+
taskBBranch ? `git stash push -m "wip: ${taskBBranch} — switching to fix ${taskA.key} PR"` : null,
|
|
1125
|
+
`git checkout ${taskABranch}`,
|
|
1126
|
+
'git fetch origin',
|
|
1127
|
+
'git rebase origin/main',
|
|
1128
|
+
`# Read feedback at: ${taskA.github?.prUrl || '(open the PR on GitHub)'}`,
|
|
1129
|
+
`# Make your fixes, then:`,
|
|
1130
|
+
`git add .`,
|
|
1131
|
+
`git commit -m "fix(${taskA.key?.toLowerCase()}): address review feedback — <describe what changed>"`,
|
|
1132
|
+
`git push origin ${taskABranch}`,
|
|
1133
|
+
taskBBranch ? `# When done — return to Task B:` : null,
|
|
1134
|
+
taskBBranch ? `git checkout ${taskBBranch}` : null,
|
|
1135
|
+
taskBBranch ? `git stash pop` : null,
|
|
1136
|
+
].filter(Boolean),
|
|
1137
|
+
nextStep: `Fix the issues on Task A's branch, push, then return to Task B. The PR updates automatically when you push.`,
|
|
1138
|
+
})
|
|
1139
|
+
}
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
// ── raise_pr ─────────────────────────────────────────────────────────────────
|
|
1143
|
+
server.tool(
|
|
1144
|
+
'raise_pr',
|
|
1145
|
+
`Create a GitHub Pull Request for a task with auto-generated title and description.
|
|
1146
|
+
|
|
1147
|
+
WHEN TO USE: After pushing commits on a feature/fix branch and ready for review.
|
|
1148
|
+
Claude suggests this automatically when a task is in_progress and you indicate commits are pushed.
|
|
1149
|
+
|
|
1150
|
+
The PR title and body are built from the task data (title, key, description, park notes).
|
|
1151
|
+
After the PR is created, the GitHub webhook will automatically move the task to in_review
|
|
1152
|
+
and notify the assigned reviewer.
|
|
1153
|
+
|
|
1154
|
+
Set confirmed=false first to preview the full PR content, then confirmed=true to create it.`,
|
|
1155
|
+
{
|
|
1156
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
1157
|
+
projectId: z.string().describe("Project's MongoDB ObjectId"),
|
|
1158
|
+
headBranch: z.string().describe('Your feature/fix branch (e.g. feature/task-001-my-feature)'),
|
|
1159
|
+
additionalNotes: z.string().optional().describe('Extra context to add to the PR body'),
|
|
1160
|
+
draft: z.boolean().optional().default(false).describe('Open as a draft PR (not yet ready for review)'),
|
|
1161
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to create the PR after reviewing the preview'),
|
|
1162
|
+
},
|
|
1163
|
+
async ({ taskId, projectId, headBranch, additionalNotes = '', draft = false, confirmed = false }) => {
|
|
1164
|
+
if (scopedProjectId && projectId !== scopedProjectId) {
|
|
1165
|
+
return errorText(`Access denied: session is scoped to project ${scopedProjectId}`)
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
1169
|
+
if (!taskRes?.success) return errorText('Task not found')
|
|
1170
|
+
const task = taskRes.data.task
|
|
1171
|
+
|
|
1172
|
+
const prTitle = `[${task.key}] ${task.title}`
|
|
1173
|
+
const bodyParts = [
|
|
1174
|
+
`## ${task.key}: ${task.title}`,
|
|
1175
|
+
'',
|
|
1176
|
+
task.description ? `### Description\n${task.description}` : null,
|
|
1177
|
+
task.readmeMarkdown ? `### Implementation Notes\n${task.readmeMarkdown.slice(0, 600)}` : null,
|
|
1178
|
+
task.parkNote?.summary ? `### Work Summary\n${task.parkNote.summary}` : null,
|
|
1179
|
+
task.parkNote?.remaining ? `### What Remains\n${task.parkNote.remaining}` : null,
|
|
1180
|
+
additionalNotes ? `### Additional Notes\n${additionalNotes}` : null,
|
|
1181
|
+
'',
|
|
1182
|
+
'---',
|
|
1183
|
+
`*Auto-generated by InternalTool MCP — task: ${task.key}*`,
|
|
1184
|
+
].filter(v => v !== null).join('\n')
|
|
1185
|
+
|
|
1186
|
+
if (!confirmed) {
|
|
1187
|
+
return text({
|
|
1188
|
+
preview: {
|
|
1189
|
+
action: 'raise_pr',
|
|
1190
|
+
prTitle,
|
|
1191
|
+
prBody: bodyParts,
|
|
1192
|
+
headBranch,
|
|
1193
|
+
draft,
|
|
1194
|
+
task: { key: task.key, title: task.title },
|
|
1195
|
+
},
|
|
1196
|
+
requiresConfirmation: true,
|
|
1197
|
+
message: `Will open a${draft ? ' draft' : ''} PR titled "${prTitle}". Call raise_pr again with confirmed=true to create it.`,
|
|
1198
|
+
})
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
try {
|
|
1202
|
+
const res = await api.post(`/api/projects/${projectId}/github/pull-requests`, {
|
|
1203
|
+
title: prTitle,
|
|
1204
|
+
body: bodyParts,
|
|
1205
|
+
head: headBranch,
|
|
1206
|
+
draft,
|
|
1207
|
+
})
|
|
1208
|
+
if (!res?.success) return errorText(res?.message || 'Could not create PR')
|
|
1209
|
+
return text({
|
|
1210
|
+
prNumber: res.data.prNumber,
|
|
1211
|
+
prUrl: res.data.prUrl,
|
|
1212
|
+
title: prTitle,
|
|
1213
|
+
draft,
|
|
1214
|
+
message: `PR #${res.data.prNumber} created.`,
|
|
1215
|
+
nextStep: draft
|
|
1216
|
+
? 'PR is a draft. Mark it ready for review on GitHub when you want reviewer notifications to fire.'
|
|
1217
|
+
: 'PR is live. The GitHub webhook will move the task to in_review and notify the reviewer within seconds.',
|
|
1218
|
+
})
|
|
1219
|
+
} catch (e) {
|
|
1220
|
+
return errorText(e.message)
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
)
|
|
1224
|
+
}
|
|
1225
|
+
|
|
438
1226
|
function registerAdminTools(server) {
|
|
439
1227
|
server.tool(
|
|
440
1228
|
'admin_list_users',
|
|
@@ -480,11 +1268,13 @@ async function main() {
|
|
|
480
1268
|
registerUserTools(server)
|
|
481
1269
|
registerProjectTools(server, ctx)
|
|
482
1270
|
registerTaskTools(server, ctx)
|
|
1271
|
+
registerProductivityTools(server)
|
|
483
1272
|
registerIssueTools(server)
|
|
484
1273
|
registerApprovalTools(server)
|
|
485
1274
|
registerCommentTools(server)
|
|
486
1275
|
registerNotificationTools(server)
|
|
487
1276
|
registerGithubTools(server, ctx)
|
|
1277
|
+
registerGitWorkflowTools(server, ctx)
|
|
488
1278
|
|
|
489
1279
|
if (isAdmin) {
|
|
490
1280
|
registerAdminTools(server)
|
package/package.json
CHANGED