@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.
- package/dist/shared/content-safety.js +28 -1
- package/dist/tools/repositories.js +8 -4
- package/dist/tools.js +35 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
1
|
+
export const packageVersion = "2.9.0-nightly.20260902";
|