@projora/mcp-server 1.2.0 → 1.3.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 CHANGED
@@ -4,27 +4,18 @@ 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)
9
7
  - **start_task** - Start working on a task (sets status to "In Progress", adds comment, returns full context)
10
8
  - **complete_task** - Mark a task as done with an optional summary
11
9
  - **get_task** - Get a specific task by its key (e.g., "PRJ-123")
12
- - **update_task** - Update task fields (title, description, priority, assignee, due date)
13
- - **update_task_status** - Update task status
14
-
15
- ### Task Lists
10
+ - **list_projects** - List all projects
16
11
  - **list_tasks** - List tasks with optional filters (project, status, priority, assignee)
17
12
  - **my_tasks** - Get tasks assigned to you
18
13
  - **overdue_tasks** - Get all overdue tasks
19
14
  - **search_tasks** - Search for tasks and projects by keyword
20
-
21
- ### Collaboration
15
+ - **update_task** - Update task fields (title, description, priority, assignee, due date)
16
+ - **update_task_status** - Update task status
22
17
  - **add_comment** - Add a comment to a task
23
18
  - **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
28
19
  - **get_project** - Get project details including linked GitHub repository
29
20
  - **get_config** - Get available statuses, priorities, and task types
30
21
  - **dashboard_stats** - Get dashboard statistics
@@ -41,18 +32,27 @@ Or install globally:
41
32
  npm install -g @projora/mcp-server
