@azure-devops/mcp 2.4.0 β 2.5.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/README.md +29 -8
- package/dist/shared/elicitations.js +62 -0
- package/dist/tools/core.js +11 -3
- package/dist/tools/pipelines.js +67 -0
- package/dist/tools/repositories.js +132 -5
- package/dist/tools/test-plans.js +77 -27
- package/dist/tools/work-items.js +39 -0
- package/dist/tools/work.js +108 -15
- package/dist/version.js +1 -1
- package/package.json +6 -7
- package/dist/tools/builds.js +0 -271
- package/dist/tools/releases.js +0 -97
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# β Azure DevOps MCP Server
|
|
2
2
|
|
|
3
|
+
> [!IMPORTANT]
|
|
4
|
+
> The Azure DevOps Remote MCP Server is now available in public preview for all organizations. We recommend migrating to the [Remote MCP Server](https://learn.microsoft.com/en-us/azure/devops/mcp-server/remote-mcp-server) going forward.
|
|
5
|
+
>
|
|
6
|
+
> [Learn more](#-remote-mcp-server)
|
|
7
|
+
|
|
3
8
|
Easily install the Azure DevOps MCP Server for VS Code or VS Code Insiders:
|
|
4
9
|
|
|
5
10
|
[](https://insiders.vscode.dev/redirect/mcp/install?name=ado&config=%7B%20%22type%22%3A%20%22stdio%22%2C%20%22command%22%3A%20%22npx%22%2C%20%22args%22%3A%20%5B%22-y%22%2C%20%22%40azure-devops%2Fmcp%22%2C%20%22%24%7Binput%3Aado_org%7D%22%5D%7D&inputs=%5B%7B%22id%22%3A%20%22ado_org%22%2C%20%22type%22%3A%20%22promptString%22%2C%20%22description%22%3A%20%22Azure%20DevOps%20organization%20name%20%20%28e.g.%20%27contoso%27%29%22%7D%5D)
|
|
@@ -11,13 +16,14 @@ This TypeScript project provides a **local** MCP server for Azure DevOps, enabli
|
|
|
11
16
|
|
|
12
17
|
1. [πΊ Overview](#-overview)
|
|
13
18
|
2. [π Expectations](#-expectations)
|
|
14
|
-
3. [
|
|
15
|
-
4. [
|
|
16
|
-
5. [
|
|
17
|
-
6. [
|
|
18
|
-
7. [
|
|
19
|
-
8. [
|
|
20
|
-
9. [
|
|
19
|
+
3. [π Remote MCP Server](#-remote-mcp-server)
|
|
20
|
+
4. [βοΈ Supported Tools](#οΈ-supported-tools)
|
|
21
|
+
5. [π Installation & Getting Started](#-installation--getting-started)
|
|
22
|
+
6. [π Using Domains](#-using-domains)
|
|
23
|
+
7. [π Troubleshooting](#-troubleshooting)
|
|
24
|
+
8. [π© Examples & Best Practices](#-examples--best-practices)
|
|
25
|
+
9. [πββοΈ Frequently Asked Questions](#οΈ-frequently-asked-questions)
|
|
26
|
+
10. [π Contributing](#-contributing)
|
|
21
27
|
|
|
22
28
|
## πΊ Overview
|
|
23
29
|
|
|
@@ -40,13 +46,28 @@ The Azure DevOps MCP Server brings Azure DevOps context to your agents. Try prom
|
|
|
40
46
|
|
|
41
47
|
The Azure DevOps MCP Server is built from tools that are concise, simple, focused, and easy to useβeach designed for a specific scenario. We intentionally avoid complex tools that try to do too much. The goal is to provide a thin abstraction layer over the REST APIs, making data access straightforward and letting the language model handle complex reasoning.
|
|
42
48
|
|
|
49
|
+
## π Remote MCP Server
|
|
50
|
+
|
|
51
|
+
The Azure DevOps **Remote MCP Server** is now available in [public preview](https://devblogs.microsoft.com/devops/azure-devops-remote-mcp-server-public-preview).
|
|
52
|
+
|
|
53
|
+
Over time, the Remote MCP Server will replace this local MCP Server. We will continue to support the local server for now, but future investments will primarily focus on the remote experience.
|
|
54
|
+
|
|
55
|
+
We encourage all users of the local MCP Server to begin migrating to the Remote MCP Server.
|
|
56
|
+
|
|
57
|
+
If you encounter issues with tools, need support, or have a feature request, you can report an issue using the [Remote MCP Server issue template](https://github.com/microsoft/azure-devops-mcp/issues/new?template=remote-mcp-server-issue.md). During the preview period, we will track Remote MCP Server issues through this repository.
|
|
58
|
+
|
|
59
|
+
> [!WARNING]
|
|
60
|
+
> Internal Microsoft users of the Remote MCP Server should **not** create issues in this repository. Please use the dedicated Teams channel instead.
|
|
61
|
+
|
|
62
|
+
For instructions on how to get started with the Remote MCP Server, see the [onboarding documentation](https://learn.microsoft.com/en-us/azure/devops/mcp-server/remote-mcp-server).
|
|
63
|
+
|
|
43
64
|
## βοΈ Supported Tools
|
|
44
65
|
|
|
45
66
|
See [TOOLSET.md](./docs/TOOLSET.md) for a comprehensive list.
|
|
46
67
|
|
|
47
68
|
## π Installation & Getting Started
|
|
48
69
|
|
|
49
|
-
For the best experience, use Visual Studio Code and GitHub Copilot. See the [getting started documentation](./docs/GETTINGSTARTED.md) to use our MCP Server with other tools such as Visual Studio 2022, Claude Code, and
|
|
70
|
+
For the best experience, use Visual Studio Code and GitHub Copilot. See the [getting started documentation](./docs/GETTINGSTARTED.md) to use our MCP Server with other tools such as Visual Studio 2022, Claude Code, Cursor, Opencode, and Kilocode.
|
|
50
71
|
|
|
51
72
|
### Prerequisites
|
|
52
73
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
export async function elicitProject(server, connection, message) {
|
|
4
|
+
const coreApi = await connection.getCoreApi();
|
|
5
|
+
const projects = await coreApi.getProjects("wellFormed", 100, 0, undefined, false);
|
|
6
|
+
if (!projects || projects.length === 0) {
|
|
7
|
+
return { response: { content: [{ type: "text", text: "No projects found to select from." }], isError: true } };
|
|
8
|
+
}
|
|
9
|
+
const result = await server.server.elicitInput({
|
|
10
|
+
mode: "form",
|
|
11
|
+
message: message ?? "Select the Azure DevOps project.",
|
|
12
|
+
requestedSchema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
project: {
|
|
16
|
+
type: "string",
|
|
17
|
+
title: "Project",
|
|
18
|
+
description: "The Azure DevOps project.",
|
|
19
|
+
oneOf: projects.map((p) => ({
|
|
20
|
+
const: p.name ?? p.id ?? "",
|
|
21
|
+
title: p.name ?? p.id ?? "Unknown project",
|
|
22
|
+
})),
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
required: ["project"],
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
if (result.action !== "accept" || !result.content?.project) {
|
|
29
|
+
return { response: { content: [{ type: "text", text: "Project selection cancelled." }] } };
|
|
30
|
+
}
|
|
31
|
+
return { resolved: String(result.content.project) };
|
|
32
|
+
}
|
|
33
|
+
export async function elicitTeam(server, connection, project, message) {
|
|
34
|
+
const coreApi = await connection.getCoreApi();
|
|
35
|
+
const teams = await coreApi.getTeams(project, undefined, undefined, undefined, false);
|
|
36
|
+
if (!teams || teams.length === 0) {
|
|
37
|
+
return { response: { content: [{ type: "text", text: "No teams found to select from." }], isError: true } };
|
|
38
|
+
}
|
|
39
|
+
const result = await server.server.elicitInput({
|
|
40
|
+
mode: "form",
|
|
41
|
+
message: message ?? "Select the team.",
|
|
42
|
+
requestedSchema: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
team: {
|
|
46
|
+
type: "string",
|
|
47
|
+
title: "Team",
|
|
48
|
+
description: "The team from a specific Azure DevOps project.",
|
|
49
|
+
oneOf: teams.map((t) => ({
|
|
50
|
+
const: t.name ?? t.id ?? "",
|
|
51
|
+
title: t.name ?? t.id ?? "Unknown team",
|
|
52
|
+
})),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
required: ["team"],
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
if (result.action !== "accept" || !result.content?.team) {
|
|
59
|
+
return { response: { content: [{ type: "text", text: "Team selection cancelled." }] } };
|
|
60
|
+
}
|
|
61
|
+
return { resolved: String(result.content.team) };
|
|
62
|
+
}
|
package/dist/tools/core.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { searchIdentities } from "./auth.js";
|
|
5
|
+
import { elicitProject } from "../shared/elicitations.js";
|
|
5
6
|
const CORE_TOOLS = {
|
|
6
7
|
list_project_teams: "core_list_project_teams",
|
|
7
8
|
list_projects: "core_list_projects",
|
|
@@ -12,8 +13,8 @@ function filterProjectsByName(projects, projectNameFilter) {
|
|
|
12
13
|
return projects.filter((project) => project.name?.toLowerCase().includes(lowerCaseFilter));
|
|
13
14
|
}
|
|
14
15
|
function configureCoreTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
15
|
-
server.tool(CORE_TOOLS.list_project_teams, "Retrieve a list of teams for
|
|
16
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
16
|
+
server.tool(CORE_TOOLS.list_project_teams, "Retrieve a list of teams for an Azure DevOps project. If a project is not specified, you will be prompted to select one.", {
|
|
17
|
+
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."),
|
|
17
18
|
mine: z.boolean().optional().describe("If true, only return teams that the authenticated user is a member of."),
|
|
18
19
|
top: z.number().optional().describe("The maximum number of teams to return. Defaults to 100."),
|
|
19
20
|
skip: z.number().optional().describe("The number of teams to skip for pagination. Defaults to 0."),
|
|
@@ -21,7 +22,14 @@ function configureCoreTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
21
22
|
try {
|
|
22
23
|
const connection = await connectionProvider();
|
|
23
24
|
const coreApi = await connection.getCoreApi();
|
|
24
|
-
|
|
25
|
+
let resolvedProject = project;
|
|
26
|
+
if (!resolvedProject) {
|
|
27
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to list teams for.");
|
|
28
|
+
if ("response" in result)
|
|
29
|
+
return result.response;
|
|
30
|
+
resolvedProject = result.resolved;
|
|
31
|
+
}
|
|
32
|
+
const teams = await coreApi.getTeams(resolvedProject, mine, top, skip, false);
|
|
25
33
|
if (!teams) {
|
|
26
34
|
return { content: [{ type: "text", text: "No teams found" }], isError: true };
|
|
27
35
|
}
|
package/dist/tools/pipelines.js
CHANGED
|
@@ -5,6 +5,8 @@ import { BuildQueryOrder, DefinitionQueryOrder } from "azure-devops-node-api/int
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { StageUpdateType } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
|
|
7
7
|
import { ConfigurationType, RepositoryType } from "azure-devops-node-api/interfaces/PipelinesInterfaces.js";
|
|
8
|
+
import { mkdirSync, createWriteStream } from "fs";
|
|
9
|
+
import { join, resolve } from "path";
|
|
8
10
|
const PIPELINE_TOOLS = {
|
|
9
11
|
pipelines_get_builds: "pipelines_get_builds",
|
|
10
12
|
pipelines_get_build_changes: "pipelines_get_build_changes",
|
|
@@ -18,6 +20,8 @@ const PIPELINE_TOOLS = {
|
|
|
18
20
|
pipelines_get_run: "pipelines_get_run",
|
|
19
21
|
pipelines_list_runs: "pipelines_list_runs",
|
|
20
22
|
pipelines_run_pipeline: "pipelines_run_pipeline",
|
|
23
|
+
pipelines_list_artifacts: "pipelines_list_artifacts",
|
|
24
|
+
pipelines_download_artifact: "pipelines_download_artifact",
|
|
21
25
|
};
|
|
22
26
|
function configurePipelineTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
23
27
|
server.tool(PIPELINE_TOOLS.pipelines_get_build_definitions, "Retrieves a list of build definitions for a given project.", {
|
|
@@ -315,5 +319,68 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
|
|
|
315
319
|
content: [{ type: "text", text: JSON.stringify(updatedBuild, null, 2) }],
|
|
316
320
|
};
|
|
317
321
|
});
|
|
322
|
+
server.tool(PIPELINE_TOOLS.pipelines_list_artifacts, "Lists artifacts for a given build.", {
|
|
323
|
+
project: z.string().describe("The name or ID of the project."),
|
|
324
|
+
buildId: z.number().describe("The ID of the build."),
|
|
325
|
+
}, async ({ project, buildId }) => {
|
|
326
|
+
const connection = await connectionProvider();
|
|
327
|
+
const buildApi = await connection.getBuildApi();
|
|
328
|
+
const artifacts = await buildApi.getArtifacts(project, buildId);
|
|
329
|
+
return {
|
|
330
|
+
content: [{ type: "text", text: JSON.stringify(artifacts, null, 2) }],
|
|
331
|
+
};
|
|
332
|
+
});
|
|
333
|
+
server.tool(PIPELINE_TOOLS.pipelines_download_artifact, "Downloads a pipeline artifact.", {
|
|
334
|
+
project: z.string().describe("The name or ID of the project."),
|
|
335
|
+
buildId: z.number().describe("The ID of the build."),
|
|
336
|
+
artifactName: z.string().describe("The name of the artifact to download."),
|
|
337
|
+
destinationPath: z.string().optional().describe("The local path to download the artifact to. If not provided, returns binary content as base64."),
|
|
338
|
+
}, async ({ project, buildId, artifactName, destinationPath }) => {
|
|
339
|
+
const connection = await connectionProvider();
|
|
340
|
+
const buildApi = await connection.getBuildApi();
|
|
341
|
+
const artifact = await buildApi.getArtifact(project, buildId, artifactName);
|
|
342
|
+
if (!artifact) {
|
|
343
|
+
return {
|
|
344
|
+
content: [{ type: "text", text: `Artifact ${artifactName} not found in build ${buildId}.` }],
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const fileStream = await buildApi.getArtifactContentZip(project, buildId, artifactName);
|
|
348
|
+
// If destinationPath is provided, save to disk
|
|
349
|
+
if (destinationPath) {
|
|
350
|
+
const fullDestinationPath = resolve(destinationPath);
|
|
351
|
+
mkdirSync(fullDestinationPath, { recursive: true });
|
|
352
|
+
const fileDestinationPath = join(fullDestinationPath, `${artifactName}.zip`);
|
|
353
|
+
const writeStream = createWriteStream(fileDestinationPath);
|
|
354
|
+
await new Promise((resolve, reject) => {
|
|
355
|
+
fileStream.pipe(writeStream);
|
|
356
|
+
fileStream.on("end", () => resolve());
|
|
357
|
+
fileStream.on("error", (err) => reject(err));
|
|
358
|
+
});
|
|
359
|
+
return {
|
|
360
|
+
content: [{ type: "text", text: `Artifact ${artifactName} downloaded to ${destinationPath}.` }],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
// Otherwise, return binary content as base64
|
|
364
|
+
const chunks = [];
|
|
365
|
+
await new Promise((resolve, reject) => {
|
|
366
|
+
fileStream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
367
|
+
fileStream.on("end", () => resolve());
|
|
368
|
+
fileStream.on("error", (err) => reject(err));
|
|
369
|
+
});
|
|
370
|
+
const buffer = Buffer.concat(chunks);
|
|
371
|
+
const base64Data = buffer.toString("base64");
|
|
372
|
+
return {
|
|
373
|
+
content: [
|
|
374
|
+
{
|
|
375
|
+
type: "resource",
|
|
376
|
+
resource: {
|
|
377
|
+
uri: `data:application/zip;base64,${base64Data}`,
|
|
378
|
+
mimeType: "application/zip",
|
|
379
|
+
text: base64Data,
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
};
|
|
384
|
+
});
|
|
318
385
|
}
|
|
319
386
|
export { PIPELINE_TOOLS, configurePipelineTools };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
-
import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, CommentThreadStatus, GitPullRequestMergeStrategy, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
|
3
|
+
import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, CommentThreadStatus, GitPullRequestMergeStrategy, VersionControlRecursionType, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { getCurrentUserDetails, getUserIdFromEmail } from "./auth.js";
|
|
6
6
|
import { getEnumKeys } from "../utils.js";
|
|
@@ -23,6 +23,8 @@ const REPO_TOOLS = {
|
|
|
23
23
|
update_pull_request_thread: "repo_update_pull_request_thread",
|
|
24
24
|
search_commits: "repo_search_commits",
|
|
25
25
|
list_pull_requests_by_commits: "repo_list_pull_requests_by_commits",
|
|
26
|
+
vote_pull_request: "repo_vote_pull_request",
|
|
27
|
+
list_directory: "repo_list_directory",
|
|
26
28
|
};
|
|
27
29
|
function branchesFilterOutIrrelevantProperties(branches, top) {
|
|
28
30
|
return branches
|
|
@@ -103,6 +105,21 @@ function trimPullRequest(pr, includeDescription = false) {
|
|
|
103
105
|
project: pr.repository?.project?.name,
|
|
104
106
|
};
|
|
105
107
|
}
|
|
108
|
+
// Helper function to build a version descriptor from branch or commit
|
|
109
|
+
function buildVersionDescriptor(version, versionType) {
|
|
110
|
+
if (!version) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
const versionTypeMap = {
|
|
114
|
+
Branch: GitVersionType.Branch,
|
|
115
|
+
Commit: GitVersionType.Commit,
|
|
116
|
+
Tag: GitVersionType.Tag,
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
version: version,
|
|
120
|
+
versionType: versionTypeMap[versionType || "Branch"] ?? GitVersionType.Branch,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
106
123
|
function configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
107
124
|
server.tool(REPO_TOOLS.create_pull_request, "Create a new pull request.", {
|
|
108
125
|
repositoryId: z.string().describe("The ID of the repository where the pull request will be created."),
|
|
@@ -127,7 +144,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
127
144
|
}
|
|
128
145
|
: undefined;
|
|
129
146
|
const labelDefinitions = labels ? labels.map((label) => ({ name: label })) : undefined;
|
|
130
|
-
|
|
147
|
+
let pullRequest = await gitApi.createPullRequest({
|
|
131
148
|
sourceRefName,
|
|
132
149
|
targetRefName,
|
|
133
150
|
title,
|
|
@@ -136,7 +153,19 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
136
153
|
workItemRefs: workItemRefs,
|
|
137
154
|
forkSource,
|
|
138
155
|
labels: labelDefinitions,
|
|
156
|
+
supportsIterations: true,
|
|
139
157
|
}, repositoryId);
|
|
158
|
+
if (!pullRequest) {
|
|
159
|
+
const prs = await gitApi.getPullRequests(repositoryId, { sourceRefName, targetRefName, status: PullRequestStatus.Active }, undefined, undefined, 0, 1);
|
|
160
|
+
if (prs && prs.length > 0) {
|
|
161
|
+
pullRequest = prs[0];
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
return {
|
|
165
|
+
content: [{ type: "text", text: "Pull request created but API returned no data." }],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
140
169
|
const trimmedPullRequest = trimPullRequest(pullRequest, true);
|
|
141
170
|
return {
|
|
142
171
|
content: [{ type: "text", text: JSON.stringify(trimmedPullRequest, null, 2) }],
|
|
@@ -738,15 +767,18 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
738
767
|
}
|
|
739
768
|
});
|
|
740
769
|
server.tool(REPO_TOOLS.get_pull_request_by_id, "Get a pull request by its ID.", {
|
|
741
|
-
repositoryId: z
|
|
770
|
+
repositoryId: z
|
|
771
|
+
.string()
|
|
772
|
+
.describe("The ID or name of the repository where the pull request is located. When using a repository name instead of a GUID, the project parameter must also be provided."),
|
|
742
773
|
pullRequestId: z.number().describe("The ID of the pull request to retrieve."),
|
|
774
|
+
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
743
775
|
includeWorkItemRefs: z.boolean().optional().default(false).describe("Whether to reference work items associated with the pull request."),
|
|
744
776
|
includeLabels: z.boolean().optional().default(false).describe("Whether to include a summary of labels in the response."),
|
|
745
|
-
}, async ({ repositoryId, pullRequestId, includeWorkItemRefs, includeLabels }) => {
|
|
777
|
+
}, async ({ repositoryId, pullRequestId, project, includeWorkItemRefs, includeLabels }) => {
|
|
746
778
|
try {
|
|
747
779
|
const connection = await connectionProvider();
|
|
748
780
|
const gitApi = await connection.getGitApi();
|
|
749
|
-
const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId,
|
|
781
|
+
const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId, project, undefined, undefined, undefined, undefined, includeWorkItemRefs);
|
|
750
782
|
if (includeLabels) {
|
|
751
783
|
try {
|
|
752
784
|
const projectId = pullRequest.repository?.project?.id;
|
|
@@ -1149,5 +1181,100 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
1149
1181
|
};
|
|
1150
1182
|
}
|
|
1151
1183
|
});
|
|
1184
|
+
server.tool(REPO_TOOLS.vote_pull_request, "Cast a vote on a pull request. Automatically adds the current user as a reviewer if they are not already one.", {
|
|
1185
|
+
repositoryId: z.string().describe("The ID of the repository."),
|
|
1186
|
+
pullRequestId: z.number().describe("The ID of the pull request."),
|
|
1187
|
+
vote: z.enum(["Approved", "ApprovedWithSuggestions", "NoVote", "WaitingForAuthor", "Rejected"]).describe("The vote to cast: Approved(10), Suggestions(5), None(0), Waiting(-5), Rejected(-10)."),
|
|
1188
|
+
}, async ({ repositoryId, pullRequestId, vote }) => {
|
|
1189
|
+
const connection = await connectionProvider();
|
|
1190
|
+
const gitApi = await connection.getGitApi();
|
|
1191
|
+
const userDetails = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
|
|
1192
|
+
const userId = userDetails.authenticatedUser.id;
|
|
1193
|
+
if (!userId) {
|
|
1194
|
+
throw new Error("Could not determine authenticated user ID.");
|
|
1195
|
+
}
|
|
1196
|
+
const voteMap = {
|
|
1197
|
+
Approved: 10,
|
|
1198
|
+
ApprovedWithSuggestions: 5,
|
|
1199
|
+
NoVote: 0,
|
|
1200
|
+
WaitingForAuthor: -5,
|
|
1201
|
+
Rejected: -10,
|
|
1202
|
+
};
|
|
1203
|
+
await gitApi.createPullRequestReviewer({ vote: voteMap[vote], id: userId }, repositoryId, pullRequestId, userId);
|
|
1204
|
+
return {
|
|
1205
|
+
content: [
|
|
1206
|
+
{
|
|
1207
|
+
type: "text",
|
|
1208
|
+
text: `Successfully cast vote '${vote}' on PR #${pullRequestId}.`,
|
|
1209
|
+
},
|
|
1210
|
+
],
|
|
1211
|
+
};
|
|
1212
|
+
});
|
|
1213
|
+
server.tool(REPO_TOOLS.list_directory, "List files and folders in a directory within a repository. Useful for exploring the structure of a codebase or finding related files.", {
|
|
1214
|
+
repositoryId: z.string().describe("The ID or name of the repository."),
|
|
1215
|
+
path: z.string().optional().default("/").describe("The directory path to list (e.g., '/src' or '/src/components'). Defaults to repository root."),
|
|
1216
|
+
project: z.string().optional().describe("Project ID or name. Required if repositoryId is a name rather than a GUID."),
|
|
1217
|
+
version: z.string().optional().describe("The version identifier - branch name (e.g., 'main'), tag name, or commit SHA. Defaults to the repository's default branch."),
|
|
1218
|
+
versionType: z.enum(["Branch", "Commit", "Tag"]).optional().default("Branch").describe("The type of version identifier: 'Branch', 'Commit', or 'Tag'. Defaults to 'Branch'."),
|
|
1219
|
+
recursive: z.boolean().optional().default(false).describe("Whether to list items recursively. Defaults to false."),
|
|
1220
|
+
recursionDepth: z.number().optional().default(1).describe("Maximum depth for recursive listing (1-10). Only applies when recursive is true. Defaults to 1."),
|
|
1221
|
+
}, async ({ repositoryId, path, project, version, versionType, recursive, recursionDepth }) => {
|
|
1222
|
+
try {
|
|
1223
|
+
const connection = await connectionProvider();
|
|
1224
|
+
const gitApi = await connection.getGitApi();
|
|
1225
|
+
const versionDescriptor = buildVersionDescriptor(version, versionType);
|
|
1226
|
+
const clampedDepth = Math.min(Math.max(recursionDepth || 1, 1), 10);
|
|
1227
|
+
let recursionType = VersionControlRecursionType.OneLevel;
|
|
1228
|
+
if (recursive) {
|
|
1229
|
+
recursionType = VersionControlRecursionType.Full;
|
|
1230
|
+
}
|
|
1231
|
+
const items = await gitApi.getItems(repositoryId, project, path, recursionType, true, false, false, false, versionDescriptor);
|
|
1232
|
+
if (!items || items.length === 0) {
|
|
1233
|
+
return {
|
|
1234
|
+
content: [{ type: "text", text: `No items found at path: ${path}` }],
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
let filteredItems = items;
|
|
1238
|
+
if (recursive && clampedDepth < 10) {
|
|
1239
|
+
const basePath = path === "/" ? "" : path;
|
|
1240
|
+
const baseDepth = basePath.split("/").filter((p) => p).length;
|
|
1241
|
+
filteredItems = items.filter((item) => {
|
|
1242
|
+
if (!item.path)
|
|
1243
|
+
return false;
|
|
1244
|
+
const itemDepth = item.path.split("/").filter((p) => p).length;
|
|
1245
|
+
return itemDepth <= baseDepth + clampedDepth;
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
const formattedItems = filteredItems.map((item) => ({
|
|
1249
|
+
path: item.path,
|
|
1250
|
+
isFolder: item.isFolder,
|
|
1251
|
+
gitObjectType: item.gitObjectType,
|
|
1252
|
+
commitId: item.commitId,
|
|
1253
|
+
contentMetadata: item.contentMetadata
|
|
1254
|
+
? {
|
|
1255
|
+
contentType: item.contentMetadata.contentType,
|
|
1256
|
+
fileName: item.contentMetadata.fileName,
|
|
1257
|
+
}
|
|
1258
|
+
: undefined,
|
|
1259
|
+
}));
|
|
1260
|
+
const response = {
|
|
1261
|
+
count: formattedItems.length,
|
|
1262
|
+
path: path,
|
|
1263
|
+
recursive: recursive,
|
|
1264
|
+
recursionDepth: recursive ? clampedDepth : undefined,
|
|
1265
|
+
items: formattedItems,
|
|
1266
|
+
};
|
|
1267
|
+
return {
|
|
1268
|
+
content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
catch (error) {
|
|
1272
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
1273
|
+
return {
|
|
1274
|
+
content: [{ type: "text", text: `Error listing directory: ${errorMessage}` }],
|
|
1275
|
+
isError: true,
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1152
1279
|
}
|
|
1153
1280
|
export { REPO_TOOLS, configureRepoTools };
|
package/dist/tools/test-plans.js
CHANGED
|
@@ -76,29 +76,47 @@ function configureTestPlanTools(server, _, connectionProvider) {
|
|
|
76
76
|
parentSuiteId: z.number().describe("ID of the parent suite under which the new suite will be created, if not given by user this can be id of a root suite of the test plan"),
|
|
77
77
|
name: z.string().describe("Name of the child test suite"),
|
|
78
78
|
}, async ({ project, planId, parentSuiteId, name }) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
name
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
79
|
+
const maxRetries = 5;
|
|
80
|
+
const baseDelay = 500; // milliseconds
|
|
81
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
82
|
+
try {
|
|
83
|
+
const connection = await connectionProvider();
|
|
84
|
+
const testPlanApi = await connection.getTestPlanApi();
|
|
85
|
+
const testSuiteToCreate = {
|
|
86
|
+
name,
|
|
87
|
+
parentSuite: {
|
|
88
|
+
id: parentSuiteId,
|
|
89
|
+
name: "",
|
|
90
|
+
},
|
|
91
|
+
suiteType: 2,
|
|
92
|
+
};
|
|
93
|
+
const createdTestSuite = await testPlanApi.createTestSuite(testSuiteToCreate, project, planId);
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: "text", text: JSON.stringify(createdTestSuite, null, 2) }],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
100
|
+
// Check if it's a concurrency conflict error
|
|
101
|
+
const isConcurrencyError = errorMessage.includes("TF26071") || errorMessage.includes("got update") || errorMessage.includes("changed by someone else");
|
|
102
|
+
// If it's a concurrency error and we have retries left, wait and retry
|
|
103
|
+
if (isConcurrencyError && attempt < maxRetries) {
|
|
104
|
+
const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 200; // Exponential backoff with jitter
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
106
|
+
continue; // Retry
|
|
107
|
+
}
|
|
108
|
+
// If not a concurrency error or out of retries, return error
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: "text", text: `Error creating test suite: ${errorMessage}` }],
|
|
111
|
+
isError: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
101
114
|
}
|
|
115
|
+
// This should never be reached, but TypeScript requires a return value
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: "text", text: "Error creating test suite: Maximum retries exceeded" }],
|
|
118
|
+
isError: true,
|
|
119
|
+
};
|
|
102
120
|
});
|
|
103
121
|
server.tool(Test_Plan_Tools.add_test_cases_to_suite, "Adds existing test cases to a test suite.", {
|
|
104
122
|
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
|
|
@@ -257,16 +275,48 @@ function configureTestPlanTools(server, _, connectionProvider) {
|
|
|
257
275
|
};
|
|
258
276
|
}
|
|
259
277
|
});
|
|
260
|
-
server.tool(Test_Plan_Tools.test_results_from_build_id, "Gets a list of test results for a given project and build ID.", {
|
|
278
|
+
server.tool(Test_Plan_Tools.test_results_from_build_id, "Gets a list of test results for a given project and build ID. Can filter by test outcome (e.g. Failed, Passed, Aborted). Returns test case titles, error messages, stack traces, and outcomes. Efficiently handles builds with large numbers of test runs.", {
|
|
261
279
|
project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
|
|
262
280
|
buildid: z.number().describe("The ID of the build."),
|
|
263
|
-
|
|
281
|
+
outcomes: z.array(z.string()).optional().describe("Filter results by test outcome, e.g. ['Failed', 'Passed', 'Aborted']."),
|
|
282
|
+
}, async ({ project, buildid, outcomes }) => {
|
|
264
283
|
try {
|
|
265
284
|
const connection = await connectionProvider();
|
|
266
|
-
const
|
|
267
|
-
|
|
285
|
+
const testResultsApi = await connection.getTestResultsApi();
|
|
286
|
+
// Build filter expression for outcomes if specified
|
|
287
|
+
const outcomeFilter = outcomes?.map((o) => `Outcome eq '${o}'`).join(" or ");
|
|
288
|
+
// Fetch test result details for the build in a single API call
|
|
289
|
+
// This is more efficient than getTestRuns + getTestResults per run,
|
|
290
|
+
// especially for builds with many test runs (e.g., cloud testing with one run per test case)
|
|
291
|
+
const testResultDetails = await testResultsApi.getTestResultDetailsForBuild(project, buildid, undefined, // publishContext
|
|
292
|
+
undefined, // groupBy
|
|
293
|
+
outcomeFilter, // filter by outcome
|
|
294
|
+
undefined, // orderby
|
|
295
|
+
true // shouldIncludeResults - get individual test results, not just aggregates
|
|
296
|
+
);
|
|
297
|
+
// Extract individual test results from the grouped response
|
|
298
|
+
const allResults = [];
|
|
299
|
+
if (testResultDetails.resultsForGroup) {
|
|
300
|
+
for (const group of testResultDetails.resultsForGroup) {
|
|
301
|
+
if (group.results) {
|
|
302
|
+
allResults.push(...group.results);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Format results to extract useful fields
|
|
307
|
+
const formattedResults = allResults.map((r) => ({
|
|
308
|
+
id: r.id,
|
|
309
|
+
testCaseTitle: r.testCaseTitle,
|
|
310
|
+
outcome: r.outcome,
|
|
311
|
+
errorMessage: r.errorMessage,
|
|
312
|
+
stackTrace: r.stackTrace,
|
|
313
|
+
automatedTestName: r.automatedTestName,
|
|
314
|
+
automatedTestStorage: r.automatedTestStorage,
|
|
315
|
+
durationInMs: r.durationInMs,
|
|
316
|
+
runId: r.testRun?.id,
|
|
317
|
+
}));
|
|
268
318
|
return {
|
|
269
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
319
|
+
content: [{ type: "text", text: JSON.stringify(formattedResults, null, 2) }],
|
|
270
320
|
};
|
|
271
321
|
}
|
|
272
322
|
catch (error) {
|
package/dist/tools/work-items.js
CHANGED
|
@@ -16,6 +16,7 @@ const WORKITEM_TOOLS = {
|
|
|
16
16
|
list_work_item_revisions: "wit_list_work_item_revisions",
|
|
17
17
|
get_work_items_for_iteration: "wit_get_work_items_for_iteration",
|
|
18
18
|
add_work_item_comment: "wit_add_work_item_comment",
|
|
19
|
+
update_work_item_comment: "wit_update_work_item_comment",
|
|
19
20
|
add_child_work_items: "wit_add_child_work_items",
|
|
20
21
|
link_work_item_to_pull_request: "wit_link_work_item_to_pull_request",
|
|
21
22
|
get_work_item_type: "wit_get_work_item_type",
|
|
@@ -259,6 +260,44 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
259
260
|
};
|
|
260
261
|
}
|
|
261
262
|
});
|
|
263
|
+
server.tool(WORKITEM_TOOLS.update_work_item_comment, "Update an existing comment on a work item by ID.", {
|
|
264
|
+
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
265
|
+
workItemId: z.number().describe("The ID of the work item."),
|
|
266
|
+
commentId: z.number().describe("The ID of the comment to update."),
|
|
267
|
+
text: z.string().describe("The updated comment text."),
|
|
268
|
+
format: z.enum(["markdown", "html"]).optional().default("html"),
|
|
269
|
+
}, async ({ project, workItemId, commentId, text, format }) => {
|
|
270
|
+
try {
|
|
271
|
+
const connection = await connectionProvider();
|
|
272
|
+
const orgUrl = connection.serverUrl;
|
|
273
|
+
const accessToken = await tokenProvider();
|
|
274
|
+
const body = { text };
|
|
275
|
+
const formatParameter = format === "markdown" ? 0 : 1;
|
|
276
|
+
const response = await fetch(`${orgUrl}/${project}/_apis/wit/workItems/${workItemId}/comments/${commentId}?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
|
|
277
|
+
method: "PATCH",
|
|
278
|
+
headers: {
|
|
279
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
280
|
+
"Content-Type": "application/json",
|
|
281
|
+
"User-Agent": userAgentProvider(),
|
|
282
|
+
},
|
|
283
|
+
body: JSON.stringify(body),
|
|
284
|
+
});
|
|
285
|
+
if (!response.ok) {
|
|
286
|
+
throw new Error(`Failed to update work item comment: ${response.statusText}`);
|
|
287
|
+
}
|
|
288
|
+
const updatedComment = await response.text();
|
|
289
|
+
return {
|
|
290
|
+
content: [{ type: "text", text: updatedComment }],
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
295
|
+
return {
|
|
296
|
+
content: [{ type: "text", text: `Error updating work item comment: ${errorMessage}` }],
|
|
297
|
+
isError: true,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
});
|
|
262
301
|
server.tool(WORKITEM_TOOLS.list_work_item_revisions, "Retrieve list of revisions for a work item by ID.", {
|
|
263
302
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
264
303
|
workItemId: z.number().describe("The ID of the work item to retrieve revisions for."),
|
package/dist/tools/work.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { TreeStructureGroup, TreeNodeStructureType } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
5
|
+
import { elicitProject, elicitTeam } from "../shared/elicitations.js";
|
|
5
6
|
const WORK_TOOLS = {
|
|
6
7
|
list_team_iterations: "work_list_team_iterations",
|
|
7
8
|
list_iterations: "work_list_iterations",
|
|
@@ -10,22 +11,40 @@ const WORK_TOOLS = {
|
|
|
10
11
|
get_team_capacity: "work_get_team_capacity",
|
|
11
12
|
update_team_capacity: "work_update_team_capacity",
|
|
12
13
|
get_iteration_capacities: "work_get_iteration_capacities",
|
|
14
|
+
get_team_settings: "work_get_team_settings",
|
|
13
15
|
};
|
|
14
16
|
function configureWorkTools(server, _, connectionProvider) {
|
|
15
|
-
server.tool(WORK_TOOLS.list_team_iterations, "Retrieve a list of iterations for a specific team in a project.", {
|
|
16
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
17
|
-
team: z.string().describe("The name or ID of the Azure DevOps team."),
|
|
17
|
+
server.tool(WORK_TOOLS.list_team_iterations, "Retrieve a list of iterations for a specific team in a project. If a project or team is not specified, you will be prompted to select one.", {
|
|
18
|
+
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."),
|
|
19
|
+
team: z.string().optional().describe("The name or ID of the Azure DevOps team. Reuse from prior context if already known. If not provided, a team selection prompt will be shown."),
|
|
18
20
|
timeframe: z.enum(["current"]).optional().describe("The timeframe for which to retrieve iterations. Currently, only 'current' is supported."),
|
|
19
21
|
}, async ({ project, team, timeframe }) => {
|
|
20
22
|
try {
|
|
21
23
|
const connection = await connectionProvider();
|
|
24
|
+
let resolvedProject = project;
|
|
25
|
+
if (!resolvedProject) {
|
|
26
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to list team iterations for.");
|
|
27
|
+
if ("response" in result)
|
|
28
|
+
return result.response;
|
|
29
|
+
resolvedProject = result.resolved;
|
|
30
|
+
}
|
|
31
|
+
let resolvedTeam = team;
|
|
32
|
+
if (!resolvedTeam) {
|
|
33
|
+
const result = await elicitTeam(server, connection, resolvedProject, "Select the Azure DevOps team to list iterations for.");
|
|
34
|
+
if ("response" in result)
|
|
35
|
+
return result.response;
|
|
36
|
+
resolvedTeam = result.resolved;
|
|
37
|
+
}
|
|
22
38
|
const workApi = await connection.getWorkApi();
|
|
23
|
-
const iterations = await workApi.getTeamIterations({ project, team }, timeframe);
|
|
39
|
+
const iterations = await workApi.getTeamIterations({ project: resolvedProject, team: resolvedTeam }, timeframe);
|
|
24
40
|
if (!iterations) {
|
|
25
41
|
return { content: [{ type: "text", text: "No iterations found" }], isError: true };
|
|
26
42
|
}
|
|
27
43
|
return {
|
|
28
|
-
content: [
|
|
44
|
+
content: [
|
|
45
|
+
{ type: "text", text: `Project: ${resolvedProject}, Team: ${resolvedTeam}` },
|
|
46
|
+
{ type: "text", text: JSON.stringify(iterations, null, 2) },
|
|
47
|
+
],
|
|
29
48
|
};
|
|
30
49
|
}
|
|
31
50
|
catch (error) {
|
|
@@ -78,19 +97,26 @@ function configureWorkTools(server, _, connectionProvider) {
|
|
|
78
97
|
};
|
|
79
98
|
}
|
|
80
99
|
});
|
|
81
|
-
server.tool(WORK_TOOLS.list_iterations, "List all iterations in a specified Azure DevOps project.", {
|
|
82
|
-
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
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
|
+
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."),
|
|
83
102
|
depth: z.number().default(2).describe("Depth of children to fetch."),
|
|
84
103
|
excludedIds: z.array(z.number()).optional().describe("An optional array of iteration IDs, and thier children, that should not be returned."),
|
|
85
104
|
}, async ({ project, depth, excludedIds: ids }) => {
|
|
86
105
|
try {
|
|
87
106
|
const connection = await connectionProvider();
|
|
107
|
+
let resolvedProject = project;
|
|
108
|
+
if (!resolvedProject) {
|
|
109
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to list iterations for.");
|
|
110
|
+
if ("response" in result)
|
|
111
|
+
return result.response;
|
|
112
|
+
resolvedProject = result.resolved;
|
|
113
|
+
}
|
|
88
114
|
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
|
|
89
115
|
let results = [];
|
|
90
116
|
if (depth === undefined) {
|
|
91
117
|
depth = 1;
|
|
92
118
|
}
|
|
93
|
-
results = await workItemTrackingApi.getClassificationNodes(
|
|
119
|
+
results = await workItemTrackingApi.getClassificationNodes(resolvedProject, [], depth);
|
|
94
120
|
// Handle null or undefined results
|
|
95
121
|
if (!results) {
|
|
96
122
|
return { content: [{ type: "text", text: "No iterations were found" }], isError: true };
|
|
@@ -165,15 +191,22 @@ function configureWorkTools(server, _, connectionProvider) {
|
|
|
165
191
|
};
|
|
166
192
|
}
|
|
167
193
|
});
|
|
168
|
-
server.tool(WORK_TOOLS.get_team_capacity, "Get the team capacity of a specific team and iteration in a project.", {
|
|
169
|
-
project: z.string().describe("The name or Id of the Azure DevOps project."),
|
|
170
|
-
team: z.string().describe("The name or Id of the Azure DevOps team."),
|
|
194
|
+
server.tool(WORK_TOOLS.get_team_capacity, "Get the team capacity of a specific team and iteration in a project. If a project is not specified, you will be prompted to select one.", {
|
|
195
|
+
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."),
|
|
196
|
+
team: z.string().describe("The name or Id of the Azure DevOps team. Reuse from prior context if already known."),
|
|
171
197
|
iterationId: z.string().describe("The Iteration Id to get capacity for."),
|
|
172
198
|
}, async ({ project, team, iterationId }) => {
|
|
173
199
|
try {
|
|
174
200
|
const connection = await connectionProvider();
|
|
201
|
+
let resolvedProject = project;
|
|
202
|
+
if (!resolvedProject) {
|
|
203
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to get team capacity for.");
|
|
204
|
+
if ("response" in result)
|
|
205
|
+
return result.response;
|
|
206
|
+
resolvedProject = result.resolved;
|
|
207
|
+
}
|
|
175
208
|
const workApi = await connection.getWorkApi();
|
|
176
|
-
const teamContext = { project, team };
|
|
209
|
+
const teamContext = { project: resolvedProject, team };
|
|
177
210
|
const rawResults = await workApi.getCapacitiesWithIdentityRefAndTotals(teamContext, iterationId);
|
|
178
211
|
if (!rawResults || rawResults.teamMembers?.length === 0) {
|
|
179
212
|
return { content: [{ type: "text", text: "No team capacity assigned to the team" }], isError: true };
|
|
@@ -271,14 +304,21 @@ function configureWorkTools(server, _, connectionProvider) {
|
|
|
271
304
|
};
|
|
272
305
|
}
|
|
273
306
|
});
|
|
274
|
-
server.tool(WORK_TOOLS.get_iteration_capacities, "Get an iteration's capacity for all teams in iteration and project.", {
|
|
275
|
-
project: z.string().describe("The name or Id of the Azure DevOps project."),
|
|
307
|
+
server.tool(WORK_TOOLS.get_iteration_capacities, "Get an iteration's capacity for all teams in iteration and project. If a project is not specified, you will be prompted to select one.", {
|
|
308
|
+
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."),
|
|
276
309
|
iterationId: z.string().describe("The Iteration Id to get capacity for."),
|
|
277
310
|
}, async ({ project, iterationId }) => {
|
|
278
311
|
try {
|
|
279
312
|
const connection = await connectionProvider();
|
|
313
|
+
let resolvedProject = project;
|
|
314
|
+
if (!resolvedProject) {
|
|
315
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to get iteration capacities for.");
|
|
316
|
+
if ("response" in result)
|
|
317
|
+
return result.response;
|
|
318
|
+
resolvedProject = result.resolved;
|
|
319
|
+
}
|
|
280
320
|
const workApi = await connection.getWorkApi();
|
|
281
|
-
const rawResults = await workApi.getTotalIterationCapacities(
|
|
321
|
+
const rawResults = await workApi.getTotalIterationCapacities(resolvedProject, iterationId);
|
|
282
322
|
if (!rawResults || !rawResults.teams || rawResults.teams.length === 0) {
|
|
283
323
|
return { content: [{ type: "text", text: "No iteration capacity assigned to the teams" }], isError: true };
|
|
284
324
|
}
|
|
@@ -294,5 +334,58 @@ function configureWorkTools(server, _, connectionProvider) {
|
|
|
294
334
|
};
|
|
295
335
|
}
|
|
296
336
|
});
|
|
337
|
+
server.tool(WORK_TOOLS.get_team_settings, "Get team settings including default iteration, backlog iteration, and default area path for a team. If a project or team is not specified, you will be prompted to select one.", {
|
|
338
|
+
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."),
|
|
339
|
+
team: z.string().optional().describe("The name or ID of the Azure DevOps team. Reuse from prior context if already known. If not provided, a team selection prompt will be shown."),
|
|
340
|
+
}, async ({ project, team }) => {
|
|
341
|
+
try {
|
|
342
|
+
const connection = await connectionProvider();
|
|
343
|
+
let resolvedProject = project;
|
|
344
|
+
if (!resolvedProject) {
|
|
345
|
+
const result = await elicitProject(server, connection, "Select the Azure DevOps project to get team settings for.");
|
|
346
|
+
if ("response" in result)
|
|
347
|
+
return result.response;
|
|
348
|
+
resolvedProject = result.resolved;
|
|
349
|
+
}
|
|
350
|
+
let resolvedTeam = team;
|
|
351
|
+
if (!resolvedTeam) {
|
|
352
|
+
const result = await elicitTeam(server, connection, resolvedProject, "Select the Azure DevOps team to get settings for.");
|
|
353
|
+
if ("response" in result)
|
|
354
|
+
return result.response;
|
|
355
|
+
resolvedTeam = result.resolved;
|
|
356
|
+
}
|
|
357
|
+
const workApi = await connection.getWorkApi();
|
|
358
|
+
const teamContext = { project: resolvedProject, team: resolvedTeam };
|
|
359
|
+
const teamSettings = await workApi.getTeamSettings(teamContext);
|
|
360
|
+
if (!teamSettings) {
|
|
361
|
+
return { content: [{ type: "text", text: "No team settings found" }], isError: true };
|
|
362
|
+
}
|
|
363
|
+
const teamFieldValues = await workApi.getTeamFieldValues(teamContext);
|
|
364
|
+
const result = {
|
|
365
|
+
backlogIteration: teamSettings.backlogIteration,
|
|
366
|
+
defaultIteration: teamSettings.defaultIteration,
|
|
367
|
+
defaultIterationMacro: teamSettings.defaultIterationMacro,
|
|
368
|
+
backlogVisibilities: teamSettings.backlogVisibilities,
|
|
369
|
+
bugsBehavior: teamSettings.bugsBehavior,
|
|
370
|
+
workingDays: teamSettings.workingDays,
|
|
371
|
+
defaultAreaPath: teamFieldValues?.defaultValue,
|
|
372
|
+
areaPathField: teamFieldValues?.field,
|
|
373
|
+
areaPaths: teamFieldValues?.values,
|
|
374
|
+
};
|
|
375
|
+
return {
|
|
376
|
+
content: [
|
|
377
|
+
{ type: "text", text: `Project: ${resolvedProject}, Team: ${resolvedTeam}` },
|
|
378
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
379
|
+
],
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
384
|
+
return {
|
|
385
|
+
content: [{ type: "text", text: `Error fetching team settings: ${errorMessage}` }],
|
|
386
|
+
isError: true,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
});
|
|
297
390
|
}
|
|
298
391
|
export { WORK_TOOLS, configureWorkTools };
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "2.
|
|
1
|
+
export const packageVersion = "2.5.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azure-devops/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"mcpName": "microsoft.com/azure-devops",
|
|
5
5
|
"description": "MCP server for interacting with Azure DevOps",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"build": "tsc && shx chmod +x dist/*.js",
|
|
29
29
|
"prepare": "npm run build && husky",
|
|
30
30
|
"watch": "tsc --watch",
|
|
31
|
-
"inspect": "ALLOWED_ORIGINS=http://127.0.0.1:6274 npx @modelcontextprotocol/inspector node dist/index.js",
|
|
31
|
+
"inspect": "ALLOWED_ORIGINS=http://127.0.0.1:6274 npx @modelcontextprotocol/inspector@0.21.0 node dist/index.js",
|
|
32
32
|
"start": "node -r tsconfig-paths/register dist/index.js",
|
|
33
33
|
"eslint": "eslint",
|
|
34
34
|
"eslint-fix": "eslint --fix",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@azure/identity": "^4.10.0",
|
|
42
|
-
"@azure/msal-node": "^
|
|
43
|
-
"@modelcontextprotocol/sdk": "1.
|
|
42
|
+
"@azure/msal-node": "^5.0.6",
|
|
43
|
+
"@modelcontextprotocol/sdk": "1.27.1",
|
|
44
44
|
"azure-devops-extension-api": "^4.264.0",
|
|
45
45
|
"azure-devops-extension-sdk": "^4.0.2",
|
|
46
46
|
"azure-devops-node-api": "^15.1.2",
|
|
@@ -50,7 +50,6 @@
|
|
|
50
50
|
"zod-to-json-schema": "^3.24.5"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@modelcontextprotocol/inspector": "^0.17.0",
|
|
54
53
|
"@types/jest": "^30.0.0",
|
|
55
54
|
"@types/node": "^22.19.1",
|
|
56
55
|
"eslint-config-prettier": "10.1.8",
|
|
@@ -60,12 +59,12 @@
|
|
|
60
59
|
"jest": "^30.0.2",
|
|
61
60
|
"jest-extended": "^7.0.0",
|
|
62
61
|
"lint-staged": "^16.2.7",
|
|
63
|
-
"prettier": "3.
|
|
62
|
+
"prettier": "3.8.1",
|
|
64
63
|
"shx": "^0.4.0",
|
|
65
64
|
"ts-jest": "^29.4.6",
|
|
66
65
|
"tsconfig-paths": "^4.2.0",
|
|
67
66
|
"typescript": "^5.9.3",
|
|
68
|
-
"typescript-eslint": "^8.
|
|
67
|
+
"typescript-eslint": "^8.54.0"
|
|
69
68
|
},
|
|
70
69
|
"lint-staged": {
|
|
71
70
|
"**/*.(js|ts|jsx|tsx|json|css|md)": [
|
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 };
|
package/dist/tools/releases.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Microsoft Corporation.
|
|
2
|
-
// Licensed under the MIT License.
|
|
3
|
-
import { ReleaseDefinitionExpands, ReleaseDefinitionQueryOrder, ReleaseExpands, ReleaseStatus, ReleaseQueryOrder } from "azure-devops-node-api/interfaces/ReleaseInterfaces.js";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { getEnumKeys, safeEnumConvert } from "../utils.js";
|
|
6
|
-
const RELEASE_TOOLS = {
|
|
7
|
-
get_release_definitions: "release_get_definitions",
|
|
8
|
-
get_releases: "release_get_releases",
|
|
9
|
-
};
|
|
10
|
-
function configureReleaseTools(server, tokenProvider, connectionProvider) {
|
|
11
|
-
server.tool(RELEASE_TOOLS.get_release_definitions, "Retrieves list of release definitions for a given project.", {
|
|
12
|
-
project: z.string().describe("Project ID or name to get release definitions for"),
|
|
13
|
-
searchText: z.string().optional().describe("Search text to filter release definitions"),
|
|
14
|
-
expand: z
|
|
15
|
-
.enum(getEnumKeys(ReleaseDefinitionExpands))
|
|
16
|
-
.default("None")
|
|
17
|
-
.describe("Expand options for release definitions"),
|
|
18
|
-
artifactType: z.string().optional().describe("Filter by artifact type"),
|
|
19
|
-
artifactSourceId: z.string().optional().describe("Filter by artifact source ID"),
|
|
20
|
-
top: z.number().optional().describe("Number of results to return (for pagination)"),
|
|
21
|
-
continuationToken: z.string().optional().describe("Continuation token for pagination"),
|
|
22
|
-
queryOrder: z
|
|
23
|
-
.enum(getEnumKeys(ReleaseDefinitionQueryOrder))
|
|
24
|
-
.default("NameAscending")
|
|
25
|
-
.describe("Order of the results"),
|
|
26
|
-
path: z.string().optional().describe("Path to filter release definitions"),
|
|
27
|
-
isExactNameMatch: z.boolean().optional().default(false).describe("Whether to match the exact name of the release definition. Default is false."),
|
|
28
|
-
tagFilter: z.array(z.string()).optional().describe("Filter by tags associated with the release definitions"),
|
|
29
|
-
propertyFilters: z.array(z.string()).optional().describe("Filter by properties associated with the release definitions"),
|
|
30
|
-
definitionIdFilter: z.array(z.string()).optional().describe("Filter by specific release definition IDs"),
|
|
31
|
-
isDeleted: z.boolean().default(false).describe("Whether to include deleted release definitions. Default is false."),
|
|
32
|
-
searchTextContainsFolderName: z.boolean().optional().describe("Whether to include folder names in the search text"),
|
|
33
|
-
}, async ({ project, searchText, expand, artifactType, artifactSourceId, top, continuationToken, queryOrder, path, isExactNameMatch, tagFilter, propertyFilters, definitionIdFilter, isDeleted, searchTextContainsFolderName, }) => {
|
|
34
|
-
const connection = await connectionProvider();
|
|
35
|
-
const releaseApi = await connection.getReleaseApi();
|
|
36
|
-
const releaseDefinitions = await releaseApi.getReleaseDefinitions(project, searchText, safeEnumConvert(ReleaseDefinitionExpands, expand), artifactType, artifactSourceId, top, continuationToken, safeEnumConvert(ReleaseDefinitionQueryOrder, queryOrder), path, isExactNameMatch, tagFilter, propertyFilters, definitionIdFilter, isDeleted, searchTextContainsFolderName);
|
|
37
|
-
return {
|
|
38
|
-
content: [{ type: "text", text: JSON.stringify(releaseDefinitions, null, 2) }],
|
|
39
|
-
};
|
|
40
|
-
});
|
|
41
|
-
server.tool(RELEASE_TOOLS.get_releases, "Retrieves a list of releases for a given project.", {
|
|
42
|
-
project: z.string().optional().describe("Project ID or name to get releases for"),
|
|
43
|
-
definitionId: z.number().optional().describe("ID of the release definition to filter releases"),
|
|
44
|
-
definitionEnvironmentId: z.number().optional().describe("ID of the definition environment to filter releases"),
|
|
45
|
-
searchText: z.string().optional().describe("Search text to filter releases"),
|
|
46
|
-
createdBy: z.string().optional().describe("User ID or name who created the release"),
|
|
47
|
-
statusFilter: z
|
|
48
|
-
.enum(getEnumKeys(ReleaseStatus))
|
|
49
|
-
.optional()
|
|
50
|
-
.default("Active")
|
|
51
|
-
.describe("Status of the releases to filter (default: Active)"),
|
|
52
|
-
environmentStatusFilter: z.number().optional().describe("Environment status to filter releases"),
|
|
53
|
-
minCreatedTime: z.coerce
|
|
54
|
-
.date()
|
|
55
|
-
.optional()
|
|
56
|
-
.default(() => {
|
|
57
|
-
const sevenDaysAgo = new Date();
|
|
58
|
-
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
59
|
-
return sevenDaysAgo;
|
|
60
|
-
})
|
|
61
|
-
.describe("Minimum created time for releases (default: 7 days ago)"),
|
|
62
|
-
maxCreatedTime: z.coerce
|
|
63
|
-
.date()
|
|
64
|
-
.optional()
|
|
65
|
-
.default(() => new Date())
|
|
66
|
-
.describe("Maximum created time for releases (default: now)"),
|
|
67
|
-
queryOrder: z
|
|
68
|
-
.enum(getEnumKeys(ReleaseQueryOrder))
|
|
69
|
-
.optional()
|
|
70
|
-
.default("Ascending")
|
|
71
|
-
.describe("Order in which to return releases (default: Ascending)"),
|
|
72
|
-
top: z.number().optional().describe("Number of releases to return"),
|
|
73
|
-
continuationToken: z.number().optional().describe("Continuation token for pagination"),
|
|
74
|
-
expand: z
|
|
75
|
-
.enum(getEnumKeys(ReleaseExpands))
|
|
76
|
-
.optional()
|
|
77
|
-
.default("None")
|
|
78
|
-
.describe("Expand options for releases"),
|
|
79
|
-
artifactTypeId: z.string().optional().describe("Filter releases by artifact type ID"),
|
|
80
|
-
sourceId: z.string().optional().describe("Filter releases by artifact source ID"),
|
|
81
|
-
artifactVersionId: z.string().optional().describe("Filter releases by artifact version ID"),
|
|
82
|
-
sourceBranchFilter: z.string().optional().describe("Filter releases by source branch"),
|
|
83
|
-
isDeleted: z.boolean().optional().default(false).describe("Whether to include deleted releases (default: false)"),
|
|
84
|
-
tagFilter: z.array(z.string()).optional().describe("Filter releases by tags"),
|
|
85
|
-
propertyFilters: z.array(z.string()).optional().describe("Filter releases by properties"),
|
|
86
|
-
releaseIdFilter: z.array(z.number()).optional().describe("Filter by specific release IDs"),
|
|
87
|
-
path: z.string().optional().describe("Path to filter releases"),
|
|
88
|
-
}, async ({ project, definitionId, definitionEnvironmentId, searchText, createdBy, statusFilter, environmentStatusFilter, minCreatedTime, maxCreatedTime, queryOrder, top, continuationToken, expand, artifactTypeId, sourceId, artifactVersionId, sourceBranchFilter, isDeleted, tagFilter, propertyFilters, releaseIdFilter, path, }) => {
|
|
89
|
-
const connection = await connectionProvider();
|
|
90
|
-
const releaseApi = await connection.getReleaseApi();
|
|
91
|
-
const releases = await releaseApi.getReleases(project, definitionId, definitionEnvironmentId, searchText, createdBy, safeEnumConvert(ReleaseStatus, statusFilter), environmentStatusFilter, minCreatedTime, maxCreatedTime, safeEnumConvert(ReleaseQueryOrder, queryOrder), top, continuationToken, safeEnumConvert(ReleaseExpands, expand), artifactTypeId, sourceId, artifactVersionId, sourceBranchFilter, isDeleted, tagFilter, propertyFilters, releaseIdFilter, path);
|
|
92
|
-
return {
|
|
93
|
-
content: [{ type: "text", text: JSON.stringify(releases, null, 2) }],
|
|
94
|
-
};
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
export { RELEASE_TOOLS, configureReleaseTools };
|