@azure-devops/mcp 2.9.0-nightly.20260901 → 2.9.0-nightly.20260902

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,6 +1,7 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
3
  import { randomBytes } from "crypto";
4
+ const spotlightedResponse = Symbol("spotlightedResponse");
4
5
  /**
5
6
  * Applies Spotlighting (delimiting mode) to untrusted external content.
6
7
  * See: https://arxiv.org/pdf/2403.14720
@@ -20,5 +21,31 @@ export function spotlightContent(content, source) {
20
21
  export function createExternalContentResponse(content, source) {
21
22
  const serialized = typeof content === "string" ? content : JSON.stringify(content, null, 2);
22
23
  const spotlighted = spotlightContent(serialized, source);
23
- return { content: [{ type: "text", text: spotlighted }] };
24
+ const response = { content: [{ type: "text", text: spotlighted }] };
25
+ response[spotlightedResponse] = true;
26
+ return response;
27
+ }
28
+ /**
29
+ * Applies Spotlighting to every textual content block in a tool response while
30
+ * preserving MCP metadata such as `isError` and `_meta`.
31
+ *
32
+ * The private symbol prevents responses created by createExternalContentResponse
33
+ * from being wrapped twice without trusting attacker-controlled marker text.
34
+ */
35
+ export function wrapExternalToolResponse(response, source) {
36
+ const markedResponse = response;
37
+ if (markedResponse[spotlightedResponse])
38
+ return response;
39
+ const content = response.content.map((block) => {
40
+ if (block.type === "text") {
41
+ return { ...block, text: spotlightContent(block.text, source) };
42
+ }
43
+ if (block.type === "resource" && "text" in block.resource) {
44
+ return { ...block, resource: { ...block.resource, text: spotlightContent(block.resource.text, source) } };
45
+ }
46
+ return block;
47
+ });
48
+ const wrapped = { ...response, content };
49
+ wrapped[spotlightedResponse] = true;
50
+ return wrapped;
24
51
  }
@@ -579,11 +579,12 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
579
579
  mergeCommitMessage: z.string().optional().describe("Commit message for autocomplete. Used for update."),
580
580
  deleteSourceBranch: z.boolean().optional().default(false).describe("Delete source branch on autocomplete. Used for update."),
581
581
  transitionWorkItems: z.boolean().optional().default(true).describe("Transition work items on autocomplete. Used for update."),
582
- 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."),
583
584
  reviewerIds: z.array(z.string()).optional().describe("List of reviewer IDs. Required for update_reviewers."),
584
585
  reviewerAction: z.enum(["add", "remove"]).optional().describe("Whether to add or remove reviewers. Required for update_reviewers."),
585
586
  vote: z.enum(["Approved", "ApprovedWithSuggestions", "NoVote", "WaitingForAuthor", "Rejected"]).optional().describe("The vote to cast. Required for vote."),
