@azure-devops/mcp 0.1.0 → 0.2.0-preview-oauth

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.
@@ -1,7 +1,9 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
+ import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, CommentThreadStatus, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
3
4
  import { z } from "zod";
4
- import { getCurrentUserDetails } from "./auth.js";
5
+ import { getCurrentUserDetails, getUserIdFromEmail } from "./auth.js";
6
+ import { getEnumKeys } from "../utils.js";
5
7
  const REPO_TOOLS = {
6
8
  list_repos_by_project: "repo_list_repos_by_project",
7
9
  list_pull_requests_by_repo: "repo_list_pull_requests_by_repo",
@@ -14,18 +16,64 @@ const REPO_TOOLS = {
14
16
  get_branch_by_name: "repo_get_branch_by_name",
15
17
  get_pull_request_by_id: "repo_get_pull_request_by_id",
16
18
  create_pull_request: "repo_create_pull_request",
17
- update_pull_request_status: "repo_update_pull_request_status",
19
+ update_pull_request: "repo_update_pull_request",
20
+ update_pull_request_reviewers: "repo_update_pull_request_reviewers",
18
21
  reply_to_comment: "repo_reply_to_comment",
22
+ create_pull_request_thread: "repo_create_pull_request_thread",
19
23
  resolve_comment: "repo_resolve_comment",
24
+ search_commits: "repo_search_commits",
25
+ list_pull_requests_by_commits: "repo_list_pull_requests_by_commits",
20
26
  };
21
27
  function branchesFilterOutIrrelevantProperties(branches, top) {
22
28
  return branches
23
29
  ?.flatMap((branch) => (branch.name ? [branch.name] : []))
24
30
  ?.filter((branch) => branch.startsWith("refs/heads/"))
25
31
  .map((branch) => branch.replace("refs/heads/", ""))
32
+ .sort((a, b) => b.localeCompare(a))
26
33
  .slice(0, top);
27
34
  }
28
- function configureRepoTools(server, tokenProvider, connectionProvider) {
35
+ /**
36
+ * Trims comment data to essential properties, filtering out deleted comments
37
+ * @param comments Array of comments to trim (can be undefined/null)
38
+ * @returns Array of trimmed comment objects with essential properties only
39
+ */
40
+ function trimComments(comments) {
41
+ return comments
42
+ ?.filter((comment) => !comment.isDeleted) // Exclude deleted comments
43
+ ?.map((comment) => ({
44
+ id: comment.id,
45
+ author: {
46
+ displayName: comment.author?.displayName,
47
+ uniqueName: comment.author?.uniqueName,
48
+ },
49
+ content: comment.content,
50
+ publishedDate: comment.publishedDate,
51
+ lastUpdatedDate: comment.lastUpdatedDate,
52
+ lastContentUpdatedDate: comment.lastContentUpdatedDate,
53
+ }));
54
+ }
55
+ function pullRequestStatusStringToInt(status) {
56
+ switch (status) {
57
+ case "Abandoned":
58
+ return PullRequestStatus.Abandoned.valueOf();
59
+ case "Active":
60
+ return PullRequestStatus.Active.valueOf();
61
+ case "All":
62
+ return PullRequestStatus.All.valueOf();
63
+ case "Completed":
64
+ return PullRequestStatus.Completed.valueOf();
65
+ case "NotSet":
66
+ return PullRequestStatus.NotSet.valueOf();
67
+ default:
68
+ throw new Error(`Unknown pull request status: ${status}`);
69
+ }
70
+ }
71
+ function filterReposByName(repositories, repoNameFilter) {
72
+ const lowerCaseFilter = repoNameFilter.toLowerCase();
73
+ const filteredByName = repositories?.filter((repo) => repo.name?.toLowerCase().includes(lowerCaseFilter));
74
+ return filteredByName;
75
+ }
76
+ function configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider) {
29
77
  server.tool(REPO_TOOLS.create_pull_request, "Create a new pull request.", {
30
78
  repositoryId: z.string().describe("The ID of the repository where the pull request will be created."),
31
79
  sourceRefName: z.string().describe("The source branch name for the pull request, e.g., 'refs/heads/feature-branch'."),
@@ -33,43 +81,105 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
33
81
  title: z.string().describe("The title of the pull request."),
34
82
  description: z.string().optional().describe("The description of the pull request. Optional."),
35
83
  isDraft: z.boolean().optional().default(false).describe("Indicates whether the pull request is a draft. Defaults to false."),
36
- }, async ({ repositoryId, sourceRefName, targetRefName, title, description, isDraft, }) => {
84
+ workItems: z.string().optional().describe("Work item IDs to associate with the pull request, space-separated."),
85
+ 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."),
86
+ }, async ({ repositoryId, sourceRefName, targetRefName, title, description, isDraft, workItems, forkSourceRepositoryId }) => {
37
87
  const connection = await connectionProvider();
38
88
  const gitApi = await connection.getGitApi();
89
+ const workItemRefs = workItems ? workItems.split(" ").map((id) => ({ id: id.trim() })) : [];
90
+ const forkSource = forkSourceRepositoryId
91
+ ? {
92
+ repository: {
93
+ id: forkSourceRepositoryId,
94
+ },
95
+ }
96
+ : undefined;
39
97
  const pullRequest = await gitApi.createPullRequest({
40
98
  sourceRefName,
41
99
  targetRefName,
42
100
  title,
43
101
  description,
44
102
  isDraft,
103
+ workItemRefs: workItemRefs,
104
+ forkSource,
45
105
  }, repositoryId);
46
106
  return {
47
107
  content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }],
48
108
  };
49
109
  });
