@azure-devops/mcp 2.2.0 → 2.2.1

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 CHANGED
@@ -7,8 +7,6 @@ Easily install the Azure DevOps MCP Server for VS Code or VS Code Insiders:
7
7
 
8
8
  This TypeScript project provides a **local** MCP server for Azure DevOps, enabling you to perform a wide range of Azure DevOps tasks directly from your code editor.
9
9
 
10
- > 🚨 **Public Preview:** This project is in public preview. Tools and features may change before general availability.
11
-
12
10
  ## 📄 Table of Contents
13
11
 
14
12
  1. [📺 Overview](#-overview)
@@ -83,8 +81,7 @@ Interact with these Azure DevOps services:
83
81
  ### 📁 Repositories
84
82
 
85
83
  - **repo_list_repos_by_project**: Retrieve a list of repositories for a given project.
86
- - **repo_list_pull_requests_by_repo**: Retrieve a list of pull requests for a given repository.
87
- - **repo_list_pull_requests_by_project**: Retrieve a list of pull requests for a given project ID or name.
84
+ - **repo_list_pull_requests_by_repo_or_project**: Retrieve a list of pull requests for a given repository or project.
88
85
  - **repo_list_branches_by_repo**: Retrieve a list of branches for a given repository.
89
86
  - **repo_list_my_branches_by_repo**: Retrieve a list of your branches for a given repository ID.
90
87
  - **repo_list_pull_requests_by_commits**: List pull requests associated with commits.
@@ -95,7 +92,6 @@ Interact with these Azure DevOps services:
95
92
  - **repo_get_pull_request_by_id**: Get a pull request by its ID.
96
93
  - **repo_create_pull_request**: Create a new pull request.
97
94
  - **repo_create_branch**: Create a new branch in the repository.
98
- - **repo_update_pull_request_status**: Update the status of an existing pull request to active or abandoned.
99
95
  - **repo_update_pull_request**: Update various fields of an existing pull request (title, description, draft status, target branch).
100
96
  - **repo_update_pull_request_reviewers**: Add or remove reviewers for an existing pull request.
101
97
  - **repo_reply_to_comment**: Replies to a specific comment on a pull request.
@@ -126,6 +122,7 @@ Interact with these Azure DevOps services:
126
122
 
127
123
  - **testplan_create_test_plan**: Create a new test plan in the project.
128
124
  - **testplan_create_test_case**: Create a new test case work item.
125
+ - **testplan_update_test_case_steps**: Update an existing test case work item's steps.
129
126
  - **testplan_add_test_cases_to_suite**: Add existing test cases to a test suite.
130
127
  - **testplan_list_test_plans**: Retrieve a paginated list of test plans from an Azure DevOps project. Allows filtering for active plans and toggling detailed information.
131
128
  - **testplan_list_test_cases**: Get a list of test cases in the test plan.
@@ -137,6 +134,7 @@ Interact with these Azure DevOps services:
137
134
  - **wiki_list_wikis**: Retrieve a list of wikis for an organization or project.
138
135
  - **wiki_get_wiki**: Get the wiki by wikiIdentifier.
139
136
  - **wiki_list_pages**: Retrieve a list of wiki pages for a specific wiki and project.
137
+ - **wiki_get_page**: Retrieve wiki page metadata by path.
140
138
  - **wiki_get_page_content**: Retrieve wiki page content by wikiIdentifier and path.
141
139
  - **wiki_create_or_update_page**: Create or update wiki pages with full content support.
142
140
 
package/dist/auth.js CHANGED
@@ -5,14 +5,19 @@ const scopes = ["499b84ac-1321-427f-aa17-267ca6975798/.default"];
5
5
  class OAuthAuthenticator {
6
6
  static clientId = "0d50963b-7bb9-4fe7-94c7-a99af00b5136";
7
7
  static defaultAuthority = "https://login.microsoftonline.com/common";
8
+ static zeroTenantId = "00000000-0000-0000-0000-000000000000";
8
9
  accountId;
9
10
  publicClientApp;
10
11
  constructor(tenantId) {
11
12
  this.accountId = null;
13
+ let authority = OAuthAuthenticator.defaultAuthority;
14
+ if (tenantId && tenantId !== OAuthAuthenticator.zeroTenantId) {
15
+ authority = `https://login.microsoftonline.com/${tenantId}`;
16
+ }
12
17
  this.publicClientApp = new PublicClientApplication({
13
18
  auth: {
14
19
  clientId: OAuthAuthenticator.clientId,
15
- authority: tenantId ? `https://login.microsoftonline.com/${tenantId}` : OAuthAuthenticator.defaultAuthority,
20
+ authority,
16
21
  },
17
22
  });
18
23
  }
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import yargs from "yargs";
8
8
  import { hideBin } from "yargs/helpers";
9
9
  import { createAuthenticator } from "./auth.js";
10
10
  import { getOrgTenant } from "./org-tenants.js";
11
- import { configurePrompts } from "./prompts.js";
11
+ //import { configurePrompts } from "./prompts.js";
12
12
  import { configureAllTools } from "./tools.js";
13
13
  import { UserAgentComposer } from "./useragent.js";
14
14
  import { packageVersion } from "./version.js";
@@ -77,7 +77,8 @@ async function main() {
77
77
  };
78
78
  const tenantId = (await getOrgTenant(orgName)) ?? argv.tenant;
79
79
  const authenticator = createAuthenticator(argv.authentication, tenantId);
80
- configurePrompts(server);
80
+ // removing prompts untill further notice
81
+ // configurePrompts(server);
81
82
  configureAllTools(server, authenticator, getAzureDevOpsClient(authenticator, userAgentComposer), () => userAgentComposer.userAgent, enabledDomains);
82
83
  const transport = new StdioServerTransport();
83
84
  await server.connect(transport);
package/dist/prompts.js CHANGED
@@ -1,8 +1,6 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
- import { z } from "zod";
4
3
  import { CORE_TOOLS } from "./tools/core.js";
5
- import { WORKITEM_TOOLS } from "./tools/work-items.js";
6
4
  function configurePrompts(server) {
7
5
  server.prompt("Projects", "Lists all projects in the Azure DevOps organization.", {}, () => ({
8
6
  messages: [
@@ -18,33 +16,5 @@ Present the results in alphabetical order in a table with the following columns:
18
16
  },
19
17
  ],
20
18
  }));
21
- server.prompt("Teams", "Retrieves all teams for a given Azure DevOps project.", { project: z.string() }, ({ project }) => ({
22
- messages: [
23
- {
24
- role: "user",
25
- content: {
26
- type: "text",
27
- text: String.raw `
28
- # Task
29
- Use the '${CORE_TOOLS.list_project_teams}' tool to retrieve all teams for the project '${project}'.
30
- Present the results in alphabetical order in a table with the following columns: Name and Id`,
31
- },
32
- },
33
- ],
34
- }));
35
- server.prompt("getWorkItem", "Retrieves details for a specific Azure DevOps work item by ID.", { id: z.string().describe("The ID of the work item to retrieve."), project: z.string().describe("The name or ID of the Azure DevOps project.") }, ({ id, project }) => ({
36
- messages: [
37
- {
38
- role: "user",
39
- content: {
40
- type: "text",
41
- text: String.raw `
42
- # Task
43
- Use the '${WORKITEM_TOOLS.get_work_item}' tool to retrieve details for the work item with ID '${id}' in project '${project}'.
44
- Present the following fields: ID, Title, State, Assigned To, Work Item Type, Description or Repro Steps, and Created Date.`,
45
- },
46
- },
47
- ],
48
- }));
49
19
  }
50
20
  export { configurePrompts };
@@ -1,13 +1,12 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
- import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, CommentThreadStatus, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
3
+ import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, CommentThreadStatus, GitPullRequestMergeStrategy, } 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";
7
7
  const REPO_TOOLS = {
8
8
  list_repos_by_project: "repo_list_repos_by_project",
9
- list_pull_requests_by_repo: "repo_list_pull_requests_by_repo",
10
- list_pull_requests_by_project: "repo_list_pull_requests_by_project",
9
+ list_pull_requests_by_repo_or_project: "repo_list_pull_requests_by_repo_or_project",
11
10
  list_branches_by_repo: "repo_list_branches_by_repo",
12
11
  list_my_branches_by_repo: "repo_list_my_branches_by_repo",
13
12
  list_pull_request_threads: "repo_list_pull_request_threads",
@@ -33,6 +32,15 @@ function branchesFilterOutIrrelevantProperties(branches, top) {
33
32
  .sort((a, b) => b.localeCompare(a))
34
33
  .slice(0, top);
35
34
  }
35
+ function trimPullRequestThread(thread) {
36
+ return {
37
+ id: thread.id,
38
+ publishedDate: thread.publishedDate,
39
+ lastUpdatedDate: thread.lastUpdatedDate,
40
+ status: thread.status,
41
+ comments: trimComments(thread.comments),
42
+ };
43
+ }
36
44
  /**
37
45
  * Trims comment data to essential properties, filtering out deleted comments
38
46
  * @param comments Array of comments to trim (can be undefined/null)
@@ -74,6 +82,24 @@ function filterReposByName(repositories, repoNameFilter) {
74
82
  const filteredByName = repositories?.filter((repo) => repo.name?.toLowerCase().includes(lowerCaseFilter));
75
83
  return filteredByName;
76
84
  }
85
+ function trimPullRequest(pr, includeDescription = false) {
86
+ return {
87
+ pullRequestId: pr.pullRequestId,
88
+ codeReviewId: pr.codeReviewId,
89
+ repository: pr.repository?.name,
90
+ status: pr.status,
91
+ createdBy: {
92
+ displayName: pr.createdBy?.displayName,
93
+ uniqueName: pr.createdBy?.uniqueName,
94
+ },
95
+ creationDate: pr.creationDate,
96
+ title: pr.title,
97
+ ...(includeDescription ? { description: pr.description ?? "" } : {}),
98
+ isDraft: pr.isDraft,
99
+ sourceRefName: pr.sourceRefName,
100
+ targetRefName: pr.targetRefName,
101
+ };
102
+ }
77
103
  function configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider) {
78
104
  server.tool(REPO_TOOLS.create_pull_request, "Create a new pull request.", {
79
105
  repositoryId: z.string().describe("The ID of the repository where the pull request will be created."),
@@ -104,8 +130,9 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
104
130
  workItemRefs: workItemRefs,
105
131
  forkSource,
106
132
  }, repositoryId);
133
+ const trimmedPullRequest = trimPullRequest(pullRequest, true);
107
134
  return {
108
- content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }],
135
+ content: [{ type: "text", text: JSON.stringify(trimmedPullRequest, null, 2) }],
109
136
  };
110
137
  });
111
138
  server.tool(REPO_TOOLS.create_branch, "Create a new branch in the repository.", {
@@ -193,7 +220,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
193
220
  };
194
221
  }
195
222
  });
196
- server.tool(REPO_TOOLS.update_pull_request, "Update a Pull Request by ID with specified fields.", {
223
+ server.tool(REPO_TOOLS.update_pull_request, "Update a Pull Request by ID with specified fields, including setting autocomplete with various completion options.", {
197
224
  repositoryId: z.string().describe("The ID of the repository where the pull request exists."),
198
225
  pullRequestId: z.number().describe("The ID of the pull request to update."),
199
226
  title: z.string().optional().describe("The new title for the pull request."),
@@ -201,7 +228,15 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
201
228
  isDraft: z.boolean().optional().describe("Whether the pull request should be a draft."),
202
229
  targetRefName: z.string().optional().describe("The new target branch name (e.g., 'refs/heads/main')."),
203
230
  status: z.enum(["Active", "Abandoned"]).optional().describe("The new status of the pull request. Can be 'Active' or 'Abandoned'."),
204
- }, async ({ repositoryId, pullRequestId, title, description, isDraft, targetRefName, status }) => {
231
+ autoComplete: z.boolean().optional().describe("Set the pull request to autocomplete when all requirements are met."),
232
+ mergeStrategy: z
233
+ .enum(getEnumKeys(GitPullRequestMergeStrategy))
234
+ .optional()
235
+ .describe("The merge strategy to use when the pull request autocompletes. Defaults to 'NoFastForward'."),
236
+ deleteSourceBranch: z.boolean().optional().default(false).describe("Whether to delete the source branch when the pull request autocompletes. Defaults to false."),
237
+ transitionWorkItems: z.boolean().optional().default(true).describe("Whether to transition associated work items to the next state when the pull request autocompletes. Defaults to true."),
238
+ bypassReason: z.string().optional().describe("Reason for bypassing branch policies. When provided, branch policies will be automatically bypassed during autocompletion."),
239
+ }, async ({ repositoryId, pullRequestId, title, description, isDraft, targetRefName, status, autoComplete, mergeStrategy, deleteSourceBranch, transitionWorkItems, bypassReason }) => {
205
240
  const connection = await connectionProvider();
206
241
  const gitApi = await connection.getGitApi();
207
242
  // Build update object with only provided fields
@@ -217,16 +252,40 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
217
252
  if (status !== undefined) {
218
253
  updateRequest.status = status === "Active" ? PullRequestStatus.Active.valueOf() : PullRequestStatus.Abandoned.valueOf();
219
254
  }
255
+ if (autoComplete !== undefined) {
256
+ if (autoComplete) {
257
+ const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
258
+ const autoCompleteUserId = data.authenticatedUser.id;
259
+ updateRequest.autoCompleteSetBy = { id: autoCompleteUserId };
260
+ const completionOptions = {
261
+ deleteSourceBranch: deleteSourceBranch || false,
262
+ transitionWorkItems: transitionWorkItems !== false, // Default to true unless explicitly set to false
263
+ bypassPolicy: !!bypassReason, // Automatically set to true if bypassReason is provided
264
+ };
265
+ if (mergeStrategy) {
266
+ completionOptions.mergeStrategy = GitPullRequestMergeStrategy[mergeStrategy];
267
+ }
268
+ if (bypassReason) {
269
+ completionOptions.bypassReason = bypassReason;
270
+ }
271
+ updateRequest.completionOptions = completionOptions;
272
+ }
273
+ else {
274
+ updateRequest.autoCompleteSetBy = null;
275
+ updateRequest.completionOptions = null;
276
+ }
277
+ }
220
278
  // Validate that at least one field is provided for update
221
279
  if (Object.keys(updateRequest).length === 0) {
222
280
  return {
223
- content: [{ type: "text", text: "Error: At least one field (title, description, isDraft, targetRefName, or status) must be provided for update." }],
281
+ content: [{ type: "text", text: "Error: At least one field (title, description, isDraft, targetRefName, status, or autoComplete options) must be provided for update." }],
224
282
  isError: true,
225
283
  };
226
284
  }
227
285
  const updatedPullRequest = await gitApi.updatePullRequest(updateRequest, repositoryId, pullRequestId);
286
+ const trimmedUpdatedPullRequest = trimPullRequest(updatedPullRequest, true);
228
287
  return {
229
- content: [{ type: "text", text: JSON.stringify(updatedPullRequest, null, 2) }],
288
+ content: [{ type: "text", text: JSON.stringify(trimmedUpdatedPullRequest, null, 2) }],
230
289
  };
231
290
  });
232
291
  server.tool(REPO_TOOLS.update_pull_request_reviewers, "Add or remove reviewers for an existing pull request.", {
@@ -240,8 +299,16 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
240
299
  let updatedPullRequest;
241
300
  if (action === "add") {
242
301
  updatedPullRequest = await gitApi.createPullRequestReviewers(reviewerIds.map((id) => ({ id: id })), repositoryId, pullRequestId);
302
+ const trimmedResponse = updatedPullRequest.map((item) => ({
303
+ displayName: item.displayName,
304
+ id: item.id,
305
+ uniqueName: item.uniqueName,
306
+ vote: item.vote,
307
+ hasDeclined: item.hasDeclined,
308
+ isFlagged: item.isFlagged,
309
+ }));
243
310
  return {
244
- content: [{ type: "text", text: JSON.stringify(updatedPullRequest, null, 2) }],
311
+ content: [{ type: "text", text: JSON.stringify(trimmedResponse, null, 2) }],
245
312
  };
246
313
  }
247
314
  else {
@@ -278,8 +345,9 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
278
345
  content: [{ type: "text", text: JSON.stringify(trimmedRepositories, null, 2) }],
279
346
  };
280
347
  });
281
- server.tool(REPO_TOOLS.list_pull_requests_by_repo, "Retrieve a list of pull requests for a given repository.", {
282
- repositoryId: z.string().describe("The ID of the repository where the pull requests are located."),
348
+ server.tool(REPO_TOOLS.list_pull_requests_by_repo_or_project, "Retrieve a list of pull requests for a given repository. Either repositoryId or project must be provided.", {
349
+ repositoryId: z.string().optional().describe("The ID of the repository where the pull requests are located."),
350
+ project: z.string().optional().describe("The ID of the project where the pull requests are located."),
283
351
  top: z.number().default(100).describe("The maximum number of pull requests to return."),
284
352
  skip: z.number().default(0).describe("The number of pull requests to skip."),
285
353
  created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
@@ -295,14 +363,27 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
295
363
  .describe("Filter pull requests by status. Defaults to 'Active'."),
296
364
  sourceRefName: z.string().optional().describe("Filter pull requests from this source branch (e.g., 'refs/heads/feature-branch')."),
297
365
  targetRefName: z.string().optional().describe("Filter pull requests into this target branch (e.g., 'refs/heads/main')."),
298
- }, async ({ repositoryId, top, skip, created_by_me, created_by_user, i_am_reviewer, user_is_reviewer, status, sourceRefName, targetRefName }) => {
366
+ }, async ({ repositoryId, project, top, skip, created_by_me, created_by_user, i_am_reviewer, user_is_reviewer, status, sourceRefName, targetRefName }) => {
299
367
  const connection = await connectionProvider();
300
368
  const gitApi = await connection.getGitApi();
301
369
  // Build the search criteria
302
370
  const searchCriteria = {
303
371
  status: pullRequestStatusStringToInt(status),
304
- repositoryId: repositoryId,
305
372
  };
373
+ if (!repositoryId && !project) {
374
+ return {
375
+ content: [
376
+ {
377
+ type: "text",
378
+ text: "Either repositoryId or project must be provided.",
379
+ },
380
+ ],
381
+ isError: true,
382
+ };
383
+ }
384
+ if (repositoryId) {
385
+ searchCriteria.repositoryId = repositoryId;
386
+ }
306
387
  if (sourceRefName) {
307
388
  searchCriteria.sourceRefName = sourceRefName;
308
389
  }
@@ -353,120 +434,30 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
353
434
  const userId = data.authenticatedUser.id;
354
435
  searchCriteria.reviewerId = userId;
355
436
  }
356
- const pullRequests = await gitApi.getPullRequests(repositoryId, searchCriteria, undefined, // project
357
- undefined, // maxCommentLength
358
- skip, top);
359
- // Filter out the irrelevant properties
360
- const filteredPullRequests = pullRequests?.map((pr) => ({
361
- pullRequestId: pr.pullRequestId,
362
- codeReviewId: pr.codeReviewId,
363
- status: pr.status,
364
- createdBy: {
365
- displayName: pr.createdBy?.displayName,
366
- uniqueName: pr.createdBy?.uniqueName,
367
- },
368
- creationDate: pr.creationDate,
369
- title: pr.title,
370
- isDraft: pr.isDraft,
371
- sourceRefName: pr.sourceRefName,
372
- targetRefName: pr.targetRefName,
373
- }));
374
- return {
375
- content: [{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) }],
376
- };
377
- });
378
- server.tool(REPO_TOOLS.list_pull_requests_by_project, "Retrieve a list of pull requests for a given project Id or Name.", {
379
- project: z.string().describe("The name or ID of the Azure DevOps project."),
380
- top: z.number().default(100).describe("The maximum number of pull requests to return."),
381
- skip: z.number().default(0).describe("The number of pull requests to skip."),
382
- created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
383
- 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."),
384
- i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
385
- user_is_reviewer: z
386
- .string()
387
- .optional()
388
- .describe("Filter pull requests where a specific user is a reviewer (provide email or unique name). Takes precedence over i_am_reviewer if both are provided."),
389
- status: z
390
- .enum(getEnumKeys(PullRequestStatus))
391
- .default("Active")
392
- .describe("Filter pull requests by status. Defaults to 'Active'."),
393
- sourceRefName: z.string().optional().describe("Filter pull requests from this source branch (e.g., 'refs/heads/feature-branch')."),
394
- targetRefName: z.string().optional().describe("Filter pull requests into this target branch (e.g., 'refs/heads/main')."),
395
- }, async ({ project, top, skip, created_by_me, created_by_user, i_am_reviewer, user_is_reviewer, status, sourceRefName, targetRefName }) => {
396
- const connection = await connectionProvider();
397
- const gitApi = await connection.getGitApi();
398
- // Build the search criteria
399
- const gitPullRequestSearchCriteria = {
400
- status: pullRequestStatusStringToInt(status),
401
- };
402
- if (sourceRefName) {
403
- gitPullRequestSearchCriteria.sourceRefName = sourceRefName;
404
- }
405
- if (targetRefName) {
406
- gitPullRequestSearchCriteria.targetRefName = targetRefName;
407
- }
408
- if (created_by_user) {
409
- try {
410
- const userId = await getUserIdFromEmail(created_by_user, tokenProvider, connectionProvider, userAgentProvider);
411
- gitPullRequestSearchCriteria.creatorId = userId;
412
- }
413
- catch (error) {
414
- return {
415
- content: [
416
- {
417
- type: "text",
418
- text: `Error finding user with email ${created_by_user}: ${error instanceof Error ? error.message : String(error)}`,
419
- },
420
- ],
421
- isError: true,
422
- };
423
- }
437
+ let pullRequests;
438
+ if (repositoryId) {
439
+ pullRequests = await gitApi.getPullRequests(repositoryId, searchCriteria, project, // project
440
+ undefined, // maxCommentLength
441
+ skip, top);
424
442
  }
425
- else if (created_by_me) {
426
- const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
427
- const userId = data.authenticatedUser.id;
428
- gitPullRequestSearchCriteria.creatorId = userId;
443
+ else if (project) {
444
+ // If only project is provided, use getPullRequestsByProject
445
+ pullRequests = await gitApi.getPullRequestsByProject(project, searchCriteria, undefined, // maxCommentLength
446
+ skip, top);
429
447
  }
430
- if (user_is_reviewer) {
431
- try {
432
- const reviewerUserId = await getUserIdFromEmail(user_is_reviewer, tokenProvider, connectionProvider, userAgentProvider);
433
- gitPullRequestSearchCriteria.reviewerId = reviewerUserId;
434
- }
435
- catch (error) {
436
- return {
437
- content: [
438
- {
439
- type: "text",
440
- text: `Error finding reviewer with email ${user_is_reviewer}: ${error instanceof Error ? error.message : String(error)}`,
441
- },
442
- ],
443
- isError: true,
444
- };
445
- }
446
- }
447
- else if (i_am_reviewer) {
448
- const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
449
- const userId = data.authenticatedUser.id;
450
- gitPullRequestSearchCriteria.reviewerId = userId;
448
+ else {
449
+ // This case should not occur due to earlier validation, but added for completeness
450
+ return {
451
+ content: [
452
+ {
453
+ type: "text",
454
+ text: "Either repositoryId or project must be provided.",
455
+ },
456
+ ],
457
+ isError: true,
458
+ };
451
459
  }
452
- const pullRequests = await gitApi.getPullRequestsByProject(project, gitPullRequestSearchCriteria, undefined, // maxCommentLength
453
- skip, top);
454
- // Filter out the irrelevant properties
455
- const filteredPullRequests = pullRequests?.map((pr) => ({
456
- pullRequestId: pr.pullRequestId,
457
- codeReviewId: pr.codeReviewId,
458
- repository: pr.repository?.name,
459
- status: pr.status,
460
- createdBy: {
461
- displayName: pr.createdBy?.displayName,
462
- uniqueName: pr.createdBy?.uniqueName,
463
- },
464
- creationDate: pr.creationDate,
465
- title: pr.title,
466
- isDraft: pr.isDraft,
467
- sourceRefName: pr.sourceRefName,
468
- targetRefName: pr.targetRefName,
469
- }));
460
+ const filteredPullRequests = pullRequests?.map((pr) => trimPullRequest(pr));
470
461
  return {
471
462
  content: [{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) }],
472
463
  };
@@ -491,13 +482,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
491
482
  };
492
483
  }
493
484
  // Return trimmed thread data focusing on essential information
494
- const trimmedThreads = paginatedThreads?.map((thread) => ({
495
- id: thread.id,
496
- publishedDate: thread.publishedDate,
497
- lastUpdatedDate: thread.lastUpdatedDate,
498
- status: thread.status,
499
- comments: trimComments(thread.comments),
500
- }));
485
+ const trimmedThreads = paginatedThreads?.map((thread) => trimPullRequestThread(thread));
501
486
  return {
502
487
  content: [{ type: "text", text: JSON.stringify(trimmedThreads, null, 2) }],
503
488
  };
@@ -687,8 +672,9 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
687
672
  }
688
673
  }
689
674
  const thread = await gitApi.createThread({ comments: [{ content: content }], threadContext: threadContext, status: CommentThreadStatus[status] }, repositoryId, pullRequestId, project);
675
+ const trimmedThread = trimPullRequestThread(thread);
690
676
  return {
691
- content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
677
+ content: [{ type: "text", text: JSON.stringify(trimmedThread, null, 2) }],
692
678
  };
693
679
  });
694
680
  server.tool(REPO_TOOLS.resolve_comment, "Resolves a specific comment thread on a pull request.", {
@@ -4,6 +4,7 @@ import { z } from "zod";
4
4
  const Test_Plan_Tools = {
5
5
  create_test_plan: "testplan_create_test_plan",
6
6
  create_test_case: "testplan_create_test_case",
7
+ update_test_case_steps: "testplan_update_test_case_steps",
7
8
  add_test_cases_to_suite: "testplan_add_test_cases_to_suite",
8
9
  test_results_from_build_id: "testplan_show_test_results_from_build_id",
9
10
  list_test_cases: "testplan_list_test_cases",
@@ -11,10 +12,6 @@ const Test_Plan_Tools = {
11
12
  create_test_suite: "testplan_create_test_suite",
12
13
  };
13
14
  function configureTestPlanTools(server, _, connectionProvider) {
14
- /*
15
- LIST OF TEST PLANS
16
- get list of test plans by project
17
- */
18
15
  server.tool(Test_Plan_Tools.list_test_plans, "Retrieve a paginated list of test plans from an Azure DevOps project. Allows filtering for active plans and toggling detailed information.", {
19
16
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
20
17
  filterActivePlans: z.boolean().default(true).describe("Filter to include only active test plans. Defaults to true."),
@@ -29,9 +26,6 @@ function configureTestPlanTools(server, _, connectionProvider) {
29
26
  content: [{ type: "text", text: JSON.stringify(testPlans, null, 2) }],
30
27
  };
31
28
  });
32
- /*
33
- Create Test Plan - CREATE
34
- */
35
29
  server.tool(Test_Plan_Tools.create_test_plan, "Creates a new test plan in the project.", {
36
30
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project where the test plan will be created."),
37
31
  name: z.string().describe("The name of the test plan to be created."),
@@ -56,9 +50,6 @@ function configureTestPlanTools(server, _, connectionProvider) {
56
50
  content: [{ type: "text", text: JSON.stringify(createdTestPlan, null, 2) }],
57
51
  };
58
52
  });
59
- /*
60
- Create Test Suite - CREATE
61
- */
62
53
  server.tool(Test_Plan_Tools.create_test_suite, "Creates a new test suite in a test plan.", {
63
54
  project: z.string().describe("Project ID or project name"),
64
55
  planId: z.number().describe("ID of the test plan that contains the suites"),
@@ -80,9 +71,6 @@ function configureTestPlanTools(server, _, connectionProvider) {
80
71
  content: [{ type: "text", text: JSON.stringify(createdTestSuite, null, 2) }],
81
72
  };
82
73
  });
83
- /*
84
- Add Test Cases to Suite - ADD
85
- */
86
74
  server.tool(Test_Plan_Tools.add_test_cases_to_suite, "Adds existing test cases to a test suite.", {
87
75
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
88
76
  planId: z.number().describe("The ID of the test plan."),
@@ -98,9 +86,6 @@ function configureTestPlanTools(server, _, connectionProvider) {
98
86
  content: [{ type: "text", text: JSON.stringify(addedTestCases, null, 2) }],
99
87
  };
100
88
  });
101
- /*
102
- Create Test Case - CREATE
103
- */
104
89
  server.tool(Test_Plan_Tools.create_test_case, "Creates a new test case work item.", {
105
90
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
106
91
  title: z.string().describe("The title of the test case."),
@@ -111,7 +96,8 @@ function configureTestPlanTools(server, _, connectionProvider) {
111
96
  priority: z.number().optional().describe("The priority of the test case."),
112
97
  areaPath: z.string().optional().describe("The area path for the test case."),
113
98
  iterationPath: z.string().optional().describe("The iteration path for the test case."),
114
- }, async ({ project, title, steps, priority, areaPath, iterationPath }) => {
99
+ testsWorkItemId: z.number().optional().describe("Optional work item id that will be set as a Microsoft.VSTS.Common.TestedBy-Reverse link to the test case."),
100
+ }, async ({ project, title, steps, priority, areaPath, iterationPath, testsWorkItemId }) => {
115
101
  const connection = await connectionProvider();
116
102
  const witClient = await connection.getWorkItemTrackingApi();
117
103
  let stepsXml;
@@ -125,6 +111,16 @@ function configureTestPlanTools(server, _, connectionProvider) {
125
111
  path: "/fields/System.Title",
126
112
  value: title,
127
113
  });
114
+ if (testsWorkItemId) {
115
+ patchDocument.push({
116
+ op: "add",
117
+ path: "/relations/-",
118
+ value: {
119
+ rel: "Microsoft.VSTS.Common.TestedBy-Reverse",
120
+ url: `${connection.serverUrl}/${project}/_apis/wit/workItems/${testsWorkItemId}`,
121
+ },
122
+ });
123
+ }
128
124
  if (stepsXml) {
129
125
  patchDocument.push({
130
126
  op: "add",
@@ -158,10 +154,32 @@ function configureTestPlanTools(server, _, connectionProvider) {
158
154
  content: [{ type: "text", text: JSON.stringify(workItem, null, 2) }],
159
155
  };
160
156
  });
161
- /*
162
- TEST PLANS
163
- Gets a list of test cases for a given testplan.
164
- */
157
+ server.tool(Test_Plan_Tools.update_test_case_steps, "Update an existing test case work item.", {
158
+ id: z.number().describe("The ID of the test case work item to update."),
159
+ steps: z
160
+ .string()
161
+ .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."),
162
+ }, async ({ id, steps }) => {
163
+ const connection = await connectionProvider();
164
+ const witClient = await connection.getWorkItemTrackingApi();
165
+ let stepsXml;
166
+ if (steps) {
167
+ stepsXml = convertStepsToXml(steps);
168
+ }
169
+ // Create JSON patch document for work item
170
+ const patchDocument = [];
171
+ if (stepsXml) {
172
+ patchDocument.push({
173
+ op: "add",
174
+ path: "/fields/Microsoft.VSTS.TCM.Steps",
175
+ value: stepsXml,
176
+ });
177
+ }
178
+ const workItem = await witClient.updateWorkItem({}, patchDocument, id);
179
+ return {
180
+ content: [{ type: "text", text: JSON.stringify(workItem, null, 2) }],
181
+ };
182
+ });
165
183
  server.tool(Test_Plan_Tools.list_test_cases, "Gets a list of test cases in the test plan.", {
166
184
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
167
185
  planid: z.number().describe("The ID of the test plan."),
@@ -174,9 +192,6 @@ function configureTestPlanTools(server, _, connectionProvider) {
174
192
  content: [{ type: "text", text: JSON.stringify(testcases, null, 2) }],
175
193
  };
176
194
  });
177
- /*
178
- Gets a list of test results for a given project and build ID
179
- */
180
195
  server.tool(Test_Plan_Tools.test_results_from_build_id, "Gets a list of test results for a given project and build ID.", {
181
196
  project: z.string().describe("The unique identifier (ID or name) of the Azure DevOps project."),
182
197
  buildid: z.number().describe("The ID of the build."),
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "2.2.0";
1
+ export const packageVersion = "2.2.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "MCP server for interacting with Azure DevOps",
5
5
  "license": "MIT",
6
6
  "author": "Microsoft Corporation",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@azure/identity": "^4.10.0",
41
- "@modelcontextprotocol/sdk": "1.17.0",
41
+ "@modelcontextprotocol/sdk": "1.20.0",
42
42
  "azure-devops-extension-api": "^4.252.0",
43
43
  "azure-devops-extension-sdk": "^4.0.2",
44
44
  "azure-devops-node-api": "^15.1.0",
@@ -47,7 +47,7 @@
47
47
  "zod-to-json-schema": "^3.24.5"
48
48
  },
49
49
  "devDependencies": {
50
- "@modelcontextprotocol/inspector": "^0.16.1",
50
+ "@modelcontextprotocol/inspector": "^0.17.0",
51
51
  "@types/jest": "^30.0.0",
52
52
  "@types/node": "^22",
53
53
  "eslint-config-prettier": "10.1.8",
@@ -59,7 +59,7 @@
59
59
  "shx": "^0.4.0",
60
60
  "ts-jest": "^29.4.0",
61
61
  "tsconfig-paths": "^4.2.0",
62
- "typescript": "^5.8.3",
63
- "typescript-eslint": "^8.32.1"
62
+ "typescript": "^5.9.3",
63
+ "typescript-eslint": "^8.45.0"
64
64
  }
65
65
  }