internaltool-mcp 1.4.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 +577 -30
- 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(
|
|
@@ -286,22 +302,56 @@ function registerTaskTools(server, { isAdmin, scopedProjectId }) {
|
|
|
286
302
|
`Pause a task and save your current work context.
|
|
287
303
|
IMPORTANT: Before calling this, run "git diff HEAD" and "git status" in the terminal to see what has changed.
|
|
288
304
|
Use that output to write precise summary/remaining/blockers fields — not generic text.
|
|
289
|
-
This is the developer's saved mental state; make it detailed enough to resume cold tomorrow
|
|
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.`,
|
|
290
308
|
{
|
|
291
309
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
292
310
|
summary: z.string().optional().describe('What was built/changed — include file names and what was done'),
|
|
293
311
|
remaining: z.string().optional().describe('Specific next steps to complete this task'),
|
|
294
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'),
|
|
295
314
|
},
|
|
296
|
-
async ({ taskId, summary = '', remaining = '', blockers = '' }) =>
|
|
297
|
-
|
|
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
|
+
}
|
|
298
329
|
)
|
|
299
330
|
|
|
300
331
|
server.tool(
|
|
301
332
|
'unpark_task',
|
|
302
|
-
'Resume a parked task.',
|
|
303
|
-
{
|
|
304
|
-
|
|
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
|
+
}
|
|
305
355
|
)
|
|
306
356
|
}
|
|
307
357
|
|
|
@@ -401,11 +451,14 @@ Use this when the developer says "wrap up", "end of day", or "I'm done for today
|
|
|
401
451
|
`Kick off a task — brief the developer and move it to in_progress.
|
|
402
452
|
Fetches the full task details plus recent project commits for context.
|
|
403
453
|
Returns everything needed to start: task plan, what recently changed in the repo, and the git branch command to run.
|
|
404
|
-
Use this when a developer says "start task", "brief me on", or "what do I need to do for TASK-X"
|
|
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.`,
|
|
405
457
|
{
|
|
406
|
-
taskId:
|
|
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'),
|
|
407
460
|
},
|
|
408
|
-
async ({ taskId }) => {
|
|
461
|
+
async ({ taskId, confirmed = false }) => {
|
|
409
462
|
// Get full task
|
|
410
463
|
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
411
464
|
if (!taskRes?.success) return errorText('Task not found')
|
|
@@ -420,6 +473,25 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
420
473
|
if (commitsRes?.success) recentCommits = commitsRes.data.commits || []
|
|
421
474
|
} catch { /* GitHub may not be linked */ }
|
|
422
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
|
+
|
|
423
495
|
// Move to in_progress
|
|
424
496
|
let moved = false
|
|
425
497
|
try {
|
|
@@ -450,10 +522,8 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
450
522
|
date: c.commit?.author?.date,
|
|
451
523
|
})),
|
|
452
524
|
movedToInProgress: moved,
|
|
453
|
-
suggestedBranch
|
|
454
|
-
|
|
455
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
456
|
-
.slice(0, 40)}`,
|
|
525
|
+
suggestedBranch,
|
|
526
|
+
nextStep: `Branch created? Run: git checkout -b ${suggestedBranch}\nOr use the create_branch tool to create it on GitHub automatically.`,
|
|
457
527
|
})
|
|
458
528
|
}
|
|
459
529
|
)
|
|
@@ -509,14 +579,17 @@ Call this when the developer asks "what have I been neglecting" or "any stale ta
|
|
|
509
579
|
'link_pr_to_task',
|
|
510
580
|
`Link a pull request to a task, post a comment, and move the task to in_review.
|
|
511
581
|
Use this when a developer pushes a branch and opens a PR.
|
|
512
|
-
Keeps the board in sync with git automatically
|
|
582
|
+
Keeps the board in sync with git automatically.
|
|
583
|
+
|
|
584
|
+
Set confirmed=false first to preview, then confirmed=true to execute.`,
|
|
513
585
|
{
|
|
514
586
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
515
587
|
prUrl: z.string().describe('Full GitHub PR URL'),
|
|
516
588
|
prTitle: z.string().optional().describe('PR title'),
|
|
517
589
|
branch: z.string().optional().describe('Branch name'),
|
|
590
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to execute after reviewing the preview'),
|
|
518
591
|
},
|
|
519
|
-
async ({ taskId, prUrl, prTitle, branch }) => {
|
|
592
|
+
async ({ taskId, prUrl, prTitle, branch, confirmed = false }) => {
|
|
520
593
|
const commentBody = [
|
|
521
594
|
`## Pull Request Opened`,
|
|
522
595
|
`**PR:** [${prTitle || prUrl}](${prUrl})`,
|
|
@@ -524,16 +597,23 @@ Keeps the board in sync with git automatically.`,
|
|
|
524
597
|
`**Status:** Ready for review`,
|
|
525
598
|
].filter(Boolean).join('\n')
|
|
526
599
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
+
}
|
|
531
614
|
|
|
532
|
-
|
|
533
|
-
const moveRes
|
|
534
|
-
column: 'in_review',
|
|
535
|
-
toIndex: 0,
|
|
536
|
-
})
|
|
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 })
|
|
537
617
|
|
|
538
618
|
return text({
|
|
539
619
|
commented: commentRes?.success ?? false,
|
|
@@ -677,6 +757,472 @@ function registerGithubTools(server, { scopedProjectId }) {
|
|
|
677
757
|
)
|
|
678
758
|
}
|
|
679
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
|
+
|
|
680
1226
|
function registerAdminTools(server) {
|
|
681
1227
|
server.tool(
|
|
682
1228
|
'admin_list_users',
|
|
@@ -728,6 +1274,7 @@ async function main() {
|
|
|
728
1274
|
registerCommentTools(server)
|
|
729
1275
|
registerNotificationTools(server)
|
|
730
1276
|
registerGithubTools(server, ctx)
|
|
1277
|
+
registerGitWorkflowTools(server, ctx)
|
|
731
1278
|
|
|
732
1279
|
if (isAdmin) {
|
|
733
1280
|
registerAdminTools(server)
|
package/package.json
CHANGED