Skip to main content
Glama
CollectionDiscoveryService.ts7.69 kB
/** * Service for discovering and listing Bruno collections and requests * Handles collection discovery, request listing, and file searching */ import * as fs from 'fs/promises'; import * as path from 'path'; import type { BrunoRequest } from '../bruno-cli.js'; import type { PerformanceManager } from '../performance.js'; // Re-export BrunoRequest for convenience export type { BrunoRequest }; /** * Service responsible for discovering collections and listing requests * Single Responsibility: Collection and request discovery operations */ export class CollectionDiscoveryService { constructor(private readonly performanceManager: PerformanceManager) {} /** * List all requests in a collection */ async listRequests(collectionPath: string): Promise<BrunoRequest[]> { try { // Check cache first const cached = this.performanceManager.getCachedRequestList(collectionPath); if (cached) { console.error(`Using cached request list for: ${collectionPath}`); return cached; } // Check if the collection path exists const stats = await fs.stat(collectionPath); if (!stats.isDirectory()) { throw new Error(`Collection path is not a directory: ${collectionPath}`); } // Check if it's a valid Bruno collection (should have bruno.json or collection.bru) const hasCollectionFile = await this.hasCollectionFile(collectionPath); if (!hasCollectionFile) { throw new Error(`Not a valid Bruno collection: ${collectionPath}`); } // Find all .bru files in the collection const requests = await this.findBrunoRequests(collectionPath); // Cache the results this.performanceManager.cacheRequestList(collectionPath, requests); return requests; } catch (error) { if ((error as any).code === 'ENOENT') { throw new Error(`Collection not found: ${collectionPath}`); } throw error; } } /** * Discover Bruno collections in a directory tree */ async discoverCollections(searchPath: string, maxDepth: number = 5): Promise<string[]> { // Check cache first const cached = this.performanceManager.getCachedCollectionDiscovery(searchPath); if (cached) { return cached; } const collections: string[] = []; const maxSearchDepth = Math.min(maxDepth, 10); // Cap at 10 for safety const searchDirectory = async (dirPath: string, currentDepth: number): Promise<void> => { if (currentDepth > maxSearchDepth) { return; } try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); // Check if this directory contains bruno.json const hasBrunoJson = entries.some(entry => entry.isFile() && entry.name === 'bruno.json' ); if (hasBrunoJson) { collections.push(dirPath); // Don't search subdirectories of a collection return; } // Recursively search subdirectories for (const entry of entries) { if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') { const subPath = path.join(dirPath, entry.name); await searchDirectory(subPath, currentDepth + 1); } } } catch (error) { // Silently skip directories we can't read console.error(`Cannot read directory: ${dirPath}`, error); } }; await searchDirectory(searchPath, 0); // Cache the results this.performanceManager.cacheCollectionDiscovery(searchPath, collections); return collections; } /** * Find a specific request file by name */ async findRequestFile(collectionPath: string, requestName: string): Promise<string | null> { const requests = await this.findBrunoRequests(collectionPath); // Try exact match first let request = requests.find(r => r.name === requestName); // Try case-insensitive match if (!request) { request = requests.find(r => r.name.toLowerCase() === requestName.toLowerCase()); } // Try partial match if (!request) { request = requests.find(r => r.name.includes(requestName)); } return request?.path || null; } /** * Check if directory has a Bruno collection file */ private async hasCollectionFile(dirPath: string): Promise<boolean> { try { // Check for bruno.json or collection.bru const brunoJsonPath = path.join(dirPath, 'bruno.json'); const collectionBruPath = path.join(dirPath, 'collection.bru'); try { await fs.access(brunoJsonPath); return true; } catch { // Try collection.bru try { await fs.access(collectionBruPath); return true; } catch { return false; } } } catch { return false; } } /** * Recursively find all Bruno request files */ private async findBrunoRequests( dirPath: string, basePath: string = dirPath, requests: BrunoRequest[] = [] ): Promise<BrunoRequest[]> { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory() && !entry.name.startsWith('.')) { // Skip node_modules and other common non-request directories if (entry.name === 'node_modules' || entry.name === 'environments') { continue; } // Recursively search subdirectories await this.findBrunoRequests(fullPath, basePath, requests); } else if (entry.isFile() && entry.name.endsWith('.bru')) { // Skip collection.bru as it's not a request if (entry.name === 'collection.bru') { continue; } // Parse the .bru file for basic info const requestInfo = await this.parseBrunoFile(fullPath); const relativePath = path.relative(basePath, dirPath); requests.push({ name: path.basename(entry.name, '.bru'), folder: relativePath || undefined, path: fullPath, ...requestInfo }); } } return requests; } /** * Parse a Bruno request file for basic information */ private async parseBrunoFile(filePath: string): Promise<Partial<BrunoRequest>> { try { const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); const info: Partial<BrunoRequest> = {}; // Look for meta name first let inMeta = false; for (const line of lines) { if (line.trim() === 'meta {') { inMeta = true; continue; } if (inMeta && line.trim() === '}') { inMeta = false; continue; } if (inMeta) { const nameMatch = line.match(/^\s*name:\s*(.+)/); if (nameMatch) { info.name = nameMatch[1].trim(); } } // Look for method and URL const methodMatch = line.match(/^(get|post|put|delete|patch|head|options)\s*\{/i); if (methodMatch) { info.method = methodMatch[1].toUpperCase(); // Look for URL in the next few lines const urlIndex = lines.indexOf(line); for (let i = urlIndex + 1; i < Math.min(urlIndex + 5, lines.length); i++) { const urlMatch = lines[i].match(/^\s*url:\s*(.+)/); if (urlMatch) { info.url = urlMatch[1].trim(); break; } } } } return info; } catch { // If we can't parse the file, just return empty info return {}; } } }

Latest Blog Posts

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/jcr82/bruno-mcp-server'

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