@azure-devops/mcp 2.2.2 → 2.3.0-nightly.20251204

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.
@@ -20,7 +20,7 @@ const REPO_TOOLS = {
20
20
  update_pull_request_reviewers: "repo_update_pull_request_reviewers",
21
21
  reply_to_comment: "repo_reply_to_comment",
22
22
  create_pull_request_thread: "repo_create_pull_request_thread",
23
- resolve_comment: "repo_resolve_comment",
23
+ update_pull_request_thread: "repo_update_pull_request_thread",
24
24
  search_commits: "repo_search_commits",
25
25
  list_pull_requests_by_commits: "repo_list_pull_requests_by_commits",
26
26
  };
@@ -94,6 +94,7 @@ function trimPullRequest(pr, includeDescription = false) {
94
94
  uniqueName: pr.createdBy?.uniqueName,
95
95
  },
96
96
  creationDate: pr.creationDate,
97
+ closedDate: pr.closedDate,
97
98
  title: pr.title,
98
99
  ...(includeDescription ? { description: pr.description ?? "" } : {}),
99
100
  isDraft: pr.isDraft,
@@ -111,30 +112,42 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
111
112
  isDraft: z.boolean().optional().default(false).describe("Indicates whether the pull request is a draft. Defaults to false."),
112
113
  workItems: z.string().optional().describe("Work item IDs to associate with the pull request, space-separated."),
113
114
  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."),
114
- }, async ({ repositoryId, sourceRefName, targetRefName, title, description, isDraft, workItems, forkSourceRepositoryId }) => {
115
- const connection = await connectionProvider();
116
- const gitApi = await connection.getGitApi();
117
- const workItemRefs = workItems ? workItems.split(" ").map((id) => ({ id: id.trim() })) : [];
118
- const forkSource = forkSourceRepositoryId
119
- ? {
120
- repository: {
121
- id: forkSourceRepositoryId,
122
- },
123
- }
124
- : undefined;
125
- const pullRequest = await gitApi.createPullRequest({
126
- sourceRefName,
127
- targetRefName,
128
- title,
129
- description,
130
- isDraft,
131
- workItemRefs: workItemRefs,
132
- forkSource,
133
- }, repositoryId);
134
- const trimmedPullRequest = trimPullRequest(pullRequest, true);
135
- return {
136
- content: [{ type: "text", text: JSON.stringify(trimmedPullRequest, null, 2) }],
137
- };
115
+ labels: z.array(z.string()).optional().describe("Array of label names to add to the pull request after creation."),
116
+ }, async ({ repositoryId, sourceRefName, targetRefName, title, description, isDraft, workItems, forkSourceRepositoryId, labels }) => {
117
+ try {
118
+ const connection = await connectionProvider();
119
+ const gitApi = await connection.getGitApi();
120
+ const workItemRefs = workItems ? workItems.split(" ").map((id) => ({ id: id.trim() })) : [];
121
+ const forkSource = forkSourceRepositoryId
122
+ ? {
123
+ repository: {
124
+ id: forkSourceRepositoryId,
125
+ },
126
+ }
127
+ : undefined;
128
+ const labelDefinitions = labels ? labels.map((label) => ({ name: label })) : undefined;
129
+ const pullRequest = await gitApi.createPullRequest({
130
+ sourceRefName,
131
+ targetRefName,
132
+ title,
133
+ description,
134
+ isDraft,
135
+ workItemRefs: workItemRefs,
136
+ forkSource,
137
+ labels: labelDefinitions,
138
+ }, repositoryId);
139
+ const trimmedPullRequest = trimPullRequest(pullRequest, true);
140
+ return {
141
+ content: [{ type: "text", text: JSON.stringify(trimmedPullRequest, null, 2) }],
142
+ };
143
+ }
144
+ catch (error) {
145
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
146
+ return {
147
+ content: [{ type: "text", text: `Error creating pull request: ${errorMessage}` }],
148
+ isError: true,
149
+ };
150
+ }
138
151
  });