50
- server.tool(REPO_TOOLS.update_pull_request_status, "Update status of an existing pull request to active or abandoned.", {
110
+ server.tool(REPO_TOOLS.update_pull_request, "Update a Pull Request by ID with specified fields.", {
51
111
  repositoryId: z.string().describe("The ID of the repository where the pull request exists."),
52
- pullRequestId: z.number().describe("The ID of the pull request to be published."),
53
- status: z.enum(["active", "abandoned"]).describe("The new status of the pull request. Can be 'active' or 'abandoned'."),
54
- }, async ({ repositoryId, pullRequestId }) => {
112
+ pullRequestId: z.number().describe("The ID of the pull request to update."),
113
+ title: z.string().optional().describe("The new title for the pull request."),
114
+ description: z.string().optional().describe("The new description for the pull request."),
115
+ isDraft: z.boolean().optional().describe("Whether the pull request should be a draft."),
116
+ targetRefName: z.string().optional().describe("The new target branch name (e.g., 'refs/heads/main')."),
117
+ status: z.enum(["Active", "Abandoned"]).optional().describe("The new status of the pull request. Can be 'Active' or 'Abandoned'."),
118
+ }, async ({ repositoryId, pullRequestId, title, description, isDraft, targetRefName, status }) => {
55
119
  const connection = await connectionProvider();
56
120
  const gitApi = await connection.getGitApi();
57
- const statusValue = status === "active" ? 3 : 2;
58
- const updatedPullRequest = await gitApi.updatePullRequest({ status: statusValue }, repositoryId, pullRequestId);
121
+ // Build update object with only provided fields
122
+ const updateRequest = {};
123
+ if (title !== undefined)
124
+ updateRequest.title = title;
125
+ if (description !== undefined)
126
+ updateRequest.description = description;
127
+ if (isDraft !== undefined)
128
+ updateRequest.isDraft = isDraft;
129
+ if (targetRefName !== undefined)
130
+ updateRequest.targetRefName = targetRefName;
131
+ if (status !== undefined) {
132
+ updateRequest.status = status === "Active" ? PullRequestStatus.Active.valueOf() : PullRequestStatus.Abandoned.valueOf();
133
+ }
134
+ // Validate that at least one field is provided for update
135
+ if (Object.keys(updateRequest).length === 0) {
136
+ return {
137
+ content: [{ type: "text", text: "Error: At least one field (title, description, isDraft, targetRefName, or status) must be provided for update." }],
138
+ isError: true,
139
+ };
140
+ }
141
+ const updatedPullRequest = await gitApi.updatePullRequest(updateRequest, repositoryId, pullRequestId);
59
142
  return {
60
- content: [
61
- { type: "text", text: JSON.stringify(updatedPullRequest, null, 2) },
62
- ],
143
+ content: [{ type: "text", text: JSON.stringify(updatedPullRequest, null, 2) }],
63
144
  };
64
145
  });
146
+ server.tool(REPO_TOOLS.update_pull_request_reviewers, "Add or remove reviewers for an existing pull request.", {
147
+ repositoryId: z.string().describe("The ID of the repository where the pull request exists."),
148
+ pullRequestId: z.number().describe("The ID of the pull request to update."),
149
+ reviewerIds: z.array(z.string()).describe("List of reviewer ids to add or remove from the pull request."),
150
+ action: z.enum(["add", "remove"]).describe("Action to perform on the reviewers. Can be 'add' or 'remove'."),
151
+ }, async ({ repositoryId, pullRequestId, reviewerIds, action }) => {
152
+ const connection = await connectionProvider();
153
+ const gitApi = await connection.getGitApi();
154
+ let updatedPullRequest;
155
+ if (action === "add") {
156
+ updatedPullRequest = await gitApi.createPullRequestReviewers(reviewerIds.map((id) => ({ id: id })), repositoryId, pullRequestId);
157
+ return {
158
+ content: [{ type: "text", text: JSON.stringify(updatedPullRequest, null, 2) }],
159
+ };
160
+ }
161
+ else {
162
+ for (const reviewerId of reviewerIds) {
163
+ await gitApi.deletePullRequestReviewer(repositoryId, pullRequestId, reviewerId);
164
+ }
165
+ return {
166
+ content: [{ type: "text", text: `Reviewers with IDs ${reviewerIds.join(", ")} removed from pull request ${pullRequestId}.` }],
167
+ };
168
+ }
169
+ });
65
170
  server.tool(REPO_TOOLS.list_repos_by_project, "Retrieve a list of repositories for a given project", {
66
171
  project: z.string().describe("The name or ID of the Azure DevOps project."),
67
- }, async ({ project }) => {
172
+ top: z.number().default(100).describe("The maximum number of repositories to return."),
173
+ skip: z.number().default(0).describe("The number of repositories to skip. Defaults to 0."),
174
+ 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."),
175
+ }, async ({ project, top, skip, repoNameFilter }) => {
68
176
  const connection = await connectionProvider();
69
177
  const gitApi = await connection.getGitApi();
70
178
  const repositories = await gitApi.getRepositories(project, false, false, false);
179
+ const filteredRepositories = repoNameFilter ? filterReposByName(repositories, repoNameFilter) : repositories;
180
+ const paginatedRepositories = filteredRepositories?.sort((a, b) => a.name?.localeCompare(b.name ?? "") ?? 0).slice(skip, skip + top);
71
181
  // Filter out the irrelevant properties
72
- const filteredRepositories = repositories?.map((repo) => ({
182
+ const trimmedRepositories = paginatedRepositories?.map((repo) => ({
73
183
  id: repo.id,
74
184
  name: repo.name,
75
185
  isDisabled: repo.isDisabled,
@@ -79,25 +189,47 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
79
189
  size: repo.size,
80
190
  }));
81
191
  return {
82
- content: [
83
- { type: "text", text: JSON.stringify(filteredRepositories, null, 2) },
84
- ],
192
+ content: [{ type: "text", text: JSON.stringify(trimmedRepositories, null, 2) }],
85
193
  };
86
194
  });
87
195
  server.tool(REPO_TOOLS.list_pull_requests_by_repo, "Retrieve a list of pull requests for a given repository.", {
88
196
  repositoryId: z.string().describe("The ID of the repository where the pull requests are located."),
197
+ top: z.number().default(100).describe("The maximum number of pull requests to return."),
198
+ skip: z.number().default(0).describe("The number of pull requests to skip."),
89
199
  created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
200
+ created_by_user: z.string().optional().describe("Filter pull requests created by a specific user (provide email or unique name). Takes precedence over created_by_me if both are provided."),
90
201
  i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
91
- }, async ({ repositoryId, created_by_me, i_am_reviewer }) => {
202
+ status: z
203
+ .enum(getEnumKeys(PullRequestStatus))
204
+ .default("Active")
205
+ .describe("Filter pull requests by status. Defaults to 'Active'."),
206
+ }, async ({ repositoryId, top, skip, created_by_me, created_by_user, i_am_reviewer, status }) => {
92
207
  const connection = await connectionProvider();
93
208
  const gitApi = await connection.getGitApi();
94
209
  // Build the search criteria
95
210
  const searchCriteria = {
96
- status: 1,
211
+ status: pullRequestStatusStringToInt(status),
97
212
  repositoryId: repositoryId,
98
213
  };
99
- if (created_by_me || i_am_reviewer) {
100
- const data = await getCurrentUserDetails(tokenProvider, connectionProvider);
214
+ if (created_by_user) {
215
+ try {
216
+ const userId = await getUserIdFromEmail(created_by_user, tokenProvider, connectionProvider, userAgentProvider);
217
+ searchCriteria.creatorId = userId;
218
+ }
219
+ catch (error) {
220
+ return {
221
+ content: [
222
+ {
223
+ type: "text",
224
+ text: `Error finding user with email ${created_by_user}: ${error instanceof Error ? error.message : String(error)}`,
225
+ },
226
+ ],
227
+ isError: true,
228
+ };
229
+ }
230
+ }
231
+ else if (created_by_me || i_am_reviewer) {
232
+ const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
101
233
  const userId = data.authenticatedUser.id;
102
234
  if (created_by_me) {
103
235
  searchCriteria.creatorId = userId;
@@ -106,7 +238,9 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
106
238
  searchCriteria.reviewerId = userId;
107
239
  }
108
240
  }
109
- const pullRequests = await gitApi.getPullRequests(repositoryId, searchCriteria);
241
+ const pullRequests = await gitApi.getPullRequests(repositoryId, searchCriteria, undefined, // project
242
+ undefined, // maxCommentLength
243
+ skip, top);
110
244
  // Filter out the irrelevant properties
111
245
  const filteredPullRequests = pullRequests?.map((pr) => ({
112
246
  pullRequestId: pr.pullRequestId,
@@ -119,26 +253,50 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
119
253
  creationDate: pr.creationDate,
120
254
  title: pr.title,
121
255
  isDraft: pr.isDraft,
256
+ sourceRefName: pr.sourceRefName,
257
+ targetRefName: pr.targetRefName,
122
258
  }));
123
259
  return {
124
- content: [
125
- { type: "text", text: JSON.stringify(filteredPullRequests, null, 2) },
126
- ],
260
+ content: [{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) }],
127
261
  };
128
262
  });
129
263
  server.tool(REPO_TOOLS.list_pull_requests_by_project, "Retrieve a list of pull requests for a given project Id or Name.", {
130
264
  project: z.string().describe("The name or ID of the Azure DevOps project."),
265
+ top: z.number().default(100).describe("The maximum number of pull requests to return."),
266
+ skip: z.number().default(0).describe("The number of pull requests to skip."),
131
267
  created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
268
+ created_by_user: z.string().optional().describe("Filter pull requests created by a specific user (provide email or unique name). Takes precedence over created_by_me if both are provided."),
132
269
  i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
133
- }, async ({ project, created_by_me, i_am_reviewer }) => {
270
+ status: z
271
+ .enum(getEnumKeys(PullRequestStatus))
272
+ .default("Active")
273
+ .describe("Filter pull requests by status. Defaults to 'Active'."),
274
+ }, async ({ project, top, skip, created_by_me, created_by_user, i_am_reviewer, status }) => {
134
275
  const connection = await connectionProvider();
135
276
  const gitApi = await connection.getGitApi();
136
277
  // Build the search criteria
137
278
  const gitPullRequestSearchCriteria = {
138
- status: 1,
279
+ status: pullRequestStatusStringToInt(status),
139
280
  };
140
- if (created_by_me || i_am_reviewer) {
141
- const data = await getCurrentUserDetails(tokenProvider, connectionProvider);
281
+ if (created_by_user) {
282
+ try {
283
+ const userId = await getUserIdFromEmail(created_by_user, tokenProvider, connectionProvider, userAgentProvider);
284
+ gitPullRequestSearchCriteria.creatorId = userId;
285
+ }
286
+ catch (error) {
287
+ return {
288
+ content: [
289
+ {
290
+ type: "text",
291
+ text: `Error finding user with email ${created_by_user}: ${error instanceof Error ? error.message : String(error)}`,
292
+ },
293
+ ],
294
+ isError: true,
295
+ };
296
+ }
297
+ }
298
+ else if (created_by_me || i_am_reviewer) {
299
+ const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
142
300
  const userId = data.authenticatedUser.id;
143
301
  if (created_by_me) {
144
302
  gitPullRequestSearchCriteria.creatorId = userId;
@@ -147,7 +305,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
147
305
  gitPullRequestSearchCriteria.reviewerId = userId;
148
306
  }
149
307
  }
150
- const pullRequests = await gitApi.getPullRequestsByProject(project, gitPullRequestSearchCriteria);
308
+ const pullRequests = await gitApi.getPullRequestsByProject(project, gitPullRequestSearchCriteria, undefined, // maxCommentLength
309
+ skip, top);
151
310
  // Filter out the irrelevant properties
152
311
  const filteredPullRequests = pullRequests?.map((pr) => ({
153
312
  pullRequestId: pr.pullRequestId,
@@ -161,11 +320,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
161
320
  creationDate: pr.creationDate,
162
321
  title: pr.title,
163
322
  isDraft: pr.isDraft,
323
+ sourceRefName: pr.sourceRefName,
324
+ targetRefName: pr.targetRefName,
164
325
  }));
165
326
  return {
166
- content: [
167
- { type: "text", text: JSON.stringify(filteredPullRequests, null, 2) },
168
- ],
327
+ content: [{ type: "text", text: JSON.stringify(filteredPullRequests, null, 2) }],
169
328
  };
170
329
  });
171
330
  server.tool(REPO_TOOLS.list_pull_request_threads, "Retrieve a list of comment threads for a pull request.", {
@@ -174,12 +333,29 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
174
333
  project: z.string().optional().describe("Project ID or project name (optional)"),
175
334
  iteration: z.number().optional().describe("The iteration ID for which to retrieve threads. Optional, defaults to the latest iteration."),
176
335
  baseIteration: z.number().optional().describe("The base iteration ID for which to retrieve threads. Optional, defaults to the latest base iteration."),
177
- }, async ({ repositoryId, pullRequestId, project, iteration, baseIteration, }) => {
336
+ top: z.number().default(100).describe("The maximum number of threads to return."),
337
+ skip: z.number().default(0).describe("The number of threads to skip."),
338
+ fullResponse: z.boolean().optional().default(false).describe("Return full thread JSON response instead of trimmed data."),
339
+ }, async ({ repositoryId, pullRequestId, project, iteration, baseIteration, top, skip, fullResponse }) => {
178
340
  const connection = await connectionProvider();
179
341
  const gitApi = await connection.getGitApi();
180
342
  const threads = await gitApi.getThreads(repositoryId, pullRequestId, project, iteration, baseIteration);
343
+ const paginatedThreads = threads?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
344
+ if (fullResponse) {
345
+ return {
346
+ content: [{ type: "text", text: JSON.stringify(paginatedThreads, null, 2) }],
347
+ };
348
+ }
349
+ // Return trimmed thread data focusing on essential information
350
+ const trimmedThreads = paginatedThreads?.map((thread) => ({
351
+ id: thread.id,
352
+ publishedDate: thread.publishedDate,
353
+ lastUpdatedDate: thread.lastUpdatedDate,
354
+ status: thread.status,
355
+ comments: trimComments(thread.comments),
356
+ }));
181
357
  return {
182
- content: [{ type: "text", text: JSON.stringify(threads, null, 2) }],
358
+ content: [{ type: "text", text: JSON.stringify(trimmedThreads, null, 2) }],
183
359
  };
184
360
  });
185
361
  server.tool(REPO_TOOLS.list_pull_request_thread_comments, "Retrieve a list of comments in a pull request thread.", {
@@ -187,37 +363,50 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
187
363
  pullRequestId: z.number().describe("The ID of the pull request for which to retrieve thread comments."),
188
364
  threadId: z.number().describe("The ID of the thread for which to retrieve comments."),
189
365
  project: z.string().optional().describe("Project ID or project name (optional)"),
190
- }, async ({ repositoryId, pullRequestId, threadId, project }) => {
366
+ top: z.number().default(100).describe("The maximum number of comments to return."),
367
+ skip: z.number().default(0).describe("The number of comments to skip."),
368
+ fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of trimmed data."),
369
+ }, async ({ repositoryId, pullRequestId, threadId, project, top, skip, fullResponse }) => {
191
370
  const connection = await connectionProvider();
192
371
  const gitApi = await connection.getGitApi();
193
372
  // Get thread comments - GitApi uses getComments for retrieving comments from a specific thread
194
373
  const comments = await gitApi.getComments(repositoryId, pullRequestId, threadId, project);
374
+ const paginatedComments = comments?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
375
+ if (fullResponse) {
376
+ return {
377
+ content: [{ type: "text", text: JSON.stringify(paginatedComments, null, 2) }],
378
+ };
379
+ }
380
+ // Return trimmed comment data focusing on essential information
381
+ const trimmedComments = trimComments(paginatedComments);
195
382
  return {
196
- content: [{ type: "text", text: JSON.stringify(comments, null, 2) }],
383
+ content: [{ type: "text", text: JSON.stringify(trimmedComments, null, 2) }],
197
384
  };
198
385
  });
199
386
  server.tool(REPO_TOOLS.list_branches_by_repo, "Retrieve a list of branches for a given repository.", {
200
387
  repositoryId: z.string().describe("The ID of the repository where the branches are located."),
201
388
  top: z.number().default(100).describe("The maximum number of branches to return. Defaults to 100."),
202
- }, async ({ repositoryId, top }) => {
389
+ filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
390
+ }, async ({ repositoryId, top, filterContains }) => {
203
391
  const connection = await connectionProvider();
204
392
  const gitApi = await connection.getGitApi();
205
- const branches = await gitApi.getRefs(repositoryId, undefined);
393
+ const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", undefined, undefined, undefined, undefined, undefined, filterContains);
206
394
  const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
207
395
  return {
208
- content: [
209
- { type: "text", text: JSON.stringify(filteredBranches, null, 2) },
210
- ],
396
+ content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
211
397
  };
212
398
  });
213
399
  server.tool(REPO_TOOLS.list_my_branches_by_repo, "Retrieve a list of my branches for a given repository Id.", {
214
400
  repositoryId: z.string().describe("The ID of the repository where the branches are located."),
215
- }, async ({ repositoryId }) => {
401
+ top: z.number().default(100).describe("The maximum number of branches to return."),
402
+ filterContains: z.string().optional().describe("Filter to find branches that contain this string in their name."),
403
+ }, async ({ repositoryId, top, filterContains }) => {
216
404
  const connection = await connectionProvider();
217
405
  const gitApi = await connection.getGitApi();
218
- const branches = await gitApi.getRefs(repositoryId, undefined, undefined, undefined, undefined, true);
406
+ const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", undefined, undefined, true, undefined, undefined, filterContains);
407
+ const filteredBranches = branchesFilterOutIrrelevantProperties(branches, top);
219
408
  return {
220
- content: [{ type: "text", text: JSON.stringify(branches, null, 2) }],
409
+ content: [{ type: "text", text: JSON.stringify(filteredBranches, null, 2) }],
221
410
  };
222
411
  });
223
412
  server.tool(REPO_TOOLS.get_repo_by_name_or_id, "Get the repository by project and repository name or ID.", {
@@ -241,8 +430,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
241
430
  }, async ({ repositoryId, branchName }) => {
242
431
  const connection = await connectionProvider();
243
432
  const gitApi = await connection.getGitApi();
244
- const branches = await gitApi.getRefs(repositoryId);
245
- const branch = branches?.find((branch) => branch.name === `refs/heads/${branchName}`);
433
+ const branches = await gitApi.getRefs(repositoryId, undefined, "heads/", false, false, undefined, false, undefined, branchName);
434
+ const branch = branches.find((branch) => branch.name === `refs/heads/${branchName}` || branch.name === branchName);
246
435
  if (!branch) {
247
436
  return {
248
437
  content: [
@@ -251,6 +440,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
251
440
  text: `Branch ${branchName} not found in repository ${repositoryId}`,
252
441
  },
253
442
  ],
443
+ isError: true,
254
444
  };
255
445
  }
256
446
  return {
@@ -260,10 +450,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
260
450
  server.tool(REPO_TOOLS.get_pull_request_by_id, "Get a pull request by its ID.", {
261
451
  repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
262
452
  pullRequestId: z.number().describe("The ID of the pull request to retrieve."),
263
- }, async ({ repositoryId, pullRequestId }) => {
453
+ includeWorkItemRefs: z.boolean().optional().default(false).describe("Whether to reference work items associated with the pull request."),
454
+ }, async ({ repositoryId, pullRequestId, includeWorkItemRefs }) => {
264
455
  const connection = await connectionProvider();
265
456
  const gitApi = await connection.getGitApi();
266
- const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId);
457
+ const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId, undefined, undefined, undefined, undefined, undefined, includeWorkItemRefs);
267
458
  return {
268
459
  content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }],
269
460
  };
@@ -274,26 +465,202 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
274
465
  threadId: z.number().describe("The ID of the thread to which the comment will be added."),
275
466
  content: z.string().describe("The content of the comment to be added."),
276
467
  project: z.string().optional().describe("Project ID or project name (optional)"),
277
- }, async ({ repositoryId, pullRequestId, threadId, content, project }) => {
468
+ fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of a simple confirmation message."),
469
+ }, async ({ repositoryId, pullRequestId, threadId, content, project, fullResponse }) => {
278
470
  const connection = await connectionProvider();
279
471
  const gitApi = await connection.getGitApi();
280
472
  const comment = await gitApi.createComment({ content }, repositoryId, pullRequestId, threadId, project);
473
+ // Check if the comment was successfully created
474
+ if (!comment) {
475
+ return {
476
+ content: [{ type: "text", text: `Error: Failed to add comment to thread ${threadId}. The comment was not created successfully.` }],
477
+ isError: true,
478
+ };
479
+ }
480
+ if (fullResponse) {
481
+ return {
482
+ content: [{ type: "text", text: JSON.stringify(comment, null, 2) }],
483
+ };
484
+ }
281
485
  return {
282
- content: [{ type: "text", text: JSON.stringify(comment, null, 2) }],
486
+ content: [{ type: "text", text: `Comment successfully added to thread ${threadId}.` }],
487
+ };
488
+ });
489
+ server.tool(REPO_TOOLS.create_pull_request_thread, "Creates a new comment thread on a pull request.", {
490
+ repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
491
+ pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
492
+ content: z.string().describe("The content of the comment to be added."),
493
+ project: z.string().optional().describe("Project ID or project name (optional)"),
494
+ filePath: z.string().optional().describe("The path of the file where the comment thread will be created. (optional)"),
495
+ status: z
496
+ .enum(getEnumKeys(CommentThreadStatus))
497
+ .optional()
498
+ .default(CommentThreadStatus[CommentThreadStatus.Active])
499
+ .describe("The status of the comment thread. Defaults to 'Active'."),
500
+ 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)"),
501
+ rightFileStartOffset: z
502
+ .number()
503
+ .optional()
504
+ .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)"),
505
+ rightFileEndLine: z
506
+ .number()
507
+ .optional()
508
+ .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)"),
509
+ rightFileEndOffset: z
510
+ .number()
511
+ .optional()
512
+ .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)"),
513
+ }, async ({ repositoryId, pullRequestId, content, project, filePath, status, rightFileStartLine, rightFileStartOffset, rightFileEndLine, rightFileEndOffset }) => {
514
+ const connection = await connectionProvider();
515
+ const gitApi = await connection.getGitApi();
516
+ const threadContext = { filePath: filePath };
517
+ if (rightFileStartLine !== undefined) {
518
+ if (rightFileStartLine < 1) {
519
+ throw new Error("rightFileStartLine must be greater than or equal to 1.");
520
+ }
521
+ threadContext.rightFileStart = { line: rightFileStartLine };
522
+ if (rightFileStartOffset !== undefined) {
523
+ if (rightFileStartOffset < 1) {
524
+ throw new Error("rightFileStartOffset must be greater than or equal to 1.");
525
+ }
526
+ threadContext.rightFileStart.offset = rightFileStartOffset;
527
+ }
528
+ }
529
+ if (rightFileEndLine !== undefined) {
530
+ if (rightFileStartLine === undefined) {
531
+ throw new Error("rightFileEndLine must only be specified if rightFileStartLine is also specified.");
532
+ }
533
+ if (rightFileEndLine < 1) {
534
+ throw new Error("rightFileEndLine must be greater than or equal to 1.");
535
+ }
536
+ threadContext.rightFileEnd = { line: rightFileEndLine };
537
+ if (rightFileEndOffset !== undefined) {
538
+ if (rightFileEndOffset < 1) {
539
+ throw new Error("rightFileEndOffset must be greater than or equal to 1.");
540
+ }
541
+ threadContext.rightFileEnd.offset = rightFileEndOffset;
542
+ }
543
+ }
544
+ const thread = await gitApi.createThread({ comments: [{ content: content }], threadContext: threadContext, status: CommentThreadStatus[status] }, repositoryId, pullRequestId, project);
545
+ return {
546
+ content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
283
547
  };
284
548
  });
