@azure-devops/mcp 2.2.1 → 2.2.2
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 +4 -0
- package/dist/auth.js +12 -1
- package/dist/index.js +10 -5
- package/dist/org-tenants.js +9 -7
- package/dist/tools/repositories.js +89 -7
- package/dist/tools/work-items.js +1 -1
- package/dist/tools/work.js +166 -1
- package/dist/version.js +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -53,8 +53,12 @@ Interact with these Azure DevOps services:
|
|
|
53
53
|
### ⚒️ Work
|
|
54
54
|
|
|
55
55
|
- **work_list_team_iterations**: Retrieve a list of iterations for a specific team in a project.
|
|
56
|
+
- **work_list_iterations**: List all iterations in a specified Azure DevOps project.
|
|
56
57
|
- **work_create_iterations**: Create new iterations in a specified Azure DevOps project.
|
|
57
58
|
- **work_assign_iterations**: Assign existing iterations to a specific team in a project.
|
|
59
|
+
- **work_get_team_capacity**: Get the team capacity of a specific team and iteration in a project.
|
|
60
|
+
- **work_update_team_capacity**: Update the team capacity of a team member for a specific iteration in a project.
|
|
61
|
+
- **work_get_iteration_capacities**: Get an iteration's capacity for all teams in iteration and project.
|
|
58
62
|
|
|
59
63
|
### 📅 Work Items
|
|
60
64
|
|
package/dist/auth.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
1
3
|
import { AzureCliCredential, ChainedTokenCredential, DefaultAzureCredential } from "@azure/identity";
|
|
2
4
|
import { PublicClientApplication } from "@azure/msal-node";
|
|
3
5
|
import open from "open";
|
|
@@ -30,7 +32,7 @@ class OAuthAuthenticator {
|
|
|
30
32
|
account: this.accountId,
|
|
31
33
|
});
|
|
32
34
|
}
|
|
33
|
-
catch
|
|
35
|
+
catch {
|
|
34
36
|
authResult = null;
|
|
35
37
|
}
|
|
36
38
|
}
|
|
@@ -51,6 +53,15 @@ class OAuthAuthenticator {
|
|
|
51
53
|
}
|
|
52
54
|
function createAuthenticator(type, tenantId) {
|
|
53
55
|
switch (type) {
|
|
56
|
+
case "envvar":
|
|
57
|
+
// Read token from fixed environment variable
|
|
58
|
+
return async () => {
|
|
59
|
+
const token = process.env["ADO_MCP_AUTH_TOKEN"];
|
|
60
|
+
if (!token) {
|
|
61
|
+
throw new Error("Environment variable 'ADO_MCP_AUTH_TOKEN' is not set or empty. Please set it with a valid Azure DevOps Personal Access Token.");
|
|
62
|
+
}
|
|
63
|
+
return token;
|
|
64
|
+
};
|
|
54
65
|
case "azcli":
|
|
55
66
|
case "env":
|
|
56
67
|
if (type !== "env") {
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Licensed under the MIT License.
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
-
import
|
|
6
|
+
import { getBearerHandler, WebApi } from "azure-devops-node-api";
|
|
7
7
|
import yargs from "yargs";
|
|
8
8
|
import { hideBin } from "yargs/helpers";
|
|
9
9
|
import { createAuthenticator } from "./auth.js";
|
|
@@ -38,9 +38,9 @@ const argv = yargs(hideBin(process.argv))
|
|
|
38
38
|
})
|
|
39
39
|
.option("authentication", {
|
|
40
40
|
alias: "a",
|
|
41
|
-
describe: "Type of authentication to use
|
|
41
|
+
describe: "Type of authentication to use",
|
|
42
42
|
type: "string",
|
|
43
|
-
choices: ["interactive", "azcli", "env"],
|
|
43
|
+
choices: ["interactive", "azcli", "env", "envvar"],
|
|
44
44
|
default: defaultAuthenticationType,
|
|
45
45
|
})
|
|
46
46
|
.option("tenant", {
|
|
@@ -57,8 +57,8 @@ export const enabledDomains = domainsManager.getEnabledDomains();
|
|
|
57
57
|
function getAzureDevOpsClient(getAzureDevOpsToken, userAgentComposer) {
|
|
58
58
|
return async () => {
|
|
59
59
|
const accessToken = await getAzureDevOpsToken();
|
|
60
|
-
const authHandler =
|
|
61
|
-
const connection = new
|
|
60
|
+
const authHandler = getBearerHandler(accessToken);
|
|
61
|
+
const connection = new WebApi(orgUrl, authHandler, undefined, {
|
|
62
62
|
productName: "AzureDevOps.MCP",
|
|
63
63
|
productVersion: packageVersion,
|
|
64
64
|
userAgent: userAgentComposer.userAgent,
|
|
@@ -70,6 +70,11 @@ async function main() {
|
|
|
70
70
|
const server = new McpServer({
|
|
71
71
|
name: "Azure DevOps MCP Server",
|
|
72
72
|
version: packageVersion,
|
|
73
|
+
icons: [
|
|
74
|
+
{
|
|
75
|
+
src: "https://cdn.vsassets.io/content/icons/favicon.ico",
|
|
76
|
+
},
|
|
77
|
+
],
|
|
73
78
|
});
|
|
74
79
|
const userAgentComposer = new UserAgentComposer(packageVersion);
|
|
75
80
|
server.server.oninitialized = () => {
|
package/dist/org-tenants.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
import { readFile, writeFile } from "fs/promises";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
const CACHE_FILE = join(homedir(), ".ado_orgs.cache");
|
|
5
7
|
const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 1 week in milliseconds
|
|
6
8
|
async function loadCache() {
|
|
7
9
|
try {
|
|
8
|
-
const cacheData = await
|
|
10
|
+
const cacheData = await readFile(CACHE_FILE, "utf-8");
|
|
9
11
|
return JSON.parse(cacheData);
|
|
10
12
|
}
|
|
11
|
-
catch
|
|
13
|
+
catch {
|
|
12
14
|
// Cache file doesn't exist or is invalid, return empty cache
|
|
13
15
|
return {};
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
async function trySavingCache(cache) {
|
|
17
19
|
try {
|
|
18
|
-
await
|
|
20
|
+
await writeFile(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
|
|
19
21
|
}
|
|
20
22
|
catch (error) {
|
|
21
23
|
console.error("Failed to save org tenants cache:", error);
|
|
@@ -39,6 +39,7 @@ function trimPullRequestThread(thread) {
|
|
|
39
39
|
lastUpdatedDate: thread.lastUpdatedDate,
|
|
40
40
|
status: thread.status,
|
|
41
41
|
comments: trimComments(thread.comments),
|
|
42
|
+
threadContext: thread.threadContext,
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
/**
|
|
@@ -106,7 +107,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
106
107
|
sourceRefName: z.string().describe("The source branch name for the pull request, e.g., 'refs/heads/feature-branch'."),
|
|
107
108
|
targetRefName: z.string().describe("The target branch name for the pull request, e.g., 'refs/heads/main'."),
|
|
108
109
|
title: z.string().describe("The title of the pull request."),
|
|
109
|
-
description: z.string().optional().describe("The description of the pull request. Optional."),
|
|
110
|
+
description: z.string().max(4000).optional().describe("The description of the pull request. Must not be longer than 4000 characters. Optional."),
|
|
110
111
|
isDraft: z.boolean().optional().default(false).describe("Indicates whether the pull request is a draft. Defaults to false."),
|
|
111
112
|
workItems: z.string().optional().describe("Work item IDs to associate with the pull request, space-separated."),
|
|
112
113
|
forkSourceRepositoryId: z.string().optional().describe("The ID of the fork repository that the pull request originates from. Optional, used when creating a pull request from a fork."),
|
|
@@ -224,7 +225,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
224
225
|
repositoryId: z.string().describe("The ID of the repository where the pull request exists."),
|
|
225
226
|
pullRequestId: z.number().describe("The ID of the pull request to update."),
|
|
226
227
|
title: z.string().optional().describe("The new title for the pull request."),
|
|
227
|
-
description: z.string().optional().describe("The new description for the pull request."),
|
|
228
|
+
description: z.string().max(4000).optional().describe("The new description for the pull request. Must not be longer than 4000 characters."),
|
|
228
229
|
isDraft: z.boolean().optional().describe("Whether the pull request should be a draft."),
|
|
229
230
|
targetRefName: z.string().optional().describe("The new target branch name (e.g., 'refs/heads/main')."),
|
|
230
231
|
status: z.enum(["Active", "Abandoned"]).optional().describe("The new status of the pull request. Can be 'Active' or 'Abandoned'."),
|
|
@@ -704,9 +705,10 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
704
705
|
};
|
|
705
706
|
});
|
|
706
707
|
const gitVersionTypeStrings = Object.values(GitVersionType).filter((value) => typeof value === "string");
|
|
707
|
-
server.tool(REPO_TOOLS.search_commits, "
|
|
708
|
+
server.tool(REPO_TOOLS.search_commits, "Search for commits in a repository with comprehensive filtering capabilities. Supports searching by description/comment text, time range, author, committer, specific commit IDs, and more. This is the unified tool for all commit search operations.", {
|
|
708
709
|
project: z.string().describe("Project name or ID"),
|
|
709
710
|
repository: z.string().describe("Repository name or ID"),
|
|
711
|
+
// Existing parameters
|
|
710
712
|
fromCommit: z.string().optional().describe("Starting commit ID"),
|
|
711
713
|
toCommit: z.string().optional().describe("Ending commit ID"),
|
|
712
714
|
version: z.string().optional().describe("The name of the branch, tag or commit to filter commits by"),
|
|
@@ -719,16 +721,79 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
719
721
|
top: z.number().optional().default(10).describe("Maximum number of commits to return"),
|
|
720
722
|
includeLinks: z.boolean().optional().default(false).describe("Include commit links"),
|
|
721
723
|
includeWorkItems: z.boolean().optional().default(false).describe("Include associated work items"),
|
|
722
|
-
|
|
724
|
+
// Enhanced search parameters
|
|
725
|
+
searchText: z.string().optional().describe("Search text to filter commits by description/comment. Supports partial matching."),
|
|
726
|
+
author: z.string().optional().describe("Filter commits by author email or display name"),
|
|
727
|
+
authorEmail: z.string().optional().describe("Filter commits by exact author email address"),
|
|
728
|
+
committer: z.string().optional().describe("Filter commits by committer email or display name"),
|
|
729
|
+
committerEmail: z.string().optional().describe("Filter commits by exact committer email address"),
|
|
730
|
+
fromDate: z.string().optional().describe("Filter commits from this date (ISO 8601 format, e.g., '2024-01-01T00:00:00Z')"),
|
|
731
|
+
toDate: z.string().optional().describe("Filter commits to this date (ISO 8601 format, e.g., '2024-12-31T23:59:59Z')"),
|
|
732
|
+
commitIds: z.array(z.string()).optional().describe("Array of specific commit IDs to retrieve. When provided, other filters are ignored except top/skip."),
|
|
733
|
+
historySimplificationMode: z.enum(["FirstParent", "SimplifyMerges", "FullHistory", "FullHistorySimplifyMerges"]).optional().describe("How to simplify the commit history"),
|
|
734
|
+
}, async ({ project, repository, fromCommit, toCommit, version, versionType, skip, top, includeLinks, includeWorkItems, searchText, author, authorEmail, committer, committerEmail, fromDate, toDate, commitIds, historySimplificationMode, }) => {
|
|
723
735
|
try {
|
|
724
736
|
const connection = await connectionProvider();
|
|
725
737
|
const gitApi = await connection.getGitApi();
|
|
738
|
+
// If specific commit IDs are provided, use getCommits with commit ID filtering
|
|
739
|
+
if (commitIds && commitIds.length > 0) {
|
|
740
|
+
const commits = [];
|
|
741
|
+
const batchSize = Math.min(top || 10, commitIds.length);
|
|
742
|
+
const startIndex = skip || 0;
|
|
743
|
+
const endIndex = Math.min(startIndex + batchSize, commitIds.length);
|
|
744
|
+
// Process commits in the requested range
|
|
745
|
+
const requestedCommitIds = commitIds.slice(startIndex, endIndex);
|
|
746
|
+
// Use getCommits for each commit ID to maintain consistency
|
|
747
|
+
for (const commitId of requestedCommitIds) {
|
|
748
|
+
try {
|
|
749
|
+
const searchCriteria = {
|
|
750
|
+
includeLinks: includeLinks,
|
|
751
|
+
includeWorkItems: includeWorkItems,
|
|
752
|
+
fromCommitId: commitId,
|
|
753
|
+
toCommitId: commitId,
|
|
754
|
+
};
|
|
755
|
+
const commitResults = await gitApi.getCommits(repository, searchCriteria, project, 0, 1);
|
|
756
|
+
if (commitResults && commitResults.length > 0) {
|
|
757
|
+
commits.push(commitResults[0]);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
catch (error) {
|
|
761
|
+
// Log error but continue with other commits
|
|
762
|
+
console.warn(`Failed to retrieve commit ${commitId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
763
|
+
// Add error information to result instead of failing completely
|
|
764
|
+
commits.push({
|
|
765
|
+
commitId: commitId,
|
|
766
|
+
error: `Failed to retrieve: ${error instanceof Error ? error.message : String(error)}`,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return {
|
|
771
|
+
content: [{ type: "text", text: JSON.stringify(commits, null, 2) }],
|
|
772
|
+
};
|
|
773
|
+
}
|
|
726
774
|
const searchCriteria = {
|
|
727
775
|
fromCommitId: fromCommit,
|
|
728
776
|
toCommitId: toCommit,
|
|
729
777
|
includeLinks: includeLinks,
|
|
730
778
|
includeWorkItems: includeWorkItems,
|
|
731
779
|
};
|
|
780
|
+
// Add author filter
|
|
781
|
+
if (author) {
|
|
782
|
+
searchCriteria.author = author;
|
|
783
|
+
}
|
|
784
|
+
// Add date range filters (ADO API expects ISO string format)
|
|
785
|
+
if (fromDate) {
|
|
786
|
+
searchCriteria.fromDate = fromDate;
|
|
787
|
+
}
|
|
788
|
+
if (toDate) {
|
|
789
|
+
searchCriteria.toDate = toDate;
|
|
790
|
+
}
|
|
791
|
+
// Add history simplification if specified
|
|
792
|
+
if (historySimplificationMode) {
|
|
793
|
+
// Note: This parameter might not be directly supported by all ADO API versions
|
|
794
|
+
// but we'll include it in the criteria for forward compatibility
|
|
795
|
+
searchCriteria.historySimplificationMode = historySimplificationMode;
|
|
796
|
+
}
|
|
732
797
|
if (version) {
|
|
733
798
|
const itemVersion = {
|
|
734
799
|
version: version,
|
|
@@ -736,10 +801,27 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
736
801
|
};
|
|
737
802
|
searchCriteria.itemVersion = itemVersion;
|
|
738
803
|
}
|
|
739
|
-
const commits = await gitApi.getCommits(repository, searchCriteria, project, skip,
|
|
740
|
-
|
|
804
|
+
const commits = await gitApi.getCommits(repository, searchCriteria, project, skip, top);
|
|
805
|
+
// Additional client-side filtering for enhanced search capabilities
|
|
806
|
+
let filteredCommits = commits;
|
|
807
|
+
// Filter by search text in commit message if not handled by API
|
|
808
|
+
if (searchText && filteredCommits) {
|
|
809
|
+
filteredCommits = filteredCommits.filter((commit) => commit.comment?.toLowerCase().includes(searchText.toLowerCase()));
|
|
810
|
+
}
|
|
811
|
+
// Filter by author email if specified
|
|
812
|
+
if (authorEmail && filteredCommits) {
|
|
813
|
+
filteredCommits = filteredCommits.filter((commit) => commit.author?.email?.toLowerCase() === authorEmail.toLowerCase());
|
|
814
|
+
}
|
|
815
|
+
// Filter by committer if specified
|
|
816
|
+
if (committer && filteredCommits) {
|
|
817
|
+
filteredCommits = filteredCommits.filter((commit) => commit.committer?.name?.toLowerCase().includes(committer.toLowerCase()) || commit.committer?.email?.toLowerCase().includes(committer.toLowerCase()));
|
|
818
|
+
}
|
|
819
|
+
// Filter by committer email if specified
|
|
820
|
+
if (committerEmail && filteredCommits) {
|
|
821
|
+
filteredCommits = filteredCommits.filter((commit) => commit.committer?.email?.toLowerCase() === committerEmail.toLowerCase());
|
|
822
|
+
}
|
|
741
823
|
return {
|
|
742
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
824
|
+
content: [{ type: "text", text: JSON.stringify(filteredCommits, null, 2) }],
|
|
743
825
|
};
|
|
744
826
|
}
|
|
745
827
|
catch (error) {
|
package/dist/tools/work-items.js
CHANGED
|
@@ -56,7 +56,7 @@ function getLinkTypeFromName(name) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
function configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
59
|
-
server.tool(WORKITEM_TOOLS.list_backlogs, "
|
|
59
|
+
server.tool(WORKITEM_TOOLS.list_backlogs, "Receive a list of backlogs for a given project and team.", {
|
|
60
60
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
61
61
|
team: z.string().describe("The name or ID of the Azure DevOps team."),
|
|
62
62
|
}, async ({ project, team }) => {
|
package/dist/tools/work.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { TreeStructureGroup } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
4
|
+
import { TreeStructureGroup, TreeNodeStructureType } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
|
|
5
5
|
const WORK_TOOLS = {
|
|
6
6
|
list_team_iterations: "work_list_team_iterations",
|
|
7
|
+
list_iterations: "work_list_iterations",
|
|
7
8
|
create_iterations: "work_create_iterations",
|
|
8
9
|
assign_iterations: "work_assign_iterations",
|
|
10
|
+
get_team_capacity: "work_get_team_capacity",
|
|
11
|
+
update_team_capacity: "work_update_team_capacity",
|
|
12
|
+
get_iteration_capacities: "work_get_iteration_capacities",
|
|
9
13
|
};
|
|
10
14
|
function configureWorkTools(server, _, connectionProvider) {
|
|
11
15
|
server.tool(WORK_TOOLS.list_team_iterations, "Retrieve a list of iterations for a specific team in a project.", {
|
|
@@ -74,6 +78,38 @@ function configureWorkTools(server, _, connectionProvider) {
|
|
|
74
78
|
};
|
|
75
79
|
}
|
|
76
80
|
});
|
|
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."),
|
|
83
|
+
depth: z.number().default(2).describe("Depth of children to fetch."),
|
|
84
|
+
}, async ({ project, depth }) => {
|
|
85
|
+
try {
|
|
86
|
+
const connection = await connectionProvider();
|
|
87
|
+
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
|
|
88
|
+
if (depth === undefined) {
|
|
89
|
+
depth = 1;
|
|
90
|
+
}
|
|
91
|
+
const results = await workItemTrackingApi.getClassificationNodes(project, [], depth);
|
|
92
|
+
// Handle null or undefined results
|
|
93
|
+
if (!results) {
|
|
94
|
+
return { content: [{ type: "text", text: "No iterations were found" }], isError: true };
|
|
95
|
+
}
|
|
96
|
+
// Filter out items with structureType=0 (Area nodes), only keep structureType=1 (Iteration nodes)
|
|
97
|
+
const filteredResults = results.filter((node) => node.structureType === TreeNodeStructureType.Iteration);
|
|
98
|
+
if (filteredResults.length === 0) {
|
|
99
|
+
return { content: [{ type: "text", text: "No iterations were found" }], isError: true };
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: "text", text: JSON.stringify(filteredResults, null, 2) }],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text", text: `Error fetching iterations: ${errorMessage}` }],
|
|
109
|
+
isError: true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
});
|
|
77
113
|
server.tool(WORK_TOOLS.assign_iterations, "Assign existing iterations to a specific team in a project.", {
|
|
78
114
|
project: z.string().describe("The name or ID of the Azure DevOps project."),
|
|
79
115
|
team: z.string().describe("The name or ID of the Azure DevOps team."),
|
|
@@ -110,5 +146,134 @@ function configureWorkTools(server, _, connectionProvider) {
|
|
|
110
146
|
};
|
|
111
147
|
}
|
|
112
148
|
});
|
|
149
|
+
server.tool(WORK_TOOLS.get_team_capacity, "Get the team capacity of a specific team and iteration in a project.", {
|
|
150
|
+
project: z.string().describe("The name or Id of the Azure DevOps project."),
|
|
151
|
+
team: z.string().describe("The name or Id of the Azure DevOps team."),
|
|
152
|
+
iterationId: z.string().describe("The Iteration Id to get capacity for."),
|
|
153
|
+
}, async ({ project, team, iterationId }) => {
|
|
154
|
+
try {
|
|
155
|
+
const connection = await connectionProvider();
|
|
156
|
+
const workApi = await connection.getWorkApi();
|
|
157
|
+
const teamContext = { project, team };
|
|
158
|
+
const rawResults = await workApi.getCapacitiesWithIdentityRefAndTotals(teamContext, iterationId);
|
|
159
|
+
if (!rawResults || rawResults.teamMembers?.length === 0) {
|
|
160
|
+
return { content: [{ type: "text", text: "No team capacity assigned to the team" }], isError: true };
|
|
161
|
+
}
|
|
162
|
+
// Remove unwanted fields from teamMember and url
|
|
163
|
+
const simplifiedResults = {
|
|
164
|
+
...rawResults,
|
|
165
|
+
teamMembers: (rawResults.teamMembers || []).map((member) => {
|
|
166
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
167
|
+
const { url, ...rest } = member;
|
|
168
|
+
return {
|
|
169
|
+
...rest,
|
|
170
|
+
teamMember: member.teamMember
|
|
171
|
+
? {
|
|
172
|
+
displayName: member.teamMember.displayName,
|
|
173
|
+
id: member.teamMember.id,
|
|
174
|
+
uniqueName: member.teamMember.uniqueName,
|
|
175
|
+
}
|
|
176
|
+
: undefined,
|
|
177
|
+
};
|
|
178
|
+
}),
|
|
179
|
+
};
|
|
180
|
+
return {
|
|
181
|
+
content: [{ type: "text", text: JSON.stringify(simplifiedResults, null, 2) }],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
186
|
+
return {
|
|
187
|
+
content: [{ type: "text", text: `Error getting team capacity: ${errorMessage}` }],
|
|
188
|
+
isError: true,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
server.tool(WORK_TOOLS.update_team_capacity, "Update the team capacity of a team member for a specific iteration in a project.", {
|
|
193
|
+
project: z.string().describe("The name or Id of the Azure DevOps project."),
|
|
194
|
+
team: z.string().describe("The name or Id of the Azure DevOps team."),
|
|
195
|
+
teamMemberId: z.string().describe("The team member Id for the specific team member."),
|
|
196
|
+
iterationId: z.string().describe("The Iteration Id to update the capacity for."),
|
|
197
|
+
activities: z
|
|
198
|
+
.array(z.object({
|
|
199
|
+
name: z.string().describe("The name of the activity (e.g., 'Development')."),
|
|
200
|
+
capacityPerDay: z.number().describe("The capacity per day for this activity."),
|
|
201
|
+
}))
|
|
202
|
+
.describe("Array of activities and their daily capacities for the team member."),
|
|
203
|
+
daysOff: z
|
|
204
|
+
.array(z.object({
|
|
205
|
+
start: z.string().describe("Start date of the day off in ISO format."),
|
|
206
|
+
end: z.string().describe("End date of the day off in ISO format."),
|
|
207
|
+
}))
|
|
208
|
+
.optional()
|
|
209
|
+
.describe("Array of days off for the team member, each with a start and end date in ISO format."),
|
|
210
|
+
}, async ({ project, team, teamMemberId, iterationId, activities, daysOff }) => {
|
|
211
|
+
try {
|
|
212
|
+
const connection = await connectionProvider();
|
|
213
|
+
const workApi = await connection.getWorkApi();
|
|
214
|
+
const teamContext = { project, team };
|
|
215
|
+
// Prepare the capacity update object
|
|
216
|
+
const capacityPatch = {
|
|
217
|
+
activities: activities.map((a) => ({
|
|
218
|
+
name: a.name,
|
|
219
|
+
capacityPerDay: a.capacityPerDay,
|
|
220
|
+
})),
|
|
221
|
+
daysOff: (daysOff || []).map((d) => ({
|
|
222
|
+
start: new Date(d.start),
|
|
223
|
+
end: new Date(d.end),
|
|
224
|
+
})),
|
|
225
|
+
};
|
|
226
|
+
// Update the team member's capacity
|
|
227
|
+
const updatedCapacity = await workApi.updateCapacityWithIdentityRef(capacityPatch, teamContext, iterationId, teamMemberId);
|
|
228
|
+
if (!updatedCapacity) {
|
|
229
|
+
return { content: [{ type: "text", text: "Failed to update team member capacity" }], isError: true };
|
|
230
|
+
}
|
|
231
|
+
// Simplify output
|
|
232
|
+
const simplifiedResult = {
|
|
233
|
+
teamMember: updatedCapacity.teamMember
|
|
234
|
+
? {
|
|
235
|
+
displayName: updatedCapacity.teamMember.displayName,
|
|
236
|
+
id: updatedCapacity.teamMember.id,
|
|
237
|
+
uniqueName: updatedCapacity.teamMember.uniqueName,
|
|
238
|
+
}
|
|
239
|
+
: undefined,
|
|
240
|
+
activities: updatedCapacity.activities,
|
|
241
|
+
daysOff: updatedCapacity.daysOff,
|
|
242
|
+
};
|
|
243
|
+
return {
|
|
244
|
+
content: [{ type: "text", text: JSON.stringify(simplifiedResult, null, 2) }],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
249
|
+
return {
|
|
250
|
+
content: [{ type: "text", text: `Error updating team capacity: ${errorMessage}` }],
|
|
251
|
+
isError: true,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
server.tool(WORK_TOOLS.get_iteration_capacities, "Get an iteration's capacity for all teams in iteration and project.", {
|
|
256
|
+
project: z.string().describe("The name or Id of the Azure DevOps project."),
|
|
257
|
+
iterationId: z.string().describe("The Iteration Id to get capacity for."),
|
|
258
|
+
}, async ({ project, iterationId }) => {
|
|
259
|
+
try {
|
|
260
|
+
const connection = await connectionProvider();
|
|
261
|
+
const workApi = await connection.getWorkApi();
|
|
262
|
+
const rawResults = await workApi.getTotalIterationCapacities(project, iterationId);
|
|
263
|
+
if (!rawResults || !rawResults.teams || rawResults.teams.length === 0) {
|
|
264
|
+
return { content: [{ type: "text", text: "No iteration capacity assigned to the teams" }], isError: true };
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
content: [{ type: "text", text: JSON.stringify(rawResults, null, 2) }],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
272
|
+
return {
|
|
273
|
+
content: [{ type: "text", text: `Error getting iteration capacities: ${errorMessage}` }],
|
|
274
|
+
isError: true,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
});
|
|
113
278
|
}
|
|
114
279
|
export { WORK_TOOLS, configureWorkTools };
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "2.2.
|
|
1
|
+
export const packageVersion = "2.2.2";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azure-devops/mcp",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "MCP server for interacting with Azure DevOps",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Microsoft Corporation",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@azure/identity": "^4.10.0",
|
|
41
|
-
"@
|
|
41
|
+
"@azure/msal-node": "^3.6.0",
|
|
42
|
+
"@modelcontextprotocol/sdk": "1.21.0",
|
|
42
43
|
"azure-devops-extension-api": "^4.252.0",
|
|
43
44
|
"azure-devops-extension-sdk": "^4.0.2",
|
|
44
45
|
"azure-devops-node-api": "^15.1.0",
|
|
@@ -54,7 +55,7 @@
|
|
|
54
55
|
"eslint-plugin-header": "^3.1.1",
|
|
55
56
|
"glob": "^11.0.3",
|
|
56
57
|
"jest": "^30.0.2",
|
|
57
|
-
"jest-extended": "^
|
|
58
|
+
"jest-extended": "^7.0.0",
|
|
58
59
|
"prettier": "3.6.2",
|
|
59
60
|
"shx": "^0.4.0",
|
|
60
61
|
"ts-jest": "^29.4.0",
|