@azure-devops/mcp 0.1.0 → 1.1.0

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,7 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
3
  import { z } from "zod";
4
- import { batchApiVersion, userAgent } from "../utils.js";
4
+ import { batchApiVersion } from "../utils.js";
5
5
  const WORKITEM_TOOLS = {
6
6
  my_work_items: "wit_my_work_items",
7
7
  list_backlogs: "wit_list_backlogs",
@@ -13,14 +13,14 @@ const WORKITEM_TOOLS = {
13
13
  list_work_item_comments: "wit_list_work_item_comments",
14
14
  get_work_items_for_iteration: "wit_get_work_items_for_iteration",
15
15
  add_work_item_comment: "wit_add_work_item_comment",
16
- add_child_work_item: "wit_add_child_work_item",
16
+ add_child_work_items: "wit_add_child_work_items",
17
17
  link_work_item_to_pull_request: "wit_link_work_item_to_pull_request",
18
18
  get_work_item_type: "wit_get_work_item_type",
19
19
  get_query: "wit_get_query",
20
20
  get_query_results_by_id: "wit_get_query_results_by_id",
21
21
  update_work_items_batch: "wit_update_work_items_batch",
22
22
  close_and_link_workitem_duplicates: "wit_close_and_link_workitem_duplicates",
23
- work_items_link: "wit_work_items_link"
23
+ work_items_link: "wit_work_items_link",
24
24
  };
25
25
  function getLinkTypeFromName(name) {
26
26
  switch (name.toLowerCase()) {
@@ -46,10 +46,10 @@ function getLinkTypeFromName(name) {
46
46
  throw new Error(`Unknown link type: ${name}`);
47
47
  }
48
48
  }
49
- function configureWorkItemTools(server, tokenProvider, connectionProvider) {
49
+ function configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider) {
50
50
  server.tool(WORKITEM_TOOLS.list_backlogs, "Revieve a list of backlogs for a given project and team.", {
51
51
  project: z.string().describe("The name or ID of the Azure DevOps project."),
52
- team: z.string().describe("The name or ID of the Azure DevOps team.")
52
+ team: z.string().describe("The name or ID of the Azure DevOps team."),
53
53
  }, async ({ project, team }) => {
54
54
  const connection = await connectionProvider();
55
55
  const workApi = await connection.getWorkApi();
@@ -62,7 +62,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
62
62
  server.tool(WORKITEM_TOOLS.list_backlog_work_items, "Retrieve a list of backlogs of for a given project, team, and backlog category", {
63
63
  project: z.string().describe("The name or ID of the Azure DevOps project."),
64
64
  team: z.string().describe("The name or ID of the Azure DevOps team."),
65
- backlogId: z.string().describe("The ID of the backlog category to retrieve work items from.")
65
+ backlogId: z.string().describe("The ID of the backlog category to retrieve work items from."),
66
66
  }, async ({ project, team, backlogId }) => {
67
67
  const connection = await connectionProvider();
68
68
  const workApi = await connection.getWorkApi();
@@ -87,7 +87,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
87
87
  });
88
88
  server.tool(WORKITEM_TOOLS.get_work_items_batch_by_ids, "Retrieve list of work items by IDs in batch.", {
89
89
  project: z.string().describe("The name or ID of the Azure DevOps project."),
90
- ids: z.array(z.number()).describe("The IDs of the work items to retrieve.")
90
+ ids: z.array(z.number()).describe("The IDs of the work items to retrieve."),
91
91
  }, async ({ project, ids }) => {
92
92
  const connection = await connectionProvider();
93
93
  const workItemApi = await connection.getWorkItemTrackingApi();
@@ -101,11 +101,12 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
101
101
  id: z.number().describe("The ID of the work item to retrieve."),
102
102
  project: z.string().describe("The name or ID of the Azure DevOps project."),
103
103
  fields: z.array(z.string()).optional().describe("Optional list of fields to include in the response. If not provided, all fields will be returned."),
104
- asOf: z.date().optional().describe("Optional date to retrieve the work item as of a specific time. If not provided, the current state will be returned."),
104
+ 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."),
105
105
  expand: z
106
106
  .enum(["all", "fields", "links", "none", "relations"])
107
107
  .describe("Optional expand parameter to include additional details in the response.")
108
- .optional().describe("Expand options include 'all', 'fields', 'links', 'none', and 'relations'. Defaults to 'none'."),
108
+ .optional()
109
+ .describe("Expand options include 'all', 'fields', 'links', 'none', and 'relations'. Defaults to 'none'."),
109
110
  }, async ({ id, project, fields, asOf, expand }) => {
110
111
  const connection = await connectionProvider();
111
112
  const workItemApi = await connection.getWorkItemTrackingApi();
@@ -117,7 +118,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
117
118
  server.tool(WORKITEM_TOOLS.list_work_item_comments, "Retrieve list of comments for a work item by ID.", {
118
119
  project: z.string().describe("The name or ID of the Azure DevOps project."),
119
120
  workItemId: z.number().describe("The ID of the work item to retrieve comments for."),
120
- top: z.number().default(50).describe("Optional number of comments to retrieve. Defaults to all comments.")
121
+ top: z.number().default(50).describe("Optional number of comments to retrieve. Defaults to all comments."),
121
122
  }, async ({ project, workItemId, top }) => {
122
123
  const connection = await connectionProvider();
123
124
  const workItemApi = await connection.getWorkItemTrackingApi();
@@ -129,79 +130,138 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
129
130
  server.tool(WORKITEM_TOOLS.add_work_item_comment, "Add comment to a work item by ID.", {
130
131
  project: z.string().describe("The name or ID of the Azure DevOps project."),
131
132
  workItemId: z.number().describe("The ID of the work item to add a comment to."),
132
- comment: z.string().describe("The text of the comment to add to the work item.")
133
+ comment: z.string().describe("The text of the comment to add to the work item."),
133
134
  }, async ({ project, workItemId, comment }) => {
134
135
  const connection = await connectionProvider();
135
136
  const workItemApi = await connection.getWorkItemTrackingApi();
136
137
  const commentCreate = { text: comment };
137
138
  const commentResponse = await workItemApi.addComment(commentCreate, project, workItemId);
138
139
  return {
139
- content: [
140
- { type: "text", text: JSON.stringify(commentResponse, null, 2) },
141
- ],
140
+ content: [{ type: "text", text: JSON.stringify(commentResponse, null, 2) }],
142
141
  };
143
142
  });
144
- server.tool(WORKITEM_TOOLS.add_child_work_item, "Create a child work item from a parent by ID.", {
143
+ 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.", {
145
144
  parentId: z.number().describe("The ID of the parent work item to create a child work item under."),
146
145
  project: z.string().describe("The name or ID of the Azure DevOps project."),
147
146
  workItemType: z.string().describe("The type of the child work item to create."),
148
- title: z.string().describe("The title of the child work item."),
149
- description: z.string().describe("The description of the child work item."),
150
- areaPath: z.string().optional().describe("Optional area path for the child work item."),
151
- iterationPath: z.string().optional().describe("Optional iteration path for the child work item."),
152
- }, async ({ parentId, project, workItemType, title, description, areaPath, iterationPath, }) => {
153
- const connection = await connectionProvider();
154
- const workItemApi = await connection.getWorkItemTrackingApi();
155
- const document = [
156
- {
157
- op: "add",
158
- path: "/fields/System.Title",
159
- value: title
160
- },
161
- {
162
- op: "add",
163
- path: "/fields/System.Description",
164
- value: description
165
- },
166
- {
167
- op: "add",
168
- path: "/relations/-",
169
- value: {
170
- rel: "System.LinkTypes.Hierarchy-Reverse",
171
- url: `${connection.serverUrl}/${project}/_apis/wit/workItems/${parentId}`,
147
+ items: z.array(z.object({
148
+ title: z.string().describe("The title of the child work item."),
149
+ description: z.string().describe("The description of the child work item."),
150
+ format: z.enum(["Markdown", "Html"]).default("Html").describe("Format for the description on the child work item, e.g., 'Markdown', 'Html'. Defaults to 'Html'."),
151
+ areaPath: z.string().optional().describe("Optional area path for the child work item."),
152
+ iterationPath: z.string().optional().describe("Optional iteration path for the child work item."),
153
+ })),
154
+ }, async ({ parentId, project, workItemType, items }) => {
155
+ try {
156
+ const connection = await connectionProvider();
157
+ const orgUrl = connection.serverUrl;
158
+ const accessToken = await tokenProvider();
159
+ if (items.length > 50) {
160
+ return {
161
+ content: [{ type: "text", text: `A maximum of 50 child work items can be created in a single call.` }],
162
+ isError: true,
163
+ };
164
+ }
165
+ const body = items.map((item, x) => {
166
+ const ops = [
167
+ {
168
+ op: "add",
169
+ path: "/id",
170
+ value: `-${x + 1}`,
171
+ },
172
+ {
173
+ op: "add",
174
+ path: "/fields/System.Title",
175
+ value: item.title,
176
+ },
177
+ {
178
+ op: "add",
179
+ path: "/fields/System.Description",
180
+ value: item.description,
181
+ },
182
+ {
183
+ op: "add",
184
+ path: "/fields/Microsoft.VSTS.TCM.ReproSteps",
185
+ value: item.description,
186
+ },
187
+ {
188
+ op: "add",
189
+ path: "/relations/-",
190
+ value: {
191
+ rel: "System.LinkTypes.Hierarchy-Reverse",
192
+ url: `${connection.serverUrl}/${project}/_apis/wit/workItems/${parentId}`,
193
+ },
194
+ },
195
+ ];
196
+ if (item.areaPath && item.areaPath.trim().length > 0) {
197
+ ops.push({
198
+ op: "add",
199
+ path: "/fields/System.AreaPath",
200
+ value: item.areaPath,
201
+ });
202
+ }
203
+ if (item.format && item.format === "Markdown") {
204
+ ops.push({
205
+ op: "add",
206
+ path: "/multilineFieldsFormat/System.Description",
207
+ value: item.format,
208
+ });
209
+ ops.push({
210
+ op: "add",
211
+ path: "/multilineFieldsFormat/Microsoft.VSTS.TCM.ReproSteps",
212
+ value: item.format,
213
+ });
214
+ }
215
+ if (item.iterationPath && item.iterationPath.trim().length > 0) {
216
+ ops.push({
217
+ op: "add",
218
+ path: "/fields/System.IterationPath",
219
+ value: item.iterationPath,
220
+ });
221
+ }
222
+ return {
223
+ method: "PATCH",
224
+ uri: `/${project}/_apis/wit/workitems/$${workItemType}?api-version=${batchApiVersion}`,
225
+ headers: {
226
+ "Content-Type": "application/json-patch+json",
227
+ },
228
+ body: ops,
229
+ };
230
+ });
231
+ const response = await fetch(`${orgUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
232
+ method: "PATCH",
233
+ headers: {
234
+ "Authorization": `Bearer ${accessToken.token}`,
235
+ "Content-Type": "application/json",
236
+ "User-Agent": userAgentProvider(),
172
237
  },
173
- },
174
- ];
175
- if (areaPath && areaPath.trim().length > 0) {
176
- document.push({
177
- op: "add",
178
- path: "/fields/System.AreaPath",
179
- value: areaPath,
238
+ body: JSON.stringify(body),
180
239
  });
240
+ if (!response.ok) {
241
+ throw new Error(`Failed to update work items in batch: ${response.statusText}`);
242
+ }
243
+ const result = await response.json();
244
+ return {
245
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
246
+ };
181
247
  }
182
- if (iterationPath && iterationPath.trim().length > 0) {
183
- document.push({
184
- op: "add",
185
- path: "/fields/System.IterationPath",
186
- value: iterationPath,
187
- });
248
+ catch (error) {
249
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
250
+ return {
251
+ content: [{ type: "text", text: `Error creating child work items: ${errorMessage}` }],
252
+ isError: true,
253
+ };
188
254
  }
189
- const childWorkItem = await workItemApi.createWorkItem(null, document, project, workItemType);
190
- return {
191
- content: [
192
- { type: "text", text: JSON.stringify(childWorkItem, null, 2) },
193
- ],
194
- };
195
255
  });
196
256
  server.tool(WORKITEM_TOOLS.link_work_item_to_pull_request, "Link a single work item to an existing pull request.", {
197
- project: z.string().describe,
257
+ project: z.string().describe("The name or ID of the Azure DevOps project."),
198
258
  repositoryId: z.string().describe("The ID of the repository containing the pull request. Do not use the repository name here, use the ID instead."),
199
259
  pullRequestId: z.number().describe("The ID of the pull request to link to."),
200
260
  workItemId: z.number().describe("The ID of the work item to link to the pull request."),
201
261
  }, async ({ project, repositoryId, pullRequestId, workItemId }) => {
202
- const connection = await connectionProvider();
203
- const workItemTrackingApi = await connection.getWorkItemTrackingApi();
204
262
  try {
263
+ const connection = await connectionProvider();
264
+ const workItemTrackingApi = await connection.getWorkItemTrackingApi();
205
265
  // Create artifact link relation using vstfs format
206
266
  // Format: vstfs:///Git/PullRequestId/{project}/{repositoryId}/{pullRequestId}
207
267
  const artifactPathValue = `${project}/${repositoryId}/${pullRequestId}`;
@@ -221,7 +281,10 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
221
281
  },
222
282
  ];
223
283
  // Use the WorkItem API to update the work item with the new relation
224
- await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId, project);
284
+ const workItem = await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId, project);
285
+ if (!workItem) {
286
+ return { content: [{ type: "text", text: "Work item update failed" }], isError: true };
287
+ }
225
288
  return {
226
289
  content: [
227
290
  {
@@ -236,18 +299,10 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
236
299
  };
237
300
  }
238
301
  catch (error) {
239
- console.error(`Error linking work item ${workItemId} to PR ${pullRequestId}:`, error);
302
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
240
303
  return {
241
- content: [
242
- {
243
- type: "text",
244
- text: JSON.stringify({
245
- workItemId,
246
- pullRequestId,
247
- success: false,
248
- }, null, 2),
249
- },
250
- ],
304
+ content: [{ type: "text", text: `Error linking work item to pull request: ${errorMessage}` }],
305
+ isError: true,
251
306
  };
252
307
  }
253
308
  });
@@ -266,19 +321,19 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
266
321
  });
267
322
  server.tool(WORKITEM_TOOLS.update_work_item, "Update a work item by ID with specified fields.", {
268
323
  id: z.number().describe("The ID of the work item to update."),
269
- updates: z.array(z.object({
324
+ updates: z
325
+ .array(z.object({
270
326
  op: z.enum(["add", "replace", "remove"]).default("add").describe("The operation to perform on the field."),
271
327
  path: z.string().describe("The path of the field to update, e.g., '/fields/System.Title'."),
272
328
  value: z.string().describe("The new value for the field. This is required for 'add' and 'replace' operations, and should be omitted for 'remove' operations."),
273
- })).describe("An array of field updates to apply to the work item."),
329
+ }))
330
+ .describe("An array of field updates to apply to the work item."),
274
331
  }, async ({ id, updates }) => {
275
332
  const connection = await connectionProvider();
276
333
  const workItemApi = await connection.getWorkItemTrackingApi();
277
334
  const updatedWorkItem = await workItemApi.updateWorkItem(null, updates, id);
278
335
  return {
279
- content: [
280
- { type: "text", text: JSON.stringify(updatedWorkItem, null, 2) },
281
- ],
336
+ content: [{ type: "text", text: JSON.stringify(updatedWorkItem, null, 2) }],
282
337
  };
283
338
  });
284
339
  server.tool(WORKITEM_TOOLS.get_work_item_type, "Get a specific work item type.", {
@@ -289,27 +344,39 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
289
344
  const workItemApi = await connection.getWorkItemTrackingApi();
290
345
  const workItemTypeInfo = await workItemApi.getWorkItemType(project, workItemType);
291
346
  return {
292
- content: [
293
- { type: "text", text: JSON.stringify(workItemTypeInfo, null, 2) },
294
- ],
347
+ content: [{ type: "text", text: JSON.stringify(workItemTypeInfo, null, 2) }],
295
348
  };
296
349
  });
297
350
  server.tool(WORKITEM_TOOLS.create_work_item, "Create a new work item in a specified project and work item type.", {
298
351
  project: z.string().describe("The name or ID of the Azure DevOps project."),
299
352
  workItemType: z.string().describe("The type of work item to create, e.g., 'Task', 'Bug', etc."),
300
- fields: z.record(z.string(), z.string()).describe("A record of field names and values to set on the new work item. Each key is a field name, and each value is the corresponding value to set for that field."),
353
+ fields: z
354
+ .record(z.string(), z.string())
355
+ .describe("A record of field names and values to set on the new work item. Each key is a field name, and each value is the corresponding value to set for that field."),
301
356
  }, async ({ project, workItemType, fields }) => {
302
- const connection = await connectionProvider();
303
- const workItemApi = await connection.getWorkItemTrackingApi();
304
- const document = Object.entries(fields).map(([key, value]) => ({
305
- op: "add",
306
- path: `/fields/${key}`,
307
- value,
308
- }));
309
- const newWorkItem = await workItemApi.createWorkItem(null, document, project, workItemType);
310
- return {
311
- content: [{ type: "text", text: JSON.stringify(newWorkItem, null, 2) }],
312
- };
357
+ try {
358
+ const connection = await connectionProvider();
359
+ const workItemApi = await connection.getWorkItemTrackingApi();
360
+ const document = Object.entries(fields).map(([key, value]) => ({
361
+ op: "add",
362
+ path: `/fields/${key}`,
363
+ value,
364
+ }));
365
+ const newWorkItem = await workItemApi.createWorkItem(null, document, project, workItemType);
366
+ if (!newWorkItem) {
367
+ return { content: [{ type: "text", text: "Work item was not created" }], isError: true };
368
+ }
369
+ return {
370
+ content: [{ type: "text", text: JSON.stringify(newWorkItem, null, 2) }],
371
+ };
372
+ }
373
+ catch (error) {
374
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
375
+ return {
376
+ content: [{ type: "text", text: `Error creating work item: ${errorMessage}` }],
377
+ isError: true,
378
+ };
379
+ }
313
380
  });
314
381
  server.tool(WORKITEM_TOOLS.get_query, "Get a query by its ID or path.", {
315
382
  project: z.string().describe("The name or ID of the Azure DevOps project."),
@@ -318,14 +385,12 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
318
385
  depth: z.number().default(0).describe("Optional depth parameter to specify how deep to expand the query. Defaults to 0."),
319
386
  includeDeleted: z.boolean().default(false).describe("Whether to include deleted items in the query results. Defaults to false."),
320
387
  useIsoDateFormat: z.boolean().default(false).describe("Whether to use ISO date format in the response. Defaults to false."),
321
- }, async ({ project, query, expand, depth, includeDeleted, useIsoDateFormat, }) => {
388
+ }, async ({ project, query, expand, depth, includeDeleted, useIsoDateFormat }) => {
322
389
  const connection = await connectionProvider();
323
390
  const workItemApi = await connection.getWorkItemTrackingApi();
324
391
  const queryDetails = await workItemApi.getQuery(project, query, expand, depth, includeDeleted, useIsoDateFormat);
325
392
  return {
326
- content: [
327
- { type: "text", text: JSON.stringify(queryDetails, null, 2) },
328
- ],
393
+ content: [{ type: "text", text: JSON.stringify(queryDetails, null, 2) }],
329
394
  };
330
395
  });
331
396
  server.tool(WORKITEM_TOOLS.get_query_results_by_id, "Retrieve the results of a work item query given the query ID.", {
@@ -344,12 +409,14 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
344
409
  };
345
410
  });
346
411
  server.tool(WORKITEM_TOOLS.update_work_items_batch, "Update work items in batch", {
347
- updates: z.array(z.object({
412
+ updates: z
413
+ .array(z.object({
348
414
  op: z.enum(["add", "replace", "remove"]).default("add").describe("The operation to perform on the field."),
349
415
  id: z.number().describe("The ID of the work item to update."),
350
416
  path: z.string().describe("The path of the field to update, e.g., '/fields/System.Title'."),
351
417
  value: z.string().describe("The new value for the field. This is required for 'add' and 'replace' operations, and should be omitted for 'remove' operations."),
352
- })).describe("An array of updates to apply to work items. Each update should include the operation (op), work item ID (id), field path (path), and new value (value)."),
418
+ }))
419
+ .describe("An array of updates to apply to work items. Each update should include the operation (op), work item ID (id), field path (path), and new value (value)."),
353
420
  }, async ({ updates }) => {
354
421
  const connection = await connectionProvider();
355
422
  const orgUrl = connection.serverUrl;
@@ -362,7 +429,9 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
362
429
  headers: {
363
430
  "Content-Type": "application/json-patch+json",
364
431
  },
365
- body: updates.filter((update) => update.id === id).map(({ op, path, value }) => ({
432
+ body: updates
433
+ .filter((update) => update.id === id)
434
+ .map(({ op, path, value }) => ({
366
435
  op: op,
367
436
  path: path,
368
437
  value: value,
@@ -371,9 +440,9 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
371
440
  const response = await fetch(`${orgUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
372
441
  method: "PATCH",
373
442
  headers: {
374
- Authorization: `Bearer ${accessToken.token}`,
443
+ "Authorization": `Bearer ${accessToken.token}`,
375
444
  "Content-Type": "application/json",
376
- "User-Agent": `${userAgent}`,
445
+ "User-Agent": userAgentProvider(),
377
446
  },
378
447
  body: JSON.stringify(body),
379
448
  });
@@ -387,12 +456,17 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
387
456
  });
388
457
  server.tool(WORKITEM_TOOLS.work_items_link, "Link work items together in batch.", {
389
458
  project: z.string().describe("The name or ID of the Azure DevOps project."),
390
- updates: z.array(z.object({
459
+ updates: z
460
+ .array(z.object({
391
461
  id: z.number().describe("The ID of the work item to update."),
392
462
  linkToId: z.number().describe("The ID of the work item to link to."),
393
- type: z.enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests"]).default("related").describe("Type of link to create between the work items. Options include 'parent', 'child', 'duplicate', 'duplicate of', 'related', 'successor', 'predecessor', 'tested by', 'tests', 'referenced by', and 'references'. Defaults to 'related'."),
463
+ type: z
464
+ .enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests"])
465
+ .default("related")
466
+ .describe("Type of link to create between the work items. Options include 'parent', 'child', 'duplicate', 'duplicate of', 'related', 'successor', 'predecessor', 'tested by', 'tests', 'referenced by', and 'references'. Defaults to 'related'."),
394
467
  comment: z.string().optional().describe("Optional comment to include with the link. This can be used to provide additional context for the link being created."),
395
- })).describe(""),
468
+ }))
469
+ .describe(""),
396
470
  }, async ({ project, updates }) => {
397
471
  const connection = await connectionProvider();
398
472
  const orgUrl = connection.serverUrl;
@@ -405,7 +479,9 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
405
479
  headers: {
406
480
  "Content-Type": "application/json-patch+json",
407
481
  },
408
- body: updates.filter((update) => update.id === id).map(({ linkToId, type, comment }) => ({
482
+ body: updates
483
+ .filter((update) => update.id === id)
484
+ .map(({ linkToId, type, comment }) => ({
409
485
  op: "add",
410
486
  path: "/relations/-",
411
487
  value: {
@@ -414,15 +490,15 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
414
490
  attributes: {
415
491
  comment: comment || "",
416
492
  },
417
- }
493
+ },
418
494
  })),
419
495
  }));
420
496
  const response = await fetch(`${orgUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
421
497
  method: "PATCH",
422
498
  headers: {
423
- Authorization: `Bearer ${accessToken.token}`,
499
+ "Authorization": `Bearer ${accessToken.token}`,
424
500
  "Content-Type": "application/json",
425
- "User-Agent": `${userAgent}`,
501
+ "User-Agent": userAgentProvider(),
426
502
  },
427
503
  body: JSON.stringify(body),
428
504
  });
@@ -467,9 +543,9 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider) {
467
543
  const response = await fetch(`${connection.serverUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
468
544
  method: "PATCH",
469
545
  headers: {
470
- Authorization: `Bearer ${accessToken.token}`,
546
+ "Authorization": `Bearer ${accessToken.token}`,
471
547
  "Content-Type": "application/json",
472
- "User-Agent": `${userAgent}`,
548
+ "User-Agent": userAgentProvider(),
473
549
  },
474
550
  body: JSON.stringify(body),
475
551
  });
package/dist/tools.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
3
  import { configureCoreTools } from "./tools/core.js";
4
+ import { configureWorkTools } from "./tools/work.js";
4
5
  import { configureBuildTools } from "./tools/builds.js";
5
6
  import { configureRepoTools } from "./tools/repos.js";
6
7
  import { configureWorkItemTools } from "./tools/workitems.js";
@@ -8,14 +9,15 @@ import { configureReleaseTools } from "./tools/releases.js";
8
9
  import { configureWikiTools } from "./tools/wiki.js";
9
10
  import { configureTestPlanTools } from "./tools/testplans.js";
10
11
  import { configureSearchTools } from "./tools/search.js";
11
- function configureAllTools(server, tokenProvider, connectionProvider) {
12
+ function configureAllTools(server, tokenProvider, connectionProvider, userAgentProvider) {
12
13
  configureCoreTools(server, tokenProvider, connectionProvider);
14
+ configureWorkTools(server, tokenProvider, connectionProvider);
13
15
  configureBuildTools(server, tokenProvider, connectionProvider);
14
16
  configureRepoTools(server, tokenProvider, connectionProvider);
15
- configureWorkItemTools(server, tokenProvider, connectionProvider);
17
+ configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
16
18
  configureReleaseTools(server, tokenProvider, connectionProvider);
17
19
  configureWikiTools(server, tokenProvider, connectionProvider);
18
20
  configureTestPlanTools(server, tokenProvider, connectionProvider);
19
- configureSearchTools(server, tokenProvider, connectionProvider);
21
+ configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider);
20
22
  }
21
23
  export { configureAllTools };
@@ -0,0 +1,18 @@
1
+ class UserAgentComposer {
2
+ _userAgent;
3
+ _mcpClientInfoAppended;
4
+ constructor(packageVersion) {
5
+ this._userAgent = `AzureDevOps.MCP/${packageVersion} (local)`;
6
+ this._mcpClientInfoAppended = false;
7
+ }
8
+ get userAgent() {
9
+ return this._userAgent;
10
+ }
11
+ appendMcpClientInfo(info) {
12
+ if (!this._mcpClientInfoAppended && info && info.name && info.version) {
13
+ this._userAgent += ` ${info.name}/${info.version}`;
14
+ this._mcpClientInfoAppended = true;
15
+ }
16
+ }
17
+ }
18
+ export { UserAgentComposer };
package/dist/utils.js CHANGED
@@ -1,6 +1,4 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
- import { packageVersion } from "./version.js";
4
3
  export const apiVersion = "7.2-preview.1";
5
4
  export const batchApiVersion = "5.0";
6
- export const userAgent = `AzureDevOps.MCP/${packageVersion} (local)`;
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "0.1.0";
1
+ export const packageVersion = "1.1.0";