Azure DevOps MCP Server

  • src
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import * as azdev from 'azure-devops-node-api'; import { AzureDevOpsConfig } from './types/config'; import { VERSION } from './config/version'; /** * Azure DevOps MCP Server * * Implements a Model Context Protocol server for Azure DevOps */ export class AzureDevOpsServer { private server: McpServer; private config: AzureDevOpsConfig; private connection: azdev.WebApi | null = null; private serverInfo: { name: string, version: string }; /** * Create a new Azure DevOps MCP Server * * @param config The Azure DevOps configuration */ constructor(config: AzureDevOpsConfig) { this.validateConfig(config); this.config = config; this.serverInfo = { name: "azure-devops-mcp", version: VERSION }; // Initialize the MCP server this.server = new McpServer({ name: this.serverInfo.name, version: this.serverInfo.version }); // Initialize the Azure DevOps connection this.initializeConnection(); // Register the tools this.registerTools(); } /** * Validate the Azure DevOps configuration * * @param config Configuration to validate * @throws Error if configuration is invalid */ private validateConfig(config: AzureDevOpsConfig): void { // Validate organization URL if (!config.organizationUrl) { throw new Error('Organization URL is required'); } try { const url = new URL(config.organizationUrl); if (!url.hostname.endsWith('azure.com') && !url.hostname.endsWith('visualstudio.com')) { throw new Error('Invalid organization URL domain'); } } catch (error: any) { throw new Error(`Invalid organization URL: ${error.message}`); } // Validate PAT if (!config.personalAccessToken) { throw new Error('Personal Access Token is required'); } if (config.personalAccessToken.length < 32) { throw new Error('Personal Access Token appears to be too short'); } // Validate API version if provided if (config.apiVersion) { const apiVersionPattern = /^\d+\.\d+(-preview(\.\d+)?)?$/; if (!apiVersionPattern.test(config.apiVersion)) { throw new Error('Invalid API version format. Expected format: major.minor or major.minor-preview.revision'); } } } /** * Initialize the Azure DevOps API connection */ private initializeConnection(): void { const authHandler = azdev.getPersonalAccessTokenHandler(this.config.personalAccessToken); this.connection = new azdev.WebApi(this.config.organizationUrl, authHandler); } /** * Register all tools with the MCP server */ private registerTools(): void { this.registerProjectTools(); this.registerWorkItemTools(); this.registerRepositoryTools(); } /** * Register project-related tools */ private registerProjectTools(): void { // List projects tool this.server.tool( "list_projects", {}, async (_args, _extras) => { // Implementation will be added later return { content: [ { type: "text", text: "Project list will be displayed here" } ] }; } ); // Get project details tool this.server.tool( "get_project", { projectId: z.string().describe("The ID or name of the project") }, async (args, _extras) => { // Implementation will be added later return { content: [ { type: "text", text: `Project details for ${args.projectId} will be displayed here` } ] }; } ); } /** * Register work item-related tools */ private registerWorkItemTools(): void { // Get work item tool this.server.tool( "get_work_item", { workItemId: z.number().describe("The ID of the work item"), project: z.string().optional().describe("The project containing the work item") }, async (args, _extras) => { // Implementation will be added later return { content: [ { type: "text", text: `Work item ${args.workItemId} details will be displayed here` } ] }; } ); // Create work item tool this.server.tool( "create_work_item", { project: z.string().describe("The project to create the work item in"), title: z.string().describe("The title of the work item"), description: z.string().optional().describe("The description of the work item"), workItemType: z.string().describe("The type of work item (e.g., Bug, Task, User Story)"), assignedTo: z.string().optional().describe("The user to assign the work item to") }, async (args, _extras) => { // Implementation will be added later return { content: [ { type: "text", text: `Work item created with title: ${args.title}` } ] }; } ); } /** * Register repository-related tools */ private registerRepositoryTools(): void { // List repositories tool this.server.tool( "list_repositories", { project: z.string().optional().describe("The project to list repositories from") }, async (args, _extras) => { // Implementation will be added later return { content: [ { type: "text", text: `Repositories for project ${args.project || 'default'} will be displayed here` } ] }; } ); // Get repository details tool this.server.tool( "get_repository", { repositoryId: z.string().describe("The ID or name of the repository"), project: z.string().optional().describe("The project containing the repository") }, async (args, _extras) => { // Implementation will be added later return { content: [ { type: "text", text: `Repository ${args.repositoryId} details will be displayed here` } ] }; } ); } /** * Test the connection to Azure DevOps * * @returns A promise that resolves to true if the connection is successful */ public async testConnection(): Promise<boolean> { try { // Try to get the location service to verify the connection if (!this.connection) { console.error('Connection test failed: No connection initialized'); return false; } console.log('Testing connection to:', this.config.organizationUrl); const locationApi = await this.connection.getLocationsApi(); console.log('Successfully got location API, attempting to get resource areas...'); const areas = await locationApi.getResourceAreas(); console.log('Successfully retrieved resource areas:', areas.length); return true; } catch (error: any) { console.error('Connection test failed:', error); if ('statusCode' in error) { console.error('Status Code:', error.statusCode); console.error('Response Headers:', JSON.stringify(error.responseHeaders, null, 2)); } if ('message' in error) { console.error('Error Message:', error.message); } return false; } } /** * Get the name of the server * * @returns The server name */ public getName(): string { return this.serverInfo.name; } /** * Get the version of the server * * @returns The server version */ public getVersion(): string { return this.serverInfo.version; } /** * Get all registered tools * * @returns Array of registered tools */ public getTools(): Array<{name: string; description: string}> { // This is a mock implementation for testing purposes // In a real implementation, we would get this information from the MCP server return [ { name: 'get_project', description: 'Get project details' }, { name: 'list_projects', description: 'List all projects' }, { name: 'get_work_item', description: 'Get work item details' }, { name: 'create_work_item', description: 'Create a new work item' }, { name: 'get_repository', description: 'Get repository details' }, { name: 'list_repositories', description: 'List all repositories' } ]; } /** * Connect to a transport * * @param transport The transport to connect to * @returns A promise that resolves when the connection is established */ public async connect(transport: any): Promise<void> { // Start the transport transport.start(); // Set up message handling transport.onMessage(async (message: any) => { // Log that we received a message console.log('Received message:', message); }); return Promise.resolve(); } }