internaltool-mcp 1.2.0 → 1.4.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 +261 -4
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -118,6 +118,20 @@ function registerUserTools(server) {
|
|
|
118
118
|
return call(() => api.get(`/api/users/directory${qs}`))
|
|
119
119
|
}
|
|
120
120
|
)
|
|
121
|
+
|
|
122
|
+
server.tool(
|
|
123
|
+
'get_my_tasks',
|
|
124
|
+
'Get all tasks assigned to you. Optionally filter by column. Use this to find your active work before a hotfix or priority switch.',
|
|
125
|
+
{
|
|
126
|
+
column: z.enum(['backlog', 'todo', 'in_progress', 'in_review', 'done'])
|
|
127
|
+
.optional()
|
|
128
|
+
.describe('Filter to a specific column — e.g. "in_progress" to find your active task'),
|
|
129
|
+
},
|
|
130
|
+
async ({ column } = {}) => {
|
|
131
|
+
const qs = column ? `?column=${encodeURIComponent(column)}` : ''
|
|
132
|
+
return call(() => api.get(`/api/users/me/tasks${qs}`))
|
|
133
|
+
}
|
|
134
|
+
)
|
|
121
135
|
}
|
|
122
136
|
|
|
123
137
|
function registerProjectTools(server, { isAdmin, scopedProjectId }) {
|
|
@@ -269,12 +283,15 @@ function registerTaskTools(server, { isAdmin, scopedProjectId }) {
|
|
|
269
283
|
|
|
270
284
|
server.tool(
|
|
271
285
|
'park_task',
|
|
272
|
-
|
|
286
|
+
`Pause a task and save your current work context.
|
|
287
|
+
IMPORTANT: Before calling this, run "git diff HEAD" and "git status" in the terminal to see what has changed.
|
|
288
|
+
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.`,
|
|
273
290
|
{
|
|
274
291
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
275
|
-
summary: z.string().optional().describe('
|
|
276
|
-
remaining: z.string().optional().describe('
|
|
277
|
-
blockers: z.string().optional().describe('
|
|
292
|
+
summary: z.string().optional().describe('What was built/changed — include file names and what was done'),
|
|
293
|
+
remaining: z.string().optional().describe('Specific next steps to complete this task'),
|
|
294
|
+
blockers: z.string().optional().describe('Anything blocking progress'),
|
|
278
295
|
},
|
|
279
296
|
async ({ taskId, summary = '', remaining = '', blockers = '' }) =>
|
|
280
297
|
call(() => api.patch(`/api/tasks/${taskId}/park`, { summary, remaining, blockers }))
|
|
@@ -288,6 +305,245 @@ function registerTaskTools(server, { isAdmin, scopedProjectId }) {
|
|
|
288
305
|
)
|
|
289
306
|
}
|
|
290
307
|
|
|
308
|
+
function registerProductivityTools(server) {
|
|
309
|
+
server.tool(
|
|
310
|
+
'generate_standup',
|
|
311
|
+
`Generate a daily standup for the current user.
|
|
312
|
+
Fetches all assigned tasks and their recent activity, then formats:
|
|
313
|
+
- What was done yesterday (based on activity + park notes)
|
|
314
|
+
- What is planned today (in_progress / todo tasks)
|
|
315
|
+
- Any blockers
|
|
316
|
+
|
|
317
|
+
Call this when the developer says "prepare my standup", "what did I do yesterday", or opens Cursor in the morning.`,
|
|
318
|
+
{},
|
|
319
|
+
async () => {
|
|
320
|
+
// Fetch all my tasks
|
|
321
|
+
const tasksRes = await api.get('/api/users/me/tasks')
|
|
322
|
+
if (!tasksRes?.success) return errorText('Could not fetch tasks')
|
|
323
|
+
|
|
324
|
+
const tasks = tasksRes.data.tasks
|
|
325
|
+
if (!tasks.length) return text({ standup: 'No assigned tasks found.', tasks: [] })
|
|
326
|
+
|
|
327
|
+
// Fetch activity for each task (parallel)
|
|
328
|
+
const withActivity = await Promise.all(
|
|
329
|
+
tasks.map(async (t) => {
|
|
330
|
+
try {
|
|
331
|
+
const actRes = await api.get(`/api/tasks/${t._id}/activity`)
|
|
332
|
+
return { ...t, activity: actRes?.data?.activity || [] }
|
|
333
|
+
} catch {
|
|
334
|
+
return { ...t, activity: [] }
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
// Summarise for standup
|
|
340
|
+
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
|
341
|
+
const standup = withActivity.map((t) => {
|
|
342
|
+
const recentActivity = t.activity.filter(
|
|
343
|
+
(a) => new Date(a.createdAt) > yesterday
|
|
344
|
+
)
|
|
345
|
+
return {
|
|
346
|
+
taskId: t._id,
|
|
347
|
+
key: t.key,
|
|
348
|
+
title: t.title,
|
|
349
|
+
column: t.column,
|
|
350
|
+
priority: t.priority,
|
|
351
|
+
project: t.project?.name,
|
|
352
|
+
parkNote: t.parkNote,
|
|
353
|
+
recentActivity: recentActivity.map((a) => a.message || a.type),
|
|
354
|
+
}
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
return text({ standup })
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
server.tool(
|
|
362
|
+
'end_of_day',
|
|
363
|
+
`End-of-day wrap-up. For every task currently in_progress:
|
|
364
|
+
1. Parks it with the provided notes (run "git diff HEAD" first to fill these accurately)
|
|
365
|
+
2. Returns a summary of what was wrapped
|
|
366
|
+
|
|
367
|
+
After this tool runs, post a short comment on each parked task via add_task_comment summarising the day.
|
|
368
|
+
Use this when the developer says "wrap up", "end of day", or "I'm done for today".`,
|
|
369
|
+
{
|
|
370
|
+
taskNotes: z.array(z.object({
|
|
371
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
372
|
+
summary: z.string().describe('What was done today — be specific, include file names'),
|
|
373
|
+
remaining: z.string().describe('What is left to do next session'),
|
|
374
|
+
blockers: z.string().optional().describe('Anything blocking'),
|
|
375
|
+
})).describe('Park notes for each in_progress task. Call get_my_tasks(column="in_progress") first to get the list.'),
|
|
376
|
+
},
|
|
377
|
+
async ({ taskNotes }) => {
|
|
378
|
+
const results = []
|
|
379
|
+
for (const note of taskNotes) {
|
|
380
|
+
try {
|
|
381
|
+
const res = await api.patch(`/api/tasks/${note.taskId}/park`, {
|
|
382
|
+
summary: note.summary,
|
|
383
|
+
remaining: note.remaining,
|
|
384
|
+
blockers: note.blockers || '',
|
|
385
|
+
})
|
|
386
|
+
results.push({
|
|
387
|
+
taskId: note.taskId,
|
|
388
|
+
success: res?.success ?? false,
|
|
389
|
+
message: res?.success ? 'Parked' : (res?.message || 'Failed'),
|
|
390
|
+
})
|
|
391
|
+
} catch (e) {
|
|
392
|
+
results.push({ taskId: note.taskId, success: false, message: e.message })
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return text({ wrapped: results })
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
server.tool(
|
|
400
|
+
'kickoff_task',
|
|
401
|
+
`Kick off a task — brief the developer and move it to in_progress.
|
|
402
|
+
Fetches the full task details plus recent project commits for context.
|
|
403
|
+
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".`,
|
|
405
|
+
{
|
|
406
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
407
|
+
},
|
|
408
|
+
async ({ taskId }) => {
|
|
409
|
+
// Get full task
|
|
410
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
411
|
+
if (!taskRes?.success) return errorText('Task not found')
|
|
412
|
+
const task = taskRes.data.task
|
|
413
|
+
|
|
414
|
+
// Get recent commits from the project
|
|
415
|
+
let recentCommits = []
|
|
416
|
+
try {
|
|
417
|
+
const commitsRes = await api.get(
|
|
418
|
+
`/api/projects/${task.project}/github/commits?per_page=10`
|
|
419
|
+
)
|
|
420
|
+
if (commitsRes?.success) recentCommits = commitsRes.data.commits || []
|
|
421
|
+
} catch { /* GitHub may not be linked */ }
|
|
422
|
+
|
|
423
|
+
// Move to in_progress
|
|
424
|
+
let moved = false
|
|
425
|
+
try {
|
|
426
|
+
const moveRes = await api.post(`/api/tasks/${taskId}/move`, {
|
|
427
|
+
column: 'in_progress',
|
|
428
|
+
toIndex: 0,
|
|
429
|
+
})
|
|
430
|
+
moved = moveRes?.success ?? false
|
|
431
|
+
} catch { /* might already be in_progress */ }
|
|
432
|
+
|
|
433
|
+
return text({
|
|
434
|
+
task: {
|
|
435
|
+
id: task._id,
|
|
436
|
+
key: task.key,
|
|
437
|
+
title: task.title,
|
|
438
|
+
description: task.description,
|
|
439
|
+
priority: task.priority,
|
|
440
|
+
column: moved ? 'in_progress' : task.column,
|
|
441
|
+
readme: task.readmeMarkdown,
|
|
442
|
+
subtasks: task.subtasks,
|
|
443
|
+
parkNote: task.parkNote,
|
|
444
|
+
approval: task.approval,
|
|
445
|
+
},
|
|
446
|
+
recentCommits: recentCommits.slice(0, 5).map((c) => ({
|
|
447
|
+
sha: c.sha?.slice(0, 7),
|
|
448
|
+
message: c.commit?.message?.split('\n')[0],
|
|
449
|
+
author: c.commit?.author?.name,
|
|
450
|
+
date: c.commit?.author?.date,
|
|
451
|
+
})),
|
|
452
|
+
movedToInProgress: moved,
|
|
453
|
+
suggestedBranch: `feature/${task.key?.toLowerCase()}-${task.title
|
|
454
|
+
.toLowerCase()
|
|
455
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
456
|
+
.slice(0, 40)}`,
|
|
457
|
+
})
|
|
458
|
+
}
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
server.tool(
|
|
462
|
+
'check_stale_tasks',
|
|
463
|
+
`Find tasks assigned to you that have had no activity for N days.
|
|
464
|
+
Use this to surface forgotten or blocked work.
|
|
465
|
+
Call this when the developer asks "what have I been neglecting" or "any stale tasks".`,
|
|
466
|
+
{
|
|
467
|
+
staleDays: z.number().int().min(1).default(3).describe('Number of days without activity to consider stale'),
|
|
468
|
+
},
|
|
469
|
+
async ({ staleDays = 3 }) => {
|
|
470
|
+
const tasksRes = await api.get('/api/users/me/tasks')
|
|
471
|
+
if (!tasksRes?.success) return errorText('Could not fetch tasks')
|
|
472
|
+
|
|
473
|
+
const activeTasks = tasksRes.data.tasks.filter(
|
|
474
|
+
(t) => t.column === 'in_progress' || t.column === 'in_review'
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
const threshold = new Date(Date.now() - staleDays * 24 * 60 * 60 * 1000)
|
|
478
|
+
|
|
479
|
+
const stale = []
|
|
480
|
+
for (const task of activeTasks) {
|
|
481
|
+
try {
|
|
482
|
+
const actRes = await api.get(`/api/tasks/${task._id}/activity`)
|
|
483
|
+
const activity = actRes?.data?.activity || []
|
|
484
|
+
const lastActivity = activity.length
|
|
485
|
+
? new Date(activity[activity.length - 1].createdAt)
|
|
486
|
+
: new Date(task.updatedAt || 0)
|
|
487
|
+
|
|
488
|
+
if (lastActivity < threshold) {
|
|
489
|
+
stale.push({
|
|
490
|
+
taskId: task._id,
|
|
491
|
+
key: task.key,
|
|
492
|
+
title: task.title,
|
|
493
|
+
column: task.column,
|
|
494
|
+
project: task.project?.name,
|
|
495
|
+
daysSilent: Math.floor((Date.now() - lastActivity) / (24 * 60 * 60 * 1000)),
|
|
496
|
+
lastActivity: lastActivity.toISOString(),
|
|
497
|
+
parkNote: task.parkNote,
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
} catch { /* skip if activity fetch fails */ }
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
stale.sort((a, b) => b.daysSilent - a.daysSilent)
|
|
504
|
+
return text({ staleTasks: stale, checkedTasks: activeTasks.length })
|
|
505
|
+
}
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
server.tool(
|
|
509
|
+
'link_pr_to_task',
|
|
510
|
+
`Link a pull request to a task, post a comment, and move the task to in_review.
|
|
511
|
+
Use this when a developer pushes a branch and opens a PR.
|
|
512
|
+
Keeps the board in sync with git automatically.`,
|
|
513
|
+
{
|
|
514
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
515
|
+
prUrl: z.string().describe('Full GitHub PR URL'),
|
|
516
|
+
prTitle: z.string().optional().describe('PR title'),
|
|
517
|
+
branch: z.string().optional().describe('Branch name'),
|
|
518
|
+
},
|
|
519
|
+
async ({ taskId, prUrl, prTitle, branch }) => {
|
|
520
|
+
const commentBody = [
|
|
521
|
+
`## Pull Request Opened`,
|
|
522
|
+
`**PR:** [${prTitle || prUrl}](${prUrl})`,
|
|
523
|
+
branch ? `**Branch:** \`${branch}\`` : '',
|
|
524
|
+
`**Status:** Ready for review`,
|
|
525
|
+
].filter(Boolean).join('\n')
|
|
526
|
+
|
|
527
|
+
// Post comment
|
|
528
|
+
const commentRes = await api.post(`/api/tasks/${taskId}/comments`, {
|
|
529
|
+
body: commentBody,
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
// Move to in_review
|
|
533
|
+
const moveRes = await api.post(`/api/tasks/${taskId}/move`, {
|
|
534
|
+
column: 'in_review',
|
|
535
|
+
toIndex: 0,
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
return text({
|
|
539
|
+
commented: commentRes?.success ?? false,
|
|
540
|
+
movedToReview: moveRes?.success ?? false,
|
|
541
|
+
message: 'Task linked to PR and moved to in_review',
|
|
542
|
+
})
|
|
543
|
+
}
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
|
|
291
547
|
function registerIssueTools(server) {
|
|
292
548
|
server.tool(
|
|
293
549
|
'create_task_issue',
|
|
@@ -466,6 +722,7 @@ async function main() {
|
|
|
466
722
|
registerUserTools(server)
|
|
467
723
|
registerProjectTools(server, ctx)
|
|
468
724
|
registerTaskTools(server, ctx)
|
|
725
|
+
registerProductivityTools(server)
|
|
469
726
|
registerIssueTools(server)
|
|
470
727
|
registerApprovalTools(server)
|
|
471
728
|
registerCommentTools(server)
|
package/package.json
CHANGED