Last active
March 30, 2026 12:54
-
-
Save tonnguyen/d7cd32e3b48b1054054b8df04321bef7 to your computer and use it in GitHub Desktop.
Azure DevOps Enhanced MCP Server Setup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| 'use strict'; | |
| // Setup @ax-ado-mcp/mcp-server-azure-devops-enhanced | |
| // Supports: Claude Desktop, VS Code (GitHub Copilot), Visual Studio, Cursor | |
| // Platforms: macOS, Linux, Windows — requires Node.js (already needed by the MCP server) | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const os = require('os'); | |
| const readline = require('readline'); | |
| const PACKAGE_VERSION = '0.1.0'; | |
| // ── Config file paths per platform ─────────────────────────────────────────── | |
| function getConfigPaths() { | |
| const home = os.homedir(); | |
| if (process.platform === 'darwin') { | |
| const appSupport = path.join(home, 'Library', 'Application Support'); | |
| return { | |
| claude: path.join(appSupport, 'Claude', 'claude_desktop_config.json'), | |
| vscode: path.join(appSupport, 'Code', 'User', 'mcp.json'), | |
| cursor: path.join(home, '.cursor', 'mcp.json'), | |
| visualstudio: path.join(home, '.mcp.json'), | |
| }; | |
| } | |
| if (process.platform === 'win32') { | |
| return { | |
| claude: path.join(process.env.APPDATA, 'Claude', 'claude_desktop_config.json'), | |
| vscode: path.join(process.env.APPDATA, 'Code', 'User', 'mcp.json'), | |
| cursor: path.join(home, '.cursor', 'mcp.json'), | |
| visualstudio: path.join(home, '.mcp.json'), | |
| }; | |
| } | |
| // Linux | |
| const configDir = process.env.XDG_CONFIG_HOME || path.join(home, '.config'); | |
| return { | |
| claude: path.join(configDir, 'Claude', 'claude_desktop_config.json'), | |
| vscode: path.join(configDir, 'Code', 'User', 'mcp.json'), | |
| cursor: path.join(home, '.cursor', 'mcp.json'), | |
| visualstudio: path.join(home, '.mcp.json'), | |
| }; | |
| } | |
| // ── Prompt helpers ──────────────────────────────────────────────────────────── | |
| function ask(rl, question) { | |
| return new Promise(resolve => rl.question(question, resolve)); | |
| } | |
| function askSecret(rl, question) { | |
| return ask(rl, question); | |
| } | |
| // ── JSON helpers ────────────────────────────────────────────────────────────── | |
| function readJson(file) { | |
| if (!fs.existsSync(file)) return {}; | |
| const content = fs.readFileSync(file, 'utf8'); | |
| // Try plain JSON first; fall back to stripping JSONC line comments | |
| // (only strips // at start of line or after whitespace, not inside strings) | |
| try { | |
| return JSON.parse(content); | |
| } catch (_) { | |
| const stripped = content.replace(/^[ \t]*\/\/[^\n]*/mg, '').replace(/\/\*[\s\S]*?\*\//g, ''); | |
| return JSON.parse(stripped); | |
| } | |
| } | |
| function writeJson(file, obj) { | |
| fs.mkdirSync(path.dirname(file), { recursive: true }); | |
| fs.writeFileSync(file, JSON.stringify(obj, null, 2) + '\n', 'utf8'); | |
| } | |
| // ── MCP server entry ────────────────────────────────────────────────────────── | |
| function mcpEntry(orgUrl, defaultProject, pat) { | |
| return { | |
| type: 'stdio', | |
| command: 'npx', | |
| args: ['-y', `@ax-ado-mcp/mcp-server-azure-devops-enhanced@${PACKAGE_VERSION}`], | |
| env: { | |
| AZURE_DEVOPS_ORG_URL: orgUrl, | |
| AZURE_DEVOPS_AUTH_METHOD: 'pat', | |
| AZURE_DEVOPS_PAT: pat, | |
| AZURE_DEVOPS_DEFAULT_PROJECT: defaultProject, | |
| }, | |
| }; | |
| } | |
| // ── Config writers ──────────────────────────────────────────────────────────── | |
| // Claude Desktop / Cursor — { "mcpServers": { ... } } | |
| function configureMcpServers(file, orgUrl, defaultProject, pat) { | |
| try { | |
| const config = readJson(file); | |
| if (!config.mcpServers) config.mcpServers = {}; | |
| config.mcpServers.azureDevOpsEnhanced = mcpEntry(orgUrl, defaultProject, pat); | |
| writeJson(file, config); | |
| return true; | |
| } catch (e) { | |
| console.error(` Error: ${e.message}`); | |
| return false; | |
| } | |
| } | |
| // VS Code mcp.json — { "servers": { ... } } | |
| function configureVscode(file, orgUrl, defaultProject, pat) { | |
| try { | |
| const config = readJson(file); | |
| if (!config.servers) config.servers = {}; | |
| config.servers.azureDevOpsEnhanced = mcpEntry(orgUrl, defaultProject, pat); | |
| writeJson(file, config); | |
| return true; | |
| } catch (e) { | |
| console.error(` Error: ${e.message}`); | |
| return false; | |
| } | |
| } | |
| // ── Main ────────────────────────────────────────────────────────────────────── | |
| async function main() { | |
| const paths = getConfigPaths(); | |
| console.log('================================================'); | |
| console.log(' Azure DevOps Enhanced MCP Server Setup'); | |
| console.log('================================================'); | |
| console.log(''); | |
| console.log('Which tool(s) do you want to configure?'); | |
| console.log(' 1) Claude Desktop'); | |
| console.log(' 2) VS Code (GitHub Copilot)'); | |
| console.log(' 3) Cursor'); | |
| console.log(' 4) Visual Studio'); | |
| console.log(''); | |
| const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); | |
| const choicesRaw = await ask(rl, 'Enter choice(s), space-separated [default: 1]: '); | |
| const choices = (choicesRaw.trim() || '1').split(/[\s,]+/).filter(Boolean); | |
| console.log(''); | |
| const orgUrlRaw = await ask(rl, 'Azure DevOps org URL (e.g. https://dev.azure.com/myorg): '); | |
| const orgUrl = orgUrlRaw.trim(); | |
| if (!orgUrl) { | |
| console.error('\nError: Org URL cannot be empty. Aborting.'); | |
| rl.close(); | |
| process.exit(1); | |
| } | |
| const defaultProjectRaw = await ask(rl, 'Default project name: '); | |
| const defaultProject = defaultProjectRaw.trim(); | |
| if (!defaultProject) { | |
| console.error('\nError: Default project cannot be empty. Aborting.'); | |
| rl.close(); | |
| process.exit(1); | |
| } | |
| console.log(''); | |
| console.log('You need a Personal Access Token (PAT) with Read access.'); | |
| console.log('To create one, go to: <your-org-url>/_usersSettings/tokens'); | |
| console.log('Recommended scopes: Work Items (Read), Code (Read),'); | |
| console.log(' Pull Request Threads (Read & Write for PR reviews).'); | |
| console.log(''); | |
| const pat = await askSecret(rl, 'Enter your Personal Access Token (PAT): '); | |
| rl.close(); | |
| if (!pat) { | |
| console.error('\nError: PAT cannot be empty. Aborting.'); | |
| process.exit(1); | |
| } | |
| console.log(''); | |
| let configured = 0; | |
| let failed = 0; | |
| for (const choice of choices) { | |
| switch (choice.trim()) { | |
| case '1': | |
| console.log('Configuring Claude Desktop...'); | |
| if (configureMcpServers(paths.claude, orgUrl, defaultProject, pat)) { console.log(' OK'); configured++; } else { failed++; } | |
| break; | |
| case '2': | |
| console.log('Configuring VS Code (GitHub Copilot)...'); | |
| if (configureVscode(paths.vscode, orgUrl, defaultProject, pat)) { console.log(' OK'); configured++; } else { failed++; } | |
| break; | |
| case '3': | |
| console.log('Configuring Cursor...'); | |
| if (configureMcpServers(paths.cursor, orgUrl, defaultProject, pat)) { console.log(' OK'); configured++; } else { failed++; } | |
| break; | |
| case '4': | |
| console.log('Configuring Visual Studio...'); | |
| if (configureVscode(paths.visualstudio, orgUrl, defaultProject, pat)) { console.log(' OK'); configured++; } else { failed++; } | |
| break; | |
| default: | |
| console.log(`Unknown choice '${choice}' — skipped.`); | |
| } | |
| } | |
| console.log(''); | |
| console.log('================================================'); | |
| if (configured > 0) console.log(' Done! Restart configured tools to apply changes.'); | |
| if (failed > 0) console.log(` ${failed} configuration(s) failed — see errors above.`); | |
| console.log('================================================'); | |
| process.exit(failed > 0 ? 1 : 0); | |
| } | |
| main().catch(e => { | |
| console.error('Unexpected error:', e.message); | |
| process.exit(1); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment