@projora/mcp-server 1.0.0 → 1.1.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/build/index.js +187 -12
  2. package/package.json +1 -1
package/build/index.js CHANGED
@@ -471,13 +471,71 @@ async function addCommentToTask(taskId, body) {
471
471
  const result = await graphqlQuery(mutation, { taskId, body });
472
472
  return !!result?.createComment;
473
473
  }
474
+ // Helper function to update task branch/PR info
475
+ async function updateTaskBranchInfo(taskId, branchName, prUrl, prNumber) {
476
+ const input = {};
477
+ if (branchName !== undefined)
478
+ input.branchName = branchName;
479
+ if (prUrl !== undefined)
480
+ input.prUrl = prUrl;
481
+ if (prNumber !== undefined)
482
+ input.prNumber = prNumber;
483
+ if (Object.keys(input).length === 0)
484
+ return true;
485
+ const mutation = `
486
+ mutation UpdateTaskBranch($id: ID!, $input: UpdateTaskInput!) {
487
+ updateTask(id: $id, input: $input) {
488
+ id
489
+ branchName
490
+ prUrl
491
+ prNumber
492
+ }
493
+ }
494
+ `;
495
+ const result = await graphqlQuery(mutation, { id: taskId, input });
496
+ return !!result?.updateTask;
497
+ }
498
+ // Helper function to log time on a task
499
+ async function logTimeOnTask(taskId, startedAt, endedAt, description) {
500
+ const mutation = `
501
+ mutation LogTime($taskId: ID!, $input: LogTimeInput!) {
502
+ logTime(taskId: $taskId, input: $input) {
503
+ id
504
+ durationMinutes
505
+ formattedDuration
506
+ }
507
+ }
508
+ `;
509
+ const result = await graphqlQuery(mutation, {
510
+ taskId,
511
+ input: {
512
+ description: description || null,
513
+ startedAt,
514
+ endedAt,
515
+ },
516
+ });
517
+ return result?.logTime || null;
518
+ }
519
+ // Helper to format duration in human-readable form
520
+ function formatDuration(minutes) {
521
+ if (minutes < 60) {
522
+ return `${minutes} minute${minutes !== 1 ? 's' : ''}`;
523
+ }
524
+ const hours = Math.floor(minutes / 60);
525
+ const mins = minutes % 60;
526
+ if (mins === 0) {
527
+ return `${hours} hour${hours !== 1 ? 's' : ''}`;
528
+ }
529
+ return `${hours} hour${hours !== 1 ? 's' : ''} ${mins} minute${mins !== 1 ? 's' : ''}`;
530
+ }
474
531
  // 9. Start working on a task - get full context and set status to In Progress
475
532
  server.registerTool("start_task", {
476
- description: "Start working on a task. This will: 1) Set the task status to 'In Progress', 2) Add a comment noting work has started, 3) Return comprehensive task details including description, comments, GitHub repo info, and suggested branch name.",
533
+ description: "Start working on a task. This will: 1) Set the task status to 'In Progress', 2) Set the branch name on the task (if provided), 3) Add a comment noting work has started, 4) Return comprehensive task details including description, comments, GitHub repo info, and suggested branch name.",
477
534
  inputSchema: {
478
535
  taskKey: z.string().describe("The task key (e.g., 'PRJ-123')"),
536
+ branchName: z.string().optional().describe("The git branch name being used for this task. If provided, it will be saved to the task for tracking."),
479
537
  },
480
- }, async ({ taskKey }) => {
538
+ }, async ({ taskKey, branchName: providedBranchName }) => {
481
539
  const query = `
482
540
  query StartTask($taskKey: String!) {
483
541
  taskByKey(taskKey: $taskKey) {
@@ -544,9 +602,10 @@ server.registerTool("start_task", {
544
602
  }
545
603
  }
546
604
  }
547
- // Add a comment noting that work has started
548
- const timestamp = new Date().toISOString().split('T')[0];
549
- await addCommentToTask(task.id, `Started working on this task via Claude Code on ${timestamp}`);
605
+ // Record the start time for time tracking
606
+ const startTime = new Date();
607
+ const startTimestamp = startTime.toISOString();
608
+ const dateStr = startTimestamp.split('T')[0];
550
609
  // Generate suggested branch name
551
610
  const slugifiedTitle = task.title
552
611
  .toLowerCase()
@@ -554,10 +613,24 @@ server.registerTool("start_task", {
554
613
  .replace(/^-|-$/g, '')
555
614
  .substring(0, 50);
556
615
  const suggestedBranch = `feature/${task.taskKey.toLowerCase()}-${slugifiedTitle}`;
616
+ // Use provided branch name or suggested one
617
+ const branchToUse = providedBranchName || suggestedBranch;
618
+ // Set the branch name on the task
619
+ let branchSetMessage = "";
620
+ if (providedBranchName) {
621
+ await updateTaskBranchInfo(task.id, providedBranchName);
622
+ branchSetMessage = `\n**Branch set:** \`${providedBranchName}\`\n`;
623
+ }
624
+ // Add a comment noting that work has started
625
+ const branchComment = providedBranchName ? ` on branch \`${providedBranchName}\`` : '';
626
+ await addCommentToTask(task.id, `Started working on this task via AI assistant on ${dateStr}${branchComment}`);
557
627
  let output = `# Task: ${task.taskKey} - ${task.title}\n`;
558
628
  if (statusUpdateMessage) {
559
629
  output += statusUpdateMessage;
560
630
  }
631
+ if (branchSetMessage) {
632
+ output += branchSetMessage;
633
+ }
561
634
  output += `\n`;
562
635
  output += `## Overview\n`;
563
636
  output += `- **Project**: ${project.name} (${project.key})\n`;
@@ -613,19 +686,22 @@ server.registerTool("start_task", {
613
686
  }
614
687
  }
615
688
  output += `\n---\n`;
616
- output += `**Tip:** When you're done, use \`complete_task\` to mark this task as Done.\n`;
689
+ output += `**Started at:** ${startTimestamp}\n`;
690
+ output += `**Tip:** When you're done, use \`complete_task\` with \`startedAt: "${startTimestamp}"\` to automatically log time spent.\n`;
617
691
  return {
618
692
  content: [{ type: "text", text: output }],
619
693
  };
620
694
  });