42
33
  ```
43
34
 
44
- ## Configuration
35
+ ## Quick Setup
36
+
37
+ Run this single command to automatically configure Claude Code or OpenCode:
45
38
 
46
- The server requires two environment variables:
39
+ ```bash
40
+ npx @projora/mcp-server setup
41
+ ```
47
42
 
48
- - `PROJORA_GRAPHQL_URL` - Your Projora GraphQL endpoint (defaults to `https://api.projora.app/graphql`)
49
- - `PROJORA_AUTH_TOKEN` - Your API token from Projora
43
+ The setup wizard will:
44
+ 1. Prompt for your API token (get it from [Projora Settings](https://projora.app/settings/integrations))
45
+ 2. Automatically configure your AI coding assistant
46
+ 3. No manual editing of config files required!
50
47
 
51
- Get your API token from **Settings > Integrations** in the Projora web app.
48
+ ### Manual Configuration
52
49
 
53
- ## Usage with Claude Code
50
+ If you prefer to manually configure, see below:
54
51
 
55
- Add to your Claude Code configuration (`~/.claude.json`):
52
+ <details>
53
+ <summary>Claude Code Manual Setup</summary>
54
+
55
+ Add to your `~/.claude.json`:
56
56
 
57
57
  ```json
58
58
  {
@@ -65,10 +65,12 @@ Add to your Claude Code configuration (`~/.claude.json`):
65
65
  }
66
66
  }
67
67
  ```
68
+ </details>
68
69
 
69
- ## Usage with OpenCode
70
+ <details>
71
+ <summary>OpenCode Manual Setup</summary>
70
72
 
71
- Add to your OpenCode configuration (`opencode.json`):
73
+ Add to your `opencode.json`:
72
74
 
73
75
  ```json
74
76
  {
@@ -84,15 +86,17 @@ Add to your OpenCode configuration (`opencode.json`):
84
86
  }
85
87
  }
86
88
  ```
89
+ </details>
90
+
91
+ ### Environment Variables
87
92
 
88
- > Note: `PROJORA_GRAPHQL_URL` defaults to `https://api.projora.app/graphql`. Only set it if you're using a self-hosted instance.
93
+ - `PROJORA_AUTH_TOKEN` - Your API token (required)
94
+ - `PROJORA_GRAPHQL_URL` - GraphQL endpoint (defaults to `https://api.projora.app/graphql`, only set for self-hosted)
89
95
 
90
96
  ## Example Prompts
91
97
 
92
98
  Once configured, use prompts like:
93
99
 
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"
96
100
  - "Start working on task PRJ-123"
97
101
  - "Show me my tasks"
98
102
  - "What are the overdue tasks?"
@@ -103,18 +107,14 @@ Once configured, use prompts like:
103
107
 
104
108
  ## Workflow Example
105
109
 
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"
110
+ 1. **Start a task**: "Use projora to start working on task PRJ-123"
111
111
  - Status automatically changes to "In Progress"
112
112
  - A comment is added noting work has started
113
113
  - You get the full task context including description, GitHub repo, and suggested branch name
114
114
 
115
- 3. **Work on the task**: Make your changes, commit code, etc.
115
+ 2. **Work on the task**: Make your changes, commit code, etc.
116
116
 
117
- 4. **Complete the task**: "Use projora to complete task PRJ-123 with summary: Implemented the new feature with tests"
117
+ 3. **Complete the task**: "Use projora to complete task PRJ-123 with summary: Implemented the new feature with tests"
118
118
  - Status changes to "Done"
119
119
  - A completion comment is added with your summary
120
120
 
package/build/index.js CHANGED
@@ -1066,167 +1066,7 @@ server.registerTool("update_task", {
1066
1066
  content: [{ type: "text", text: `Updated ${result.updateTask.taskKey}: ${updatedFields}` }],
1067
1067
  };
1068
1068
  });
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
1069
+ // 14. Get project with GitHub info
1230
1070
  server.registerTool("get_project", {
1231
1071
  description: "Get project details including linked GitHub repository",
1232
1072
  inputSchema: {
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/build/setup.js ADDED
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import * as readline from 'readline';
6
+ const CLAUDE_CONFIG_PATH = path.join(os.homedir(), '.claude.json');
7
+ const OPENCODE_CONFIG_PATH = path.join(os.homedir(), 'opencode.json');
8
+ // Colors for terminal output
9
+ const colors = {
10
+ reset: '\x1b[0m',
11
+ bold: '\x1b[1m',
12
+ green: '\x1b[32m',
13
+ blue: '\x1b[34m',
14
+ yellow: '\x1b[33m',
15
+ red: '\x1b[31m',
16
+ };
17
+ function log(message, color = colors.reset) {
18
+ console.log(`${color}${message}${colors.reset}`);
19
+ }
20
+ function promptUser(question) {
21
+ const rl = readline.createInterface({
22
+ input: process.stdin,
23
+ output: process.stdout,
24
+ });
25
+ return new Promise((resolve) => {
26
+ rl.question(question, (answer) => {
27
+ rl.close();
28
+ resolve(answer.trim());
29
+ });
30
+ });
31
+ }
32
+ async function setupClaude(authToken) {
33
+ log('\n📝 Setting up Claude Code configuration...', colors.blue);
34
+ const config = {
35
+ projora: {
36
+ command: 'npx',
37
+ args: ['-y', '@projora/mcp-server'],
38
+ env: {
39
+ PROJORA_AUTH_TOKEN: authToken,
40
+ },
41
+ },
42
+ };
43
+ let existingConfig = {};
44
+ // Read existing config if it exists
45
+ if (fs.existsSync(CLAUDE_CONFIG_PATH)) {
46
+ try {
47
+ const fileContent = fs.readFileSync(CLAUDE_CONFIG_PATH, 'utf-8');
48
+ existingConfig = JSON.parse(fileContent);
49
+ log(' ✓ Found existing Claude Code configuration', colors.green);
50
+ }
51
+ catch (error) {
52
+ log(' ⚠ Warning: Could not parse existing config, will create new one', colors.yellow);
53
+ }
54
+ }
55
+ // Merge the configs
56
+ const updatedConfig = {
57
+ ...existingConfig,
58
+ ...config,
59
+ };
60
+ // Write the updated config
61
+ try {
62
+ fs.writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(updatedConfig, null, 2), 'utf-8');
63
+ log(` ✓ Successfully updated ${CLAUDE_CONFIG_PATH}`, colors.green);
64
+ return true;
65
+ }
66
+ catch (error) {
67
+ log(` ✗ Failed to write config: ${error}`, colors.red);
68
+ return false;
69
+ }
70
+ }
71
+ async function setupOpenCode(authToken) {
72
+ log('\n📝 Setting up OpenCode configuration...', colors.blue);
73
+ const mcpConfig = {
74
+ projora: {
75
+ type: 'local',
76
+ command: ['npx', '-y', '@projora/mcp-server'],
77
+ enabled: true,
78
+ environment: {
79
+ PROJORA_AUTH_TOKEN: authToken,
80
+ },
81
+ },
82
+ };
83
+ let existingConfig = {};
84
+ // Read existing config if it exists
85
+ if (fs.existsSync(OPENCODE_CONFIG_PATH)) {
86
+ try {
87
+ const fileContent = fs.readFileSync(OPENCODE_CONFIG_PATH, 'utf-8');
88
+ existingConfig = JSON.parse(fileContent);
89
+ log(' ✓ Found existing OpenCode configuration', colors.green);
90
+ }
91
+ catch (error) {
92
+ log(' ⚠ Warning: Could not parse existing config, will create new one', colors.yellow);
93
+ }
94
+ }
95
+ // Ensure mcp key exists
96
+ if (!existingConfig.mcp) {
97
+ existingConfig.mcp = {};
98
+ }
99
+ // Add/update Projora MCP config
100
+ existingConfig.mcp.projora = mcpConfig.projora;
101
+ // Write the updated config
102
+ try {
103
+ fs.writeFileSync(OPENCODE_CONFIG_PATH, JSON.stringify(existingConfig, null, 2), 'utf-8');
104
+ log(` ✓ Successfully updated ${OPENCODE_CONFIG_PATH}`, colors.green);
105
+ return true;
106
+ }
107
+ catch (error) {
108
+ log(` ✗ Failed to write config: ${error}`, colors.red);
109
+ return false;
110
+ }
111
+ }
112
+ async function main() {
113
+ log('\n╔═══════════════════════════════════════════╗', colors.bold);
114
+ log('║ Projora MCP Server Setup ║', colors.bold);
115
+ log('╚═══════════════════════════════════════════╝', colors.bold);
116
+ log('\nThis wizard will configure Projora integration for your AI coding assistant.', colors.blue);
117
+ log('You can get your API token from: https://projora.app/settings/integrations\n');
118
+ // Prompt for auth token
119
+ const authToken = await promptUser('Enter your Projora API token: ');
120
+ if (!authToken) {
121
+ log('\n✗ Error: API token is required', colors.red);
122
+ process.exit(1);
123
+ }
124
+ // Ask which assistant to configure
125
+ log('\n' + colors.bold + 'Which AI coding assistant do you want to configure?' + colors.reset);
126
+ log(' 1. Claude Code');
127
+ log(' 2. OpenCode');
128
+ log(' 3. Both');
129
+ const choice = await promptUser('\nEnter your choice (1, 2, or 3): ');
130
+ let claudeSuccess = false;
131
+ let opencodeSuccess = false;
132
+ switch (choice) {
133
+ case '1':
134
+ claudeSuccess = await setupClaude(authToken);
135
+ break;
136
+ case '2':
137
+ opencodeSuccess = await setupOpenCode(authToken);
138
+ break;
139
+ case '3':
140
+ claudeSuccess = await setupClaude(authToken);
141
+ opencodeSuccess = await setupOpenCode(authToken);
142
+ break;
143
+ default:
144
+ log('\n✗ Invalid choice', colors.red);
145
+ process.exit(1);
146
+ }
147
+ // Summary
148
+ log('\n' + colors.bold + '═'.repeat(45) + colors.reset);
149
+ log(colors.bold + 'Setup Complete!' + colors.reset, colors.green);
150
+ log('═'.repeat(45) + '\n', colors.bold);
151
+ if (claudeSuccess) {
152
+ log('✓ Claude Code is configured', colors.green);
153
+ log(' Restart Claude Code to start using Projora tools\n');
154
+ }
155
+ if (opencodeSuccess) {
156
+ log('✓ OpenCode is configured', colors.green);
157
+ log(' Restart OpenCode to start using Projora tools\n');
158
+ }
159
+ log('Available commands:', colors.blue);
160
+ log(' • start_task - Start working on a task');
161
+ log(' • complete_task - Mark task as done');
162
+ log(' • my_tasks - List your tasks');
163
+ log(' • add_comment - Add a comment to a task');
164
+ log(' • log_time - Log time worked');
165
+ log(' • search_tasks - Search tasks\n');
166
+ log('Documentation: https://docs.projora.app/integrations/mcp\n');
167
+ }
168
+ main().catch((error) => {
169
+ log(`\n✗ Setup failed: ${error.message}`, colors.red);
170
+ process.exit(1);
171
+ });
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "@projora/mcp-server",
3
- "version": "1.2.0",
3
+ "version": "1.3.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": {
7
- "projora-mcp": "./build/index.js"
7
+ "projora-mcp": "./build/index.js",
8
+ "projora-mcp-setup": "./build/setup.js"
8
9
  },
9
10
  "main": "./build/index.js",
10
11
  "scripts": {
11
- "build": "tsc && chmod 755 build/index.js",
12
+ "build": "tsc && chmod 755 build/index.js build/setup.js",
12
13
  "dev": "tsc --watch",
13
14
  "start": "node build/index.js",
15
+ "setup": "node build/setup.js",
14
16
  "prepublishOnly": "npm run build"
15
17
  },
16
18
  "files": [