@azure-devops/mcp 2.9.0 → 2.10.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.
@@ -8,111 +8,102 @@ import { ConfigurationType, RepositoryType } from "azure-devops-node-api/interfa
8
8
  import { mkdirSync, createWriteStream } from "fs";
9
9
  import { createExternalContentResponse } from "../shared/content-safety.js";
10
10
  import { join, posix, resolve, win32 } from "path";
11
- import { dispatchAction, errorResult } from "../shared/command.js";
12
11
  import { pipelinesWriteShape } from "./pipelines.dto.js";
13
- // ─── pipelines_write commands ────────────────────────────────────────────────
14
- // Each write action is a self-contained command. Shared infrastructure arrives
15
- // via `CommandContext`; the action-specific input arrives via a single typed
16
- // args object (see pipelines.dto.ts). This keeps the dispatcher agnostic of
17
- // individual argument lists.
18
- const runPipelineCommand = {
19
- async execute(context, args) {
20
- if (!args.pipelineId)
21
- return errorResult("pipelineId is required for run_pipeline");
22
- if (!args.previewRun && args.yamlOverride)
23
- throw new Error("Parameter 'yamlOverride' can only be specified together with parameter 'previewRun'.");
24
- const connection = await context.connectionProvider();
25
- const pipelinesApi = await connection.getPipelinesApi();
26
- const runRequest = {
27
- previewRun: args.previewRun,
28
- resources: { ...args.resources },
29
- stagesToSkip: args.stagesToSkip,
30
- templateParameters: args.templateParameters,
31
- variables: args.variables,
32
- yamlOverride: args.yamlOverride,
33
- };
34
- const pipelineRun = await pipelinesApi.runPipeline(runRequest, args.project, args.pipelineId, args.pipelineVersion);
35
- if (pipelineRun.id === undefined)
36
- throw new Error("Failed to get build ID from pipeline run");
37
- return { content: [{ type: "text", text: JSON.stringify(pipelineRun, null, 2) }] };
38
- },
39
- };
40
- const createPipelineCommand = {
41
- async execute(context, args) {
42
- if (!args.name)
43
- return errorResult("name is required for create_pipeline");
44
- if (!args.yamlPath)
45
- return errorResult("yamlPath is required for create_pipeline");
46
- if (!args.repositoryType)
47
- return errorResult("repositoryType is required for create_pipeline");
48
- if (!args.repositoryName)
49
- return errorResult("repositoryName is required for create_pipeline");
50
- const connection = await context.connectionProvider();
51
- const pipelinesApi = await connection.getPipelinesApi();
52
- const repositoryTypeEnumValue = safeEnumConvert(RepositoryType, args.repositoryType);
53
- const repositoryPayload = { type: args.repositoryType };
54
- if (repositoryTypeEnumValue === RepositoryType.AzureReposGit) {
55
- repositoryPayload.id = args.repositoryId;
56
- repositoryPayload.name = args.repositoryName;
57
- }
58
- else if (repositoryTypeEnumValue === RepositoryType.GitHub) {
59
- if (!args.repositoryConnectionId)
60
- throw new Error("Parameter 'repositoryConnectionId' is required for GitHub repositories.");
61
- repositoryPayload.connection = { id: args.repositoryConnectionId };
62
- repositoryPayload.fullname = args.repositoryName;
63
- }
64
- else {
65
- throw new Error("Unsupported repository type");
66
- }
67
- const yamlConfigurationType = getEnumKeys(ConfigurationType).find((k) => ConfigurationType[k] === ConfigurationType.Yaml);
68
- const createParams = {
69
- name: args.name,
70
- folder: args.folder || "\\",
71
- configuration: { type: yamlConfigurationType, path: args.yamlPath, repository: repositoryPayload, variables: undefined },
72
- };
73
- const newPipeline = await pipelinesApi.createPipeline(createParams, args.project);
74
- return { content: [{ type: "text", text: JSON.stringify(newPipeline, null, 2) }] };
75
- },
76
- };
77
- const updateBuildStageCommand = {
78
- async execute(context, args) {
79
- if (!args.buildId)
80
- return errorResult("buildId is required for update_build_stage");
81
- if (!args.stageName)
82
- return errorResult("stageName is required for update_build_stage");
83
- if (!args.status)
84
- return errorResult("status is required for update_build_stage");
85
- const connection = await context.connectionProvider();
86
- const orgUrl = connection.serverUrl;
87
- const endpoint = `${orgUrl}/${encodeURIComponent(args.project)}/_apis/build/builds/${args.buildId}/stages/${encodeURIComponent(args.stageName)}?api-version=${apiVersion}`;
88
- const token = await context.tokenProvider();
89
- const body = { forceRetryAllJobs: args.forceRetryAllJobs, state: safeEnumConvert(StageUpdateType, args.status) };
90
- const response = await fetch(endpoint, {
91
- method: "PATCH",
92
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, "User-Agent": context.userAgentProvider() },
93
- body: JSON.stringify(body),
94
- });
95
- if (!response.ok) {
96
- const errorText = await response.text();
97
- throw new Error(`Failed to update build stage: ${response.status} ${errorText}`);
98
- }
99
- const updatedBuild = await response.text();
100
- return { content: [{ type: "text", text: JSON.stringify(updatedBuild, null, 2) }] };
101
- },
102
- };
103
- /**
104
- * The registry is the lookup table that couples each action to its command.
105
- * Adding a new write action means registering one entry here — the dispatcher
106
- * (`dispatchAction`) never changes.
107
- */
108
- const pipelinesWriteCommands = {
109
- run_pipeline: runPipelineCommand,
110
- create_pipeline: createPipelineCommand,
111
- update_build_stage: updateBuildStageCommand,
112
- };
12
+ const errorResult = (text) => ({ content: [{ type: "text", text }], isError: true });
13
+ async function runPipeline(args, connectionProvider) {
14
+ if (!args.pipelineId)
15
+ return errorResult("pipelineId is required for run_pipeline");
16
+ if (!args.previewRun && args.yamlOverride)
17
+ throw new Error("Parameter 'yamlOverride' can only be specified together with parameter 'previewRun'.");
18
+ const connection = await connectionProvider();
19
+ const pipelinesApi = await connection.getPipelinesApi();
20
+ const runRequest = {
21
+ previewRun: args.previewRun,
22
+ resources: { ...args.resources },
23
+ stagesToSkip: args.stagesToSkip,
24
+ templateParameters: args.templateParameters,
25
+ variables: args.variables,
26
+ yamlOverride: args.yamlOverride,
27
+ };
28
+ const pipelineRun = await pipelinesApi.runPipeline(runRequest, args.project, args.pipelineId, args.pipelineVersion);
29
+ if (pipelineRun.id === undefined)
30
+ throw new Error("Failed to get build ID from pipeline run");
31
+ return { content: [{ type: "text", text: JSON.stringify(pipelineRun, null, 2) }] };
32
+ }
33
+ async function createPipeline(args, connectionProvider) {
34
+ if (!args.name)
35
+ return errorResult("name is required for create_pipeline");
36
+ if (!args.yamlPath)
37
+ return errorResult("yamlPath is required for create_pipeline");
38
+ if (!args.repositoryType)
39
+ return errorResult("repositoryType is required for create_pipeline");
40
+ if (!args.repositoryName)
41
+ return errorResult("repositoryName is required for create_pipeline");
42
+ const connection = await connectionProvider();
43
+ const pipelinesApi = await connection.getPipelinesApi();
44
+ const repositoryTypeEnumValue = safeEnumConvert(RepositoryType, args.repositoryType);
45
+ const repositoryPayload = { type: args.repositoryType };
46
+ if (repositoryTypeEnumValue === RepositoryType.AzureReposGit) {
47
+ repositoryPayload.id = args.repositoryId;
48
+ repositoryPayload.name = args.repositoryName;
49
+ }
50
+ else if (repositoryTypeEnumValue === RepositoryType.GitHub) {
51
+ if (!args.repositoryConnectionId)
52
+ throw new Error("Parameter 'repositoryConnectionId' is required for GitHub repositories.");
53
+ repositoryPayload.connection = { id: args.repositoryConnectionId };
54
+ repositoryPayload.fullname = args.repositoryName;
55
+ }
56
+ else {
57
+ throw new Error("Unsupported repository type");
58
+ }
59
+ const yamlConfigurationType = getEnumKeys(ConfigurationType).find((k) => ConfigurationType[k] === ConfigurationType.Yaml);
60
+ const createParams = {
61
+ name: args.name,
62
+ folder: args.folder || "\\",
63
+ configuration: { type: yamlConfigurationType, path: args.yamlPath, repository: repositoryPayload, variables: undefined },
64
+ };
65
+ const newPipeline = await pipelinesApi.createPipeline(createParams, args.project);
66
+ return { content: [{ type: "text", text: JSON.stringify(newPipeline, null, 2) }] };
67
+ }
68
+ async function renamePipeline(args, connectionProvider) {
69
+ if (!args.pipelineId)
70
+ return errorResult("pipelineId is required for rename_pipeline");
71
+ if (!args.name)
72
+ return errorResult("name is required for rename_pipeline");
73
+ const connection = await connectionProvider();
74
+ const buildApi = await connection.getBuildApi();
75
+ const definition = await buildApi.getDefinition(args.project, args.pipelineId);
76
+ const updatedDefinition = await buildApi.updateDefinition({ ...definition, name: args.name }, args.project, args.pipelineId);
77
+ return { content: [{ type: "text", text: JSON.stringify(updatedDefinition, null, 2) }] };
78
+ }
79
+ async function updateBuildStage(args, connectionProvider, tokenProvider, userAgentProvider) {
80
+ if (!args.buildId)
81
+ return errorResult("buildId is required for update_build_stage");
82
+ if (!args.stageName)
83
+ return errorResult("stageName is required for update_build_stage");
84
+ if (!args.status)
85
+ return errorResult("status is required for update_build_stage");
86
+ const connection = await connectionProvider();
87
+ const orgUrl = connection.serverUrl;
88
+ const endpoint = `${orgUrl}/${encodeURIComponent(args.project)}/_apis/build/builds/${args.buildId}/stages/${encodeURIComponent(args.stageName)}?api-version=${apiVersion}`;
89
+ const token = await tokenProvider();
90
+ const body = { forceRetryAllJobs: args.forceRetryAllJobs, state: safeEnumConvert(StageUpdateType, args.status) };
91
+ const response = await fetch(endpoint, {
92
+ method: "PATCH",
93
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, "User-Agent": userAgentProvider() },
94
+ body: JSON.stringify(body),
95
+ });
96
+ if (!response.ok) {
97
+ const errorText = await response.text();
98
+ throw new Error(`Failed to update build stage: ${response.status} ${errorText}`);
99
+ }
100
+ const updatedBuild = await response.text();
101
+ return { content: [{ type: "text", text: JSON.stringify(updatedBuild, null, 2) }] };
102
+ }
113
103
  const pipelinesWriteErrorPrefixes = {
114
104
  run_pipeline: "Error running pipeline: ",
115
105
  create_pipeline: "Error creating pipeline: ",
106
+ rename_pipeline: "Error renaming pipeline: ",
116
107
  update_build_stage: "Error updating build stage: ",
117
108
  };
