UNPKG

@tiberriver256/mcp-server-azure-devops

Version:

Azure DevOps reference server for the Model Context Protocol (MCP)

192 lines 8.67 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.searchCode = searchCode; const axios_1 = __importDefault(require("axios")); const identity_1 = require("@azure/identity"); const errors_1 = require("../../../shared/errors"); const GitInterfaces_1 = require("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 */ async function searchCode(connection, options) { 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 errors_1.AzureDevOpsValidationError('Project ID is required. Either provide a projectId or set the AZURE_DEVOPS_DEFAULT_PROJECT environment variable.'); } // Prepare the search request const searchRequest = { 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_1.default.post(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 errors_1.AzureDevOpsError) { throw error; } if (axios_1.default.isAxiosError(error)) { const status = error.response?.status; if (status === 404) { throw new errors_1.AzureDevOpsResourceNotFoundError('Repository or project not found', { cause: error }); } if (status === 400) { throw new errors_1.AzureDevOpsValidationError('Invalid search parameters', error.response?.data, { cause: error }); } if (status === 401) { throw new errors_1.AzureDevOpsAuthenticationError('Authentication failed', { cause: error, }); } if (status === 403) { throw new errors_1.AzureDevOpsPermissionError('Permission denied to access repository', { cause: error }); } } throw new errors_1.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) { // 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 errors_1.AzureDevOpsValidationError('Could not extract organization from connection URL'); } return { organization, }; } /** * Get the authorization header from the connection * * @returns The authorization header */ async function getAuthorizationHeader() { 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 identity_1.AzureCliCredential() : new identity_1.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 errors_1.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, results) { 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: GitInterfaces_1.GitVersionType.Commit, }, // Version descriptor true); // 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 = []; // 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((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)}`); } } //# sourceMappingURL=feature.js.map