targetprocess-mcp-server 2.3.1 → 2.4.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 +10 -0
- package/build/handlers/create_card_relation.js +35 -0
- package/build/handlers/delete_card_relation.js +19 -0
- package/build/handlers/get_card_relations.js +38 -0
- package/build/index.js +204 -6
- package/build/tp.js +94 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,6 +61,8 @@ Cards — Read
|
|
|
61
61
|
- `get_bug_comments` — Get comments on a bug (id, optional results)
|
|
62
62
|
- `get_user_story_comments` — Get comments on a user story (id, optional results)
|
|
63
63
|
- `get_user_story_test_cases` — Fetch the linked test plan and all its test cases (with steps) for a user story (resourceId)
|
|
64
|
+
- `get_card_relations` — Get all relations (Dependency, Blocker, Relation, Link, Duplicate) for a card, with direction and the related card (id)
|
|
65
|
+
- `get_relation_types` — List the relation types available in this instance (id + name); use to find the `relationType` name for `create_card_relation` (no params)
|
|
64
66
|
- `search_tp_cards` — Search TP cards by keyword or phrase in description (keyword, optional entityType: UserStories | Bugs, default: UserStories)
|
|
65
67
|
|
|
66
68
|
Cards — Write
|
|
@@ -70,6 +72,11 @@ Cards — Write
|
|
|
70
72
|
> Resolve state name → ID via `get_bug_workflows` before passing `entityStateId`
|
|
71
73
|
- `update_user_story` — Update an existing user story (id, optional title, optional description, optional projectId, optional teamId, optional entityStateId)
|
|
72
74
|
> Resolve state name → ID via `get_user_story_workflows` before passing `entityStateId`
|
|
75
|
+
- `create_card_relation` — Create a relation between two cards (masterId, slaveId, optional relationType name, default: "Depends on")
|
|
76
|
+
> The Slave depends on the Master — the Master must be done first
|
|
77
|
+
> `relationType` is matched by name against this instance's types; resolve exact names via `get_relation_types` (it's resolved to an ID before the API call, since TP rejects relation types passed by name)
|
|
78
|
+
- `delete_card_relation` — Remove a relation by its relation ID (relationId)
|
|
79
|
+
> Get the `relationId` from `get_card_relations` first — it's the relation's own ID, not a card ID
|
|
73
80
|
- `update_user_story_state` — Update the sub-state (team assignment entity state) for a user story (id, optional entityStateId, optional teamId, optional teamAssignmentId)
|
|
74
81
|
> 1. Call `get_user_story_content` first to find the assigned team and `teamAssignmentId`
|
|
75
82
|
> 2. Call `get_user_story_workflows` to resolve the target state name → `entityStateId`
|
|
@@ -84,6 +91,9 @@ Cards — Write
|
|
|
84
91
|
- `create_user_story` — Create a new user story (title, optional description, optional featureId, optional releaseId, optional projectId, optional teamId)
|
|
85
92
|
> [!NOTE]
|
|
86
93
|
> `projectId` and `teamId` are optional — fall back to `TP_PROJECT_ID` and `TP_TEAM_ID` from config
|
|
94
|
+
- `create_formatted_user_story` — Create a new user story with a structured template description (title, header object with asA/iWant/soThat, acceptanceCriteria array, scenarios array with Gherkin steps, optional definitions, examplesTable, edgeCases, references, notes, optional featureId, releaseId, projectId, teamId)
|
|
95
|
+
> [!NOTE]
|
|
96
|
+
> `projectId` and `teamId` are optional — fall back to `TP_PROJECT_ID` and `TP_TEAM_ID` from config
|
|
87
97
|
- `create_feature` — Create a new feature (title, optional description, optional epicId, optional releaseId, optional projectId, optional teamId)
|
|
88
98
|
> [!NOTE]
|
|
89
99
|
> `projectId` and `teamId` are optional — fall back to `TP_PROJECT_ID` and `TP_TEAM_ID` from config
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export async function handleCreateCardRelation(tp, params) {
|
|
2
|
+
const wanted = params.relationType || 'Depends on';
|
|
3
|
+
// Resolve the relation type name → Id. TP rejects relations whose RelationType
|
|
4
|
+
// is passed by Name (405 Method Not Allowed), so we look up the existing type.
|
|
5
|
+
const typesResponse = await tp.getRelationTypes();
|
|
6
|
+
const types = typesResponse?.Items || [];
|
|
7
|
+
const match = types.find((t) => t.Name.toLowerCase() === wanted.toLowerCase());
|
|
8
|
+
if (!match) {
|
|
9
|
+
const available = types.map((t) => `${t.Name} (id: ${t.Id})`).join(', ');
|
|
10
|
+
return {
|
|
11
|
+
content: [{
|
|
12
|
+
type: 'text',
|
|
13
|
+
text: `Unknown relation type "${wanted}". Available relation types in this instance: ${available || '(none returned)'}`
|
|
14
|
+
}],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const result = await tp.createRelation({
|
|
18
|
+
masterId: params.masterId,
|
|
19
|
+
slaveId: params.slaveId,
|
|
20
|
+
relationTypeId: String(match.Id),
|
|
21
|
+
});
|
|
22
|
+
if (!result.ok) {
|
|
23
|
+
return {
|
|
24
|
+
content: [{
|
|
25
|
+
type: 'text',
|
|
26
|
+
text: `Failed to create "${match.Name}" relation between master id: ${params.masterId} and slave id: ${params.slaveId}\n` +
|
|
27
|
+
`HTTP status: ${result.status}\n` +
|
|
28
|
+
`Response body: ${result.body}`
|
|
29
|
+
}],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: 'text', text: JSON.stringify(result.data) }],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export async function handleDeleteCardRelation(tp, relationId) {
|
|
2
|
+
const result = await tp.deleteRelation(relationId);
|
|
3
|
+
if (!result.ok) {
|
|
4
|
+
return {
|
|
5
|
+
content: [{
|
|
6
|
+
type: 'text',
|
|
7
|
+
text: `Failed to delete relation id: ${relationId}\n` +
|
|
8
|
+
`HTTP status: ${result.status}\n` +
|
|
9
|
+
`Response body: ${result.body}`
|
|
10
|
+
}],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
content: [{
|
|
15
|
+
type: 'text',
|
|
16
|
+
text: JSON.stringify({ deleted: true, relationId: Number(relationId), relation: result.data })
|
|
17
|
+
}],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export async function handleGetCardRelations(tp, id) {
|
|
2
|
+
const response = await tp.getCardRelations(id);
|
|
3
|
+
if (!response) {
|
|
4
|
+
return {
|
|
5
|
+
content: [{
|
|
6
|
+
type: 'text',
|
|
7
|
+
text: `Failed to get relations for card id: ${id}`
|
|
8
|
+
}],
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const items = response.Items || [];
|
|
12
|
+
if (items.length === 0) {
|
|
13
|
+
return {
|
|
14
|
+
content: [{
|
|
15
|
+
type: 'text',
|
|
16
|
+
text: `No relations found for card id: ${id}`,
|
|
17
|
+
}],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const cardId = Number(id);
|
|
21
|
+
const relations = items.map((relation) => {
|
|
22
|
+
const isMaster = relation.Master.Id === cardId;
|
|
23
|
+
const other = isMaster ? relation.Slave : relation.Master;
|
|
24
|
+
return {
|
|
25
|
+
relationId: relation.Id,
|
|
26
|
+
relationType: relation.RelationType.Name,
|
|
27
|
+
direction: isMaster ? 'outbound' : 'inbound',
|
|
28
|
+
relatedCard: {
|
|
29
|
+
id: other.Id,
|
|
30
|
+
name: other.Name,
|
|
31
|
+
entityType: other.EntityType?.Name,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: 'text', text: JSON.stringify(relations) }],
|
|
37
|
+
};
|
|
38
|
+
}
|
package/build/index.js
CHANGED
|
@@ -23,7 +23,7 @@ import { handleAddComment } from "./handlers/add_comment.js";
|
|
|
23
23
|
import { handleGetUserStoryComments } from "./handlers/get_user_story_comments.js";
|
|
24
24
|
import { handleGetBugComments } from "./handlers/get_bug_comments.js";
|
|
25
25
|
import { handleCreateBug } from "./handlers/create_bug.js";
|
|
26
|
-
import { handleCreateUserStory } from "./handlers/create_user_story.js";
|
|
26
|
+
// import { handleCreateUserStory } from "./handlers/create_user_story.js";
|
|
27
27
|
import { handleCreateFeature } from "./handlers/create_feature.js";
|
|
28
28
|
import { handleCreateTask } from "./handlers/create_task.js";
|
|
29
29
|
import { handleUpdateBug } from "./handlers/update_bug.js";
|
|
@@ -36,6 +36,9 @@ import { handleGetFeatureUserStories } from "./handlers/get_feature_user_stories
|
|
|
36
36
|
import { handleGetUserStoryBugs } from "./handlers/get_user_story_bugs.js";
|
|
37
37
|
import { handleGetCardCurrentStatus } from "./handlers/get_card_current_status.js";
|
|
38
38
|
import { handleUpdateUserStorySubState } from "./handlers/update_user_story_sub_state.js";
|
|
39
|
+
import { handleGetCardRelations } from "./handlers/get_card_relations.js";
|
|
40
|
+
import { handleCreateCardRelation } from "./handlers/create_card_relation.js";
|
|
41
|
+
import { handleDeleteCardRelation } from "./handlers/delete_card_relation.js";
|
|
39
42
|
const server = new McpServer({
|
|
40
43
|
name: "tp",
|
|
41
44
|
version: "1.0.0"
|
|
@@ -527,15 +530,98 @@ server.registerTool('create_bug', {
|
|
|
527
530
|
.describe('Optional Entity State ID — if user gave a state name, resolve it via "get_bug_workflows" first; defaults to "Done"'),
|
|
528
531
|
},
|
|
529
532
|
}, async ({ title, bugContent, origin, projectId, teamId, entityStateId }) => handleCreateBug(tp, { title, bugContent, origin, projectId, teamId, entityStateId }));
|
|
533
|
+
// server.registerTool(
|
|
534
|
+
// 'create_user_story',
|
|
535
|
+
// {
|
|
536
|
+
// title: 'Create a new user story',
|
|
537
|
+
// description: `Create a new user story in Targetprocess.`,
|
|
538
|
+
// inputSchema: {
|
|
539
|
+
// title: z.string()
|
|
540
|
+
// .describe('User story title'),
|
|
541
|
+
// description: z.string()
|
|
542
|
+
// .optional()
|
|
543
|
+
// .describe('Optional user story description (when provided, format as HTML)'),
|
|
544
|
+
// featureId: z.string()
|
|
545
|
+
// .min(5)
|
|
546
|
+
// .max(6)
|
|
547
|
+
// .optional()
|
|
548
|
+
// .describe('Optional Feature ID to link this user story to (e.g. 145636)'),
|
|
549
|
+
// releaseId: z.string()
|
|
550
|
+
// .min(5)
|
|
551
|
+
// .max(6)
|
|
552
|
+
// .optional()
|
|
553
|
+
// .describe('Optional Release ID to link this user story to (e.g. 145200)'),
|
|
554
|
+
// projectId: z.string()
|
|
555
|
+
// .optional()
|
|
556
|
+
// .describe('Optional Project ID — defaults to TP_PROJECT_ID from config'),
|
|
557
|
+
// teamId: z.string()
|
|
558
|
+
// .optional()
|
|
559
|
+
// .describe('Optional Team ID — defaults to TP_TEAM_ID from config'),
|
|
560
|
+
// },
|
|
561
|
+
// },
|
|
562
|
+
// async ({ title, description, featureId, releaseId, projectId, teamId }) =>
|
|
563
|
+
// handleCreateUserStory(tp, { title, description, featureId, releaseId, projectId, teamId })
|
|
564
|
+
// )
|
|
530
565
|
server.registerTool('create_user_story', {
|
|
531
|
-
title: 'Create a
|
|
532
|
-
description: `Create a new user story in Targetprocess
|
|
566
|
+
title: 'Create a formatted user story',
|
|
567
|
+
description: `Create a new user story in Targetprocess with a structured, template-driven description.
|
|
568
|
+
The description is assembled from discrete sections (header, definitions, acceptance criteria, Gherkin scenarios, edge cases, references, notes) and stored as HTML.
|
|
569
|
+
CRITICAL WORKFLOW: Before calling this tool, you MUST follow these steps:
|
|
570
|
+
1) IF the user specified a feature by name (not ID), call "get_feature_user_stories" or "search_tp_cards" to resolve the feature ID;
|
|
571
|
+
2) IF the user specified a release by name (not ID), call "get_current_releases" to resolve the release ID;
|
|
572
|
+
3) IF the user specified a team by name (not ID), call "get_teams" to find the matching team and use its ID as teamId;
|
|
573
|
+
4) IF the user specified a project by name (not ID), call "get_projects" to find the matching project and use its ID as projectId;`,
|
|
533
574
|
inputSchema: {
|
|
534
575
|
title: z.string()
|
|
535
576
|
.describe('User story title'),
|
|
536
|
-
|
|
577
|
+
header: z.object({
|
|
578
|
+
storyId: z.string()
|
|
579
|
+
.optional()
|
|
580
|
+
.describe('Story ID if already known (e.g. US-12345), omit for new stories'),
|
|
581
|
+
asA: z.string()
|
|
582
|
+
.describe('Role or persona — the "As a ..." part'),
|
|
583
|
+
iWant: z.string()
|
|
584
|
+
.describe('Goal — the "I want ..." part'),
|
|
585
|
+
soThat: z.string()
|
|
586
|
+
.describe('Benefit — the "so that ..." part'),
|
|
587
|
+
})
|
|
588
|
+
.describe('Story header following the As a / I want / so that format'),
|
|
589
|
+
definitions: z.array(z.object({
|
|
590
|
+
term: z.string()
|
|
591
|
+
.describe('The term, module name, or feature flag being defined'),
|
|
592
|
+
description: z.string()
|
|
593
|
+
.describe('Explanation of the term'),
|
|
594
|
+
})),
|
|
595
|
+
acceptanceCriteria: z.array(z.string())
|
|
596
|
+
.min(1)
|
|
597
|
+
.describe('Bullet checklist items for quick review sign-off — each string is one criterion'),
|
|
598
|
+
scenarios: z.array(z.object({
|
|
599
|
+
name: z.string()
|
|
600
|
+
.describe('Scenario name'),
|
|
601
|
+
steps: z.array(z.string())
|
|
602
|
+
.min(1)
|
|
603
|
+
.describe('Gherkin steps — each string is a full step line, e.g. "Given I am on the login page"'),
|
|
604
|
+
}))
|
|
605
|
+
.min(1)
|
|
606
|
+
.describe('Gherkin scenario blocks, one per behavior branch'),
|
|
607
|
+
examplesTable: z.string()
|
|
608
|
+
.optional()
|
|
609
|
+
.describe('Examples table for parameterized or matrix behavior (plain text or Gherkin Examples: table format)'),
|
|
610
|
+
edgeCases: z.array(z.object({
|
|
611
|
+
name: z.string()
|
|
612
|
+
.describe('Edge case scenario name'),
|
|
613
|
+
steps: z.array(z.string())
|
|
614
|
+
.min(1)
|
|
615
|
+
.describe('Gherkin steps for this edge case'),
|
|
616
|
+
}))
|
|
617
|
+
.optional()
|
|
618
|
+
.describe('Explicit edge case or boundary condition scenarios'),
|
|
619
|
+
references: z.string()
|
|
537
620
|
.optional()
|
|
538
|
-
.describe('
|
|
621
|
+
.describe('Links to Axure mockups or other external references (not inline in prose)'),
|
|
622
|
+
notes: z.string()
|
|
623
|
+
.optional()
|
|
624
|
+
.describe('Anything that helps understand the story context but does not fit other sections'),
|
|
539
625
|
featureId: z.string()
|
|
540
626
|
.min(5)
|
|
541
627
|
.max(6)
|
|
@@ -553,7 +639,63 @@ server.registerTool('create_user_story', {
|
|
|
553
639
|
.optional()
|
|
554
640
|
.describe('Optional Team ID — defaults to TP_TEAM_ID from config'),
|
|
555
641
|
},
|
|
556
|
-
}, async ({ title,
|
|
642
|
+
}, async ({ title, header, definitions, acceptanceCriteria, scenarios, examplesTable, edgeCases, references, notes, featureId, releaseId, projectId, teamId }) => {
|
|
643
|
+
const gherkinBlock = (items) => items.map((s, indx) => `<div><strong>Scenario ${indx + 1} - ${s.name}:</strong></div><div>${s.steps.map(step => `<div>\t${step}</div>`).join('\n')}</div>`).join('\n');
|
|
644
|
+
const parts = ['<div>'];
|
|
645
|
+
parts.push('<h3>Header</h3>');
|
|
646
|
+
if (header.storyId)
|
|
647
|
+
parts.push(`<p><strong>Story ID:</strong> ${header.storyId}</p>`);
|
|
648
|
+
parts.push(`<p>As a ${header.asA} / I want ${header.iWant} / so that ${header.soThat}</p>`);
|
|
649
|
+
if (definitions) {
|
|
650
|
+
parts.push('<h3>Definitions</h3>');
|
|
651
|
+
parts.push(`<div>`);
|
|
652
|
+
for (const def of definitions) {
|
|
653
|
+
parts.push(`<p><strong>${def.term}</strong> — ${def.description}</p>`);
|
|
654
|
+
}
|
|
655
|
+
parts.push(`</div>`);
|
|
656
|
+
}
|
|
657
|
+
parts.push('<h3>Acceptance Criteria</h3>');
|
|
658
|
+
parts.push('<ol>');
|
|
659
|
+
for (const criterion of acceptanceCriteria) {
|
|
660
|
+
parts.push(`<li>${criterion}</li>`);
|
|
661
|
+
}
|
|
662
|
+
parts.push('</ol>');
|
|
663
|
+
parts.push('<h3>Scenarios</h3>');
|
|
664
|
+
parts.push(gherkinBlock(scenarios));
|
|
665
|
+
if (examplesTable) {
|
|
666
|
+
parts.push('<h3>Examples</h3>');
|
|
667
|
+
parts.push(`<pre>${examplesTable}</pre>`);
|
|
668
|
+
}
|
|
669
|
+
if (edgeCases && edgeCases.length > 0) {
|
|
670
|
+
parts.push('<h3>Edge Cases</h3>');
|
|
671
|
+
parts.push(gherkinBlock(edgeCases));
|
|
672
|
+
}
|
|
673
|
+
if (references) {
|
|
674
|
+
parts.push('<h3>References</h3>');
|
|
675
|
+
parts.push(`<p>${references}</p>`);
|
|
676
|
+
}
|
|
677
|
+
if (notes) {
|
|
678
|
+
parts.push('<h3>Notes</h3>');
|
|
679
|
+
parts.push(`<p>${notes}</p>`);
|
|
680
|
+
}
|
|
681
|
+
parts.push('</div>');
|
|
682
|
+
const description = parts.join('\n');
|
|
683
|
+
const userStoryResponse = await tp.createUserStory({ title, description, featureId, releaseId, projectId, teamId });
|
|
684
|
+
if (!userStoryResponse) {
|
|
685
|
+
return {
|
|
686
|
+
content: [{
|
|
687
|
+
type: 'text',
|
|
688
|
+
text: `Failed to create formatted user story "${title}"\n JSON: ${JSON.stringify(userStoryResponse, null, 2)}`
|
|
689
|
+
}]
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
content: [{
|
|
694
|
+
type: 'text',
|
|
695
|
+
text: JSON.stringify(userStoryResponse)
|
|
696
|
+
}],
|
|
697
|
+
};
|
|
698
|
+
});
|
|
557
699
|
server.registerTool('create_feature', {
|
|
558
700
|
title: 'Create a new feature',
|
|
559
701
|
description: `Create a new Feature in Targetprocess.`,
|
|
@@ -1086,6 +1228,62 @@ server.registerTool('get_card_current_status', {
|
|
|
1086
1228
|
.describe('Type of the TP card — UserStory, Bug, or Feature (default: UserStory)'),
|
|
1087
1229
|
},
|
|
1088
1230
|
}, async ({ id, resourceType = 'UserStory' }) => handleGetCardCurrentStatus(tp, id, resourceType));
|
|
1231
|
+
server.registerTool('get_card_relations', {
|
|
1232
|
+
title: 'Get card relations',
|
|
1233
|
+
description: `Get all relations (Dependency, Blocker, Relation, Link, Duplicate) for a TP card (UserStory, Bug, Feature, etc.) by its ID.
|
|
1234
|
+
Each relation shows the related card and the direction:
|
|
1235
|
+
- "outbound" — this card is the Master (e.g. for Dependency, the related card depends on this card)
|
|
1236
|
+
- "inbound" — this card is the Slave (e.g. for Dependency, this card depends on the related card)`,
|
|
1237
|
+
inputSchema: {
|
|
1238
|
+
id: z.string()
|
|
1239
|
+
.min(5)
|
|
1240
|
+
.max(6)
|
|
1241
|
+
.describe('TP card ID (e.g. 145789)'),
|
|
1242
|
+
},
|
|
1243
|
+
}, async ({ id }) => handleGetCardRelations(tp, id));
|
|
1244
|
+
server.registerTool('get_relation_types', {
|
|
1245
|
+
title: 'Get relation types',
|
|
1246
|
+
description: 'Get all relation types available in this Targetprocess instance (id + name). Use this to find the correct relationType name for "create_card_relation".',
|
|
1247
|
+
}, async () => {
|
|
1248
|
+
const response = await tp.getRelationTypes();
|
|
1249
|
+
if (!response) {
|
|
1250
|
+
return {
|
|
1251
|
+
content: [{ type: 'text', text: `Failed to get relation types` }],
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
const items = (response.Items || []).map((t) => ({ id: t.Id, name: t.Name }));
|
|
1255
|
+
return {
|
|
1256
|
+
content: [{ type: 'text', text: JSON.stringify(items) }],
|
|
1257
|
+
};
|
|
1258
|
+
});
|
|
1259
|
+
server.registerTool('create_card_relation', {
|
|
1260
|
+
title: 'Create a relation between two cards',
|
|
1261
|
+
description: `Create a relation between two TP cards (UserStory, Bug, Feature, etc.).
|
|
1262
|
+
The Master is the source of the relation and the Slave is the target — e.g. for a "Depends on" relation, the Slave depends on the Master (Master must be done first).
|
|
1263
|
+
NOTE: relationType is matched by name against this instance's relation types. If unsure of the exact name, call "get_relation_types" first. The handler resolves the name to its ID before creating the relation.`,
|
|
1264
|
+
inputSchema: {
|
|
1265
|
+
masterId: z.string()
|
|
1266
|
+
.min(5)
|
|
1267
|
+
.max(6)
|
|
1268
|
+
.describe('Master card ID — the source of the relation (e.g. 145789)'),
|
|
1269
|
+
slaveId: z.string()
|
|
1270
|
+
.min(5)
|
|
1271
|
+
.max(6)
|
|
1272
|
+
.describe('Slave card ID — the target of the relation (e.g. 145790)'),
|
|
1273
|
+
relationType: z.string()
|
|
1274
|
+
.optional()
|
|
1275
|
+
.describe('Relation type name as defined in this instance (e.g. "Depends on", "Relate to"). Resolve exact names via "get_relation_types". Defaults to "Depends on".'),
|
|
1276
|
+
},
|
|
1277
|
+
}, async ({ masterId, slaveId, relationType }) => handleCreateCardRelation(tp, { masterId, slaveId, relationType }));
|
|
1278
|
+
server.registerTool('delete_card_relation', {
|
|
1279
|
+
title: 'Delete a relation between two cards',
|
|
1280
|
+
description: `Delete (remove) a relation between two TP cards by the relation's own ID — not the card IDs.
|
|
1281
|
+
To find the relationId, call "get_card_relations" for one of the cards; each entry includes a "relationId" field.`,
|
|
1282
|
+
inputSchema: {
|
|
1283
|
+
relationId: z.string()
|
|
1284
|
+
.describe('The relation ID to delete (the "relationId" field from "get_card_relations", e.g. 20748)'),
|
|
1285
|
+
},
|
|
1286
|
+
}, async ({ relationId }) => handleDeleteCardRelation(tp, relationId));
|
|
1089
1287
|
server.registerTool('get_in_progress_tasks_and_bugs', {
|
|
1090
1288
|
title: 'Get in-progress tasks and bugs for a user',
|
|
1091
1289
|
description: 'Get all Tasks and Bugs currently in "In Progress" state assigned to a given user ID',
|
package/build/tp.js
CHANGED
|
@@ -82,6 +82,54 @@ export class TpClient {
|
|
|
82
82
|
return null;
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
+
// Like post(), but on failure returns the HTTP status and raw response body
|
|
86
|
+
// instead of null, so callers can surface TP's error detail to the user.
|
|
87
|
+
async postRaw(params, data) {
|
|
88
|
+
params.param["access_token"] = this.token;
|
|
89
|
+
let _url = this.params(params);
|
|
90
|
+
console.error(JSON.stringify({ "TP_POST_URL": _url }));
|
|
91
|
+
console.error(JSON.stringify({ "TP_POST_BODY": data }));
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetch(_url, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: this.headers,
|
|
96
|
+
body: JSON.stringify(data),
|
|
97
|
+
});
|
|
98
|
+
const text = await response.text();
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
console.error(JSON.stringify({ "TP_POST_ERROR_STATUS": response.status, "TP_POST_ERROR_BODY": text }));
|
|
101
|
+
return { ok: false, status: response.status, body: text };
|
|
102
|
+
}
|
|
103
|
+
return { ok: true, data: (text ? JSON.parse(text) : null) };
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.error("Error making TP request:", error);
|
|
107
|
+
return { ok: false, status: 0, body: String(error) };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// DELETE request that, like postRaw(), surfaces the HTTP status and raw
|
|
111
|
+
// response body on failure so callers can report TP's error detail.
|
|
112
|
+
async del(params) {
|
|
113
|
+
params.param["access_token"] = this.token;
|
|
114
|
+
let _url = this.params(params);
|
|
115
|
+
console.error(JSON.stringify({ "TP_DELETE_URL": _url }));
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(_url, {
|
|
118
|
+
method: "DELETE",
|
|
119
|
+
headers: this.headers,
|
|
120
|
+
});
|
|
121
|
+
const text = await response.text();
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
console.error(JSON.stringify({ "TP_DELETE_ERROR_STATUS": response.status, "TP_DELETE_ERROR_BODY": text }));
|
|
124
|
+
return { ok: false, status: response.status, body: text };
|
|
125
|
+
}
|
|
126
|
+
return { ok: true, data: (text ? JSON.parse(text) : null) };
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.error("Error making TP request:", error);
|
|
130
|
+
return { ok: false, status: 0, body: String(error) };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
85
133
|
async getUserStory(userStoryId) {
|
|
86
134
|
const response = await this.get({
|
|
87
135
|
pathParam: ["userStories", userStoryId],
|
|
@@ -768,6 +816,52 @@ export class TpClient {
|
|
|
768
816
|
},
|
|
769
817
|
});
|
|
770
818
|
}
|
|
819
|
+
// TP's Relations endpoint silently returns nothing for an OR across
|
|
820
|
+
// Master.Id/Slave.Id, so we query each side separately and merge the results.
|
|
821
|
+
async getCardRelations(cardId) {
|
|
822
|
+
const include = "[Id,RelationType[Name],Master[Id,Name,EntityType],Slave[Id,Name,EntityType]]";
|
|
823
|
+
const query = (side) => this.get({
|
|
824
|
+
pathParam: ["Relations"],
|
|
825
|
+
param: {
|
|
826
|
+
"format": "json",
|
|
827
|
+
"where": `${side}.Id eq ${cardId}`,
|
|
828
|
+
"include": include,
|
|
829
|
+
"take": 100,
|
|
830
|
+
},
|
|
831
|
+
});
|
|
832
|
+
const [asMaster, asSlave] = await Promise.all([query("Master"), query("Slave")]);
|
|
833
|
+
const items = [...(asMaster?.Items ?? []), ...(asSlave?.Items ?? [])];
|
|
834
|
+
return { Next: "", Items: items };
|
|
835
|
+
}
|
|
836
|
+
async getRelationTypes() {
|
|
837
|
+
return this.get({
|
|
838
|
+
pathParam: ["RelationTypes"],
|
|
839
|
+
param: {
|
|
840
|
+
"format": "json",
|
|
841
|
+
"include": "[Id,Name]",
|
|
842
|
+
"take": 100,
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
// RelationType must be referenced by Id — passing it by Name makes TP try to
|
|
847
|
+
// create a new RelationType resource, which returns 405 Method Not Allowed.
|
|
848
|
+
async createRelation({ masterId, slaveId, relationTypeId }) {
|
|
849
|
+
const relation = {
|
|
850
|
+
"Master": { "Id": masterId },
|
|
851
|
+
"Slave": { "Id": slaveId },
|
|
852
|
+
"RelationType": { "Id": relationTypeId },
|
|
853
|
+
};
|
|
854
|
+
return this.postRaw({
|
|
855
|
+
pathParam: ["Relations"],
|
|
856
|
+
param: { "format": "json" },
|
|
857
|
+
}, relation);
|
|
858
|
+
}
|
|
859
|
+
async deleteRelation(relationId) {
|
|
860
|
+
return this.del({
|
|
861
|
+
pathParam: ["Relations", relationId],
|
|
862
|
+
param: { "format": "json" },
|
|
863
|
+
});
|
|
864
|
+
}
|
|
771
865
|
async addAttachedFile(generalId, source) {
|
|
772
866
|
let blob;
|
|
773
867
|
let fileName;
|