118
109
  const PIPELINE_TOOLS = {
@@ -385,8 +376,26 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
385
376
  });
386
377
  // ─── pipelines_write ────────────────────────────────────────────────────────
387
378
  server.tool(PIPELINE_TOOLS.pipelines_write, "Write operations for pipelines and builds. Use the action parameter to specify the operation.", pipelinesWriteShape, async (args) => {
388
- const context = { connectionProvider, tokenProvider, userAgentProvider };
389
- return dispatchAction(pipelinesWriteCommands, context, args, pipelinesWriteErrorPrefixes);
379
+ try {
380
+ switch (args.action) {
381
+ case "run_pipeline":
382
+ return await runPipeline(args, connectionProvider);
383
+ case "create_pipeline":
384
+ return await createPipeline(args, connectionProvider);
385
+ case "rename_pipeline":
386
+ return await renamePipeline(args, connectionProvider);
387
+ case "update_build_stage":
388
+ return await updateBuildStage(args, connectionProvider, tokenProvider, userAgentProvider);
389
+ default: {
390
+ const unsupportedAction = args.action;
391
+ return errorResult(`Unknown action: ${unsupportedAction}. Supported actions: ${Object.keys(pipelinesWriteErrorPrefixes).sort().join(", ")}`);
392
+ }
393
+ }
394
+ }
395
+ catch (error) {
396
+ const message = error instanceof Error ? error.message : "Unknown error occurred";
397
+ return errorResult(`${pipelinesWriteErrorPrefixes[args.action]}${message}`);
398
+ }
390
399
  });
