@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.
- package/build/index.js +187 -12
- 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,
|
|
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
|
-
//
|
|
548
|
-
const
|
|
549
|
-
|
|
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 += `**
|
|
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
|
|
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("
|
|
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
|
|
664
|
-
let commentBody = `Task completed via
|
|
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