@azure-devops/mcp 1.3.1 → 2.0.0-nightly.20250825

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) Microsoft Corporation.
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE
1
+ MIT License
2
+
3
+ Copyright (c) Microsoft Corporation.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE
package/README.md CHANGED
@@ -16,7 +16,7 @@ This TypeScript project provides a **local** MCP server for Azure DevOps, enabli
16
16
  3. [⚙️ Supported Tools](#️-supported-tools)
17
17
  4. [🔌 Installation & Getting Started](#-installation--getting-started)
18
18
  5. [📝 Troubleshooting](#-troubleshooting)
19
- 6. [🎩 Examples & Best Practices](#-samples--best-practices)
19
+ 6. [🎩 Examples & Best Practices](#-examples--best-practices)
20
20
  7. [🙋‍♀️ Frequently Asked Questions](#️-frequently-asked-questions)
21
21
  8. [📌 Contributing](#-contributing)
22
22
 
@@ -33,6 +33,10 @@ The Azure DevOps MCP Server brings Azure DevOps context to your agents. Try prom
33
33
  - "List iterations for project 'Contoso'"
34
34
  - "List my work items for project 'Contoso'"
35
35
  - "List work items in current iteration for 'Contoso' project and 'Contoso Team'"
36
+ - "List all wikis in the 'Contoso' project"
37
+ - "Create a wiki page '/Architecture/Overview' with content about system design"
38
+ - "Update the wiki page '/Getting Started' with new onboarding instructions"
39
+ - "Get the content of the wiki page '/API/Authentication' from the Documentation wiki"
36
40
 
37
41
  ## 🏆 Expectations
38
42
 
@@ -74,11 +78,7 @@ Interact with these Azure DevOps services:
74
78
  - **wit_update_work_items_batch**: Update work items in batch.
75
79
  - **wit_work_items_link**: Link work items together in batch.
76
80
  - **wit_work_item_unlink**: Unlink one or many links from a work item.
77
-
78
- #### Deprecated Tools
79
-
80
- - **wit_add_child_work_item**: Replaced by `wit_add_child_work_items` to allow creating one or more child items per call.
81
- - **wit_close_and_link_workitem_duplicates**: This tool is no longer needed. Finding and marking duplicates can be done with other tools.
81
+ - **wit_add_artifact_link**: Link to artifacts like branch, pull request, commit, and build.
82
82
 
83
83
  ### 📁 Repositories
84
84
 
@@ -133,6 +133,14 @@ Interact with these Azure DevOps services:
133
133
  - **testplan_list_test_cases**: Get a list of test cases in the test plan.
134
134
  - **testplan_show_test_results_from_build_id**: Get a list of test results for a given project and build ID.
135
135
 
136
+ ### 📖 Wiki
137
+
138
+ - **wiki_list_wikis**: Retrieve a list of wikis for an organization or project.
139
+ - **wiki_get_wiki**: Get the wiki by wikiIdentifier.
140
+ - **wiki_list_pages**: Retrieve a list of wiki pages for a specific wiki and project.
141
+ - **wiki_get_page_content**: Retrieve wiki page content by wikiIdentifier and path.
142
+ - **wiki_create_or_update_page**: Create or update wiki pages with full content support.
143
+
136
144
  ### 🔎 Search
137
145
 
138
146
  - **search_code**: Get code search results for a given search text.
@@ -196,19 +204,41 @@ In your project, add a `.vscode\mcp.json` file with the following content:
196
204
  }
197
205
  ```
198
206
 
207
+ 🔥 To stay up to date with the latest features, you can use our nightly builds. Simply update your `mcp.json` configuration to use `@azure-devops/mcp@next`. Here is an updated example:
208
+
209
+ ```json
210
+ {
211
+ "inputs": [
212
+ {
213
+ "id": "ado_org",
214
+ "type": "promptString",
215
+ "description": "Azure DevOps organization name (e.g. 'contoso')"
216
+ }
217
+ ],
218
+ "servers": {
219
+ "ado": {
220
+ "type": "stdio",
221
+ "command": "npx",
222
+ "args": ["-y", "@azure-devops/mcp@next", "${input:ado_org}"]
223
+ }
224
+ }
225
+ }
226
+ ```
227
+
199
228
  Save the file, then click 'Start'.
200
229
 
201
- <img src="./docs/media/start-mcp-server.gif" alt="start mcp server" width="250"/>
230
+ ![start mcp server](./docs/media/start-mcp-server.gif)
202
231
 
203
232
  In chat, switch to [Agent Mode](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode).
204
233
 
205
234
  Click "Select Tools" and choose the available tools.
206
235
 
207
- <img src="./docs/media/configure-mcp-server-tools.gif" alt="configure mcp server tools" width="300"/>
236
+ ![configure mcp server tools](./docs/media/configure-mcp-server-tools.gif)
208
237
 
209
238
  Open GitHub Copilot Chat and try a prompt like `List ADO projects`.
210
239
 
211
- > 💥 We strongly recommend creating a `.github\copilot-instructions.md` in your project and copying the contents from this [copilot-instructions.md](./.github/copilot-instructions.md) file. This will enhance your experience using the Azure DevOps MCP Server with GitHub Copilot Chat.
240
+ > 💥 We strongly recommend creating a `.github\copilot-instructions.md` in your project. This will enhance your experience using the Azure DevOps MCP Server with GitHub Copilot Chat.
241
+ > To start, just include "`This project uses Azure DevOps. Always check to see if the Azure DevOps MCP server has a tool relevant to the user's request`" in your copilot instructions file.
212
242
 
213
243
  See the [getting started documentation](./docs/GETTINGSTARTED.md) to use our MCP Server with other tools such as Visual Studio 2022, Claude Code, and Cursor.
214
244
 
package/dist/index.js CHANGED
@@ -11,16 +11,25 @@ import { configurePrompts } from "./prompts.js";
11
11
  import { configureAllTools } from "./tools.js";
12
12
  import { UserAgentComposer } from "./useragent.js";
13
13
  import { packageVersion } from "./version.js";
14
+ import { DomainsManager } from "./shared/domains.js";
14
15
  // Parse command line arguments using yargs
15
16
  const argv = yargs(hideBin(process.argv))
16
17
  .scriptName("mcp-server-azuredevops")
17
18
  .usage("Usage: $0 <organization> [options]")
18
19
  .version(packageVersion)
19
- .command("$0 <organization>", "Azure DevOps MCP Server", (yargs) => {
20
+ .command("$0 <organization> [options]", "Azure DevOps MCP Server", (yargs) => {
20
21
  yargs.positional("organization", {
21
22
  describe: "Azure DevOps organization name",
22
23
  type: "string",
24
+ demandOption: true,
23
25
  });
26
+ })
27
+ .option("domains", {
28
+ alias: "d",
29
+ describe: "Domain(s) to enable: 'all' for everything, or specific domains like 'repositories builds work'. Defaults to 'all'.",
30
+ type: "string",
31
+ array: true,
32
+ default: "all",
24
33
  })
25
34
  .option("tenant", {
26
35
  alias: "t",
@@ -29,9 +38,11 @@ const argv = yargs(hideBin(process.argv))
29
38
  })
30
39
  .help()
31
40
  .parseSync();
32
- export const orgName = argv.organization;
33
41
  const tenantId = argv.tenant;
42
+ export const orgName = argv.organization;
34
43
  const orgUrl = "https://dev.azure.com/" + orgName;
44
+ const domainsManager = new DomainsManager(argv.domains);
45
+ export const enabledDomains = domainsManager.getEnabledDomains();
35
46
  async function getAzureDevOpsToken() {
36
47
  if (process.env.ADO_MCP_AZURE_TOKEN_CREDENTIALS) {
37
48
  process.env.AZURE_TOKEN_CREDENTIALS = process.env.ADO_MCP_AZURE_TOKEN_CREDENTIALS;
@@ -73,7 +84,7 @@ async function main() {
73
84
  userAgentComposer.appendMcpClientInfo(server.server.getClientVersion());
74
85
  };
75
86
  configurePrompts(server);
76
- configureAllTools(server, getAzureDevOpsToken, getAzureDevOpsClient(userAgentComposer), () => userAgentComposer.userAgent);
87
+ configureAllTools(server, getAzureDevOpsToken, getAzureDevOpsClient(userAgentComposer), () => userAgentComposer.userAgent, enabledDomains);
77
88
  const transport = new StdioServerTransport();
78
89
  await server.connect(transport);
79
90
  }
package/dist/prompts.js CHANGED
@@ -2,9 +2,9 @@
2
2
  // Licensed under the MIT License.
3
3
  import { z } from "zod";
4
4
  import { CORE_TOOLS } from "./tools/core.js";
5
- import { WORKITEM_TOOLS } from "./tools/workitems.js";
5
+ import { WORKITEM_TOOLS } from "./tools/work-items.js";
6
6
  function configurePrompts(server) {
7
- server.prompt("listProjects", "Lists all projects in the Azure DevOps organization.", {}, () => ({
7
+ server.prompt("Projects", "Lists all projects in the Azure DevOps organization.", {}, () => ({
8
8
  messages: [
9
9
  {
10
10
  role: "user",
@@ -12,13 +12,13 @@ function configurePrompts(server) {
12
12
  type: "text",
13
13
  text: String.raw `
14
14
  # Task
15
- Use the '${CORE_TOOLS.list_projects}' tool to retrieve all projects in the current Azure DevOps organization.
16
- Present the results in a table with the following columns: Project ID, Name, and Description.`,
15
+ Use the '${CORE_TOOLS.list_projects}' tool to retrieve all 'wellFormed' projects in the current Azure DevOps organization.
16
+ Present the results in alphabetical order in a table with the following columns: Name and ID.`,
17
17
  },
18
18
  },
19
19
  ],
20
20
  }));
21
- server.prompt("listTeams", "Retrieves all teams for a given Azure DevOps project.", { project: z.string() }, ({ project }) => ({
21
+ server.prompt("Teams", "Retrieves all teams for a given Azure DevOps project.", { project: z.string() }, ({ project }) => ({
22
22
  messages: [
23
23
  {
24
24
  role: "user",
@@ -27,7 +27,7 @@ Present the results in a table with the following columns: Project ID, Name, and
27
27
  text: String.raw `
28
28
  # Task
29
29
  Use the '${CORE_TOOLS.list_project_teams}' tool to retrieve all teams for the project '${project}'.
30
- Present the results in a table with the following columns: Team ID, and Name`,
30
+ Present the results in alphabetical order in a table with the following columns: Name and Id`,
31
31
  },
32
32
  },
33
33
  ],
@@ -0,0 +1,122 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ /**
4
+ * Available Azure DevOps MCP domains
5
+ */
6
+ export var Domain;
7
+ (function (Domain) {
8
+ Domain["ADVANCED_SECURITY"] = "advanced-security";
9
+ Domain["BUILDS"] = "builds";
10
+ Domain["CORE"] = "core";
11
+ Domain["RELEASES"] = "releases";
12
+ Domain["REPOSITORIES"] = "repositories";
13
+ Domain["SEARCH"] = "search";
14
+ Domain["TEST_PLANS"] = "test-plans";
15
+ Domain["WIKI"] = "wiki";
16
+ Domain["WORK"] = "work";
17
+ Domain["WORK_ITEMS"] = "work-items";
18
+ })(Domain || (Domain = {}));
19
+ export const ALL_DOMAINS = "all";
20
+ /**
21
+ * Manages domain parsing and validation for Azure DevOps MCP server tools
22
+ */
23
+ export class DomainsManager {
24
+ static AVAILABLE_DOMAINS = Object.values(Domain);
25
+ enabledDomains;
26
+ constructor(domainsInput) {
27
+ this.enabledDomains = new Set();
28
+ const normalizedInput = DomainsManager.parseDomainsInput(domainsInput);
29
+ this.parseDomains(normalizedInput);
30
+ }
31
+ /**
32
+ * Parse and validate domains from input
33
+ * @param domainsInput - Either "all", single domain name, array of domain names, or undefined (defaults to "all")
34
+ */
35
+ parseDomains(domainsInput) {
36
+ if (!domainsInput) {
37
+ this.enableAllDomains();
38
+ return;
39
+ }
40
+ if (Array.isArray(domainsInput)) {
41
+ this.handleArrayInput(domainsInput);
42
+ return;
43
+ }
44
+ this.handleStringInput(domainsInput);
45
+ }
46
+ handleArrayInput(domainsInput) {
47
+ if (domainsInput.length === 0 || domainsInput.includes(ALL_DOMAINS)) {
48
+ this.enableAllDomains();
49
+ return;
50
+ }
51
+ if (domainsInput.length === 1 && domainsInput[0] === ALL_DOMAINS) {
52
+ this.enableAllDomains();
53
+ return;
54
+ }
55
+ const domains = domainsInput.map((d) => d.trim().toLowerCase());
56
+ this.validateAndAddDomains(domains);
57
+ }
58
+ handleStringInput(domainsInput) {
59
+ if (domainsInput === ALL_DOMAINS) {
60
+ this.enableAllDomains();
61
+ return;
62
+ }
63
+ const domains = [domainsInput.trim().toLowerCase()];
64
+ this.validateAndAddDomains(domains);
65
+ }
66
+ validateAndAddDomains(domains) {
67
+ const availableDomainsAsStringArray = Object.values(Domain);
68
+ domains.forEach((domain) => {
69
+ if (availableDomainsAsStringArray.includes(domain)) {
70
+ this.enabledDomains.add(domain);
71
+ }
72
+ else if (domain === ALL_DOMAINS) {
73
+ this.enableAllDomains();
74
+ }
75
+ else {
76
+ console.error(`Error: Specified invalid domain '${domain}'. Please specify exactly as available domains: ${Object.values(Domain).join(", ")}`);
77
+ }
78
+ });
79
+ if (this.enabledDomains.size === 0) {
80
+ this.enableAllDomains();
81
+ }
82
+ }
83
+ enableAllDomains() {
84
+ Object.values(Domain).forEach((domain) => this.enabledDomains.add(domain));
85
+ }
86
+ /**
87
+ * Check if a specific domain is enabled
88
+ * @param domain - Domain name to check
89
+ * @returns true if domain is enabled
90
+ */
91
+ isDomainEnabled(domain) {
92
+ return this.enabledDomains.has(domain);
93
+ }
94
+ /**
95
+ * Get all enabled domains
96
+ * @returns Set of enabled domain names
97
+ */
98
+ getEnabledDomains() {
99
+ return new Set(this.enabledDomains);
100
+ }
101
+ /**
102
+ * Get list of all available domains
103
+ * @returns Array of available domain names
104
+ */
105
+ static getAvailableDomains() {
106
+ return Object.values(Domain);
107
+ }
108
+ /**
109
+ * Parse domains input from string or array to a normalized array of strings
110
+ * @param domainsInput - Domains input to parse
111
+ * @returns Normalized array of domain strings
112
+ */
113
+ static parseDomainsInput(domainsInput) {
114
+ if (!domainsInput) {
115
+ return [];
116
+ }
117
+ if (typeof domainsInput === "string") {
118
+ return domainsInput.split(",").map((d) => d.trim().toLowerCase());
119
+ }
120
+ return domainsInput.map((d) => d.trim().toLowerCase());
121
+ }
122
+ }
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
- async function getCurrentUserDetails(tokenProvider, connectionProvider) {
3
+ import { apiVersion } from "../utils.js";
4
+ async function getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider) {
4
5
  const connection = await connectionProvider();
5
6
  const url = `${connection.serverUrl}/_apis/connectionData`;
6
7
  const token = (await tokenProvider()).token;
@@ -9,6 +10,7 @@ async function getCurrentUserDetails(tokenProvider, connectionProvider) {
9
10
  headers: {
10
11
  "Authorization": `Bearer ${token}`,
11
12
  "Content-Type": "application/json",
13
+ "User-Agent": userAgentProvider(),
12
14
  },
13
15
  });
14
16
  const data = await response.json();
@@ -17,4 +19,44 @@ async function getCurrentUserDetails(tokenProvider, connectionProvider) {
17
19
  }
18
20
  return data;
19
21
  }
20
- export { getCurrentUserDetails };
22
+ /**
23
+ * Searches for identities using Azure DevOps Identity API
24
+ */
25
+ async function searchIdentities(identity, tokenProvider, connectionProvider, userAgentProvider) {
26
+ const token = await tokenProvider();
27
+ const connection = await connectionProvider();
28
+ const orgName = connection.serverUrl.split("/")[3];
29
+ const baseUrl = `https://vssps.dev.azure.com/${orgName}/_apis/identities`;
30
+ const params = new URLSearchParams({
31
+ "api-version": apiVersion,
32
+ "searchFilter": "General",
33
+ "filterValue": identity,
34
+ });
35
+ const response = await fetch(`${baseUrl}?${params}`, {
36
+ headers: {
37
+ "Authorization": `Bearer ${token.token}`,
38
+ "Content-Type": "application/json",
39
+ "User-Agent": userAgentProvider(),
40
+ },
41
+ });
42
+ if (!response.ok) {
43
+ const errorText = await response.text();
44
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
45
+ }
46
+ return await response.json();
47
+ }
48
+ /**
49
+ * Gets the user ID from email or unique name using Azure DevOps Identity API
50
+ */
51
+ async function getUserIdFromEmail(userEmail, tokenProvider, connectionProvider, userAgentProvider) {
52
+ const identities = await searchIdentities(userEmail, tokenProvider, connectionProvider, userAgentProvider);
53
+ if (!identities || identities.value?.length === 0) {
54
+ throw new Error(`No user found with email/unique name: ${userEmail}`);
55
+ }
56
+ const firstIdentity = identities.value[0];
57
+ if (!firstIdentity.id) {
58
+ throw new Error(`No ID found for user with email/unique name: ${userEmail}`);
59
+ }
60
+ return firstIdentity.id;
61
+ }
62
+ export { getCurrentUserDetails, getUserIdFromEmail, searchIdentities };
@@ -15,7 +15,7 @@ const BUILD_TOOLS = {
15
15
  get_status: "build_get_status",
16
16
  update_build_stage: "build_update_build_stage",
17
17
  };
18
- function configureBuildTools(server, tokenProvider, connectionProvider) {
18
+ function configureBuildTools(server, tokenProvider, connectionProvider, userAgentProvider) {
19
19
  server.tool(BUILD_TOOLS.get_definitions, "Retrieves a list of build definitions for a given project.", {
20
20
  project: z.string().describe("Project ID or name to get build definitions for"),
21
21
  repositoryId: z.string().optional().describe("Repository ID to filter build definitions"),
@@ -191,6 +191,7 @@ function configureBuildTools(server, tokenProvider, connectionProvider) {
191
191
  headers: {
192
192
  "Content-Type": "application/json",
193
193
  "Authorization": `Bearer ${token.token}`,
194
+ "User-Agent": userAgentProvider(),
194
195
  },
195
196
  body: JSON.stringify(body),
196
197
  });
@@ -1,7 +1,7 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
3
  import { z } from "zod";
4
- import { apiVersion } from "../utils.js";
4
+ import { searchIdentities } from "./auth.js";
5
5
  const CORE_TOOLS = {
6
6
  list_project_teams: "core_list_project_teams",
7
7
  list_projects: "core_list_projects",
@@ -11,7 +11,7 @@ function filterProjectsByName(projects, projectNameFilter) {
11
11
  const lowerCaseFilter = projectNameFilter.toLowerCase();
12
12
  return projects.filter((project) => project.name?.toLowerCase().includes(lowerCaseFilter));
13
13
  }
14
- function configureCoreTools(server, tokenProvider, connectionProvider) {
14
+ function configureCoreTools(server, tokenProvider, connectionProvider, userAgentProvider) {
15
15
  server.tool(CORE_TOOLS.list_project_teams, "Retrieve a list of teams for the specified Azure DevOps project.", {
16
16
  project: z.string().describe("The name or ID of the Azure DevOps project."),
17
17
  mine: z.boolean().optional().describe("If true, only return teams that the authenticated user is a member of."),
@@ -68,26 +68,7 @@ function configureCoreTools(server, tokenProvider, connectionProvider) {
68
68
  searchFilter: z.string().describe("Search filter (unique namme, display name, email) to retrieve identity IDs for."),
69
69
  }, async ({ searchFilter }) => {
70
70
  try {
71
- const token = await tokenProvider();
72
- const connection = await connectionProvider();
73
- const orgName = connection.serverUrl.split("/")[3];
74
- const baseUrl = `https://vssps.dev.azure.com/${orgName}/_apis/identities`;
75
- const params = new URLSearchParams({
76
- "api-version": apiVersion,
77
- "searchFilter": "General",
78
- "filterValue": searchFilter,
79
- });
80
- const response = await fetch(`${baseUrl}?${params}`, {
81
- headers: {
82
- "Authorization": `Bearer ${token.token}`,
83
- "Content-Type": "application/json",
84
- },
85
- });
86
- if (!response.ok) {
87
- const errorText = await response.text();
88
- throw new Error(`HTTP ${response.status}: ${errorText}`);
89
- }
90
- const identities = await response.json();
71
+ const identities = await searchIdentities(searchFilter, tokenProvider, connectionProvider, userAgentProvider);
91
72
  if (!identities || identities.value?.length === 0) {
92
73
  return { content: [{ type: "text", text: "No identities found" }], isError: true };
93
74
  }
@@ -2,7 +2,7 @@
2
2
  // Licensed under the MIT License.
3
3
  import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, CommentThreadStatus, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
4
4
  import { z } from "zod";
5
- import { getCurrentUserDetails } from "./auth.js";
5
+ import { getCurrentUserDetails, getUserIdFromEmail } from "./auth.js";
6
6
  import { getEnumKeys } from "../utils.js";
7
7
  const REPO_TOOLS = {
8
8
  list_repos_by_project: "repo_list_repos_by_project",
@@ -73,7 +73,7 @@ function filterReposByName(repositories, repoNameFilter) {
73
73
  const filteredByName = repositories?.filter((repo) => repo.name?.toLowerCase().includes(lowerCaseFilter));
74
74
  return filteredByName;
75
75
  }
76
- function configureRepoTools(server, tokenProvider, connectionProvider) {
76
+ function configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider) {
77
77
  server.tool(REPO_TOOLS.create_pull_request, "Create a new pull request.", {
78
78
  repositoryId: z.string().describe("The ID of the repository where the pull request will be created."),
79
79
  sourceRefName: z.string().describe("The source branch name for the pull request, e.g., 'refs/heads/feature-branch'."),
@@ -197,12 +197,13 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
197
197
  top: z.number().default(100).describe("The maximum number of pull requests to return."),
198
198
  skip: z.number().default(0).describe("The number of pull requests to skip."),
199
199
  created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
200
+ created_by_user: z.string().optional().describe("Filter pull requests created by a specific user (provide email or unique name). Takes precedence over created_by_me if both are provided."),
200
201
  i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
201
202
  status: z
202
203
  .enum(getEnumKeys(PullRequestStatus))
203
204
  .default("Active")
204
205
  .describe("Filter pull requests by status. Defaults to 'Active'."),
205
- }, async ({ repositoryId, top, skip, created_by_me, i_am_reviewer, status }) => {
206
+ }, async ({ repositoryId, top, skip, created_by_me, created_by_user, i_am_reviewer, status }) => {
206
207
  const connection = await connectionProvider();
207
208
  const gitApi = await connection.getGitApi();
208
209
  // Build the search criteria
@@ -210,8 +211,25 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
210
211
  status: pullRequestStatusStringToInt(status),
211
212
  repositoryId: repositoryId,
212
213
  };
213
- if (created_by_me || i_am_reviewer) {
214
- const data = await getCurrentUserDetails(tokenProvider, connectionProvider);
214
+ if (created_by_user) {
215
+ try {
216
+ const userId = await getUserIdFromEmail(created_by_user, tokenProvider, connectionProvider, userAgentProvider);
217
+ searchCriteria.creatorId = userId;
218
+ }
219
+ catch (error) {
220
+ return {
221
+ content: [
222
+ {
223
+ type: "text",
224
+ text: `Error finding user with email ${created_by_user}: ${error instanceof Error ? error.message : String(error)}`,
225
+ },
226
+ ],
227
+ isError: true,
228
+ };
229
+ }
230
+ }
231
+ else if (created_by_me || i_am_reviewer) {
232
+ const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
215
233
  const userId = data.authenticatedUser.id;
216
234
  if (created_by_me) {
217
235
  searchCriteria.creatorId = userId;
@@ -235,6 +253,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
235
253
  creationDate: pr.creationDate,
236
254
  title: pr.title,
237
255
  isDraft: pr.isDraft,
256
+ sourceRefName: pr.sourceRefName,
257
+ targetRefName: pr.targetRefName,
238
258
  }));
239
259
  return {
240
260
  content: [{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) }],
@@ -245,20 +265,38 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
245
265
  top: z.number().default(100).describe("The maximum number of pull requests to return."),
246
266
  skip: z.number().default(0).describe("The number of pull requests to skip."),
247
267
  created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
268
+ created_by_user: z.string().optional().describe("Filter pull requests created by a specific user (provide email or unique name). Takes precedence over created_by_me if both are provided."),
248
269
  i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
249
270
  status: z
250
271
  .enum(getEnumKeys(PullRequestStatus))
251
272
  .default("Active")
252
273
  .describe("Filter pull requests by status. Defaults to 'Active'."),
253
- }, async ({ project, top, skip, created_by_me, i_am_reviewer, status }) => {
274
+ }, async ({ project, top, skip, created_by_me, created_by_user, i_am_reviewer, status }) => {
254
275
  const connection = await connectionProvider();
255
276
  const gitApi = await connection.getGitApi();
256
277
  // Build the search criteria
257
278
  const gitPullRequestSearchCriteria = {
258
279
  status: pullRequestStatusStringToInt(status),
259
280
  };
260
- if (created_by_me || i_am_reviewer) {
261
- const data = await getCurrentUserDetails(tokenProvider, connectionProvider);
281
+ if (created_by_user) {
282
+ try {
283
+ const userId = await getUserIdFromEmail(created_by_user, tokenProvider, connectionProvider, userAgentProvider);
284
+ gitPullRequestSearchCriteria.creatorId = userId;
285
+ }
286
+ catch (error) {
287
+ return {
288
+ content: [
289
+ {
290
+ type: "text",
291
+ text: `Error finding user with email ${created_by_user}: ${error instanceof Error ? error.message : String(error)}`,
292
+ },
293
+ ],
294
+ isError: true,
295
+ };
296
+ }
297
+ }
298
+ else if (created_by_me || i_am_reviewer) {
299
+ const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
262
300
  const userId = data.authenticatedUser.id;
263
301
  if (created_by_me) {
264
302
  gitPullRequestSearchCriteria.creatorId = userId;
@@ -282,6 +320,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
282
320
  creationDate: pr.creationDate,
283
321
  title: pr.title,
284
322
  isDraft: pr.isDraft,
323
+ sourceRefName: pr.sourceRefName,
324
+ targetRefName: pr.targetRefName,
285
325
  }));
286
326
  return {
287
327
  content: [{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) }],
@@ -346,10 +386,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
346
386
  server.tool(REPO_TOOLS.list_branches_by_repo, "Retrieve a list of branches for a given repository.", {
347
387
  repositoryId: z.string().describe("The ID of the repository where the branches are located."),
348
388
  top: z.number().default(100).describe("The maximum number of branches to return. Defaults to 100."),
349
- }, async ({ repositoryId, top }) => {
389
+ filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
390
+ }, async ({ repositoryId, top, filterContains }) => {
350
391
  const connection = await connectionProvider();
351
392
  const gitApi = await connection.getGitApi();
352
- const branches = await gitApi.getRefs(repositoryId, undefined);
393
+ const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", undefined, undefined, undefined, undefined, undefined, filterContains);
353
394
  const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
354
395
  return {
355
396
  content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
@@ -358,10 +399,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
358
399
  server.tool(REPO_TOOLS.list_my_branches_by_repo, "Retrieve a list of my branches for a given repository Id.", {
359
400
  repositoryId: z.string().describe("The ID of the repository where the branches are located."),
360
401
  top: z.number().default(100).describe("The maximum number of branches to return."),
361
- }, async ({ repositoryId, top }) => {
402
+ filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
403
+ }, async ({ repositoryId, top, filterContains }) => {
362
404
  const connection = await connectionProvider();
363
405
  const gitApi = await connection.getGitApi();
364
- const branches = await gitApi.getRefs(repositoryId, undefined, undefined, undefined, undefined, true);
406
+ const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", undefined, undefined, true, undefined, undefined, filterContains);
365
407
  const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
366
408
  return {
367
409
  content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
@@ -388,8 +430,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
388
430
  }, async ({ repositoryId, branchName }) => {
389
431
  const connection = await connectionProvider();
390
432
  const gitApi = await connection.getGitApi();
391
- const branches = await gitApi.getRefs(repositoryId);
392
- const branch = branches?.find((branch) => branch.name === `refs/heads/${branchName}`);
433
+ const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", false, false, undefined, false, undefined, branchName);
434
+ const branch = branches.find((branch) => branch.name === `refs/heads/${branchName}` || branch.name === branchName);
393
435
  if (!branch) {
394
436
  return {
395
437
  content: [
@@ -398,6 +440,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
398
440
  text: `Branch ${branchName} not found in repository ${repositoryId}`,
399
441
  },
400
442
  ],
443
+ isError: true,
401
444
  };
402
445
  }
403
446
  return {
@@ -79,7 +79,10 @@ function configureTestPlanTools(server, tokenProvider, connectionProvider) {
79
79
  server.tool(Test_Plan_Tools.create_test_case, "Creates a new test case work item.", {
80
80
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
81
81
  title: z.string().describe("The title of the test case."),
82
- steps: z.string().optional().describe("The steps to reproduce the test case. Make sure to format each step as '1. Step one|Expected result one\n2. Step two|Expected result two"),
82
+ steps: z
83
+ .string()
84
+ .optional()
85
+ .describe("The steps to reproduce the test case. Make sure to format each step as '1. Step one|Expected result one\n2. Step two|Expected result two. USE '|' as the delimiter between step and expected result. DO NOT use '|' in the description of the step or expected result."),
83
86
  priority: z.number().optional().describe("The priority of the test case."),
84
87
  areaPath: z.string().optional().describe("The area path for the test case."),
85
88
  iterationPath: z.string().optional().describe("The iteration path for the test case."),