@azure-devops/mcp 2.5.0-nightly.20260413 → 2.5.0-nightly.20260415
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/auth.js +13 -0
- package/dist/index.js +23 -5
- package/dist/tools/repositories.js +46 -12
- package/dist/tools/work-items.js +11 -3
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/auth.js
CHANGED
|
@@ -71,6 +71,19 @@ class OAuthAuthenticator {
|
|
|
71
71
|
function createAuthenticator(type, tenantId) {
|
|
72
72
|
logger.debug(`Creating authenticator of type '${type}' with tenantId='${tenantId ?? "undefined"}'`);
|
|
73
73
|
switch (type) {
|
|
74
|
+
case "pat":
|
|
75
|
+
logger.debug(`Authenticator: Using PAT authentication (PERSONAL_ACCESS_TOKEN)`);
|
|
76
|
+
return async () => {
|
|
77
|
+
logger.debug(`${type}: Reading token from PERSONAL_ACCESS_TOKEN environment variable`);
|
|
78
|
+
const b64Pat = process.env["PERSONAL_ACCESS_TOKEN"];
|
|
79
|
+
if (!b64Pat) {
|
|
80
|
+
logger.error(`${type}: PERSONAL_ACCESS_TOKEN environment variable is not set or empty`);
|
|
81
|
+
throw new Error("Environment variable 'PERSONAL_ACCESS_TOKEN' is not set or empty. Please set it with a valid base64-encoded Azure DevOps Personal Access Token.");
|
|
82
|
+
}
|
|
83
|
+
// Return base64 value as-is — caller uses it directly as the Basic auth credential
|
|
84
|
+
logger.debug(`${type}: Successfully retrieved PAT from environment variable`);
|
|
85
|
+
return b64Pat;
|
|
86
|
+
};
|
|
74
87
|
case "envvar":
|
|
75
88
|
logger.debug(`Authenticator: Using environment variable authentication (ADO_MCP_AUTH_TOKEN)`);
|
|
76
89
|
// Read token from fixed environment variable
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Licensed under the MIT License.
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
-
import { getBearerHandler, WebApi } from "azure-devops-node-api";
|
|
6
|
+
import { getBearerHandler, getPersonalAccessTokenHandler, WebApi } from "azure-devops-node-api";
|
|
7
7
|
import yargs from "yargs";
|
|
8
8
|
import { hideBin } from "yargs/helpers";
|
|
9
9
|
import { createAuthenticator } from "./auth.js";
|
|
@@ -41,7 +41,7 @@ const argv = yargs(hideBin(process.argv))
|
|
|
41
41
|
alias: "a",
|
|
42
42
|
describe: "Type of authentication to use",
|
|
43
43
|
type: "string",
|
|
44
|
-
choices: ["interactive", "azcli", "env", "envvar"],
|
|
44
|
+
choices: ["interactive", "azcli", "env", "envvar", "pat"],
|
|
45
45
|
default: defaultAuthenticationType,
|
|
46
46
|
})
|
|
47
47
|
.option("tenant", {
|
|
@@ -55,10 +55,12 @@ export const orgName = argv.organization;
|
|
|
55
55
|
const orgUrl = "https://dev.azure.com/" + orgName;
|
|
56
56
|
const domainsManager = new DomainsManager(argv.domains);
|
|
57
57
|
export const enabledDomains = domainsManager.getEnabledDomains();
|
|
58
|
-
function getAzureDevOpsClient(getAzureDevOpsToken, userAgentComposer) {
|
|
58
|
+
function getAzureDevOpsClient(getAzureDevOpsToken, userAgentComposer, authType) {
|
|
59
59
|
return async () => {
|
|
60
60
|
const accessToken = await getAzureDevOpsToken();
|
|
61
|
-
|
|
61
|
+
// For pat, accessToken is base64("{email}:{token}"). Decode to extract the token part,
|
|
62
|
+
// since getPersonalAccessTokenHandler prepends ":" internally and just needs the raw token.
|
|
63
|
+
const authHandler = authType === "pat" ? getPersonalAccessTokenHandler(Buffer.from(accessToken, "base64").toString("utf8").split(":").slice(1).join(":")) : getBearerHandler(accessToken);
|
|
62
64
|
const connection = new WebApi(orgUrl, authHandler, undefined, {
|
|
63
65
|
productName: "AzureDevOps.MCP",
|
|
64
66
|
productVersion: packageVersion,
|
|
@@ -93,9 +95,25 @@ async function main() {
|
|
|
93
95
|
};
|
|
94
96
|
const tenantId = (await getOrgTenant(orgName)) ?? argv.tenant;
|
|
95
97
|
const authenticator = createAuthenticator(argv.authentication, tenantId);
|
|
98
|
+
if (argv.authentication === "pat") {
|
|
99
|
+
const basicValue = await authenticator();
|
|
100
|
+
// basicValue is already base64("{email}:{token}") — use it directly in the Authorization header
|
|
101
|
+
const _originalFetch = globalThis.fetch;
|
|
102
|
+
globalThis.fetch = async (input, init) => {
|
|
103
|
+
if (init?.headers) {
|
|
104
|
+
const headers = new Headers(init.headers);
|
|
105
|
+
if (headers.get("Authorization")?.startsWith("Bearer ")) {
|
|
106
|
+
headers.set("Authorization", `Basic ${basicValue}`);
|
|
107
|
+
init = { ...init, headers };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return _originalFetch(input, init);
|
|
111
|
+
};
|
|
112
|
+
logger.debug("PAT mode: global fetch interceptor installed to rewrite Bearer -> Basic auth headers");
|
|
113
|
+
}
|
|
96
114
|
// removing prompts untill further notice
|
|
97
115
|
// configurePrompts(server);
|
|
98
|
-
configureAllTools(server, authenticator, getAzureDevOpsClient(authenticator, userAgentComposer), () => userAgentComposer.userAgent, enabledDomains);
|
|
116
|
+
configureAllTools(server, authenticator, getAzureDevOpsClient(authenticator, userAgentComposer, argv.authentication), () => userAgentComposer.userAgent, enabledDomains);
|
|
99
117
|
const transport = new StdioServerTransport();
|
|
100
118
|
await server.connect(transport);
|
|
101
119
|
}
|
|
@@ -811,42 +811,76 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
|
|
|
811
811
|
project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a repository name instead of a GUID."),
|
|
812
812
|
includeWorkItemRefs: z.boolean().optional().default(false).describe("Whether to reference work items associated with the pull request."),
|
|
813
813
|
includeLabels: z.boolean().optional().default(false).describe("Whether to include a summary of labels in the response."),
|
|
814
|
-
|
|
814
|
+
includeChangedFiles: z.boolean().optional().default(false).describe("Whether to include the list of files changed in the pull request."),
|
|
815
|
+
}, async ({ repositoryId, pullRequestId, project, includeWorkItemRefs, includeLabels, includeChangedFiles }) => {
|
|
815
816
|
try {
|
|
816
817
|
const connection = await connectionProvider();
|
|
817
818
|
const gitApi = await connection.getGitApi();
|
|
818
819
|
const pullRequest = await gitApi.getPullRequest(repositoryId, pullRequestId, project, undefined, undefined, undefined, undefined, includeWorkItemRefs);
|
|
820
|
+
let enhancedResponse = { ...pullRequest };
|
|
819
821
|
if (includeLabels) {
|
|
820
822
|
try {
|
|
821
823
|
const projectId = pullRequest.repository?.project?.id;
|
|
822
824
|
const projectName = pullRequest.repository?.project?.name;
|
|
823
825
|
const labels = await gitApi.getPullRequestLabels(repositoryId, pullRequestId, projectName, projectId);
|
|
824
826
|
const labelNames = labels.map((label) => label.name).filter((name) => name !== undefined);
|
|
825
|
-
|
|
826
|
-
...
|
|
827
|
+
enhancedResponse = {
|
|
828
|
+
...enhancedResponse,
|
|
827
829
|
labelSummary: {
|
|
828
830
|
labels: labelNames,
|
|
829
831
|
labelCount: labelNames.length,
|
|
830
832
|
},
|
|
831
833
|
};
|
|
832
|
-
return {
|
|
833
|
-
content: [{ type: "text", text: JSON.stringify(enhancedResponse, null, 2) }],
|
|
834
|
-
};
|
|
835
834
|
}
|
|
836
835
|
catch (error) {
|
|
837
836
|
console.warn(`Error fetching PR labels: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
...pullRequest,
|
|
837
|
+
enhancedResponse = {
|
|
838
|
+
...enhancedResponse,
|
|
841
839
|
labelSummary: {},
|
|
842
840
|
};
|
|
843
|
-
|
|
844
|
-
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (includeChangedFiles) {
|
|
844
|
+
try {
|
|
845
|
+
const iterations = await gitApi.getPullRequestIterations(repositoryId, pullRequestId, project);
|
|
846
|
+
if (iterations?.length) {
|
|
847
|
+
const latestIteration = iterations[iterations.length - 1];
|
|
848
|
+
if (latestIteration.id != null) {
|
|
849
|
+
const changes = await gitApi.getPullRequestIterationChanges(repositoryId, pullRequestId, latestIteration.id, project);
|
|
850
|
+
enhancedResponse = {
|
|
851
|
+
...enhancedResponse,
|
|
852
|
+
changedFilesSummary: {
|
|
853
|
+
changeEntries: changes?.changeEntries ?? [],
|
|
854
|
+
fileCount: changes?.changeEntries?.length ?? 0,
|
|
855
|
+
nextSkip: changes?.nextSkip,
|
|
856
|
+
nextTop: changes?.nextTop,
|
|
857
|
+
},
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
enhancedResponse = {
|
|
862
|
+
...enhancedResponse,
|
|
863
|
+
changedFilesSummary: { changeEntries: [], fileCount: 0 },
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
enhancedResponse = {
|
|
869
|
+
...enhancedResponse,
|
|
870
|
+
changedFilesSummary: { changeEntries: [], fileCount: 0 },
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
console.warn(`Error fetching PR changed files: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
876
|
+
enhancedResponse = {
|
|
877
|
+
...enhancedResponse,
|
|
878
|
+
changedFilesSummary: {},
|
|
845
879
|
};
|
|
846
880
|
}
|
|
847
881
|
}
|
|
848
882
|
return {
|
|
849
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
883
|
+
content: [{ type: "text", text: JSON.stringify(enhancedResponse, null, 2) }],
|
|
850
884
|
};
|
|
851
885
|
}
|
|
852
886
|
catch (error) {
|
package/dist/tools/work-items.js
CHANGED
|
@@ -222,13 +222,16 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
222
222
|
server.tool(WORKITEM_TOOLS.get_work_item, "Get a single work item by ID. If a project is not specified, you will be prompted to select one.", {
|
|
223
223
|
id: z.coerce.number().min(1).describe("The ID of the work item to retrieve."),
|
|
224
224
|
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."),
|
|
225
|
-
fields: z
|
|
225
|
+
fields: z
|
|
226
|
+
.array(z.string())
|
|
227
|
+
.optional()
|
|
228
|
+
.describe("Optional list of fields to include in the response. If not provided, all fields will be returned. Cannot be used together with the expand parameter."),
|
|
226
229
|
asOf: z.coerce.date().optional().describe("Optional date string to retrieve the work item as of a specific time. If not provided, the current state will be returned."),
|
|
227
230
|
expand: z
|
|
228
231
|
.enum(["all", "fields", "links", "none", "relations"])
|
|
229
|
-
.describe("Optional expand parameter to include additional details in the response.")
|
|
232
|
+
.describe("Optional expand parameter to include additional details in the response. Cannot be used together with the fields parameter.")
|
|
230
233
|
.optional()
|
|
231
|
-
.describe("Expand options include '
|
|
234
|
+
.describe("Expand options include 'All', 'Fields', 'Links', 'None', and 'Relations'. Relations can be used to get child workitems. Defaults to 'None'. Cannot be used together with the fields parameter."),
|
|
232
235
|
}, async ({ id, project, fields, asOf, expand }) => {
|
|
233
236
|
try {
|
|
234
237
|
const connection = await connectionProvider();
|
|
@@ -239,6 +242,11 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
|
|
|
239
242
|
return result.response;
|
|
240
243
|
resolvedProject = result.resolved;
|
|
241
244
|
}
|
|
245
|
+
// The Azure DevOps API does not support using expand and fields together.
|
|
246
|
+
// When both are provided, prefer fields as it is the more specific selection.
|
|
247
|
+
if (fields && fields.length > 0 && expand != null) {
|
|
248
|
+
expand = "none";
|
|
249
|
+
}
|
|
242
250
|
const workItemApi = await connection.getWorkItemTrackingApi();
|
|
243
251
|
const workItem = await workItemApi.getWorkItem(id, fields, asOf, expand, resolvedProject);
|
|
244
252
|
return {
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const packageVersion = "2.5.0-nightly.
|
|
1
|
+
export const packageVersion = "2.5.0-nightly.20260415";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azure-devops/mcp",
|
|
3
|
-
"version": "2.5.0-nightly.
|
|
3
|
+
"version": "2.5.0-nightly.20260415",
|
|
4
4
|
"mcpName": "microsoft.com/azure-devops",
|
|
5
5
|
"description": "MCP server for interacting with Azure DevOps",
|
|
6
6
|
"license": "MIT",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"jest": "^30.0.2",
|
|
60
60
|
"jest-extended": "^7.0.0",
|
|
61
61
|
"lint-staged": "^16.2.7",
|
|
62
|
-
"prettier": "3.8.
|
|
62
|
+
"prettier": "3.8.3",
|
|
63
63
|
"shx": "^0.4.0",
|
|
64
64
|
"ts-jest": "^29.4.6",
|
|
65
65
|
"tsconfig-paths": "^4.2.0",
|