586
- }, 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, }) => {
587
588
  try {
588
589
  const connection = await connectionProvider();
589
590
  const gitApi = await connection.getGitApi();
@@ -633,18 +634,21 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
633
634
  }
634
635
  if (autoComplete !== undefined) {
635
636
  if (autoComplete) {
637
+ if (bypassPolicy && !bypassReason) {
638
+ return { content: [{ type: "text", text: "bypassReason is required when bypassPolicy is true" }], isError: true };
639
+ }
636
640
  const data = await getCurrentUserDetails(tokenProvider, connectionProvider, userAgentProvider);
637
641
  updateRequest.autoCompleteSetBy = { id: data.authenticatedUser.id };
638
642
  const completionOptions = {
639
643
  deleteSourceBranch: deleteSourceBranch || false,
640
644
  transitionWorkItems: transitionWorkItems !== false,
641
- bypassPolicy: !!bypassReason,
645
+ bypassPolicy: bypassPolicy === true,
642
646
  };
643
647
  if (mergeStrategy)
644
648
  completionOptions.mergeStrategy = GitPullRequestMergeStrategy[mergeStrategy];
645
649
  if (mergeCommitMessage)
646
650
  completionOptions.mergeCommitMessage = mergeCommitMessage;
647
- if (bypassReason)
651
+ if (bypassPolicy && bypassReason)
648
652
  completionOptions.bypassReason = bypassReason;
649
653
  updateRequest.completionOptions = completionOptions;
650
654
  }
package/dist/tools.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
+ import { wrapExternalToolResponse } from "./shared/content-safety.js";
3
4
  import { Domain } from "./shared/domains.js";
4
5
  import { configureAdvSecTools } from "./tools/advanced-security.js";
5
6
  import { configureMcpAppsTools } from "./tools/mcp-apps.js";
@@ -14,11 +15,13 @@ import { configureWorkItemTools } from "./tools/work-items.js";
14
15
  function configureAllTools(server, tokenProvider, connectionProvider, userAgentProvider, enabledDomains) {
15
16
  const configureIfDomainEnabled = (domain, configureFn) => {
16
17
  if (enabledDomains.has(domain)) {
17
- configureFn();
18
+ configureToolsWithContentSafety(server, domain, configureFn);
18
19
  }
19
20
  };
20
21
  configureIfDomainEnabled(Domain.CORE, () => configureCoreTools(server, tokenProvider, connectionProvider, userAgentProvider));
21
- configureIfDomainEnabled(Domain.MCP_APPS, () => configureMcpAppsTools(server));
22
+ // This is a local health-check response and contains no Azure DevOps content.
23
+ if (enabledDomains.has(Domain.MCP_APPS))
24
+ configureMcpAppsTools(server);
22
25
  configureIfDomainEnabled(Domain.WORK, () => configureWorkTools(server, tokenProvider, connectionProvider));
23
26
  configureIfDomainEnabled(Domain.PIPELINES, () => configurePipelineTools(server, tokenProvider, connectionProvider, userAgentProvider));
24
27
  configureIfDomainEnabled(Domain.REPOSITORIES, () => configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider));
@@ -28,4 +31,34 @@ function configureAllTools(server, tokenProvider, connectionProvider, userAgentP
28
31
  configureIfDomainEnabled(Domain.SEARCH, () => configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider));
29
32
  configureIfDomainEnabled(Domain.ADVANCED_SECURITY, () => configureAdvSecTools(server, tokenProvider, connectionProvider));
30
33
  }
34
+ /**
35
+ * Centralizes the untrusted-content boundary for tool responses. Tool registration
36
+ * is synchronous, so the original method is restored before this function returns.
37
+ */
38
+ function configureToolsWithContentSafety(server, domain, configureFn) {
39
+ const originalTool = server.tool;
40
+ const originalRegisterTool = server.registerTool;
41
+ const wrapRegistrationMethod = (registrationMethod) => new Proxy(registrationMethod, {
42
+ apply(target, thisArg, argumentsList) {
43
+ const callbackIndex = argumentsList.length - 1;
44
+ const callback = argumentsList[callbackIndex];
45
+ if (typeof callback === "function") {
46
+ argumentsList[callbackIndex] = async (...callbackArgs) => {
47
+ const response = (await Reflect.apply(callback, undefined, callbackArgs));
48
+ return wrapExternalToolResponse(response, `Azure DevOps ${domain}`);
49
+ };
50
+ }
51
+ return Reflect.apply(target, thisArg, argumentsList);
52
+ },
53
+ });
54
+ server.tool = wrapRegistrationMethod(originalTool);
55
+ server.registerTool = wrapRegistrationMethod(originalRegisterTool);
56
+ try {
57
+ configureFn();
58
+ }
59
+ finally {
60
+ server.tool = originalTool;
61
+ server.registerTool = originalRegisterTool;
62
+ }
63
+ }
31
64
  export { configureAllTools };
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "2.9.0-nightly.20260901";
1
+ export const packageVersion = "2.9.0-nightly.20260902";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "2.9.0-nightly.20260901",
3
+ "version": "2.9.0-nightly.20260902",
4
4
  "mcpName": "microsoft.com/azure-devops",
5
5
  "description": "MCP server for interacting with Azure DevOps",
6
6
  "license": "MIT",