openproject-mcp 1.0.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/index.js +444 -0
  4. package/package.json +42 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cyborgx0x
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # OpenProject MCP Server
2
+
3
+ Model Context Protocol (MCP) server for OpenProject API integration. Enables AI assistants to interact with OpenProject work packages, projects, and time tracking.
4
+
5
+ ## Installation
6
+
7
+ ### Global Installation (Recommended)
8
+
9
+ ```bash
10
+ npm install -g openproject-mcp
11
+ ```
12
+
13
+ ### Local Installation
14
+
15
+ ```bash
16
+ npm install openproject-mcp
17
+ ```
18
+
19
+ ## Configuration
20
+
21
+ ### Get OpenProject API Key
22
+
23
+ 1. Log into your OpenProject instance
24
+ 2. Go to **My Account** → **Access tokens**
25
+ 3. Create a new API token
26
+ 4. Copy the token
27
+
28
+ ### Add to Kiro MCP Config
29
+
30
+ Add to `.kiro/settings/mcp.json`:
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "openproject": {
36
+ "command": "npx",
37
+ "args": ["-y", "openproject-mcp"],
38
+ "env": {
39
+ "OPENPROJECT_URL": "https://your-openproject-instance.com",
40
+ "OPENPROJECT_API_KEY": "your-api-key-here"
41
+ }
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ Or if installed globally:
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "openproject": {
53
+ "command": "openproject-mcp",
54
+ "env": {
55
+ "OPENPROJECT_URL": "https://your-openproject-instance.com",
56
+ "OPENPROJECT_API_KEY": "your-api-key-here"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## Available Tools
64
+
65
+ | Tool | Description |
66
+ |------|-------------|
67
+ | `list_projects` | List all projects |
68
+ | `get_work_package` | Get work package details by ID |
69
+ | `list_work_packages` | List work packages with filters |
70
+ | `get_children` | Get child work packages of a parent |
71
+ | `list_statuses` | List all available statuses |
72
+ | `list_types` | List all work package types (Feature, Task, Bug, etc.) |
73
+ | `get_user` | Get user information |
74
+ | `create_work_package` | Create a new work package |
75
+ | `update_work_package` | Update an existing work package |
76
+ | `log_time` | Log time entry for a work package |
77
+ | `raw_api_call` | Make a raw API call to any endpoint |
78
+
79
+ ## Usage Examples
80
+
81
+ ### List Children of a Feature
82
+
83
+ ```javascript
84
+ get_children({ parentId: 211 })
85
+ ```
86
+
87
+ ### Create a New Task
88
+
89
+ ```javascript
90
+ create_work_package({
91
+ subject: "Implement token budget management",
92
+ parentId: 538,
93
+ assigneeId: 10,
94
+ startDate: "2026-01-15",
95
+ dueDate: "2026-01-15"
96
+ })
97
+ ```
98
+
99
+ ### List Tasks Assigned to Me
100
+
101
+ ```javascript
102
+ list_work_packages({ assigneeId: "me" })
103
+ ```
104
+
105
+ ### Update Work Package Status
106
+
107
+ ```javascript
108
+ update_work_package({
109
+ id: 123,
110
+ statusId: 12, // Status ID from list_statuses
111
+ estimatedTime: "PT2H" // 2 hours in ISO 8601 format
112
+ })
113
+ ```
114
+
115
+ ### Log Time
116
+
117
+ ```javascript
118
+ log_time({
119
+ workPackageId: 123,
120
+ hours: 2.5,
121
+ comment: "Implemented feature X",
122
+ spentOn: "2026-01-23"
123
+ })
124
+ ```
125
+
126
+ ## Environment Variables
127
+
128
+ | Variable | Required | Description |
129
+ |----------|----------|-------------|
130
+ | `OPENPROJECT_URL` | Yes | Your OpenProject instance URL |
131
+ | `OPENPROJECT_API_KEY` | Yes | API key from OpenProject |
132
+
133
+ ## Requirements
134
+
135
+ - Node.js >= 18.0.0
136
+ - OpenProject instance with API access
137
+
138
+ ## License
139
+
140
+ MIT
141
+
142
+ ## Contributing
143
+
144
+ Contributions are welcome! Please open an issue or submit a pull request.
145
+
146
+ ## Links
147
+
148
+ - [OpenProject API Documentation](https://www.openproject.org/docs/api/)
149
+ - [Model Context Protocol](https://modelcontextprotocol.io/)
package/index.js ADDED
@@ -0,0 +1,444 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } from "@modelcontextprotocol/sdk/types.js";
9
+
10
+ const BASE_URL = process.env.OPENPROJECT_URL || "https://openproject.cyber.ai.vn";
11
+ const API_KEY = process.env.OPENPROJECT_API_KEY || "";
12
+
13
+ const server = new Server(
14
+ { name: "openproject-mcp", version: "1.0.0" },
15
+ { capabilities: { tools: {} } }
16
+ );
17
+
18
+ async function apiRequest(endpoint, method, body) {
19
+ method = method || "GET";
20
+ const url = endpoint.startsWith("http") ? endpoint : BASE_URL + endpoint;
21
+
22
+ const headers = {
23
+ "Accept": "application/json",
24
+ "Content-Type": "application/json",
25
+ };
26
+
27
+ if (API_KEY) {
28
+ const auth = Buffer.from("apikey:" + API_KEY).toString("base64");
29
+ headers["Authorization"] = "Basic " + auth;
30
+ }
31
+
32
+ const options = { method: method, headers: headers };
33
+ if (body && method !== "GET") {
34
+ options.body = JSON.stringify(body);
35
+ }
36
+
37
+ const response = await fetch(url, options);
38
+
39
+ if (!response.ok) {
40
+ throw new Error("API Error: " + response.status + " " + response.statusText);
41
+ }
42
+
43
+ return response.json();
44
+ }
45
+
46
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
47
+ tools: [
48
+ {
49
+ name: "list_projects",
50
+ description: "List all projects in OpenProject",
51
+ inputSchema: {
52
+ type: "object",
53
+ properties: {
54
+ pageSize: { type: "number", description: "Number of results per page", default: 20 }
55
+ }
56
+ }
57
+ },
58
+ {
59
+ name: "get_work_package",
60
+ description: "Get a specific work package by ID",
61
+ inputSchema: {
62
+ type: "object",
63
+ properties: {
64
+ id: { type: "number", description: "Work package ID" }
65
+ },
66
+ required: ["id"]
67
+ }
68
+ },
69
+ {
70
+ name: "list_work_packages",
71
+ description: "List work packages with optional filters",
72
+ inputSchema: {
73
+ type: "object",
74
+ properties: {
75
+ projectId: { type: "string", description: "Project ID or slug" },
76
+ parentId: { type: "number", description: "Parent work package ID to list children" },
77
+ assigneeId: { type: "string", description: "Assignee user ID or 'me'" },
78
+ status: { type: "string", description: "Status filter" },
79
+ pageSize: { type: "number", description: "Number of results", default: 100 }
80
+ }
81
+ }
82
+ },
83
+ {
84
+ name: "get_children",
85
+ description: "Get child work packages of a parent",
86
+ inputSchema: {
87
+ type: "object",
88
+ properties: {
89
+ parentId: { type: "number", description: "Parent work package ID" }
90
+ },
91
+ required: ["parentId"]
92
+ }
93
+ },
94
+ {
95
+ name: "list_statuses",
96
+ description: "List all available statuses",
97
+ inputSchema: { type: "object", properties: {} }
98
+ },
99
+ {
100
+ name: "list_types",
101
+ description: "List all work package types (Feature, Task, Bug, etc.)",
102
+ inputSchema: { type: "object", properties: {} }
103
+ },
104
+ {
105
+ name: "get_user",
106
+ description: "Get user information",
107
+ inputSchema: {
108
+ type: "object",
109
+ properties: {
110
+ userId: { type: "string", description: "User ID or 'me' for current user" }
111
+ },
112
+ required: ["userId"]
113
+ }
114
+ },
115
+ {
116
+ name: "create_work_package",
117
+ description: "Create a new work package (Task)",
118
+ inputSchema: {
119
+ type: "object",
120
+ properties: {
121
+ projectId: { type: "string", description: "Project ID or slug", default: "quan-ly-van-ban-hai-quan" },
122
+ subject: { type: "string", description: "Work package title/subject" },
123
+ description: { type: "string", description: "Work package description" },
124
+ typeId: { type: "number", description: "Type ID (1=Task, 4=Feature, 5=Bug)", default: 1 },
125
+ parentId: { type: "number", description: "Parent work package ID" },
126
+ assigneeId: { type: "number", description: "Assignee user ID" },
127
+ versionId: { type: "number", description: "Sprint/Version ID (22=Sprint 9, 23=Sprint 10)" },
128
+ startDate: { type: "string", description: "Start date (YYYY-MM-DD)" },
129
+ dueDate: { type: "string", description: "Due date (YYYY-MM-DD)" }
130
+ },
131
+ required: ["subject"]
132
+ }
133
+ },
134
+ {
135
+ name: "update_work_package",
136
+ description: "Update an existing work package",
137
+ inputSchema: {
138
+ type: "object",
139
+ properties: {
140
+ id: { type: "number", description: "Work package ID" },
141
+ subject: { type: "string", description: "New subject" },
142
+ description: { type: "string", description: "New description" },
143
+ statusId: { type: "number", description: "New status ID" },
144
+ assigneeId: { type: "number", description: "New assignee ID" },
145
+ versionId: { type: "number", description: "Sprint/Version ID (22=Sprint 9, 23=Sprint 10)" },
146
+ estimatedTime: { type: "string", description: "Estimated time in ISO 8601 duration format (e.g., PT2H for 2 hours, PT30M for 30 minutes)" }
147
+ },
148
+ required: ["id"]
149
+ }
150
+ },
151
+ {
152
+ name: "log_time",
153
+ description: "Log time entry for a work package",
154
+ inputSchema: {
155
+ type: "object",
156
+ properties: {
157
+ workPackageId: { type: "number", description: "Work package ID" },
158
+ hours: { type: "number", description: "Hours spent (e.g., 2 for 2 hours, 0.5 for 30 minutes)" },
159
+ comment: { type: "string", description: "Comment for the time entry" },
160
+ spentOn: { type: "string", description: "Date spent (YYYY-MM-DD), defaults to today" },
161
+ activityId: { type: "number", description: "Activity type ID (1=Development)", default: 1 }
162
+ },
163
+ required: ["workPackageId", "hours"]
164
+ }
165
+ },
166
+ {
167
+ name: "raw_api_call",
168
+ description: "Make a raw API call to any OpenProject endpoint",
169
+ inputSchema: {
170
+ type: "object",
171
+ properties: {
172
+ endpoint: { type: "string", description: "API endpoint (e.g., /api/v3/work_packages)" },
173
+ method: { type: "string", description: "HTTP method", default: "GET" },
174
+ body: { type: "object", description: "Request body for POST/PATCH" }
175
+ },
176
+ required: ["endpoint"]
177
+ }
178
+ }
179
+ ]
180
+ }));
181
+
182
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
183
+ const { name, arguments: args } = request.params;
184
+
185
+ try {
186
+ let result;
187
+
188
+ switch (name) {
189
+ case "list_projects": {
190
+ const pageSize = args.pageSize || 20;
191
+ result = await apiRequest("/api/v3/projects?pageSize=" + pageSize);
192
+ if (result._embedded && result._embedded.elements) {
193
+ result = result._embedded.elements.map(function(p) {
194
+ return {
195
+ id: p.id,
196
+ name: p.name,
197
+ identifier: p.identifier,
198
+ status: p.status
199
+ };
200
+ });
201
+ }
202
+ break;
203
+ }
204
+
205
+ case "get_work_package": {
206
+ result = await apiRequest("/api/v3/work_packages/" + args.id);
207
+ var parentHref = result._links && result._links.parent && result._links.parent.href;
208
+ result = {
209
+ id: result.id,
210
+ subject: result.subject,
211
+ description: result.des,
212
+ description: result.description?.raw,
213
+ status: result._links?.status?.title,
214
+ type: result._links?.type?.title,
215
+ assignee: result._links?.assignee?.title,
216
+ parent: result._links?.parent?.title,
217
+ parentId: result._linksDate,
218
+ dueDate: result.dueDate,
219
+ percentageDone: result.percentageDone
220
+ };
221
+ break;
222
+ }
223
+
224
+ case "list_work_packages": {
225
+ var filters = [];
226
+ if (args.parentId) {
227
+ filters.push({ parent: { operator: "=", values: [String(args.parentId)] } });
228
+ }
229
+ if (args.assigneeId) {
230
+ filters.push({ assignee: { operator: "=", values: [args.assigneeId] } });
231
+ }
232
+ if (args.status) {
233
+ filters.push({ status: { operator: "=", values: [args.status] } });
234
+ }
235
+
236
+ var endpoint = "/api/v3/work_packages";
237
+ if (args.projectId) {
238
+ endpoint = "/api/v3/projects/" + args.projectId + "/work_packages";
239
+ }
240
+
241
+ var params = new URLSearchParams();
242
+ params.set("pageSize", String(args.pageSize || 100));
243
+ if (filters.length > 0) {
244
+ params.set("filters", JSON.stringify(filters));
245
+ }
246
+
247
+ result = await apiRequest(endpoint + "?" + params.toString());
248
+ if (result._embedded && result._embedded.elements) {
249
+ result = result._embedded.elements.map(function(wp) {
250
+ var wpParentHref = wp._links && wp._links.parent && wp._links.parent.href;
251
+ return {
252
+ id: wp.id,
253
+ subject: wp.subject,
254
+ status: wp._links && wp._links.status ? wp._links.status.title : null,
255
+ type: wp._links && wp._links.type ? wp._links.type.title : null,
256
+ assignee: wp._links && wp._links.assignee ? wp._links.assignee.title : null,
257
+ parentId: wpParentHref ? wpParentHref.split("/").pop() : null
258
+ };
259
+ });
260
+ }
261
+ break;
262
+ }
263
+
264
+ case "get_children": {
265
+ var childFilters = [{ parent: { operator: "=", values: [String(args.parentId)] } }];
266
+ var childParams = new URLSearchParams();
267
+ childParams.set("pageSize", "100");
268
+ childParams.set("filters", JSON.stringify(childFilters));
269
+
270
+ result = await apiRequest("/api/v3/work_packages?" + childParams.toString());
271
+ if (result._embedded && result._embedded.elements) {
272
+ result = result._embedded.elements.map(function(wp) {
273
+ return {
274
+ id: wp.id,
275
+ subject: wp.subject,
276
+ status: wp._links && wp._links.status ? wp._links.status.title : null,
277
+ type: wp._links && wp._links.type ? wp._links.type.title : null,
278
+ assignee: wp._links && wp._links.assignee ? wp._links.assignee.title : null
279
+ };
280
+ });
281
+ }
282
+ break;
283
+ }
284
+
285
+ case "list_statuses": {
286
+ result = await apiRequest("/api/v3/statuses");
287
+ if (result._embedded && result._embedded.elements) {
288
+ result = result._embedded.elements.map(function(s) {
289
+ return {
290
+ id: s.id,
291
+ name: s.name,
292
+ isClosed: s.isClosed,
293
+ isDefault: s.isDefault
294
+ };
295
+ });
296
+ }
297
+ break;
298
+ }
299
+
300
+ case "list_types": {
301
+ result = await apiRequest("/api/v3/types");
302
+ if (result._embedded && result._embedded.elements) {
303
+ result = result._embedded.elements.map(function(t) {
304
+ return {
305
+ id: t.id,
306
+ name: t.name,
307
+ color: t.color
308
+ };
309
+ });
310
+ }
311
+ break;
312
+ }
313
+
314
+ case "get_user": {
315
+ var userId = args.userId === "me" ? "me" : args.userId;
316
+ result = await apiRequest("/api/v3/users/" + userId);
317
+ result = {
318
+ id: result.id,
319
+ name: result.name,
320
+ login: result.login,
321
+ email: result.email,
322
+ status: result.status
323
+ };
324
+ break;
325
+ }
326
+
327
+ case "create_work_package": {
328
+ var projectId = args.projectId || "quan-ly-van-ban-hai-quan";
329
+ var createBody = {
330
+ subject: args.subject,
331
+ _links: {
332
+ type: { href: "/api/v3/types/" + (args.typeId || 1) }
333
+ }
334
+ };
335
+
336
+ if (args.description) {
337
+ createBody.description = { format: "markdown", raw: args.description };
338
+ }
339
+ if (args.parentId) {
340
+ createBody._links.parent = { href: "/api/v3/work_packages/" + args.parentId };
341
+ }
342
+ if (args.assigneeId) {
343
+ createBody._links.assignee = { href: "/api/v3/users/" + args.assigneeId };
344
+ }
345
+ if (args.versionId) {
346
+ createBody._links.version = { href: "/api/v3/versions/" + args.versionId };
347
+ }
348
+ if (args.startDate) {
349
+ createBody.startDate = args.startDate;
350
+ }
351
+ if (args.dueDate) {
352
+ createBody.dueDate = args.dueDate;
353
+ }
354
+
355
+ result = await apiRequest("/api/v3/projects/" + projectId + "/work_packages", "POST", createBody);
356
+ result = {
357
+ id: result.id,
358
+ subject: result.subject,
359
+ status: result._links && result._links.status ? result._links.status.title : null,
360
+ type: result._links && result._links.type ? result._links.type.title : null,
361
+ version: result._links && result._links.version ? result._links.version.title : null
362
+ };
363
+ break;
364
+ }
365
+
366
+ case "update_work_package": {
367
+ var current = await apiRequest("/api/v3/work_packages/" + args.id);
368
+
369
+ var updateBody = {
370
+ lockVersion: current.lockVersion
371
+ };
372
+
373
+ if (args.subject) updateBody.subject = args.subject;
374
+ if (args.description) updateBody.description = { format: "markdown", raw: args.description };
375
+ if (args.estimatedTime) updateBody.estimatedTime = args.estimatedTime;
376
+ if (args.statusId || args.assigneeId || args.versionId) {
377
+ updateBody._links = {};
378
+ if (args.statusId) {
379
+ updateBody._links.status = { href: "/api/v3/statuses/" + args.statusId };
380
+ }
381
+ if (args.assigneeId) {
382
+ updateBody._links.assignee = { href: "/api/v3/users/" + args.assigneeId };
383
+ }
384
+ if (args.versionId) {
385
+ updateBody._links.version = { href: "/api/v3/versions/" + args.versionId };
386
+ }
387
+ }
388
+
389
+ result = await apiRequest("/api/v3/work_packages/" + args.id, "PATCH", updateBody);
390
+ result = {
391
+ id: result.id,
392
+ subject: result.subject,
393
+ status: result._links && result._links.status ? result._links.status.title : null,
394
+ version: result._links && result._links.version ? result._links.version.title : null,
395
+ estimatedTime: result.estimatedTime
396
+ };
397
+ break;
398
+ }
399
+
400
+ case "log_time": {
401
+ var today = new Date().toISOString().split('T')[0];
402
+ var timeBody = {
403
+ _links: {
404
+ workPackage: { href: "/api/v3/work_packages/" + args.workPackageId },
405
+ activity: { href: "/api/v3/time_entries/activities/" + (args.activityId || 1) }
406
+ },
407
+ hours: "PT" + args.hours + "H",
408
+ spentOn: args.spentOn || today,
409
+ comment: { format: "plain", raw: args.comment || "" }
410
+ };
411
+
412
+ result = await apiRequest("/api/v3/time_entries", "POST", timeBody);
413
+ result = {
414
+ id: result.id,
415
+ hours: result.hours,
416
+ spentOn: result.spentOn,
417
+ workPackageId: args.workPackageId,
418
+ comment: result.comment?.raw
419
+ };
420
+ break;
421
+ }
422
+
423
+ case "raw_api_call": {
424
+ result = await apiRequest(args.endpoint, args.method || "GET", args.body);
425
+ break;
426
+ }
427
+
428
+ default:
429
+ throw new Error("Unknown tool: " + name);
430
+ }
431
+
432
+ return {
433
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
434
+ };
435
+ } catch (error) {
436
+ return {
437
+ content: [{ type: "text", text: "Error: " + error.message }],
438
+ isError: true
439
+ };
440
+ }
441
+ });
442
+
443
+ const transport = new StdioServerTransport();
444
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "openproject-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for OpenProject API integration",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "openproject-mcp": "index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js"
12
+ },
13
+ "keywords": [
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "openproject",
17
+ "project-management",
18
+ "api",
19
+ "mcp-server"
20
+ ],
21
+ "author": "cyborgx0x <leeboykt@gmail.com>",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/cyborgx0x/mcp-openproject.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/cyborgx0x/mcp-openproject/issues"
29
+ },
30
+ "homepage": "https://github.com/cyborgx0x/mcp-openproject#readme",
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ },
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.0.0"
36
+ },
37
+ "files": [
38
+ "index.js",
39
+ "README.md",
40
+ "LICENSE"
41
+ ]
42
+ }