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.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/index.js +444 -0
- 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
|
+
}
|