@web-marketing-hr/azure-devops-mcp 2.3.5

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.
Files changed (45) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +238 -0
  3. package/dist/auth.js +117 -0
  4. package/dist/auth.js.map +1 -0
  5. package/dist/index.js +155 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/logger.js +35 -0
  8. package/dist/logger.js.map +1 -0
  9. package/dist/org-tenants.js +79 -0
  10. package/dist/org-tenants.js.map +1 -0
  11. package/dist/prompts.js +21 -0
  12. package/dist/prompts.js.map +1 -0
  13. package/dist/shared/domains.js +126 -0
  14. package/dist/shared/domains.js.map +1 -0
  15. package/dist/shared/tool-validation.js +93 -0
  16. package/dist/shared/tool-validation.js.map +1 -0
  17. package/dist/tools/advanced-security.js +109 -0
  18. package/dist/tools/advanced-security.js.map +1 -0
  19. package/dist/tools/auth.js +64 -0
  20. package/dist/tools/auth.js.map +1 -0
  21. package/dist/tools/core.js +96 -0
  22. package/dist/tools/core.js.map +1 -0
  23. package/dist/tools/pipelines.js +322 -0
  24. package/dist/tools/pipelines.js.map +1 -0
  25. package/dist/tools/repositories.js +938 -0
  26. package/dist/tools/repositories.js.map +1 -0
  27. package/dist/tools/search.js +211 -0
  28. package/dist/tools/search.js.map +1 -0
  29. package/dist/tools/test-plans.js +387 -0
  30. package/dist/tools/test-plans.js.map +1 -0
  31. package/dist/tools/wiki.js +390 -0
  32. package/dist/tools/wiki.js.map +1 -0
  33. package/dist/tools/work-items.js +1007 -0
  34. package/dist/tools/work-items.js.map +1 -0
  35. package/dist/tools/work.js +299 -0
  36. package/dist/tools/work.js.map +1 -0
  37. package/dist/tools.js +30 -0
  38. package/dist/tools.js.map +1 -0
  39. package/dist/useragent.js +21 -0
  40. package/dist/useragent.js.map +1 -0
  41. package/dist/utils.js +94 -0
  42. package/dist/utils.js.map +1 -0
  43. package/dist/version.js +2 -0
  44. package/dist/version.js.map +1 -0
  45. package/package.json +86 -0