621
- // 9b. Complete a task - mark as Done and add completion comment
695
+ // 9b. Complete a task - mark as Done, log time, and add completion comment
622
696
  server.registerTool("complete_task", {
623
- description: "Mark a task as complete/done. This will: 1) Set the task status to 'Done', 2) Add a comment with an optional summary of what was accomplished.",
697
+ description: "Mark a task as complete/done. This will: 1) Set the task status to 'Done', 2) Log the time spent (if startedAt is provided), 3) Set the PR URL (if provided), 4) Add a comment with the summary and time logged.",
624
698
  inputSchema: {
625
699
  taskKey: z.string().describe("The task key (e.g., 'PRJ-123')"),
626
- summary: z.string().optional().describe("Optional summary of what was accomplished"),
700
+ summary: z.string().optional().describe("Summary of what was accomplished"),
701
+ startedAt: z.string().optional().describe("ISO timestamp of when work started (from start_task). Used to calculate and log time spent."),
702
+ prUrl: z.string().optional().describe("URL of the pull request created for this task"),
627
703
  },
628
- }, async ({ taskKey, summary }) => {
704
+ }, async ({ taskKey, summary, startedAt, prUrl }) => {
629
705
  // Get the task
630
706
  const getTaskQuery = `
631
707
  query GetTaskId($taskKey: String!) {
@@ -645,6 +721,8 @@ server.registerTool("complete_task", {
645
721
  }
646
722
  const task = taskData.taskByKey;
647
723
  const previousStatus = task.status?.name || 'No status';
724
+ const endTime = new Date();
725
+ const endTimestamp = endTime.toISOString();
648
726
  // Find the "Done" status
649
727
  const doneStatus = await findStatusByName('done');
650
728
  if (!doneStatus) {
@@ -659,9 +737,47 @@ server.registerTool("complete_task", {
659
737
  content: [{ type: "text", text: `Failed to update task status. Check your permissions.` }],
660
738
  };
661
739
  }
740
+ // Log time if startedAt was provided
741
+ let timeLogMessage = "";
742
+ let durationMinutes = 0;
743
+ if (startedAt) {
744
+ try {
745
+ const startDate = new Date(startedAt);
746
+ durationMinutes = Math.round((endTime.getTime() - startDate.getTime()) / 60000);
747
+ if (durationMinutes > 0) {
748
+ // Format timestamps for the API (YYYY-MM-DD HH:MM:SS)
749
+ const formatForApi = (date) => {
750
+ return date.toISOString().replace('T', ' ').substring(0, 19);
751
+ };
752
+ const timeLog = await logTimeOnTask(task.id, formatForApi(startDate), formatForApi(endTime), `AI-assisted work session${summary ? `: ${summary.substring(0, 100)}` : ''}`);
753
+ if (timeLog) {
754
+ timeLogMessage = `Time logged: ${timeLog.formattedDuration}`;
755
+ }
756
+ }
757
+ }
758
+ catch (e) {
759
+ // If parsing fails, just skip time logging
760
+ console.error("Failed to parse startedAt timestamp:", e);
761
+ }
762
+ }
763
+ // Set PR URL if provided
764
+ let prSetMessage = "";
765
+ if (prUrl) {
766
+ // Extract PR number from URL if possible (e.g., https://github.com/owner/repo/pull/123)
767
+ const prMatch = prUrl.match(/\/pull\/(\d+)/);
768
+ const prNumber = prMatch ? parseInt(prMatch[1], 10) : undefined;
769
+ await updateTaskBranchInfo(task.id, undefined, prUrl, prNumber);
770
+ prSetMessage = `PR linked: ${prUrl}\n`;
771
+ }
662
772
  // Add completion comment
663
- const timestamp = new Date().toISOString().split('T')[0];
664
- let commentBody = `Task completed via Claude Code on ${timestamp}`;
773
+ const dateStr = endTimestamp.split('T')[0];
774
+ let commentBody = `Task completed via AI assistant on ${dateStr}`;
775
+ if (durationMinutes > 0) {
776
+ commentBody += `\n\n**Time spent:** ${formatDuration(durationMinutes)}`;
777
+ }
778
+ if (prUrl) {
779
+ commentBody += `\n\n**Pull Request:** ${prUrl}`;
780
+ }
665
781
  if (summary) {
666
782
  commentBody += `\n\n**Summary:**\n${summary}`;
667
783
  }
@@ -669,6 +785,12 @@ server.registerTool("complete_task", {
669
785
  let output = `# Task Completed: ${task.taskKey}\n\n`;
670
786
  output += `**${task.title}**\n\n`;
671
787
  output += `Status updated: ${previousStatus} → ${updated.status.name}\n`;
788
+ if (timeLogMessage) {
789
+ output += `${timeLogMessage}\n`;
790
+ }
791
+ if (prSetMessage) {
792
+ output += `${prSetMessage}`;
793
+ }
672
794
  if (summary) {
673
795
  output += `\nSummary added to task comments.\n`;
674
796
  }
@@ -817,6 +939,59 @@ server.registerTool("log_time", {
817
939
  content: [{ type: "text", text: `Logged ${result.logTime.formattedDuration} on ${taskKey}${description ? `: ${description}` : ''}` }],
818
940
  };
819
941
  });
942
+ // 12b. Link branch or PR to a task
943
+ server.registerTool("link_branch", {
944
+ description: "Link a git branch or pull request to a task. This helps track which branch/PR is associated with the task.",
945
+ inputSchema: {
946
+ taskKey: z.string().describe("The task key (e.g., 'PRJ-123')"),
947
+ branchName: z.string().optional().describe("The git branch name"),
948
+ prUrl: z.string().optional().describe("URL of the pull request"),
949
+ },
950
+ }, async ({ taskKey, branchName, prUrl }) => {
951
+ if (!branchName && !prUrl) {
952
+ return {
953
+ content: [{ type: "text", text: `Please provide either a branch name or PR URL.` }],
954
+ };
955
+ }
956
+ // First get the task ID
957
+ const getTaskQuery = `
958
+ query GetTaskId($taskKey: String!) {
959
+ taskByKey(taskKey: $taskKey) {
960
+ id
961
+ branchName
962
+ prUrl
963
+ }
964
+ }
965
+ `;
966
+ const taskData = await graphqlQuery(getTaskQuery, { taskKey });
967
+ if (!taskData || !taskData.taskByKey) {
968
+ return {
969
+ content: [{ type: "text", text: `Task '${taskKey}' not found.` }],
970
+ };
971
+ }
972
+ // Extract PR number from URL if possible
973
+ let prNumber;
974
+ if (prUrl) {
975
+ const prMatch = prUrl.match(/\/pull\/(\d+)/);
976
+ prNumber = prMatch ? parseInt(prMatch[1], 10) : undefined;
977
+ }
978
+ const success = await updateTaskBranchInfo(taskData.taskByKey.id, branchName, prUrl, prNumber);
979
+ if (!success) {
980
+ return {
981
+ content: [{ type: "text", text: `Failed to update task. Check your permissions.` }],
982
+ };
983
+ }
984
+ let output = `Updated ${taskKey}:\n`;
985
+ if (branchName) {
986
+ output += ` Branch: ${branchName}\n`;
987
+ }
988
+ if (prUrl) {
989
+ output += ` PR: ${prUrl}\n`;
990
+ }
991
+ return {
992
+ content: [{ type: "text", text: output }],
993
+ };
994
+ });
820
995
  // 13. Update task (general)
821
996
  server.registerTool("update_task", {
822
997
  description: "Update task fields (title, description, priority, assignee, due date, estimate)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projora/mcp-server",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for Projora task management - integrate with Claude Code and OpenCode to manage tasks, update statuses, and track work",
5
5
  "type": "module",
6
6
  "bin": {