@mcp-apps/azure-devops-mcp-server
Version:
A Model Context Protocol (MCP) server for Azure DevOps integration
140 lines • 6.15 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAccessToken = getAccessToken;
const identity_1 = require("@azure/identity");
const dotenv = __importStar(require("dotenv"));
dotenv.config();
const azureDevOpsScopes = ["https://app.vssps.visualstudio.com/.default"];
// Only pass a tenantId when one is explicitly set. Forcing `common` (the
// previous default) makes AzureCliCredential / VSCodeCredential fail and
// causes the chain to fall through to interactive auth, which pops the WAM
// account picker even when `az login` is valid.
const tenantId = process.env.AZURE_TENANT_ID || process.env.TENANT_ID;
// Background MCP processes have no UI; default to non-interactive only.
const allowInteractive = process.env.MCP_ALLOW_INTERACTIVE_AUTH === "1";
// MCP servers communicate over stdout (JSON-RPC). All diagnostics MUST go to
// stderr or they will corrupt the protocol stream.
const log = (...args) => console.error("[azure-devops-mcp]", ...args);
let cachedToken = null;
let tokenExpiresAt = 0;
let authenticationPromise = null;
let credentialChain = null;
// Lazily build a platform-aware credential chain:
// 1. AzureCliCredential (`az login`) - all platforms
// 2. AzureDeveloperCliCredential (`azd auth login`) - all platforms
// 3. VisualStudioCodeCredential (VS Code Azure sign-in) - all platforms
// 4. InteractiveBrowserCredential + WAM broker - Windows only
// 5. DeviceCodeCredential (prints code to stderr) - all platforms
function buildCredential() {
if (credentialChain) {
return credentialChain;
}
const tenantOpts = tenantId
? { tenantId, additionallyAllowedTenants: ["*"] }
: { additionallyAllowedTenants: ["*"] };
const credentials = [
new identity_1.AzureCliCredential(tenantOpts),
new identity_1.AzureDeveloperCliCredential(tenantOpts),
new identity_1.VisualStudioCodeCredential(tenantOpts),
];
if (allowInteractive && process.platform === "win32") {
try {
// Load the WAM broker only on Windows; the native binding does not
// exist for macOS/Linux and requiring it there throws.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { nativeBrokerPlugin } = require("@azure/identity-broker");
(0, identity_1.useIdentityPlugin)(nativeBrokerPlugin);
credentials.push(new identity_1.InteractiveBrowserCredential({
additionallyAllowedTenants: ["*"],
...(tenantId ? { tenantId } : {}),
brokerOptions: {
enabled: true,
parentWindowHandle: new Uint8Array(0),
useDefaultBrokerAccount: false,
legacyEnableMsaPassthrough: true,
},
}));
}
catch (err) {
log("WAM broker unavailable, skipping:", err.message);
}
}
// Last-resort interactive flow that works in any terminal.
if (allowInteractive) {
credentials.push(new identity_1.DeviceCodeCredential({
...(tenantId ? { tenantId } : {}),
additionallyAllowedTenants: ["*"],
userPromptCallback: (info) => {
log(`To sign in, open ${info.verificationUri} and enter code ${info.userCode}`);
},
}));
}
credentialChain = new identity_1.ChainedTokenCredential(...credentials);
return credentialChain;
}
async function getAccessToken() {
const now = Date.now();
if (cachedToken && tokenExpiresAt > now) {
return cachedToken;
}
if (authenticationPromise) {
log("Authentication already in progress, waiting...");
return authenticationPromise;
}
authenticationPromise = (async () => {
try {
const credential = buildCredential();
const tokenResponse = await credential.getToken(azureDevOpsScopes.join(" "), {
tenantId,
});
if (!tokenResponse || !tokenResponse.token) {
throw new Error("Failed to acquire Azure DevOps token");
}
cachedToken = tokenResponse.token;
tokenExpiresAt = tokenResponse.expiresOnTimestamp - 5 * 60 * 1000;
return cachedToken;
}
catch (error) {
log("Error acquiring token:", error.message ?? error);
throw new Error("Failed to acquire Azure DevOps access token. Try `az login` (recommended), `azd auth login`, or sign in to the Azure VS Code extension.");
}
finally {
authenticationPromise = null;
}
})();
return authenticationPromise;
}
//# sourceMappingURL=token-manager.js.map