391
400
  }
392
- export { PIPELINE_TOOLS, configurePipelineTools, runPipelineCommand, createPipelineCommand, updateBuildStageCommand };
401
+ export { PIPELINE_TOOLS, configurePipelineTools, runPipeline, createPipeline, renamePipeline, updateBuildStage };
@@ -5,6 +5,7 @@ import { z } from "zod";
5
5
  import { getCurrentUserDetails, getUserIdFromEmail } from "./auth.js";
6
6
  import { extractAdoStreamError, getEnumKeys, streamToString, apiVersion } from "../utils.js";
7
7
  import { orgName } from "../index.js";
8
+ import { createExternalContentResponse } from "../shared/content-safety.js";
8
9
  const REPO_TOOLS = {
9
10
  repo_repository: "repo_repository",
10
11
  repo_pull_request: "repo_pull_request",
@@ -32,6 +33,7 @@ function trimPullRequestThread(thread) {
32
33
  status: thread.status,
33
34
  comments: trimComments(thread.comments),
34
35
  threadContext: thread.threadContext,
36
+ pullRequestThreadContext: thread.pullRequestThreadContext,
35
37
  };
36
38
  }
37
39
  function trimComments(comments) {
@@ -222,6 +224,8 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
222
224
  changedFilesSummary: {
223
225
  changeEntries: changes?.changeEntries ?? [],
224
226
  fileCount: changes?.changeEntries?.length ?? 0,
227
+ firstComparingIteration: Math.max(0, latestIteration.id - 1),
228
+ secondComparingIteration: latestIteration.id,
225
229
  nextSkip: changes?.nextSkip,
226
230
  nextTop: changes?.nextTop,
227
231
  },
@@ -240,7 +244,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
240
244
  enhancedResponse = { ...enhancedResponse, changedFilesSummary: {} };
241
245
  }
