@azure-devops/mcp 2.5.0-nightly.20260412 → 2.5.0-nightly.20260414

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.
@@ -811,42 +811,76 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
811
811
  project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
812
812
  includeWorkItemRefs: z.boolean().optional().default(false).describe("Whether to reference work items associated with the pull request."),
813
813
  includeLabels: z.boolean().optional().default(false).describe("Whether to include a summary of labels in the response."),
814
- }, async ({ repositoryId, pullRequestId, project, includeWorkItemRefs, includeLabels }) => {
814
+ includeChangedFiles: z.boolean().optional().default(false).describe("Whether to include the list of files changed in the pull request."),
815
+ }, async ({ repositoryId, pullRequestId, project, includeWorkItemRefs, includeLabels, includeChangedFiles }) => {
815
816
  try {
816
817
  const connection = await connectionProvider();
817
818
  const gitApi = await connection.getGitApi();
818
819
  const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId, project, undefined, undefined, undefined, undefined, includeWorkItemRefs);
820
+ let enhancedResponse = { ...pullRequest };
819
821
  if (includeLabels) {
820
822
  try {
821
823
  const projectId = pullRequest.repository?.project?.id;
822
824
  const projectName = pullRequest.repository?.project?.name;
823
825
  const labels = await gitApi.getPullRequestLabels(repositoryId, pullRequestId, projectName, projectId);
824
826
  const labelNames = labels.map((label) => label.name).filter((name) => name !== undefined);
825
- const enhancedResponse = {
826
- ...pullRequest,
827
+ enhancedResponse = {
828
+ ...enhancedResponse,
827
829
  labelSummary: {
828
830
  labels: labelNames,
829
831
  labelCount: labelNames.length,
830
832
  },
831
833
  };
832
- return {
833
- content: [{ type: "text", text: JSON.stringify(enhancedResponse, null, 2) }],
834
- };
835
834
  }
836
835
  catch (error) {
837
836
  console.warn(`Error fetching PR labels: ${error instanceof Error ? error.message : "Unknown error"}`);
838
- // Fall back to the original response without labels
839
- const enhancedResponse = {
840
- ...pullRequest,
837
+ enhancedResponse = {
838
+ ...enhancedResponse,
841
839
  labelSummary: {},
842
840
  };
843
- return {
844
- content: [{ type: "text", text: JSON.stringify(enhancedResponse, null, 2) }],
841
+ }
842
+ }
843
+ if (includeChangedFiles) {
844
+ try {
845
+ const iterations = await gitApi.getPullRequestIterations(repositoryId, pullRequestId, project);
846
+ if (iterations?.length) {
847
+ const latestIteration = iterations[iterations.length - 1];
848
+ if (latestIteration.id != null) {
849
+ const changes = await gitApi.getPullRequestIterationChanges(repositoryId, pullRequestId, latestIteration.id, project);
850
+ enhancedResponse = {
851
+ ...enhancedResponse,
852
+ changedFilesSummary: {
853
+ changeEntries: changes?.changeEntries ?? [],
854
+ fileCount: changes?.changeEntries?.length ?? 0,
855
+ nextSkip: changes?.nextSkip,
856
+ nextTop: changes?.nextTop,
857
+ },
858
+ };
859
+ }
860
+ else {
861
+ enhancedResponse = {
862
+ ...enhancedResponse,
863
+ changedFilesSummary: { changeEntries: [], fileCount: 0 },
864
+ };
865
+ }
866
+ }
867
+ else {
868
+ enhancedResponse = {
869
+ ...enhancedResponse,
870
+ changedFilesSummary: { changeEntries: [], fileCount: 0 },
871
+ };
872
+ }
873
+ }
874
+ catch (error) {
875
+ console.warn(`Error fetching PR changed files: ${error instanceof Error ? error.message : "Unknown error"}`);
876
+ enhancedResponse = {
877
+ ...enhancedResponse,
878
+ changedFilesSummary: {},
845
879
  };
846
880
  }
847
881
  }
848
882
  return {
849
- content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }],
883
+ content: [{ type: "text", text: JSON.stringify(enhancedResponse, null, 2) }],
850
884
  };
851
885
  }
852
886
  catch (error) {
@@ -12,7 +12,10 @@ const SEARCH_TOOLS = {
12
12
  function configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider) {
13
13
  server.tool(SEARCH_TOOLS.search_code, "Search Azure DevOps Repositories for a given search text", {
14
14
  searchText: z.string().describe("Keywords to search for in code repositories"),
15
- project: z.array(z.string()).optional().describe("Filter by projects"),
15
+ project: z
16
+ .union([z.string().transform((value) => [value]), z.array(z.string())])
17
+ .optional()
18
+ .describe("Filter by projects"),
16
19
  repository: z.array(z.string()).optional().describe("Filter by repositories"),
17
20
  path: z.array(z.string()).optional().describe("Filter by paths"),
18
21
  branch: z.array(z.string()).optional().describe("Filter by branches"),
@@ -4,7 +4,7 @@ import { WorkItemExpand } from "azure-devops-node-api/interfaces/WorkItemTrackin
4
4
  import { QueryExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
5
5
  import { z } from "zod";
6
6
  import { batchApiVersion, markdownCommentsApiVersion, getEnumKeys, safeEnumConvert, encodeFormattedValue } from "../utils.js";
7
- import { elicitProject } from "../shared/elicitations.js";
7
+ import { elicitProject, elicitTeam } from "../shared/elicitations.js";
8
8
  import { createExternalContentResponse } from "../shared/content-safety.js";
9
9
  const WORKITEM_TOOLS = {
10
10
  my_work_items: "wit_my_work_items",
@@ -62,14 +62,28 @@ function getLinkTypeFromName(name) {
62
62
  }
63
63
  }
64
64
  function configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider) {
65
- server.tool(WORKITEM_TOOLS.list_backlogs, "Receive a list of backlogs for a given project and team.", {
66
- project: z.string().describe("The name or ID of the Azure DevOps project."),
67
- team: z.string().describe("The name or ID of the Azure DevOps team."),
65
+ server.tool(WORKITEM_TOOLS.list_backlogs, "Receive a list of backlogs for a given project and team. If a project or team is not specified, you will be prompted to select one.", {
66
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
67
+ team: z.string().optional().describe("The name or ID of the Azure DevOps team. Reuse from prior context if already known. If not provided, a team selection prompt will be shown."),
68
68
  }, async ({ project, team }) => {
69
69
  try {
70
70
  const connection = await connectionProvider();
71
+ let resolvedProject = project;
72
+ if (!resolvedProject) {
73
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to list backlogs for.");
74
+ if ("response" in result)
75
+ return result.response;
76
+ resolvedProject = result.resolved;
77
+ }
78
+ let resolvedTeam = team;
79
+ if (!resolvedTeam) {
80
+ const result = await elicitTeam(server, connection, resolvedProject, "Select the Azure DevOps team to list backlogs for.");
81
+ if ("response" in result)
82
+ return result.response;
83
+ resolvedTeam = result.resolved;
84
+ }
71
85
  const workApi = await connection.getWorkApi();
72
- const teamContext = { project, team };
86
+ const teamContext = { project: resolvedProject, team: resolvedTeam };
73
87
  const backlogs = await workApi.getBacklogs(teamContext);
74
88
  return {
75
89
  content: [{ type: "text", text: JSON.stringify(backlogs, null, 2) }],
@@ -83,15 +97,29 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
83
97
  };
84
98
  }
85
99
  });
86
- server.tool(WORKITEM_TOOLS.list_backlog_work_items, "Retrieve a list of backlogs of for a given project, team, and backlog category", {
87
- project: z.string().describe("The name or ID of the Azure DevOps project."),
88
- team: z.string().describe("The name or ID of the Azure DevOps team."),
100
+ server.tool(WORKITEM_TOOLS.list_backlog_work_items, "Retrieve a list of backlogs of for a given project, team, and backlog category. If a project or team is not specified, you will be prompted to select one.", {
101
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
102
+ team: z.string().optional().describe("The name or ID of the Azure DevOps team. Reuse from prior context if already known. If not provided, a team selection prompt will be shown."),
89
103
  backlogId: z.string().describe("The ID of the backlog category to retrieve work items from."),
90
104
  }, async ({ project, team, backlogId }) => {
91
105
  try {
92
106
  const connection = await connectionProvider();
107
+ let resolvedProject = project;
108
+ if (!resolvedProject) {
109
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to list backlog work items for.");
110
+ if ("response" in result)
111
+ return result.response;
112
+ resolvedProject = result.resolved;
113
+ }
114
+ let resolvedTeam = team;
115
+ if (!resolvedTeam) {
116
+ const result = await elicitTeam(server, connection, resolvedProject, "Select the Azure DevOps team to list backlog work items for.");
117
+ if ("response" in result)
118
+ return result.response;
119
+ resolvedTeam = result.resolved;
120
+ }
93
121
  const workApi = await connection.getWorkApi();
94
- const teamContext = { project, team };
122
+ const teamContext = { project: resolvedProject, team: resolvedTeam };
95
123
  const workItems = await workApi.getBacklogLevelWorkItems(teamContext, backlogId);
96
124
  return {
97
125
  content: [{ type: "text", text: JSON.stringify(workItems, null, 2) }],
@@ -105,16 +133,23 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
105
133
  };
106
134
  }
107
135
  });
108
- server.tool(WORKITEM_TOOLS.my_work_items, "Retrieve a list of work items relevent to the authenticated user.", {
109
- project: z.string().describe("The name or ID of the Azure DevOps project."),
136
+ server.tool(WORKITEM_TOOLS.my_work_items, "Retrieve a list of work items relevent to the authenticated user. If a project is not specified, you will be prompted to select one.", {
137
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
110
138
  type: z.enum(["assignedtome", "myactivity"]).default("assignedtome").describe("The type of work items to retrieve. Defaults to 'assignedtome'."),
111
139
  top: z.coerce.number().default(50).describe("The maximum number of work items to return. Defaults to 50."),
112
140
  includeCompleted: z.boolean().default(false).describe("Whether to include completed work items. Defaults to false."),
113
141
  }, async ({ project, type, top, includeCompleted }) => {
114
142
  try {
115
143
  const connection = await connectionProvider();
144
+ let resolvedProject = project;
145
+ if (!resolvedProject) {
146
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve work items for.");
147
+ if ("response" in result)
148
+ return result.response;
149
+ resolvedProject = result.resolved;
150
+ }
116
151
  const workApi = await connection.getWorkApi();
117
- const workItems = await workApi.getPredefinedQueryResults(project, type, top, includeCompleted);
152
+ const workItems = await workApi.getPredefinedQueryResults(resolvedProject, type, top, includeCompleted);
118
153
  return {
119
154
  content: [{ type: "text", text: JSON.stringify(workItems, null, 2) }],
120
155
  };
@@ -127,18 +162,25 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
127
162
  };
128
163
  }
129
164
  });
130
- server.tool(WORKITEM_TOOLS.get_work_items_batch_by_ids, "Retrieve list of work items by IDs in batch.", {
131
- project: z.string().describe("The name or ID of the Azure DevOps project."),
165
+ server.tool(WORKITEM_TOOLS.get_work_items_batch_by_ids, "Retrieve list of work items by IDs in batch. If a project is not specified, you will be prompted to select one.", {
166
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
132
167
  ids: z.array(z.coerce.number().min(1)).describe("The IDs of the work items to retrieve."),
133
168
  fields: z.array(z.string()).optional().describe("Optional list of fields to include in the response. If not provided, a hardcoded default set of fields will be used."),
134
169
  }, async ({ project, ids, fields }) => {
135
170
  try {
136
171
  const connection = await connectionProvider();
172
+ let resolvedProject = project;
173
+ if (!resolvedProject) {
174
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve work items for.");
175
+ if ("response" in result)
176
+ return result.response;
177
+ resolvedProject = result.resolved;
178
+ }
137
179
  const workItemApi = await connection.getWorkItemTrackingApi();
138
180
  const defaultFields = ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.Parent", "System.Tags", "Microsoft.VSTS.Common.StackRank", "System.AssignedTo"];
139
181
  // If no fields are provided, use the default set of fields
140
182
  const fieldsToUse = !fields || fields.length === 0 ? defaultFields : fields;
141
- const workitems = await workItemApi.getWorkItemsBatch({ ids, fields: fieldsToUse }, project);
183
+ const workitems = await workItemApi.getWorkItemsBatch({ ids, fields: fieldsToUse }, resolvedProject);
142
184
  // List of identity fields that need to be transformed from objects to formatted strings
143
185
  const identityFields = [
144
186
  "System.AssignedTo",
@@ -177,21 +219,36 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
177
219
  };
178
220
  }
179
221
  });
180
- server.tool(WORKITEM_TOOLS.get_work_item, "Get a single work item by ID.", {
222
+ server.tool(WORKITEM_TOOLS.get_work_item, "Get a single work item by ID. If a project is not specified, you will be prompted to select one.", {
181
223
  id: z.coerce.number().min(1).describe("The ID of the work item to retrieve."),
182
- project: z.string().describe("The name or ID of the Azure DevOps project."),
183
- fields: z.array(z.string()).optional().describe("Optional list of fields to include in the response. If not provided, all fields will be returned."),
224
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
225
+ fields: z
226
+ .array(z.string())
227
+ .optional()
228
+ .describe("Optional list of fields to include in the response. If not provided, all fields will be returned. Cannot be used together with the expand parameter."),
184
229
  asOf: z.coerce.date().optional().describe("Optional date string to retrieve the work item as of a specific time. If not provided, the current state will be returned."),
185
230
  expand: z
186
231
  .enum(["all", "fields", "links", "none", "relations"])
187
- .describe("Optional expand parameter to include additional details in the response.")
232
+ .describe("Optional expand parameter to include additional details in the response. Cannot be used together with the fields parameter.")
188
233
  .optional()
189
- .describe("Expand options include 'all', 'fields', 'links', 'none', and 'relations'. Relations can be used to get child workitems. Defaults to 'none'."),
234
+ .describe("Expand options include 'All', 'Fields', 'Links', 'None', and 'Relations'. Relations can be used to get child workitems. Defaults to 'None'. Cannot be used together with the fields parameter."),
190
235
  }, async ({ id, project, fields, asOf, expand }) => {
191
236
  try {
192
237
  const connection = await connectionProvider();
238
+ let resolvedProject = project;
239
+ if (!resolvedProject) {
240
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve the work item from.");
241
+ if ("response" in result)
242
+ return result.response;
243
+ resolvedProject = result.resolved;
244
+ }
245
+ // The Azure DevOps API does not support using expand and fields together.
246
+ // When both are provided, prefer fields as it is the more specific selection.
247
+ if (fields && fields.length > 0 && expand != null) {
248
+ expand = "none";
249
+ }
193
250
  const workItemApi = await connection.getWorkItemTrackingApi();
194
- const workItem = await workItemApi.getWorkItem(id, fields, asOf, expand, project);
251
+ const workItem = await workItemApi.getWorkItem(id, fields, asOf, expand, resolvedProject);
195
252
  return {
196
253
  content: [{ type: "text", text: JSON.stringify(workItem, null, 2) }],
197
254
  };
@@ -204,15 +261,22 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
204
261
  };
205
262
  }
206
263
  });
207
- server.tool(WORKITEM_TOOLS.list_work_item_comments, "Retrieve list of comments for a work item by ID.", {
208
- project: z.string().describe("The name or ID of the Azure DevOps project."),
264
+ server.tool(WORKITEM_TOOLS.list_work_item_comments, "Retrieve list of comments for a work item by ID. If a project is not specified, you will be prompted to select one.", {
265
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
209
266
  workItemId: z.coerce.number().min(1).describe("The ID of the work item to retrieve comments for."),
210
267
  top: z.coerce.number().default(50).describe("Optional number of comments to retrieve. Defaults to all comments."),
211
268
  }, async ({ project, workItemId, top }) => {
212
269
  try {
213
270
  const connection = await connectionProvider();
271
+ let resolvedProject = project;
272
+ if (!resolvedProject) {
273
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to list work item comments for.");
274
+ if ("response" in result)
275
+ return result.response;
276
+ resolvedProject = result.resolved;
277
+ }
214
278
  const workItemApi = await connection.getWorkItemTrackingApi();
215
- const comments = await workItemApi.getComments(project, workItemId, top);
279
+ const comments = await workItemApi.getComments(resolvedProject, workItemId, top);
216
280
  return {
217
281
  content: [{ type: "text", text: JSON.stringify(comments, null, 2) }],
218
282
  };
@@ -225,21 +289,28 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
225
289
  };
226
290
  }
227
291
  });
228
- server.tool(WORKITEM_TOOLS.add_work_item_comment, "Add comment to a work item by ID.", {
229
- project: z.string().describe("The name or ID of the Azure DevOps project."),
292
+ server.tool(WORKITEM_TOOLS.add_work_item_comment, "Add comment to a work item by ID. If a project is not specified, you will be prompted to select one.", {
293
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
230
294
  workItemId: z.coerce.number().min(1).describe("The ID of the work item to add a comment to."),
231
295
  comment: z.string().describe("The text of the comment to add to the work item."),
232
296
  format: z.enum(["markdown", "html"]).optional().default("html"),
233
297
  }, async ({ project, workItemId, comment, format }) => {
234
298
  try {
235
299
  const connection = await connectionProvider();
300
+ let resolvedProject = project;
301
+ if (!resolvedProject) {
302
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to add a work item comment in.");
303
+ if ("response" in result)
304
+ return result.response;
305
+ resolvedProject = result.resolved;
306
+ }
236
307
  const orgUrl = connection.serverUrl;
237
308
  const accessToken = await tokenProvider();
238
309
  const body = {
239
310
  text: comment,
240
311
  };
241
312
  const formatParameter = format === "markdown" ? 0 : 1;
242
- const response = await fetch(`${orgUrl}/${encodeURIComponent(project)}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
313
+ const response = await fetch(`${orgUrl}/${encodeURIComponent(resolvedProject)}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
243
314
  method: "POST",
244
315
  headers: {
245
316
  "Authorization": `Bearer ${accessToken}`,
@@ -264,8 +335,8 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
264
335
  };
265
336
  }
266
337
  });
267
- server.tool(WORKITEM_TOOLS.update_work_item_comment, "Update an existing comment on a work item by ID.", {
268
- project: z.string().describe("The name or ID of the Azure DevOps project."),
338
+ server.tool(WORKITEM_TOOLS.update_work_item_comment, "Update an existing comment on a work item by ID. If a project is not specified, you will be prompted to select one.", {
339
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
269
340
  workItemId: z.coerce.number().min(1).describe("The ID of the work item."),
270
341
  commentId: z.coerce.number().min(1).describe("The ID of the comment to update."),
271
342
  text: z.string().describe("The updated comment text."),
@@ -273,11 +344,18 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
273
344
  }, async ({ project, workItemId, commentId, text, format }) => {
274
345
  try {
275
346
  const connection = await connectionProvider();
347
+ let resolvedProject = project;
348
+ if (!resolvedProject) {
349
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to update the work item comment in.");
350
+ if ("response" in result)
351
+ return result.response;
352
+ resolvedProject = result.resolved;
353
+ }
276
354
  const orgUrl = connection.serverUrl;
277
355
  const accessToken = await tokenProvider();
278
356
  const body = { text };
279
357
  const formatParameter = format === "markdown" ? 0 : 1;
280
- const response = await fetch(`${orgUrl}/${encodeURIComponent(project)}/_apis/wit/workItems/${workItemId}/comments/${commentId}?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
358
+ const response = await fetch(`${orgUrl}/${encodeURIComponent(resolvedProject)}/_apis/wit/workItems/${workItemId}/comments/${commentId}?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
281
359
  method: "PATCH",
282
360
  headers: {
283
361
  "Authorization": `Bearer ${accessToken}`,
@@ -302,8 +380,8 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
302
380
  };
303
381
  }
304
382
  });
305
- server.tool(WORKITEM_TOOLS.list_work_item_revisions, "Retrieve list of revisions for a work item by ID.", {
306
- project: z.string().describe("The name or ID of the Azure DevOps project."),
383
+ server.tool(WORKITEM_TOOLS.list_work_item_revisions, "Retrieve list of revisions for a work item by ID. If a project is not specified, you will be prompted to select one.", {
384
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
307
385
  workItemId: z.coerce.number().min(1).describe("The ID of the work item to retrieve revisions for."),
308
386
  top: z.coerce.number().default(50).describe("Optional number of revisions to retrieve. If not provided, all revisions will be returned."),
309
387
  skip: z.coerce.number().optional().describe("Optional number of revisions to skip for pagination. Defaults to 0."),
@@ -315,8 +393,15 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
315
393
  }, async ({ project, workItemId, top, skip, expand }) => {
316
394
  try {
317
395
  const connection = await connectionProvider();
396
+ let resolvedProject = project;
397
+ if (!resolvedProject) {
398
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to list work item revisions for.");
399
+ if ("response" in result)
400
+ return result.response;
401
+ resolvedProject = result.resolved;
402
+ }
318
403
  const workItemApi = await connection.getWorkItemTrackingApi();
319
- const revisions = await workItemApi.getRevisions(workItemId, top, skip, safeEnumConvert(WorkItemExpand, expand), project);
404
+ const revisions = await workItemApi.getRevisions(workItemId, top, skip, safeEnumConvert(WorkItemExpand, expand), resolvedProject);
320
405
  // Dynamically clean up identity objects in revision fields
321
406
  // Identity objects typically have properties like displayName, url, _links, id, uniqueName, imageUrl, descriptor
322
407
  if (revisions && Array.isArray(revisions)) {
@@ -354,9 +439,9 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
354
439
  };
355
440
  }
356
441
  });
357
- server.tool(WORKITEM_TOOLS.add_child_work_items, "Create one or many child work items from a parent by work item type and parent id.", {
442
+ server.tool(WORKITEM_TOOLS.add_child_work_items, "Create one or many child work items from a parent by work item type and parent id. If a project is not specified, you will be prompted to select one.", {
358
443
  parentId: z.coerce.number().min(1).describe("The ID of the parent work item to create a child work item under."),
359
- project: z.string().describe("The name or ID of the Azure DevOps project."),
444
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
360
445
  workItemType: z.string().describe("The type of the child work item to create."),
361
446
  items: z.array(z.object({
362
447
  title: z.string().describe("The title of the child work item."),
@@ -368,6 +453,13 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
368
453
  }, async ({ parentId, project, workItemType, items }) => {
369
454
  try {
370
455
  const connection = await connectionProvider();
456
+ let resolvedProject = project;
457
+ if (!resolvedProject) {
458
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to create child work items in.");
459
+ if ("response" in result)
460
+ return result.response;
461
+ resolvedProject = result.resolved;
462
+ }
371
463
  const orgUrl = connection.serverUrl;
372
464
  const accessToken = await tokenProvider();
373
465
  if (items.length > 50) {
@@ -404,7 +496,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
404
496
  path: "/relations/-",
405
497
  value: {
406
498
  rel: "System.LinkTypes.Hierarchy-Reverse",
407
- url: `${connection.serverUrl}/${project}/_apis/wit/workItems/${parentId}`,
499
+ url: `${connection.serverUrl}/${resolvedProject}/_apis/wit/workItems/${parentId}`,
408
500
  },
409
501
  },
410
502
  ];
@@ -436,7 +528,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
436
528
  }
437
529
  return {
438
530
  method: "PATCH",
439
- uri: `/${encodeURIComponent(project)}/_apis/wit/workitems/$${encodeURIComponent(workItemType)}?api-version=${batchApiVersion}`,
531
+ uri: `/${encodeURIComponent(resolvedProject)}/_apis/wit/workitems/$${encodeURIComponent(workItemType)}?api-version=${batchApiVersion}`,
440
532
  headers: {
441
533
  "Content-Type": "application/json-patch+json",
442
534
  },
@@ -523,16 +615,23 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
523
615
  };
524
616
  }
525
617
  });
526
- server.tool(WORKITEM_TOOLS.get_work_items_for_iteration, "Retrieve a list of work items for a specified iteration.", {
527
- project: z.string().describe("The name or ID of the Azure DevOps project."),
618
+ server.tool(WORKITEM_TOOLS.get_work_items_for_iteration, "Retrieve a list of work items for a specified iteration. If a project is not specified, you will be prompted to select one.", {
619
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
528
620
  team: z.string().optional().describe("The name or ID of the Azure DevOps team. If not provided, the default team will be used."),
529
621
  iterationId: z.string().describe("The ID of the iteration to retrieve work items for."),
530
622
  }, async ({ project, team, iterationId }) => {
531
623
  try {
532
624
  const connection = await connectionProvider();
625
+ let resolvedProject = project;
626
+ if (!resolvedProject) {
627
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve work items for iteration.");
628
+ if ("response" in result)
629
+ return result.response;
630
+ resolvedProject = result.resolved;
631
+ }
533
632
  const workApi = await connection.getWorkApi();
534
633
  //get the work items for the current iteration
535
- const workItems = await workApi.getIterationWorkItems({ project, team }, iterationId);
634
+ const workItems = await workApi.getIterationWorkItems({ project: resolvedProject, team }, iterationId);
536
635
  return {
537
636
  content: [{ type: "text", text: JSON.stringify(workItems, null, 2) }],
538
637
  };
@@ -581,14 +680,21 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
581
680
  };
582
681
  }
583
682
  });
584
- server.tool(WORKITEM_TOOLS.get_work_item_type, "Get a specific work item type.", {
585
- project: z.string().describe("The name or ID of the Azure DevOps project."),
683
+ server.tool(WORKITEM_TOOLS.get_work_item_type, "Get a specific work item type. If a project is not specified, you will be prompted to select one.", {
684
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
586
685
  workItemType: z.string().describe("The name of the work item type to retrieve."),
587
686
  }, async ({ project, workItemType }) => {
588
687
  try {
589
688
  const connection = await connectionProvider();
689
+ let resolvedProject = project;
690
+ if (!resolvedProject) {
691
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve the work item type from.");
692
+ if ("response" in result)
693
+ return result.response;
694
+ resolvedProject = result.resolved;
695
+ }
590
696
  const workItemApi = await connection.getWorkItemTrackingApi();
591
- const workItemTypeInfo = await workItemApi.getWorkItemType(project, workItemType);
697
+ const workItemTypeInfo = await workItemApi.getWorkItemType(resolvedProject, workItemType);
592
698
  return {
593
699
  content: [{ type: "text", text: JSON.stringify(workItemTypeInfo, null, 2) }],
594
700
  };
@@ -601,8 +707,8 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
601
707
  };
602
708
  }
603
709
  });
604
- server.tool(WORKITEM_TOOLS.create_work_item, "Create a new work item in a specified project and work item type.", {
605
- project: z.string().describe("The name or ID of the Azure DevOps project."),
710
+ server.tool(WORKITEM_TOOLS.create_work_item, "Create a new work item in a specified project and work item type. If a project is not specified, you will be prompted to select one.", {
711
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
606
712
  workItemType: z.string().describe("The type of work item to create, e.g., 'Task', 'Bug', etc."),
607
713
  fields: z
608
714
  .array(z.object({
@@ -614,6 +720,13 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
614
720
  }, async ({ project, workItemType, fields }) => {
615
721
  try {
616
722
  const connection = await connectionProvider();
723
+ let resolvedProject = project;
724
+ if (!resolvedProject) {
725
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to create the work item in.");
726
+ if ("response" in result)
727
+ return result.response;
728
+ resolvedProject = result.resolved;
729
+ }
617
730
  const workItemApi = await connection.getWorkItemTrackingApi();
618
731
  const document = fields.map(({ name, value, format }) => ({
619
732
  op: "add",
@@ -632,7 +745,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
632
745
  });
633
746
  }
634
747
  });
635
- const newWorkItem = await workItemApi.createWorkItem(null, document, project, workItemType);
748
+ const newWorkItem = await workItemApi.createWorkItem(null, document, resolvedProject, workItemType);
636
749
  if (!newWorkItem) {
637
750
  return { content: [{ type: "text", text: "Work item was not created" }], isError: true };
638
751
  }
@@ -648,8 +761,8 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
648
761
  };
649
762
  }
650
763
  });
651
- server.tool(WORKITEM_TOOLS.get_query, "Get a query by its ID or path.", {
652
- project: z.string().describe("The name or ID of the Azure DevOps project."),
764
+ server.tool(WORKITEM_TOOLS.get_query, "Get a query by its ID or path. If a project is not specified, you will be prompted to select one.", {
765
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
653
766
  query: z.string().describe("The ID or path of the query to retrieve."),
654
767
  expand: z
655
768
  .enum(getEnumKeys(QueryExpand))
@@ -661,8 +774,15 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
661
774
  }, async ({ project, query, expand, depth, includeDeleted, useIsoDateFormat }) => {
662
775
  try {
663
776
  const connection = await connectionProvider();
777
+ let resolvedProject = project;
778
+ if (!resolvedProject) {
779
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve the query from.");
780
+ if ("response" in result)
781
+ return result.response;
782
+ resolvedProject = result.resolved;
783
+ }
664
784
  const workItemApi = await connection.getWorkItemTrackingApi();
665
- const queryDetails = await workItemApi.getQuery(project, query, safeEnumConvert(QueryExpand, expand), depth, includeDeleted, useIsoDateFormat);
785
+ const queryDetails = await workItemApi.getQuery(resolvedProject, query, safeEnumConvert(QueryExpand, expand), depth, includeDeleted, useIsoDateFormat);
666
786
  return {
667
787
  content: [{ type: "text", text: JSON.stringify(queryDetails, null, 2) }],
668
788
  };
@@ -776,8 +896,8 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
776
896
  };
777
897
  }
778
898
  });
779
- server.tool(WORKITEM_TOOLS.work_items_link, "Link work items together in batch.", {
780
- project: z.string().describe("The name or ID of the Azure DevOps project."),
899
+ server.tool(WORKITEM_TOOLS.work_items_link, "Link work items together in batch. If a project is not specified, you will be prompted to select one.", {
900
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
781
901
  updates: z
782
902
  .array(z.object({
783
903
  id: z.coerce.number().min(1).describe("The ID of the work item to update."),
@@ -792,6 +912,13 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
792
912
  }, async ({ project, updates }) => {
793
913
  try {
794
914
  const connection = await connectionProvider();
915
+ let resolvedProject = project;
916
+ if (!resolvedProject) {
917
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to link work items in.");
918
+ if ("response" in result)
919
+ return result.response;
920
+ resolvedProject = result.resolved;
921
+ }
795
922
  const orgUrl = connection.serverUrl;
796
923
  const accessToken = await tokenProvider();
797
924
  // Extract unique IDs from the updates array
@@ -809,7 +936,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
809
936
  path: "/relations/-",
810
937
  value: {
811
938
  rel: `${getLinkTypeFromName(type)}`,
812
- url: `${orgUrl}/${project}/_apis/wit/workItems/${linkToId}`,
939
+ url: `${orgUrl}/${resolvedProject}/_apis/wit/workItems/${linkToId}`,
813
940
  attributes: {
814
941
  comment: comment || "",
815
942
  },
@@ -841,8 +968,8 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
841
968
  };
842
969
  }
843
970
  });
844
- server.tool(WORKITEM_TOOLS.work_item_unlink, "Remove one or many links from a single work item", {
845
- project: z.string().describe("The name or ID of the Azure DevOps project."),
971
+ server.tool(WORKITEM_TOOLS.work_item_unlink, "Remove one or many links from a single work item. If a project is not specified, you will be prompted to select one.", {
972
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
846
973
  id: z.coerce.number().min(1).describe("The ID of the work item to remove the links from."),
847
974
  type: z
848
975
  .enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by", "artifact"])
@@ -852,8 +979,15 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
852
979
  }, async ({ project, id, type, url }) => {
853
980
  try {
854
981
  const connection = await connectionProvider();
982
+ let resolvedProject = project;
983
+ if (!resolvedProject) {
984
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to unlink work items in.");
985
+ if ("response" in result)
986
+ return result.response;
987
+ resolvedProject = result.resolved;
988
+ }
855
989
  const workItemApi = await connection.getWorkItemTrackingApi();
856
- const workItem = await workItemApi.getWorkItem(id, undefined, undefined, WorkItemExpand.Relations, project);
990
+ const workItem = await workItemApi.getWorkItem(id, undefined, undefined, WorkItemExpand.Relations, resolvedProject);
857
991
  const relations = workItem.relations ?? [];
858
992
  const linkType = getLinkTypeFromName(type);
859
993
  let relationIndexes = [];
@@ -879,7 +1013,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
879
1013
  op: "remove",
880
1014
  path: `/relations/${idx}`,
881
1015
  }));
882
- const updatedWorkItem = await workItemApi.updateWorkItem(null, apiUpdates, id, project);
1016
+ const updatedWorkItem = await workItemApi.updateWorkItem(null, apiUpdates, id, resolvedProject);
883
1017
  return {
884
1018
  content: [
885
1019
  {
@@ -905,9 +1039,9 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
905
1039
  };
906
1040
  }
907
1041
  });
908
- server.tool(WORKITEM_TOOLS.add_artifact_link, "Add artifact links (repository, branch, commit, builds) to work items. You can either provide the full vstfs URI or the individual components to build it automatically.", {
1042
+ server.tool(WORKITEM_TOOLS.add_artifact_link, "Add artifact links (repository, branch, commit, builds) to work items. You can either provide the full vstfs URI or the individual components to build it automatically. If a project is not specified, you will be prompted to select one.", {
909
1043
  workItemId: z.coerce.number().min(1).describe("The ID of the work item to add the artifact link to."),
910
- project: z.string().describe("The name or ID of the Azure DevOps project."),
1044
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
911
1045
  // Option 1: Provide full URI directly
912
1046
  artifactUri: z.string().optional().describe("The complete VSTFS URI of the artifact to link. If provided, individual component parameters are ignored."),
913
1047
  // Option 2: Provide individual components to build URI automatically based on linkType
@@ -940,6 +1074,13 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
940
1074
  }, async ({ workItemId, project, artifactUri, projectId, repositoryId, branchName, commitId, pullRequestId, buildId, linkType, comment }) => {
941
1075
  try {
942
1076
  const connection = await connectionProvider();
1077
+ let resolvedProject = project;
1078
+ if (!resolvedProject) {
1079
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to add the artifact link in.");
1080
+ if ("response" in result)
1081
+ return result.response;
1082
+ resolvedProject = result.resolved;
1083
+ }
943
1084
  const workItemTrackingApi = await connection.getWorkItemTrackingApi();
944
1085
  let finalArtifactUri;
945
1086
  if (artifactUri) {
@@ -1010,7 +1151,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
1010
1151
  },
1011
1152
  ];
1012
1153
  // Use the WorkItem API to update the work item with the new relation
1013
- const workItem = await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId, project);
1154
+ const workItem = await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId, resolvedProject);
1014
1155
  if (!workItem) {
1015
1156
  return { content: [{ type: "text", text: "Work item update failed" }], isError: true };
1016
1157
  }
@@ -1066,15 +1207,22 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
1066
1207
  };
1067
1208
  }
1068
1209
  });
1069
- server.tool(WORKITEM_TOOLS.get_work_item_attachment, "Download a work item attachment by its ID and return the content as a base64-encoded resource. Useful for viewing images (e.g. screenshots) attached to work items such as bugs.", {
1070
- project: z.string().describe("The name or ID of the Azure DevOps project."),
1210
+ server.tool(WORKITEM_TOOLS.get_work_item_attachment, "Download a work item attachment by its ID and return the content as a base64-encoded resource. Useful for viewing images (e.g. screenshots) attached to work items such as bugs. If a project is not specified, you will be prompted to select one.", {
1211
+ project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
1071
1212
  attachmentId: z.string().describe("The GUID of the attachment. Found in the attachment URL: https://dev.azure.com/{org}/{project}/_apis/wit/attachments/{attachmentId}"),
1072
1213
  fileName: z.string().optional().describe("The file name of the attachment, e.g. 'screenshot.png'. Used to determine the MIME type for the returned resource."),
1073
1214
  }, async ({ project, attachmentId, fileName }) => {
1074
1215
  try {
1075
1216
  const connection = await connectionProvider();
1217
+ let resolvedProject = project;
1218
+ if (!resolvedProject) {
1219
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve the work item attachment from.");
1220
+ if ("response" in result)
1221
+ return result.response;
1222
+ resolvedProject = result.resolved;
1223
+ }
1076
1224
  const workItemApi = await connection.getWorkItemTrackingApi();
1077
- const stream = await workItemApi.getAttachmentContent(attachmentId, fileName, project);
1225
+ const stream = await workItemApi.getAttachmentContent(attachmentId, fileName, resolvedProject);
1078
1226
  const chunks = [];
1079
1227
  await new Promise((resolve, reject) => {
1080
1228
  stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "2.5.0-nightly.20260412";
1
+ export const packageVersion = "2.5.0-nightly.20260414";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "2.5.0-nightly.20260412",
3
+ "version": "2.5.0-nightly.20260414",
4
4
  "mcpName": "microsoft.com/azure-devops",
5
5
  "description": "MCP server for interacting with Azure DevOps",
6
6
  "license": "MIT",