UNPKG

@wangkanai/devops-mcp

Version:

Dynamic Azure DevOps MCP Server for directory-based environment switching

454 lines 20 kB
#!/usr/bin/env node "use strict"; /** * Azure DevOps MCP Proxy Server * Main entry point for the dynamic Azure DevOps MCP proxy */ Object.defineProperty(exports, "__esModule", { value: true }); exports.AzureDevOpsMCPProxy = void 0; exports.main = main; const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const directory_detector_js_1 = require("./directory-detector.js"); const config_loader_js_1 = require("./utils/config-loader.js"); const local_config_loader_js_1 = require("./utils/local-config-loader.js"); const tool_handlers_js_1 = require("./handlers/tool-handlers.js"); class AzureDevOpsMCPProxy { constructor() { this.currentConfig = null; this.server = new index_js_1.Server({ name: 'devops-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, }); this.toolHandlers = new tool_handlers_js_1.ToolHandlers(); this.initializeConfiguration(); this.setupHandlers(); } /** * Initialize configuration from local .azure-devops.json files */ initializeConfiguration() { try { // Try loading local configuration first this.currentConfig = local_config_loader_js_1.LocalConfigLoader.findLocalConfig(); if (this.currentConfig) { this.toolHandlers.setCurrentConfig(this.currentConfig); console.log('Azure DevOps MCP Proxy initialized with local configuration:', { organizationUrl: this.currentConfig.organizationUrl, project: this.currentConfig.project, directory: process.cwd() }); return; } // Fallback to environment-based configuration console.log('No local configuration found, trying environment-based config...'); try { const envConfig = config_loader_js_1.ConfigLoader.loadConfig(); this.directoryDetector = new directory_detector_js_1.DirectoryDetector(envConfig.mappings, envConfig.defaultConfig); this.currentConfig = this.directoryDetector.detectConfiguration(); } catch (error) { // Environment config file doesn't exist - this is normal in the new system console.log('No environment configuration found - operating in local-only mode'); this.currentConfig = null; } if (this.currentConfig) { this.toolHandlers.setCurrentConfig(this.currentConfig); console.log('Azure DevOps MCP Proxy initialized with environment configuration:', { organizationUrl: this.currentConfig.organizationUrl, project: this.currentConfig.project }); } else { console.warn('No Azure DevOps configuration detected for current directory'); console.log('Consider creating a .azure-devops.json file in your repository'); } } catch (error) { console.error('Failed to initialize configuration:', error); // Initialize with empty configuration as fallback this.directoryDetector = new directory_detector_js_1.DirectoryDetector([]); } } /** * Setup MCP server handlers */ setupHandlers() { // List available tools this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => { const tools = [ { name: 'get-work-items', description: 'Get work items from Azure DevOps', inputSchema: { type: 'object', properties: { wiql: { type: 'string', description: 'Work Item Query Language (WIQL) query', }, ids: { type: 'array', items: { type: 'number' }, description: 'Specific work item IDs to retrieve', }, fields: { type: 'array', items: { type: 'string' }, description: 'Fields to include in the response', }, }, }, }, { name: 'create-work-item', description: 'Create a new work item in Azure DevOps', inputSchema: { type: 'object', properties: { type: { type: 'string', description: 'Work item type (e.g., Task, Bug, User Story)', }, title: { type: 'string', description: 'Work item title', }, description: { type: 'string', description: 'Work item description', }, assignedTo: { type: 'string', description: 'Email of the person to assign the work item to', }, tags: { type: 'string', description: 'Semicolon-separated tags', }, parent: { type: 'number', description: 'Parent work item ID for establishing hierarchy during creation', }, iterationPath: { type: 'string', description: 'Iteration path for sprint assignment (e.g., ProjectName\\Sprint 1)', }, state: { type: 'string', description: 'Initial work item state (e.g., New, Active)', }, }, required: ['type', 'title'], }, }, { name: 'update-work-item', description: 'Update an existing work item in Azure DevOps', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Work item ID to update', }, title: { type: 'string', description: 'Updated work item title', }, description: { type: 'string', description: 'Updated work item description', }, state: { type: 'string', description: 'Updated work item state (e.g., Active, Resolved, Closed)', }, assignedTo: { type: 'string', description: 'Email of the person to assign the work item to', }, parent: { type: 'number', description: 'Parent work item ID for establishing hierarchy', }, iterationPath: { type: 'string', description: 'Iteration path for sprint assignment (e.g., ProjectName\\Sprint 1)', }, tags: { type: 'string', description: 'Semicolon-separated tags', }, fields: { type: 'object', description: 'Generic field updates as key-value pairs', }, }, required: ['id'], }, }, { name: 'add-work-item-comment', description: 'Add a comment to an existing work item in Azure DevOps', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Work item ID to add comment to', }, comment: { type: 'string', description: 'Comment text to add', }, }, required: ['id', 'comment'], }, }, { name: 'get-repositories', description: 'Get repositories from Azure DevOps project', inputSchema: { type: 'object', properties: { includeLinks: { type: 'boolean', description: 'Include repository links in response', }, }, }, }, { name: 'get-builds', description: 'Get build definitions and recent builds', inputSchema: { type: 'object', properties: { definitionIds: { type: 'array', items: { type: 'number' }, description: 'Specific build definition IDs', }, top: { type: 'number', description: 'Number of builds to return', }, }, }, }, { name: 'get-pull-requests', description: 'Get pull requests from Azure DevOps repository', inputSchema: { type: 'object', properties: { repositoryId: { type: 'string', description: 'Repository ID or name (optional, defaults to all repos)', }, status: { type: 'string', enum: ['active', 'completed', 'abandoned', 'all'], description: 'Pull request status filter (default: active)', }, createdBy: { type: 'string', description: 'Filter by creator (user ID or email)', }, top: { type: 'number', description: 'Number of pull requests to return (default: 25)', }, }, }, }, { name: 'trigger-pipeline', description: 'Trigger a build pipeline in Azure DevOps', inputSchema: { type: 'object', properties: { definitionId: { type: 'number', description: 'Build definition ID to trigger', }, definitionName: { type: 'string', description: 'Build definition name (alternative to ID)', }, sourceBranch: { type: 'string', description: 'Source branch to build (default: default branch)', }, parameters: { type: 'object', description: 'Pipeline parameters as key-value pairs', }, }, }, }, { name: 'get-pipeline-status', description: 'Get status of a specific build or pipeline', inputSchema: { type: 'object', properties: { buildId: { type: 'number', description: 'Specific build ID to check status', }, definitionId: { type: 'number', description: 'Get latest builds for this definition ID', }, includeTimeline: { type: 'boolean', description: 'Include detailed timeline information', }, }, }, }, { name: 'get-current-context', description: 'Get current Azure DevOps context based on directory', inputSchema: { type: 'object', properties: { directory: { type: 'string', description: 'Directory path to check (defaults to current working directory)', }, }, }, }, ]; return { tools }; }); // Handle tool calls this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => { try { // Detect current environment context await this.updateCurrentContext(); if (!this.currentConfig) { return { content: [{ type: 'text', text: 'Error: No Azure DevOps configuration found for current directory. Please ensure you are in a configured project directory.', }], isError: true, }; } // Handle special context tool if (request.params.name === 'get-current-context') { return this.handleGetCurrentContext(request.params.arguments); } // Route to tool handlers with current context return await this.toolHandlers.handleToolCall(request); } catch (error) { console.error('Tool call error:', error); return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error occurred'}`, }], isError: true, }; } }); } /** * Update current Azure DevOps context based on working directory */ async updateCurrentContext() { // Skip update if using local configuration (no directory detector) if (!this.directoryDetector) { return; } const detectedConfig = this.directoryDetector.detectConfiguration(); if (detectedConfig && (!this.currentConfig || this.currentConfig.organizationUrl !== detectedConfig.organizationUrl || this.currentConfig.project !== detectedConfig.project)) { this.currentConfig = detectedConfig; this.toolHandlers.setCurrentConfig(detectedConfig); console.log(`Switched to Azure DevOps context: ${detectedConfig.organizationUrl}/${detectedConfig.project}`); } } /** * Handle get-current-context tool call */ handleGetCurrentContext(args) { const directory = args?.directory || process.cwd(); // If using local configuration, return current config if (!this.directoryDetector && this.currentConfig) { return { content: [{ type: 'text', text: JSON.stringify({ organizationUrl: this.currentConfig.organizationUrl, project: this.currentConfig.project, directory: directory, configurationSource: 'local', configFile: '.azure-devops.json' }, null, 2), }], }; } // Fall back to directory detector if available if (this.directoryDetector) { const context = this.directoryDetector.getProjectContext(directory); if (!context) { return { content: [{ type: 'text', text: 'No Azure DevOps context configured for the specified directory.', }], }; } return { content: [{ type: 'text', text: JSON.stringify({ organizationUrl: context.organizationUrl, project: context.projectName, directory: directory, configurationSource: 'environment', configuredDirectories: this.directoryDetector.getConfiguredDirectories(), }, null, 2), }], }; } return { content: [{ type: 'text', text: 'No Azure DevOps configuration found.', }], }; } /** * Start the MCP server */ async start() { const transport = new stdio_js_1.StdioServerTransport(); await this.server.connect(transport); console.error('Azure DevOps MCP Proxy Server started'); } } exports.AzureDevOpsMCPProxy = AzureDevOpsMCPProxy; async function main() { const proxy = new AzureDevOpsMCPProxy(); await proxy.start(); } if (require.main === module) { main().catch((error) => { console.error('Failed to start server:', error); process.exit(1); }); } //# sourceMappingURL=index.js.map