@@ -0,0 +1,938 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, CommentThreadStatus, GitPullRequestMergeStrategy, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
4
+ import { z } from "zod";
5
+ import { getCurrentUserDetails, getUserIdFromEmail } from "./auth.js";
6
+ import { getEnumKeys } from "../utils.js";
7
+ const REPO_TOOLS = {
8
+ list_repos_by_project: "repo_list_repos_by_project",
9
+ list_pull_requests_by_repo_or_project: "repo_list_pull_requests_by_repo_or_project",
10
+ list_branches_by_repo: "repo_list_branches_by_repo",
11
+ list_my_branches_by_repo: "repo_list_my_branches_by_repo",
12
+ list_pull_request_threads: "repo_list_pull_request_threads",
13
+ list_pull_request_thread_comments: "repo_list_pull_request_thread_comments",
14
+ get_repo_by_name_or_id: "repo_get_repo_by_name_or_id",
15
+ get_branch_by_name: "repo_get_branch_by_name",
16
+ get_pull_request_by_id: "repo_get_pull_request_by_id",
17
+ create_pull_request: "repo_create_pull_request",
18
+ create_branch: "repo_create_branch",
19
+ update_pull_request: "repo_update_pull_request",
20
+ update_pull_request_reviewers: "repo_update_pull_request_reviewers",
21
+ reply_to_comment: "repo_reply_to_comment",
22
+ create_pull_request_thread: "repo_create_pull_request_thread",
23
+ resolve_comment: "repo_resolve_comment",
24
+ search_commits: "repo_search_commits",
25
+ list_pull_requests_by_commits: "repo_list_pull_requests_by_commits",
26
+ };
27
+ function branchesFilterOutIrrelevantProperties(branches, top) {
28
+ return branches
29
+ ?.flatMap((branch) => (branch.name ? [branch.name] : []))
30
+ ?.filter((branch) => branch.startsWith("refs/heads/"))
31
+ .map((branch) => branch.replace("refs/heads/", ""))
32
+ .sort((a, b) => b.localeCompare(a))
33
+ .slice(0, top);
34
+ }
35
+ function trimPullRequestThread(thread) {
36
+ return {
37
+ id: thread.id,
38
+ publishedDate: thread.publishedDate,
39
+ lastUpdatedDate: thread.lastUpdatedDate,
40
+ status: thread.status,
41
+ comments: trimComments(thread.comments),
42
+ threadContext: thread.threadContext,
43
+ };
44
+ }
45
+ /**
46
+ * Trims comment data to essential properties, filtering out deleted comments
47
+ * @param comments Array of comments to trim (can be undefined/null)
48
+ * @returns Array of trimmed comment objects with essential properties only
49
+ */
50
+ function trimComments(comments) {
51
+ return comments
52
+ ?.filter((comment) => !comment.isDeleted) // Exclude deleted comments
53
+ ?.map((comment) => ({
54
+ id: comment.id,
55
+ author: {
56
+ displayName: comment.author?.displayName,
57
+ uniqueName: comment.author?.uniqueName,
58
+ },
59
+ content: comment.content,
60
+ publishedDate: comment.publishedDate,
61
+ lastUpdatedDate: comment.lastUpdatedDate,
62
+ lastContentUpdatedDate: comment.lastContentUpdatedDate,
63
+ }));
64
+ }
65
+ function pullRequestStatusStringToInt(status) {
66
+ switch (status) {
67
+ case "Abandoned":
68
+ return PullRequestStatus.Abandoned.valueOf();
69
+ case "Active":
70
+ return PullRequestStatus.Active.valueOf();
71
+ case "All":
72
+ return PullRequestStatus.All.valueOf();
73
+ case "Completed":
74
+ return PullRequestStatus.Completed.valueOf();
75
+ case "NotSet":
76
+ return PullRequestStatus.NotSet.valueOf();
77
+ default:
78
+ throw new Error(`Unknown pull request status: ${status}`);
79
+ }
80
+ }
81
+ function filterReposByName(repositories, repoNameFilter) {
82
+ const lowerCaseFilter = repoNameFilter.toLowerCase();
83
+ const filteredByName = repositories?.filter((repo) => repo.name?.toLowerCase().includes(lowerCaseFilter));
84
+ return filteredByName;
85
+ }
86
+ function trimPullRequest(pr, includeDescription = false) {
87
+ return {
88
+ pullRequestId: pr.pullRequestId,
89
+ codeReviewId: pr.codeReviewId,
90
+ repository: pr.repository?.name,
91
+ status: pr.status,
92
+ createdBy: {
93
+ displayName: pr.createdBy?.displayName,
94
+ uniqueName: pr.createdBy?.uniqueName,
95
+ },
96
+ creationDate: pr.creationDate,
97
+ closedDate: pr.closedDate,
98
+ title: pr.title,
99
+ ...(includeDescription ? { description: pr.description ?? "" } : {}),
100
+ isDraft: pr.isDraft,
101
+ sourceRefName: pr.sourceRefName,
102
+ targetRefName: pr.targetRefName,
103
+ };
104
+ }
105
+ function configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider) {
106
+ server.tool(REPO_TOOLS.create_pull_request, "Create a new pull request.", {
107
+ repositoryId: z.string().describe("The ID of the repository where the pull request will be created."),
108
+ sourceRefName: z.string().describe("The source branch name for the pull request, e.g., 'refs/heads/feature-branch'."),
109
+ targetRefName: z.string().describe("The target branch name for the pull request, e.g., 'refs/heads/main'."),
110
+ title: z.string().describe("The title of the pull request."),
111
+ description: z.string().max(4000).optional().describe("The description of the pull request. Must not be longer than 4000 characters. Optional."),
112
+ isDraft: z.boolean().optional().default(false).describe("Indicates whether the pull request is a draft. Defaults to false."),
113
+ workItems: z.string().optional().describe("Work item IDs to associate with the pull request, space-separated."),
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."),
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
+ const connection = await connectionProvider();
118
+ const gitApi = await connection.getGitApi();
119
+ const workItemRefs = workItems ? workItems.split(" ").map((id) => ({ id: id.trim() })) : [];
120
+ const forkSource = forkSourceRepositoryId
121
+ ? {
122
+ repository: {
123
+ id: forkSourceRepositoryId,
124
+ },
125
+ }
126
+ : undefined;
127
+ const labelDefinitions = labels ? labels.map((label) => ({ name: label })) : undefined;
128
+ const pullRequest = await gitApi.createPullRequest({
129
+ sourceRefName,
130
+ targetRefName,
131
+ title,
132
+ description,
133
+ isDraft,
134
+ workItemRefs: workItemRefs,
135
+ forkSource,
136
+ labels: labelDefinitions,
137
+ }, repositoryId);
138
+ const trimmedPullRequest = trimPullRequest(pullRequest, true);
139
+ return {
140
+ content: [{ type: "text", text: JSON.stringify(trimmedPullRequest, null, 2) }],
141
+ };
142
+ });
143
+ server.tool(REPO_TOOLS.create_branch, "Create a new branch in the repository.", {
144
+ repositoryId: z.string().describe("The ID of the repository where the branch will be created."),
145
+ branchName: z.string().describe("The name of the new branch to create, e.g., 'feature-branch'."),
146
+ sourceBranchName: z.string().optional().default("main").describe("The name of the source branch to create the new branch from. Defaults to 'main'."),
147
+ sourceCommitId: z.string().optional().describe("The commit ID to create the branch from. If not provided, uses the latest commit of the source branch."),
148
+ }, async ({ repositoryId, branchName, sourceBranchName, sourceCommitId }) => {
149
+ const connection = await connectionProvider();
150
+ const gitApi = await connection.getGitApi();
151
+ let commitId = sourceCommitId;
152
+ // If no commit ID is provided, get the latest commit from the source branch
153
+ if (!commitId) {
154
+ const sourceRefName = `refs/heads/${sourceBranchName}`;
155
+ try {
156
+ const sourceBranch = await gitApi.getRefs(repositoryId, undefined, "heads/", false, false, undefined, false, undefined, sourceBranchName);
157
+ const branch = sourceBranch.find((b) => b.name === sourceRefName);
158
+ if (!branch || !branch.objectId) {
159
+ return {
160
+ content: [
161
+ {
162
+ type: "text",
163
+ text: `Error: Source branch '${sourceBranchName}' not found in repository ${repositoryId}`,
164
+ },
165
+ ],
166
+ isError: true,
167
+ };
168
+ }
169
+ commitId = branch.objectId;
170
+ }
171
+ catch (error) {
172
+ return {
173
+ content: [
174
+ {
175
+ type: "text",
176
+ text: `Error retrieving source branch '${sourceBranchName}': ${error instanceof Error ? error.message : String(error)}`,
177
+ },
178
+ ],
179
+ isError: true,
180
+ };
181
+ }
182
+ }
183
+ // Create the new branch using updateRefs
184
+ const newRefName = `refs/heads/${branchName}`;
185
+ const refUpdate = {
186
+ name: newRefName,
187
+ newObjectId: commitId,
188
+ oldObjectId: "0000000000000000000000000000000000000000", // All zeros indicates creating a new ref
189
+ };
190
+ try {
191
+ const result = await gitApi.updateRefs([refUpdate], repositoryId);
192
+ // Check if the branch creation was successful
193
+ if (result && result.length > 0 && result[0].success) {
194
+ return {
195
+ content: [
196
+ {
197
+ type: "text",
198
+ text: `Branch '${branchName}' created successfully from '${sourceBranchName}' (${commitId})`,
199
+ },
200
+ ],
201
+ };
202
+ }
203
+ else {
204
+ const errorMessage = result && result.length > 0 && result[0].customMessage ? result[0].customMessage : "Unknown error occurred during branch creation";
205
+ return {
206
+ content: [
207
+ {
208
+ type: "text",
209
+ text: `Error creating branch '${branchName}': ${errorMessage}`,
210
+ },
211
+ ],
212
+ isError: true,
213
+ };
214
+ }
215
+ }
216
+ catch (error) {
217
+ return {
218
+ content: [
219
+ {
220
+ type: "text",
221
+ text: `Error creating branch '${branchName}': ${error instanceof Error ? error.message : String(error)}`,
222
+ },
223
+ ],
224
+ isError: true,
225
+ };
226
+ }
227
+ });
228
+ server.tool(REPO_TOOLS.update_pull_request, "Update a Pull Request by ID with specified fields, including setting autocomplete with various completion options.", {
229
+ repositoryId: z.string().describe("The ID of the repository where the pull request exists."),
230
+ pullRequestId: z.number().describe("The ID of the pull request to update."),
231
+ title: z.string().optional().describe("The new title for the pull request."),
232
+ description: z.string().max(4000).optional().describe("The new description for the pull request. Must not be longer than 4000 characters."),
233
+ isDraft: z.boolean().optional().describe("Whether the pull request should be a draft."),
234
+ targetRefName: z.string().optional().describe("The new target branch name (e.g., 'refs/heads/main')."),
235
+ status: z.enum(["Active", "Abandoned"]).optional().describe("The new status of the pull request. Can be 'Active' or 'Abandoned'."),
236
+ autoComplete: z.boolean().optional().describe("Set the pull request to autocomplete when all requirements are met."),
237
+ mergeStrategy: z
238
+ .enum(getEnumKeys(GitPullRequestMergeStrategy))
239
+ .optional()
240
+ .describe("The merge strategy to use when the pull request autocompletes. Defaults to 'NoFastForward'."),
241
+ deleteSourceBranch: z.boolean().optional().default(false).describe("Whether to delete the source branch when the pull request autocompletes. Defaults to false."),
242
+ 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."),
243
+ bypassReason: z.string().optional().describe("Reason for bypassing branch policies. When provided, branch policies will be automatically bypassed during autocompletion."),
244
+ }, async ({ repositoryId, pullRequestId, title, description, isDraft, targetRefName, status, autoComplete, mergeStrategy, deleteSourceBranch, transitionWorkItems, bypassReason }) => {
245
+ const connection = await connectionProvider();
246
+ const gitApi = await connection.getGitApi();
247
+ // Build update object with only provided fields
248
+ const updateRequest = {};
249
+ if (title !== undefined)
250
+ updateRequest.title = title;
251
+ if (description !== undefined)
252
+ updateRequest.description = description;
253
+ if (isDraft !== undefined)
254
+ updateRequest.isDraft = isDraft;
255
+ if (targetRefName !== undefined)
256
+ updateRequest.targetRefName = targetRefName;
257
+ if (status !== undefined) {
258
+ updateRequest.status = status === "Active" ? PullRequestStatus.Active.valueOf() : PullRequestStatus.Abandoned.valueOf();
259
+ }
260
+ if (autoComplete !== undefined) {
261
+ if (autoComplete) {
262
+ const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
263
+ const autoCompleteUserId = data.authenticatedUser.id;
264
+ updateRequest.autoCompleteSetBy = { id: autoCompleteUserId };
265
+ const completionOptions = {
266
+ deleteSourceBranch: deleteSourceBranch || false,
267
+ transitionWorkItems: transitionWorkItems !== false, // Default to true unless explicitly set to false
268
+ bypassPolicy: !!bypassReason, // Automatically set to true if bypassReason is provided
269
+ };
270
+ if (mergeStrategy) {
271
+ completionOptions.mergeStrategy = GitPullRequestMergeStrategy[mergeStrategy];
272
+ }
273
+ if (bypassReason) {
274
+ completionOptions.bypassReason = bypassReason;
275
+ }
276
+ updateRequest.completionOptions = completionOptions;
277
+ }
278
+ else {
279
+ updateRequest.autoCompleteSetBy = null;
280
+ updateRequest.completionOptions = null;
281
+ }
282
+ }
283
+ // Validate that at least one field is provided for update
284
+ if (Object.keys(updateRequest).length === 0) {
285
+ return {
286
+ content: [{ type: "text", text: "Error: At least one field (title, description, isDraft, targetRefName, status, or autoComplete options) must be provided for update." }],
287
+ isError: true,
288
+ };
289
+ }
290
+ const updatedPullRequest = await gitApi.updatePullRequest(updateRequest, repositoryId, pullRequestId);
291
+ const trimmedUpdatedPullRequest = trimPullRequest(updatedPullRequest, true);
292
+ return {
293
+ content: [{ type: "text", text: JSON.stringify(trimmedUpdatedPullRequest, null, 2) }],
294
+ };
295
+ });
296
+ server.tool(REPO_TOOLS.update_pull_request_reviewers, "Add or remove reviewers for an existing pull request.", {
297
+ repositoryId: z.string().describe("The ID of the repository where the pull request exists."),
298
+ pullRequestId: z.number().describe("The ID of the pull request to update."),
299
+ reviewerIds: z.array(z.string()).describe("List of reviewer ids to add or remove from the pull request."),
300
+ action: z.enum(["add", "remove"]).describe("Action to perform on the reviewers. Can be 'add' or 'remove'."),
301
+ }, async ({ repositoryId, pullRequestId, reviewerIds, action }) => {
302
+ const connection = await connectionProvider();
303
+ const gitApi = await connection.getGitApi();
304
+ let updatedPullRequest;
305
+ if (action === "add") {
306
+ updatedPullRequest = await gitApi.createPullRequestReviewers(reviewerIds.map((id) => ({ id: id })), repositoryId, pullRequestId);
307
+ const trimmedResponse = updatedPullRequest.map((item) => ({
308
+ displayName: item.displayName,
309
+ id: item.id,
310
+ uniqueName: item.uniqueName,
311
+ vote: item.vote,
312
+ hasDeclined: item.hasDeclined,
313
+ isFlagged: item.isFlagged,
314
+ }));
315
+ return {
316
+ content: [{ type: "text", text: JSON.stringify(trimmedResponse, null, 2) }],
317
+ };
318
+ }
319
+ else {
320
+ for (const reviewerId of reviewerIds) {
321
+ await gitApi.deletePullRequestReviewer(repositoryId, pullRequestId, reviewerId);
322
+ }
323
+ return {
324
+ content: [{ type: "text", text: `Reviewers with IDs ${reviewerIds.join(", ")} removed from pull request ${pullRequestId}.` }],
325
+ };
326
+ }
327
+ });
328
+ server.tool(REPO_TOOLS.list_repos_by_project, "Retrieve a list of repositories for a given project", {
329
+ project: z.string().describe("The name or ID of the Azure DevOps project."),
330
+ top: z.number().default(100).describe("The maximum number of repositories to return."),
331
+ skip: z.number().default(0).describe("The number of repositories to skip. Defaults to 0."),
332
+ 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."),
333
+ }, async ({ project, top, skip, repoNameFilter }) => {
334
+ const connection = await connectionProvider();
335
+ const gitApi = await connection.getGitApi();
336
+ const repositories = await gitApi.getRepositories(project, false, false, false);
337
+ const filteredRepositories = repoNameFilter ? filterReposByName(repositories, repoNameFilter) : repositories;
338
+ const paginatedRepositories = filteredRepositories?.sort((a, b) => a.name?.localeCompare(b.name ?? "") ?? 0).slice(skip, skip + top);
339
+ // Filter out the irrelevant properties
340
+ const trimmedRepositories = paginatedRepositories?.map((repo) => ({
341
+ id: repo.id,
342
+ name: repo.name,
343
+ isDisabled: repo.isDisabled,
344
+ isFork: repo.isFork,
345
+ isInMaintenance: repo.isInMaintenance,
346
+ webUrl: repo.webUrl,
347
+ size: repo.size,
348
+ }));
349
+ return {
350
+ content: [{ type: "text", text: JSON.stringify(trimmedRepositories, null, 2) }],
351
+ };
352
+ });
353
+ 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.", {
354
+ repositoryId: z.string().optional().describe("The ID of the repository where the pull requests are located."),
355
+ project: z.string().optional().describe("The ID of the project where the pull requests are located."),
356
+ top: z.number().default(100).describe("The maximum number of pull requests to return."),
357
+ skip: z.number().default(0).describe("The number of pull requests to skip."),
358
+ created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
359
+ created_by_user: z.string().optional().describe("Filter pull requests created by a specific user (provide email or unique name). Takes precedence over created_by_me if both are provided."),
360
+ i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
361
+ user_is_reviewer: z
362
+ .string()
363
+ .optional()
364
+ .describe("Filter pull requests where a specific user is a reviewer (provide email or unique name). Takes precedence over i_am_reviewer if both are provided."),
365
+ status: z
366
+ .enum(getEnumKeys(PullRequestStatus))
367
+ .default("Active")
368
+ .describe("Filter pull requests by status. Defaults to 'Active'."),
369
+ sourceRefName: z.string().optional().describe("Filter pull requests from this source branch (e.g., 'refs/heads/feature-branch')."),
370
+ targetRefName: z.string().optional().describe("Filter pull requests into this target branch (e.g., 'refs/heads/main')."),
371
+ }, async ({ repositoryId, project, top, skip, created_by_me, created_by_user, i_am_reviewer, user_is_reviewer, status, sourceRefName, targetRefName }) => {
372
+ const connection = await connectionProvider();
373
+ const gitApi = await connection.getGitApi();
374
+ // Build the search criteria
375
+ const searchCriteria = {
376
+ status: pullRequestStatusStringToInt(status),
377
+ };
378
+ if (!repositoryId && !project) {
379
+ return {
380
+ content: [
381
+ {
382
+ type: "text",
383
+ text: "Either repositoryId or project must be provided.",
384
+ },
385
+ ],
386
+ isError: true,
387
+ };
388
+ }
389
+ if (repositoryId) {
390
+ searchCriteria.repositoryId = repositoryId;
391
+ }
392
+ if (sourceRefName) {
393
+ searchCriteria.sourceRefName = sourceRefName;
394
+ }
395
+ if (targetRefName) {
396
+ searchCriteria.targetRefName = targetRefName;
397
+ }
398
+ if (created_by_user) {
399
+ try {
400
+ const userId = await getUserIdFromEmail(created_by_user, tokenProvider, connectionProvider, userAgentProvider);
401
+ searchCriteria.creatorId = userId;
402
+ }
403
+ catch (error) {
404
+ return {
405
+ content: [
406
+ {
407
+ type: "text",
408
+ text: `Error finding user with email ${created_by_user}: ${error instanceof Error ? error.message : String(error)}`,
409
+ },
410
+ ],
411
+ isError: true,
412
+ };
413
+ }
414
+ }
415
+ else if (created_by_me) {
416
+ const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
417
+ const userId = data.authenticatedUser.id;
418
+ searchCriteria.creatorId = userId;
419
+ }
420
+ if (user_is_reviewer) {
421
+ try {
422
+ const reviewerUserId = await getUserIdFromEmail(user_is_reviewer, tokenProvider, connectionProvider, userAgentProvider);
423
+ searchCriteria.reviewerId = reviewerUserId;
424
+ }
425
+ catch (error) {
426
+ return {
427
+ content: [
428
+ {
429
+ type: "text",
430
+ text: `Error finding reviewer with email ${user_is_reviewer}: ${error instanceof Error ? error.message : String(error)}`,
431
+ },
432
+ ],
433
+ isError: true,
434
+ };
435
+ }
436
+ }
437
+ else if (i_am_reviewer) {
438
+ const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
439
+ const userId = data.authenticatedUser.id;
440
+ searchCriteria.reviewerId = userId;
441
+ }
442
+ let pullRequests;
443
+ if (repositoryId) {
444
+ pullRequests = await gitApi.getPullRequests(repositoryId, searchCriteria, project, // project
445
+ undefined, // maxCommentLength
446
+ skip, top);
447
+ }
448
+ else if (project) {
449
+ // If only project is provided, use getPullRequestsByProject
450
+ pullRequests = await gitApi.getPullRequestsByProject(project, searchCriteria, undefined, // maxCommentLength
451
+ skip, top);
452
+ }
453
+ else {
454
+ // This case should not occur due to earlier validation, but added for completeness
455
+ return {
456
+ content: [
457
+ {
458
+ type: "text",
459
+ text: "Either repositoryId or project must be provided.",
460
+ },
461
+ ],
462
+ isError: true,
463
+ };
464
+ }
465
+ const filteredPullRequests = pullRequests?.map((pr) => trimPullRequest(pr));
466
+ return {
467
+ content: [{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) }],
468
+ };
469
+ });
470
+ server.tool(REPO_TOOLS.list_pull_request_threads, "Retrieve a list of comment threads for a pull request.", {
471
+ repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
472
+ pullRequestId: z.number().describe("The ID of the pull request for which to retrieve threads."),
473
+ project: z.string().optional().describe("Project ID or project name (optional)"),
474
+ iteration: z.number().optional().describe("The iteration ID for which to retrieve threads. Optional, defaults to the latest iteration."),
475
+ baseIteration: z.number().optional().describe("The base iteration ID for which to retrieve threads. Optional, defaults to the latest base iteration."),
476
+ top: z.number().default(100).describe("The maximum number of threads to return after filtering."),
477
+ skip: z.number().default(0).describe("The number of threads to skip after filtering."),
478
+ fullResponse: z.boolean().optional().default(false).describe("Return full thread JSON response instead of trimmed data."),
479
+ status: z
480
+ .enum(getEnumKeys(CommentThreadStatus))
481
+ .optional()
482
+ .describe("Filter threads by status. If not specified, returns threads of all statuses."),
483
+ authorEmail: z.string().optional().describe("Filter threads by the email of the thread author (first comment author)."),
484
+ authorDisplayName: z.string().optional().describe("Filter threads by the display name of the thread author (first comment author). Case-insensitive partial matching."),
485
+ }, async ({ repositoryId, pullRequestId, project, iteration, baseIteration, top, skip, fullResponse, status, authorEmail, authorDisplayName }) => {
486
+ const connection = await connectionProvider();
487
+ const gitApi = await connection.getGitApi();
488
+ const threads = await gitApi.getThreads(repositoryId, pullRequestId, project, iteration, baseIteration);
489
+ let filteredThreads = threads;
490
+ if (status !== undefined) {
491
+ const statusValue = CommentThreadStatus[status];
492
+ filteredThreads = filteredThreads?.filter((thread) => thread.status === statusValue);
493
+ }
494
+ if (authorEmail !== undefined) {
495
+ filteredThreads = filteredThreads?.filter((thread) => {
496
+ const firstComment = thread.comments?.[0];
497
+ return firstComment?.author?.uniqueName?.toLowerCase() === authorEmail.toLowerCase();
498
+ });
499
+ }
500
+ if (authorDisplayName !== undefined) {
501
+ const lowerAuthorName = authorDisplayName.toLowerCase();
502
+ filteredThreads = filteredThreads?.filter((thread) => {
503
+ const firstComment = thread.comments?.[0];
504
+ return firstComment?.author?.displayName?.toLowerCase().includes(lowerAuthorName);
505
+ });
506
+ }
507
+ const paginatedThreads = filteredThreads?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
508
+ if (fullResponse) {
509
+ return {
510
+ content: [{ type: "text", text: JSON.stringify(paginatedThreads, null, 2) }],
511
+ };
512
+ }
513
+ // Return trimmed thread data focusing on essential information
514
+ const trimmedThreads = paginatedThreads?.map((thread) => trimPullRequestThread(thread));
515
+ return {
516
+ content: [{ type: "text", text: JSON.stringify(trimmedThreads, null, 2) }],
517
+ };
518
+ });
519
+ server.tool(REPO_TOOLS.list_pull_request_thread_comments, "Retrieve a list of comments in a pull request thread.", {
520
+ repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
521
+ pullRequestId: z.number().describe("The ID of the pull request for which to retrieve thread comments."),
522
+ threadId: z.number().describe("The ID of the thread for which to retrieve comments."),
523
+ project: z.string().optional().describe("Project ID or project name (optional)"),
524
+ top: z.number().default(100).describe("The maximum number of comments to return."),
525
+ skip: z.number().default(0).describe("The number of comments to skip."),
526
+ fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of trimmed data."),
527
+ }, async ({ repositoryId, pullRequestId, threadId, project, top, skip, fullResponse }) => {
528
+ const connection = await connectionProvider();
529
+ const gitApi = await connection.getGitApi();
530
+ // Get thread comments - GitApi uses getComments for retrieving comments from a specific thread
531
+ const comments = await gitApi.getComments(repositoryId, pullRequestId, threadId, project);
532
+ const paginatedComments = comments?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
533
+ if (fullResponse) {
534
+ return {
535
+ content: [{ type: "text", text: JSON.stringify(paginatedComments, null, 2) }],
536
+ };
537
+ }
538
+ // Return trimmed comment data focusing on essential information
539
+ const trimmedComments = trimComments(paginatedComments);
540
+ return {
541
+ content: [{ type: "text", text: JSON.stringify(trimmedComments, null, 2) }],
542
+ };
543
+ });
544
+ server.tool(REPO_TOOLS.list_branches_by_repo, "Retrieve a list of branches for a given repository.", {
545
+ repositoryId: z.string().describe("The ID of the repository where the branches are located."),
546
+ top: z.number().default(100).describe("The maximum number of branches to return. Defaults to 100."),
547
+ filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
548
+ }, async ({ repositoryId, top, filterContains }) => {
549
+ const connection = await connectionProvider();
550
+ const gitApi = await connection.getGitApi();
551
+ const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", undefined, undefined, undefined, undefined, undefined, filterContains);
552
+ const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
553
+ return {
554
+ content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
555
+ };
556
+ });
557
+ server.tool(REPO_TOOLS.list_my_branches_by_repo, "Retrieve a list of my branches for a given repository Id.", {
558
+ repositoryId: z.string().describe("The ID of the repository where the branches are located."),
559
+ top: z.number().default(100).describe("The maximum number of branches to return."),
560
+ filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
561
+ }, async ({ repositoryId, top, filterContains }) => {
562
+ const connection = await connectionProvider();
563
+ const gitApi = await connection.getGitApi();
564
+ const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", undefined, undefined, true, undefined, undefined, filterContains);
565
+ const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
566
+ return {
567
+ content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
568
+ };
569
+ });
570
+ server.tool(REPO_TOOLS.get_repo_by_name_or_id, "Get the repository by project and repository name or ID.", {
571
+ project: z.string().describe("Project name or ID where the repository is located."),
572
+ repositoryNameOrId: z.string().describe("Repository name or ID."),
573
+ }, async ({ project, repositoryNameOrId }) => {
574
+ const connection = await connectionProvider();
575
+ const gitApi = await connection.getGitApi();
576
+ const repositories = await gitApi.getRepositories(project);
577
+ const repository = repositories?.find((repo) => repo.name === repositoryNameOrId || repo.id === repositoryNameOrId);
578
+ if (!repository) {
579
+ throw new Error(`Repository ${repositoryNameOrId} not found in project ${project}`);
580
+ }
581
+ return {
582
+ content: [{ type: "text", text: JSON.stringify(repository, null, 2) }],
583
+ };
584
+ });
585
+ server.tool(REPO_TOOLS.get_branch_by_name, "Get a branch by its name.", {
586
+ repositoryId: z.string().describe("The ID of the repository where the branch is located."),
587
+ branchName: z.string().describe("The name of the branch to retrieve, e.g., 'main' or 'feature-branch'."),
588
+ }, async ({ repositoryId, branchName }) => {
589
+ const connection = await connectionProvider();
590
+ const gitApi = await connection.getGitApi();
591
+ const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", false, false, undefined, false, undefined, branchName);
592
+ const branch = branches.find((branch) => branch.name === `refs/heads/${branchName}` || branch.name === branchName);
593
+ if (!branch) {
594
+ return {
595
+ content: [
596
+ {
597
+ type: "text",
598
+ text: `Branch ${branchName} not found in repository ${repositoryId}`,
599
+ },
600
+ ],
601
+ isError: true,
602
+ };
603
+ }
604
+ return {
605
+ content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
606
+ };
607
+ });
608
+ server.tool(REPO_TOOLS.get_pull_request_by_id, "Get a pull request by its ID.", {
609
+ repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
610
+ pullRequestId: z.number().describe("The ID of the pull request to retrieve."),
611
+ includeWorkItemRefs: z.boolean().optional().default(false).describe("Whether to reference work items associated with the pull request."),
612
+ includeLabels: z.boolean().optional().default(false).describe("Whether to include a summary of labels in the response."),
613
+ }, async ({ repositoryId, pullRequestId, includeWorkItemRefs, includeLabels }) => {
614
+ const connection = await connectionProvider();
615
+ const gitApi = await connection.getGitApi();
616
+ const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId, undefined, undefined, undefined, undefined, undefined, includeWorkItemRefs);
617
+ if (includeLabels) {
618
+ try {
619
+ const projectId = pullRequest.repository?.project?.id;
620
+ const projectName = pullRequest.repository?.project?.name;
621
+ const labels = await gitApi.getPullRequestLabels(repositoryId, pullRequestId, projectName, projectId);
622
+ const labelNames = labels.map((label) => label.name).filter((name) => name !== undefined);
623
+ const enhancedResponse = {
624
+ ...pullRequest,
625
+ labelSummary: {
626
+ labels: labelNames,
627
+ labelCount: labelNames.length,
628
+ },
629
+ };
630
+ return {
631
+ content: [{ type: "text", text: JSON.stringify(enhancedResponse, null, 2) }],
632
+ };
633
+ }
634
+ catch (error) {
635
+ console.warn(`Error fetching PR labels: ${error instanceof Error ? error.message : "Unknown error"}`);
636
+ // Fall back to the original response without labels
637
+ const enhancedResponse = {
638
+ ...pullRequest,
639
+ labelSummary: {},
640
+ };
641
+ return {
642
+ content: [{ type: "text", text: JSON.stringify(enhancedResponse, null, 2) }],
643
+ };
644
+ }
645
+ }
646
+ return {
647
+ content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }],
648
+ };
649
+ });
650
+ server.tool(REPO_TOOLS.reply_to_comment, "Replies to a specific comment on a pull request.", {
651
+ repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
652
+ pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
653
+ threadId: z.number().describe("The ID of the thread to which the comment will be added."),
654
+ content: z.string().describe("The content of the comment to be added."),
655
+ project: z.string().optional().describe("Project ID or project name (optional)"),
656
+ fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of a simple confirmation message."),
657
+ }, async ({ repositoryId, pullRequestId, threadId, content, project, fullResponse }) => {
658
+ const connection = await connectionProvider();
659
+ const gitApi = await connection.getGitApi();
660
+ const comment = await gitApi.createComment({ content }, repositoryId, pullRequestId, threadId, project);
661
+ // Check if the comment was successfully created
662
+ if (!comment) {
663
+ return {
664
+ content: [{ type: "text", text: `Error: Failed to add comment to thread ${threadId}. The comment was not created successfully.` }],
665
+ isError: true,
666
+ };
667
+ }
668
+ if (fullResponse) {
669
+ return {
670
+ content: [{ type: "text", text: JSON.stringify(comment, null, 2) }],
671
+ };
672
+ }
673
+ return {
674
+ content: [{ type: "text", text: `Comment successfully added to thread ${threadId}.` }],
675
+ };
676
+ });
677
+ server.tool(REPO_TOOLS.create_pull_request_thread, "Creates a new comment thread on a pull request.", {
678
+ repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
679
+ pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
680
+ content: z.string().describe("The content of the comment to be added."),
681
+ project: z.string().optional().describe("Project ID or project name (optional)"),
682
+ filePath: z.string().optional().describe("The path of the file where the comment thread will be created. (optional)"),
683
+ status: z
684
+ .enum(getEnumKeys(CommentThreadStatus))
685
+ .optional()
686
+ .default(CommentThreadStatus[CommentThreadStatus.Active])
687
+ .describe("The status of the comment thread. Defaults to 'Active'."),
688
+ rightFileStartLine: z.number().optional().describe("Position of first character of the thread's span in right file. The line number of a thread's position. Starts at 1. (optional)"),
689
+ rightFileStartOffset: z
690
+ .number()
691
+ .optional()
692
+ .describe("Position of first character of the thread's span in right file. The line number of a thread's position. The character offset of a thread's position inside of a line. Starts at 1. Must only be set if rightFileStartLine is also specified. (optional)"),
693
+ rightFileEndLine: z
694
+ .number()
695
+ .optional()
696
+ .describe("Position of last character of the thread's span in right file. The line number of a thread's position. Starts at 1. Must only be set if rightFileStartLine is also specified. (optional)"),
697
+ rightFileEndOffset: z
698
+ .number()
699
+ .optional()
700
+ .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)"),
701
+ }, async ({ repositoryId, pullRequestId, content, project, filePath, status, rightFileStartLine, rightFileStartOffset, rightFileEndLine, rightFileEndOffset }) => {
702
+ const connection = await connectionProvider();
703
+ const gitApi = await connection.getGitApi();
704
+ const normalizedFilePath = filePath && !filePath.startsWith("/") ? `/${filePath}` : filePath;
705
+ const threadContext = { filePath: normalizedFilePath };
706
+ if (rightFileStartLine !== undefined) {
707
+ if (rightFileStartLine < 1) {
708
+ throw new Error("rightFileStartLine must be greater than or equal to 1.");
709
+ }
710
+ threadContext.rightFileStart = { line: rightFileStartLine };
711
+ if (rightFileStartOffset !== undefined) {
712
+ if (rightFileStartOffset < 1) {
713
+ throw new Error("rightFileStartOffset must be greater than or equal to 1.");
714
+ }
715
+ threadContext.rightFileStart.offset = rightFileStartOffset;
716
+ }
717
+ }
718
+ if (rightFileEndLine !== undefined) {
719
+ if (rightFileStartLine === undefined) {
720
+ throw new Error("rightFileEndLine must only be specified if rightFileStartLine is also specified.");
721
+ }
722
+ if (rightFileEndLine < 1) {
723
+ throw new Error("rightFileEndLine must be greater than or equal to 1.");
724
+ }
725
+ threadContext.rightFileEnd = { line: rightFileEndLine };
726
+ if (rightFileEndOffset !== undefined) {
727
+ if (rightFileEndOffset < 1) {
728
+ throw new Error("rightFileEndOffset must be greater than or equal to 1.");
729
+ }
730
+ threadContext.rightFileEnd.offset = rightFileEndOffset;
731
+ }
732
+ }
733
+ const thread = await gitApi.createThread({ comments: [{ content: content }], threadContext: threadContext, status: CommentThreadStatus[status] }, repositoryId, pullRequestId, project);
734
+ const trimmedThread = trimPullRequestThread(thread);
735
+ return {
736
+ content: [{ type: "text", text: JSON.stringify(trimmedThread, null, 2) }],
737
+ };
738
+ });
739
+ server.tool(REPO_TOOLS.resolve_comment, "Resolves a specific comment thread on a pull request.", {
740
+ repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
741
+ pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
742
+ threadId: z.number().describe("The ID of the thread to be resolved."),
743
+ fullResponse: z.boolean().optional().default(false).describe("Return full thread JSON response instead of a simple confirmation message."),
744
+ }, async ({ repositoryId, pullRequestId, threadId, fullResponse }) => {
745
+ const connection = await connectionProvider();
746
+ const gitApi = await connection.getGitApi();
747
+ const thread = await gitApi.updateThread({ status: 2 }, // 2 corresponds to "Resolved" status
748
+ repositoryId, pullRequestId, threadId);
749
+ // Check if the thread was successfully resolved
750
+ if (!thread) {
751
+ return {
752
+ content: [{ type: "text", text: `Error: Failed to resolve thread ${threadId}. The thread status was not updated successfully.` }],
753
+ isError: true,
754
+ };
755
+ }
756
+ if (fullResponse) {
757
+ return {
758
+ content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
759
+ };
760
+ }
761
+ return {
762
+ content: [{ type: "text", text: `Thread ${threadId} was successfully resolved.` }],
763
+ };
764
+ });
765
+ const gitVersionTypeStrings = Object.values(GitVersionType).filter((value) => typeof value === "string");
766
+ 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.", {
767
+ project: z.string().describe("Project name or ID"),
768
+ repository: z.string().describe("Repository name or ID"),
769
+ // Existing parameters
770
+ fromCommit: z.string().optional().describe("Starting commit ID"),
771
+ toCommit: z.string().optional().describe("Ending commit ID"),
772
+ version: z.string().optional().describe("The name of the branch, tag or commit to filter commits by"),
773
+ versionType: z
774
+ .enum(gitVersionTypeStrings)
775
+ .optional()
776
+ .default(GitVersionType[GitVersionType.Branch])
777
+ .describe("The meaning of the version parameter, e.g., branch, tag or commit"),
778
+ skip: z.number().optional().default(0).describe("Number of commits to skip"),
779
+ top: z.number().optional().default(10).describe("Maximum number of commits to return"),
780
+ includeLinks: z.boolean().optional().default(false).describe("Include commit links"),
781
+ includeWorkItems: z.boolean().optional().default(false).describe("Include associated work items"),
782
+ // Enhanced search parameters
783
+ searchText: z.string().optional().describe("Search text to filter commits by description/comment. Supports partial matching."),
784
+ author: z.string().optional().describe("Filter commits by author email or display name"),
785
+ authorEmail: z.string().optional().describe("Filter commits by exact author email address"),
786
+ committer: z.string().optional().describe("Filter commits by committer email or display name"),
787
+ committerEmail: z.string().optional().describe("Filter commits by exact committer email address"),
788
+ fromDate: z.string().optional().describe("Filter commits from this date (ISO 8601 format, e.g., '2024-01-01T00:00:00Z')"),
789
+ toDate: z.string().optional().describe("Filter commits to this date (ISO 8601 format, e.g., '2024-12-31T23:59:59Z')"),
790
+ commitIds: z.array(z.string()).optional().describe("Array of specific commit IDs to retrieve. When provided, other filters are ignored except top/skip."),
791
+ historySimplificationMode: z.enum(["FirstParent", "SimplifyMerges", "FullHistory", "FullHistorySimplifyMerges"]).optional().describe("How to simplify the commit history"),
792
+ }, async ({ project, repository, fromCommit, toCommit, version, versionType, skip, top, includeLinks, includeWorkItems, searchText, author, authorEmail, committer, committerEmail, fromDate, toDate, commitIds, historySimplificationMode, }) => {
793
+ try {
794
+ const connection = await connectionProvider();
795
+ const gitApi = await connection.getGitApi();
796
+ // If specific commit IDs are provided, use getCommits with commit ID filtering
797
+ if (commitIds && commitIds.length > 0) {
798
+ const commits = [];
799
+ const batchSize = Math.min(top || 10, commitIds.length);
800
+ const startIndex = skip || 0;
801
+ const endIndex = Math.min(startIndex + batchSize, commitIds.length);
802
+ // Process commits in the requested range
803
+ const requestedCommitIds = commitIds.slice(startIndex, endIndex);
804
+ // Use getCommits for each commit ID to maintain consistency
805
+ for (const commitId of requestedCommitIds) {
806
+ try {
807
+ const searchCriteria = {
808
+ includeLinks: includeLinks,
809
+ includeWorkItems: includeWorkItems,
810
+ fromCommitId: commitId,
811
+ toCommitId: commitId,
812
+ };
813
+ const commitResults = await gitApi.getCommits(repository, searchCriteria, project, 0, 1);
814
+ if (commitResults && commitResults.length > 0) {
815
+ commits.push(commitResults[0]);
816
+ }
817
+ }
818
+ catch (error) {
819
+ // Log error but continue with other commits
820
+ console.warn(`Failed to retrieve commit ${commitId}: ${error instanceof Error ? error.message : String(error)}`);
821
+ // Add error information to result instead of failing completely
822
+ commits.push({
823
+ commitId: commitId,
824
+ error: `Failed to retrieve: ${error instanceof Error ? error.message : String(error)}`,
825
+ });
826
+ }
827
+ }
828
+ return {
829
+ content: [{ type: "text", text: JSON.stringify(commits, null, 2) }],
830
+ };
831
+ }
832
+ const searchCriteria = {
833
+ fromCommitId: fromCommit,
834
+ toCommitId: toCommit,
835
+ includeLinks: includeLinks,
836
+ includeWorkItems: includeWorkItems,
837
+ };
838
+ // Add author filter
839
+ if (author) {
840
+ searchCriteria.author = author;
841
+ }
842
+ // Add date range filters (ADO API expects ISO string format)
843
+ if (fromDate) {
844
+ searchCriteria.fromDate = fromDate;
845
+ }
846
+ if (toDate) {
847
+ searchCriteria.toDate = toDate;
848
+ }
849
+ // Add history simplification if specified
850
+ if (historySimplificationMode) {
851
+ // Note: This parameter might not be directly supported by all ADO API versions
852
+ // but we'll include it in the criteria for forward compatibility
853
+ searchCriteria.historySimplificationMode = historySimplificationMode;
854
+ }
855
+ if (version) {
856
+ const itemVersion = {
857
+ version: version,
858
+ versionType: GitVersionType[versionType],
859
+ };
860
+ searchCriteria.itemVersion = itemVersion;
861
+ }
862
+ const commits = await gitApi.getCommits(repository, searchCriteria, project, skip, top);
863
+ // Additional client-side filtering for enhanced search capabilities
864
+ let filteredCommits = commits;
865
+ // Filter by search text in commit message if not handled by API
866
+ if (searchText && filteredCommits) {
867
+ filteredCommits = filteredCommits.filter((commit) => commit.comment?.toLowerCase().includes(searchText.toLowerCase()));
868
+ }
869
+ // Filter by author email if specified
870
+ if (authorEmail && filteredCommits) {
871
+ filteredCommits = filteredCommits.filter((commit) => commit.author?.email?.toLowerCase() === authorEmail.toLowerCase());
872
+ }
873
+ // Filter by committer if specified
874
+ if (committer && filteredCommits) {
875
+ filteredCommits = filteredCommits.filter((commit) => commit.committer?.name?.toLowerCase().includes(committer.toLowerCase()) || commit.committer?.email?.toLowerCase().includes(committer.toLowerCase()));
876
+ }
877
+ // Filter by committer email if specified
878
+ if (committerEmail && filteredCommits) {
879
+ filteredCommits = filteredCommits.filter((commit) => commit.committer?.email?.toLowerCase() === committerEmail.toLowerCase());
880
+ }
881
+ return {
882
+ content: [{ type: "text", text: JSON.stringify(filteredCommits, null, 2) }],
883
+ };
884
+ }
885
+ catch (error) {
886
+ return {
887
+ content: [
888
+ {
889
+ type: "text",
890
+ text: `Error searching commits: ${error instanceof Error ? error.message : String(error)}`,
891
+ },
892
+ ],
893
+ isError: true,
894
+ };
895
+ }
896
+ });
897
+ const pullRequestQueryTypesStrings = Object.values(GitPullRequestQueryType).filter((value) => typeof value === "string");
898
+ server.tool(REPO_TOOLS.list_pull_requests_by_commits, "Lists pull requests by commit IDs to find which pull requests contain specific commits", {
899
+ project: z.string().describe("Project name or ID"),
900
+ repository: z.string().describe("Repository name or ID"),
901
+ commits: z.array(z.string()).describe("Array of commit IDs to query for"),
902
+ queryType: z
903
+ .enum(pullRequestQueryTypesStrings)
904
+ .optional()
905
+ .default(GitPullRequestQueryType[GitPullRequestQueryType.LastMergeCommit])
906
+ .describe("Type of query to perform"),
907
+ }, async ({ project, repository, commits, queryType }) => {
908
+ try {
909
+ const connection = await connectionProvider();
910
+ const gitApi = await connection.getGitApi();
911
+ const query = {
912
+ queries: [
913
+ {
914
+ items: commits,
915
+ type: GitPullRequestQueryType[queryType],
916
+ },
917
+ ],
918
+ };
919
+ const queryResult = await gitApi.getPullRequestQuery(query, repository, project);
920
+ return {
921
+ content: [{ type: "text", text: JSON.stringify(queryResult, null, 2) }],
922
+ };
923
+ }
924
+ catch (error) {
925
+ return {
926
+ content: [
927
+ {
928
+ type: "text",
929
+ text: `Error querying pull requests by commits: ${error instanceof Error ? error.message : String(error)}`,
930
+ },
931
+ ],
932
+ isError: true,
933
+ };
934
+ }
935
+ });
936
+ }
937
+ export { REPO_TOOLS, configureRepoTools };
938
+ //# sourceMappingURL=repositories.js.map