@azure-devops/mcp 2.9.0 → 2.10.0-nightly.20260910
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/README.md +70 -107
- package/dist/auth.js +66 -6
- package/dist/index.js +5 -16
- package/dist/logger.js +0 -0
- package/dist/org-tenants.js +0 -0
- package/dist/prompts.js +0 -0
- package/dist/shared/content-safety.js +28 -1
- package/dist/tools/auth.js +10 -6
- package/dist/tools/pipelines.dto.js +11 -3
- package/dist/tools/pipelines.js +113 -104
- package/dist/tools/repositories.js +50 -13
- package/dist/tools/search.js +6 -11
- package/dist/tools/work-items.js +115 -38
- package/dist/tools.js +35 -2
- package/dist/useragent.js +0 -0
- package/dist/utils.js +13 -0
- package/dist/version.js +1 -1
- package/package.json +11 -4
- package/dist/shared/command.js +0 -34
package/dist/tools/work-items.js
CHANGED
|
@@ -8,6 +8,7 @@ import { z } from "zod";
|
|
|
8
8
|
import { batchApiVersion, markdownCommentsApiVersion, getEnumKeys, safeEnumConvert, encodeFormattedValue } from "../utils.js";
|
|
9
9
|
import { elicitProject, elicitTeam } from "../shared/elicitations.js";
|
|
10
10
|
import { createExternalContentResponse } from "../shared/content-safety.js";
|
|
11
|
+
import { getUserIdentityFromEmail } from "./auth.js";
|
|
11
12
|
const WORKITEM_TOOLS = {
|
|
12
13
|
wit_work_item: "wit_work_item",
|
|
13
14
|
wit_query: "wit_query",
|
|
@@ -43,6 +44,8 @@ function getLinkTypeFromName(name) {
|
|
|
43
44
|
return "Microsoft.VSTS.Common.Affects-Reverse";
|
|
44
45
|
case "artifact":
|
|
45
46
|
return "ArtifactLink";
|
|
47
|
+
case "hyperlink":
|
|
48
|
+
return "Hyperlink";
|
|
46
49
|
default:
|
|
47
50
|
throw new Error(`Unknown link type: ${name}`);
|
|
48
51
|
}
|
|
@@ -55,13 +58,46 @@ function getArtifactLinkAttributeName(linkType) {
|
|
|
55
58
|
return linkType;
|
|
56
59
|
}
|
|
57
60
|
}
|
|
61
|
+
function escapeHtml(value) {
|
|
62
|
+
const entities = {
|
|
63
|
+
"&": "&",
|
|
64
|
+
"<": "<",
|
|
65
|
+
">": ">",
|
|
66
|
+
'"': """,
|
|
67
|
+
"'": "'",
|
|
68
|
+
};
|
|
69
|
+
return value.replace(/[&<>"']/g, (character) => entities[character]);
|
|
70
|
+
}
|
|
71
|
+
async function resolveCommentMentions(text, format, tokenProvider, connectionProvider, userAgentProvider) {
|
|
72
|
+
const emailMatches = [...text.matchAll(/@<([^<>\s]+@[^<>\s]+)>/g)];
|
|
73
|
+
if (emailMatches.length === 0)
|
|
74
|
+
return text;
|
|
75
|
+
const identities = new Map();
|
|
76
|
+
for (const email of new Set(emailMatches.map((match) => match[1]))) {
|
|
77
|
+
try {
|
|
78
|
+
identities.set(email, await getUserIdentityFromEmail(email, tokenProvider, connectionProvider, userAgentProvider));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Leave mentions unchanged when their identities cannot be resolved.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return text.replace(/@<([^<>\s]+@[^<>\s]+)>/g, (mention, email) => {
|
|
85
|
+
const identity = identities.get(email);
|
|
86
|
+
if (!identity)
|
|
87
|
+
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>`;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
58
91
|
function configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider) {
|
|
59
92
|
// --- wit_work_item ----------------------------------------------------------
|
|
60
|
-
server.tool(WORKITEM_TOOLS.wit_work_item, "Retrieve work item data
|
|
93
|
+
server.tool(WORKITEM_TOOLS.wit_work_item, "Retrieve work item data. Use the action parameter to specify the operation.", {
|
|
61
94
|
action: z
|
|
62
95
|
.enum(["get", "get_batch", "list_comments", "my", "list_revisions", "list_for_iteration", "get_type"])
|
|
63
96
|
.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)."),
|
|
64
|
-
project: z
|
|
97
|
+
project: z
|
|
98
|
+
.string()
|
|
99
|
+
.optional()
|
|
100
|
+
.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."),
|
|
65
101
|
id: z.coerce.number().min(1).optional().describe("Work item ID. Required for: get."),
|
|
66
102
|
ids: z.array(z.coerce.number().min(1)).optional().describe("Work item IDs. Required for: get_batch."),
|
|
67
103
|
workItemId: z.coerce.number().min(1).optional().describe("Work item ID. Required for: list_comments, list_revisions."),
|
|
@@ -85,12 +121,6 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
85
121
|
if (action === "get") {
|
|
86
122
|
if (!id)
|
|
87
123
|
return { content: [{ type: "text", text: "id is required for get" }], isError: true };
|
|
88
|
-
if (!resolvedProject) {
|
|
89
|
-
const result = await elicitProject(server, connection, "Select the Azure DevOps project to retrieve the work item from.");
|
|
90
|
-
if ("response" in result)
|
|
91
|
-
return result.response;
|
|
92
|
-
resolvedProject = result.resolved;
|
|
93
|
-
}
|
|
94
124
|
let effectiveExpand = expand;
|
|
95
125
|
if (fields && fields.length > 0 && effectiveExpand != null) {
|
|
96
126
|
effectiveExpand = "none";
|
|
@@ -309,11 +339,24 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
309
339
|
});
|
|
310
340
|
// --- wit_backlog ------------------------------------------------------------
|
|
311
341
|
server.tool(WORKITEM_TOOLS.wit_backlog, "Retrieve backlog data for a project and team. Use the action parameter to specify the operation.", {
|
|
312
|
-
action: z
|
|
342
|
+
action: z
|
|
343
|
+
.enum(["list", "list_work_items", "reorder"])
|
|
344
|
+
.describe("The action to perform. Options: list (list backlog levels for a team), list_work_items (list work items in a specific backlog level), reorder (move work items to a new position in a backlog or iteration)."),
|
|
313
345
|
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."),
|
|
314
346
|
team: z.string().optional().describe("The name or ID of the Azure DevOps team. Reuse from prior context if already known. If not provided, a team selection prompt will be shown."),
|
|
315
347
|
backlogId: z.string().optional().describe("The ID of the backlog category to retrieve work items from. Required for: list_work_items."),
|
|
316
|
-
|
|
348
|
+
ids: z.array(z.coerce.number().int().min(1)).min(1).optional().describe("The IDs of the work items to reorder. Required for: reorder."),
|
|
349
|
+
previousId: z.coerce
|
|
350
|
+
.number()
|
|
351
|
+
.int()
|
|
352
|
+
.min(0)
|
|
353
|
+
.optional()
|
|
354
|
+
.describe("The ID of the work item that should be before the reordered items. Use 0 to specify the beginning of the list. Optional for: reorder."),
|
|
355
|
+
nextId: z.coerce.number().int().min(0).optional().describe("The ID of the work item that should be after the reordered items. Use 0 to specify the end of the list. Optional for: reorder."),
|
|
356
|
+
parentId: z.coerce.number().int().min(0).optional().describe("The parent ID for all work items involved in the operation. Use 0 to indicate the items have no parent. Optional for: reorder."),
|
|
357
|
+
iterationPath: z.string().optional().describe("The iteration path for the reorder operation. Used when reordering items in an iteration backlog. Optional for: reorder."),
|
|
358
|
+
iterationId: z.string().optional().describe("The iteration ID. When provided, reorder items in that iteration instead of the team backlog. Used for: reorder."),
|
|
359
|
+
}, async ({ action, project, team, backlogId, ids, previousId, nextId, parentId, iterationPath, iterationId }) => {
|
|
317
360
|
try {
|
|
318
361
|
const connection = await connectionProvider();
|
|
319
362
|
let resolvedProject = project;
|
|
@@ -344,6 +387,13 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
344
387
|
const workItems = await workApi.getBacklogLevelWorkItems(teamContext, backlogId);
|
|
345
388
|
return { content: [{ type: "text", text: JSON.stringify(workItems, null, 2) }] };
|
|
346
389
|
}
|
|
390
|
+
if (action === "reorder") {
|
|
391
|
+
if (!ids?.length)
|
|
392
|
+
return { content: [{ type: "text", text: "ids is required for reorder" }], isError: true };
|
|
393
|
+
const operation = { ids, previousId, nextId, parentId, iterationPath };
|
|
394
|
+
const reorderedItems = iterationId ? await workApi.reorderIterationWorkItems(operation, teamContext, iterationId) : await workApi.reorderBacklogWorkItems(operation, teamContext);
|
|
395
|
+
return { content: [{ type: "text", text: JSON.stringify(reorderedItems, null, 2) }] };
|
|
396
|
+
}
|
|
347
397
|
return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
|
|
348
398
|
}
|
|
349
399
|
catch (error) {
|
|
@@ -351,6 +401,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
351
401
|
const msgs = {
|
|
352
402
|
list: `Error listing backlogs: ${errorMessage}`,
|
|
353
403
|
list_work_items: `Error listing backlog work items: ${errorMessage}`,
|
|
404
|
+
reorder: `Error reordering backlog work items: ${errorMessage}`,
|
|
354
405
|
};
|
|
355
406
|
return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
|
|
356
407
|
}
|
|
@@ -404,9 +455,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
404
455
|
}
|
|
405
456
|
const mimeType = getMimeType(fileName);
|
|
406
457
|
if (mimeType.startsWith("text/")) {
|
|
407
|
-
return
|
|
408
|
-
content: [{ type: "text", text: buffer.toString("utf-8") }],
|
|
409
|
-
};
|
|
458
|
+
return createExternalContentResponse(buffer.toString("utf-8"), "work item attachment");
|
|
410
459
|
}
|
|
411
460
|
const base64Data = buffer.toString("base64");
|
|
412
461
|
return {
|
|
@@ -451,14 +500,17 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
451
500
|
op: z
|
|
452
501
|
.string()
|
|
453
502
|
.transform((val) => val.toLowerCase())
|
|
454
|
-
.pipe(z.enum(["add", "replace", "remove"]))
|
|
503
|
+
.pipe(z.enum(["add", "replace", "remove", "test"]))
|
|
455
504
|
.default("add")
|
|
456
|
-
.describe("The operation to perform."),
|
|
457
|
-
path: z.string().describe("The
|
|
458
|
-
value: z
|
|
505
|
+
.describe("The operation to perform. Use 'test' with path '/rev' to enforce optimistic concurrency."),
|
|
506
|
+
path: z.string().describe("The path to operate on, e.g. '/fields/System.Title' or '/rev' for a revision test."),
|
|
507
|
+
value: z
|
|
508
|
+
.union([z.string(), z.number(), z.boolean(), z.null()])
|
|
509
|
+
.optional()
|
|
510
|
+
.describe("The operation value. Required for add, replace, and test; omit for remove. For a test on '/rev', pass the numeric revision previously read."),
|
|
459
511
|
}))
|
|
460
512
|
.optional()
|
|
461
|
-
.describe(
|
|
513
|
+
.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.'),
|
|
462
514
|
batchUpdates: z
|
|
463
515
|
.array(z.object({
|
|
464
516
|
op: z.enum(["Add", "Replace", "Remove"]).default("Add").describe("The operation to perform."),
|
|
@@ -521,6 +573,10 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
521
573
|
return { content: [{ type: "text", text: "id is required for update" }], isError: true };
|
|
522
574
|
if (!updates || updates.length === 0)
|
|
523
575
|
return { content: [{ type: "text", text: "updates is required for update" }], isError: true };
|
|
576
|
+
const updateWithoutValue = updates.find((update) => update.op !== "remove" && update.value === undefined);
|
|
577
|
+
if (updateWithoutValue) {
|
|
578
|
+
return { content: [{ type: "text", text: `value is required for ${updateWithoutValue.op}` }], isError: true };
|
|
579
|
+
}
|
|
524
580
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
525
581
|
const apiUpdates = updates.map((update) => ({ ...update, op: update.op }));
|
|
526
582
|
const updatedWorkItem = await workItemApi.updateWorkItem(null, apiUpdates, id);
|
|
@@ -593,8 +649,6 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
593
649
|
const ops = [
|
|
594
650
|
{ op: "add", path: "/id", value: `-${x + 1}` },
|
|
595
651
|
{ op: "add", path: "/fields/System.Title", value: item.title },
|
|
596
|
-
{ op: "add", path: "/fields/System.Description", value: encodedDescription },
|
|
597
|
-
{ op: "add", path: "/fields/Microsoft.VSTS.TCM.ReproSteps", value: encodedDescription },
|
|
598
652
|
{
|
|
599
653
|
op: "add",
|
|
600
654
|
path: "/relations/-",
|
|
@@ -610,9 +664,19 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
610
664
|
if (item.iterationPath && item.iterationPath.trim().length > 0) {
|
|
611
665
|
ops.push({ op: "add", path: "/fields/System.IterationPath", value: item.iterationPath });
|
|
612
666
|
}
|
|
613
|
-
if
|
|
614
|
-
|
|
615
|
-
|
|
667
|
+
// check if the work item type is "Bug" to determine which field to use for the description
|
|
668
|
+
// ReproSteps is used for Bugs, while Description is used for other work item types
|
|
669
|
+
if (workItemType.toLowerCase() === "bug") {
|
|
670
|
+
ops.push({ op: "add", path: "/fields/Microsoft.VSTS.TCM.ReproSteps", value: encodedDescription });
|
|
671
|
+
if (item.format && item.format === "Markdown") {
|
|
672
|
+
ops.push({ op: "add", path: "/multilineFieldsFormat/Microsoft.VSTS.TCM.ReproSteps", value: item.format });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
ops.push({ op: "add", path: "/fields/System.Description", value: encodedDescription });
|
|
677
|
+
if (item.format && item.format === "Markdown") {
|
|
678
|
+
ops.push({ op: "add", path: "/multilineFieldsFormat/System.Description", value: item.format });
|
|
679
|
+
}
|
|
616
680
|
}
|
|
617
681
|
return {
|
|
618
682
|
method: "PATCH",
|
|
@@ -641,9 +705,12 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
641
705
|
}
|
|
642
706
|
catch (error) {
|
|
643
707
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
708
|
+
const statusCode = typeof error === "object" && error !== null && "statusCode" in error && typeof error.statusCode === "number" ? error.statusCode : undefined;
|
|
709
|
+
const statusText = statusCode === 409 ? " Conflict" : statusCode === 412 ? " Precondition Failed" : "";
|
|
710
|
+
const updateStatus = statusCode !== undefined ? ` [HTTP ${statusCode}${statusText}]` : "";
|
|
644
711
|
const msgs = {
|
|
645
712
|
create: `Error creating work item: ${errorMessage}`,
|
|
646
|
-
update: `Error updating work item: ${errorMessage}`,
|
|
713
|
+
update: `Error updating work item${updateStatus}: ${errorMessage}`,
|
|
647
714
|
update_batch: `Error updating work items in batch: ${errorMessage}`,
|
|
648
715
|
add_child: `Error creating child work items: ${errorMessage}`,
|
|
649
716
|
};
|
|
@@ -676,6 +743,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
676
743
|
const orgUrl = connection.serverUrl;
|
|
677
744
|
const accessToken = await tokenProvider();
|
|
678
745
|
const formatParameter = (format ?? "Markdown") === "Markdown" ? 0 : 1;
|
|
746
|
+
const resolvedText = await resolveCommentMentions(text, format, tokenProvider, connectionProvider, userAgentProvider);
|
|
679
747
|
if (action === "add") {
|
|
680
748
|
const response = await fetch(`${orgUrl}/${encodeURIComponent(resolvedProject)}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
|
|
681
749
|
method: "POST",
|
|
@@ -684,7 +752,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
684
752
|
"Content-Type": "application/json",
|
|
685
753
|
"User-Agent": userAgentProvider(),
|
|
686
754
|
},
|
|
687
|
-
body: JSON.stringify({ text }),
|
|
755
|
+
body: JSON.stringify({ text: resolvedText }),
|
|
688
756
|
});
|
|
689
757
|
if (!response.ok) {
|
|
690
758
|
throw new Error(`Failed to add a work item comment: ${response.statusText}`);
|
|
@@ -701,7 +769,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
701
769
|
"Content-Type": "application/json",
|
|
702
770
|
"User-Agent": userAgentProvider(),
|
|
703
771
|
},
|
|
704
|
-
body: JSON.stringify({ text }),
|
|
772
|
+
body: JSON.stringify({ text: resolvedText }),
|
|
705
773
|
});
|
|
706
774
|
if (!response.ok) {
|
|
707
775
|
throw new Error(`Failed to update work item comment: ${response.statusText}`);
|
|
@@ -729,9 +797,10 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
729
797
|
updates: z
|
|
730
798
|
.array(z.object({
|
|
731
799
|
id: z.coerce.number().min(1).describe("The ID of the work item to update."),
|
|
732
|
-
linkToId: z.coerce.number().min(1).describe("The ID of the work item to link to."),
|
|
800
|
+
linkToId: z.coerce.number().min(1).optional().describe("The ID of the work item to link to. Required unless type is 'hyperlink'."),
|
|
801
|
+
url: z.string().optional().describe("The URL for a hyperlink. Required when type is 'hyperlink'."),
|
|
733
802
|
type: z
|
|
734
|
-
.enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by"])
|
|
803
|
+
.enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by", "hyperlink"])
|
|
735
804
|
.default("related")
|
|
736
805
|
.describe("Type of link. Defaults to 'related'."),
|
|
737
806
|
comment: z.string().optional().describe("Optional comment for the link."),
|
|
@@ -741,7 +810,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
741
810
|
// unlink
|
|
742
811
|
id: z.coerce.number().min(1).optional().describe("Work item ID to remove links from. Required for: unlink."),
|
|
743
812
|
type: z
|
|
744
|
-
.enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by", "artifact"])
|
|
813
|
+
.enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by", "artifact", "hyperlink"])
|
|
745
814
|
.optional()
|
|
746
815
|
.describe("Link type to remove. Required for: unlink."),
|
|
747
816
|
url: z.string().optional().describe("URL to match when removing a link. Used for: unlink. If not provided, all links of the specified type are removed."),
|
|
@@ -801,15 +870,23 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
801
870
|
headers: { "Content-Type": "application/json-patch+json" },
|
|
802
871
|
body: updates
|
|
803
872
|
.filter((update) => update.id === uid)
|
|
804
|
-
.map(({ linkToId, type: linkTypeName, comment: linkComment }) =>
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
873
|
+
.map(({ linkToId, url: linkUrl, type: linkTypeName, comment: linkComment }) => {
|
|
874
|
+
if (linkTypeName === "hyperlink" && !linkUrl) {
|
|
875
|
+
throw new Error("url is required for hyperlink links");
|
|
876
|
+
}
|
|
877
|
+
if (linkTypeName !== "hyperlink" && !linkToId) {
|
|
878
|
+
throw new Error("linkToId is required for work item links");
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
op: "add",
|
|
882
|
+
path: "/relations/-",
|
|
883
|
+
value: {
|
|
884
|
+
rel: getLinkTypeFromName(linkTypeName),
|
|
885
|
+
url: linkTypeName === "hyperlink" ? linkUrl : `${orgUrl}/${resolvedProject}/_apis/wit/workItems/${linkToId}`,
|
|
886
|
+
attributes: { comment: linkComment || "" },
|
|
887
|
+
},
|
|
888
|
+
};
|
|
889
|
+
}),
|
|
813
890
|
}));
|
|
814
891
|
const response = await fetch(`${orgUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
|
|
815
892
|
method: "PATCH",
|
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/useragent.js
CHANGED
|
File without changes
|
package/dist/utils.js
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
export const apiVersion = "7.2-preview.1";
|
|
4
4
|
export const batchApiVersion = "5.0";
|
|
5
5
|
export const markdownCommentsApiVersion = "7.2-preview.4";
|
|
6
|
+
/**
|
|
7
|
+
* Returns the user-supplied CLI arguments.
|
|
8
|
+
*
|
|
9
|
+
* The server is always started script-style — `[runtime, scriptPath, ...args]` — on Node
|
|
10
|
+
* and on Electron hosts alike, so the first two entries are always dropped.
|
|
11
|
+
*
|
|
12
|
+
* Do not replace this with yargs' `hideBin`: it drops a single entry whenever
|
|
13
|
+
* `process.versions.electron` is set and `process.defaultApp` is not, so on an Electron
|
|
14
|
+
* host the script path survives and is parsed as the organization name.
|
|
15
|
+
*/
|
|
16
|
+
export function getCliArgs(argv = process.argv) {
|
|
17
|
+
return argv.slice(2);
|
|
18
|
+
}
|
|
6
19
|
export function createEnumMapping(enumObject) {
|
|
7
20
|
const mapping = {};
|
|
8
21
|
for (const [key, value] of Object.entries(enumObject)) {
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "2.
|
|
1
|
+
export const packageVersion = "2.10.0-nightly.20260910";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azure-devops/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0-nightly.20260910",
|
|
4
4
|
"mcpName": "microsoft.com/azure-devops",
|
|
5
5
|
"description": "MCP server for interacting with Azure DevOps",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,12 +37,15 @@
|
|
|
37
37
|
"test": "jest"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@azure/identity": "^4.
|
|
41
|
-
"@azure/
|
|
42
|
-
"@
|
|
40
|
+
"@azure/identity": "^4.13.0",
|
|
41
|
+
"@azure/logger": "^1.3.0",
|
|
42
|
+
"@azure/msal-node": "^5.5.0",
|
|
43
|
+
"@azure/msal-node-extensions": "^5.3.5",
|
|
44
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
43
45
|
"azure-devops-extension-api": "^5.272.3",
|
|
44
46
|
"azure-devops-extension-sdk": "^4.0.2",
|
|
45
47
|
"azure-devops-node-api": "^15.1.2",
|
|
48
|
+
"open": "^10.2.0",
|
|
46
49
|
"winston": "^3.18.3",
|
|
47
50
|
"yargs": "^18.0.0",
|
|
48
51
|
"zod": "^3.25.63",
|
|
@@ -69,5 +72,9 @@
|
|
|
69
72
|
"**/*.(js|ts|jsx|tsx|json|css|md)": [
|
|
70
73
|
"npm run format"
|
|
71
74
|
]
|
|
75
|
+
},
|
|
76
|
+
"allowScripts": {
|
|
77
|
+
"keytar@7.9.0": true,
|
|
78
|
+
"@azure/msal-node-extensions@5.3.5": true
|
|
72
79
|
}
|
|
73
80
|
}
|
package/dist/shared/command.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Microsoft Corporation.
|
|
2
|
-
// Licensed under the MIT License.
|
|
3
|
-
/** Builds an error `CallToolResult`. */
|
|
4
|
-
export const errorResult = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
5
|
-
/**
|
|
6
|
-
* Routes a validated, action-carrying args object to the matching command.
|
|
7
|
-
*
|
|
8
|
-
* This is what removes long positional parameter lists from grouped ("action")
|
|
9
|
-
* tools: instead of destructuring every possible field, the whole typed args
|
|
10
|
-
* object is forwarded to the single command keyed by `args.action`, coupling
|
|
11
|
-
* each action to exactly one command.
|
|
12
|
-
*
|
|
13
|
-
* - Unknown actions short-circuit with an "Unknown action" error and never
|
|
14
|
-
* touch the context (so no connection is opened).
|
|
15
|
-
* - Errors thrown by a command are caught and formatted using the optional
|
|
16
|
-
* per-action `errorPrefixes` map (falling back to a generic message).
|
|
17
|
-
* - Errors returned by a command (e.g. validation `errorResult`s) pass through
|
|
18
|
-
* unchanged.
|
|
19
|
-
*/
|
|
20
|
-
export async function dispatchAction(commands, context, args, errorPrefixes) {
|
|
21
|
-
const command = commands[args.action];
|
|
22
|
-
if (!command) {
|
|
23
|
-
const supportedActions = Object.keys(commands).sort().join(", ");
|
|
24
|
-
return errorResult(`Unknown action: ${args.action}. Supported actions: ${supportedActions}`);
|
|
25
|
-
}
|
|
26
|
-
try {
|
|
27
|
-
return await command.execute(context, args);
|
|
28
|
-
}
|
|
29
|
-
catch (error) {
|
|
30
|
-
const message = error instanceof Error ? error.message : "Unknown error occurred";
|
|
31
|
-
const prefix = errorPrefixes?.[args.action];
|
|
32
|
-
return errorResult(prefix ? `${prefix}${message}` : `Error: ${message}`);
|
|
33
|
-
}
|
|
34
|
-
}
|