@azure-devops/mcp 2.9.0-nightly.20260831 → 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 +30 -10
- package/dist/tools/search.js +1 -1
- 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
|
}
|
|
@@ -751,20 +755,21 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
751
755
|
// --- repo_pull_request_thread_write ----------------------------------------
|
|
752
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.", {
|
|
753
757
|
action: z
|
|
754
|
-
.enum(["create", "reply", "update_status"])
|
|
755
|
-
.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)."),
|
|
756
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."),
|
|
757
761
|
pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request."),
|
|
758
762
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a name instead of a GUID."),
|
|
759
|
-
threadId: z.coerce.number().min(1).optional().describe("The ID of the thread. Required for reply and update_status."),
|
|
760
|
-
|
|
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."),
|
|
761
766
|
status: z
|
|
762
767
|
.enum(getEnumKeys(CommentThreadStatus))
|
|
763
768
|
.optional()
|
|
764
769
|
.default(CommentThreadStatus[CommentThreadStatus.Active])
|
|
765
770
|
.describe("The thread status. Used for create (defaults to 'Active') and required for update_status."),
|
|
766
771
|
filePath: z.string().optional().describe("The file path for the comment thread. Used for create."),
|
|
767
|
-
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."),
|
|
768
773
|
rightFileStartLine: z.coerce.number().min(1).optional().describe("Start line in the right file. Used for create."),
|
|
769
774
|
rightFileStartOffset: z.number().optional().describe("Start character offset in the right file. Used for create."),
|
|
770
775
|
rightFileEndLine: z.number().optional().describe("End line in the right file. Used for create."),
|
|
@@ -772,7 +777,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
772
777
|
changeTrackingId: z.coerce.number().int().min(1).optional().describe("The file change tracking ID from the pull request iteration changes. Used for create."),
|
|
773
778
|
firstComparingIteration: z.coerce.number().int().min(0).optional().describe("The iteration on the left side of the diff. Used for create."),
|
|
774
779
|
secondComparingIteration: z.coerce.number().int().min(1).optional().describe("The iteration on the right side of the diff. Used for create."),
|
|
775
|
-
}, async ({ action, repositoryId, pullRequestId, project, threadId, content, status, filePath, fullResponse, rightFileStartLine, rightFileStartOffset, rightFileEndLine, rightFileEndOffset, changeTrackingId, firstComparingIteration, secondComparingIteration, }) => {
|
|
780
|
+
}, async ({ action, repositoryId, pullRequestId, project, threadId, commentId, content, status, filePath, fullResponse, rightFileStartLine, rightFileStartOffset, rightFileEndLine, rightFileEndOffset, changeTrackingId, firstComparingIteration, secondComparingIteration, }) => {
|
|
776
781
|
try {
|
|
777
782
|
const connection = await connectionProvider();
|
|
778
783
|
const gitApi = await connection.getGitApi();
|
|
@@ -848,6 +853,21 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
848
853
|
return { content: [{ type: "text", text: JSON.stringify(comment, null, 2) }] };
|
|
849
854
|
return { content: [{ type: "text", text: `Comment successfully added to thread ${threadId}.` }] };
|
|
850
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
|
+
}
|
|
851
871
|
if (action === "update_status") {
|
|
852
872
|
if (!threadId)
|
|
853
873
|
return { content: [{ type: "text", text: "threadId is required for update_status" }], isError: true };
|
package/dist/tools/search.js
CHANGED
|
@@ -165,7 +165,7 @@ async function fetchCombinedResults(topSearchResults, gitApi) {
|
|
|
165
165
|
});
|
|
166
166
|
continue;
|
|
167
167
|
}
|
|
168
|
-
const versionDescriptor =
|
|
168
|
+
const versionDescriptor = { version: changeId, versionType: 2, versionOptions: 0 };
|
|
169
169
|
const item = await gitApi.getItem(repositoryId, filePath, projectId, undefined, VersionControlRecursionType.None, true, // includeContentMetadata
|
|
170
170
|
false, // latestProcessedChange
|
|
171
171
|
false, // download
|
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";
|