Skip to main content
Glama

Azure DevOps MCP Server

feature.ts8.38 kB
import { WebApi } from 'azure-devops-node-api'; import axios from 'axios'; import { DefaultAzureCredential, AzureCliCredential } from '@azure/identity'; import { AzureDevOpsError, AzureDevOpsResourceNotFoundError, AzureDevOpsValidationError, AzureDevOpsPermissionError, AzureDevOpsAuthenticationError, } from '../../../shared/errors'; import { SearchCodeOptions, CodeSearchRequest, CodeSearchResponse, CodeSearchResult, } from '../types'; import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces'; /** * Search for code in Azure DevOps repositories * * @param connection The Azure DevOps WebApi connection * @param options Parameters for searching code * @returns Search results with optional file content */ export async function searchCode( connection: WebApi, options: SearchCodeOptions, ): Promise<CodeSearchResponse> { try { // When includeContent is true, limit results to prevent timeouts const top = options.includeContent ? Math.min(options.top || 10, 10) : options.top; // Get the project ID (either provided or default) const projectId = options.projectId || process.env.AZURE_DEVOPS_DEFAULT_PROJECT; if (!projectId) { throw new AzureDevOpsValidationError( 'Project ID is required. Either provide a projectId or set the AZURE_DEVOPS_DEFAULT_PROJECT environment variable.', ); } // Prepare the search request const searchRequest: CodeSearchRequest = { searchText: options.searchText, $skip: options.skip, $top: top, // Use limited top value when includeContent is true filters: { Project: [projectId], ...(options.filters || {}), }, includeFacets: true, includeSnippet: options.includeSnippet, }; // Get the authorization header from the connection const authHeader = await getAuthorizationHeader(); // Extract organization from the connection URL const { organization } = extractOrgFromUrl(connection); // Make the search API request with the project ID const searchUrl = `https://almsearch.dev.azure.com/${organization}/${projectId}/_apis/search/codesearchresults?api-version=7.1`; const searchResponse = await axios.post<CodeSearchResponse>( searchUrl, searchRequest, { headers: { Authorization: authHeader, 'Content-Type': 'application/json', }, }, ); const results = searchResponse.data; // If includeContent is true, fetch the content for each result if (options.includeContent && results.results.length > 0) { await enrichResultsWithContent(connection, results.results); } return results; } catch (error) { if (error instanceof AzureDevOpsError) { throw error; } if (axios.isAxiosError(error)) { const status = error.response?.status; if (status === 404) { throw new AzureDevOpsResourceNotFoundError( 'Repository or project not found', { cause: error }, ); } if (status === 400) { throw new AzureDevOpsValidationError( 'Invalid search parameters', error.response?.data, { cause: error }, ); } if (status === 401) { throw new AzureDevOpsAuthenticationError('Authentication failed', { cause: error, }); } if (status === 403) { throw new AzureDevOpsPermissionError( 'Permission denied to access repository', { cause: error }, ); } } throw new AzureDevOpsError('Failed to search code', { cause: error }); } } /** * Extract organization from the connection URL * * @param connection The Azure DevOps WebApi connection * @returns The organization */ function extractOrgFromUrl(connection: WebApi): { organization: string } { // Extract organization from the connection URL const url = connection.serverUrl; const match = url.match(/https?:\/\/dev\.azure\.com\/([^/]+)/); const organization = match ? match[1] : ''; if (!organization) { throw new AzureDevOpsValidationError( 'Could not extract organization from connection URL', ); } return { organization, }; } /** * Get the authorization header from the connection * * @returns The authorization header */ async function getAuthorizationHeader(): Promise<string> { try { // For PAT authentication, we can construct the header directly if ( process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'pat' && process.env.AZURE_DEVOPS_PAT ) { // For PAT auth, we can construct the Basic auth header directly const token = process.env.AZURE_DEVOPS_PAT; const base64Token = Buffer.from(`:${token}`).toString('base64'); return `Basic ${base64Token}`; } // For Azure Identity / Azure CLI auth, we need to get a token // using the Azure DevOps resource ID // Choose the appropriate credential based on auth method const credential = process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'azure-cli' ? new AzureCliCredential() : new DefaultAzureCredential(); // Azure DevOps resource ID for token acquisition const AZURE_DEVOPS_RESOURCE_ID = '499b84ac-1321-427f-aa17-267ca6975798'; // Get token for Azure DevOps const token = await credential.getToken( `${AZURE_DEVOPS_RESOURCE_ID}/.default`, ); if (!token || !token.token) { throw new Error('Failed to acquire token for Azure DevOps'); } return `Bearer ${token.token}`; } catch (error) { throw new AzureDevOpsValidationError( `Failed to get authorization header: ${error instanceof Error ? error.message : String(error)}`, ); } } /** * Enrich search results with file content * * @param connection The Azure DevOps WebApi connection * @param results The search results to enrich */ async function enrichResultsWithContent( connection: WebApi, results: CodeSearchResult[], ): Promise<void> { try { const gitApi = await connection.getGitApi(); // Process each result in parallel await Promise.all( results.map(async (result) => { try { // Get the file content using the Git API // Pass only the required parameters to avoid the "path" and "scopePath" conflict const contentStream = await gitApi.getItemContent( result.repository.id, result.path, result.project.name, undefined, // No version descriptor object undefined, // No recursion level undefined, // Don't include content metadata undefined, // No latest processed change false, // Don't download { version: result.versions[0]?.changeId, versionType: GitVersionType.Commit, }, // Version descriptor true, // Include content ); // Convert the stream to a string and store it in the result if (contentStream) { // Since getItemContent always returns NodeJS.ReadableStream, we need to read the stream const chunks: Buffer[] = []; // Listen for data events to collect chunks contentStream.on('data', (chunk) => { chunks.push(Buffer.from(chunk)); }); // Use a promise to wait for the stream to finish result.content = await new Promise<string>((resolve, reject) => { contentStream.on('end', () => { // Concatenate all chunks and convert to string const buffer = Buffer.concat(chunks); resolve(buffer.toString('utf8')); }); contentStream.on('error', (err) => { reject(err); }); }); } } catch (error) { // Log the error but don't fail the entire operation console.error( `Failed to fetch content for ${result.path}: ${error instanceof Error ? error.message : String(error)}`, ); } }), ); } catch (error) { // Log the error but don't fail the entire operation console.error( `Failed to enrich results with content: ${error instanceof Error ? error.message : String(error)}`, ); } }

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/Tiberriver256/mcp-server-azure-devops'

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