Skip to main content
Glama
index.ts15.5 kB
#!/usr/bin/env node /** * Azure DevOps MCP Proxy Server * Main entry point for the dynamic Azure DevOps MCP proxy */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { DirectoryDetector } from './directory-detector.js'; import { ConfigLoader } from './utils/config-loader.js'; import { LocalConfigLoader } from './utils/local-config-loader.js'; import { ToolHandlers } from './handlers/tool-handlers.js'; import { AzureDevOpsConfig } from './types/index.js'; class AzureDevOpsMCPProxy { private server: Server; private directoryDetector!: DirectoryDetector; private toolHandlers: ToolHandlers; private currentConfig: AzureDevOpsConfig | null = null; constructor() { this.server = new Server( { name: 'devops-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.toolHandlers = new ToolHandlers(); this.initializeConfiguration(); this.setupHandlers(); } /** * Initialize configuration from local .azure-devops.json files */ private initializeConfiguration(): void { try { // Try loading local configuration first this.currentConfig = 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 = ConfigLoader.loadConfig(); this.directoryDetector = new 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 DirectoryDetector([]); } } /** * Setup MCP server handlers */ private setupHandlers(): void { // List available tools this.server.setRequestHandler(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(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 */ private async updateCurrentContext(): Promise<void> { // 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 */ private handleGetCurrentContext(args?: any): any { 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(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Azure DevOps MCP Proxy Server started'); } } 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); }); } export { main, AzureDevOpsMCPProxy };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wangkanai/devops-enhanced-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server