@azure-devops/mcp 2.10.0 → 2.11.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,8 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
- import * as fs from "fs";
3
+ import { constants, open, realpath } from "fs/promises";
4
4
  import * as path from "path";
5
+ import { Readable } from "stream";
5
6
  import { WorkItemExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
6
7
  import { QueryExpand } from "azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js";
7
8
  import { z } from "zod";
@@ -14,10 +15,25 @@ const WORKITEM_TOOLS = {
14
15
  wit_query: "wit_query",
15
16
  wit_backlog: "wit_backlog",
16
17
  wit_work_item_attachment: "wit_work_item_attachment",
18
+ wit_work_item_attachment_upload: "wit_work_item_attachment_upload",
19
+ wit_work_item_attachment_link: "wit_work_item_attachment_link",
17
20
  wit_work_item_write: "wit_work_item_write",
18
21
  wit_work_item_comment_write: "wit_work_item_comment_write",
19
22
  wit_work_item_link_write: "wit_work_item_link_write",
20
23
  };
24
+ const jsonPatchValueSchema = z.lazy(() => z.union([z.string(), z.number().finite(), z.boolean(), z.null(), z.array(jsonPatchValueSchema), z.record(jsonPatchValueSchema)]));
25
+ const readConsistencyHandler = {
26
+ prepareRequest(options) {
27
+ options.headers = { ...options.headers, "X-VSS-ReadConsistencyLevel": "Eventual" };
28
+ },
29
+ canHandleAuthentication: () => false,
30
+ async handleAuthentication() {
31
+ throw new Error("The read-consistency handler does not handle authentication.");
32
+ },
33
+ };
34
+ function getReadReplicaWorkItemTrackingApi(connection) {
35
+ return connection.getWorkItemTrackingApi(undefined, [connection.authHandler, readConsistencyHandler]);
36
+ }
21
37
  function getLinkTypeFromName(name) {
22
38
  switch (name.toLowerCase()) {
23
39
  case "parent":
@@ -68,10 +84,8 @@ function escapeHtml(value) {
68
84
  };
69
85
  return value.replace(/[&<>"']/g, (character) => entities[character]);
70
86
  }
71
- async function resolveCommentMentions(text, format, tokenProvider, connectionProvider, userAgentProvider) {
87
+ async function prepareCommentText(text, format, tokenProvider, connectionProvider, userAgentProvider) {
72
88
  const emailMatches = [...text.matchAll(/@<([^<>\s]+@[^<>\s]+)>/g)];
73
- if (emailMatches.length === 0)
74
- return text;
75
89
  const identities = new Map();
76
90
  for (const email of new Set(emailMatches.map((match) => match[1]))) {
77
91
  try {
@@ -81,20 +95,30 @@ async function resolveCommentMentions(text, format, tokenProvider, connectionPro
81
95
  // Leave mentions unchanged when their identities cannot be resolved.
82
96
  }
83
97
  }
84
- return text.replace(/@<([^<>\s]+@[^<>\s]+)>/g, (mention, email) => {
98
+ // Encode Markdown transport text once, preserving mention tokens for Azure DevOps.
99
+ // Encoding ampersands also preserves intentional entity text after one decode.
100
+ const markdown = format === "Markdown" || format === undefined;
101
+ return text.replace(/@<([^<>\s]+@[^<>\s]+)>|@<([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})>|[&<>]/gi, (mention, email, id) => {
102
+ if (id)
103
+ return mention;
104
+ if (!email)
105
+ return markdown ? escapeHtml(mention) : mention;
85
106
  const identity = identities.get(email);
86
107
  if (!identity)
87
108
  return escapeHtml(mention);
88
- return format === "Markdown" || format === undefined ? `@<${identity.id}>` : `<a href="#" data-vss-mention="version:2.0,${identity.id}">@${escapeHtml(identity.displayName)}</a>`;
109
+ return markdown ? `@<${identity.id}>` : `<a href="#" data-vss-mention="version:2.0,${identity.id}">@${escapeHtml(identity.displayName)}</a>`;
89
110
  });
90
111
  }
91
112
  function configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider) {
92
113
  // --- wit_work_item ----------------------------------------------------------
93
- server.tool(WORKITEM_TOOLS.wit_work_item, "Retrieve work item data for a project. Use the action parameter to specify the operation.", {
114
+ server.tool(WORKITEM_TOOLS.wit_work_item, "Retrieve work item data. Use the action parameter to specify the operation.", {
94
115
  action: z
95
116
  .enum(["get", "get_batch", "list_comments", "my", "list_revisions", "list_for_iteration", "get_type"])
96
117
  .describe("The action to perform. Options: get (get a single work item by ID), get_batch (get multiple work items by IDs), list_comments (list comments on a work item), my (get work items relevant to the authenticated user), list_revisions (list revisions of a work item), list_for_iteration (list work items for a team iteration), get_type (get metadata for a work item type)."),
97
- 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."),
118
+ project: z
119
+ .string()
120
+ .optional()
121
+ .describe("The name or ID of the Azure DevOps project. Optional for get; when omitted, the work item is retrieved at organization scope. For other actions, a project selection prompt will be shown if omitted."),
98
122
  id: z.coerce.number().min(1).optional().describe("Work item ID. Required for: get."),
99
123
  ids: z.array(z.coerce.number().min(1)).optional().describe("Work item IDs. Required for: get_batch."),
100
124
  workItemId: z.coerce.number().min(1).optional().describe("Work item ID. Required for: list_comments, list_revisions."),
@@ -105,25 +129,20 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
105
129
  .optional()
106
130
  .describe("Expand options (None, Fields, Relations, Links, All). Used for: get, list_revisions. For get, cannot be combined with fields."),
107
131
  top: z.coerce.number().optional().describe("Maximum number of results to return. Used for: list_comments, my, list_revisions. Defaults vary by action."),
132
+ continuationToken: z.string().optional().describe("Token for continuing paged results. Used for: list_comments."),
108
133
  includeCompleted: z.boolean().optional().default(false).describe("Include completed work items. Used for: my. Defaults to false."),
109
134
  type: z.enum(["assignedtome", "myactivity"]).optional().describe("Type of work items to retrieve. Used for: my. Defaults to 'assignedtome'."),
110
135
  skip: z.coerce.number().optional().describe("Number of results to skip for pagination. Used for: list_revisions."),
111
136
  team: z.string().optional().describe("Team name or ID. Used for: list_for_iteration."),
112
137
  iterationId: z.string().optional().describe("Iteration ID. Required for: list_for_iteration."),
113
138
  workItemType: z.string().optional().describe("Work item type name. Required for: get_type."),
114
- }, async ({ action, project, id, ids, workItemId, fields, asOf, expand, top, includeCompleted, type, skip, team, iterationId, workItemType }) => {
139
+ }, async ({ action, project, id, ids, workItemId, fields, asOf, expand, top, continuationToken, includeCompleted, type, skip, team, iterationId, workItemType }) => {
115
140
  try {
116
141
  const connection = await connectionProvider();
117
142
  let resolvedProject = project;
118
143
  if (action === "get") {
119
144
  if (!id)
120
145
  return { content: [{ type: "text", text: "id is required for get" }], isError: true };
121
- if (!resolvedProject) {
122
- const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve the work item from.");
123
- if ("response" in result)
124
- return result.response;
125
- resolvedProject = result.resolved;
126
- }
127
146
  let effectiveExpand = expand;
128
147
  if (fields && fields.length > 0 && effectiveExpand != null) {
129
148
  effectiveExpand = "none";
@@ -180,7 +199,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
180
199
  resolvedProject = result.resolved;
181
200
  }
182
201
  const workItemApi = await connection.getWorkItemTrackingApi();
183
- const comments = await workItemApi.getComments(resolvedProject, workItemId, top ?? 50);
202
+ const comments = await workItemApi.getComments(resolvedProject, workItemId, top ?? 50, continuationToken);
184
203
  return { content: [{ type: "text", text: JSON.stringify(comments, null, 2) }] };
185
204
  }
186
205
  if (action === "my") {
@@ -289,7 +308,10 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
289
308
  team: z.string().optional().describe("Team name or ID. Used for: get_results, wiql."),
290
309
  timePrecision: z.boolean().optional().describe("Include time precision in date fields. Used for: get_results, wiql."),
291
310
  top: z.coerce.number().default(50).describe("Maximum number of results to return. Used for: get_results, wiql. Defaults to 50."),
292
- responseType: z.enum(["full", "ids"]).default("full").describe("Response type: 'full' returns complete results (default), 'ids' returns only work item IDs. Used for: get_results."),
311
+ responseType: z
312
+ .enum(["full", "ids"])
313
+ .default("full")
314
+ .describe("Response type: 'full' returns complete results (default). 'ids' returns only work item IDs and supports flat queries only; any other query type is not supported. Used for: get_results."),
293
315
  wiql: z.string().max(32768).optional().describe('The WIQL query string to execute. Required for: wiql. Example: "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @project".'),
294
316
  }, async ({ action, project, query, expand, depth, includeDeleted, useIsoDateFormat, id, team, timePrecision, top, responseType, wiql }) => {
295
317
  try {
@@ -311,7 +333,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
311
333
  if (action === "get_results") {
312
334
  if (!id)
313
335
  return { content: [{ type: "text", text: "id is required for get_results" }], isError: true };
314
- const workItemApi = await connection.getWorkItemTrackingApi();
336
+ const workItemApi = await getReadReplicaWorkItemTrackingApi(connection);
315
337
  const teamContext = { project: resolvedProject, team };
316
338
  const queryResult = await workItemApi.queryById(id, teamContext, timePrecision, top);
317
339
  if (responseType === "ids") {
@@ -323,7 +345,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
323
345
  if (action === "wiql") {
324
346
  if (!wiql)
325
347
  return { content: [{ type: "text", text: "wiql is required for wiql" }], isError: true };
326
- const workItemApi = await connection.getWorkItemTrackingApi();
348
+ const workItemApi = await getReadReplicaWorkItemTrackingApi(connection);
327
349
  const teamContext = { project: resolvedProject, team };
328
350
  const queryResult = await workItemApi.queryByWiql({ query: wiql }, teamContext, timePrecision, top);
329
351
  return createExternalContentResponse(queryResult, "wiql query results");
@@ -412,12 +434,12 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
412
434
  // --- wit_work_item_attachment -----------------------------------------------
413
435
  server.tool(WORKITEM_TOOLS.wit_work_item_attachment, "Download a work item attachment by its ID. By default returns the content as a base64-encoded resource. If savePath is provided, saves the file locally to that directory and returns the file path instead. Useful for viewing images (e.g. screenshots) or other files attached to work items such as bugs. If a project is not specified, you will be prompted to select one.", {
414
436
  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."),
415
- attachmentId: z.string().describe("The GUID of the attachment. Found in the attachment URL: https://dev.azure.com/{org}/{project}/_apis/wit/attachments/{attachmentId}"),
437
+ attachmentId: z.string().describe("The GUID of the attachment. Found in the attachment URL: https://dev.azure.com/{org}/{project}/_apis/wit/attachments/{attachmentId}."),
416
438
  fileName: z.string().optional().describe("The file name of the attachment, e.g. 'screenshot.png'. Used to determine the MIME type or the saved file's name."),
417
439
  savePath: z
418
440
  .string()
419
441
  .optional()
420
- .describe("Optional local directory path where the file should be saved. Must be a relative path (e.g. 'temp' or 'downloads/attachments'); absolute paths and path traversals are not allowed. If provided, saves the attachment to this directory and returns the file path. If omitted, returns the content as a base64-encoded resource."),
442
+ .describe("Optional local directory path where the downloaded file should be saved. Must be a relative path (e.g. 'temp' or 'downloads/attachments') to an existing directory that resolves (including symlinks) inside the server's working directory; absolute paths are not allowed. If provided, saves the attachment to this directory and returns the file path. If omitted, returns the content as a base64-encoded resource."),
421
443
  }, async ({ project, attachmentId, fileName, savePath }) => {
422
444
  const isAbsolutePath = (value) => path.posix.isAbsolute(value) || path.win32.isAbsolute(value);
423
445
  const hasDriveLetter = (value) => /^[a-zA-Z]:/.test(value);
@@ -427,6 +449,14 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
427
449
  if (fileName !== undefined && fileName.includes("..")) {
428
450
  throw new Error("Invalid fileName: path traversal is not allowed.");
429
451
  }
452
+ if (!attachmentId)
453
+ return { content: [{ type: "text", text: "attachmentId is required for download" }], isError: true };
454
+ if (savePath) {
455
+ const localFileName = fileName ?? attachmentId;
456
+ if (!localFileName || localFileName.includes("\0") || path.posix.basename(localFileName) !== localFileName || path.win32.basename(localFileName) !== localFileName) {
457
+ throw new Error("Invalid fileName: path components are not allowed.");
458
+ }
459
+ }
430
460
  try {
431
461
  const connection = await connectionProvider();
432
462
  let resolvedProject = project;
@@ -438,6 +468,12 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
438
468
  }
439
469
  const workItemApi = await connection.getWorkItemTrackingApi();
440
470
  const stream = await workItemApi.getAttachmentContent(attachmentId, fileName, resolvedProject);
471
+ const responseStream = stream;
472
+ if (responseStream.statusCode !== undefined && (responseStream.statusCode < 200 || responseStream.statusCode >= 300)) {
473
+ responseStream.resume();
474
+ const statusMessage = responseStream.statusMessage ? ` ${responseStream.statusMessage}` : "";
475
+ throw new Error(`Attachment download failed: HTTP ${responseStream.statusCode}${statusMessage}`);
476
+ }
441
477
  const chunks = [];
442
478
  await new Promise((resolve, reject) => {
443
479
  stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
@@ -446,12 +482,21 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
446
482
  });
447
483
  const buffer = Buffer.concat(chunks);
448
484
  if (savePath) {
449
- const resolvedFileName = fileName ?? attachmentId;
450
- const localFilePath = path.join(savePath, resolvedFileName);
451
- if (fs.existsSync(localFilePath)) {
452
- throw new Error(`File already exists: ${localFilePath}`);
485
+ const workspaceRoot = await realpath(process.cwd());
486
+ const resolvedDirectory = await realpath(path.resolve(workspaceRoot, savePath));
487
+ const relativeDirectory = path.relative(workspaceRoot, resolvedDirectory);
488
+ if (relativeDirectory === ".." || relativeDirectory.startsWith(`..${path.sep}`) || path.isAbsolute(relativeDirectory)) {
489
+ throw new Error("Invalid savePath: destination must remain inside the workspace.");
490
+ }
491
+ const localFilePath = path.resolve(resolvedDirectory, fileName ?? attachmentId);
492
+ // O_EXCL fails if the file (or a symlink) already exists, so there is no check-then-write race.
493
+ const file = await open(localFilePath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0), 0o600);
494
+ try {
495
+ await file.writeFile(buffer);
496
+ }
497
+ finally {
498
+ await file.close();
453
499
  }
454
- fs.writeFileSync(localFilePath, buffer);
455
500
  return {
456
501
  content: [{ type: "text", text: `Attachment saved to: ${localFilePath}` }],
457
502
  };
@@ -482,6 +527,80 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
482
527
  };
483
528
  }
484
529
  });
530
+ // --- wit_work_item_attachment_upload ----------------------------------------
531
+ server.tool(WORKITEM_TOOLS.wit_work_item_attachment_upload, "Upload base64-encoded content as a new work item attachment. This tool only uploads the attachment; use wit_work_item_attachment_link separately to link the returned attachment URL to a work item.", {
532
+ 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."),
533
+ fileName: z.string().describe("The name of the file to upload, e.g. 'screenshot.png'."),
534
+ content: z.string().describe("Base64-encoded file content to upload."),
535
+ }, async ({ project, fileName, content }) => {
536
+ try {
537
+ if (fileName.includes(".."))
538
+ return { content: [{ type: "text", text: "Invalid fileName: path traversal is not allowed." }], isError: true };
539
+ if (!content)
540
+ return { content: [{ type: "text", text: "content is required for upload" }], isError: true };
541
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(content))
542
+ return { content: [{ type: "text", text: "content must be valid base64-encoded data" }], isError: true };
543
+ const connection = await connectionProvider();
544
+ let resolvedProject = project;
545
+ if (!resolvedProject) {
546
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project to upload the work item attachment to.");
547
+ if ("response" in result)
548
+ return result.response;
549
+ resolvedProject = result.resolved;
550
+ }
551
+ const workItemApi = await connection.getWorkItemTrackingApi();
552
+ const attachmentReference = await workItemApi.createAttachment({}, Readable.from(Buffer.from(content, "base64")), fileName, undefined, resolvedProject);
553
+ return { content: [{ type: "text", text: JSON.stringify(attachmentReference, null, 2) }] };
554
+ }
555
+ catch (error) {
556
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
557
+ return {
558
+ content: [{ type: "text", text: `Error uploading work item attachment: ${errorMessage}` }],
559
+ isError: true,
560
+ };
561
+ }
562
+ });
563
+ // --- wit_work_item_attachment_link ------------------------------------------
564
+ server.tool(WORKITEM_TOOLS.wit_work_item_attachment_link, "Link an uploaded attachment to a work item using the attachment URL returned by wit_work_item_attachment_upload.", {
565
+ 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."),
566
+ workItemId: z.coerce.number().min(1).describe("The ID of the work item to link the uploaded attachment to."),
567
+ attachmentUrl: z.string().url().describe("The attachment URL returned by wit_work_item_attachment_upload."),
568
+ comment: z.string().optional().describe("Optional comment to include with the attachment link."),
569
+ }, async ({ project, workItemId, attachmentUrl, comment }) => {
570
+ try {
571
+ const connection = await connectionProvider();
572
+ let resolvedProject = project;
573
+ if (!resolvedProject) {
574
+ const result = await elicitProject(server, connection, "Select the Azure DevOps project containing the work item to link the attachment to.");
575
+ if ("response" in result)
576
+ return result.response;
577
+ resolvedProject = result.resolved;
578
+ }
579
+ const patchDocument = [
580
+ {
581
+ op: "add",
582
+ path: "/relations/-",
583
+ value: {
584
+ rel: "AttachedFile",
585
+ url: attachmentUrl,
586
+ attributes: { comment: comment || "" },
587
+ },
588
+ },
589
+ ];
590
+ const workItemApi = await connection.getWorkItemTrackingApi();
591
+ const workItem = await workItemApi.updateWorkItem({}, patchDocument, workItemId, resolvedProject);
592
+ if (!workItem)
593
+ throw new Error("Work item not found");
594
+ return { content: [{ type: "text", text: JSON.stringify({ attachmentUrl, workItemId }, null, 2) }] };
595
+ }
596
+ catch (error) {
597
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
598
+ return {
599
+ content: [{ type: "text", text: `Error linking work item attachment: ${errorMessage}` }],
600
+ isError: true,
601
+ };
602
+ }
603
+ });
485
604
  // --- wit_work_item_write ----------------------------------------------------
486
605
  server.tool(WORKITEM_TOOLS.wit_work_item_write, "Write operations for work items. Use the action parameter to specify the operation.", {
487
606
  action: z
@@ -507,13 +626,12 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
507
626
  .default("add")
508
627
  .describe("The operation to perform. Use 'test' with path '/rev' to enforce optimistic concurrency."),
509
628
  path: z.string().describe("The path to operate on, e.g. '/fields/System.Title' or '/rev' for a revision test."),
510
- value: z
511
- .union([z.string(), z.number(), z.boolean(), z.null()])
629
+ value: jsonPatchValueSchema
512
630
  .optional()
513
- .describe("The operation value. Required for add, replace, and test; omit for remove. For a test on '/rev', pass the numeric revision previously read."),
631
+ .describe("The JSON operation value, including objects and arrays. Required for add, replace, and test; omit for remove. Pass relation objects directly, not as JSON strings. For a test on '/rev', pass the numeric revision previously read."),
514
632
  }))
515
633
  .optional()
516
- .describe('Field updates for a single work item. Required for: update. For a safe read-modify-write, prepend a test operation on "/rev" with value set to the numeric revision returned by the preceding read; Azure DevOps rejects the entire update if the current revision differs.'),
634
+ .describe('JSON Patch operations for fields and relations on a single work item. Required for: update. For a safe read-modify-write, prepend a test operation on "/rev" with value set to the numeric revision returned by the preceding read; Azure DevOps rejects the entire update if the current revision differs.'),
517
635
  batchUpdates: z
518
636
  .array(z.object({
519
637
  op: z.enum(["Add", "Replace", "Remove"]).default("Add").describe("The operation to perform."),
@@ -746,7 +864,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
746
864
  const orgUrl = connection.serverUrl;
747
865
  const accessToken = await tokenProvider();
748
866
  const formatParameter = (format ?? "Markdown") === "Markdown" ? 0 : 1;
749
- const resolvedText = await resolveCommentMentions(text, format, tokenProvider, connectionProvider, userAgentProvider);
867
+ const resolvedText = await prepareCommentText(text, format, tokenProvider, connectionProvider, userAgentProvider);
750
868
  if (action === "add") {
751
869
  const response = await fetch(`${orgUrl}/${encodeURIComponent(resolvedProject)}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
752
870
  method: "POST",
package/dist/tools.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // Licensed under the MIT License.
3
3
  import { wrapExternalToolResponse } from "./shared/content-safety.js";
4
4
  import { Domain } from "./shared/domains.js";
5
+ import { configureToolsWithAnnotations } from "./shared/tool-annotations.js";
5
6
  import { configureAdvSecTools } from "./tools/advanced-security.js";
6
7
  import { configureMcpAppsTools } from "./tools/mcp-apps.js";
7
8
  import { configurePipelineTools } from "./tools/pipelines.js";
@@ -13,23 +14,25 @@ import { configureWikiTools } from "./tools/wiki.js";
13
14
  import { configureWorkTools } from "./tools/work.js";
14
15
  import { configureWorkItemTools } from "./tools/work-items.js";
15
16
  function configureAllTools(server, tokenProvider, connectionProvider, userAgentProvider, enabledDomains) {
16
- const configureIfDomainEnabled = (domain, configureFn) => {
17
- if (enabledDomains.has(domain)) {
18
- configureToolsWithContentSafety(server, domain, configureFn);
19
- }
20
- };
21
- configureIfDomainEnabled(Domain.CORE, () => configureCoreTools(server, tokenProvider, connectionProvider, userAgentProvider));
22
- // This is a local health-check response and contains no Azure DevOps content.
23
- if (enabledDomains.has(Domain.MCP_APPS))
24
- configureMcpAppsTools(server);
25
- configureIfDomainEnabled(Domain.WORK, () => configureWorkTools(server, tokenProvider, connectionProvider));
26
- configureIfDomainEnabled(Domain.PIPELINES, () => configurePipelineTools(server, tokenProvider, connectionProvider, userAgentProvider));
27
- configureIfDomainEnabled(Domain.REPOSITORIES, () => configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider));
28
- configureIfDomainEnabled(Domain.WORK_ITEMS, () => configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider));
29
- configureIfDomainEnabled(Domain.WIKI, () => configureWikiTools(server, tokenProvider, connectionProvider, userAgentProvider));
30
- configureIfDomainEnabled(Domain.TEST_PLANS, () => configureTestPlanTools(server, tokenProvider, connectionProvider, userAgentProvider));
31
- configureIfDomainEnabled(Domain.SEARCH, () => configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider));
32
- configureIfDomainEnabled(Domain.ADVANCED_SECURITY, () => configureAdvSecTools(server, tokenProvider, connectionProvider));
17
+ configureToolsWithAnnotations(server, () => {
18
+ const configureIfDomainEnabled = (domain, configureFn) => {
19
+ if (enabledDomains.has(domain)) {
20
+ configureToolsWithContentSafety(server, domain, configureFn);
21
+ }
22
+ };
23
+ configureIfDomainEnabled(Domain.CORE, () => configureCoreTools(server, tokenProvider, connectionProvider, userAgentProvider));
24
+ // This is a local health-check response and contains no Azure DevOps content.
25
+ if (enabledDomains.has(Domain.MCP_APPS))
26
+ configureMcpAppsTools(server);
27
+ configureIfDomainEnabled(Domain.WORK, () => configureWorkTools(server, tokenProvider, connectionProvider));
28
+ configureIfDomainEnabled(Domain.PIPELINES, () => configurePipelineTools(server, tokenProvider, connectionProvider, userAgentProvider));
29
+ configureIfDomainEnabled(Domain.REPOSITORIES, () => configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider));
30
+ configureIfDomainEnabled(Domain.WORK_ITEMS, () => configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider));
31
+ configureIfDomainEnabled(Domain.WIKI, () => configureWikiTools(server, tokenProvider, connectionProvider, userAgentProvider));
32
+ configureIfDomainEnabled(Domain.TEST_PLANS, () => configureTestPlanTools(server, tokenProvider, connectionProvider, userAgentProvider));
33
+ configureIfDomainEnabled(Domain.SEARCH, () => configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider));
34
+ configureIfDomainEnabled(Domain.ADVANCED_SECURITY, () => configureAdvSecTools(server, tokenProvider, connectionProvider));
35
+ });
33
36
  }
34
37
  /**
35
38
  * Centralizes the untrusted-content boundary for tool responses. Tool registration
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "2.10.0";
1
+ export const packageVersion = "2.11.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "mcpName": "microsoft.com/azure-devops",
5
5
  "description": "MCP server for interacting with Azure DevOps",
6
6
  "license": "MIT",
@@ -40,8 +40,7 @@
40
40
  "@azure/identity": "^4.13.0",
41
41
  "@azure/logger": "^1.3.0",
42
42
  "@azure/msal-node": "^5.5.0",
43
- "@azure/msal-node-extensions": "^5.3.5",
44
- "@modelcontextprotocol/sdk": "1.29.0",
43
+ "@modelcontextprotocol/sdk": "1.31.0",
45
44
  "azure-devops-extension-api": "^5.272.3",
46
45
  "azure-devops-extension-sdk": "^4.0.2",
47
46
  "azure-devops-node-api": "^15.1.2",
@@ -51,6 +50,9 @@
51
50
  "zod": "^3.25.63",
52
51
  "zod-to-json-schema": "^3.24.5"
53
52
  },
53
+ "optionalDependencies": {
54
+ "@azure/msal-node-extensions": "^5.3.5"
55
+ },
54
56
  "devDependencies": {
55
57
  "@types/jest": "^30.0.0",
56
58
  "@types/node": "^22.19.1",