targetprocess-mcp-server 2.2.0 → 2.2.2

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
@@ -65,6 +65,9 @@ Cards — Write
65
65
  - `create_user_story` — Create a new user story (title, optional description, optional featureId, optional releaseId, optional projectId, optional teamId)
66
66
  > [!NOTE]
67
67
  > `projectId` and `teamId` are optional — fall back to `TP_PROJECT_ID` and `TP_TEAM_ID` from config
68
+ - `create_feature` — Create a new feature (title, optional description, optional epicId, optional releaseId, optional projectId, optional teamId)
69
+ > [!NOTE]
70
+ > `projectId` and `teamId` are optional — fall back to `TP_PROJECT_ID` and `TP_TEAM_ID` from config
68
71
  - `create_test_plan` — Create a test plan linked to a UserStory, Bug, or Feature (title, resourceId, optional resourceType, optional description/startDate/endDate)
69
72
  > [!NOTE]
70
73
  > requires `TP_PROJECT_ID`,
package/build/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { execSync } from "child_process";
3
+ import { homedir } from "os";
2
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
5
  import { z } from "zod";
4
6
  import { JSDOM } from "jsdom";
@@ -749,6 +751,49 @@ server.registerTool('create_user_story', {
749
751
  }],
750
752
  };
751
753
  });
754
+ server.registerTool('create_feature', {
755
+ title: 'Create a new feature',
756
+ description: `Create a new Feature in Targetprocess.`,
757
+ inputSchema: {
758
+ title: z.string()
759
+ .describe('Feature title'),
760
+ description: z.string()
761
+ .optional()
762
+ .describe('Optional feature description (when provided, format as HTML)'),
763
+ epicId: z.string()
764
+ .min(5)
765
+ .max(6)
766
+ .optional()
767
+ .describe('Optional Epic ID to link this feature to (e.g. 145636)'),
768
+ releaseId: z.string()
769
+ .min(5)
770
+ .max(6)
771
+ .optional()
772
+ .describe('Optional Release ID to link this feature to (e.g. 145200)'),
773
+ projectId: z.string()
774
+ .optional()
775
+ .describe('Optional Project ID — defaults to TP_PROJECT_ID from config'),
776
+ teamId: z.string()
777
+ .optional()
778
+ .describe('Optional Team ID — defaults to TP_TEAM_ID from config'),
779
+ },
780
+ }, async ({ title, description, epicId, releaseId, projectId, teamId }) => {
781
+ const featureResponse = await tp.createFeature({ title, description, epicId, releaseId, projectId, teamId });
782
+ if (!featureResponse) {
783
+ return {
784
+ content: [{
785
+ type: 'text',
786
+ text: `Failed to create feature "${title}"\n JSON: ${JSON.stringify(featureResponse, null, 2)}`
787
+ }]
788
+ };
789
+ }
790
+ return {
791
+ content: [{
792
+ type: 'text',
793
+ text: JSON.stringify(featureResponse)
794
+ }],
795
+ };
796
+ });
752
797
  server.registerTool('create_test_plan', {
753
798
  title: 'Create a new test plan linked to a TP card',
754
799
  description: `Create a new test plan linked to a UserStory, Bug, or Feature. Name and Project are required by the API; Description, StartDate, and EndDate are optional.`,
@@ -1195,6 +1240,92 @@ server.registerTool('add_test_cases_to_test_plan', {
1195
1240
  }]
1196
1241
  };
1197
1242
  });
