@azure-devops/mcp 2.7.0 → 2.8.0-nightly.20260624
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 +68 -18
- package/dist/auth.js +0 -0
- package/dist/index.js +1 -1
- package/dist/logger.js +0 -0
- package/dist/org-tenants.js +0 -0
- package/dist/prompts.js +0 -0
- package/dist/shared/elicitations.js +10 -0
- package/dist/tools/pipelines.js +31 -9
- package/dist/tools/repositories.js +248 -207
- package/dist/tools/test-plans.js +6 -3
- package/dist/tools/wiki.js +30 -6
- package/dist/tools/work-items.js +48 -8
- package/dist/tools/work.js +1 -1
- package/dist/tools.js +0 -0
- package/dist/useragent.js +0 -0
- package/dist/utils.js +54 -0
- package/dist/version.js +1 -1
- package/package.json +4 -4
- package/dist/tools/builds.js +0 -271
- package/dist/tools/releases.js +0 -97
package/dist/tools/test-plans.js
CHANGED
|
@@ -341,8 +341,9 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider, userA
|
|
|
341
341
|
try {
|
|
342
342
|
const connection = await connectionProvider();
|
|
343
343
|
const testResultsApi = await connection.getTestResultsApi();
|
|
344
|
-
// Build filter expression for outcomes if specified
|
|
345
|
-
|
|
344
|
+
// Build filter expression for outcomes if specified.
|
|
345
|
+
// The API accepts: Outcome eq Failed,Passed (unquoted, comma-separated)
|
|
346
|
+
const outcomeFilter = outcomes?.length ? `Outcome eq ${outcomes.join(",")}` : undefined;
|
|
346
347
|
// Fetch test result details for the build in a single API call
|
|
347
348
|
// This is more efficient than getTestRuns + getTestResults per run,
|
|
348
349
|
// especially for builds with many test runs (e.g., cloud testing with one run per test case)
|
|
@@ -357,7 +358,9 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider, userA
|
|
|
357
358
|
if (testResultDetails.resultsForGroup) {
|
|
358
359
|
for (const group of testResultDetails.resultsForGroup) {
|
|
359
360
|
if (group.results) {
|
|
360
|
-
|
|
361
|
+
for (const result of group.results) {
|
|
362
|
+
allResults.push(result);
|
|
363
|
+
}
|
|
361
364
|
}
|
|
362
365
|
}
|
|
363
366
|
}
|
package/dist/tools/wiki.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { apiVersion } from "../utils.js";
|
|
4
|
+
import { apiVersion, extractAdoStreamError, getOrgFromUrl } from "../utils.js";
|
|
5
5
|
import { createExternalContentResponse } from "../shared/content-safety.js";
|
|
6
6
|
const WIKI_TOOLS = {
|
|
7
7
|
list_wikis: "wiki_list_wikis",
|
|
@@ -88,7 +88,7 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
});
|
|
91
|
-
server.tool(WIKI_TOOLS.get_wiki_page, "Retrieve wiki page metadata by path. This tool does not return page content.", {
|
|
91
|
+
server.tool(WIKI_TOOLS.get_wiki_page, "Retrieve wiki page metadata by path. This tool does not return page content. Returns isError: true if the page is not found.", {
|
|
92
92
|
wikiIdentifier: z.string().describe("The unique identifier of the wiki."),
|
|
93
93
|
project: z.string().describe("The project name or ID where the wiki is located."),
|
|
94
94
|
path: z.string().describe("The path of the wiki page (e.g., '/Home' or '/Documentation/Setup')."),
|
|
@@ -136,7 +136,7 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
138
|
});
|
|
139
|
-
server.tool(WIKI_TOOLS.get_wiki_page_content, "Retrieve wiki page content. Provide either a 'url' parameter OR the combination of 'wikiIdentifier' and 'project' parameters.", {
|
|
139
|
+
server.tool(WIKI_TOOLS.get_wiki_page_content, "Retrieve wiki page content. Provide either a 'url' parameter OR the combination of 'wikiIdentifier' and 'project' parameters. " + "Returns isError: true if the wiki page is not found.", {
|
|
140
140
|
url: z
|
|
141
141
|
.string()
|
|
142
142
|
.optional()
|
|
@@ -165,6 +165,22 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
165
165
|
if ("error" in parsed) {
|
|
166
166
|
return { content: [{ type: "text", text: `Error fetching wiki page content: ${parsed.error}` }], isError: true };
|
|
167
167
|
}
|
|
168
|
+
// Guard against cross-organization requests: a user-supplied URL must target the
|
|
169
|
+
// same organization the server is connected to. Otherwise the org segment in the
|
|
170
|
+
// URL would be silently ignored and content fetched from the configured org instead.
|
|
171
|
+
const configuredOrg = getOrgFromUrl(connection.serverUrl);
|
|
172
|
+
const urlOrg = getOrgFromUrl(url);
|
|
173
|
+
if (configuredOrg && urlOrg !== configuredOrg) {
|
|
174
|
+
return {
|
|
175
|
+
content: [
|
|
176
|
+
{
|
|
177
|
+
type: "text",
|
|
178
|
+
text: `Error fetching wiki page content: The provided URL targets organization '${urlOrg ?? "unknown"}', which does not match the configured organization '${configuredOrg}'. Cross-organization requests are not allowed.`,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
isError: true,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
168
184
|
resolvedProject = parsed.project;
|
|
169
185
|
resolvedWiki = parsed.wikiIdentifier;
|
|
170
186
|
if (parsed.pagePath) {
|
|
@@ -201,14 +217,22 @@ function configureWikiTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
201
217
|
if (!resolvedPath) {
|
|
202
218
|
resolvedPath = "/";
|
|
203
219
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
220
|
+
// resolvedProject and resolvedWiki are guaranteed to be defined here:
|
|
221
|
+
// - the url branch errors out in parseWikiUrl when project/wikiIdentifier are missing
|
|
222
|
+
// - the pair branch enforces both via the hasPair check above
|
|
223
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
207
224
|
const stream = await wikiApi.getPageText(resolvedProject, resolvedWiki, resolvedPath, undefined, undefined, true);
|
|
208
225
|
if (!stream) {
|
|
209
226
|
return { content: [{ type: "text", text: "No wiki page content found" }], isError: true };
|
|
210
227
|
}
|
|
211
228
|
pageContent = await streamToString(stream);
|
|
229
|
+
const streamError = extractAdoStreamError(pageContent);
|
|
230
|
+
if (streamError) {
|
|
231
|
+
return {
|
|
232
|
+
content: [{ type: "text", text: `Error fetching wiki page content: ${streamError}` }],
|
|
233
|
+
isError: true,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
212
236
|
}
|
|
213
237
|
return createExternalContentResponse(pageContent, "wiki page");
|
|
214
238
|
}
|
package/dist/tools/work-items.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
3
5
|
import { WorkItemExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
4
6
|
import { QueryExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
5
7
|
import { z } from "zod";
|
|
@@ -133,7 +135,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
133
135
|
};
|
|
134
136
|
}
|
|
135
137
|
});
|
|
136
|
-
server.tool(WORKITEM_TOOLS.my_work_items, "Retrieve a list of work items
|
|
138
|
+
server.tool(WORKITEM_TOOLS.my_work_items, "Retrieve a list of work items relevant to the authenticated user. If a project is not specified, you will be prompted to select one.", {
|
|
137
139
|
project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
|
|
138
140
|
type: z.enum(["assignedtome", "myactivity"]).default("assignedtome").describe("The type of work items to retrieve. Defaults to 'assignedtome'."),
|
|
139
141
|
top: z.coerce.number().default(50).describe("The maximum number of work items to return. Defaults to 50."),
|
|
@@ -407,8 +409,9 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
407
409
|
if (revisions && Array.isArray(revisions)) {
|
|
408
410
|
revisions.forEach((revision) => {
|
|
409
411
|
if (revision.fields) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
+
const fields = revision.fields;
|
|
413
|
+
Object.keys(fields).forEach((fieldName) => {
|
|
414
|
+
const fieldValue = fields[fieldName];
|
|
412
415
|
// Check if this is an identity object by looking for common identity properties
|
|
413
416
|
if (fieldValue &&
|
|
414
417
|
typeof fieldValue === "object" &&
|
|
@@ -734,7 +737,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
734
737
|
value: encodeFormattedValue(value, format),
|
|
735
738
|
}));
|
|
736
739
|
// Check if any field has format === "Markdown" and add the multilineFieldsFormat operation
|
|
737
|
-
// this should only happen for large text fields, but since we
|
|
740
|
+
// this should only happen for large text fields, but since we don't know by field name, lets assume if the users
|
|
738
741
|
// passes a value longer than 100 characters, then we can set the format to Markdown
|
|
739
742
|
fields.forEach(({ name, value, format }) => {
|
|
740
743
|
if (value.length > 100 && format === "Markdown") {
|
|
@@ -1210,11 +1213,23 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
1210
1213
|
};
|
|
1211
1214
|
}
|
|
1212
1215
|
});
|
|
1213
|
-
server.tool(WORKITEM_TOOLS.get_work_item_attachment, "Download a work item attachment by its ID
|
|
1216
|
+
server.tool(WORKITEM_TOOLS.get_work_item_attachment, "Download a work item attachment by its ID. By default returns the content as a base64-encoded resource. If savePath is provided, saves the file locally to that directory and returns the file path instead. Useful for viewing images (e.g. screenshots) or other files attached to work items such as bugs. If a project is not specified, you will be prompted to select one.", {
|
|
1214
1217
|
project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
|
|
1215
1218
|
attachmentId: z.string().describe("The GUID of the attachment. Found in the attachment URL: https://dev.azure.com/{org}/{project}/_apis/wit/attachments/{attachmentId}"),
|
|
1216
|
-
fileName: z.string().optional().describe("The file name of the attachment, e.g. 'screenshot.png'. Used to determine the MIME type
|
|
1217
|
-
|
|
1219
|
+
fileName: z.string().optional().describe("The file name of the attachment, e.g. 'screenshot.png'. Used to determine the MIME type or the saved file's name."),
|
|
1220
|
+
savePath: z
|
|
1221
|
+
.string()
|
|
1222
|
+
.optional()
|
|
1223
|
+
.describe("Optional local directory path where the file should be saved. Must be a relative path (e.g. 'temp' or 'downloads/attachments'); absolute paths and path traversals are not allowed. If provided, saves the attachment to this directory and returns the file path. If omitted, returns the content as a base64-encoded resource."),
|
|
1224
|
+
}, async ({ project, attachmentId, fileName, savePath }) => {
|
|
1225
|
+
const isAbsolutePath = (value) => path.posix.isAbsolute(value) || path.win32.isAbsolute(value);
|
|
1226
|
+
const hasDriveLetter = (value) => /^[a-zA-Z]:/.test(value);
|
|
1227
|
+
if (savePath !== undefined && (savePath.includes("..") || isAbsolutePath(savePath) || hasDriveLetter(savePath))) {
|
|
1228
|
+
throw new Error("Invalid savePath: absolute paths and path traversals are not allowed.");
|
|
1229
|
+
}
|
|
1230
|
+
if (fileName !== undefined && fileName.includes("..")) {
|
|
1231
|
+
throw new Error("Invalid fileName: path traversal is not allowed.");
|
|
1232
|
+
}
|
|
1218
1233
|
try {
|
|
1219
1234
|
const connection = await connectionProvider();
|
|
1220
1235
|
let resolvedProject = project;
|
|
@@ -1233,8 +1248,24 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
1233
1248
|
stream.on("error", reject);
|
|
1234
1249
|
});
|
|
1235
1250
|
const buffer = Buffer.concat(chunks);
|
|
1236
|
-
|
|
1251
|
+
if (savePath) {
|
|
1252
|
+
const resolvedFileName = fileName ?? attachmentId;
|
|
1253
|
+
const localFilePath = path.join(savePath, resolvedFileName);
|
|
1254
|
+
if (fs.existsSync(localFilePath)) {
|
|
1255
|
+
throw new Error(`File already exists: ${localFilePath}`);
|
|
1256
|
+
}
|
|
1257
|
+
fs.writeFileSync(localFilePath, buffer);
|
|
1258
|
+
return {
|
|
1259
|
+
content: [{ type: "text", text: `Attachment saved to: ${localFilePath}` }],
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1237
1262
|
const mimeType = getMimeType(fileName);
|
|
1263
|
+
if (mimeType.startsWith("text/")) {
|
|
1264
|
+
return {
|
|
1265
|
+
content: [{ type: "text", text: buffer.toString("utf-8") }],
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
const base64Data = buffer.toString("base64");
|
|
1238
1269
|
return {
|
|
1239
1270
|
content: [
|
|
1240
1271
|
{
|
|
@@ -1269,6 +1300,15 @@ function getMimeType(fileName) {
|
|
|
1269
1300
|
webp: "image/webp",
|
|
1270
1301
|
pdf: "application/pdf",
|
|
1271
1302
|
txt: "text/plain",
|
|
1303
|
+
md: "text/markdown",
|
|
1304
|
+
markdown: "text/markdown",
|
|
1305
|
+
csv: "text/csv",
|
|
1306
|
+
html: "text/html",
|
|
1307
|
+
htm: "text/html",
|
|
1308
|
+
xml: "text/xml",
|
|
1309
|
+
json: "application/json",
|
|
1310
|
+
yaml: "text/yaml",
|
|
1311
|
+
yml: "text/yaml",
|
|
1272
1312
|
zip: "application/zip",
|
|
1273
1313
|
};
|
|
1274
1314
|
return (ext && mimeTypes[ext]) ?? "application/octet-stream";
|
package/dist/tools/work.js
CHANGED
|
@@ -100,7 +100,7 @@ function configureWorkTools(server, _, connectionProvider) {
|
|
|
100
100
|
server.tool(WORK_TOOLS.list_iterations, "List all iterations in a specified Azure DevOps project. If a project is not specified, you will be prompted to select one.", {
|
|
101
101
|
project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
|
|
102
102
|
depth: z.coerce.number().default(2).describe("Depth of children to fetch."),
|
|
103
|
-
excludedIds: z.array(z.coerce.number().min(1)).optional().describe("An optional array of iteration IDs, and
|
|
103
|
+
excludedIds: z.array(z.coerce.number().min(1)).optional().describe("An optional array of iteration IDs, and their children, that should not be returned."),
|
|
104
104
|
}, async ({ project, depth, excludedIds: ids }) => {
|
|
105
105
|
try {
|
|
106
106
|
const connection = await connectionProvider();
|
package/dist/tools.js
CHANGED
|
File without changes
|
package/dist/useragent.js
CHANGED
|
File without changes
|
package/dist/utils.js
CHANGED
|
@@ -67,6 +67,60 @@ export function encodeFormattedValue(value, format) {
|
|
|
67
67
|
const result = value.replace(/</g, "<").replace(/>/g, ">");
|
|
68
68
|
return result;
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Detects whether a string returned from an ADO API stream is actually an error
|
|
72
|
+
* response serialized as JSON (e.g. a 404 GitItemNotFoundException or
|
|
73
|
+
* WikiPageNotFoundException) rather than real content.
|
|
74
|
+
*
|
|
75
|
+
* The ADO Node API client swallows non-2xx HTTP responses and delivers the
|
|
76
|
+
* error body as a stream, so callers must check explicitly after reading.
|
|
77
|
+
*
|
|
78
|
+
* @returns The human-readable error message extracted from the JSON, or null if
|
|
79
|
+
* the content is not an ADO error response.
|
|
80
|
+
*/
|
|
81
|
+
export function extractAdoStreamError(content) {
|
|
82
|
+
try {
|
|
83
|
+
const json = JSON.parse(content.trim());
|
|
84
|
+
if (json && typeof json.typeName === "string" && typeof json.message === "string") {
|
|
85
|
+
return json.message;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Not JSON — not an ADO error response.
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Extracts the Azure DevOps organization identifier from a URL.
|
|
95
|
+
*
|
|
96
|
+
* Only recognized Azure DevOps hosts are accepted; any other host returns null
|
|
97
|
+
* so that callers can treat unrecognized URLs as a boundary violation.
|
|
98
|
+
*
|
|
99
|
+
* Supports both modern and legacy organization URL forms:
|
|
100
|
+
* - https://dev.azure.com/{org}/... -> org is the first path segment
|
|
101
|
+
* - https://{org}.visualstudio.com/... -> org is the host subdomain
|
|
102
|
+
*
|
|
103
|
+
* @param url Any Azure DevOps URL (e.g. a wiki page link or a connection serverUrl).
|
|
104
|
+
* @returns The lowercased organization name, or null if it cannot be determined.
|
|
105
|
+
*/
|
|
106
|
+
export function getOrgFromUrl(url) {
|
|
107
|
+
try {
|
|
108
|
+
const u = new URL(url);
|
|
109
|
+
const host = u.hostname.toLowerCase();
|
|
110
|
+
if (host === "visualstudio.com" || host.endsWith(".visualstudio.com")) {
|
|
111
|
+
const subdomain = host.split(".")[0];
|
|
112
|
+
return subdomain && subdomain !== "visualstudio" ? subdomain : null;
|
|
113
|
+
}
|
|
114
|
+
if (host === "dev.azure.com" || host.endsWith(".dev.azure.com")) {
|
|
115
|
+
const firstSegment = u.pathname.split("/").filter(Boolean)[0];
|
|
116
|
+
return firstSegment ? firstSegment.toLowerCase() : null;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
70
124
|
/**
|
|
71
125
|
* Convert a Node.js ReadableStream to a string.
|
|
72
126
|
* Shared utility for consistent stream handling across tools.
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "2.
|
|
1
|
+
export const packageVersion = "2.8.0-nightly.20260624";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azure-devops/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0-nightly.20260624",
|
|
4
4
|
"mcpName": "microsoft.com/azure-devops",
|
|
5
5
|
"description": "MCP server for interacting with Azure DevOps",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@azure/identity": "^4.10.0",
|
|
42
42
|
"@azure/msal-node": "^5.0.6",
|
|
43
43
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
44
|
-
"azure-devops-extension-api": "^
|
|
44
|
+
"azure-devops-extension-api": "^5.272.3",
|
|
45
45
|
"azure-devops-extension-sdk": "^4.0.2",
|
|
46
46
|
"azure-devops-node-api": "^15.1.2",
|
|
47
47
|
"winston": "^3.18.3",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"husky": "^9.1.7",
|
|
59
59
|
"jest": "^30.0.2",
|
|
60
60
|
"jest-extended": "^7.0.0",
|
|
61
|
-
"lint-staged": "^
|
|
62
|
-
"prettier": "3.8.
|
|
61
|
+
"lint-staged": "^17.0.0",
|
|
62
|
+
"prettier": "3.8.4",
|
|
63
63
|
"shx": "^0.4.0",
|
|
64
64
|
"ts-jest": "^29.4.6",
|
|
65
65
|
"tsconfig-paths": "^4.2.0",
|
package/dist/tools/builds.js
DELETED
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Microsoft Corporation.
|
|
2
|
-
// Licensed under the MIT License.
|
|
3
|
-
import { apiVersion, getEnumKeys, safeEnumConvert } from "../utils.js";
|
|
4
|
-
import { BuildQueryOrder, DefinitionQueryOrder } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
import { StageUpdateType } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
|
|
7
|
-
const BUILD_TOOLS = {
|
|
8
|
-
get_builds: "build_get_builds",
|
|
9
|
-
get_changes: "build_get_changes",
|
|
10
|
-
get_definitions: "build_get_definitions",
|
|
11
|
-
get_definition_revisions: "build_get_definition_revisions",
|
|
12
|
-
get_log: "build_get_log",
|
|
13
|
-
get_log_by_id: "build_get_log_by_id",
|
|
14
|
-
get_status: "build_get_status",
|
|
15
|
-
pipelines_get_run: "pipelines_get_run",
|
|
16
|
-
pipelines_list_runs: "pipelines_list_runs",
|
|
17
|
-
pipelines_run_pipeline: "pipelines_run_pipeline",
|
|
18
|
-
update_build_stage: "build_update_build_stage",
|
|
19
|
-
};
|
|
20
|
-
function configureBuildTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
21
|
-
server.tool(BUILD_TOOLS.get_definitions, "Retrieves a list of build definitions for a given project.", {
|
|
22
|
-
project: z.string().describe("Project ID or name to get build definitions for"),
|
|
23
|
-
repositoryId: z.string().optional().describe("Repository ID to filter build definitions"),
|
|
24
|
-
repositoryType: z.enum(["TfsGit", "GitHub", "BitbucketCloud"]).optional().describe("Type of repository to filter build definitions"),
|
|
25
|
-
name: z.string().optional().describe("Name of the build definition to filter"),
|
|
26
|
-
path: z.string().optional().describe("Path of the build definition to filter"),
|
|
27
|
-
queryOrder: z
|
|
28
|
-
.enum(getEnumKeys(DefinitionQueryOrder))
|
|
29
|
-
.optional()
|
|
30
|
-
.describe("Order in which build definitions are returned"),
|
|
31
|
-
top: z.number().optional().describe("Maximum number of build definitions to return"),
|
|
32
|
-
continuationToken: z.string().optional().describe("Token for continuing paged results"),
|
|
33
|
-
minMetricsTime: z.coerce.date().optional().describe("Minimum metrics time to filter build definitions"),
|
|
34
|
-
definitionIds: z.array(z.number()).optional().describe("Array of build definition IDs to filter"),
|
|
35
|
-
builtAfter: z.coerce.date().optional().describe("Return definitions that have builds after this date"),
|
|
36
|
-
notBuiltAfter: z.coerce.date().optional().describe("Return definitions that do not have builds after this date"),
|
|
37
|
-
includeAllProperties: z.boolean().optional().describe("Whether to include all properties in the results"),
|
|
38
|
-
includeLatestBuilds: z.boolean().optional().describe("Whether to include the latest builds for each definition"),
|
|
39
|
-
taskIdFilter: z.string().optional().describe("Task ID to filter build definitions"),
|
|
40
|
-
processType: z.number().optional().describe("Process type to filter build definitions"),
|
|
41
|
-
yamlFilename: z.string().optional().describe("YAML filename to filter build definitions"),
|
|
42
|
-
}, async ({ project, repositoryId, repositoryType, name, path, queryOrder, top, continuationToken, minMetricsTime, definitionIds, builtAfter, notBuiltAfter, includeAllProperties, includeLatestBuilds, taskIdFilter, processType, yamlFilename, }) => {
|
|
43
|
-
const connection = await connectionProvider();
|
|
44
|
-
const buildApi = await connection.getBuildApi();
|
|
45
|
-
const buildDefinitions = await buildApi.getDefinitions(project, name, repositoryId, repositoryType, safeEnumConvert(DefinitionQueryOrder, queryOrder), top, continuationToken, minMetricsTime, definitionIds, path, builtAfter, notBuiltAfter, includeAllProperties, includeLatestBuilds, taskIdFilter, processType, yamlFilename);
|
|
46
|
-
return {
|
|
47
|
-
content: [{ type: "text", text: JSON.stringify(buildDefinitions, null, 2) }],
|
|
48
|
-
};
|
|
49
|
-
});
|
|
50
|
-
server.tool(BUILD_TOOLS.get_definition_revisions, "Retrieves a list of revisions for a specific build definition.", {
|
|
51
|
-
project: z.string().describe("Project ID or name to get the build definition revisions for"),
|
|
52
|
-
definitionId: z.number().describe("ID of the build definition to get revisions for"),
|
|
53
|
-
}, async ({ project, definitionId }) => {
|
|
54
|
-
const connection = await connectionProvider();
|
|
55
|
-
const buildApi = await connection.getBuildApi();
|
|
56
|
-
const revisions = await buildApi.getDefinitionRevisions(project, definitionId);
|
|
57
|
-
return {
|
|
58
|
-
content: [{ type: "text", text: JSON.stringify(revisions, null, 2) }],
|
|
59
|
-
};
|
|
60
|
-
});
|
|
61
|
-
server.tool(BUILD_TOOLS.get_builds, "Retrieves a list of builds for a given project.", {
|
|
62
|
-
project: z.string().describe("Project ID or name to get builds for"),
|
|
63
|
-
definitions: z.array(z.number()).optional().describe("Array of build definition IDs to filter builds"),
|
|
64
|
-
queues: z.array(z.number()).optional().describe("Array of queue IDs to filter builds"),
|
|
65
|
-
buildNumber: z.string().optional().describe("Build number to filter builds"),
|
|
66
|
-
minTime: z.coerce.date().optional().describe("Minimum finish time to filter builds"),
|
|
67
|
-
maxTime: z.coerce.date().optional().describe("Maximum finish time to filter builds"),
|
|
68
|
-
requestedFor: z.string().optional().describe("User ID or name who requested the build"),
|
|
69
|
-
reasonFilter: z.number().optional().describe("Reason filter for the build (see BuildReason enum)"),
|
|
70
|
-
statusFilter: z.number().optional().describe("Status filter for the build (see BuildStatus enum)"),
|
|
71
|
-
resultFilter: z.number().optional().describe("Result filter for the build (see BuildResult enum)"),
|
|
72
|
-
tagFilters: z.array(z.string()).optional().describe("Array of tags to filter builds"),
|
|
73
|
-
properties: z.array(z.string()).optional().describe("Array of property names to include in the results"),
|
|
74
|
-
top: z.number().optional().describe("Maximum number of builds to return"),
|
|
75
|
-
continuationToken: z.string().optional().describe("Token for continuing paged results"),
|
|
76
|
-
maxBuildsPerDefinition: z.number().optional().describe("Maximum number of builds per definition"),
|
|
77
|
-
deletedFilter: z.number().optional().describe("Filter for deleted builds (see QueryDeletedOption enum)"),
|
|
78
|
-
queryOrder: z
|
|
79
|
-
.enum(getEnumKeys(BuildQueryOrder))
|
|
80
|
-
.default("QueueTimeDescending")
|
|
81
|
-
.optional()
|
|
82
|
-
.describe("Order in which builds are returned"),
|
|
83
|
-
branchName: z.string().optional().describe("Branch name to filter builds"),
|
|
84
|
-
buildIds: z.array(z.number()).optional().describe("Array of build IDs to retrieve"),
|
|
85
|
-
repositoryId: z.string().optional().describe("Repository ID to filter builds"),
|
|
86
|
-
repositoryType: z.enum(["TfsGit", "GitHub", "BitbucketCloud"]).optional().describe("Type of repository to filter builds"),
|
|
87
|
-
}, async ({ project, definitions, queues, buildNumber, minTime, maxTime, requestedFor, reasonFilter, statusFilter, resultFilter, tagFilters, properties, top, continuationToken, maxBuildsPerDefinition, deletedFilter, queryOrder, branchName, buildIds, repositoryId, repositoryType, }) => {
|
|
88
|
-
const connection = await connectionProvider();
|
|
89
|
-
const buildApi = await connection.getBuildApi();
|
|
90
|
-
const builds = await buildApi.getBuilds(project, definitions, queues, buildNumber, minTime, maxTime, requestedFor, reasonFilter, statusFilter, resultFilter, tagFilters, properties, top, continuationToken, maxBuildsPerDefinition, deletedFilter, safeEnumConvert(BuildQueryOrder, queryOrder), branchName, buildIds, repositoryId, repositoryType);
|
|
91
|
-
return {
|
|
92
|
-
content: [{ type: "text", text: JSON.stringify(builds, null, 2) }],
|
|
93
|
-
};
|
|
94
|
-
});
|
|
95
|
-
server.tool(BUILD_TOOLS.get_log, "Retrieves the logs for a specific build.", {
|
|
96
|
-
project: z.string().describe("Project ID or name to get the build log for"),
|
|
97
|
-
buildId: z.number().describe("ID of the build to get the log for"),
|
|
98
|
-
}, async ({ project, buildId }) => {
|
|
99
|
-
const connection = await connectionProvider();
|
|
100
|
-
const buildApi = await connection.getBuildApi();
|
|
101
|
-
const logs = await buildApi.getBuildLogs(project, buildId);
|
|
102
|
-
return {
|
|
103
|
-
content: [{ type: "text", text: JSON.stringify(logs, null, 2) }],
|
|
104
|
-
};
|
|
105
|
-
});
|
|
106
|
-
server.tool(BUILD_TOOLS.get_log_by_id, "Get a specific build log by log ID.", {
|
|
107
|
-
project: z.string().describe("Project ID or name to get the build log for"),
|
|
108
|
-
buildId: z.number().describe("ID of the build to get the log for"),
|
|
109
|
-
logId: z.number().describe("ID of the log to retrieve"),
|
|
110
|
-
startLine: z.number().optional().describe("Starting line number for the log content, defaults to 0"),
|
|
111
|
-
endLine: z.number().optional().describe("Ending line number for the log content, defaults to the end of the log"),
|
|
112
|
-
}, async ({ project, buildId, logId, startLine, endLine }) => {
|
|
113
|
-
const connection = await connectionProvider();
|
|
114
|
-
const buildApi = await connection.getBuildApi();
|
|
115
|
-
const logLines = await buildApi.getBuildLogLines(project, buildId, logId, startLine, endLine);
|
|
116
|
-
return {
|
|
117
|
-
content: [{ type: "text", text: JSON.stringify(logLines, null, 2) }],
|
|
118
|
-
};
|
|
119
|
-
});
|
|
120
|
-
server.tool(BUILD_TOOLS.get_changes, "Get the changes associated with a specific build.", {
|
|
121
|
-
project: z.string().describe("Project ID or name to get the build changes for"),
|
|
122
|
-
buildId: z.number().describe("ID of the build to get changes for"),
|
|
123
|
-
continuationToken: z.string().optional().describe("Continuation token for pagination"),
|
|
124
|
-
top: z.number().default(100).describe("Number of changes to retrieve, defaults to 100"),
|
|
125
|
-
includeSourceChange: z.boolean().optional().describe("Whether to include source changes in the results, defaults to false"),
|
|
126
|
-
}, async ({ project, buildId, continuationToken, top, includeSourceChange }) => {
|
|
127
|
-
const connection = await connectionProvider();
|
|
128
|
-
const buildApi = await connection.getBuildApi();
|
|
129
|
-
const changes = await buildApi.getBuildChanges(project, buildId, continuationToken, top, includeSourceChange);
|
|
130
|
-
return {
|
|
131
|
-
content: [{ type: "text", text: JSON.stringify(changes, null, 2) }],
|
|
132
|
-
};
|
|
133
|
-
});
|
|
134
|
-
server.tool(BUILD_TOOLS.pipelines_get_run, "Gets a run for a particular pipeline.", {
|
|
135
|
-
project: z.string().describe("Project ID or name to run the build in"),
|
|
136
|
-
pipelineId: z.number().describe("ID of the pipeline to run"),
|
|
137
|
-
runId: z.number().describe("ID of the run to get"),
|
|
138
|
-
}, async ({ project, pipelineId, runId }) => {
|
|
139
|
-
const connection = await connectionProvider();
|
|
140
|
-
const pipelinesApi = await connection.getPipelinesApi();
|
|
141
|
-
const pipelineRun = await pipelinesApi.getRun(project, pipelineId, runId);
|
|
142
|
-
return {
|
|
143
|
-
content: [{ type: "text", text: JSON.stringify(pipelineRun, null, 2) }],
|
|
144
|
-
};
|
|
145
|
-
});
|
|
146
|
-
server.tool(BUILD_TOOLS.pipelines_list_runs, "Gets top 10000 runs for a particular pipeline.", {
|
|
147
|
-
project: z.string().describe("Project ID or name to run the build in"),
|
|
148
|
-
pipelineId: z.number().describe("ID of the pipeline to run"),
|
|
149
|
-
}, async ({ project, pipelineId }) => {
|
|
150
|
-
const connection = await connectionProvider();
|
|
151
|
-
const pipelinesApi = await connection.getPipelinesApi();
|
|
152
|
-
const pipelineRuns = await pipelinesApi.listRuns(project, pipelineId);
|
|
153
|
-
return {
|
|
154
|
-
content: [{ type: "text", text: JSON.stringify(pipelineRuns, null, 2) }],
|
|
155
|
-
};
|
|
156
|
-
});
|
|
157
|
-
const variableSchema = z.object({
|
|
158
|
-
value: z.string().optional(),
|
|
159
|
-
isSecret: z.boolean().optional(),
|
|
160
|
-
});
|
|
161
|
-
const resourcesSchema = z.object({
|
|
162
|
-
builds: z
|
|
163
|
-
.record(z.string().describe("Name of the build resource."), z.object({
|
|
164
|
-
version: z.string().optional().describe("Version of the build resource."),
|
|
165
|
-
}))
|
|
166
|
-
.optional(),
|
|
167
|
-
containers: z
|
|
168
|
-
.record(z.string().describe("Name of the container resource."), z.object({
|
|
169
|
-
version: z.string().optional().describe("Version of the container resource."),
|
|
170
|
-
}))
|
|
171
|
-
.optional(),
|
|
172
|
-
packages: z
|
|
173
|
-
.record(z.string().describe("Name of the package resource."), z.object({
|
|
174
|
-
version: z.string().optional().describe("Version of the package resource."),
|
|
175
|
-
}))
|
|
176
|
-
.optional(),
|
|
177
|
-
pipelines: z.record(z.string().describe("Name of the pipeline resource."), z.object({
|
|
178
|
-
runId: z.number().describe("Id of the source pipeline run that triggered or is referenced by this pipeline run."),
|
|
179
|
-
version: z.string().optional().describe("Version of the source pipeline run."),
|
|
180
|
-
})),
|
|
181
|
-
repositories: z
|
|
182
|
-
.record(z.string().describe("Name of the repository resource."), z.object({
|
|
183
|
-
refName: z.string().describe("Reference name, e.g., refs/heads/main."),
|
|
184
|
-
token: z.string().optional(),
|
|
185
|
-
tokenType: z.string().optional(),
|
|
186
|
-
version: z.string().optional().describe("Version of the repository resource, git commit sha."),
|
|
187
|
-
}))
|
|
188
|
-
.optional(),
|
|
189
|
-
});
|
|
190
|
-
server.tool(BUILD_TOOLS.pipelines_run_pipeline, "Starts a new run of a pipeline.", {
|
|
191
|
-
project: z.string().describe("Project ID or name to run the build in"),
|
|
192
|
-
pipelineId: z.number().describe("ID of the pipeline to run"),
|
|
193
|
-
pipelineVersion: z.number().optional().describe("Version of the pipeline to run. If not provided, the latest version will be used."),
|
|
194
|
-
previewRun: z.boolean().optional().describe("If true, returns the final YAML document after parsing templates without creating a new run."),
|
|
195
|
-
resources: resourcesSchema.optional().describe("A dictionary of resources to pass to the pipeline."),
|
|
196
|
-
stagesToSkip: z.array(z.string()).optional().describe("A list of stages to skip."),
|
|
197
|
-
templateParameters: z.record(z.string(), z.string()).optional().describe("Custom build parameters as key-value pairs"),
|
|
198
|
-
variables: z.record(z.string(), variableSchema).optional().describe("A dictionary of variables to pass to the pipeline."),
|
|
199
|
-
yamlOverride: z.string().optional().describe("YAML override for the pipeline run."),
|
|
200
|
-
}, async ({ project, pipelineId, pipelineVersion, previewRun, resources, stagesToSkip, templateParameters, variables, yamlOverride }) => {
|
|
201
|
-
if (!previewRun && yamlOverride) {
|
|
202
|
-
throw new Error("Parameter 'yamlOverride' can only be specified together with parameter 'previewRun'.");
|
|
203
|
-
}
|
|
204
|
-
const connection = await connectionProvider();
|
|
205
|
-
const pipelinesApi = await connection.getPipelinesApi();
|
|
206
|
-
const runRequest = {
|
|
207
|
-
previewRun: previewRun,
|
|
208
|
-
resources: {
|
|
209
|
-
...resources,
|
|
210
|
-
},
|
|
211
|
-
stagesToSkip: stagesToSkip,
|
|
212
|
-
templateParameters: templateParameters,
|
|
213
|
-
variables: variables,
|
|
214
|
-
yamlOverride: yamlOverride,
|
|
215
|
-
};
|
|
216
|
-
const pipelineRun = await pipelinesApi.runPipeline(runRequest, project, pipelineId, pipelineVersion);
|
|
217
|
-
const queuedBuild = { id: pipelineRun.id };
|
|
218
|
-
const buildId = queuedBuild.id;
|
|
219
|
-
if (buildId === undefined) {
|
|
220
|
-
throw new Error("Failed to get build ID from pipeline run");
|
|
221
|
-
}
|
|
222
|
-
return {
|
|
223
|
-
content: [{ type: "text", text: JSON.stringify(pipelineRun, null, 2) }],
|
|
224
|
-
};
|
|
225
|
-
});
|
|
226
|
-
server.tool(BUILD_TOOLS.get_status, "Fetches the status of a specific build.", {
|
|
227
|
-
project: z.string().describe("Project ID or name to get the build status for"),
|
|
228
|
-
buildId: z.number().describe("ID of the build to get the status for"),
|
|
229
|
-
}, async ({ project, buildId }) => {
|
|
230
|
-
const connection = await connectionProvider();
|
|
231
|
-
const buildApi = await connection.getBuildApi();
|
|
232
|
-
const build = await buildApi.getBuildReport(project, buildId);
|
|
233
|
-
return {
|
|
234
|
-
content: [{ type: "text", text: JSON.stringify(build, null, 2) }],
|
|
235
|
-
};
|
|
236
|
-
});
|
|
237
|
-
server.tool(BUILD_TOOLS.update_build_stage, "Updates the stage of a specific build.", {
|
|
238
|
-
project: z.string().describe("Project ID or name to update the build stage for"),
|
|
239
|
-
buildId: z.number().describe("ID of the build to update"),
|
|
240
|
-
stageName: z.string().describe("Name of the stage to update"),
|
|
241
|
-
status: z.enum(getEnumKeys(StageUpdateType)).describe("New status for the stage"),
|
|
242
|
-
forceRetryAllJobs: z.boolean().default(false).describe("Whether to force retry all jobs in the stage."),
|
|
243
|
-
}, async ({ project, buildId, stageName, status, forceRetryAllJobs }) => {
|
|
244
|
-
const connection = await connectionProvider();
|
|
245
|
-
const orgUrl = connection.serverUrl;
|
|
246
|
-
const endpoint = `${orgUrl}/${project}/_apis/build/builds/${buildId}/stages/${stageName}?api-version=${apiVersion}`;
|
|
247
|
-
const token = await tokenProvider();
|
|
248
|
-
const body = {
|
|
249
|
-
forceRetryAllJobs: forceRetryAllJobs,
|
|
250
|
-
state: safeEnumConvert(StageUpdateType, status),
|
|
251
|
-
};
|
|
252
|
-
const response = await fetch(endpoint, {
|
|
253
|
-
method: "PATCH",
|
|
254
|
-
headers: {
|
|
255
|
-
"Content-Type": "application/json",
|
|
256
|
-
"Authorization": `Bearer ${token.token}`,
|
|
257
|
-
"User-Agent": userAgentProvider(),
|
|
258
|
-
},
|
|
259
|
-
body: JSON.stringify(body),
|
|
260
|
-
});
|
|
261
|
-
if (!response.ok) {
|
|
262
|
-
const errorText = await response.text();
|
|
263
|
-
throw new Error(`Failed to update build stage: ${response.status} ${errorText}`);
|
|
264
|
-
}
|
|
265
|
-
const updatedBuild = await response.text();
|
|
266
|
-
return {
|
|
267
|
-
content: [{ type: "text", text: JSON.stringify(updatedBuild, null, 2) }],
|
|
268
|
-
};
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
export { BUILD_TOOLS, configureBuildTools };
|