285
549
  server.tool(REPO_TOOLS.resolve_comment, "Resolves a specific comment thread on a pull request.", {
286
550
  repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
287
551
  pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
288
552
  threadId: z.number().describe("The ID of the thread to be resolved."),
289
- }, async ({ repositoryId, pullRequestId, threadId }) => {
553
+ fullResponse: z.boolean().optional().default(false).describe("Return full thread JSON response instead of a simple confirmation message."),
554
+ }, async ({ repositoryId, pullRequestId, threadId, fullResponse }) => {
290
555
  const connection = await connectionProvider();
291
556
  const gitApi = await connection.getGitApi();
292
557
  const thread = await gitApi.updateThread({ status: 2 }, // 2 corresponds to "Resolved" status
293
558
  repositoryId, pullRequestId, threadId);
559
+ // Check if the thread was successfully resolved
560
+ if (!thread) {
561
+ return {
562
+ content: [{ type: "text", text: `Error: Failed to resolve thread ${threadId}. The thread status was not updated successfully.` }],
563
+ isError: true,
564
+ };
565
+ }
566
+ if (fullResponse) {
567
+ return {
568
+ content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
569
+ };
570
+ }
294
571
  return {
295
- content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
572
+ content: [{ type: "text", text: `Thread ${threadId} was successfully resolved.` }],
296
573
  };
297
574
  });
