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

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