targetprocess-mcp-server 2.4.0 → 2.4.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 +5 -0
- package/build/handlers/create_card_relation.js +35 -0
- package/build/handlers/delete_card_relation.js +19 -0
- package/build/handlers/format_existing_user_story.js +55 -0
- package/build/handlers/get_card_relations.js +38 -0
- package/build/index.js +271 -6
- package/build/tp.js +94 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,6 +91,11 @@ Cards — Write
|
|
|
91
91
|
- `create_user_story` — Create a new user story (title, optional description, optional featureId, optional releaseId, optional projectId, optional teamId)
|
|
92
92
|
> [!NOTE]
|
|
93
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
|
|
97
|
+
- `format_existing_user_story` — Re-format the description of an existing user story using the structured template (id, header object with asA/iWant/soThat, acceptanceCriteria array, scenarios array with Gherkin steps, optional title, definitions, examplesTable, edgeCases, references, notes)
|
|
98
|
+
> Call `get_user_story_content` first to read the current content, then reconstruct the structured fields before calling this tool
|
|
94
99
|
- `create_feature` — Create a new feature (title, optional description, optional epicId, optional releaseId, optional projectId, optional teamId)
|
|
95
100
|
> [!NOTE]
|
|
96
101
|
> `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,55 @@
|
|
|
1
|
+
export async function handleFormatExistingUserStory(tp, params) {
|
|
2
|
+
const { id, title, header, definitions, acceptanceCriteria, scenarios, examplesTable, edgeCases, references, notes, } = params;
|
|
3
|
+
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');
|
|
4
|
+
const parts = ['<div>'];
|
|
5
|
+
parts.push('<h3>Header</h3>');
|
|
6
|
+
if (header.storyId)
|
|
7
|
+
parts.push(`<p><strong>Story ID:</strong> ${header.storyId}</p>`);
|
|
8
|
+
parts.push(`<p>As a ${header.asA} / I want ${header.iWant} / so that ${header.soThat}</p>`);
|
|
9
|
+
if (definitions && definitions.length > 0) {
|
|
10
|
+
parts.push('<h3>Definitions</h3>');
|
|
11
|
+
parts.push(`<div>`);
|
|
12
|
+
for (const def of definitions) {
|
|
13
|
+
parts.push(`<p><strong>${def.term}</strong> — ${def.description}</p>`);
|
|
14
|
+
}
|
|
15
|
+
parts.push(`</div>`);
|
|
16
|
+
}
|
|
17
|
+
parts.push('<h3>Acceptance Criteria</h3>');
|
|
18
|
+
parts.push('<ol>');
|
|
19
|
+
for (const criterion of acceptanceCriteria) {
|
|
20
|
+
parts.push(`<li>${criterion}</li>`);
|
|
21
|
+
}
|
|
22
|
+
parts.push('</ol>');
|
|
23
|
+
parts.push('<h3>Scenarios</h3>');
|
|
24
|
+
parts.push(gherkinBlock(scenarios));
|
|
25
|
+
if (examplesTable) {
|
|
26
|
+
parts.push('<h3>Examples</h3>');
|
|
27
|
+
parts.push(`<pre>${examplesTable}</pre>`);
|
|
28
|
+
}
|
|
29
|
+
if (edgeCases && edgeCases.length > 0) {
|
|
30
|
+
parts.push('<h3>Edge Cases</h3>');
|
|
31
|
+
parts.push(gherkinBlock(edgeCases));
|
|
32
|
+
}
|
|
33
|
+
if (references) {
|
|
34
|
+
parts.push('<h3>References</h3>');
|
|
35
|
+
parts.push(`<p>${references}</p>`);
|
|
36
|
+
}
|
|
37
|
+
if (notes) {
|
|
38
|
+
parts.push('<h3>Notes</h3>');
|
|
39
|
+
parts.push(`<p>${notes}</p>`);
|
|
40
|
+
}
|
|
41
|
+
parts.push('</div>');
|
|
42
|
+
const description = parts.join('\n');
|
|
43
|
+
const response = await tp.updateUserStory({ id, title, description });
|
|
44
|
+
if (!response) {
|
|
45
|
+
return {
|
|
46
|
+
content: [{
|
|
47
|
+
type: 'text',
|
|
48
|
+
text: `Failed to format user story id: ${id}\n JSON: ${JSON.stringify(response, null, 2)}`
|
|
49
|
+
}],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: 'text', text: JSON.stringify(response) }],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -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,10 @@ 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";
|
|
42
|
+
import { handleFormatExistingUserStory } from "./handlers/format_existing_user_story.js";
|
|
39
43
|
const server = new McpServer({
|
|
40
44
|
name: "tp",
|
|
41
45
|
version: "1.0.0"
|
|
@@ -527,15 +531,98 @@ server.registerTool('create_bug', {
|
|
|
527
531
|
.describe('Optional Entity State ID — if user gave a state name, resolve it via "get_bug_workflows" first; defaults to "Done"'),
|
|
528
532
|
},
|
|
529
533
|
}, async ({ title, bugContent, origin, projectId, teamId, entityStateId }) => handleCreateBug(tp, { title, bugContent, origin, projectId, teamId, entityStateId }));
|
|
534
|
+
// server.registerTool(
|
|
535
|
+
// 'create_user_story',
|
|
536
|
+
// {
|
|
537
|
+
// title: 'Create a new user story',
|
|
538
|
+
// description: `Create a new user story in Targetprocess.`,
|
|
539
|
+
// inputSchema: {
|
|
540
|
+
// title: z.string()
|
|
541
|
+
// .describe('User story title'),
|
|
542
|
+
// description: z.string()
|
|
543
|
+
// .optional()
|
|
544
|
+
// .describe('Optional user story description (when provided, format as HTML)'),
|
|
545
|
+
// featureId: z.string()
|
|
546
|
+
// .min(5)
|
|
547
|
+
// .max(6)
|
|
548
|
+
// .optional()
|
|
549
|
+
// .describe('Optional Feature ID to link this user story to (e.g. 145636)'),
|
|
550
|
+
// releaseId: z.string()
|
|
551
|
+
// .min(5)
|
|
552
|
+
// .max(6)
|
|
553
|
+
// .optional()
|
|
554
|
+
// .describe('Optional Release ID to link this user story to (e.g. 145200)'),
|
|
555
|
+
// projectId: z.string()
|
|
556
|
+
// .optional()
|
|
557
|
+
// .describe('Optional Project ID — defaults to TP_PROJECT_ID from config'),
|
|
558
|
+
// teamId: z.string()
|
|
559
|
+
// .optional()
|
|
560
|
+
// .describe('Optional Team ID — defaults to TP_TEAM_ID from config'),
|
|
561
|
+
// },
|
|
562
|
+
// },
|
|
563
|
+
// async ({ title, description, featureId, releaseId, projectId, teamId }) =>
|
|
564
|
+
// handleCreateUserStory(tp, { title, description, featureId, releaseId, projectId, teamId })
|
|
565
|
+
// )
|
|
530
566
|
server.registerTool('create_user_story', {
|
|
531
|
-
title: 'Create a
|
|
532
|
-
description: `Create a new user story in Targetprocess
|
|
567
|
+
title: 'Create a formatted user story',
|
|
568
|
+
description: `Create a new user story in Targetprocess with a structured, template-driven description.
|
|
569
|
+
The description is assembled from discrete sections (header, definitions, acceptance criteria, Gherkin scenarios, edge cases, references, notes) and stored as HTML.
|
|
570
|
+
CRITICAL WORKFLOW: Before calling this tool, you MUST follow these steps:
|
|
571
|
+
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;
|
|
572
|
+
2) IF the user specified a release by name (not ID), call "get_current_releases" to resolve the release ID;
|
|
573
|
+
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;
|
|
574
|
+
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
575
|
inputSchema: {
|
|
534
576
|
title: z.string()
|
|
535
577
|
.describe('User story title'),
|
|
536
|
-
|
|
578
|
+
header: z.object({
|
|
579
|
+
storyId: z.string()
|
|
580
|
+
.optional()
|
|
581
|
+
.describe('Story ID if already known (e.g. US-12345), omit for new stories'),
|
|
582
|
+
asA: z.string()
|
|
583
|
+
.describe('Role or persona — the "As a ..." part'),
|
|
584
|
+
iWant: z.string()
|
|
585
|
+
.describe('Goal — the "I want ..." part'),
|
|
586
|
+
soThat: z.string()
|
|
587
|
+
.describe('Benefit — the "so that ..." part'),
|
|
588
|
+
})
|
|
589
|
+
.describe('Story header following the As a / I want / so that format'),
|
|
590
|
+
definitions: z.array(z.object({
|
|
591
|
+
term: z.string()
|
|
592
|
+
.describe('The term, module name, or feature flag being defined'),
|
|
593
|
+
description: z.string()
|
|
594
|
+
.describe('Explanation of the term'),
|
|
595
|
+
})),
|
|
596
|
+
acceptanceCriteria: z.array(z.string())
|
|
597
|
+
.min(1)
|
|
598
|
+
.describe('Bullet checklist items for quick review sign-off — each string is one criterion'),
|
|
599
|
+
scenarios: z.array(z.object({
|
|
600
|
+
name: z.string()
|
|
601
|
+
.describe('Scenario name'),
|
|
602
|
+
steps: z.array(z.string())
|
|
603
|
+
.min(1)
|
|
604
|
+
.describe('Gherkin steps — each string is a full step line, e.g. "Given I am on the login page"'),
|
|
605
|
+
}))
|
|
606
|
+
.min(1)
|
|
607
|
+
.describe('Gherkin scenario blocks, one per behavior branch'),
|
|
608
|
+
examplesTable: z.string()
|
|
537
609
|
.optional()
|
|
538
|
-
.describe('
|
|
610
|
+
.describe('Examples table for parameterized or matrix behavior (plain text or Gherkin Examples: table format)'),
|
|
611
|
+
edgeCases: z.array(z.object({
|
|
612
|
+
name: z.string()
|
|
613
|
+
.describe('Edge case scenario name'),
|
|
614
|
+
steps: z.array(z.string())
|
|
615
|
+
.min(1)
|
|
616
|
+
.describe('Gherkin steps for this edge case'),
|
|
617
|
+
}))
|
|
618
|
+
.optional()
|
|
619
|
+
.describe('Explicit edge case or boundary condition scenarios'),
|
|
620
|
+
references: z.string()
|
|
621
|
+
.optional()
|
|
622
|
+
.describe('Links to Axure mockups or other external references (not inline in prose)'),
|
|
623
|
+
notes: z.string()
|
|
624
|
+
.optional()
|
|
625
|
+
.describe('Anything that helps understand the story context but does not fit other sections'),
|
|
539
626
|
featureId: z.string()
|
|
540
627
|
.min(5)
|
|
541
628
|
.max(6)
|
|
@@ -553,7 +640,63 @@ server.registerTool('create_user_story', {
|
|
|
553
640
|
.optional()
|
|
554
641
|
.describe('Optional Team ID — defaults to TP_TEAM_ID from config'),
|
|
555
642
|
},
|
|
556
|
-
}, async ({ title,
|
|
643
|
+
}, async ({ title, header, definitions, acceptanceCriteria, scenarios, examplesTable, edgeCases, references, notes, featureId, releaseId, projectId, teamId }) => {
|
|
644
|
+
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');
|
|
645
|
+
const parts = ['<div>'];
|
|
646
|
+
parts.push('<h3>Header</h3>');
|
|
647
|
+
if (header.storyId)
|
|
648
|
+
parts.push(`<p><strong>Story ID:</strong> ${header.storyId}</p>`);
|
|
649
|
+
parts.push(`<p>As a ${header.asA} / I want ${header.iWant} / so that ${header.soThat}</p>`);
|
|
650
|
+
if (definitions) {
|
|
651
|
+
parts.push('<h3>Definitions</h3>');
|
|
652
|
+
parts.push(`<div>`);
|
|
653
|
+
for (const def of definitions) {
|
|
654
|
+
parts.push(`<p><strong>${def.term}</strong> — ${def.description}</p>`);
|
|
655
|
+
}
|
|
656
|
+
parts.push(`</div>`);
|
|
657
|
+
}
|
|
658
|
+
parts.push('<h3>Acceptance Criteria</h3>');
|
|
659
|
+
parts.push('<ol>');
|
|
660
|
+
for (const criterion of acceptanceCriteria) {
|
|
661
|
+
parts.push(`<li>${criterion}</li>`);
|
|
662
|
+
}
|
|
663
|
+
parts.push('</ol>');
|
|
664
|
+
parts.push('<h3>Scenarios</h3>');
|
|
665
|
+
parts.push(gherkinBlock(scenarios));
|
|
666
|
+
if (examplesTable) {
|
|
667
|
+
parts.push('<h3>Examples</h3>');
|
|
668
|
+
parts.push(`<pre>${examplesTable}</pre>`);
|
|
669
|
+
}
|
|
670
|
+
if (edgeCases && edgeCases.length > 0) {
|
|
671
|
+
parts.push('<h3>Edge Cases</h3>');
|
|
672
|
+
parts.push(gherkinBlock(edgeCases));
|
|
673
|
+
}
|
|
674
|
+
if (references) {
|
|
675
|
+
parts.push('<h3>References</h3>');
|
|
676
|
+
parts.push(`<p>${references}</p>`);
|
|
677
|
+
}
|
|
678
|
+
if (notes) {
|
|
679
|
+
parts.push('<h3>Notes</h3>');
|
|
680
|
+
parts.push(`<p>${notes}</p>`);
|
|
681
|
+
}
|
|
682
|
+
parts.push('</div>');
|
|
683
|
+
const description = parts.join('\n');
|
|
684
|
+
const userStoryResponse = await tp.createUserStory({ title, description, featureId, releaseId, projectId, teamId });
|
|
685
|
+
if (!userStoryResponse) {
|
|
686
|
+
return {
|
|
687
|
+
content: [{
|
|
688
|
+
type: 'text',
|
|
689
|
+
text: `Failed to create formatted user story "${title}"\n JSON: ${JSON.stringify(userStoryResponse, null, 2)}`
|
|
690
|
+
}]
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
content: [{
|
|
695
|
+
type: 'text',
|
|
696
|
+
text: JSON.stringify(userStoryResponse)
|
|
697
|
+
}],
|
|
698
|
+
};
|
|
699
|
+
});
|
|
557
700
|
server.registerTool('create_feature', {
|
|
558
701
|
title: 'Create a new feature',
|
|
559
702
|
description: `Create a new Feature in Targetprocess.`,
|
|
@@ -1086,6 +1229,62 @@ server.registerTool('get_card_current_status', {
|
|
|
1086
1229
|
.describe('Type of the TP card — UserStory, Bug, or Feature (default: UserStory)'),
|
|
1087
1230
|
},
|
|
1088
1231
|
}, async ({ id, resourceType = 'UserStory' }) => handleGetCardCurrentStatus(tp, id, resourceType));
|
|
1232
|
+
server.registerTool('get_card_relations', {
|
|
1233
|
+
title: 'Get card relations',
|
|
1234
|
+
description: `Get all relations (Dependency, Blocker, Relation, Link, Duplicate) for a TP card (UserStory, Bug, Feature, etc.) by its ID.
|
|
1235
|
+
Each relation shows the related card and the direction:
|
|
1236
|
+
- "outbound" — this card is the Master (e.g. for Dependency, the related card depends on this card)
|
|
1237
|
+
- "inbound" — this card is the Slave (e.g. for Dependency, this card depends on the related card)`,
|
|
1238
|
+
inputSchema: {
|
|
1239
|
+
id: z.string()
|
|
1240
|
+
.min(5)
|
|
1241
|
+
.max(6)
|
|
1242
|
+
.describe('TP card ID (e.g. 145789)'),
|
|
1243
|
+
},
|
|
1244
|
+
}, async ({ id }) => handleGetCardRelations(tp, id));
|
|
1245
|
+
server.registerTool('get_relation_types', {
|
|
1246
|
+
title: 'Get relation types',
|
|
1247
|
+
description: 'Get all relation types available in this Targetprocess instance (id + name). Use this to find the correct relationType name for "create_card_relation".',
|
|
1248
|
+
}, async () => {
|
|
1249
|
+
const response = await tp.getRelationTypes();
|
|
1250
|
+
if (!response) {
|
|
1251
|
+
return {
|
|
1252
|
+
content: [{ type: 'text', text: `Failed to get relation types` }],
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
const items = (response.Items || []).map((t) => ({ id: t.Id, name: t.Name }));
|
|
1256
|
+
return {
|
|
1257
|
+
content: [{ type: 'text', text: JSON.stringify(items) }],
|
|
1258
|
+
};
|
|
1259
|
+
});
|
|
1260
|
+
server.registerTool('create_card_relation', {
|
|
1261
|
+
title: 'Create a relation between two cards',
|
|
1262
|
+
description: `Create a relation between two TP cards (UserStory, Bug, Feature, etc.).
|
|
1263
|
+
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).
|
|
1264
|
+
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.`,
|
|
1265
|
+
inputSchema: {
|
|
1266
|
+
masterId: z.string()
|
|
1267
|
+
.min(5)
|
|
1268
|
+
.max(6)
|
|
1269
|
+
.describe('Master card ID — the source of the relation (e.g. 145789)'),
|
|
1270
|
+
slaveId: z.string()
|
|
1271
|
+
.min(5)
|
|
1272
|
+
.max(6)
|
|
1273
|
+
.describe('Slave card ID — the target of the relation (e.g. 145790)'),
|
|
1274
|
+
relationType: z.string()
|
|
1275
|
+
.optional()
|
|
1276
|
+
.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".'),
|
|
1277
|
+
},
|
|
1278
|
+
}, async ({ masterId, slaveId, relationType }) => handleCreateCardRelation(tp, { masterId, slaveId, relationType }));
|
|
1279
|
+
server.registerTool('delete_card_relation', {
|
|
1280
|
+
title: 'Delete a relation between two cards',
|
|
1281
|
+
description: `Delete (remove) a relation between two TP cards by the relation's own ID — not the card IDs.
|
|
1282
|
+
To find the relationId, call "get_card_relations" for one of the cards; each entry includes a "relationId" field.`,
|
|
1283
|
+
inputSchema: {
|
|
1284
|
+
relationId: z.string()
|
|
1285
|
+
.describe('The relation ID to delete (the "relationId" field from "get_card_relations", e.g. 20748)'),
|
|
1286
|
+
},
|
|
1287
|
+
}, async ({ relationId }) => handleDeleteCardRelation(tp, relationId));
|
|
1089
1288
|
server.registerTool('get_in_progress_tasks_and_bugs', {
|
|
1090
1289
|
title: 'Get in-progress tasks and bugs for a user',
|
|
1091
1290
|
description: 'Get all Tasks and Bugs currently in "In Progress" state assigned to a given user ID',
|
|
@@ -1187,6 +1386,72 @@ server.registerTool('get_my_time_logs', {
|
|
|
1187
1386
|
.describe('Number of entries to return, default is 25'),
|
|
1188
1387
|
},
|
|
1189
1388
|
}, async ({ take }) => handleGetMyTimeLogs(tp, take));
|
|
1389
|
+
server.registerTool('format_existing_user_story', {
|
|
1390
|
+
title: 'Format an existing user story',
|
|
1391
|
+
description: `Re-format the description of an existing user story using the structured template (Header, Definitions, Acceptance Criteria, Gherkin Scenarios, Edge Cases, References, Notes).
|
|
1392
|
+
This is identical in structure to "create_user_story" but updates an existing card instead of creating a new one.
|
|
1393
|
+
CRITICAL WORKFLOW: Before calling this tool, you MUST follow these steps:
|
|
1394
|
+
1) Call "get_user_story_content" to retrieve the current content of the user story;
|
|
1395
|
+
2) Extract or derive the structured fields (header, definitions, acceptanceCriteria, scenarios, etc.) from the existing description;
|
|
1396
|
+
3) Optionally update the title if the user requested it.`,
|
|
1397
|
+
inputSchema: {
|
|
1398
|
+
id: z.string()
|
|
1399
|
+
.min(5)
|
|
1400
|
+
.max(6)
|
|
1401
|
+
.describe('ID of the user story to format (e.g. 145789)'),
|
|
1402
|
+
title: z.string()
|
|
1403
|
+
.optional()
|
|
1404
|
+
.describe('Updated user story title — omit to keep the existing title'),
|
|
1405
|
+
header: z.object({
|
|
1406
|
+
storyId: z.string()
|
|
1407
|
+
.optional()
|
|
1408
|
+
.describe('Story ID if already known (e.g. US-12345), omit if not applicable'),
|
|
1409
|
+
asA: z.string()
|
|
1410
|
+
.describe('Role or persona — the "As a ..." part'),
|
|
1411
|
+
iWant: z.string()
|
|
1412
|
+
.describe('Goal — the "I want ..." part'),
|
|
1413
|
+
soThat: z.string()
|
|
1414
|
+
.describe('Benefit — the "so that ..." part'),
|
|
1415
|
+
})
|
|
1416
|
+
.describe('Story header following the As a / I want / so that format'),
|
|
1417
|
+
definitions: z.array(z.object({
|
|
1418
|
+
term: z.string()
|
|
1419
|
+
.describe('The term, module name, or feature flag being defined'),
|
|
1420
|
+
description: z.string()
|
|
1421
|
+
.describe('Explanation of the term'),
|
|
1422
|
+
})),
|
|
1423
|
+
acceptanceCriteria: z.array(z.string())
|
|
1424
|
+
.min(1)
|
|
1425
|
+
.describe('Bullet checklist items for quick review sign-off — each string is one criterion'),
|
|
1426
|
+
scenarios: z.array(z.object({
|
|
1427
|
+
name: z.string()
|
|
1428
|
+
.describe('Scenario name'),
|
|
1429
|
+
steps: z.array(z.string())
|
|
1430
|
+
.min(1)
|
|
1431
|
+
.describe('Gherkin steps — each string is a full step line, e.g. "Given I am on the login page"'),
|
|
1432
|
+
}))
|
|
1433
|
+
.min(1)
|
|
1434
|
+
.describe('Gherkin scenario blocks, one per behavior branch'),
|
|
1435
|
+
examplesTable: z.string()
|
|
1436
|
+
.optional()
|
|
1437
|
+
.describe('Examples table for parameterized or matrix behavior (plain text or Gherkin Examples: table format)'),
|
|
1438
|
+
edgeCases: z.array(z.object({
|
|
1439
|
+
name: z.string()
|
|
1440
|
+
.describe('Edge case scenario name'),
|
|
1441
|
+
steps: z.array(z.string())
|
|
1442
|
+
.min(1)
|
|
1443
|
+
.describe('Gherkin steps for this edge case'),
|
|
1444
|
+
}))
|
|
1445
|
+
.optional()
|
|
1446
|
+
.describe('Explicit edge case or boundary condition scenarios'),
|
|
1447
|
+
references: z.string()
|
|
1448
|
+
.optional()
|
|
1449
|
+
.describe('Links to Axure mockups or other external references (not inline in prose)'),
|
|
1450
|
+
notes: z.string()
|
|
1451
|
+
.optional()
|
|
1452
|
+
.describe('Anything that helps understand the story context but does not fit other sections'),
|
|
1453
|
+
},
|
|
1454
|
+
}, async ({ id, title, header, definitions, acceptanceCriteria, scenarios, examplesTable, edgeCases, references, notes }) => handleFormatExistingUserStory(tp, { id, title, header, definitions, acceptanceCriteria, scenarios, examplesTable, edgeCases, references, notes }));
|
|
1190
1455
|
async function main() {
|
|
1191
1456
|
const transport = new StdioServerTransport();
|
|
1192
1457
|
await server.connect(transport);
|
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;
|