targetprocess-mcp-server 1.0.1
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 +83 -0
- package/build/src/config.js +10 -0
- package/build/src/index.js +517 -0
- package/build/src/tp.js +268 -0
- package/build/src/types.js +7 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Targetprocess MCP Server
|
|
2
|
+
|
|
3
|
+
An **MCP (Model Context Protocol) server** that enables AI assistants to interact with the **Targetprocess** project management platform through structured tools and natural language workflows.
|
|
4
|
+
|
|
5
|
+
This server exposes powerful capabilities for querying, creating, and managing Targetprocess entities such as user stories, bugs, tasks, and features.
|
|
6
|
+
|
|
7
|
+
It acts as a **bridge between LLM agents and the Targetprocess API**, providing:
|
|
8
|
+
|
|
9
|
+
- Structured **tool-based access**
|
|
10
|
+
- Semantic, natural-language-driven workflows
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
- Create Bugs based on user story
|
|
16
|
+
- Retreive TP entities: bugs, user stories
|
|
17
|
+
|
|
18
|
+
- **LLM-Friendly Tools**
|
|
19
|
+
- Designed for Claude MCP AI agents
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Use Cases examples:
|
|
24
|
+
|
|
25
|
+
- "Show me currently active release"
|
|
26
|
+
- "write me test cases based on 145322 tp user story"
|
|
27
|
+
- "add a comment to 145155 card saying 'test'"
|
|
28
|
+
|
|
29
|
+
- "write test cases based on 145640 user story"
|
|
30
|
+
- "write detailed test cases based on 145642 user story, format them inside html <div> element and add them as a comment"
|
|
31
|
+
|
|
32
|
+
- "create a bug based on 145637 user story where Add Tile flyout (for a Static Tile) not show"
|
|
33
|
+
|
|
34
|
+
- search for a card with 'Text Element' title
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
### Local
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"targetprocess": {
|
|
44
|
+
"command": "/Users/mcs/.config/nvm/versions/node/v22.14.0/bin/node",
|
|
45
|
+
"args": [
|
|
46
|
+
"/path/to/repository"
|
|
47
|
+
],
|
|
48
|
+
"env": {
|
|
49
|
+
"TP_TOKEN": "<your-tp-token>" // Settings -> Authentication and Security -> New Access Token,
|
|
50
|
+
"TP_BASE_URL": "<tp-api-endpoint>",
|
|
51
|
+
"TP_API_VERSION": "v1",
|
|
52
|
+
"TP_OWNER_ID": "<tp-owner-id>", // your user id
|
|
53
|
+
"TP_PROJECT_ID": "<tp-project-id>",
|
|
54
|
+
"TP_TEAM_ID": "<tp-team-id>"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
### From Repository
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"mcpServers": {
|
|
63
|
+
"github": {
|
|
64
|
+
"command": "https://github.com/SerhiiMaksymiv/targetprocess-mcp-server.git",
|
|
65
|
+
"env": {
|
|
66
|
+
"GITHUB_PERSONAL_ACCESS_TOKEN": "your_github_token_here",
|
|
67
|
+
...
|
|
68
|
+
"TP_TEAM_ID": ...
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Local Development
|
|
77
|
+
```
|
|
78
|
+
git clone --recursive https://github.com/SerhiiMaksymiv/targetprocess-mcp-server.git
|
|
79
|
+
cd targetprocess-mcp-server
|
|
80
|
+
|
|
81
|
+
npm install
|
|
82
|
+
npm run build
|
|
83
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
export const config = {
|
|
3
|
+
tp: {
|
|
4
|
+
url: process.env.TP_BASE_URL || "",
|
|
5
|
+
token: process.env.TP_TOKEN || "",
|
|
6
|
+
ownerId: process.env.TP_OWNER_ID || "",
|
|
7
|
+
projectId: process.env.TP_PROJECT_ID || "",
|
|
8
|
+
teamId: process.env.TP_TEAM_ID || "",
|
|
9
|
+
}
|
|
10
|
+
};
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { JSDOM } from "jsdom";
|
|
4
|
+
import { TpClient } from "./tp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
const server = new McpServer({
|
|
7
|
+
name: "tp",
|
|
8
|
+
version: "1.0.0"
|
|
9
|
+
}, {
|
|
10
|
+
capabilities: {
|
|
11
|
+
"tools": {
|
|
12
|
+
"listChanged": true
|
|
13
|
+
},
|
|
14
|
+
"prompts": {
|
|
15
|
+
"listChanged": true
|
|
16
|
+
},
|
|
17
|
+
"resources": {
|
|
18
|
+
"subscribe": true,
|
|
19
|
+
"listChanged": true
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const tp = new TpClient();
|
|
24
|
+
server.registerTool('get_user_story_content', {
|
|
25
|
+
title: 'Get TP user story content',
|
|
26
|
+
description: 'Get tp card (user story) content by specified id, e.g. 145789',
|
|
27
|
+
inputSchema: {
|
|
28
|
+
id: z.string()
|
|
29
|
+
.min(5)
|
|
30
|
+
.max(6)
|
|
31
|
+
.describe('TP (or tp) ID (e.g. 145789)')
|
|
32
|
+
},
|
|
33
|
+
}, async ({ id }) => {
|
|
34
|
+
const userStory = await tp.getUserStory(id);
|
|
35
|
+
if (!userStory) {
|
|
36
|
+
return {
|
|
37
|
+
content: [{
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: `Failed to get user story, id: ${id}\n JSON: ${JSON.stringify(userStory, null, 2)}`
|
|
40
|
+
}],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const description = userStory.Description || '';
|
|
44
|
+
if (!description) {
|
|
45
|
+
return {
|
|
46
|
+
content: [{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: `No description for ${id} tp card`,
|
|
49
|
+
}],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const dom = new JSDOM(`<html><body><div id="content">${description}</div></body></html>`);
|
|
53
|
+
const descriptionText = dom.window.document.getElementById('content')?.textContent;
|
|
54
|
+
return {
|
|
55
|
+
content: [{
|
|
56
|
+
type: 'text',
|
|
57
|
+
text: JSON.stringify(descriptionText)
|
|
58
|
+
}],
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
server.registerTool('get_current_releases', {
|
|
62
|
+
title: 'Get current releases',
|
|
63
|
+
description: 'Get current releases',
|
|
64
|
+
}, async ({}) => {
|
|
65
|
+
const releases = await tp.getCurrentReleases();
|
|
66
|
+
if (!releases) {
|
|
67
|
+
return {
|
|
68
|
+
content: [{
|
|
69
|
+
type: 'text',
|
|
70
|
+
text: `Failed to get current releases, JSON: ${JSON.stringify(releases, null, 2)}`
|
|
71
|
+
}],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const items = releases.Items || [];
|
|
75
|
+
if (items.length == 0) {
|
|
76
|
+
return {
|
|
77
|
+
content: [{
|
|
78
|
+
type: "text",
|
|
79
|
+
text: `No releases found`,
|
|
80
|
+
}],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
content: [{
|
|
85
|
+
type: 'text',
|
|
86
|
+
text: JSON.stringify(items)
|
|
87
|
+
}],
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
server.registerTool('get_release_user_stories', {
|
|
91
|
+
title: 'Get release user stories',
|
|
92
|
+
description: 'Get release user stories',
|
|
93
|
+
inputSchema: {
|
|
94
|
+
name: z.string()
|
|
95
|
+
.describe('Release name'),
|
|
96
|
+
results: z.number()
|
|
97
|
+
.default(50)
|
|
98
|
+
.optional()
|
|
99
|
+
.describe('Number of results to return, default is 50'),
|
|
100
|
+
},
|
|
101
|
+
}, async ({ name, results }) => {
|
|
102
|
+
const release = await tp.getReleaseUserStories({ name, results });
|
|
103
|
+
if (!release) {
|
|
104
|
+
return {
|
|
105
|
+
content: [{
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: `Failed to get ${name} release user stories, JSON: ${JSON.stringify(release, null, 2)}`
|
|
108
|
+
}],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const items = release.Items || [];
|
|
112
|
+
if (items.length == 0) {
|
|
113
|
+
return {
|
|
114
|
+
content: [{
|
|
115
|
+
type: "text",
|
|
116
|
+
text: `No release user stories found`,
|
|
117
|
+
}],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
content: [{
|
|
122
|
+
type: 'text',
|
|
123
|
+
text: JSON.stringify(items)
|
|
124
|
+
}],
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
server.registerTool('get_release_bugs', {
|
|
128
|
+
title: 'Get release bugs',
|
|
129
|
+
description: 'Get release bugs',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
name: z.string()
|
|
132
|
+
.describe('Release name'),
|
|
133
|
+
results: z.number()
|
|
134
|
+
.default(100)
|
|
135
|
+
.optional()
|
|
136
|
+
.describe('Number of results to return, default is 100'),
|
|
137
|
+
},
|
|
138
|
+
}, async ({ name, results }) => {
|
|
139
|
+
const release = await tp.getReleaseBugs({ name, results });
|
|
140
|
+
if (!release) {
|
|
141
|
+
return {
|
|
142
|
+
content: [{
|
|
143
|
+
type: 'text',
|
|
144
|
+
text: `Failed to get ${name} release bugs, JSON: ${JSON.stringify(release, null, 2)}`
|
|
145
|
+
}],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const items = release.Items || [];
|
|
149
|
+
if (items.length == 0) {
|
|
150
|
+
return {
|
|
151
|
+
content: [{
|
|
152
|
+
type: "text",
|
|
153
|
+
text: `No release bugs found`,
|
|
154
|
+
}],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
content: [{
|
|
159
|
+
type: 'text',
|
|
160
|
+
text: JSON.stringify(items)
|
|
161
|
+
}],
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
server.registerTool('get_release_features', {
|
|
165
|
+
title: 'Get release features',
|
|
166
|
+
description: 'Get release features',
|
|
167
|
+
inputSchema: {
|
|
168
|
+
name: z.string()
|
|
169
|
+
.describe('Release name'),
|
|
170
|
+
results: z.number()
|
|
171
|
+
.default(50)
|
|
172
|
+
.optional()
|
|
173
|
+
.describe('Number of results to return, default is 100'),
|
|
174
|
+
},
|
|
175
|
+
}, async ({ name, results }) => {
|
|
176
|
+
const release = await tp.getReleaseFeatures({ name, results });
|
|
177
|
+
if (!release) {
|
|
178
|
+
return {
|
|
179
|
+
content: [{
|
|
180
|
+
type: 'text',
|
|
181
|
+
text: `Failed to get ${name} release features, JSON: ${JSON.stringify(release, null, 2)}`
|
|
182
|
+
}],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const items = release.Items || [];
|
|
186
|
+
if (items.length == 0) {
|
|
187
|
+
return {
|
|
188
|
+
content: [{
|
|
189
|
+
type: "text",
|
|
190
|
+
text: `No release features found`,
|
|
191
|
+
}],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
content: [{
|
|
196
|
+
type: 'text',
|
|
197
|
+
text: JSON.stringify(items)
|
|
198
|
+
}],
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
server.registerTool('get_release_user_stories_with_description', {
|
|
202
|
+
title: 'Get release user stories with description',
|
|
203
|
+
description: `Get release user stories with description in the response.
|
|
204
|
+
Note: this is slower than "get_release_user_stories_names" tool,
|
|
205
|
+
but if user wants to get descriptions, then this tool is the way to go.
|
|
206
|
+
`,
|
|
207
|
+
inputSchema: {
|
|
208
|
+
name: z.string()
|
|
209
|
+
.describe('Release name'),
|
|
210
|
+
withDescription: z.boolean()
|
|
211
|
+
.describe('Include description in the response'),
|
|
212
|
+
},
|
|
213
|
+
}, async ({ name, withDescription }) => {
|
|
214
|
+
const release = await tp.getReleaseUserStories({ name, withDescription });
|
|
215
|
+
if (!release) {
|
|
216
|
+
return {
|
|
217
|
+
content: [{
|
|
218
|
+
type: 'text',
|
|
219
|
+
text: `Failed to get ${name} release user stories, JSON: ${JSON.stringify(release, null, 2)}`
|
|
220
|
+
}],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const items = release.Items || [];
|
|
224
|
+
if (items.length == 0) {
|
|
225
|
+
return {
|
|
226
|
+
content: [{
|
|
227
|
+
type: "text",
|
|
228
|
+
text: `No release user stories found`,
|
|
229
|
+
}],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
content: [{
|
|
234
|
+
type: 'text',
|
|
235
|
+
text: JSON.stringify(items)
|
|
236
|
+
}],
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
server.registerTool('get_release_open_bugs', {
|
|
240
|
+
title: 'Get release active bugs',
|
|
241
|
+
description: `Get release active bugs (bugs that are not closed, done, passed, ready to deploy)`,
|
|
242
|
+
inputSchema: {
|
|
243
|
+
name: z.string()
|
|
244
|
+
.describe('Release name'),
|
|
245
|
+
results: z.number()
|
|
246
|
+
.default(200)
|
|
247
|
+
.optional()
|
|
248
|
+
.describe('Number of results to return, default is 50'),
|
|
249
|
+
withDescription: z.boolean()
|
|
250
|
+
.describe('Include description in the response'),
|
|
251
|
+
},
|
|
252
|
+
}, async ({ name, results, withDescription }) => {
|
|
253
|
+
const release = await tp.getReleaseOpenBugs({ name, results, withDescription });
|
|
254
|
+
if (!release) {
|
|
255
|
+
return {
|
|
256
|
+
content: [{
|
|
257
|
+
type: 'text',
|
|
258
|
+
text: `Failed to get ${name} release bugs, JSON: ${JSON.stringify(release, null, 2)}`
|
|
259
|
+
}],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const items = release.Items || [];
|
|
263
|
+
if (items.length == 0) {
|
|
264
|
+
return {
|
|
265
|
+
content: [{
|
|
266
|
+
type: "text",
|
|
267
|
+
text: `No release bugs found`,
|
|
268
|
+
}],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
content: [{
|
|
273
|
+
type: 'text',
|
|
274
|
+
text: JSON.stringify(items)
|
|
275
|
+
}],
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
server.registerTool('get_release_open_user_stories', {
|
|
279
|
+
title: 'Get release active user stories',
|
|
280
|
+
description: `Get release active user stories (user stories that are not closed, done, passed, ready to deploy)`,
|
|
281
|
+
inputSchema: {
|
|
282
|
+
name: z.string()
|
|
283
|
+
.describe('Release name'),
|
|
284
|
+
results: z.number()
|
|
285
|
+
.default(100)
|
|
286
|
+
.optional()
|
|
287
|
+
.describe('Number of results to return, default is 50'),
|
|
288
|
+
withDescription: z.boolean()
|
|
289
|
+
.describe('Include description in the response'),
|
|
290
|
+
},
|
|
291
|
+
}, async ({ name, results, withDescription }) => {
|
|
292
|
+
const release = await tp.getReleaseOpenUserStories({ name, results, withDescription });
|
|
293
|
+
if (!release) {
|
|
294
|
+
return {
|
|
295
|
+
content: [{
|
|
296
|
+
type: 'text',
|
|
297
|
+
text: `Failed to get ${name} release user stories, JSON: ${JSON.stringify(release, null, 2)}`
|
|
298
|
+
}],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const items = release.Items || [];
|
|
302
|
+
if (items.length == 0) {
|
|
303
|
+
return {
|
|
304
|
+
content: [{
|
|
305
|
+
type: "text",
|
|
306
|
+
text: `No release user stories found`,
|
|
307
|
+
}],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
content: [{
|
|
312
|
+
type: 'text',
|
|
313
|
+
text: JSON.stringify(items)
|
|
314
|
+
}],
|
|
315
|
+
};
|
|
316
|
+
});
|
|
317
|
+
server.registerTool('search_all_cards_by_keyword', {
|
|
318
|
+
title: 'Search TP cards (user stories, bugs, features) by keyword or partial name',
|
|
319
|
+
description: `Searches tp cards (user stories, bugs, features) by keyword or partial name or partial keyphrase e.g. "Text Element"`,
|
|
320
|
+
inputSchema: {
|
|
321
|
+
keyword: z.string()
|
|
322
|
+
.describe('Keyword or partial name or keyphrase to search for'),
|
|
323
|
+
},
|
|
324
|
+
}, async ({ keyword }) => {
|
|
325
|
+
const results = await Promise.all([
|
|
326
|
+
tp.searchContainsNameText({ text: keyword, entityType: "UserStories" }),
|
|
327
|
+
tp.searchContainsNameText({ text: keyword, entityType: "Bugs" }),
|
|
328
|
+
tp.searchContainsNameText({ text: keyword, entityType: "Features" }),
|
|
329
|
+
]);
|
|
330
|
+
const generalResults = results.map((item) => item.Items).flat();
|
|
331
|
+
if (!generalResults) {
|
|
332
|
+
return {
|
|
333
|
+
content: [{
|
|
334
|
+
type: 'text',
|
|
335
|
+
text: `Failed to find card by keyword: "${keyword}"\n JSON: ${JSON.stringify(generalResults, null, 2)}`
|
|
336
|
+
}],
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const items = generalResults || [];
|
|
340
|
+
if (items.length == 0) {
|
|
341
|
+
return {
|
|
342
|
+
content: [{
|
|
343
|
+
type: "text",
|
|
344
|
+
text: `No results for ${keyword}`,
|
|
345
|
+
}],
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const parsedItems = items.map((item) => {
|
|
349
|
+
const dom = new JSDOM(`<html><body><div id="content">${item.Description}</div></body></html>`);
|
|
350
|
+
const descriptionText = dom.window.document.getElementById('content')?.textContent;
|
|
351
|
+
return {
|
|
352
|
+
title: item.Name,
|
|
353
|
+
id: item.Id,
|
|
354
|
+
description: descriptionText,
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
return {
|
|
358
|
+
content: [{
|
|
359
|
+
type: 'text',
|
|
360
|
+
text: JSON.stringify(parsedItems)
|
|
361
|
+
}],
|
|
362
|
+
};
|
|
363
|
+
});
|
|
364
|
+
server.registerTool('get_bug_content', {
|
|
365
|
+
title: 'Get TP bug content',
|
|
366
|
+
description: 'Get tp card (bug) content by specified id, e.g. 145789',
|
|
367
|
+
inputSchema: {
|
|
368
|
+
id: z.string()
|
|
369
|
+
.min(5)
|
|
370
|
+
.max(6)
|
|
371
|
+
.describe('Bug card ID (e.g. 145789)')
|
|
372
|
+
},
|
|
373
|
+
}, async ({ id }) => {
|
|
374
|
+
const bug = await tp.getBug(id);
|
|
375
|
+
if (!bug) {
|
|
376
|
+
return {
|
|
377
|
+
content: [{
|
|
378
|
+
type: 'text',
|
|
379
|
+
text: `Failed to get bug, id: ${id}\n JSON: ${JSON.stringify(bug, null, 2)}`
|
|
380
|
+
}],
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const description = bug.Description || '';
|
|
384
|
+
if (!description) {
|
|
385
|
+
return {
|
|
386
|
+
content: [{
|
|
387
|
+
type: "text",
|
|
388
|
+
text: `No description for ${id} tp card`,
|
|
389
|
+
}],
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
const dom = new JSDOM(`<html><body><div id="content">${description}</div></body></html>`);
|
|
393
|
+
const descriptionText = dom.window.document.getElementById('content')?.textContent;
|
|
394
|
+
return {
|
|
395
|
+
content: [{
|
|
396
|
+
type: 'text',
|
|
397
|
+
text: JSON.stringify(descriptionText)
|
|
398
|
+
}],
|
|
399
|
+
};
|
|
400
|
+
});
|
|
401
|
+
server.registerTool('add_comment', {
|
|
402
|
+
title: 'Adds provided content to TP card (user story) as a comment',
|
|
403
|
+
description: `Adds provided content as a comment to the specified tp card by id, e.g. 145789`,
|
|
404
|
+
inputSchema: {
|
|
405
|
+
id: z.string()
|
|
406
|
+
.min(5)
|
|
407
|
+
.max(6)
|
|
408
|
+
.describe('TP card id, usually user story or bug ID (e.g. 145789)'),
|
|
409
|
+
comment: z.string()
|
|
410
|
+
.describe('Comment content to add'),
|
|
411
|
+
},
|
|
412
|
+
}, async ({ id, comment }) => {
|
|
413
|
+
try {
|
|
414
|
+
const addCommentResponse = await tp.addComment(id, comment);
|
|
415
|
+
if (!addCommentResponse) {
|
|
416
|
+
return {
|
|
417
|
+
content: [{
|
|
418
|
+
type: 'text',
|
|
419
|
+
text: `Failed to add comment to user story, id: ${id}\n JSON: ${JSON.stringify(addCommentResponse, null, 2)}`
|
|
420
|
+
}]
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
content: [{
|
|
425
|
+
type: 'text',
|
|
426
|
+
text: JSON.stringify(addCommentResponse)
|
|
427
|
+
}],
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
console.error("Error adding comment to user story:", error);
|
|
432
|
+
return {
|
|
433
|
+
content: [{
|
|
434
|
+
type: 'text',
|
|
435
|
+
text: `Failed to add comment to user story, id: ${id}\n Error: ${error}`
|
|
436
|
+
}]
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
server.registerTool('create_bug', {
|
|
441
|
+
title: 'Create a new bug card based on provided card id',
|
|
442
|
+
description: `Create a new bug card based on provided card id that summarizes the problem in concise,
|
|
443
|
+
descriptive manner answering questions What? Where? When?,
|
|
444
|
+
and content explaining what happened in detail.
|
|
445
|
+
CRITICAL WORKFLOW: Before calling this tool, you MUST follow these steps:
|
|
446
|
+
1) IF you already have user story or bug card content, proceed to step 3 skipping step 2;
|
|
447
|
+
2) ELSE call "get_user_story_content" tool or "get_bug_content" tool to get user story or bug card content;
|
|
448
|
+
3) format the new bug inside html <div> tags with Issue Description, Steps to Reproduce, Expected Behavior, Actual Behavior;
|
|
449
|
+
4) add a comment to the card with bug Id provided in the first step and Title`,
|
|
450
|
+
inputSchema: {
|
|
451
|
+
title: z.string()
|
|
452
|
+
.describe('Bug card title that summarizes the problem in concise, descriptive, and actionable manner, enabling a developer to understand the issue without opening the report'),
|
|
453
|
+
id: z.string()
|
|
454
|
+
.min(5)
|
|
455
|
+
.max(6)
|
|
456
|
+
.describe(`Usually user story id or bug ID (e.g. 145789)`),
|
|
457
|
+
bugContent: z.string()
|
|
458
|
+
.describe(`Comment content to add, explain what happened in detail.
|
|
459
|
+
Include expected behaviour and what actually occurred.
|
|
460
|
+
Be specific and avoid assumptions.
|
|
461
|
+
Clearly outline the actions needed to trigger the bug.
|
|
462
|
+
Number each step so anyone can follow them easily`),
|
|
463
|
+
},
|
|
464
|
+
}, async ({ title, id, bugContent }) => {
|
|
465
|
+
const bugResponse = await tp.createBugBasedOnCardId(title, id, bugContent);
|
|
466
|
+
if (!bugResponse) {
|
|
467
|
+
return {
|
|
468
|
+
content: [{
|
|
469
|
+
type: 'text',
|
|
470
|
+
text: `Failed to create comment "${title}"\n JSON: ${JSON.stringify(bugResponse, null, 2)}`
|
|
471
|
+
}]
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
content: [{
|
|
476
|
+
type: 'text',
|
|
477
|
+
text: JSON.stringify(bugResponse)
|
|
478
|
+
}],
|
|
479
|
+
};
|
|
480
|
+
});
|
|
481
|
+
server.registerTool('create_test_plan', {
|
|
482
|
+
title: 'Create a new test plan for a user story',
|
|
483
|
+
description: `Create a new test plan with provided title and user story id`,
|
|
484
|
+
inputSchema: {
|
|
485
|
+
title: z.string()
|
|
486
|
+
.describe(`Test plan title that is taken from user story title`),
|
|
487
|
+
userStoryId: z.string()
|
|
488
|
+
.min(5)
|
|
489
|
+
.max(6)
|
|
490
|
+
.describe(`User story id, usually user story or bug ID (e.g. 145789)`),
|
|
491
|
+
},
|
|
492
|
+
}, async ({ title, userStoryId }) => {
|
|
493
|
+
const testPlanResponse = await tp.createTestPlan(title, userStoryId);
|
|
494
|
+
if (!testPlanResponse) {
|
|
495
|
+
return {
|
|
496
|
+
content: [{
|
|
497
|
+
type: 'text',
|
|
498
|
+
text: `Failed to create testPlanResponse "${title}"\n JSON: ${JSON.stringify(testPlanResponse, null, 2)}`
|
|
499
|
+
}]
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
content: [{
|
|
504
|
+
type: 'text',
|
|
505
|
+
text: JSON.stringify(testPlanResponse)
|
|
506
|
+
}],
|
|
507
|
+
};
|
|
508
|
+
});
|
|
509
|
+
async function main() {
|
|
510
|
+
const transport = new StdioServerTransport();
|
|
511
|
+
await server.connect(transport);
|
|
512
|
+
console.error("Weather MCP Server running on stdio");
|
|
513
|
+
}
|
|
514
|
+
main().catch((error) => {
|
|
515
|
+
console.error("Fatal error in main():", error);
|
|
516
|
+
process.exit(1);
|
|
517
|
+
});
|
package/build/src/tp.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { config } from "./config.js";
|
|
2
|
+
export class TpClient {
|
|
3
|
+
baseUrl = config.tp.url;
|
|
4
|
+
token = config.tp.token;
|
|
5
|
+
headers;
|
|
6
|
+
constructor() {
|
|
7
|
+
this.headers = {
|
|
8
|
+
"Content-Type": "application/json",
|
|
9
|
+
Accept: "application/json",
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
params(params) {
|
|
13
|
+
let _url = this.baseUrl;
|
|
14
|
+
for (const [key, value] of Object.entries(params.pathParam)) {
|
|
15
|
+
_url += value ? `/${key}/${value}` : `/${key}`;
|
|
16
|
+
}
|
|
17
|
+
let _urlParams = [];
|
|
18
|
+
for (const [key, value] of Object.entries(params.param)) {
|
|
19
|
+
_urlParams.push(`${key}=${encodeURIComponent(value)}`);
|
|
20
|
+
}
|
|
21
|
+
return _url + "/?" + _urlParams.join("&");
|
|
22
|
+
}
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
async getAll(params) {
|
|
25
|
+
const allItems = [];
|
|
26
|
+
let skip = 0;
|
|
27
|
+
const take = 100;
|
|
28
|
+
while (true) {
|
|
29
|
+
params.param["take"] = take;
|
|
30
|
+
params.param["skip"] = skip;
|
|
31
|
+
const page = await this.get(params);
|
|
32
|
+
if (!page?.Items?.length)
|
|
33
|
+
break;
|
|
34
|
+
allItems.push(...page.Items);
|
|
35
|
+
if (!page.Next)
|
|
36
|
+
break;
|
|
37
|
+
skip += take;
|
|
38
|
+
}
|
|
39
|
+
return allItems;
|
|
40
|
+
}
|
|
41
|
+
async get(params) {
|
|
42
|
+
params.param["access_token"] = this.token;
|
|
43
|
+
let _url = this.params(params);
|
|
44
|
+
try {
|
|
45
|
+
const response = await fetch(_url, {
|
|
46
|
+
method: "GET",
|
|
47
|
+
headers: this.headers
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
51
|
+
}
|
|
52
|
+
return (await response.json());
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error("Error making TP request:", error);
|
|
56
|
+
console.error("Request URL:", _url);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async post(params, data) {
|
|
61
|
+
params.param["access_token"] = this.token;
|
|
62
|
+
let _url = this.params(params);
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch(_url, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: this.headers,
|
|
67
|
+
body: JSON.stringify(data),
|
|
68
|
+
});
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
71
|
+
}
|
|
72
|
+
return (await response.json());
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.error("Error making TP request:", error);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async getUserStory(userStoryId) {
|
|
80
|
+
const response = await this.get({
|
|
81
|
+
pathParam: {
|
|
82
|
+
"userStories": userStoryId,
|
|
83
|
+
},
|
|
84
|
+
param: {
|
|
85
|
+
"format": "json",
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return response;
|
|
89
|
+
}
|
|
90
|
+
async getBug(bugId) {
|
|
91
|
+
const response = await this.get({
|
|
92
|
+
pathParam: { "bugs": bugId },
|
|
93
|
+
param: { "format": "json" }
|
|
94
|
+
});
|
|
95
|
+
return response;
|
|
96
|
+
}
|
|
97
|
+
async createBug(title, bugContent) {
|
|
98
|
+
const bug = {
|
|
99
|
+
"Name": title,
|
|
100
|
+
"Project": {
|
|
101
|
+
"Id": 59901
|
|
102
|
+
},
|
|
103
|
+
"customFields": [{
|
|
104
|
+
"name": "Origin",
|
|
105
|
+
"type": "DropDown",
|
|
106
|
+
"value": "Manual QA"
|
|
107
|
+
}],
|
|
108
|
+
"assignedTeams": [{
|
|
109
|
+
"team": {
|
|
110
|
+
"id": 127065
|
|
111
|
+
}
|
|
112
|
+
}],
|
|
113
|
+
"Description": bugContent,
|
|
114
|
+
};
|
|
115
|
+
return this.post({
|
|
116
|
+
pathParam: { "bugs": '' },
|
|
117
|
+
param: { "format": "json" },
|
|
118
|
+
}, bug);
|
|
119
|
+
}
|
|
120
|
+
async createBugBasedOnCardId(title, userStoryId, bugContent) {
|
|
121
|
+
const bug = {
|
|
122
|
+
"Name": title,
|
|
123
|
+
"Project": {
|
|
124
|
+
"Id": 59901
|
|
125
|
+
},
|
|
126
|
+
"UserStory": {
|
|
127
|
+
"Id": userStoryId
|
|
128
|
+
},
|
|
129
|
+
"customFields": [{
|
|
130
|
+
"name": "Origin",
|
|
131
|
+
"type": "DropDown",
|
|
132
|
+
"value": "Manual QA"
|
|
133
|
+
}],
|
|
134
|
+
"assignedTeams": [{
|
|
135
|
+
"team": {
|
|
136
|
+
"id": 127065
|
|
137
|
+
}
|
|
138
|
+
}],
|
|
139
|
+
"Description": bugContent,
|
|
140
|
+
};
|
|
141
|
+
return this.post({
|
|
142
|
+
pathParam: { "bugs": '' },
|
|
143
|
+
param: { "format": "json" },
|
|
144
|
+
}, bug);
|
|
145
|
+
}
|
|
146
|
+
async createTestPlan(title, userStoryId) {
|
|
147
|
+
const testPlan = {
|
|
148
|
+
"Name": title,
|
|
149
|
+
"Project": {
|
|
150
|
+
"Id": 59901
|
|
151
|
+
},
|
|
152
|
+
"LinkedGeneral": {
|
|
153
|
+
"ResourceType": "General",
|
|
154
|
+
"Id": userStoryId,
|
|
155
|
+
"Name": title,
|
|
156
|
+
},
|
|
157
|
+
"LinkedAssignable": {
|
|
158
|
+
"ResourceType": "Assignable",
|
|
159
|
+
"Id": userStoryId,
|
|
160
|
+
"Name": title,
|
|
161
|
+
},
|
|
162
|
+
"LinkedUserStory": {
|
|
163
|
+
"ResourceType": "UserStory",
|
|
164
|
+
"Id": userStoryId,
|
|
165
|
+
"Name": title,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
return this.post({
|
|
169
|
+
pathParam: { "testPlans": '' },
|
|
170
|
+
param: { "format": "json" },
|
|
171
|
+
}, testPlan);
|
|
172
|
+
}
|
|
173
|
+
async addComment(userStoryId, comment) {
|
|
174
|
+
const commentData = {
|
|
175
|
+
description: comment,
|
|
176
|
+
owner: {
|
|
177
|
+
id: "1504",
|
|
178
|
+
},
|
|
179
|
+
general: {
|
|
180
|
+
id: userStoryId,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
return this.post({
|
|
184
|
+
pathParam: { "comments": '' },
|
|
185
|
+
param: { "format": "json" },
|
|
186
|
+
}, commentData);
|
|
187
|
+
}
|
|
188
|
+
async searchContainsNameText({ text, entityType }) {
|
|
189
|
+
return this.get({
|
|
190
|
+
pathParam: { [entityType]: '' },
|
|
191
|
+
param: {
|
|
192
|
+
"format": "json",
|
|
193
|
+
"take": "25",
|
|
194
|
+
"where": `Name contains '${text}'`,
|
|
195
|
+
"include": "[Name, Description, Id]"
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
async getCurrentReleases() {
|
|
200
|
+
return this.get({
|
|
201
|
+
pathParam: { "Releases": '' },
|
|
202
|
+
param: {
|
|
203
|
+
"format": "json",
|
|
204
|
+
"where": `IsCurrent eq 'true'`,
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
async getReleaseUserStories({ name, results = 50, withDescription = false }) {
|
|
209
|
+
const includeFilter = withDescription ? "[Name, Description, Id]" : "[Name, Id]";
|
|
210
|
+
return this.get({
|
|
211
|
+
pathParam: { "UserStories": '' },
|
|
212
|
+
param: {
|
|
213
|
+
"format": "json",
|
|
214
|
+
"take": results,
|
|
215
|
+
"where": `Release.Name eq '${name}'`,
|
|
216
|
+
"include": includeFilter,
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
async getReleaseOpenUserStories({ name, results = 100, withDescription = false }) {
|
|
221
|
+
const includeFilter = withDescription ? "[Name, Description, Id]" : "[Name, Id]";
|
|
222
|
+
return this.get({
|
|
223
|
+
pathParam: { "UserStories": '' },
|
|
224
|
+
param: {
|
|
225
|
+
"format": "json",
|
|
226
|
+
"take": results,
|
|
227
|
+
"where": `Release.Name eq '${name}' and EntityState.Name ne 'Closed' and EntityState.Name ne 'Done' and EntityState.Name ne 'Passed Dev01 QA' and EntityState.Name ne 'Ready to Deploy to prod'`,
|
|
228
|
+
"include": includeFilter,
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
async getReleaseOpenBugs({ name, results = 200, withDescription = false }) {
|
|
233
|
+
const includeFilter = withDescription ? "[Name, Description, Id]" : "[Name, Id]";
|
|
234
|
+
return this.get({
|
|
235
|
+
pathParam: { "Bugs": '' },
|
|
236
|
+
param: {
|
|
237
|
+
"format": "json",
|
|
238
|
+
"take": results,
|
|
239
|
+
"where": `Release.Name eq '${name}' and EntityState.Name ne 'Closed' and EntityState.Name ne 'Done' and EntityState.Name ne 'Passed Dev01 QA' and EntityState.Name ne 'Ready to Deploy to prod'`,
|
|
240
|
+
"include": includeFilter,
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
async getReleaseBugs({ name, results = 100, withDescription = false }) {
|
|
245
|
+
const includeFilter = withDescription ? "[Name, Description, Id]" : "[Name, Id]";
|
|
246
|
+
return this.get({
|
|
247
|
+
pathParam: { "Bugs": '' },
|
|
248
|
+
param: {
|
|
249
|
+
"format": "json",
|
|
250
|
+
"take": results,
|
|
251
|
+
"where": `Release.Name eq '${name}'`,
|
|
252
|
+
"include": includeFilter,
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
async getReleaseFeatures({ name, results = 50, withDescription = false }) {
|
|
257
|
+
const includeFilter = withDescription ? "[Name, Description, Id]" : "[Name, Id]";
|
|
258
|
+
return this.get({
|
|
259
|
+
pathParam: { "Features": '' },
|
|
260
|
+
param: {
|
|
261
|
+
"format": "json",
|
|
262
|
+
"take": results,
|
|
263
|
+
"where": `Release.Name eq '${name}'`,
|
|
264
|
+
"include": includeFilter,
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "targetprocess-mcp-server",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"bin": {
|
|
5
|
+
"tp": "build/src/index.js"
|
|
6
|
+
},
|
|
7
|
+
"scripts": {
|
|
8
|
+
"prebuild": "rimraf build dist",
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"postbuild": "shx chmod +x build/**/*.js",
|
|
11
|
+
"prestart": "npm run build",
|
|
12
|
+
"start": "node build/src/main.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"build"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"ignore": [
|
|
19
|
+
"!build/",
|
|
20
|
+
"src/",
|
|
21
|
+
"test/"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/SerhiiMaksymiv/targetprocess-mcp-server.git"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20.x"
|
|
30
|
+
},
|
|
31
|
+
"version": "1.0.1",
|
|
32
|
+
"description": "MCP server for Tartget Process",
|
|
33
|
+
"main": "build/src/index.js",
|
|
34
|
+
"keywords": [
|
|
35
|
+
"mcp",
|
|
36
|
+
"tp",
|
|
37
|
+
"mcp-server",
|
|
38
|
+
"targetprocess",
|
|
39
|
+
"targetprocess-server"
|
|
40
|
+
],
|
|
41
|
+
"author": "Serhii Maksymiv",
|
|
42
|
+
"license": "ISC",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
45
|
+
"zod": "^3.25.76",
|
|
46
|
+
"@types/node": "^25.2.2",
|
|
47
|
+
"dotenv": "^17.2.4",
|
|
48
|
+
"@types/jsdom": "^27.0.0",
|
|
49
|
+
"typescript": "^5.9.3",
|
|
50
|
+
"rimraf": "^6.1.2",
|
|
51
|
+
"shx": "^0.4.0",
|
|
52
|
+
"tsup": "^8.5.0",
|
|
53
|
+
"jsdom": "^28.0.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {},
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/SerhiiMaksymiv/targetprocess-mcp-server/issues"
|
|
58
|
+
},
|
|
59
|
+
"homepage": "https://github.com/SerhiiMaksymiv/targetprocess-mcp-server#readme"
|
|
60
|
+
}
|