139
152
  server.tool(REPO_TOOLS.create_branch, "Create a new branch in the repository.", {
140
153
  repositoryId: z.string().describe("The ID of the repository where the branch will be created."),
@@ -142,67 +155,80 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
142
155
  sourceBranchName: z.string().optional().default("main").describe("The name of the source branch to create the new branch from. Defaults to 'main'."),
143
156
  sourceCommitId: z.string().optional().describe("The commit ID to create the branch from. If not provided, uses the latest commit of the source branch."),
144
157
  }, async ({ repositoryId, branchName, sourceBranchName, sourceCommitId }) => {
145
- const connection = await connectionProvider();
146
- const gitApi = await connection.getGitApi();
147
- let commitId = sourceCommitId;
148
- // If no commit ID is provided, get the latest commit from the source branch
149
- if (!commitId) {
150
- const sourceRefName = `refs/heads/${sourceBranchName}`;
158
+ try {
159
+ const connection = await connectionProvider();
160
+ const gitApi = await connection.getGitApi();
161
+ let commitId = sourceCommitId;
162
+ // If no commit ID is provided, get the latest commit from the source branch
163
+ if (!commitId) {
164
+ const sourceRefName = `refs/heads/${sourceBranchName}`;
165
+ try {
166
+ const sourceBranch = await gitApi.getRefs(repositoryId, undefined, "heads/", false, false, undefined, false, undefined, sourceBranchName);
167
+ const branch = sourceBranch.find((b) => b.name === sourceRefName);
168
+ if (!branch || !branch.objectId) {
169
+ return {
170
+ content: [
171
+ {
172
+ type: "text",
173
+ text: `Error: Source branch '${sourceBranchName}' not found in repository ${repositoryId}`,
174
+ },
175
+ ],
176
+ isError: true,
177
+ };
178
+ }
179
+ commitId = branch.objectId;
180
+ }
181
+ catch (error) {
182
+ return {
183
+ content: [
184
+ {
185
+ type: "text",
186
+ text: `Error retrieving source branch '${sourceBranchName}': ${error instanceof Error ? error.message : String(error)}`,
187
+ },
188
+ ],
189
+ isError: true,
190
+ };
191
+ }
192
+ }
193
+ // Create the new branch using updateRefs
194
+ const newRefName = `refs/heads/${branchName}`;
195
+ const refUpdate = {
196
+ name: newRefName,
197
+ newObjectId: commitId,
198
+ oldObjectId: "0000000000000000000000000000000000000000", // All zeros indicates creating a new ref
199
+ };
151
200
  try {
152
- const sourceBranch = await gitApi.getRefs(repositoryId, undefined, "heads/", false, false, undefined, false, undefined, sourceBranchName);
153
- const branch = sourceBranch.find((b) => b.name === sourceRefName);
154
- if (!branch || !branch.objectId) {
201
+ const result = await gitApi.updateRefs([refUpdate], repositoryId);
202
+ // Check if the branch creation was successful
203
+ if (result && result.length > 0 && result[0].success) {
204
+ return {
205
+ content: [
206
+ {
207
+ type: "text",
208
+ text: `Branch '${branchName}' created successfully from '${sourceBranchName}' (${commitId})`,
209
+ },
210
+ ],
211
+ };
212
+ }
213
+ else {
214
+ const errorMessage = result && result.length > 0 && result[0].customMessage ? result[0].customMessage : "Unknown error occurred during branch creation";
155
215
  return {
156
216
  content: [
157
217
  {
158
218
  type: "text",
159
- text: `Error: Source branch '${sourceBranchName}' not found in repository ${repositoryId}`,
219
+ text: `Error creating branch '${branchName}': ${errorMessage}`,
160
220
  },
161
221
  ],
162
222
  isError: true,
163
223
  };
164
224
  }
165
- commitId = branch.objectId;
166
225
  }
167
226
  catch (error) {
168
227
  return {
169
228
  content: [
170
229
  {
171
230
  type: "text",
172
- text: `Error retrieving source branch '${sourceBranchName}': ${error instanceof Error ? error.message : String(error)}`,
173
- },
174
- ],
175
- isError: true,
176
- };
177
- }
178
- }
179
- // Create the new branch using updateRefs
180
- const newRefName = `refs/heads/${branchName}`;
181
- const refUpdate = {
182
- name: newRefName,
183
- newObjectId: commitId,
184
- oldObjectId: "0000000000000000000000000000000000000000", // All zeros indicates creating a new ref
185
- };
186
- try {
187
- const result = await gitApi.updateRefs([refUpdate], repositoryId);
188
- // Check if the branch creation was successful
189
- if (result && result.length > 0 && result[0].success) {
190
- return {
191
- content: [
192
- {
193
- type: "text",
194
- text: `Branch '${branchName}' created successfully from '${sourceBranchName}' (${commitId})`,
195
- },
196
- ],
197
- };
198
- }
199
- else {
200
- const errorMessage = result && result.length > 0 && result[0].customMessage ? result[0].customMessage : "Unknown error occurred during branch creation";
201
- return {
202
- content: [
203
- {
204
- type: "text",
205
- text: `Error creating branch '${branchName}': ${errorMessage}`,
231
+ text: `Error creating branch '${branchName}': ${error instanceof Error ? error.message : String(error)}`,
206
232
  },
207
233
  ],
208
234
  isError: true,
@@ -210,13 +236,9 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
210
236
  }
211
237
  }
212
238
  catch (error) {
239
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
213
240
  return {
214
- content: [
215
- {
216
- type: "text",
217
- text: `Error creating branch '${branchName}': ${error instanceof Error ? error.message : String(error)}`,
218
- },
219
- ],
241
+ content: [{ type: "text", text: `Error creating branch: ${errorMessage}` }],
220
242
  isError: true,
221
243
  };
222
244
  }
@@ -238,56 +260,65 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
238
260
  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."),
239
261
  bypassReason: z.string().optional().describe("Reason for bypassing branch policies. When provided, branch policies will be automatically bypassed during autocompletion."),
240
262
  }, async ({ repositoryId, pullRequestId, title, description, isDraft, targetRefName, status, autoComplete, mergeStrategy, deleteSourceBranch, transitionWorkItems, bypassReason }) => {
241
- const connection = await connectionProvider();
242
- const gitApi = await connection.getGitApi();
243
- // Build update object with only provided fields
244
- const updateRequest = {};
245
- if (title !== undefined)
246
- updateRequest.title = title;
247
- if (description !== undefined)
248
- updateRequest.description = description;
249
- if (isDraft !== undefined)
250
- updateRequest.isDraft = isDraft;
251
- if (targetRefName !== undefined)
252
- updateRequest.targetRefName = targetRefName;
253
- if (status !== undefined) {
254
- updateRequest.status = status === "Active" ? PullRequestStatus.Active.valueOf() : PullRequestStatus.Abandoned.valueOf();
255
- }
256
- if (autoComplete !== undefined) {
257
- if (autoComplete) {
258
- const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
259
- const autoCompleteUserId = data.authenticatedUser.id;
260
- updateRequest.autoCompleteSetBy = { id: autoCompleteUserId };
261
- const completionOptions = {
262
- deleteSourceBranch: deleteSourceBranch || false,
263
- transitionWorkItems: transitionWorkItems !== false, // Default to true unless explicitly set to false
264
- bypassPolicy: !!bypassReason, // Automatically set to true if bypassReason is provided
265
- };
266
- if (mergeStrategy) {
267
- completionOptions.mergeStrategy = GitPullRequestMergeStrategy[mergeStrategy];
263
+ try {
264
+ const connection = await connectionProvider();
265
+ const gitApi = await connection.getGitApi();
266
+ // Build update object with only provided fields
267
+ const updateRequest = {};
268
+ if (title !== undefined)
269
+ updateRequest.title = title;
270
+ if (description !== undefined)
271
+ updateRequest.description = description;
272
+ if (isDraft !== undefined)
273
+ updateRequest.isDraft = isDraft;
274
+ if (targetRefName !== undefined)
275
+ updateRequest.targetRefName = targetRefName;
276
+ if (status !== undefined) {
277
+ updateRequest.status = status === "Active" ? PullRequestStatus.Active.valueOf() : PullRequestStatus.Abandoned.valueOf();
278
+ }
279
+ if (autoComplete !== undefined) {
280
+ if (autoComplete) {
281
+ const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
282
+ const autoCompleteUserId = data.authenticatedUser.id;
283
+ updateRequest.autoCompleteSetBy = { id: autoCompleteUserId };
284
+ const completionOptions = {
285
+ deleteSourceBranch: deleteSourceBranch || false,
286
+ transitionWorkItems: transitionWorkItems !== false, // Default to true unless explicitly set to false
287
+ bypassPolicy: !!bypassReason, // Automatically set to true if bypassReason is provided
288
+ };
289
+ if (mergeStrategy) {
290
+ completionOptions.mergeStrategy = GitPullRequestMergeStrategy[mergeStrategy];
291
+ }
292
+ if (bypassReason) {
293
+ completionOptions.bypassReason = bypassReason;
294
+ }
295
+ updateRequest.completionOptions = completionOptions;
268
296
  }
269
- if (bypassReason) {
270
- completionOptions.bypassReason = bypassReason;
297
+ else {
298
+ updateRequest.autoCompleteSetBy = null;
299
+ updateRequest.completionOptions = null;
271
300
  }
272
- updateRequest.completionOptions = completionOptions;
273
301
  }
274
- else {
275
- updateRequest.autoCompleteSetBy = null;
276
- updateRequest.completionOptions = null;
302
+ // Validate that at least one field is provided for update
303
+ if (Object.keys(updateRequest).length === 0) {
304
+ return {
305
+ content: [{ type: "text", text: "Error: At least one field (title, description, isDraft, targetRefName, status, or autoComplete options) must be provided for update." }],
306
+ isError: true,
307
+ };
277
308
  }
309
+ const updatedPullRequest = await gitApi.updatePullRequest(updateRequest, repositoryId, pullRequestId);
310
+ const trimmedUpdatedPullRequest = trimPullRequest(updatedPullRequest, true);
311
+ return {
312
+ content: [{ type: "text", text: JSON.stringify(trimmedUpdatedPullRequest, null, 2) }],
313
+ };
278
314
  }
279
- // Validate that at least one field is provided for update
280
- if (Object.keys(updateRequest).length === 0) {
315
+ catch (error) {
316
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
281
317
  return {
282
- content: [{ type: "text", text: "Error: At least one field (title, description, isDraft, targetRefName, status, or autoComplete options) must be provided for update." }],
318
+ content: [{ type: "text", text: `Error updating pull request: ${errorMessage}` }],
283
319
  isError: true,
284
320
  };
285
321
  }
286
- const updatedPullRequest = await gitApi.updatePullRequest(updateRequest, repositoryId, pullRequestId);
287
- const trimmedUpdatedPullRequest = trimPullRequest(updatedPullRequest, true);
288
- return {
289
- content: [{ type: "text", text: JSON.stringify(trimmedUpdatedPullRequest, null, 2) }],
290
- };
291
322
  });
292
323
  server.tool(REPO_TOOLS.update_pull_request_reviewers, "Add or remove reviewers for an existing pull request.", {
293
324
  repositoryId: z.string().describe("The ID of the repository where the pull request exists."),
@@ -295,29 +326,38 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
295
326
  reviewerIds: z.array(z.string()).describe("List of reviewer ids to add or remove from the pull request."),
296
327
  action: z.enum(["add", "remove"]).describe("Action to perform on the reviewers. Can be 'add' or 'remove'."),
297
328
  }, async ({ repositoryId, pullRequestId, reviewerIds, action }) => {
298
- const connection = await connectionProvider();
299
- const gitApi = await connection.getGitApi();
300
- let updatedPullRequest;
301
- if (action === "add") {
302
- updatedPullRequest = await gitApi.createPullRequestReviewers(reviewerIds.map((id) => ({ id: id })), repositoryId, pullRequestId);
303
- const trimmedResponse = updatedPullRequest.map((item) => ({
304
- displayName: item.displayName,
305
- id: item.id,
306
- uniqueName: item.uniqueName,
307
- vote: item.vote,
308
- hasDeclined: item.hasDeclined,
309
- isFlagged: item.isFlagged,
310
- }));
311
- return {
312
- content: [{ type: "text", text: JSON.stringify(trimmedResponse, null, 2) }],
313
- };
314
- }
315
- else {
316
- for (const reviewerId of reviewerIds) {
317
- await gitApi.deletePullRequestReviewer(repositoryId, pullRequestId, reviewerId);
329
+ try {
330
+ const connection = await connectionProvider();
331
+ const gitApi = await connection.getGitApi();
332
+ let updatedPullRequest;
333
+ if (action === "add") {
334
+ updatedPullRequest = await gitApi.createPullRequestReviewers(reviewerIds.map((id) => ({ id: id })), repositoryId, pullRequestId);
335
+ const trimmedResponse = updatedPullRequest.map((item) => ({
336
+ displayName: item.displayName,
337
+ id: item.id,
338
+ uniqueName: item.uniqueName,
339
+ vote: item.vote,
340
+ hasDeclined: item.hasDeclined,
341
+ isFlagged: item.isFlagged,
342
+ }));
343
+ return {
344
+ content: [{ type: "text", text: JSON.stringify(trimmedResponse, null, 2) }],
345
+ };
346
+ }
347
+ else {
348
+ for (const reviewerId of reviewerIds) {
349
+ await gitApi.deletePullRequestReviewer(repositoryId, pullRequestId, reviewerId);
350
+ }
351
+ return {
352
+ content: [{ type: "text", text: `Reviewers with IDs ${reviewerIds.join(", ")} removed from pull request ${pullRequestId}.` }],
353
+ };
318
354
  }
355
+ }
356
+ catch (error) {
357
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
319
358
  return {
320
- content: [{ type: "text", text: `Reviewers with IDs ${reviewerIds.join(", ")} removed from pull request ${pullRequestId}.` }],
359
+ content: [{ type: "text", text: `Error updating pull request reviewers: ${errorMessage}` }],
360
+ isError: true,
321
361
  };
322
362
  }
323
363
  });
@@ -327,24 +367,33 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
327
367
  skip: z.number().default(0).describe("The number of repositories to skip. Defaults to 0."),
328
368
  repoNameFilter: z.string().optional().describe("Optional filter to search for repositories by name. If provided, only repositories with names containing this string will be returned."),
329
369
  }, async ({ project, top, skip, repoNameFilter }) => {
330
- const connection = await connectionProvider();
331
- const gitApi = await connection.getGitApi();
332
- const repositories = await gitApi.getRepositories(project, false, false, false);
333
- const filteredRepositories = repoNameFilter ? filterReposByName(repositories, repoNameFilter) : repositories;
334
- const paginatedRepositories = filteredRepositories?.sort((a, b) => a.name?.localeCompare(b.name ?? "") ?? 0).slice(skip, skip + top);
335
- // Filter out the irrelevant properties
336
- const trimmedRepositories = paginatedRepositories?.map((repo) => ({
337
- id: repo.id,
338
- name: repo.name,
339
- isDisabled: repo.isDisabled,
340
- isFork: repo.isFork,
341
- isInMaintenance: repo.isInMaintenance,
342
- webUrl: repo.webUrl,
343
- size: repo.size,
344
- }));
345
- return {
346
- content: [{ type: "text", text: JSON.stringify(trimmedRepositories, null, 2) }],
347
- };
370
+ try {
371
+ const connection = await connectionProvider();
372
+ const gitApi = await connection.getGitApi();
373
+ const repositories = await gitApi.getRepositories(project, false, false, false);
374
+ const filteredRepositories = repoNameFilter ? filterReposByName(repositories, repoNameFilter) : repositories;
375
+ const paginatedRepositories = filteredRepositories?.sort((a, b) => a.name?.localeCompare(b.name ?? "") ?? 0).slice(skip, skip + top);
376
+ // Filter out the irrelevant properties
377
+ const trimmedRepositories = paginatedRepositories?.map((repo) => ({
378
+ id: repo.id,
379
+ name: repo.name,
380
+ isDisabled: repo.isDisabled,
381
+ isFork: repo.isFork,
382
+ isInMaintenance: repo.isInMaintenance,
383
+ webUrl: repo.webUrl,
384
+ size: repo.size,
385
+ }));
386
+ return {
387
+ content: [{ type: "text", text: JSON.stringify(trimmedRepositories, null, 2) }],
388
+ };
389
+ }
390
+ catch (error) {
391
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
392
+ return {
393
+ content: [{ type: "text", text: `Error listing repositories: ${errorMessage}` }],
394
+ isError: true,
395
+ };
396
+ }
348
397
  });
349
398
  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.", {
350
399
  repositoryId: z.string().optional().describe("The ID of the repository where the pull requests are located."),
@@ -365,103 +414,112 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
365
414
  sourceRefName: z.string().optional().describe("Filter pull requests from this source branch (e.g., 'refs/heads/feature-branch')."),
366
415
  targetRefName: z.string().optional().describe("Filter pull requests into this target branch (e.g., 'refs/heads/main')."),
367
416
  }, async ({ repositoryId, project, top, skip, created_by_me, created_by_user, i_am_reviewer, user_is_reviewer, status, sourceRefName, targetRefName }) => {
368
- const connection = await connectionProvider();
369
- const gitApi = await connection.getGitApi();
370
- // Build the search criteria
371
- const searchCriteria = {
372
- status: pullRequestStatusStringToInt(status),
373
- };
374
- if (!repositoryId && !project) {
375
- return {
376
- content: [
377
- {
378
- type: "text",
379
- text: "Either repositoryId or project must be provided.",
380
- },
381
- ],
382
- isError: true,
417
+ try {
418
+ const connection = await connectionProvider();
419
+ const gitApi = await connection.getGitApi();
420
+ // Build the search criteria
421
+ const searchCriteria = {
422
+ status: pullRequestStatusStringToInt(status),
383
423
  };
384
- }
385
- if (repositoryId) {
386
- searchCriteria.repositoryId = repositoryId;
387
- }
388
- if (sourceRefName) {
389
- searchCriteria.sourceRefName = sourceRefName;
390
- }
391
- if (targetRefName) {
392
- searchCriteria.targetRefName = targetRefName;
393
- }
394
- if (created_by_user) {
395
- try {
396
- const userId = await getUserIdFromEmail(created_by_user, tokenProvider, connectionProvider, userAgentProvider);
397
- searchCriteria.creatorId = userId;
398
- }
399
- catch (error) {
424
+ if (!repositoryId && !project) {
400
425
  return {
401
426
  content: [
402
427
  {
403
428
  type: "text",
404
- text: `Error finding user with email ${created_by_user}: ${error instanceof Error ? error.message : String(error)}`,
429
+ text: "Either repositoryId or project must be provided.",
405
430
  },
406
431
  ],
407
432
  isError: true,
408
433
  };
409
434
  }
410
- }
411
- else if (created_by_me) {
412
- const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
413
- const userId = data.authenticatedUser.id;
414
- searchCriteria.creatorId = userId;
415
- }
416
- if (user_is_reviewer) {
417
- try {
418
- const reviewerUserId = await getUserIdFromEmail(user_is_reviewer, tokenProvider, connectionProvider, userAgentProvider);
419
- searchCriteria.reviewerId = reviewerUserId;
435
+ if (repositoryId) {
436
+ searchCriteria.repositoryId = repositoryId;
420
437
  }
421
- catch (error) {
438
+ if (sourceRefName) {
439
+ searchCriteria.sourceRefName = sourceRefName;
440
+ }
441
+ if (targetRefName) {
442
+ searchCriteria.targetRefName = targetRefName;
443
+ }
444
+ if (created_by_user) {
445
+ try {
446
+ const userId = await getUserIdFromEmail(created_by_user, tokenProvider, connectionProvider, userAgentProvider);
447
+ searchCriteria.creatorId = userId;
448
+ }
449
+ catch (error) {
450
+ return {
451
+ content: [
452
+ {
453
+ type: "text",
454
+ text: `Error finding user with email ${created_by_user}: ${error instanceof Error ? error.message : String(error)}`,
455
+ },
456
+ ],
457
+ isError: true,
458
+ };
459
+ }
460
+ }
461
+ else if (created_by_me) {
462
+ const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
463
+ const userId = data.authenticatedUser.id;
464
+ searchCriteria.creatorId = userId;
465
+ }
466
+ if (user_is_reviewer) {
467
+ try {
468
+ const reviewerUserId = await getUserIdFromEmail(user_is_reviewer, tokenProvider, connectionProvider, userAgentProvider);
469
+ searchCriteria.reviewerId = reviewerUserId;
470
+ }
471
+ catch (error) {
472
+ return {
473
+ content: [
474
+ {
475
+ type: "text",
476
+ text: `Error finding reviewer with email ${user_is_reviewer}: ${error instanceof Error ? error.message : String(error)}`,
477
+ },
478
+ ],
479
+ isError: true,
480
+ };
481
+ }
482
+ }
483
+ else if (i_am_reviewer) {
484
+ const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
485
+ const userId = data.authenticatedUser.id;
486
+ searchCriteria.reviewerId = userId;
487
+ }
488
+ let pullRequests;
489
+ if (repositoryId) {
490
+ pullRequests = await gitApi.getPullRequests(repositoryId, searchCriteria, project, // project
491
+ undefined, // maxCommentLength
492
+ skip, top);
493
+ }
494
+ else if (project) {
495
+ // If only project is provided, use getPullRequestsByProject
496
+ pullRequests = await gitApi.getPullRequestsByProject(project, searchCriteria, undefined, // maxCommentLength
497
+ skip, top);
498
+ }
499
+ else {
500
+ // This case should not occur due to earlier validation, but added for completeness
422
501
  return {
423
502
  content: [
424
503
  {
425
504
  type: "text",
426
- text: `Error finding reviewer with email ${user_is_reviewer}: ${error instanceof Error ? error.message : String(error)}`,
505
+ text: "Either repositoryId or project must be provided.",
427
506
  },
428
507
  ],
429
508
  isError: true,
430
509
  };
431
510
  }
511
+ const filteredPullRequests = pullRequests?.map((pr) => trimPullRequest(pr));
512
+ return {
513
+ content: [{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) }],
514
+ };
432
515
  }
433
- else if (i_am_reviewer) {
434
- const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
435
- const userId = data.authenticatedUser.id;
436
- searchCriteria.reviewerId = userId;
437
- }
438
- let pullRequests;
439
- if (repositoryId) {
440
- pullRequests = await gitApi.getPullRequests(repositoryId, searchCriteria, project, // project
441
- undefined, // maxCommentLength
442
- skip, top);
443
- }
444
- else if (project) {
445
- // If only project is provided, use getPullRequestsByProject
446
- pullRequests = await gitApi.getPullRequestsByProject(project, searchCriteria, undefined, // maxCommentLength
447
- skip, top);
448
- }
449
- else {
450
- // This case should not occur due to earlier validation, but added for completeness
516
+ catch (error) {
517
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
451
518
  return {
452
- content: [
453
- {
454
- type: "text",
455
- text: "Either repositoryId or project must be provided.",
456
- },
457
- ],
519
+ content: [{ type: "text", text: `Error listing pull requests: ${errorMessage}` }],
458
520
  isError: true,
459
521
  };
460
522
  }
461
- const filteredPullRequests = pullRequests?.map((pr) => trimPullRequest(pr));
462
- return {
463
- content: [{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) }],
464
- };
465
523
  });
466
524
  server.tool(REPO_TOOLS.list_pull_request_threads, "Retrieve a list of comment threads for a pull request.", {
467
525
  repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
@@ -469,24 +527,57 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
469
527
  project: z.string().optional().describe("Project ID or project name (optional)"),
470
528
  iteration: z.number().optional().describe("The iteration ID for which to retrieve threads. Optional, defaults to the latest iteration."),
471
529
  baseIteration: z.number().optional().describe("The base iteration ID for which to retrieve threads. Optional, defaults to the latest base iteration."),
472
- top: z.number().default(100).describe("The maximum number of threads to return."),
473
- skip: z.number().default(0).describe("The number of threads to skip."),
530
+ top: z.number().default(100).describe("The maximum number of threads to return after filtering."),
531
+ skip: z.number().default(0).describe("The number of threads to skip after filtering."),
474
532
  fullResponse: z.boolean().optional().default(false).describe("Return full thread JSON response instead of trimmed data."),
475
- }, async ({ repositoryId, pullRequestId, project, iteration, baseIteration, top, skip, fullResponse }) => {
476
- const connection = await connectionProvider();
477
- const gitApi = await connection.getGitApi();
478
- const threads = await gitApi.getThreads(repositoryId, pullRequestId, project, iteration, baseIteration);
479
- const paginatedThreads = threads?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
480
- if (fullResponse) {
533
+ status: z
534
+ .enum(getEnumKeys(CommentThreadStatus))
535
+ .optional()
536
+ .describe("Filter threads by status. If not specified, returns threads of all statuses."),
537
+ authorEmail: z.string().optional().describe("Filter threads by the email of the thread author (first comment author)."),
538
+ authorDisplayName: z.string().optional().describe("Filter threads by the display name of the thread author (first comment author). Case-insensitive partial matching."),
539
+ }, async ({ repositoryId, pullRequestId, project, iteration, baseIteration, top, skip, fullResponse, status, authorEmail, authorDisplayName }) => {
540
+ try {
541
+ const connection = await connectionProvider();
542
+ const gitApi = await connection.getGitApi();
543
+ const threads = await gitApi.getThreads(repositoryId, pullRequestId, project, iteration, baseIteration);
544
+ let filteredThreads = threads;
545
+ if (status !== undefined) {
546
+ const statusValue = CommentThreadStatus[status];
547
+ filteredThreads = filteredThreads?.filter((thread) => thread.status === statusValue);
548
+ }
549
+ if (authorEmail !== undefined) {
550
+ filteredThreads = filteredThreads?.filter((thread) => {
551
+ const firstComment = thread.comments?.[0];
552
+ return firstComment?.author?.uniqueName?.toLowerCase() === authorEmail.toLowerCase();
553
+ });
554
+ }
555
+ if (authorDisplayName !== undefined) {
556
+ const lowerAuthorName = authorDisplayName.toLowerCase();
557
+ filteredThreads = filteredThreads?.filter((thread) => {
558
+ const firstComment = thread.comments?.[0];
559
+ return firstComment?.author?.displayName?.toLowerCase().includes(lowerAuthorName);
560
+ });
561
+ }
562
+ const paginatedThreads = filteredThreads?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
563
+ if (fullResponse) {
564
+ return {
565
+ content: [{ type: "text", text: JSON.stringify(paginatedThreads, null, 2) }],
566
+ };
567
+ }
568
+ // Return trimmed thread data focusing on essential information
569
+ const trimmedThreads = paginatedThreads?.map((thread) => trimPullRequestThread(thread));
570
+ return {
571
+ content: [{ type: "text", text: JSON.stringify(trimmedThreads, null, 2) }],
572
+ };
573
+ }
574
+ catch (error) {
575
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
481
576
  return {
482
- content: [{ type: "text", text: JSON.stringify(paginatedThreads, null, 2) }],
577
+ content: [{ type: "text", text: `Error listing pull request threads: ${errorMessage}` }],
578
+ isError: true,
483
579
  };
484
580
  }
485
- // Return trimmed thread data focusing on essential information
486
- const trimmedThreads = paginatedThreads?.map((thread) => trimPullRequestThread(thread));
487
- return {
488
- content: [{ type: "text", text: JSON.stringify(trimmedThreads, null, 2) }],
489
- };
490
581
  });
491
582
  server.tool(REPO_TOOLS.list_pull_request_thread_comments, "Retrieve a list of comments in a pull request thread.", {
492
583
  repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
@@ -497,97 +588,184 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
497
588
  skip: z.number().default(0).describe("The number of comments to skip."),
498
589
  fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of trimmed data."),
499
590
  }, async ({ repositoryId, pullRequestId, threadId, project, top, skip, fullResponse }) => {
500
- const connection = await connectionProvider();
501
- const gitApi = await connection.getGitApi();
502
- // Get thread comments - GitApi uses getComments for retrieving comments from a specific thread
503
- const comments = await gitApi.getComments(repositoryId, pullRequestId, threadId, project);
504
- const paginatedComments = comments?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
505
- if (fullResponse) {
591
+ try {
592
+ const connection = await connectionProvider();
593
+ const gitApi = await connection.getGitApi();
594
+ // Get thread comments - GitApi uses getComments for retrieving comments from a specific thread
595
+ const comments = await gitApi.getComments(repositoryId, pullRequestId, threadId, project);
596
+ const paginatedComments = comments?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
597
+ if (fullResponse) {
598
+ return {
599
+ content: [{ type: "text", text: JSON.stringify(paginatedComments, null, 2) }],
600
+ };
601
+ }
602
+ // Return trimmed comment data focusing on essential information
603
+ const trimmedComments = trimComments(paginatedComments);
604
+ return {
605
+ content: [{ type: "text", text: JSON.stringify(trimmedComments, null, 2) }],
606
+ };
607
+ }
608
+ catch (error) {
609
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
506
610
  return {
507
- content: [{ type: "text", text: JSON.stringify(paginatedComments, null, 2) }],
611
+ content: [{ type: "text", text: `Error listing pull request thread comments: ${errorMessage}` }],
612
+ isError: true,
508
613
  };
509
614
  }
510
- // Return trimmed comment data focusing on essential information
511
- const trimmedComments = trimComments(paginatedComments);
512
- return {
513
- content: [{ type: "text", text: JSON.stringify(trimmedComments, null, 2) }],
514
- };
515
615
  });
516
616
  server.tool(REPO_TOOLS.list_branches_by_repo, "Retrieve a list of branches for a given repository.", {
517
617
  repositoryId: z.string().describe("The ID of the repository where the branches are located."),
518
618
  top: z.number().default(100).describe("The maximum number of branches to return. Defaults to 100."),
519
619
  filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
520
620
  }, async ({ repositoryId, top, filterContains }) => {
521
- const connection = await connectionProvider();
522
- const gitApi = await connection.getGitApi();
523
- const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", undefined, undefined, undefined, undefined, undefined, filterContains);
524
- const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
525
- return {
526
- content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
527
- };
621
+ try {
622
+ const connection = await connectionProvider();
623
+ const gitApi = await connection.getGitApi();
624
+ const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", undefined, undefined, undefined, undefined, undefined, filterContains);
625
+ const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
626
+ return {
627
+ content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
628
+ };
629
+ }
630
+ catch (error) {
631
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
632
+ return {
633
+ content: [{ type: "text", text: `Error listing branches: ${errorMessage}` }],
634
+ isError: true,
635
+ };
636
+ }
528
637
  });
529
638
  server.tool(REPO_TOOLS.list_my_branches_by_repo, "Retrieve a list of my branches for a given repository Id.", {
530
639
  repositoryId: z.string().describe("The ID of the repository where the branches are located."),
531
640
  top: z.number().default(100).describe("The maximum number of branches to return."),
532
641
  filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
533
642
  }, async ({ repositoryId, top, filterContains }) => {
534
- const connection = await connectionProvider();
535
- const gitApi = await connection.getGitApi();
536
- const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", undefined, undefined, true, undefined, undefined, filterContains);
537
- const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
538
- return {
539
- content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
540
- };
643
+ try {
644
+ const connection = await connectionProvider();
645
+ const gitApi = await connection.getGitApi();
646
+ const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", undefined, undefined, true, undefined, undefined, filterContains);
647
+ const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
648
+ return {
649
+ content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
650
+ };
651
+ }
652
+ catch (error) {
653
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
654
+ return {
655
+ content: [{ type: "text", text: `Error listing my branches: ${errorMessage}` }],
656
+ isError: true,
657
+ };
658
+ }
541
659
  });
542
660
  server.tool(REPO_TOOLS.get_repo_by_name_or_id, "Get the repository by project and repository name or ID.", {
543
661
  project: z.string().describe("Project name or ID where the repository is located."),
544
662
  repositoryNameOrId: z.string().describe("Repository name or ID."),
545
663
  }, async ({ project, repositoryNameOrId }) => {
546
- const connection = await connectionProvider();
547
- const gitApi = await connection.getGitApi();
548
- const repositories = await gitApi.getRepositories(project);
549
- const repository = repositories?.find((repo) => repo.name === repositoryNameOrId || repo.id === repositoryNameOrId);
550
- if (!repository) {
551
- throw new Error(`Repository ${repositoryNameOrId} not found in project ${project}`);
552
- }
553
- return {
554
- content: [{ type: "text", text: JSON.stringify(repository, null, 2) }],
555
- };
664
+ try {
665
+ const connection = await connectionProvider();
666
+ const gitApi = await connection.getGitApi();
667
+ const repositories = await gitApi.getRepositories(project);
668
+ const repository = repositories?.find((repo) => repo.name === repositoryNameOrId || repo.id === repositoryNameOrId);
669
+ if (!repository) {
670
+ return {
671
+ content: [{ type: "text", text: `Repository ${repositoryNameOrId} not found in project ${project}` }],
672
+ isError: true,
673
+ };
674
+ }
675
+ return {
676
+ content: [{ type: "text", text: JSON.stringify(repository, null, 2) }],
677
+ };
678
+ }
679
+ catch (error) {
680
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
681
+ return {
682
+ content: [{ type: "text", text: `Error getting repository: ${errorMessage}` }],
683
+ isError: true,
684
+ };
685
+ }
556
686
  });
557
687
  server.tool(REPO_TOOLS.get_branch_by_name, "Get a branch by its name.", {
558
688
  repositoryId: z.string().describe("The ID of the repository where the branch is located."),
559
689
  branchName: z.string().describe("The name of the branch to retrieve, e.g., 'main' or 'feature-branch'."),
560
690
  }, async ({ repositoryId, branchName }) => {
561
- const connection = await connectionProvider();
562
- const gitApi = await connection.getGitApi();
563
- const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", false, false, undefined, false, undefined, branchName);
564
- const branch = branches.find((branch) => branch.name === `refs/heads/${branchName}` || branch.name === branchName);
565
- if (!branch) {
691
+ try {
692
+ const connection = await connectionProvider();
693
+ const gitApi = await connection.getGitApi();
694
+ const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", false, false, undefined, false, undefined, branchName);
695
+ const branch = branches.find((branch) => branch.name === `refs/heads/${branchName}` || branch.name === branchName);
696
+ if (!branch) {
697
+ return {
698
+ content: [
699
+ {
700
+ type: "text",
701
+ text: `Branch ${branchName} not found in repository ${repositoryId}`,
702
+ },
703
+ ],
704
+ isError: true,
705
+ };
706
+ }
566
707
  return {
567
- content: [
568
- {
569
- type: "text",
570
- text: `Branch ${branchName} not found in repository ${repositoryId}`,
571
- },
572
- ],
708
+ content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
709
+ };
710
+ }
711
+ catch (error) {
712
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
713
+ return {
714
+ content: [{ type: "text", text: `Error getting branch: ${errorMessage}` }],
573
715
  isError: true,
574
716
  };
575
717
  }
576
- return {
577
- content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
578
- };
579
718
  });
580
719
  server.tool(REPO_TOOLS.get_pull_request_by_id, "Get a pull request by its ID.", {
581
720
  repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
582
721
  pullRequestId: z.number().describe("The ID of the pull request to retrieve."),
583
722
  includeWorkItemRefs: z.boolean().optional().default(false).describe("Whether to reference work items associated with the pull request."),
584
- }, async ({ repositoryId, pullRequestId, includeWorkItemRefs }) => {
585
- const connection = await connectionProvider();
586
- const gitApi = await connection.getGitApi();
587
- const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId, undefined, undefined, undefined, undefined, undefined, includeWorkItemRefs);
588
- return {
589
- content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }],
590
- };
723
+ includeLabels: z.boolean().optional().default(false).describe("Whether to include a summary of labels in the response."),
724
+ }, async ({ repositoryId, pullRequestId, includeWorkItemRefs, includeLabels }) => {
725
+ try {
726
+ const connection = await connectionProvider();
727
+ const gitApi = await connection.getGitApi();
728
+ const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId, undefined, undefined, undefined, undefined, undefined, includeWorkItemRefs);
729
+ if (includeLabels) {
730
+ try {
731
+ const projectId = pullRequest.repository?.project?.id;
732
+ const projectName = pullRequest.repository?.project?.name;
733
+ const labels = await gitApi.getPullRequestLabels(repositoryId, pullRequestId, projectName, projectId);
734
+ const labelNames = labels.map((label) => label.name).filter((name) => name !== undefined);
735
+ const enhancedResponse = {
736
+ ...pullRequest,
737
+ labelSummary: {
738
+ labels: labelNames,
739
+ labelCount: labelNames.length,
740
+ },
741
+ };
742
+ return {
743
+ content: [{ type: "text", text: JSON.stringify(enhancedResponse, null, 2) }],
744
+ };
745
+ }
746
+ catch (error) {
747
+ console.warn(`Error fetching PR labels: ${error instanceof Error ? error.message : "Unknown error"}`);
748
+ // Fall back to the original response without labels
749
+ const enhancedResponse = {
750
+ ...pullRequest,
751
+ labelSummary: {},
752
+ };
753
+ return {
754
+ content: [{ type: "text", text: JSON.stringify(enhancedResponse, null, 2) }],
755
+ };
756
+ }
757
+ }
758
+ return {
759
+ content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }],
760
+ };
761
+ }
762
+ catch (error) {
763
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
764
+ return {
765
+ content: [{ type: "text", text: `Error getting pull request: ${errorMessage}` }],
766
+ isError: true,
767
+ };
768
+ }
591
769
  });
592
770
  server.tool(REPO_TOOLS.reply_to_comment, "Replies to a specific comment on a pull request.", {
593
771
  repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
@@ -597,24 +775,33 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
597
775
  project: z.string().optional().describe("Project ID or project name (optional)"),
598
776
  fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of a simple confirmation message."),
599
777
  }, async ({ repositoryId, pullRequestId, threadId, content, project, fullResponse }) => {
600
- const connection = await connectionProvider();
601
- const gitApi = await connection.getGitApi();
602
- const comment = await gitApi.createComment({ content }, repositoryId, pullRequestId, threadId, project);
603
- // Check if the comment was successfully created
604
- if (!comment) {
778
+ try {
779
+ const connection = await connectionProvider();
780
+ const gitApi = await connection.getGitApi();
781
+ const comment = await gitApi.createComment({ content }, repositoryId, pullRequestId, threadId, project);
782
+ // Check if the comment was successfully created
783
+ if (!comment) {
784
+ return {
785
+ content: [{ type: "text", text: `Error: Failed to add comment to thread ${threadId}. The comment was not created successfully.` }],
786
+ isError: true,
787
+ };
788
+ }
789
+ if (fullResponse) {
790
+ return {
791
+ content: [{ type: "text", text: JSON.stringify(comment, null, 2) }],
792
+ };
793
+ }
605
794
  return {
606
- content: [{ type: "text", text: `Error: Failed to add comment to thread ${threadId}. The comment was not created successfully.` }],
607
- isError: true,
795
+ content: [{ type: "text", text: `Comment successfully added to thread ${threadId}.` }],
608
796
  };
609
797
  }
610
- if (fullResponse) {
798
+ catch (error) {
799
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
611
800
  return {
612
- content: [{ type: "text", text: JSON.stringify(comment, null, 2) }],
801
+ content: [{ type: "text", text: `Error replying to comment: ${errorMessage}` }],
802
+ isError: true,
613
803
  };
614
804
  }
615
- return {
616
- content: [{ type: "text", text: `Comment successfully added to thread ${threadId}.` }],
617
- };
618
805
  });
619
806
  server.tool(REPO_TOOLS.create_pull_request_thread, "Creates a new comment thread on a pull request.", {
620
807
  repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
@@ -641,68 +828,109 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
641
828
  .optional()
642
829
  .describe("Position of last character of the thread's span in right file. The character offset of a thread's position inside of a line. Must only be set if rightFileEndLine is also specified. (optional)"),
643
830
  }, async ({ repositoryId, pullRequestId, content, project, filePath, status, rightFileStartLine, rightFileStartOffset, rightFileEndLine, rightFileEndOffset }) => {
644
- const connection = await connectionProvider();
645
- const gitApi = await connection.getGitApi();
646
- const normalizedFilePath = filePath && !filePath.startsWith("/") ? `/${filePath}` : filePath;
647
- const threadContext = { filePath: normalizedFilePath };
648
- if (rightFileStartLine !== undefined) {
649
- if (rightFileStartLine < 1) {
650
- throw new Error("rightFileStartLine must be greater than or equal to 1.");
651
- }
652
- threadContext.rightFileStart = { line: rightFileStartLine };
653
- if (rightFileStartOffset !== undefined) {
654
- if (rightFileStartOffset < 1) {
655
- throw new Error("rightFileStartOffset must be greater than or equal to 1.");
831
+ try {
832
+ const connection = await connectionProvider();
833
+ const gitApi = await connection.getGitApi();
834
+ const normalizedFilePath = filePath && !filePath.startsWith("/") ? `/${filePath}` : filePath;
835
+ const threadContext = { filePath: normalizedFilePath };
836
+ if (rightFileStartLine !== undefined) {
837
+ if (rightFileStartLine < 1) {
838
+ return {
839
+ content: [{ type: "text", text: "rightFileStartLine must be greater than or equal to 1." }],
840
+ isError: true,
841
+ };
842
+ }
843
+ threadContext.rightFileStart = { line: rightFileStartLine };
844
+ if (rightFileStartOffset !== undefined) {
845
+ if (rightFileStartOffset < 1) {
846
+ return {
847
+ content: [{ type: "text", text: "rightFileStartOffset must be greater than or equal to 1." }],
848
+ isError: true,
849
+ };
850
+ }
851
+ threadContext.rightFileStart.offset = rightFileStartOffset;
656
852
  }
657
- threadContext.rightFileStart.offset = rightFileStartOffset;
658
- }
659
- }
660
- if (rightFileEndLine !== undefined) {
661
- if (rightFileStartLine === undefined) {
662
- throw new Error("rightFileEndLine must only be specified if rightFileStartLine is also specified.");
663
- }
664
- if (rightFileEndLine < 1) {
665
- throw new Error("rightFileEndLine must be greater than or equal to 1.");
666
853
  }
667
- threadContext.rightFileEnd = { line: rightFileEndLine };
668
- if (rightFileEndOffset !== undefined) {
669
- if (rightFileEndOffset < 1) {
670
- throw new Error("rightFileEndOffset must be greater than or equal to 1.");
854
+ if (rightFileEndLine !== undefined) {
855
+ if (rightFileStartLine === undefined) {
856
+ return {
857
+ content: [{ type: "text", text: "rightFileEndLine must only be specified if rightFileStartLine is also specified." }],
858
+ isError: true,
859
+ };
860
+ }
861
+ if (rightFileEndLine < 1) {
862
+ return {
863
+ content: [{ type: "text", text: "rightFileEndLine must be greater than or equal to 1." }],
864
+ isError: true,
865
+ };
866
+ }
867
+ threadContext.rightFileEnd = { line: rightFileEndLine };
868
+ if (rightFileEndOffset !== undefined) {
869
+ if (rightFileEndOffset < 1) {
870
+ return {
871
+ content: [{ type: "text", text: "rightFileEndOffset must be greater than or equal to 1." }],
872
+ isError: true,
873
+ };
874
+ }
875
+ threadContext.rightFileEnd.offset = rightFileEndOffset;
671
876
  }
672
- threadContext.rightFileEnd.offset = rightFileEndOffset;
673
877
  }
878
+ const thread = await gitApi.createThread({ comments: [{ content: content }], threadContext: threadContext, status: CommentThreadStatus[status] }, repositoryId, pullRequestId, project);
879
+ const trimmedThread = trimPullRequestThread(thread);
880
+ return {
881
+ content: [{ type: "text", text: JSON.stringify(trimmedThread, null, 2) }],
882
+ };
883
+ }
884
+ catch (error) {
885
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
886
+ return {
887
+ content: [{ type: "text", text: `Error creating pull request thread: ${errorMessage}` }],
888
+ isError: true,
889
+ };
674
890
  }
675
- const thread = await gitApi.createThread({ comments: [{ content: content }], threadContext: threadContext, status: CommentThreadStatus[status] }, repositoryId, pullRequestId, project);
676
- const trimmedThread = trimPullRequestThread(thread);
677
- return {
678
- content: [{ type: "text", text: JSON.stringify(trimmedThread, null, 2) }],
679
- };
680
891
  });
681
- server.tool(REPO_TOOLS.resolve_comment, "Resolves a specific comment thread on a pull request.", {
892
+ server.tool(REPO_TOOLS.update_pull_request_thread, "Updates an existing comment thread on a pull request.", {
682
893
  repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
683
894
  pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
684
- threadId: z.number().describe("The ID of the thread to be resolved."),
685
- fullResponse: z.boolean().optional().default(false).describe("Return full thread JSON response instead of a simple confirmation message."),
686
- }, async ({ repositoryId, pullRequestId, threadId, fullResponse }) => {
687
- const connection = await connectionProvider();
688
- const gitApi = await connection.getGitApi();
689
- const thread = await gitApi.updateThread({ status: 2 }, // 2 corresponds to "Resolved" status
690
- repositoryId, pullRequestId, threadId);
691
- // Check if the thread was successfully resolved
692
- if (!thread) {
693
- return {
694
- content: [{ type: "text", text: `Error: Failed to resolve thread ${threadId}. The thread status was not updated successfully.` }],
695
- isError: true,
895
+ threadId: z.number().describe("The ID of the thread to update."),
896
+ project: z.string().optional().describe("Project ID or project name (optional)"),
897
+ status: z
898
+ .enum(getEnumKeys(CommentThreadStatus))
899
+ .optional()
900
+ .describe("The new status for the comment thread."),
901
+ }, async ({ repositoryId, pullRequestId, threadId, project, status }) => {
902
+ try {
903
+ const connection = await connectionProvider();
904
+ const gitApi = await connection.getGitApi();
905
+ const updateRequest = {};
906
+ if (status !== undefined) {
907
+ updateRequest.status = CommentThreadStatus[status];
908
+ }
909
+ if (Object.keys(updateRequest).length === 0) {
910
+ return {
911
+ content: [{ type: "text", text: "Error: At least one field (status) must be provided for update." }],
912
+ isError: true,
913
+ };
914
+ }
915
+ const thread = await gitApi.updateThread(updateRequest, repositoryId, pullRequestId, threadId, project);
916
+ if (!thread) {
917
+ return {
918
+ content: [{ type: "text", text: `Error: Failed to update thread ${threadId}. The thread was not updated successfully.` }],
919
+ isError: true,
920
+ };
921
+ }
922
+ const trimmedThread = trimPullRequestThread(thread);
923
+ return {
924
+ content: [{ type: "text", text: JSON.stringify(trimmedThread, null, 2) }],
696
925
  };
697
926
  }
698
- if (fullResponse) {
927
+ catch (error) {
928
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
699
929
  return {
700
- content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
930
+ content: [{ type: "text", text: `Error updating pull request thread: ${errorMessage}` }],
931
+ isError: true,
701
932
  };
702
933
  }
703
- return {
704
- content: [{ type: "text", text: `Thread ${threadId} was successfully resolved.` }],
705
- };
706
934
  });
707
935
  const gitVersionTypeStrings = Object.values(GitVersionType).filter((value) => typeof value === "string");
708
936
  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.", {
@@ -792,7 +1020,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
792
1020
  if (historySimplificationMode) {
793
1021
  // Note: This parameter might not be directly supported by all ADO API versions
794
1022
  // but we'll include it in the criteria for forward compatibility
795
- searchCriteria.historySimplificationMode = historySimplificationMode;
1023
+ const extendedCriteria = searchCriteria;
1024
+ extendedCriteria.historySimplificationMode = historySimplificationMode;
796
1025
  }
797
1026
  if (version) {
798
1027
  const itemVersion = {
@@ -864,13 +1093,9 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
864
1093
  };
865
1094
  }
866
1095
  catch (error) {
1096
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
867
1097
  return {
868
- content: [
869
- {
870
- type: "text",
871
- text: `Error querying pull requests by commits: ${error instanceof Error ? error.message : String(error)}`,
872
- },
873
- ],
1098
+ content: [{ type: "text", text: `Error querying pull requests by commits: ${errorMessage}` }],
874
1099
  isError: true,
875
1100
  };
876
1101
  }