internaltool-mcp 1.3.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.
Files changed (2) hide show
  1. package/index.js +247 -4
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -283,12 +283,15 @@ function registerTaskTools(server, { isAdmin, scopedProjectId }) {
283
283
 
284
284
  server.tool(
285
285
  'park_task',
286
- 'Pause a task and save your current work context.',
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.`,
287
290
  {
288
291
  taskId: z.string().describe("Task's MongoDB ObjectId"),
289
- summary: z.string().optional().describe('Work done so far'),
290
- remaining: z.string().optional().describe('What still needs to be done'),
291
- blockers: z.string().optional().describe('What is blocking progress'),
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'),
292
295
  },
293
296
  async ({ taskId, summary = '', remaining = '', blockers = '' }) =>
294
297
  call(() => api.patch(`/api/tasks/${taskId}/park`, { summary, remaining, blockers }))
@@ -302,6 +305,245 @@ function registerTaskTools(server, { isAdmin, scopedProjectId }) {
302
305
  )
303
306
  }
304
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
+
305
547
  function registerIssueTools(server) {
306
548
  server.tool(
307
549
  'create_task_issue',
@@ -480,6 +722,7 @@ async function main() {
480
722
  registerUserTools(server)
481
723
  registerProjectTools(server, ctx)
482
724
  registerTaskTools(server, ctx)
725
+ registerProductivityTools(server)
483
726
  registerIssueTools(server)
484
727
  registerApprovalTools(server)
485
728
  registerCommentTools(server)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "internaltool-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "MCP server for InternalTool — connect AI assistants (Claude Code, Cursor) to your project and task management platform",
5
5
  "type": "module",
6
6
  "main": "index.js",