1243
+ server.registerTool('add_attached_file_to_card', {
1244
+ title: 'Attach a file to a TP card',
1245
+ description: 'Upload and attach a local file to a Targetprocess entity (UserStory, Bug, Feature, etc.) by its ID',
1246
+ inputSchema: {
1247
+ generalId: z.string()
1248
+ .min(5)
1249
+ .max(6)
1250
+ .describe('TP entity ID to attach the file to (e.g. 145789)'),
1251
+ filePath: z.string()
1252
+ .describe('Absolute path to the local file to upload (e.g. /Users/mcs/Desktop/image.png)'),
1253
+ },
1254
+ }, async ({ generalId, filePath }) => {
1255
+ try {
1256
+ const result = await tp.addAttachedFile(generalId, filePath);
1257
+ if (!result) {
1258
+ return {
1259
+ content: [{
1260
+ type: 'text',
1261
+ text: `Failed to attach file "${filePath}" to card id: ${generalId}`
1262
+ }]
1263
+ };
1264
+ }
1265
+ return {
1266
+ content: [{
1267
+ type: 'text',
1268
+ text: result
1269
+ }]
1270
+ };
1271
+ }
1272
+ catch (error) {
1273
+ return {
1274
+ content: [{
1275
+ type: 'text',
1276
+ text: `Error attaching file to card ${generalId}: ${error}`
1277
+ }]
1278
+ };
1279
+ }
1280
+ });
1281
+ server.registerTool('find_file_on_disk', {
1282
+ title: 'Find a file on disk by name',
1283
+ description: `Search for a file by name on the local filesystem.
1284
+ First tries an exact (case-insensitive) match.
1285
+ If nothing is found, retries using the first half of the filename (without extension) as a wildcard — e.g. "screenshot_bug" becomes "*screensho*".
1286
+ Returns a list of matching absolute paths.`,
1287
+ inputSchema: {
1288
+ fileName: z.string()
1289
+ .describe('Filename to search for, with or without extension (e.g. "screenshot.png" or "report")'),
1290
+ searchDir: z.string()
1291
+ .optional()
1292
+ .describe('Directory to search in — defaults to the user home directory'),
1293
+ },
1294
+ }, async ({ fileName, searchDir }) => {
1295
+ const dir = searchDir || homedir();
1296
+ const runFind = (pattern) => {
1297
+ try {
1298
+ const out = execSync(`find "${dir}" -iname "${pattern}" 2>/dev/null`, {
1299
+ encoding: 'utf8',
1300
+ timeout: 15000,
1301
+ });
1302
+ return out.trim().split('\n').filter(Boolean);
1303
+ }
1304
+ catch {
1305
+ return [];
1306
+ }
1307
+ };
1308
+ // Exact match
1309
+ let files = runFind(fileName);
1310
+ if (files.length > 0) {
1311
+ return {
1312
+ content: [{ type: 'text', text: JSON.stringify({ match: 'exact', files }) }]
1313
+ };
1314
+ }
1315
+ // Partial match: first half of name without extension
1316
+ const dotIndex = fileName.lastIndexOf('.');
1317
+ const nameOnly = dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;
1318
+ const half = nameOnly.slice(0, Math.ceil(nameOnly.length / 2));
1319
+ files = runFind(`*${half}*`);
1320
+ if (files.length > 0) {
1321
+ return {
1322
+ content: [{ type: 'text', text: JSON.stringify({ match: 'partial', keyword: half, files }) }]
1323
+ };
1324
+ }
1325
+ return {
1326
+ content: [{ type: 'text', text: `No files found matching "${fileName}" or partial keyword "${half}" in ${dir}` }]
1327
+ };
1328
+ });
1198
1329
  async function main() {
1199
1330
  const transport = new StdioServerTransport();
1200
1331
  await server.connect(transport);
package/build/tp.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { readFileSync } from "fs";
2
+ import { basename } from "path";
1
3
  import { config } from "./config.js";
2
4
  export class TpClient {
3
5
  baseUrl = config.tp.url;
@@ -168,6 +170,23 @@ export class TpClient {
168
170
  param: { "format": "json" },
169
171
  }, userStory);
170
172
  }
173
+ async createFeature({ title, description, epicId, releaseId, projectId, teamId }) {
174
+ const feature = {
175
+ "Name": title,
176
+ "Project": { "Id": projectId || config.tp.projectId },
177
+ "assignedTeams": [{ "team": { "id": teamId || config.tp.teamId } }],
178
+ };
179
+ if (description)
180
+ feature["Description"] = description;
181
+ if (epicId)
182
+ feature["Epic"] = { "Id": epicId };
183
+ if (releaseId)
184
+ feature["Release"] = { "Id": releaseId };
185
+ return this.post({
186
+ pathParam: { "Features": '' },
187
+ param: { "format": "json" },
188
+ }, feature);
189
+ }
171
190
  async createBugBasedOnUserStory(title, userStoryId, bugContent) {
172
191
  const bug = {
173
192
  "Name": title,
@@ -474,4 +493,26 @@ export class TpClient {
474
493
  param: { "format": "json", }
475
494
  });
476
495
  }
496
+ async addAttachedFile(generalId, filePath) {
497
+ const fileContent = readFileSync(filePath);
498
+ const fileName = basename(filePath);
499
+ const formData = new FormData();
500
+ formData.append("generalId", generalId);
501
+ formData.append("file", new Blob([fileContent]), fileName);
502
+ const url = `${this.baseUrl}/UploadFile.ashx?access_token=${this.token}`;
503
+ try {
504
+ const response = await fetch(url, {
505
+ method: "POST",
506
+ body: formData,
507
+ });
508
+ if (!response.ok) {
509
+ throw new Error(`HTTP error! status: ${response.status}`);
510
+ }
511
+ return response.text();
512
+ }
513
+ catch (error) {
514
+ console.error("Error uploading file:", error);
515
+ return null;
516
+ }
517
+ }
477
518
  }
package/package.json CHANGED
@@ -25,7 +25,7 @@
25
25
  "engines": {
26
26
  "node": ">=20.x"
27
27
  },
28
- "version": "2.2.0",
28
+ "version": "2.2.2",
29
29
  "description": "MCP server for Tartget Process",
30
30
  "main": "build/index.js",
31
31
  "keywords": [