cursor-azure-devops-mcp
Version:
MCP Server for Cursor IDE-Azure DevOps Integration
359 lines (358 loc) • 13.5 kB
JavaScript
import fs from 'fs';
import path from 'path';
import os from 'os';
import { z } from 'zod';
import dotenv from 'dotenv';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
// Define configuration schema with Zod
const ConfigSchema = z.object({
version: z.string(),
server: z.object({
port: z.coerce.number().default(3000),
host: z.string().default('localhost'),
}),
azureDevOps: z.object({
organizationUrl: z.string().url().optional(),
token: z.string().optional(),
project: z.string().optional(),
}),
logging: z.object({
level: z.enum(['error', 'warn', 'info', 'debug']).default('info'),
enableConsole: z.boolean().default(true),
}),
});
// Create a default config object
const DEFAULT_CONFIG = {
version: '1.0.0',
server: {
port: 3000,
host: 'localhost',
},
azureDevOps: {
organizationUrl: undefined,
token: undefined,
project: undefined,
},
logging: {
level: 'info',
enableConsole: true,
},
};
/**
* Configuration Manager class that loads configuration from multiple sources
* with the following priority:
* 1. Command line arguments
* 2. VSCode/Cursor IDE settings
* 3. Environment variables / .env file
* 4. Default values
*/
export class ConfigManager {
static instance;
config = null;
packageJson = { version: '1.0.0' };
constructor() {
try {
// Load package.json for version info
const packageJsonPath = path.join(process.cwd(), 'package.json');
if (fs.existsSync(packageJsonPath)) {
this.packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
}
}
catch (error) {
console.error('Error loading package.json:', error);
}
}
/**
* Get the singleton instance of the ConfigManager
*/
static getInstance() {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager();
}
return ConfigManager.instance;
}
/**
* Parse command line arguments using yargs
*/
parseCommandLineArgs() {
const argv = yargs(hideBin(process.argv))
.options({
port: {
type: 'number',
describe: 'Server port number',
alias: 'p',
},
host: {
type: 'string',
describe: 'Server hostname',
alias: 'h',
},
'azure-org-url': {
type: 'string',
describe: 'Azure DevOps organization URL',
alias: 'org',
},
'azure-token': {
type: 'string',
describe: 'Azure DevOps personal access token',
alias: 'token',
},
'azure-project': {
type: 'string',
describe: 'Azure DevOps project name',
alias: 'project',
},
'log-level': {
type: 'string',
describe: 'Logging level (error, warn, info, debug)',
choices: ['error', 'warn', 'info', 'debug'],
alias: 'log',
},
})
.help()
.alias('help', '?')
.parseSync();
const result = {
server: {
port: DEFAULT_CONFIG.server.port,
host: DEFAULT_CONFIG.server.host,
},
azureDevOps: {
organizationUrl: undefined,
token: undefined,
project: undefined,
},
logging: {
level: DEFAULT_CONFIG.logging.level,
enableConsole: DEFAULT_CONFIG.logging.enableConsole,
},
};
// Map command line args to config structure
if (argv.port !== undefined) {
result.server.port = argv.port;
}
if (argv.host !== undefined) {
result.server.host = argv.host;
}
if (argv['azure-org-url'] !== undefined) {
result.azureDevOps.organizationUrl = argv['azure-org-url'];
}
if (argv['azure-token'] !== undefined) {
result.azureDevOps.token = argv['azure-token'];
}
if (argv['azure-project'] !== undefined) {
result.azureDevOps.project = argv['azure-project'];
}
if (argv['log-level'] !== undefined) {
result.logging.level = argv['log-level'];
}
return result;
}
/**
* Load configuration from VSCode/Cursor IDE settings
*/
loadIdeSettings() {
const result = {
server: {
port: DEFAULT_CONFIG.server.port,
host: DEFAULT_CONFIG.server.host,
},
azureDevOps: {
organizationUrl: undefined,
token: undefined,
project: undefined,
},
logging: {
level: DEFAULT_CONFIG.logging.level,
enableConsole: DEFAULT_CONFIG.logging.enableConsole,
},
};
// Standard locations for VSCode settings
const vscodePaths = [
// Global VSCode settings
path.join(os.homedir(), '.vscode', 'settings.json'),
// Workspace VSCode settings
path.join(process.cwd(), '.vscode', 'settings.json'),
// Cursor IDE settings (similar location to VSCode)
path.join(os.homedir(), '.cursor', 'settings.json'),
path.join(process.cwd(), '.cursor', 'settings.json'),
];
for (const settingsPath of vscodePaths) {
try {
if (fs.existsSync(settingsPath)) {
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
// Try to find Azure DevOps settings in VSCode/Cursor configs
if (settings['azureDevOps.organization']) {
result.azureDevOps.organizationUrl = `https://dev.azure.com/${settings['azureDevOps.organization']}`;
}
if (settings['azureDevOps.orgUrl']) {
result.azureDevOps.organizationUrl = settings['azureDevOps.orgUrl'];
}
if (settings['azureDevOps.token']) {
result.azureDevOps.token = settings['azureDevOps.token'];
}
if (settings['azureDevOps.project']) {
result.azureDevOps.project = settings['azureDevOps.project'];
}
// Also look for specific cursor-azure-devops-mcp settings
const cursorSettings = settings['cursor-azure-devops-mcp'];
if (cursorSettings) {
if (cursorSettings.port !== undefined) {
result.server.port = cursorSettings.port;
}
if (cursorSettings.host !== undefined) {
result.server.host = cursorSettings.host;
}
if (cursorSettings.organizationUrl !== undefined) {
result.azureDevOps.organizationUrl = cursorSettings.organizationUrl;
}
if (cursorSettings.token !== undefined) {
result.azureDevOps.token = cursorSettings.token;
}
if (cursorSettings.project !== undefined) {
result.azureDevOps.project = cursorSettings.project;
}
if (cursorSettings.logLevel !== undefined) {
result.logging.level = cursorSettings.logLevel;
}
}
}
}
catch (error) {
// Silently ignore errors reading settings files
}
}
return result;
}
/**
* Load configuration from environment variables / .env file
*/
loadEnvConfig() {
// Load .env file from various locations
const rootEnvPath = path.resolve('.env');
const rootEnvResult = dotenv.config({ path: rootEnvPath });
// Also try loading from the current working directory
const cwd = process.cwd();
const cwdEnvPath = path.join(cwd, '.env');
if (fs.existsSync(cwdEnvPath)) {
dotenv.config({ path: cwdEnvPath });
}
// Try loading from the parent directory
const parentEnvPath = path.join(cwd, '..', '.env');
if (fs.existsSync(parentEnvPath)) {
dotenv.config({ path: parentEnvPath });
}
// Create the result configuration object directly from environment variables
return {
server: {
port: process.env.PORT ? parseInt(process.env.PORT, 10) : DEFAULT_CONFIG.server.port,
host: process.env.HOST || DEFAULT_CONFIG.server.host,
},
azureDevOps: {
organizationUrl: process.env.AZURE_DEVOPS_ORG_URL,
token: process.env.AZURE_DEVOPS_TOKEN,
project: process.env.AZURE_DEVOPS_PROJECT,
},
logging: {
level: (process.env.LOG_LEVEL || DEFAULT_CONFIG.logging.level),
enableConsole: process.env.ENABLE_CONSOLE_LOGGING !== 'false',
},
};
}
/**
* Load the configuration from all sources with proper priority
*/
loadConfig() {
if (this.config) {
return this.config;
}
// Start with default config
const baseConfig = {
...DEFAULT_CONFIG,
version: this.packageJson.version,
};
// Get config from different sources
const envConfig = this.loadEnvConfig();
const ideConfig = this.loadIdeSettings();
const cmdLineConfig = this.parseCommandLineArgs();
// Directly create the merged config to avoid issues with nested objects
const mergedConfig = {
version: this.packageJson.version,
server: {
port: cmdLineConfig.server?.port ||
ideConfig.server?.port ||
envConfig.server?.port ||
DEFAULT_CONFIG.server.port,
host: cmdLineConfig.server?.host ||
ideConfig.server?.host ||
envConfig.server?.host ||
DEFAULT_CONFIG.server.host,
},
azureDevOps: {
organizationUrl: cmdLineConfig.azureDevOps?.organizationUrl ||
ideConfig.azureDevOps?.organizationUrl ||
envConfig.azureDevOps?.organizationUrl,
token: cmdLineConfig.azureDevOps?.token ||
ideConfig.azureDevOps?.token ||
envConfig.azureDevOps?.token,
project: cmdLineConfig.azureDevOps?.project ||
ideConfig.azureDevOps?.project ||
envConfig.azureDevOps?.project,
},
logging: {
level: (cmdLineConfig.logging?.level ||
ideConfig.logging?.level ||
envConfig.logging?.level ||
DEFAULT_CONFIG.logging.level),
enableConsole: cmdLineConfig.logging?.enableConsole !== undefined
? cmdLineConfig.logging?.enableConsole
: ideConfig.logging?.enableConsole !== undefined
? ideConfig.logging?.enableConsole
: envConfig.logging?.enableConsole !== undefined
? envConfig.logging?.enableConsole
: DEFAULT_CONFIG.logging.enableConsole,
},
};
// Validate the configuration
try {
this.config = ConfigSchema.parse(mergedConfig);
return this.config;
}
catch (_error) {
console.error('Configuration validation error:', _error);
// Return a default config if validation fails
return baseConfig;
}
}
/**
* Print the current configuration (with sensitive info redacted)
*/
printConfig() {
const config = this.loadConfig();
// Create a copy with redacted token
const printableConfig = {
...config,
azureDevOps: {
...config.azureDevOps,
token: config.azureDevOps.token ? '*****' : undefined,
},
};
console.log('=== Configuration ===');
console.log(JSON.stringify(printableConfig, null, 2));
console.log('====================');
}
/**
* Check if the Azure DevOps configuration is valid
*/
isAzureDevOpsConfigValid() {
const config = this.loadConfig();
return !!config.azureDevOps.organizationUrl && !!config.azureDevOps.token;
}
}
// Export an instance of the config manager
export const configManager = ConfigManager.getInstance();
// Export a function to get the config for backward compatibility
export function loadConfig() {
return configManager.loadConfig();
}