@projora/mcp-server 1.1.0 → 1.2.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/README.md +21 -6
- package/build/index.js +161 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,18 +4,27 @@ A Model Context Protocol (MCP) server for [Projora](https://projora.app) task ma
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
+
### Task Management
|
|
8
|
+
- **create_task** - Create a new task in a project (specify project key, title, and optional details)
|
|
7
9
|
- **start_task** - Start working on a task (sets status to "In Progress", adds comment, returns full context)
|
|
8
10
|
- **complete_task** - Mark a task as done with an optional summary
|
|
9
11
|
- **get_task** - Get a specific task by its key (e.g., "PRJ-123")
|
|
10
|
-
- **
|
|
12
|
+
- **update_task** - Update task fields (title, description, priority, assignee, due date)
|
|
13
|
+
- **update_task_status** - Update task status
|
|
14
|
+
|
|
15
|
+
### Task Lists
|
|
11
16
|
- **list_tasks** - List tasks with optional filters (project, status, priority, assignee)
|
|
12
17
|
- **my_tasks** - Get tasks assigned to you
|
|
13
18
|
- **overdue_tasks** - Get all overdue tasks
|
|
14
19
|
- **search_tasks** - Search for tasks and projects by keyword
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
|
|
21
|
+
### Collaboration
|
|
17
22
|
- **add_comment** - Add a comment to a task
|
|
18
23
|
- **log_time** - Log time worked on a task
|
|
24
|
+
- **link_branch** - Link a git branch or pull request to a task
|
|
25
|
+
|
|
26
|
+
### Projects & Config
|
|
27
|
+
- **list_projects** - List all projects
|
|
19
28
|
- **get_project** - Get project details including linked GitHub repository
|
|
20
29
|
- **get_config** - Get available statuses, priorities, and task types
|
|
21
30
|
- **dashboard_stats** - Get dashboard statistics
|
|
@@ -82,6 +91,8 @@ Add to your OpenCode configuration (`opencode.json`):
|
|
|
82
91
|
|
|
83
92
|
Once configured, use prompts like:
|
|
84
93
|
|
|
94
|
+
- "Create a task in project PRJ: Fix the login button"
|
|
95
|
+
- "Create a task in PRJ with title 'Add unit tests' and assign it to me"
|
|
85
96
|
- "Start working on task PRJ-123"
|
|
86
97
|
- "Show me my tasks"
|
|
87
98
|
- "What are the overdue tasks?"
|
|
@@ -92,14 +103,18 @@ Once configured, use prompts like:
|
|
|
92
103
|
|
|
93
104
|
## Workflow Example
|
|
94
105
|
|
|
95
|
-
1. **
|
|
106
|
+
1. **Create a task**: "Use projora to create a task in project PRJ: Implement user authentication"
|
|
107
|
+
- Task is created in the specified project
|
|
108
|
+
- Returns the task key (e.g., PRJ-123) for reference
|
|
109
|
+
|
|
110
|
+
2. **Start the task**: "Use projora to start working on task PRJ-123"
|
|
96
111
|
- Status automatically changes to "In Progress"
|
|
97
112
|
- A comment is added noting work has started
|
|
98
113
|
- You get the full task context including description, GitHub repo, and suggested branch name
|
|
99
114
|
|
|
100
|
-
|
|
115
|
+
3. **Work on the task**: Make your changes, commit code, etc.
|
|
101
116
|
|
|
102
|
-
|
|
117
|
+
4. **Complete the task**: "Use projora to complete task PRJ-123 with summary: Implemented the new feature with tests"
|
|
103
118
|
- Status changes to "Done"
|
|
104
119
|
- A completion comment is added with your summary
|
|
105
120
|
|
package/build/index.js
CHANGED
|
@@ -1066,7 +1066,167 @@ server.registerTool("update_task", {
|
|
|
1066
1066
|
content: [{ type: "text", text: `Updated ${result.updateTask.taskKey}: ${updatedFields}` }],
|
|
1067
1067
|
};
|
|
1068
1068
|
});
|
|
1069
|
-
// 14.
|
|
1069
|
+
// 14. Create a new task
|
|
1070
|
+
server.registerTool("create_task", {
|
|
1071
|
+
description: "Create a new task in a project. You can specify the project by key and optionally the task list name, or provide IDs directly. Returns the created task details.",
|
|
1072
|
+
inputSchema: {
|
|
1073
|
+
projectKey: z.string().optional().describe("The project key (e.g., 'PRJ'). Either projectKey or taskListId is required."),
|
|
1074
|
+
taskListName: z.string().optional().describe("Name of the task list to add the task to (e.g., 'To Do', 'Backlog'). If not provided, uses the first task list."),
|
|
1075
|
+
taskListId: z.string().optional().describe("The task list ID. If provided, projectKey and taskListName are ignored."),
|
|
1076
|
+
title: z.string().describe("The task title (required)"),
|
|
1077
|
+
description: z.string().optional().describe("Task description (supports markdown)"),
|
|
1078
|
+
statusId: z.string().optional().describe("Status ID (use get_config to see available statuses)"),
|
|
1079
|
+
priorityId: z.string().optional().describe("Priority ID (use get_config to see available priorities)"),
|
|
1080
|
+
taskTypeId: z.string().optional().describe("Task type ID (use get_config to see available task types)"),
|
|
1081
|
+
assigneeId: z.string().optional().describe("User ID to assign the task to"),
|
|
1082
|
+
dueDate: z.string().optional().describe("Due date in YYYY-MM-DD format"),
|
|
1083
|
+
startDate: z.string().optional().describe("Start date in YYYY-MM-DD format"),
|
|
1084
|
+
estimatedHours: z.number().optional().describe("Estimated hours to complete the task"),
|
|
1085
|
+
},
|
|
1086
|
+
}, async ({ projectKey, taskListName, taskListId, title, description, statusId, priorityId, taskTypeId, assigneeId, dueDate, startDate, estimatedHours }) => {
|
|
1087
|
+
let resolvedTaskListId = taskListId;
|
|
1088
|
+
let projectName = "";
|
|
1089
|
+
// If taskListId is not provided, we need to look it up from projectKey
|
|
1090
|
+
if (!resolvedTaskListId) {
|
|
1091
|
+
if (!projectKey) {
|
|
1092
|
+
return {
|
|
1093
|
+
content: [{ type: "text", text: "Either projectKey or taskListId is required to create a task." }],
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
// Get project and its task lists
|
|
1097
|
+
const projectQuery = `
|
|
1098
|
+
query GetProjectWithTaskLists {
|
|
1099
|
+
projects {
|
|
1100
|
+
id
|
|
1101
|
+
name
|
|
1102
|
+
key
|
|
1103
|
+
taskLists {
|
|
1104
|
+
id
|
|
1105
|
+
name
|
|
1106
|
+
sortOrder
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
`;
|
|
1111
|
+
const projectData = await graphqlQuery(projectQuery);
|
|
1112
|
+
if (!projectData) {
|
|
1113
|
+
return {
|
|
1114
|
+
content: [{ type: "text", text: "Failed to fetch projects. Check your authentication token." }],
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
const project = projectData.projects.find(p => p.key.toLowerCase() === projectKey.toLowerCase());
|
|
1118
|
+
if (!project) {
|
|
1119
|
+
const availableProjects = projectData.projects.map(p => p.key).join(", ");
|
|
1120
|
+
return {
|
|
1121
|
+
content: [{ type: "text", text: `Project '${projectKey}' not found. Available projects: ${availableProjects}` }],
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
projectName = project.name;
|
|
1125
|
+
if (project.taskLists.length === 0) {
|
|
1126
|
+
return {
|
|
1127
|
+
content: [{ type: "text", text: `Project '${projectKey}' has no task lists. Please create a task list first.` }],
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
// Find the task list by name or use the first one
|
|
1131
|
+
if (taskListName) {
|
|
1132
|
+
const taskList = project.taskLists.find(tl => tl.name.toLowerCase() === taskListName.toLowerCase());
|
|
1133
|
+
if (!taskList) {
|
|
1134
|
+
const availableLists = project.taskLists.map(tl => tl.name).join(", ");
|
|
1135
|
+
return {
|
|
1136
|
+
content: [{ type: "text", text: `Task list '${taskListName}' not found in project '${projectKey}'. Available lists: ${availableLists}` }],
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
resolvedTaskListId = taskList.id;
|
|
1140
|
+
}
|
|
1141
|
+
else {
|
|
1142
|
+
// Use the first task list (sorted by sortOrder)
|
|
1143
|
+
const sortedLists = [...project.taskLists].sort((a, b) => a.sortOrder - b.sortOrder);
|
|
1144
|
+
resolvedTaskListId = sortedLists[0].id;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
// Build the input object
|
|
1148
|
+
const input = {
|
|
1149
|
+
taskListId: resolvedTaskListId,
|
|
1150
|
+
title,
|
|
1151
|
+
};
|
|
1152
|
+
if (description !== undefined)
|
|
1153
|
+
input.description = description;
|
|
1154
|
+
if (statusId !== undefined)
|
|
1155
|
+
input.statusId = statusId;
|
|
1156
|
+
if (priorityId !== undefined)
|
|
1157
|
+
input.priorityId = priorityId;
|
|
1158
|
+
if (taskTypeId !== undefined)
|
|
1159
|
+
input.taskTypeId = taskTypeId;
|
|
1160
|
+
if (assigneeId !== undefined)
|
|
1161
|
+
input.assigneeId = assigneeId;
|
|
1162
|
+
if (dueDate !== undefined)
|
|
1163
|
+
input.dueDate = dueDate;
|
|
1164
|
+
if (startDate !== undefined)
|
|
1165
|
+
input.startDate = startDate;
|
|
1166
|
+
if (estimatedHours !== undefined)
|
|
1167
|
+
input.estimatedHours = estimatedHours;
|
|
1168
|
+
// Create the task
|
|
1169
|
+
const mutation = `
|
|
1170
|
+
mutation CreateTask($input: CreateTaskInput!) {
|
|
1171
|
+
createTask(input: $input) {
|
|
1172
|
+
id
|
|
1173
|
+
taskKey
|
|
1174
|
+
title
|
|
1175
|
+
description
|
|
1176
|
+
dueDate
|
|
1177
|
+
startDate
|
|
1178
|
+
estimatedHours
|
|
1179
|
+
status { id name }
|
|
1180
|
+
priority { id name }
|
|
1181
|
+
taskType { id name }
|
|
1182
|
+
assignee { id name }
|
|
1183
|
+
project { id name key }
|
|
1184
|
+
taskList { id name }
|
|
1185
|
+
createdAt
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
`;
|
|
1189
|
+
const result = await graphqlQuery(mutation, { input });
|
|
1190
|
+
if (!result || !result.createTask) {
|
|
1191
|
+
return {
|
|
1192
|
+
content: [{ type: "text", text: "Failed to create task. Check your permissions and input values." }],
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
const task = result.createTask;
|
|
1196
|
+
let output = `# Task Created: ${task.taskKey}\n\n`;
|
|
1197
|
+
output += `**${task.title}**\n\n`;
|
|
1198
|
+
output += `- **Project**: ${task.project.name} (${task.project.key})\n`;
|
|
1199
|
+
output += `- **List**: ${task.taskList.name}\n`;
|
|
1200
|
+
output += `- **Status**: ${task.status?.name || 'Default'}\n`;
|
|
1201
|
+
if (task.priority) {
|
|
1202
|
+
output += `- **Priority**: ${task.priority.name}\n`;
|
|
1203
|
+
}
|
|
1204
|
+
if (task.taskType) {
|
|
1205
|
+
output += `- **Type**: ${task.taskType.name}\n`;
|
|
1206
|
+
}
|
|
1207
|
+
if (task.assignee) {
|
|
1208
|
+
output += `- **Assignee**: ${task.assignee.name}\n`;
|
|
1209
|
+
}
|
|
1210
|
+
if (task.dueDate) {
|
|
1211
|
+
output += `- **Due Date**: ${task.dueDate}\n`;
|
|
1212
|
+
}
|
|
1213
|
+
if (task.startDate) {
|
|
1214
|
+
output += `- **Start Date**: ${task.startDate}\n`;
|
|
1215
|
+
}
|
|
1216
|
+
if (task.estimatedHours) {
|
|
1217
|
+
output += `- **Estimate**: ${task.estimatedHours} hours\n`;
|
|
1218
|
+
}
|
|
1219
|
+
if (task.description) {
|
|
1220
|
+
output += `\n## Description\n${task.description}\n`;
|
|
1221
|
+
}
|
|
1222
|
+
output += `\n---\n`;
|
|
1223
|
+
output += `Created at: ${task.createdAt}\n`;
|
|
1224
|
+
output += `\nYou can now use \`start_task\` with taskKey "${task.taskKey}" to begin working on it.`;
|
|
1225
|
+
return {
|
|
1226
|
+
content: [{ type: "text", text: output }],
|
|
1227
|
+
};
|
|
1228
|
+
});
|
|
1229
|
+
// 15. Get project with GitHub info
|
|
1070
1230
|
server.registerTool("get_project", {
|
|
1071
1231
|
description: "Get project details including linked GitHub repository",
|
|
1072
1232
|
inputSchema: {
|
package/package.json
CHANGED