575
+ const gitVersionTypeStrings = Object.values(GitVersionType).filter((value) => typeof value === "string");
576
+ server.tool(REPO_TOOLS.search_commits, "Searches for commits in a repository", {
577
+ project: z.string().describe("Project name or ID"),
578
+ repository: z.string().describe("Repository name or ID"),
579
+ fromCommit: z.string().optional().describe("Starting commit ID"),
580
+ toCommit: z.string().optional().describe("Ending commit ID"),
581
+ version: z.string().optional().describe("The name of the branch, tag or commit to filter commits by"),
582
+ versionType: z
583
+ .enum(gitVersionTypeStrings)
584
+ .optional()
585
+ .default(GitVersionType[GitVersionType.Branch])
586
+ .describe("The meaning of the version parameter, e.g., branch, tag or commit"),
587
+ skip: z.number().optional().default(0).describe("Number of commits to skip"),
588
+ top: z.number().optional().default(10).describe("Maximum number of commits to return"),
589
+ includeLinks: z.boolean().optional().default(false).describe("Include commit links"),
590
+ includeWorkItems: z.boolean().optional().default(false).describe("Include associated work items"),
591
+ }, async ({ project, repository, fromCommit, toCommit, version, versionType, skip, top, includeLinks, includeWorkItems }) => {
592
+ try {
593
+ const connection = await connectionProvider();
594
+ const gitApi = await connection.getGitApi();
595
+ const searchCriteria = {
596
+ fromCommitId: fromCommit,
597
+ toCommitId: toCommit,
598
+ includeLinks: includeLinks,
599
+ includeWorkItems: includeWorkItems,
600
+ };
601
+ if (version) {
602
+ const itemVersion = {
603
+ version: version,
604
+ versionType: GitVersionType[versionType],
605
+ };
606
+ searchCriteria.itemVersion = itemVersion;
607
+ }
608
+ const commits = await gitApi.getCommits(repository, searchCriteria, project, skip, // skip
609
+ top);
610
+ return {
611
+ content: [{ type: "text", text: JSON.stringify(commits, null, 2) }],
612
+ };
613
+ }
614
+ catch (error) {
615
+ return {
616
+ content: [
617
+ {
618
+ type: "text",
619
+ text: `Error searching commits: ${error instanceof Error ? error.message : String(error)}`,
620
+ },
621
+ ],
622
+ isError: true,
623
+ };
624
+ }
625
+ });
626
+ const pullRequestQueryTypesStrings = Object.values(GitPullRequestQueryType).filter((value) => typeof value === "string");
627
+ server.tool(REPO_TOOLS.list_pull_requests_by_commits, "Lists pull requests by commit IDs to find which pull requests contain specific commits", {
628
+ project: z.string().describe("Project name or ID"),
629
+ repository: z.string().describe("Repository name or ID"),
630
+ commits: z.array(z.string()).describe("Array of commit IDs to query for"),
631
+ queryType: z
632
+ .enum(pullRequestQueryTypesStrings)
633
+ .optional()
634
+ .default(GitPullRequestQueryType[GitPullRequestQueryType.LastMergeCommit])
635
+ .describe("Type of query to perform"),
636
+ }, async ({ project, repository, commits, queryType }) => {
637
+ try {
638
+ const connection = await connectionProvider();
639
+ const gitApi = await connection.getGitApi();
640
+ const query = {
641
+ queries: [
642
+ {
643
+ items: commits,
644
+ type: GitPullRequestQueryType[queryType],
645
+ },
646
+ ],
647
+ };
648
+ const queryResult = await gitApi.getPullRequestQuery(query, repository, project);
649
+ return {
650
+ content: [{ type: "text", text: JSON.stringify(queryResult, null, 2) }],
651
+ };
652
+ }
653
+ catch (error) {
654
+ return {
655
+ content: [
656
+ {
657
+ type: "text",
658
+ text: `Error querying pull requests by commits: ${error instanceof Error ? error.message : String(error)}`,
659
+ },
660
+ ],
661
+ isError: true,
662
+ };
663
+ }
664
+ });
298
665
  }
299
666
  export { REPO_TOOLS, configureRepoTools };