@azure-devops/mcp 0.1.0 → 0.2.0-preview-oauth
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/LICENSE.md +21 -21
- package/README.md +320 -247
- package/dist/auth.js +74 -0
- package/dist/domains.js +1 -0
- package/dist/http.js +52 -0
- package/dist/index.js +61 -28
- package/dist/org-tenants.js +73 -0
- package/dist/orgtenants.js +73 -0
- package/dist/prompts.js +35 -10
- package/dist/server.js +36 -0
- package/dist/shared/domains.js +122 -0
- package/dist/shared/tool-validation.js +92 -0
- package/dist/tenant.js +73 -0
- package/dist/tools/advanced-security.js +108 -0
- package/dist/tools/advsec.js +108 -0
- package/dist/tools/auth.js +46 -4
- package/dist/tools/builds.js +146 -21
- package/dist/tools/core.js +73 -14
- package/dist/tools/releases.js +40 -15
- package/dist/tools/repos.js +421 -54
- package/dist/tools/repositories.js +666 -0
- package/dist/tools/search.js +100 -89
- package/dist/tools/test-plans.js +213 -0
- package/dist/tools/testplans.js +22 -21
- package/dist/tools/wiki.js +295 -37
- package/dist/tools/work-items.js +809 -0
- package/dist/tools/work.js +83 -39
- package/dist/tools/workitems.js +495 -171
- package/dist/tools.js +24 -14
- package/dist/useragent.js +20 -0
- package/dist/utils.js +52 -2
- package/dist/version.js +1 -1
- package/package.json +65 -55
package/dist/tools/search.js
CHANGED
|
@@ -1,142 +1,155 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { apiVersion
|
|
4
|
+
import { apiVersion } from "../utils.js";
|
|
5
5
|
import { orgName } from "../index.js";
|
|
6
6
|
import { VersionControlRecursionType } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
|
7
7
|
const SEARCH_TOOLS = {
|
|
8
8
|
search_code: "search_code",
|
|
9
9
|
search_wiki: "search_wiki",
|
|
10
|
-
search_workitem: "search_workitem"
|
|
10
|
+
search_workitem: "search_workitem",
|
|
11
11
|
};
|
|
12
|
-
function configureSearchTools(server, tokenProvider, connectionProvider) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Project: z.array(z.string()).optional().describe("Filter in these projects"),
|
|
24
|
-
Repository: z.array(z.string()).optional().describe("Filter in these repositories"),
|
|
25
|
-
Path: z.array(z.string()).optional().describe("Filter in these paths"),
|
|
26
|
-
Branch: z.array(z.string()).optional().describe("Filter in these branches"),
|
|
27
|
-
CodeElement: z.array(z.string()).optional().describe("Filter for these code elements (e.g., classes, functions, symbols)"),
|
|
28
|
-
// Note: CodeElement is optional and can be used to filter results by specific code elements.
|
|
29
|
-
// It can be a string or an array of strings.
|
|
30
|
-
// If provided, the search will only return results that match the specified code elements.
|
|
31
|
-
// This is useful for narrowing down the search to specific classes, functions, definitions, or symbols.
|
|
32
|
-
// Example: CodeElement: ["MyClass", "MyFunction"]
|
|
33
|
-
}).partial().optional(),
|
|
34
|
-
includeFacets: z.boolean().optional()
|
|
35
|
-
}).strict()
|
|
36
|
-
}, async ({ searchRequest }) => {
|
|
12
|
+
function configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
13
|
+
server.tool(SEARCH_TOOLS.search_code, "Search Azure DevOps Repositories for a given search text", {
|
|
14
|
+
searchText: z.string().describe("Keywords to search for in code repositories"),
|
|
15
|
+
project: z.array(z.string()).optional().describe("Filter by projects"),
|
|
16
|
+
repository: z.array(z.string()).optional().describe("Filter by repositories"),
|
|
17
|
+
path: z.array(z.string()).optional().describe("Filter by paths"),
|
|
18
|
+
branch: z.array(z.string()).optional().describe("Filter by branches"),
|
|
19
|
+
includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
|
|
20
|
+
skip: z.number().default(0).describe("Number of results to skip"),
|
|
21
|
+
top: z.number().default(5).describe("Maximum number of results to return"),
|
|
22
|
+
}, async ({ searchText, project, repository, path, branch, includeFacets, skip, top }) => {
|
|
37
23
|
const accessToken = await tokenProvider();
|
|
38
24
|
const connection = await connectionProvider();
|
|
39
25
|
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/codesearchresults?api-version=${apiVersion}`;
|
|
26
|
+
const requestBody = {
|
|
27
|
+
searchText,
|
|
28
|
+
includeFacets,
|
|
29
|
+
$skip: skip,
|
|
30
|
+
$top: top,
|
|
31
|
+
};
|
|
32
|
+
const filters = {};
|
|
33
|
+
if (project && project.length > 0)
|
|
34
|
+
filters.Project = project;
|
|
35
|
+
if (repository && repository.length > 0)
|
|
36
|
+
filters.Repository = repository;
|
|
37
|
+
if (path && path.length > 0)
|
|
38
|
+
filters.Path = path;
|
|
39
|
+
if (branch && branch.length > 0)
|
|
40
|
+
filters.Branch = branch;
|
|
41
|
+
if (Object.keys(filters).length > 0) {
|
|
42
|
+
requestBody.filters = filters;
|
|
43
|
+
}
|
|
40
44
|
const response = await fetch(url, {
|
|
41
45
|
method: "POST",
|
|
42
46
|
headers: {
|
|
43
47
|
"Content-Type": "application/json",
|
|
44
|
-
Authorization: `Bearer ${accessToken
|
|
45
|
-
"User-Agent":
|
|
48
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
49
|
+
"User-Agent": userAgentProvider(),
|
|
46
50
|
},
|
|
47
|
-
body: JSON.stringify(
|
|
51
|
+
body: JSON.stringify(requestBody),
|
|
48
52
|
});
|
|
49
53
|
if (!response.ok) {
|
|
50
54
|
throw new Error(`Azure DevOps Code Search API error: ${response.status} ${response.statusText}`);
|
|
51
55
|
}
|
|
52
56
|
const resultText = await response.text();
|
|
53
57
|
const resultJson = JSON.parse(resultText);
|
|
54
|
-
const topResults = Array.isArray(resultJson.results)
|
|
55
|
-
? resultJson.results.slice(0, Math.min(searchRequest.$top, resultJson.results.length))
|
|
56
|
-
: [];
|
|
57
58
|
const gitApi = await connection.getGitApi();
|
|
58
|
-
const combinedResults = await fetchCombinedResults(
|
|
59
|
+
const combinedResults = await fetchCombinedResults(resultJson.results ?? [], gitApi);
|
|
59
60
|
return {
|
|
60
|
-
content: [
|
|
61
|
-
{ type: "text", text: resultText + JSON.stringify(combinedResults) }
|
|
62
|
-
]
|
|
61
|
+
content: [{ type: "text", text: resultText + JSON.stringify(combinedResults) }],
|
|
63
62
|
};
|
|
64
63
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
$top: z.number().default(10).describe("Number of results to return (for pagination)"),
|
|
74
|
-
filters: z.object({
|
|
75
|
-
Project: z.array(z.string()).optional().describe("Filter in these projects"),
|
|
76
|
-
Wiki: z.array(z.string()).optional().describe("Filter in these wiki names"),
|
|
77
|
-
}).partial().optional().describe("Filters to apply to the search text"),
|
|
78
|
-
includeFacets: z.boolean().optional()
|
|
79
|
-
}).strict()
|
|
80
|
-
}, async ({ searchRequest }) => {
|
|
64
|
+
server.tool(SEARCH_TOOLS.search_wiki, "Search Azure DevOps Wiki for a given search text", {
|
|
65
|
+
searchText: z.string().describe("Keywords to search for wiki pages"),
|
|
66
|
+
project: z.array(z.string()).optional().describe("Filter by projects"),
|
|
67
|
+
wiki: z.array(z.string()).optional().describe("Filter by wiki names"),
|
|
68
|
+
includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
|
|
69
|
+
skip: z.number().default(0).describe("Number of results to skip"),
|
|
70
|
+
top: z.number().default(10).describe("Maximum number of results to return"),
|
|
71
|
+
}, async ({ searchText, project, wiki, includeFacets, skip, top }) => {
|
|
81
72
|
const accessToken = await tokenProvider();
|
|
82
73
|
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/wikisearchresults?api-version=${apiVersion}`;
|
|
74
|
+
const requestBody = {
|
|
75
|
+
searchText,
|
|
76
|
+
includeFacets,
|
|
77
|
+
$skip: skip,
|
|
78
|
+
$top: top,
|
|
79
|
+
};
|
|
80
|
+
const filters = {};
|
|
81
|
+
if (project && project.length > 0)
|
|
82
|
+
filters.Project = project;
|
|
83
|
+
if (wiki && wiki.length > 0)
|
|
84
|
+
filters.Wiki = wiki;
|
|
85
|
+
if (Object.keys(filters).length > 0) {
|
|
86
|
+
requestBody.filters = filters;
|
|
87
|
+
}
|
|
83
88
|
const response = await fetch(url, {
|
|
84
89
|
method: "POST",
|
|
85
90
|
headers: {
|
|
86
91
|
"Content-Type": "application/json",
|
|
87
|
-
Authorization: `Bearer ${accessToken
|
|
88
|
-
"User-Agent":
|
|
92
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
93
|
+
"User-Agent": userAgentProvider(),
|
|
89
94
|
},
|
|
90
|
-
body: JSON.stringify(
|
|
95
|
+
body: JSON.stringify(requestBody),
|
|
91
96
|
});
|
|
92
97
|
if (!response.ok) {
|
|
93
98
|
throw new Error(`Azure DevOps Wiki Search API error: ${response.status} ${response.statusText}`);
|
|
94
99
|
}
|
|
95
100
|
const result = await response.text();
|
|
96
101
|
return {
|
|
97
|
-
content: [
|
|
98
|
-
{ type: "text", text: result }
|
|
99
|
-
]
|
|
102
|
+
content: [{ type: "text", text: result }],
|
|
100
103
|
};
|
|
101
104
|
});
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
"System.AreaPath": z.array(z.string()).optional().describe("Filter by area path"),
|
|
114
|
-
"System.WorkItemType": z.array(z.string()).optional().describe("Filter by work item type like Bug, Task, User Story"),
|
|
115
|
-
"System.State": z.array(z.string()).optional().describe("Filter by state"),
|
|
116
|
-
"System.AssignedTo": z.array(z.string()).optional().describe("Filter by assigned to"),
|
|
117
|
-
}).partial().optional(),
|
|
118
|
-
includeFacets: z.boolean().optional()
|
|
119
|
-
}).strict()
|
|
120
|
-
}, async ({ searchRequest }) => {
|
|
105
|
+
server.tool(SEARCH_TOOLS.search_workitem, "Get Azure DevOps Work Item search results for a given search text", {
|
|
106
|
+
searchText: z.string().describe("Search text to find in work items"),
|
|
107
|
+
project: z.array(z.string()).optional().describe("Filter by projects"),
|
|
108
|
+
areaPath: z.array(z.string()).optional().describe("Filter by area paths"),
|
|
109
|
+
workItemType: z.array(z.string()).optional().describe("Filter by work item types"),
|
|
110
|
+
state: z.array(z.string()).optional().describe("Filter by work item states"),
|
|
111
|
+
assignedTo: z.array(z.string()).optional().describe("Filter by assigned to users"),
|
|
112
|
+
includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
|
|
113
|
+
skip: z.number().default(0).describe("Number of results to skip for pagination"),
|
|
114
|
+
top: z.number().default(10).describe("Number of results to return"),
|
|
115
|
+
}, async ({ searchText, project, areaPath, workItemType, state, assignedTo, includeFacets, skip, top }) => {
|
|
121
116
|
const accessToken = await tokenProvider();
|
|
122
117
|
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/workitemsearchresults?api-version=${apiVersion}`;
|
|
118
|
+
const requestBody = {
|
|
119
|
+
searchText,
|
|
120
|
+
includeFacets,
|
|
121
|
+
$skip: skip,
|
|
122
|
+
$top: top,
|
|
123
|
+
};
|
|
124
|
+
const filters = {};
|
|
125
|
+
if (project && project.length > 0)
|
|
126
|
+
filters["System.TeamProject"] = project;
|
|
127
|
+
if (areaPath && areaPath.length > 0)
|
|
128
|
+
filters["System.AreaPath"] = areaPath;
|
|
129
|
+
if (workItemType && workItemType.length > 0)
|
|
130
|
+
filters["System.WorkItemType"] = workItemType;
|
|
131
|
+
if (state && state.length > 0)
|
|
132
|
+
filters["System.State"] = state;
|
|
133
|
+
if (assignedTo && assignedTo.length > 0)
|
|
134
|
+
filters["System.AssignedTo"] = assignedTo;
|
|
135
|
+
if (Object.keys(filters).length > 0) {
|
|
136
|
+
requestBody.filters = filters;
|
|
137
|
+
}
|
|
123
138
|
const response = await fetch(url, {
|
|
124
139
|
method: "POST",
|
|
125
140
|
headers: {
|
|
126
141
|
"Content-Type": "application/json",
|
|
127
|
-
Authorization: `Bearer ${accessToken
|
|
128
|
-
"User-Agent":
|
|
142
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
143
|
+
"User-Agent": userAgentProvider(),
|
|
129
144
|
},
|
|
130
|
-
body: JSON.stringify(
|
|
145
|
+
body: JSON.stringify(requestBody),
|
|
131
146
|
});
|
|
132
147
|
if (!response.ok) {
|
|
133
148
|
throw new Error(`Azure DevOps Work Item Search API error: ${response.status} ${response.statusText}`);
|
|
134
149
|
}
|
|
135
150
|
const result = await response.text();
|
|
136
151
|
return {
|
|
137
|
-
content: [
|
|
138
|
-
{ type: "text", text: result }
|
|
139
|
-
]
|
|
152
|
+
content: [{ type: "text", text: result }],
|
|
140
153
|
};
|
|
141
154
|
});
|
|
142
155
|
}
|
|
@@ -154,9 +167,7 @@ async function fetchCombinedResults(topSearchResults, gitApi) {
|
|
|
154
167
|
});
|
|
155
168
|
continue;
|
|
156
169
|
}
|
|
157
|
-
const versionDescriptor = changeId
|
|
158
|
-
? { version: changeId, versionType: 2, versionOptions: 0 }
|
|
159
|
-
: undefined;
|
|
170
|
+
const versionDescriptor = changeId ? { version: changeId, versionType: 2, versionOptions: 0 } : undefined;
|
|
160
171
|
const item = await gitApi.getItem(repositoryId, filePath, projectId, undefined, VersionControlRecursionType.None, true, // includeContentMetadata
|
|
161
172
|
false, // latestProcessedChange
|
|
162
173
|
false, // download
|
|
@@ -165,12 +176,12 @@ async function fetchCombinedResults(topSearchResults, gitApi) {
|
|
|
165
176
|
true // sanitize
|
|
166
177
|
);
|
|
167
178
|
combinedResults.push({
|
|
168
|
-
gitItem: item
|
|
179
|
+
gitItem: item,
|
|
169
180
|
});
|
|
170
181
|
}
|
|
171
182
|
catch (err) {
|
|
172
183
|
combinedResults.push({
|
|
173
|
-
error: err instanceof Error ? err.message : String(err)
|
|
184
|
+
error: err instanceof Error ? err.message : String(err),
|
|
174
185
|
});
|
|
175
186
|
}
|
|
176
187
|
}
|
|
@@ -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, _, 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 "<";
|
|
200
|
+
case ">":
|
|
201
|
+
return ">";
|
|
202
|
+
case "&":
|
|
203
|
+
return "&";
|
|
204
|
+
case "'":
|
|
205
|
+
return "'";
|
|
206
|
+
case '"':
|
|
207
|
+
return """;
|
|
208
|
+
default:
|
|
209
|
+
return c;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
export { Test_Plan_Tools, configureTestPlanTools };
|
package/dist/tools/testplans.js
CHANGED
|
@@ -7,7 +7,7 @@ const Test_Plan_Tools = {
|
|
|
7
7
|
add_test_cases_to_suite: "testplan_add_test_cases_to_suite",
|
|
8
8
|
test_results_from_build_id: "testplan_show_test_results_from_build_id",
|
|
9
9
|
list_test_cases: "testplan_list_test_cases",
|
|
10
|
-
list_test_plans: "testplan_list_test_plans"
|
|
10
|
+
list_test_plans: "testplan_list_test_plans",
|
|
11
11
|
};
|
|
12
12
|
function configureTestPlanTools(server, tokenProvider, connectionProvider) {
|
|
13
13
|
/*
|
|
@@ -19,7 +19,7 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider) {
|
|
|
19
19
|
filterActivePlans: z.boolean().default(true).describe("Filter to include only active test plans. Defaults to true."),
|
|
20
20
|
includePlanDetails: z.boolean().default(false).describe("Include detailed information about each test plan."),
|
|
21
21
|
continuationToken: z.string().optional().describe("Token to continue fetching test plans from a previous request."),
|
|
22
|
-
}, async ({ project, filterActivePlans, includePlanDetails, continuationToken
|
|
22
|
+
}, async ({ project, filterActivePlans, includePlanDetails, continuationToken }) => {
|
|
23
23
|
const owner = ""; //making owner an empty string untill we can figure out how to get owner id
|
|
24
24
|
const connection = await connectionProvider();
|
|
25
25
|
const testPlanApi = await connection.getTestPlanApi();
|
|
@@ -39,7 +39,7 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider) {
|
|
|
39
39
|
startDate: z.string().optional().describe("The start date of the test plan"),
|
|
40
40
|
endDate: z.string().optional().describe("The end date of the test plan"),
|
|
41
41
|
areaPath: z.string().optional().describe("The area path for the test plan"),
|
|
42
|
-
}, async ({ project, name, iteration, description, startDate, endDate, areaPath
|
|
42
|
+
}, async ({ project, name, iteration, description, startDate, endDate, areaPath }) => {
|
|
43
43
|
const connection = await connectionProvider();
|
|
44
44
|
const testPlanApi = await connection.getTestPlanApi();
|
|
45
45
|
const testPlanToCreate = {
|
|
@@ -52,9 +52,7 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider) {
|
|
|
52
52
|
};
|
|
53
53
|
const createdTestPlan = await testPlanApi.createTestPlan(testPlanToCreate, project);
|
|
54
54
|
return {
|
|
55
|
-
content: [
|
|
56
|
-
{ type: "text", text: JSON.stringify(createdTestPlan, null, 2) },
|
|
57
|
-
],
|
|
55
|
+
content: [{ type: "text", text: JSON.stringify(createdTestPlan, null, 2) }],
|
|
58
56
|
};
|
|
59
57
|
});
|
|
60
58
|
/*
|
|
@@ -69,14 +67,10 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider) {
|
|
|
69
67
|
const connection = await connectionProvider();
|
|
70
68
|
const testApi = await connection.getTestApi();
|
|
71
69
|
// If testCaseIds is an array, convert it to comma-separated string
|
|
72
|
-
const testCaseIdsString = Array.isArray(testCaseIds)
|
|
73
|
-
? testCaseIds.join(",")
|
|
74
|
-
: testCaseIds;
|
|
70
|
+
const testCaseIdsString = Array.isArray(testCaseIds) ? testCaseIds.join(",") : testCaseIds;
|
|
75
71
|
const addedTestCases = await testApi.addTestCasesToSuite(project, planId, suiteId, testCaseIdsString);
|
|
76
72
|
return {
|
|
77
|
-
content: [
|
|
78
|
-
{ type: "text", text: JSON.stringify(addedTestCases, null, 2) },
|
|
79
|
-
],
|
|
73
|
+
content: [{ type: "text", text: JSON.stringify(addedTestCases, null, 2) }],
|
|
80
74
|
};
|
|
81
75
|
});
|
|
82
76
|
/*
|
|
@@ -85,7 +79,10 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider) {
|
|
|
85
79
|
server.tool(Test_Plan_Tools.create_test_case, "Creates a new test case work item.", {
|
|
86
80
|
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
|
|
87
81
|
title: z.string().describe("The title of the test case."),
|
|
88
|
-
steps: z
|
|
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."),
|
|
89
86
|
priority: z.number().optional().describe("The priority of the test case."),
|
|
90
87
|
areaPath: z.string().optional().describe("The area path for the test case."),
|
|
91
88
|
iterationPath: z.string().optional().describe("The iteration path for the test case."),
|
|
@@ -169,19 +166,23 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider) {
|
|
|
169
166
|
}
|
|
170
167
|
/*
|
|
171
168
|
* Helper function to convert steps text to XML format required
|
|
172
|
-
*/
|
|
169
|
+
*/
|
|
173
170
|
function convertStepsToXml(steps) {
|
|
171
|
+
// Accepts steps in the format: '1. Step one|Expected result one\n2. Step two|Expected result two'
|
|
174
172
|
const stepsLines = steps.split("\n").filter((line) => line.trim() !== "");
|
|
175
173
|
let xmlSteps = `<steps id="0" last="${stepsLines.length}">`;
|
|
176
174
|
for (let i = 0; i < stepsLines.length; i++) {
|
|
177
175
|
const stepLine = stepsLines[i].trim();
|
|
178
176
|
if (stepLine) {
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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>
|
|
185
186
|
</step>`;
|
|
186
187
|
}
|
|
187
188
|
}
|
|
@@ -190,7 +191,7 @@ function convertStepsToXml(steps) {
|
|
|
190
191
|
}
|
|
191
192
|
/*
|
|
192
193
|
* Helper function to escape XML special characters
|
|
193
|
-
*/
|
|
194
|
+
*/
|
|
194
195
|
function escapeXml(unsafe) {
|
|
195
196
|
return unsafe.replace(/[<>&'"]/g, (c) => {
|
|
196
197
|
switch (c) {
|