@azure-devops/mcp 1.2.0-daily.20250715 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@
3
3
  import { PullRequestStatus, GitVersionType, GitPullRequestQueryType, } from "azure-devops-node-api/interfaces/GitInterfaces.js";
4
4
  import { z } from "zod";
5
5
  import { getCurrentUserDetails } from "./auth.js";
6
+ import { getEnumKeys } from "../utils.js";
6
7
  const REPO_TOOLS = {
7
8
  list_repos_by_project: "repo_list_repos_by_project",
8
9
  list_pull_requests_by_repo: "repo_list_pull_requests_by_repo",
@@ -18,6 +19,7 @@ const REPO_TOOLS = {
18
19
  update_pull_request_status: "repo_update_pull_request_status",
19
20
  update_pull_request_reviewers: "repo_update_pull_request_reviewers",
20
21
  reply_to_comment: "repo_reply_to_comment",
22
+ create_pull_request_thread: "repo_create_pull_request_thread",
21
23
  resolve_comment: "repo_resolve_comment",
22
24
  search_commits: "repo_search_commits",
23
25
  list_pull_requests_by_commits: "repo_list_pull_requests_by_commits",
@@ -30,17 +32,37 @@ function branchesFilterOutIrrelevantProperties(branches, top) {
30
32
  .sort((a, b) => b.localeCompare(a))
31
33
  .slice(0, top);
32
34
  }
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
+ }
33
55
  function pullRequestStatusStringToInt(status) {
34
56
  switch (status) {
35
- case "abandoned":
57
+ case "Abandoned":
36
58
  return PullRequestStatus.Abandoned.valueOf();
37
- case "active":
59
+ case "Active":
38
60
  return PullRequestStatus.Active.valueOf();
39
- case "all":
61
+ case "All":
40
62
  return PullRequestStatus.All.valueOf();
41
- case "completed":
63
+ case "Completed":
42
64
  return PullRequestStatus.Completed.valueOf();
43
- case "notSet":
65
+ case "NotSet":
44
66
  return PullRequestStatus.NotSet.valueOf();
45
67
  default:
46
68
  throw new Error(`Unknown pull request status: ${status}`);
@@ -79,11 +101,11 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
79
101
  server.tool(REPO_TOOLS.update_pull_request_status, "Update status of an existing pull request to active or abandoned.", {
80
102
  repositoryId: z.string().describe("The ID of the repository where the pull request exists."),
81
103
  pullRequestId: z.number().describe("The ID of the pull request to be published."),
82
- status: z.enum(["active", "abandoned"]).describe("The new status of the pull request. Can be 'active' or 'abandoned'."),
104
+ status: z.enum(["Active", "Abandoned"]).describe("The new status of the pull request. Can be 'Active' or 'Abandoned'."),
83
105
  }, async ({ repositoryId, pullRequestId, status }) => {
84
106
  const connection = await connectionProvider();
85
107
  const gitApi = await connection.getGitApi();
86
- const statusValue = status === "active" ? 3 : 2;
108
+ const statusValue = status === "Active" ? PullRequestStatus.Active.valueOf() : PullRequestStatus.Abandoned.valueOf();
87
109
  const updatedPullRequest = await gitApi.updatePullRequest({ status: statusValue }, repositoryId, pullRequestId);
88
110
  return {
89
111
  content: [{ type: "text", text: JSON.stringify(updatedPullRequest, null, 2) }],
@@ -144,7 +166,10 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
144
166
  skip: z.number().default(0).describe("The number of pull requests to skip."),
145
167
  created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
146
168
  i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
147
- status: z.enum(["abandoned", "active", "all", "completed", "notSet"]).default("active").describe("Filter pull requests by status. Defaults to 'active'."),
169
+ status: z
170
+ .enum(getEnumKeys(PullRequestStatus))
171
+ .default("Active")
172
+ .describe("Filter pull requests by status. Defaults to 'Active'."),
148
173
  }, async ({ repositoryId, top, skip, created_by_me, i_am_reviewer, status }) => {
149
174
  const connection = await connectionProvider();
150
175
  const gitApi = await connection.getGitApi();
@@ -189,7 +214,10 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
189
214
  skip: z.number().default(0).describe("The number of pull requests to skip."),
190
215
  created_by_me: z.boolean().default(false).describe("Filter pull requests created by the current user."),
191
216
  i_am_reviewer: z.boolean().default(false).describe("Filter pull requests where the current user is a reviewer."),
192
- status: z.enum(["abandoned", "active", "all", "completed", "notSet"]).default("active").describe("Filter pull requests by status. Defaults to 'active'."),
217
+ status: z
218
+ .enum(getEnumKeys(PullRequestStatus))
219
+ .default("Active")
220
+ .describe("Filter pull requests by status. Defaults to 'Active'."),
193
221
  }, async ({ project, top, skip, created_by_me, i_am_reviewer, status }) => {
194
222
  const connection = await connectionProvider();
195
223
  const gitApi = await connection.getGitApi();
@@ -235,13 +263,27 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
235
263
  baseIteration: z.number().optional().describe("The base iteration ID for which to retrieve threads. Optional, defaults to the latest base iteration."),
236
264
  top: z.number().default(100).describe("The maximum number of threads to return."),
237
265
  skip: z.number().default(0).describe("The number of threads to skip."),
238
- }, async ({ repositoryId, pullRequestId, project, iteration, baseIteration, top, skip }) => {
266
+ fullResponse: z.boolean().optional().default(false).describe("Return full thread JSON response instead of trimmed data."),
267
+ }, async ({ repositoryId, pullRequestId, project, iteration, baseIteration, top, skip, fullResponse }) => {
239
268
  const connection = await connectionProvider();
240
269
  const gitApi = await connection.getGitApi();
241
270
  const threads = await gitApi.getThreads(repositoryId, pullRequestId, project, iteration, baseIteration);
242
271
  const paginatedThreads = threads?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
272
+ if (fullResponse) {
273
+ return {
274
+ content: [{ type: "text", text: JSON.stringify(paginatedThreads, null, 2) }],
275
+ };
276
+ }
277
+ // Return trimmed thread data focusing on essential information
278
+ const trimmedThreads = paginatedThreads?.map((thread) => ({
279
+ id: thread.id,
280
+ publishedDate: thread.publishedDate,
281
+ lastUpdatedDate: thread.lastUpdatedDate,
282
+ status: thread.status,
283
+ comments: trimComments(thread.comments),
284
+ }));
243
285
  return {
244
- content: [{ type: "text", text: JSON.stringify(paginatedThreads, null, 2) }],
286
+ content: [{ type: "text", text: JSON.stringify(trimmedThreads, null, 2) }],
245
287
  };
246
288
  });
247
289
  server.tool(REPO_TOOLS.list_pull_request_thread_comments, "Retrieve a list of comments in a pull request thread.", {
@@ -251,14 +293,22 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
251
293
  project: z.string().optional().describe("Project ID or project name (optional)"),
252
294
  top: z.number().default(100).describe("The maximum number of comments to return."),
253
295
  skip: z.number().default(0).describe("The number of comments to skip."),
254
- }, async ({ repositoryId, pullRequestId, threadId, project, top, skip }) => {
296
+ fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of trimmed data."),
297
+ }, async ({ repositoryId, pullRequestId, threadId, project, top, skip, fullResponse }) => {
255
298
  const connection = await connectionProvider();
256
299
  const gitApi = await connection.getGitApi();
257
300
  // Get thread comments - GitApi uses getComments for retrieving comments from a specific thread
258
301
  const comments = await gitApi.getComments(repositoryId, pullRequestId, threadId, project);
259
302
  const paginatedComments = comments?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
303
+ if (fullResponse) {
304
+ return {
305
+ content: [{ type: "text", text: JSON.stringify(paginatedComments, null, 2) }],
306
+ };
307
+ }
308
+ // Return trimmed comment data focusing on essential information
309
+ const trimmedComments = trimComments(paginatedComments);
260
310
  return {
261
- content: [{ type: "text", text: JSON.stringify(paginatedComments, null, 2) }],
311
+ content: [{ type: "text", text: JSON.stringify(trimmedComments, null, 2) }],
262
312
  };
263
313
  });
264
314
  server.tool(REPO_TOOLS.list_branches_by_repo, "Retrieve a list of branches for a given repository.", {
@@ -339,25 +389,106 @@ function configureRepoTools(server, tokenProvider, connectionProvider) {
339
389
  threadId: z.number().describe("The ID of the thread to which the comment will be added."),
340
390
  content: z.string().describe("The content of the comment to be added."),
341
391
  project: z.string().optional().describe("Project ID or project name (optional)"),
342
- }, async ({ repositoryId, pullRequestId, threadId, content, project }) => {
392
+ fullResponse: z.boolean().optional().default(false).describe("Return full comment JSON response instead of a simple confirmation message."),
393
+ }, async ({ repositoryId, pullRequestId, threadId, content, project, fullResponse }) => {
343
394
  const connection = await connectionProvider();
344
395
  const gitApi = await connection.getGitApi();
345
396
  const comment = await gitApi.createComment({ content }, repositoryId, pullRequestId, threadId, project);
397
+ // Check if the comment was successfully created
398
+ if (!comment) {
399
+ return {
400
+ content: [{ type: "text", text: `Error: Failed to add comment to thread ${threadId}. The comment was not created successfully.` }],
401
+ isError: true,
402
+ };
403
+ }
404
+ if (fullResponse) {
405
+ return {
406
+ content: [{ type: "text", text: JSON.stringify(comment, null, 2) }],
407
+ };
408
+ }
346
409
  return {
347
- content: [{ type: "text", text: JSON.stringify(comment, null, 2) }],
410
+ content: [{ type: "text", text: `Comment successfully added to thread ${threadId}.` }],
411
+ };
412
+ });
413
+ server.tool(REPO_TOOLS.create_pull_request_thread, "Creates a new comment thread on a pull request.", {
414
+ repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
415
+ pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
416
+ content: z.string().describe("The content of the comment to be added."),
417
+ project: z.string().optional().describe("Project ID or project name (optional)"),
418
+ filePath: z.string().optional().describe("The path of the file where the comment thread will be created. (optional)"),
419
+ 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)"),
420
+ rightFileStartOffset: z
421
+ .number()
422
+ .optional()
423
+ .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)"),
424
+ rightFileEndLine: z
425
+ .number()
426
+ .optional()
427
+ .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)"),
428
+ rightFileEndOffset: z
429
+ .number()
430
+ .optional()
431
+ .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)"),
432
+ }, async ({ repositoryId, pullRequestId, content, project, filePath, rightFileStartLine, rightFileStartOffset, rightFileEndLine, rightFileEndOffset }) => {
433
+ const connection = await connectionProvider();
434
+ const gitApi = await connection.getGitApi();
435
+ const threadContext = { filePath: filePath };
436
+ if (rightFileStartLine !== undefined) {
437
+ if (rightFileStartLine < 1) {
438
+ throw new Error("rightFileStartLine must be greater than or equal to 1.");
439
+ }
440
+ threadContext.rightFileStart = { line: rightFileStartLine };
441
+ if (rightFileStartOffset !== undefined) {
442
+ if (rightFileStartOffset < 1) {
443
+ throw new Error("rightFileStartOffset must be greater than or equal to 1.");
444
+ }
445
+ threadContext.rightFileStart.offset = rightFileStartOffset;
446
+ }
447
+ }
448
+ if (rightFileEndLine !== undefined) {
449
+ if (rightFileStartLine === undefined) {
450
+ throw new Error("rightFileEndLine must only be specified if rightFileStartLine is also specified.");
451
+ }
452
+ if (rightFileEndLine < 1) {
453
+ throw new Error("rightFileEndLine must be greater than or equal to 1.");
454
+ }
455
+ threadContext.rightFileEnd = { line: rightFileEndLine };
456
+ if (rightFileEndOffset !== undefined) {
457
+ if (rightFileEndOffset < 1) {
458
+ throw new Error("rightFileEndOffset must be greater than or equal to 1.");
459
+ }
460
+ threadContext.rightFileEnd.offset = rightFileEndOffset;
461
+ }
462
+ }
463
+ const thread = await gitApi.createThread({ comments: [{ content: content }], threadContext: threadContext }, repositoryId, pullRequestId, project);
464
+ return {
465
+ content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
348
466
  };
349
467
  });
350
468
  server.tool(REPO_TOOLS.resolve_comment, "Resolves a specific comment thread on a pull request.", {
351
469
  repositoryId: z.string().describe("The ID of the repository where the pull request is located."),
352
470
  pullRequestId: z.number().describe("The ID of the pull request where the comment thread exists."),
353
471
  threadId: z.number().describe("The ID of the thread to be resolved."),
354
- }, async ({ repositoryId, pullRequestId, threadId }) => {
472
+ fullResponse: z.boolean().optional().default(false).describe("Return full thread JSON response instead of a simple confirmation message."),
473
+ }, async ({ repositoryId, pullRequestId, threadId, fullResponse }) => {
355
474
  const connection = await connectionProvider();
356
475
  const gitApi = await connection.getGitApi();
357
476
  const thread = await gitApi.updateThread({ status: 2 }, // 2 corresponds to "Resolved" status
358
477
  repositoryId, pullRequestId, threadId);
478
+ // Check if the thread was successfully resolved
479
+ if (!thread) {
480
+ return {
481
+ content: [{ type: "text", text: `Error: Failed to resolve thread ${threadId}. The thread status was not updated successfully.` }],
482
+ isError: true,
483
+ };
484
+ }
485
+ if (fullResponse) {
486
+ return {
487
+ content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
488
+ };
489
+ }
359
490
  return {
360
- content: [{ type: "text", text: JSON.stringify(thread, null, 2) }],
491
+ content: [{ type: "text", text: `Thread ${threadId} was successfully resolved.` }],
361
492
  };
362
493
  });
363
494
  const gitVersionTypeStrings = Object.values(GitVersionType).filter((value) => typeof value === "string");
@@ -10,38 +10,37 @@ const SEARCH_TOOLS = {
10
10
  search_workitem: "search_workitem",
11
11
  };
12
12
  function configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider) {
13
- /*
14
- CODE SEARCH
15
- Get the code search results for a given search text.
16
- */
17
- server.tool(SEARCH_TOOLS.search_code, "Get the code search results for a given search text.", {
18
- searchRequest: z
19
- .object({
20
- searchText: z.string().describe("Search text to find in code"),
21
- $skip: z.number().default(0).describe("Number of results to skip (for pagination)"),
22
- $top: z.number().default(5).describe("Number of results to return (for pagination)"),
23
- filters: z
24
- .object({
25
- Project: z.array(z.string()).optional().describe("Filter in these projects"),
26
- Repository: z.array(z.string()).optional().describe("Filter in these repositories"),
27
- Path: z.array(z.string()).optional().describe("Filter in these paths"),
28
- Branch: z.array(z.string()).optional().describe("Filter in these branches"),
29
- CodeElement: z.array(z.string()).optional().describe("Filter for these code elements (e.g., classes, functions, symbols)"),
30
- // Note: CodeElement is optional and can be used to filter results by specific code elements.
31
- // It can be a string or an array of strings.
32
- // If provided, the search will only return results that match the specified code elements.
33
- // This is useful for narrowing down the search to specific classes, functions, definitions, or symbols.
34
- // Example: CodeElement: ["MyClass", "MyFunction"]
35
- })
36
- .partial()
37
- .optional(),
38
- includeFacets: z.boolean().optional(),
39
- })
40
- .strict(),
41
- }, async ({ searchRequest }) => {
13
+ server.tool(SEARCH_TOOLS.search_code, "Search Azure DevOps Repositories for a given search text", {
14
+ searchText: z.string().describe("Keywords to search for in code repositories"),
15
+ project: z.array(z.string()).optional().describe("Filter by projects"),
16
+ repository: z.array(z.string()).optional().describe("Filter by repositories"),
17
+ path: z.array(z.string()).optional().describe("Filter by paths"),
18
+ branch: z.array(z.string()).optional().describe("Filter by branches"),
19
+ includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
20
+ skip: z.number().default(0).describe("Number of results to skip"),
21
+ top: z.number().default(5).describe("Maximum number of results to return"),
22
+ }, async ({ searchText, project, repository, path, branch, includeFacets, skip, top }) => {
42
23
  const accessToken = await tokenProvider();
43
24
  const connection = await connectionProvider();
44
25
  const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/codesearchresults?api-version=${apiVersion}`;
26
+ const requestBody = {
27
+ searchText,
28
+ includeFacets,
29
+ $skip: skip,
30
+ $top: top,
31
+ };
32
+ const filters = {};
33
+ if (project && project.length > 0)
34
+ filters.Project = project;
35
+ if (repository && repository.length > 0)
36
+ filters.Repository = repository;
37
+ if (path && path.length > 0)
38
+ filters.Path = path;
39
+ if (branch && branch.length > 0)
40
+ filters.Branch = branch;
41
+ if (Object.keys(filters).length > 0) {
42
+ requestBody.filters = filters;
43
+ }
45
44
  const response = await fetch(url, {
46
45
  method: "POST",
47
46
  headers: {
@@ -49,44 +48,43 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
49
48
  "Authorization": `Bearer ${accessToken.token}`,
50
49
  "User-Agent": userAgentProvider(),
51
50
  },
52
- body: JSON.stringify(searchRequest),
51
+ body: JSON.stringify(requestBody),
53
52
  });
54
53
  if (!response.ok) {
55
54
  throw new Error(`Azure DevOps Code Search API error: ${response.status} ${response.statusText}`);
56
55
  }
57
56
  const resultText = await response.text();
58
57
  const resultJson = JSON.parse(resultText);
59
- const topResults = Array.isArray(resultJson.results) ? resultJson.results.slice(0, Math.min(searchRequest.$top, resultJson.results.length)) : [];
60
58
  const gitApi = await connection.getGitApi();
61
- const combinedResults = await fetchCombinedResults(topResults, gitApi);
59
+ const combinedResults = await fetchCombinedResults(resultJson.results ?? [], gitApi);
62
60
  return {
63
61
  content: [{ type: "text", text: resultText + JSON.stringify(combinedResults) }],
64
62
  };
65
63
  });
66
- /*
67
- WIKI SEARCH
68
- Get wiki search results for a given search text.
69
- */
70
- server.tool(SEARCH_TOOLS.search_wiki, "Get wiki search results for a given search text.", {
71
- searchRequest: z
72
- .object({
73
- searchText: z.string().describe("Search text to find in wikis"),
74
- $skip: z.number().default(0).describe("Number of results to skip (for pagination)"),
75
- $top: z.number().default(10).describe("Number of results to return (for pagination)"),
76
- filters: z
77
- .object({
78
- Project: z.array(z.string()).optional().describe("Filter in these projects"),
79
- Wiki: z.array(z.string()).optional().describe("Filter in these wiki names"),
80
- })
81
- .partial()
82
- .optional()
83
- .describe("Filters to apply to the search text"),
84
- includeFacets: z.boolean().optional(),
85
- })
86
- .strict(),
87
- }, async ({ searchRequest }) => {
64
+ server.tool(SEARCH_TOOLS.search_wiki, "Search Azure DevOps Wiki for a given search text", {
65
+ searchText: z.string().describe("Keywords to search for wiki pages"),
66
+ project: z.array(z.string()).optional().describe("Filter by projects"),
67
+ wiki: z.array(z.string()).optional().describe("Filter by wiki names"),
68
+ includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
69
+ skip: z.number().default(0).describe("Number of results to skip"),
70
+ top: z.number().default(10).describe("Maximum number of results to return"),
71
+ }, async ({ searchText, project, wiki, includeFacets, skip, top }) => {
88
72
  const accessToken = await tokenProvider();
89
73
  const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/wikisearchresults?api-version=${apiVersion}`;
74
+ const requestBody = {
75
+ searchText,
76
+ includeFacets,
77
+ $skip: skip,
78
+ $top: top,
79
+ };
80
+ const filters = {};
81
+ if (project && project.length > 0)
82
+ filters.Project = project;
83
+ if (wiki && wiki.length > 0)
84
+ filters.Wiki = wiki;
85
+ if (Object.keys(filters).length > 0) {
86
+ requestBody.filters = filters;
87
+ }
90
88
  const response = await fetch(url, {
91
89
  method: "POST",
92
90
  headers: {
@@ -94,7 +92,7 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
94
92
  "Authorization": `Bearer ${accessToken.token}`,
95
93
  "User-Agent": userAgentProvider(),
96
94
  },
97
- body: JSON.stringify(searchRequest),
95
+ body: JSON.stringify(requestBody),
98
96
  });
99
97
  if (!response.ok) {
100
98
  throw new Error(`Azure DevOps Wiki Search API error: ${response.status} ${response.statusText}`);
@@ -104,32 +102,39 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
104
102
  content: [{ type: "text", text: result }],
105
103
  };
106
104
  });
107
- /*
108
- WORK ITEM SEARCH
109
- Get work item search results for a given search text.
110
- */
111
- server.tool(SEARCH_TOOLS.search_workitem, "Get work item search results for a given search text.", {
112
- searchRequest: z
113
- .object({
114
- searchText: z.string().describe("Search text to find in work items"),
115
- $skip: z.number().default(0).describe("Number of results to skip for pagination"),
116
- $top: z.number().default(10).describe("Number of results to return"),
117
- filters: z
118
- .object({
119
- "System.TeamProject": z.array(z.string()).optional().describe("Filter by team project"),
120
- "System.AreaPath": z.array(z.string()).optional().describe("Filter by area path"),
121
- "System.WorkItemType": z.array(z.string()).optional().describe("Filter by work item type like Bug, Task, User Story"),
122
- "System.State": z.array(z.string()).optional().describe("Filter by state"),
123
- "System.AssignedTo": z.array(z.string()).optional().describe("Filter by assigned to"),
124
- })
125
- .partial()
126
- .optional(),
127
- includeFacets: z.boolean().optional(),
128
- })
129
- .strict(),
130
- }, async ({ searchRequest }) => {
105
+ server.tool(SEARCH_TOOLS.search_workitem, "Get Azure DevOps Work Item search results for a given search text", {
106
+ searchText: z.string().describe("Search text to find in work items"),
107
+ project: z.array(z.string()).optional().describe("Filter by projects"),
108
+ areaPath: z.array(z.string()).optional().describe("Filter by area paths"),
109
+ workItemType: z.array(z.string()).optional().describe("Filter by work item types"),
110
+ state: z.array(z.string()).optional().describe("Filter by work item states"),
111
+ assignedTo: z.array(z.string()).optional().describe("Filter by assigned to users"),
112
+ includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
113
+ skip: z.number().default(0).describe("Number of results to skip for pagination"),
114
+ top: z.number().default(10).describe("Number of results to return"),
115
+ }, async ({ searchText, project, areaPath, workItemType, state, assignedTo, includeFacets, skip, top }) => {
131
116
  const accessToken = await tokenProvider();
132
117
  const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/workitemsearchresults?api-version=${apiVersion}`;
118
+ const requestBody = {
119
+ searchText,
120
+ includeFacets,
121
+ $skip: skip,
122
+ $top: top,
123
+ };
124
+ const filters = {};
125
+ if (project && project.length > 0)
126
+ filters["System.TeamProject"] = project;
127
+ if (areaPath && areaPath.length > 0)
128
+ filters["System.AreaPath"] = areaPath;
129
+ if (workItemType && workItemType.length > 0)
130
+ filters["System.WorkItemType"] = workItemType;
131
+ if (state && state.length > 0)
132
+ filters["System.State"] = state;
133
+ if (assignedTo && assignedTo.length > 0)
134
+ filters["System.AssignedTo"] = assignedTo;
135
+ if (Object.keys(filters).length > 0) {
136
+ requestBody.filters = filters;
137
+ }
133
138
  const response = await fetch(url, {
134
139
  method: "POST",
135
140
  headers: {
@@ -137,7 +142,7 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
137
142
  "Authorization": `Bearer ${accessToken.token}`,
138
143
  "User-Agent": userAgentProvider(),
139
144
  },
140
- body: JSON.stringify(searchRequest),
145
+ body: JSON.stringify(requestBody),
141
146
  });
142
147
  if (!response.ok) {
143
148
  throw new Error(`Azure DevOps Work Item Search API error: ${response.status} ${response.statusText}`);