@azure-devops/mcp 0.1.0
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 -0
- package/README.md +247 -0
- package/dist/index.js +51 -0
- package/dist/prompts.js +25 -0
- package/dist/tools/auth.js +20 -0
- package/dist/tools/builds.js +146 -0
- package/dist/tools/core.js +36 -0
- package/dist/tools/releases.js +72 -0
- package/dist/tools/repos.js +299 -0
- package/dist/tools/search.js +179 -0
- package/dist/tools/testplans.js +212 -0
- package/dist/tools/wiki.js +74 -0
- package/dist/tools/work.js +70 -0
- package/dist/tools/workitems.js +485 -0
- package/dist/tools.js +21 -0
- package/dist/utils.js +6 -0
- package/dist/version.js +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,212 @@
|
|
|
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: [
|
|
56
|
+
{ type: "text", text: JSON.stringify(createdTestPlan, null, 2) },
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
/*
|
|
61
|
+
Add Test Cases to Suite - ADD
|
|
62
|
+
*/
|
|
63
|
+
server.tool(Test_Plan_Tools.add_test_cases_to_suite, "Adds existing test cases to a test suite.", {
|
|
64
|
+
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
|
|
65
|
+
planId: z.number().describe("The ID of the test plan."),
|
|
66
|
+
suiteId: z.number().describe("The ID of the test suite."),
|
|
67
|
+
testCaseIds: z.string().or(z.array(z.string())).describe("The ID(s) of the test case(s) to add. "),
|
|
68
|
+
}, async ({ project, planId, suiteId, testCaseIds }) => {
|
|
69
|
+
const connection = await connectionProvider();
|
|
70
|
+
const testApi = await connection.getTestApi();
|
|
71
|
+
// If testCaseIds is an array, convert it to comma-separated string
|
|
72
|
+
const testCaseIdsString = Array.isArray(testCaseIds)
|
|
73
|
+
? testCaseIds.join(",")
|
|
74
|
+
: testCaseIds;
|
|
75
|
+
const addedTestCases = await testApi.addTestCasesToSuite(project, planId, suiteId, testCaseIdsString);
|
|
76
|
+
return {
|
|
77
|
+
content: [
|
|
78
|
+
{ type: "text", text: JSON.stringify(addedTestCases, null, 2) },
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
/*
|
|
83
|
+
Create Test Case - CREATE
|
|
84
|
+
*/
|
|
85
|
+
server.tool(Test_Plan_Tools.create_test_case, "Creates a new test case work item.", {
|
|
86
|
+
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
|
|
87
|
+
title: z.string().describe("The title of the test case."),
|
|
88
|
+
steps: z.string().optional().describe("The steps to reproduce the test case. Make sure to format each step as '1. Step one\\n2. Step two' etc."),
|
|
89
|
+
priority: z.number().optional().describe("The priority of the test case."),
|
|
90
|
+
areaPath: z.string().optional().describe("The area path for the test case."),
|
|
91
|
+
iterationPath: z.string().optional().describe("The iteration path for the test case."),
|
|
92
|
+
}, async ({ project, title, steps, priority, areaPath, iterationPath }) => {
|
|
93
|
+
const connection = await connectionProvider();
|
|
94
|
+
const witClient = await connection.getWorkItemTrackingApi();
|
|
95
|
+
let stepsXml;
|
|
96
|
+
if (steps) {
|
|
97
|
+
stepsXml = convertStepsToXml(steps);
|
|
98
|
+
}
|
|
99
|
+
// Create JSON patch document for work item
|
|
100
|
+
const patchDocument = [];
|
|
101
|
+
patchDocument.push({
|
|
102
|
+
op: "add",
|
|
103
|
+
path: "/fields/System.Title",
|
|
104
|
+
value: title,
|
|
105
|
+
});
|
|
106
|
+
if (stepsXml) {
|
|
107
|
+
patchDocument.push({
|
|
108
|
+
op: "add",
|
|
109
|
+
path: "/fields/Microsoft.VSTS.TCM.Steps",
|
|
110
|
+
value: stepsXml,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (priority) {
|
|
114
|
+
patchDocument.push({
|
|
115
|
+
op: "add",
|
|
116
|
+
path: "/fields/Microsoft.VSTS.Common.Priority",
|
|
117
|
+
value: priority,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (areaPath) {
|
|
121
|
+
patchDocument.push({
|
|
122
|
+
op: "add",
|
|
123
|
+
path: "/fields/System.AreaPath",
|
|
124
|
+
value: areaPath,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (iterationPath) {
|
|
128
|
+
patchDocument.push({
|
|
129
|
+
op: "add",
|
|
130
|
+
path: "/fields/System.IterationPath",
|
|
131
|
+
value: iterationPath,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const workItem = await witClient.createWorkItem({}, patchDocument, project, "Test Case");
|
|
135
|
+
return {
|
|
136
|
+
content: [{ type: "text", text: JSON.stringify(workItem, null, 2) }],
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
/*
|
|
140
|
+
TEST PLANS
|
|
141
|
+
Gets a list of test cases for a given testplan.
|
|
142
|
+
*/
|
|
143
|
+
server.tool(Test_Plan_Tools.list_test_cases, "Gets a list of test cases in the test plan.", {
|
|
144
|
+
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
|
|
145
|
+
planid: z.number().describe("The ID of the test plan."),
|
|
146
|
+
suiteid: z.number().describe("The ID of the test suite."),
|
|
147
|
+
}, async ({ project, planid, suiteid }) => {
|
|
148
|
+
const connection = await connectionProvider();
|
|
149
|
+
const coreApi = await connection.getTestPlanApi();
|
|
150
|
+
const testcases = await coreApi.getTestCaseList(project, planid, suiteid);
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: "text", text: JSON.stringify(testcases, null, 2) }],
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
/*
|
|
156
|
+
Gets a list of test results for a given project and build ID
|
|
157
|
+
*/
|
|
158
|
+
server.tool(Test_Plan_Tools.test_results_from_build_id, "Gets a list of test results for a given project and build ID.", {
|
|
159
|
+
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
|
|
160
|
+
buildid: z.number().describe("The ID of the build."),
|
|
161
|
+
}, async ({ project, buildid }) => {
|
|
162
|
+
const connection = await connectionProvider();
|
|
163
|
+
const coreApi = await connection.getTestResultsApi();
|
|
164
|
+
const testResults = await coreApi.getTestResultDetailsForBuild(project, buildid);
|
|
165
|
+
return {
|
|
166
|
+
content: [{ type: "text", text: JSON.stringify(testResults, null, 2) }],
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/*
|
|
171
|
+
* Helper function to convert steps text to XML format required
|
|
172
|
+
*/
|
|
173
|
+
function convertStepsToXml(steps) {
|
|
174
|
+
const stepsLines = steps.split("\n").filter((line) => line.trim() !== "");
|
|
175
|
+
let xmlSteps = `<steps id="0" last="${stepsLines.length}">`;
|
|
176
|
+
for (let i = 0; i < stepsLines.length; i++) {
|
|
177
|
+
const stepLine = stepsLines[i].trim();
|
|
178
|
+
if (stepLine) {
|
|
179
|
+
const stepMatch = stepLine.match(/^(\d+)\.\s*(.+)$/);
|
|
180
|
+
const stepText = stepMatch ? stepMatch[2] : stepLine;
|
|
181
|
+
xmlSteps += `
|
|
182
|
+
<step id="${i + 1}" type="ActionStep">
|
|
183
|
+
<parameterizedString isformatted="true">${escapeXml(stepText)}</parameterizedString>
|
|
184
|
+
<parameterizedString isformatted="true">Verify step completes successfully</parameterizedString>
|
|
185
|
+
</step>`;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
xmlSteps += "</steps>";
|
|
189
|
+
return xmlSteps;
|
|
190
|
+
}
|
|
191
|
+
/*
|
|
192
|
+
* Helper function to escape XML special characters
|
|
193
|
+
*/
|
|
194
|
+
function escapeXml(unsafe) {
|
|
195
|
+
return unsafe.replace(/[<>&'"]/g, (c) => {
|
|
196
|
+
switch (c) {
|
|
197
|
+
case "<":
|
|
198
|
+
return "<";
|
|
199
|
+
case ">":
|
|
200
|
+
return ">";
|
|
201
|
+
case "&":
|
|
202
|
+
return "&";
|
|
203
|
+
case "'":
|
|
204
|
+
return "'";
|
|
205
|
+
case '"':
|
|
206
|
+
return """;
|
|
207
|
+
default:
|
|
208
|
+
return c;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
export { Test_Plan_Tools, configureTestPlanTools };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
const WIKI_TOOLS = {
|
|
5
|
+
list_wikis: "wiki_list_wikis",
|
|
6
|
+
get_wiki: "wiki_get_wiki",
|
|
7
|
+
list_wiki_pages: "wiki_list_pages",
|
|
8
|
+
get_wiki_page_content: "wiki_get_page_content",
|
|
9
|
+
};
|
|
10
|
+
function configureWikiTools(server, tokenProvider, connectionProvider) {
|
|
11
|
+
server.tool(WIKI_TOOLS.get_wiki, "Get the wiki by wikiIdentifier", {
|
|
12
|
+
wikiIdentifier: z.string().describe("The unique identifier of the wiki."),
|
|
13
|
+
project: z.string().optional().describe("The project name or ID where the wiki is located. If not provided, the default project will be used."),
|
|
14
|
+
}, async ({ wikiIdentifier, project }) => {
|
|
15
|
+
const connection = await connectionProvider();
|
|
16
|
+
const wikiApi = await connection.getWikiApi();
|
|
17
|
+
const wiki = await wikiApi.getWiki(wikiIdentifier, project);
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: "text", text: JSON.stringify(wiki, null, 2) }],
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
server.tool(WIKI_TOOLS.list_wikis, "Retrieve a list of wikis for an organization or project.", {
|
|
23
|
+
project: z.string().optional().describe("The project name or ID to filter wikis. If not provided, all wikis in the organization will be returned."),
|
|
24
|
+
}, async ({ project }) => {
|
|
25
|
+
const connection = await connectionProvider();
|
|
26
|
+
const wikiApi = await connection.getWikiApi();
|
|
27
|
+
const wikis = await wikiApi.getAllWikis(project);
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text: JSON.stringify(wikis, null, 2) }],
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
server.tool(WIKI_TOOLS.list_wiki_pages, "Retrieve a list of wiki pages for a specific wiki and project.", {
|
|
33
|
+
wikiIdentifier: z.string().describe("The unique identifier of the wiki."),
|
|
34
|
+
project: z.string().describe("The project name or ID where the wiki is located."),
|
|
35
|
+
top: z.number().default(20).describe("The maximum number of pages to return. Defaults to 20."),
|
|
36
|
+
continuationToken: z.string().optional().describe("Token for pagination to retrieve the next set of pages."),
|
|
37
|
+
pageViewsForDays: z.number().optional().describe("Number of days to retrieve page views for. If not specified, page views are not included."),
|
|
38
|
+
}, async ({ wikiIdentifier, project, top = 20, continuationToken, pageViewsForDays, }) => {
|
|
39
|
+
const connection = await connectionProvider();
|
|
40
|
+
const wikiApi = await connection.getWikiApi();
|
|
41
|
+
const pagesBatchRequest = {
|
|
42
|
+
top,
|
|
43
|
+
continuationToken,
|
|
44
|
+
pageViewsForDays,
|
|
45
|
+
};
|
|
46
|
+
const pages = await wikiApi.getPagesBatch(pagesBatchRequest, project, wikiIdentifier);
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: "text", text: JSON.stringify(pages, null, 2) }],
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
server.tool(WIKI_TOOLS.get_wiki_page_content, "Retrieve wiki page content by wikiIdentifier and path.", {
|
|
52
|
+
wikiIdentifier: z.string().describe("The unique identifier of the wiki."),
|
|
53
|
+
project: z.string().describe("The project name or ID where the wiki is located."),
|
|
54
|
+
path: z.string().describe("The path of the wiki page to retrieve content for."),
|
|
55
|
+
}, async ({ wikiIdentifier, project, path }) => {
|
|
56
|
+
const connection = await connectionProvider();
|
|
57
|
+
const wikiApi = await connection.getWikiApi();
|
|
58
|
+
const stream = await wikiApi.getPageText(project, wikiIdentifier, path, undefined, undefined, true);
|
|
59
|
+
const content = await streamToString(stream);
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: "text", text: JSON.stringify(content, null, 2) }],
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function streamToString(stream) {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
let data = "";
|
|
68
|
+
stream.setEncoding("utf8");
|
|
69
|
+
stream.on("data", (chunk) => (data += chunk));
|
|
70
|
+
stream.on("end", () => resolve(data));
|
|
71
|
+
stream.on("error", reject);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export { WIKI_TOOLS, configureWikiTools };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { TreeStructureGroup } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
5
|
+
const WORK_TOOLS = {
|
|
6
|
+
list_team_iterations: "work_list_team_iterations",
|
|
7
|
+
create_iterations: "work_create_iterations",
|
|
8
|
+
assign_iterations: "work_assign_iterations",
|
|
9
|
+
};
|
|
10
|
+
function configureWorkTools(server, tokenProvider, connectionProvider) {
|
|
11
|
+
server.tool(WORK_TOOLS.list_team_iterations, "Retrieve a list of iterations for a specific team in a project.", {
|
|
12
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
13
|
+
team: z.string().describe("The name or ID of the Azure DevOps team."),
|
|
14
|
+
timeframe: z.enum(["current"]).optional().describe("The timeframe for which to retrieve iterations. Currently, only 'current' is supported."),
|
|
15
|
+
}, async ({ project, team, timeframe }) => {
|
|
16
|
+
const connection = await connectionProvider();
|
|
17
|
+
const workApi = await connection.getWorkApi();
|
|
18
|
+
const iterations = await workApi.getTeamIterations({ project, team }, timeframe);
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: "text", text: JSON.stringify(iterations, null, 2) }],
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
server.tool(WORK_TOOLS.create_iterations, "Create new iterations in a specified Azure DevOps project.", {
|
|
24
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
25
|
+
iterations: z.array(z.object({
|
|
26
|
+
iterationName: z.string().describe("The name of the iteration to create."),
|
|
27
|
+
startDate: z.string().optional().describe("The start date of the iteration in ISO format (e.g., '2023-01-01T00:00:00Z'). Optional."),
|
|
28
|
+
finishDate: z.string().optional().describe("The finish date of the iteration in ISO format (e.g., '2023-01-31T23:59:59Z'). Optional.")
|
|
29
|
+
})).describe("An array of iterations to create. Each iteration must have a name and can optionally have start and finish dates in ISO format.")
|
|
30
|
+
}, async ({ project, iterations }) => {
|
|
31
|
+
const connection = await connectionProvider();
|
|
32
|
+
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
|
|
33
|
+
const results = [];
|
|
34
|
+
for (const { iterationName, startDate, finishDate } of iterations) {
|
|
35
|
+
// Step 1: Create the iteration
|
|
36
|
+
const iteration = await workItemTrackingApi.createOrUpdateClassificationNode({
|
|
37
|
+
name: iterationName,
|
|
38
|
+
attributes: {
|
|
39
|
+
startDate: startDate ? new Date(startDate) : undefined,
|
|
40
|
+
finishDate: finishDate ? new Date(finishDate) : undefined,
|
|
41
|
+
},
|
|
42
|
+
}, project, TreeStructureGroup.Iterations);
|
|
43
|
+
results.push(iteration);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
server.tool(WORK_TOOLS.assign_iterations, "Assign existing iterations to a specific team in a project.", {
|
|
50
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
51
|
+
team: z.string().describe("The name or ID of the Azure DevOps team."),
|
|
52
|
+
iterations: z.array(z.object({
|
|
53
|
+
identifier: z.string().describe("The identifier of the iteration to assign."),
|
|
54
|
+
path: z.string().describe("The path of the iteration to assign, e.g., 'Project/Iteration'.")
|
|
55
|
+
})).describe("An array of iterations to assign. Each iteration must have an identifier and a path."),
|
|
56
|
+
}, async ({ project, team, iterations }) => {
|
|
57
|
+
const connection = await connectionProvider();
|
|
58
|
+
const workApi = await connection.getWorkApi();
|
|
59
|
+
const teamContext = { project, team };
|
|
60
|
+
const results = [];
|
|
61
|
+
for (const { identifier, path } of iterations) {
|
|
62
|
+
const assignment = await workApi.postTeamIteration({ path: path, id: identifier }, teamContext);
|
|
63
|
+
results.push(assignment);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export { WORK_TOOLS, configureWorkTools };
|