@azure-devops/mcp 2.2.0-nightly.20251013 → 2.2.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.
@@ -4,6 +4,7 @@ import { z } from "zod";
4
4
  const Test_Plan_Tools = {
5
5
  create_test_plan: "testplan_create_test_plan",
6
6
  create_test_case: "testplan_create_test_case",
7
+ update_test_case_steps: "testplan_update_test_case_steps",
7
8
  add_test_cases_to_suite: "testplan_add_test_cases_to_suite",
8
9
  test_results_from_build_id: "testplan_show_test_results_from_build_id",
9
10
  list_test_cases: "testplan_list_test_cases",
@@ -11,10 +12,6 @@ const Test_Plan_Tools = {
11
12
  create_test_suite: "testplan_create_test_suite",
12
13
  };
13
14
  function configureTestPlanTools(server, _, connectionProvider) {
14
- /*
15
- LIST OF TEST PLANS
16
- get list of test plans by project
17
- */
18
15
  server.tool(Test_Plan_Tools.list_test_plans, "Retrieve a paginated list of test plans from an Azure DevOps project. Allows filtering for active plans and toggling detailed information.", {
19
16
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
20
17
  filterActivePlans: z.boolean().default(true).describe("Filter to include only active test plans. Defaults to true."),
@@ -29,9 +26,6 @@ function configureTestPlanTools(server, _, connectionProvider) {
29
26
  content: [{ type: "text", text: JSON.stringify(testPlans, null, 2) }],
30
27
  };
31
28
  });
32
- /*
33
- Create Test Plan - CREATE
34
- */
35
29
  server.tool(Test_Plan_Tools.create_test_plan, "Creates a new test plan in the project.", {
36
30
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project where the test plan will be created."),
37
31
  name: z.string().describe("The name of the test plan to be created."),
@@ -56,9 +50,6 @@ function configureTestPlanTools(server, _, connectionProvider) {
56
50
  content: [{ type: "text", text: JSON.stringify(createdTestPlan, null, 2) }],
57
51
  };
58
52
  });
59
- /*
60
- Create Test Suite - CREATE
61
- */
62
53
  server.tool(Test_Plan_Tools.create_test_suite, "Creates a new test suite in a test plan.", {
63
54
  project: z.string().describe("Project ID or project name"),
64
55
  planId: z.number().describe("ID of the test plan that contains the suites"),
@@ -80,9 +71,6 @@ function configureTestPlanTools(server, _, connectionProvider) {
80
71
  content: [{ type: "text", text: JSON.stringify(createdTestSuite, null, 2) }],
81
72
  };
82
73
  });
83
- /*
84
- Add Test Cases to Suite - ADD
85
- */
86
74
  server.tool(Test_Plan_Tools.add_test_cases_to_suite, "Adds existing test cases to a test suite.", {
87
75
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
88
76
  planId: z.number().describe("The ID of the test plan."),
@@ -98,9 +86,6 @@ function configureTestPlanTools(server, _, connectionProvider) {
98
86
  content: [{ type: "text", text: JSON.stringify(addedTestCases, null, 2) }],
99
87
  };
100
88
  });
101
- /*
102
- Create Test Case - CREATE
103
- */
104
89
  server.tool(Test_Plan_Tools.create_test_case, "Creates a new test case work item.", {
105
90
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
106
91
  title: z.string().describe("The title of the test case."),
@@ -111,7 +96,8 @@ function configureTestPlanTools(server, _, connectionProvider) {
111
96
  priority: z.number().optional().describe("The priority of the test case."),
112
97
  areaPath: z.string().optional().describe("The area path for the test case."),
113
98
  iterationPath: z.string().optional().describe("The iteration path for the test case."),
114
- }, async ({ project, title, steps, priority, areaPath, iterationPath }) => {
99
+ testsWorkItemId: z.number().optional().describe("Optional work item id that will be set as a Microsoft.VSTS.Common.TestedBy-Reverse link to the test case."),
100
+ }, async ({ project, title, steps, priority, areaPath, iterationPath, testsWorkItemId }) => {
115
101
  const connection = await connectionProvider();
116
102
  const witClient = await connection.getWorkItemTrackingApi();
117
103
  let stepsXml;
@@ -125,6 +111,16 @@ function configureTestPlanTools(server, _, connectionProvider) {
125
111
  path: "/fields/System.Title",
126
112
  value: title,
127
113
  });
114
+ if (testsWorkItemId) {
115
+ patchDocument.push({
116
+ op: "add",
117
+ path: "/relations/-",
118
+ value: {
119
+ rel: "Microsoft.VSTS.Common.TestedBy-Reverse",
120
+ url: `${connection.serverUrl}/${project}/_apis/wit/workItems/${testsWorkItemId}`,
121
+ },
122
+ });
123
+ }
128
124
  if (stepsXml) {
129
125
  patchDocument.push({
130
126
  op: "add",
@@ -158,10 +154,32 @@ function configureTestPlanTools(server, _, connectionProvider) {
158
154
  content: [{ type: "text", text: JSON.stringify(workItem, null, 2) }],
159
155
  };
160
156
  });
161
- /*
162
- TEST PLANS
163
- Gets a list of test cases for a given testplan.
164
- */
157
+ server.tool(Test_Plan_Tools.update_test_case_steps, "Update an existing test case work item.", {
158
+ id: z.number().describe("The ID of the test case work item to update."),
159
+ steps: z
160
+ .string()
161
+ .describe("The steps to reproduce the test case. Make sure to format each step as '1. Step one|Expected result one\n2. Step two|Expected result two. USE '|' as the delimiter between step and expected result. DO NOT use '|' in the description of the step or expected result."),
162
+ }, async ({ id, steps }) => {
163
+ const connection = await connectionProvider();
164
+ const witClient = await connection.getWorkItemTrackingApi();
165
+ let stepsXml;
166
+ if (steps) {
167
+ stepsXml = convertStepsToXml(steps);
168
+ }
169
+ // Create JSON patch document for work item
170
+ const patchDocument = [];
171
+ if (stepsXml) {
172
+ patchDocument.push({
173
+ op: "add",
174
+ path: "/fields/Microsoft.VSTS.TCM.Steps",
175
+ value: stepsXml,
176
+ });
177
+ }
178
+ const workItem = await witClient.updateWorkItem({}, patchDocument, id);
179
+ return {
180
+ content: [{ type: "text", text: JSON.stringify(workItem, null, 2) }],
181
+ };
182
+ });
165
183
  server.tool(Test_Plan_Tools.list_test_cases, "Gets a list of test cases in the test plan.", {
166
184
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
167
185
  planid: z.number().describe("The ID of the test plan."),
@@ -174,9 +192,6 @@ function configureTestPlanTools(server, _, connectionProvider) {
174
192
  content: [{ type: "text", text: JSON.stringify(testcases, null, 2) }],
175
193
  };
176
194
  });
177
- /*
178
- Gets a list of test results for a given project and build ID
179
- */
180
195
  server.tool(Test_Plan_Tools.test_results_from_build_id, "Gets a list of test results for a given project and build ID.", {
181
196
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
182
197
  buildid: z.number().describe("The ID of the build."),
@@ -0,0 +1,213 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { z } from "zod";
4
+ const Test_Plan_Tools = {
5
+ create_test_plan: "testplan_create_test_plan",
6
+ create_test_case: "testplan_create_test_case",
7
+ add_test_cases_to_suite: "testplan_add_test_cases_to_suite",
8
+ test_results_from_build_id: "testplan_show_test_results_from_build_id",
9
+ list_test_cases: "testplan_list_test_cases",
10
+ list_test_plans: "testplan_list_test_plans",
11
+ };
12
+ function configureTestPlanTools(server, tokenProvider, connectionProvider) {
13
+ /*
14
+ LIST OF TEST PLANS
15
+ get list of test plans by project
16
+ */
17
+ server.tool(Test_Plan_Tools.list_test_plans, "Retrieve a paginated list of test plans from an Azure DevOps project. Allows filtering for active plans and toggling detailed information.", {
18
+ project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
19
+ filterActivePlans: z.boolean().default(true).describe("Filter to include only active test plans. Defaults to true."),
20
+ includePlanDetails: z.boolean().default(false).describe("Include detailed information about each test plan."),
21
+ continuationToken: z.string().optional().describe("Token to continue fetching test plans from a previous request."),
22
+ }, async ({ project, filterActivePlans, includePlanDetails, continuationToken }) => {
23
+ const owner = ""; //making owner an empty string untill we can figure out how to get owner id
24
+ const connection = await connectionProvider();
25
+ const testPlanApi = await connection.getTestPlanApi();
26
+ const testPlans = await testPlanApi.getTestPlans(project, owner, continuationToken, includePlanDetails, filterActivePlans);
27
+ return {
28
+ content: [{ type: "text", text: JSON.stringify(testPlans, null, 2) }],
29
+ };
30
+ });
31
+ /*
32
+ Create Test Plan - CREATE
33
+ */
34
+ server.tool(Test_Plan_Tools.create_test_plan, "Creates a new test plan in the project.", {
35
+ project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project where the test plan will be created."),
36
+ name: z.string().describe("The name of the test plan to be created."),
37
+ iteration: z.string().describe("The iteration path for the test plan"),
38
+ description: z.string().optional().describe("The description of the test plan"),
39
+ startDate: z.string().optional().describe("The start date of the test plan"),
40
+ endDate: z.string().optional().describe("The end date of the test plan"),
41
+ areaPath: z.string().optional().describe("The area path for the test plan"),
42
+ }, async ({ project, name, iteration, description, startDate, endDate, areaPath }) => {
43
+ const connection = await connectionProvider();
44
+ const testPlanApi = await connection.getTestPlanApi();
45
+ const testPlanToCreate = {
46
+ name,
47
+ iteration,
48
+ description,
49
+ startDate: startDate ? new Date(startDate) : undefined,
50
+ endDate: endDate ? new Date(endDate) : undefined,
51
+ areaPath,
52
+ };
53
+ const createdTestPlan = await testPlanApi.createTestPlan(testPlanToCreate, project);
54
+ return {
55
+ content: [{ type: "text", text: JSON.stringify(createdTestPlan, null, 2) }],
56
+ };
57
+ });
58
+ /*
59
+ Add Test Cases to Suite - ADD
60
+ */
61
+ server.tool(Test_Plan_Tools.add_test_cases_to_suite, "Adds existing test cases to a test suite.", {
62
+ project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
63
+ planId: z.number().describe("The ID of the test plan."),
64
+ suiteId: z.number().describe("The ID of the test suite."),
65
+ testCaseIds: z.string().or(z.array(z.string())).describe("The ID(s) of the test case(s) to add. "),
66
+ }, async ({ project, planId, suiteId, testCaseIds }) => {
67
+ const connection = await connectionProvider();
68
+ const testApi = await connection.getTestApi();
69
+ // If testCaseIds is an array, convert it to comma-separated string
70
+ const testCaseIdsString = Array.isArray(testCaseIds) ? testCaseIds.join(",") : testCaseIds;
71
+ const addedTestCases = await testApi.addTestCasesToSuite(project, planId, suiteId, testCaseIdsString);
72
+ return {
73
+ content: [{ type: "text", text: JSON.stringify(addedTestCases, null, 2) }],
74
+ };
75
+ });
76
+ /*
77
+ Create Test Case - CREATE
78
+ */
79
+ server.tool(Test_Plan_Tools.create_test_case, "Creates a new test case work item.", {
80
+ project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
81
+ title: z.string().describe("The title of the test case."),
82
+ steps: z
83
+ .string()
84
+ .optional()
85
+ .describe("The steps to reproduce the test case. Make sure to format each step as '1. Step one|Expected result one\n2. Step two|Expected result two. USE '|' as the delimiter between step and expected result. DO NOT use '|' in the description of the step or expected result."),
86
+ priority: z.number().optional().describe("The priority of the test case."),
87
+ areaPath: z.string().optional().describe("The area path for the test case."),
88
+ iterationPath: z.string().optional().describe("The iteration path for the test case."),
89
+ }, async ({ project, title, steps, priority, areaPath, iterationPath }) => {
90
+ const connection = await connectionProvider();
91
+ const witClient = await connection.getWorkItemTrackingApi();
92
+ let stepsXml;
93
+ if (steps) {
94
+ stepsXml = convertStepsToXml(steps);
95
+ }
96
+ // Create JSON patch document for work item
97
+ const patchDocument = [];
98
+ patchDocument.push({
99
+ op: "add",
100
+ path: "/fields/System.Title",
101
+ value: title,
102
+ });
103
+ if (stepsXml) {
104
+ patchDocument.push({
105
+ op: "add",
106
+ path: "/fields/Microsoft.VSTS.TCM.Steps",
107
+ value: stepsXml,
108
+ });
109
+ }
110
+ if (priority) {
111
+ patchDocument.push({
112
+ op: "add",
113
+ path: "/fields/Microsoft.VSTS.Common.Priority",
114
+ value: priority,
115
+ });
116
+ }
117
+ if (areaPath) {
118
+ patchDocument.push({
119
+ op: "add",
120
+ path: "/fields/System.AreaPath",
121
+ value: areaPath,
122
+ });
123
+ }
124
+ if (iterationPath) {
125
+ patchDocument.push({
126
+ op: "add",
127
+ path: "/fields/System.IterationPath",
128
+ value: iterationPath,
129
+ });
130
+ }
131
+ const workItem = await witClient.createWorkItem({}, patchDocument, project, "Test Case");
132
+ return {
133
+ content: [{ type: "text", text: JSON.stringify(workItem, null, 2) }],
134
+ };
135
+ });
136
+ /*
137
+ TEST PLANS
138
+ Gets a list of test cases for a given testplan.
139
+ */
140
+ server.tool(Test_Plan_Tools.list_test_cases, "Gets a list of test cases in the test plan.", {
141
+ project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
142
+ planid: z.number().describe("The ID of the test plan."),
143
+ suiteid: z.number().describe("The ID of the test suite."),
144
+ }, async ({ project, planid, suiteid }) => {
145
+ const connection = await connectionProvider();
146
+ const coreApi = await connection.getTestPlanApi();
147
+ const testcases = await coreApi.getTestCaseList(project, planid, suiteid);
148
+ return {
149
+ content: [{ type: "text", text: JSON.stringify(testcases, null, 2) }],
150
+ };
151
+ });
152
+ /*
153
+ Gets a list of test results for a given project and build ID
154
+ */
155
+ server.tool(Test_Plan_Tools.test_results_from_build_id, "Gets a list of test results for a given project and build ID.", {
156
+ project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
157
+ buildid: z.number().describe("The ID of the build."),
158
+ }, async ({ project, buildid }) => {
159
+ const connection = await connectionProvider();
160
+ const coreApi = await connection.getTestResultsApi();
161
+ const testResults = await coreApi.getTestResultDetailsForBuild(project, buildid);
162
+ return {
163
+ content: [{ type: "text", text: JSON.stringify(testResults, null, 2) }],
164
+ };
165
+ });
166
+ }
167
+ /*
168
+ * Helper function to convert steps text to XML format required
169
+ */
170
+ function convertStepsToXml(steps) {
171
+ // Accepts steps in the format: '1. Step one|Expected result one\n2. Step two|Expected result two'
172
+ const stepsLines = steps.split("\n").filter((line) => line.trim() !== "");
173
+ let xmlSteps = `<steps id="0" last="${stepsLines.length}">`;
174
+ for (let i = 0; i < stepsLines.length; i++) {
175
+ const stepLine = stepsLines[i].trim();
176
+ if (stepLine) {
177
+ // Split step and expected result by '|', fallback to default if not provided
178
+ const [stepPart, expectedPart] = stepLine.split("|").map((s) => s.trim());
179
+ const stepMatch = stepPart.match(/^(\d+)\.\s*(.+)$/);
180
+ const stepText = stepMatch ? stepMatch[2] : stepPart;
181
+ const expectedText = expectedPart || "Verify step completes successfully";
182
+ xmlSteps += `
183
+ <step id="${i + 1}" type="ActionStep">
184
+ <parameterizedString isformatted="true">${escapeXml(stepText)}</parameterizedString>
185
+ <parameterizedString isformatted="true">${escapeXml(expectedText)}</parameterizedString>
186
+ </step>`;
187
+ }
188
+ }
189
+ xmlSteps += "</steps>";
190
+ return xmlSteps;
191
+ }
192
+ /*
193
+ * Helper function to escape XML special characters
194
+ */
195
+ function escapeXml(unsafe) {
196
+ return unsafe.replace(/[<>&'"]/g, (c) => {
197
+ switch (c) {
198
+ case "<":
199
+ return "&lt;";
200
+ case ">":
201
+ return "&gt;";
202
+ case "&":
203
+ return "&amp;";
204
+ case "'":
205
+ return "&apos;";
206
+ case '"':
207
+ return "&quot;";
208
+ default:
209
+ return c;
210
+ }
211
+ });
212
+ }
213
+ export { Test_Plan_Tools, configureTestPlanTools };