242
246
  }
243
- return { content: [{ type: "text", text: JSON.stringify(enhancedResponse, null, 2) }] };
247
+ return createExternalContentResponse(enhancedResponse, "pull request");
244
248
  }
245
249
  if (action === "list") {
246
250
  if (!repositoryId && !project) {
@@ -454,7 +458,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
454
458
  if (streamError) {
455
459
  return { content: [{ type: "text", text: `Error getting file content for '${path}': ${streamError}` }], isError: true };
456
460
  }
457
- return { content: [{ type: "text", text: content }] };
461
+ return createExternalContentResponse(content, "repository file");
458
462
  }
459
463
  if (action === "list_directory") {
460
464
  const versionDescriptor = buildVersionDescriptor(version, versionType === "Commit" ? "Branch" : versionType);
@@ -575,11 +579,12 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
575
579
  mergeCommitMessage: z.string().optional().describe("Commit message for autocomplete. Used for update."),
576
580
  deleteSourceBranch: z.boolean().optional().default(false).describe("Delete source branch on autocomplete. Used for update."),
577
581
  transitionWorkItems: z.boolean().optional().default(true).describe("Transition work items on autocomplete. Used for update."),
578
- bypassReason: z.string().optional().describe("Reason for bypassing branch policies. Used for update."),
582
+ bypassPolicy: z.boolean().optional().default(false).describe("Explicitly bypass branch policies on autocomplete. Used for update and requires bypassReason when true."),
583
+ bypassReason: z.string().optional().describe("Reason for bypassing branch policies. Used for update only when bypassPolicy is true."),
579
584
  reviewerIds: z.array(z.string()).optional().describe("List of reviewer IDs. Required for update_reviewers."),
580
585
  reviewerAction: z.enum(["add", "remove"]).optional().describe("Whether to add or remove reviewers. Required for update_reviewers."),
581
586
  vote: z.enum(["Approved", "ApprovedWithSuggestions", "NoVote", "WaitingForAuthor", "Rejected"]).optional().describe("The vote to cast. Required for vote."),
582
- }, async ({ action, repositoryId, pullRequestId, project, sourceRefName, targetRefName, title, description, isDraft, workItems, forkSourceRepositoryId, labels, status, autoComplete, mergeStrategy, mergeCommitMessage, deleteSourceBranch, transitionWorkItems, bypassReason, reviewerIds, reviewerAction, vote, }) => {
587
+ }, async ({ action, repositoryId, pullRequestId, project, sourceRefName, targetRefName, title, description, isDraft, workItems, forkSourceRepositoryId, labels, status, autoComplete, mergeStrategy, mergeCommitMessage, deleteSourceBranch, transitionWorkItems, bypassPolicy, bypassReason, reviewerIds, reviewerAction, vote, }) => {
583
588
  try {
584
589
  const connection = await connectionProvider();
585
590
  const gitApi = await connection.getGitApi();
@@ -629,18 +634,21 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
629
634
  }
630
635
  if (autoComplete !== undefined) {
631
636
  if (autoComplete) {
637
+ if (bypassPolicy && !bypassReason) {
638
+ return { content: [{ type: "text", text: "bypassReason is required when bypassPolicy is true" }], isError: true };
639
+ }
632
640
  const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
633
641
  updateRequest.autoCompleteSetBy = { id: data.authenticatedUser.id };
634
642
  const completionOptions = {
635
643
  deleteSourceBranch: deleteSourceBranch || false,
636
644
  transitionWorkItems: transitionWorkItems !== false,
637
- bypassPolicy: !!bypassReason,
645
+ bypassPolicy: bypassPolicy === true,
638
646
  };
639
647
  if (mergeStrategy)
640
648
  completionOptions.mergeStrategy = GitPullRequestMergeStrategy[mergeStrategy];
641
649
  if (mergeCommitMessage)
642
650
  completionOptions.mergeCommitMessage = mergeCommitMessage;
643
- if (bypassReason)
651
+ if (bypassPolicy && bypassReason)
644
652
  completionOptions.bypassReason = bypassReason;
645
653
  updateRequest.completionOptions = completionOptions;
646
654
  }
@@ -747,25 +755,29 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
747
755
  // --- repo_pull_request_thread_write ----------------------------------------
748
756
  server.tool(REPO_TOOLS.repo_pull_request_thread_write, "Write operations for pull request comment threads. Use the action parameter to specify the operation.", {
749
757
  action: z
750
- .enum(["create", "reply", "update_status"])
751
- .describe("The action to perform. Options: create (create a new comment thread on a pull request), reply (reply to a comment in a thread), update_status (update the status of a comment thread)."),
758
+ .enum(["create", "reply", "update", "update_status"])
759
+ .describe("The action to perform. Options: create (create a new comment thread on a pull request), reply (reply to a comment in a thread), update (update an existing comment), update_status (update the status of a comment thread)."),
752
760
  repositoryId: z.string().describe("The ID or name of the repository. When using a name instead of a GUID, project must also be provided."),
753
761
  pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request."),
754
762
  project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a name instead of a GUID."),
755
- threadId: z.coerce.number().min(1).optional().describe("The ID of the thread. Required for reply and update_status."),
756
- content: z.string().optional().describe("The content of the comment. Required for create and reply."),
763
+ threadId: z.coerce.number().min(1).optional().describe("The ID of the thread. Required for reply, update, and update_status."),
764
+ commentId: z.coerce.number().min(1).optional().describe("The ID of the comment to update. Required for update."),
765
+ content: z.string().optional().describe("The content of the comment. Required for create, reply, and update."),
757
766
  status: z
758
767
  .enum(getEnumKeys(CommentThreadStatus))
759
768
  .optional()
760
769
  .default(CommentThreadStatus[CommentThreadStatus.Active])
761
770
  .describe("The thread status. Used for create (defaults to 'Active') and required for update_status."),
762
771
  filePath: z.string().optional().describe("The file path for the comment thread. Used for create."),
763
- fullResponse: z.boolean().optional().default(false).describe("Return full JSON response. Used for reply."),
772
+ fullResponse: z.boolean().optional().default(false).describe("Return full JSON response. Used for reply and update."),
764
773
  rightFileStartLine: z.coerce.number().min(1).optional().describe("Start line in the right file. Used for create."),
765
774
  rightFileStartOffset: z.number().optional().describe("Start character offset in the right file. Used for create."),
766
775
  rightFileEndLine: z.number().optional().describe("End line in the right file. Used for create."),
767
776
  rightFileEndOffset: z.number().optional().describe("End character offset in the right file. Used for create."),
768
- }, async ({ action, repositoryId, pullRequestId, project, threadId, content, status, filePath, fullResponse, rightFileStartLine, rightFileStartOffset, rightFileEndLine, rightFileEndOffset }) => {
777
+ changeTrackingId: z.coerce.number().int().min(1).optional().describe("The file change tracking ID from the pull request iteration changes. Used for create."),
778
+ firstComparingIteration: z.coerce.number().int().min(0).optional().describe("The iteration on the left side of the diff. Used for create."),
779
+ secondComparingIteration: z.coerce.number().int().min(1).optional().describe("The iteration on the right side of the diff. Used for create."),
780
+ }, async ({ action, repositoryId, pullRequestId, project, threadId, commentId, content, status, filePath, fullResponse, rightFileStartLine, rightFileStartOffset, rightFileEndLine, rightFileEndOffset, changeTrackingId, firstComparingIteration, secondComparingIteration, }) => {
769
781
  try {
770
782
  const connection = await connectionProvider();
771
783
  const gitApi = await connection.getGitApi();
@@ -815,7 +827,17 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
815
827
  return { content: [{ type: "text", text: "rightFileEndOffset must be greater than or equal to rightFileStartOffset when both are on the same line." }], isError: true };
816
828
  }
817
829
  }
818
- const thread = await gitApi.createThread({ comments: [{ content, commentType: 1 }], threadContext, status: CommentThreadStatus[status] }, repositoryId, pullRequestId, project);
830
+ const iterationContextValues = [changeTrackingId, firstComparingIteration, secondComparingIteration];
831
+ if (iterationContextValues.some((value) => value !== undefined) && iterationContextValues.some((value) => value === undefined)) {
832
+ return {
833
+ content: [{ type: "text", text: "changeTrackingId, firstComparingIteration, and secondComparingIteration must all be specified together." }],
834
+ isError: true,
835
+ };
836
+ }
837
+ const pullRequestThreadContext = changeTrackingId !== undefined && firstComparingIteration !== undefined && secondComparingIteration !== undefined
838
+ ? { changeTrackingId, iterationContext: { firstComparingIteration, secondComparingIteration } }
839
+ : undefined;
840
+ const thread = await gitApi.createThread({ comments: [{ content, commentType: 1 }], threadContext, pullRequestThreadContext, status: CommentThreadStatus[status] }, repositoryId, pullRequestId, project);
819
841
  return { content: [{ type: "text", text: JSON.stringify(trimPullRequestThread(thread), null, 2) }] };
820
842
  }
821
843
  if (action === "reply") {
@@ -831,6 +853,21 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
831
853
  return { content: [{ type: "text", text: JSON.stringify(comment, null, 2) }] };
832
854
  return { content: [{ type: "text", text: `Comment successfully added to thread ${threadId}.` }] };
833
855
  }
856
+ if (action === "update") {
857
+ if (!threadId)
858
+ return { content: [{ type: "text", text: "threadId is required for update" }], isError: true };
859
+ if (!commentId)
860
+ return { content: [{ type: "text", text: "commentId is required for update" }], isError: true };
861
+ if (!content)
862
+ return { content: [{ type: "text", text: "content is required for update" }], isError: true };
863
+ const comment = await gitApi.updateComment({ content }, repositoryId, pullRequestId, threadId, commentId, project);
864
+ if (!comment) {
865
+ return { content: [{ type: "text", text: `Error: Failed to update comment ${commentId} in thread ${threadId}. The comment was not updated successfully.` }], isError: true };
866
+ }
867
+ if (fullResponse)
868
+ return { content: [{ type: "text", text: JSON.stringify(comment, null, 2) }] };
869
+ return { content: [{ type: "text", text: `Comment ${commentId} successfully updated in thread ${threadId}.` }] };
870
+ }
834
871
  if (action === "update_status") {
835
872
  if (!threadId)
836
873
  return { content: [{ type: "text", text: "threadId is required for update_status" }], isError: true };
@@ -1,9 +1,10 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
+ import { VersionControlRecursionType } from "azure-devops-node-api/interfaces/GitInterfaces.js";
3
4
  import { z } from "zod";
4
5
  import { apiVersion } from "../utils.js";
5
6
  import { orgName } from "../index.js";
6
- import { VersionControlRecursionType } from "azure-devops-node-api/interfaces/GitInterfaces.js";
7
+ import { createExternalContentResponse } from "../shared/content-safety.js";
7
8
  const SEARCH_TOOLS = {
8
9
  search_code: "search_code",
9
10
  search_wiki: "search_wiki",
@@ -60,9 +61,7 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
60
61
  const resultJson = JSON.parse(resultText);
61
62
  const gitApi = await connection.getGitApi();
62
63
  const combinedResults = await fetchCombinedResults(resultJson.results ?? [], gitApi);
63
- return {
64
- content: [{ type: "text", text: resultText + JSON.stringify(combinedResults) }],
65
- };
64
+ return createExternalContentResponse(resultText + JSON.stringify(combinedResults), "code search results");
66
65
  });
67
66
  server.tool(SEARCH_TOOLS.search_wiki, "Search Azure DevOps Wiki for a given search text", {
68
67
  searchText: z.string().describe("Keywords to search for wiki pages"),
@@ -101,9 +100,7 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
101
100
  throw new Error(`Azure DevOps Wiki Search API error: ${response.status} ${response.statusText}`);
102
101
  }
103
102
  const result = await response.text();
104
- return {
105
- content: [{ type: "text", text: result }],
106
- };
103
+ return createExternalContentResponse(result, "wiki search results");
107
104
  });
108
105
  server.tool(SEARCH_TOOLS.search_workitem, "Get Azure DevOps Work Item search results for a given search text", {
109
106
  searchText: z.string().describe("Search text to find in work items"),
@@ -151,9 +148,7 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
151
148
  throw new Error(`Azure DevOps Work Item Search API error: ${response.status} ${response.statusText}`);
152
149
  }
153
150
  const result = await response.text();
154
- return {
155
- content: [{ type: "text", text: result }],
156
- };
151
+ return createExternalContentResponse(result, "work item search results");
157
152
  });
158
153
  }
159
154
  async function fetchCombinedResults(topSearchResults, gitApi) {
@@ -170,7 +165,7 @@ async function fetchCombinedResults(topSearchResults, gitApi) {
170
165
  });
171
166
  continue;
172
167
  }
173
- const versionDescriptor = changeId ? { version: changeId, versionType: 2, versionOptions: 0 } : undefined;
168
+ const versionDescriptor = { version: changeId, versionType: 2, versionOptions: 0 };
174
169
  const item = await gitApi.getItem(repositoryId, filePath, projectId, undefined, VersionControlRecursionType.None, true, // includeContentMetadata
175
170
  false, // latestProcessedChange
176
171
  false, // download