Skip to main content
Glama

Bruno MCP Server

by macarthy
request.ts•13.5 kB
/** * Bruno request builder * Handles creation and management of .bru request files */ import { promises as fs } from 'fs'; import { join, dirname } from 'path'; import { BruFile, CreateRequestInput, FileOperationResult, BrunoError, BruFileError, HttpMethod, AuthType, BodyType } from './types.js'; import { generateBruFile } from './generator.js'; export class RequestBuilder { /** * Create a new .bru request file */ async createRequest(input: CreateRequestInput): Promise<FileOperationResult> { try { // Validate input this.validateRequestInput(input); // Build BRU file structure const bruFile = this.buildBruFile(input); // Determine file path const filePath = this.getRequestFilePath(input); // Ensure directory exists await this.ensureDirectory(dirname(filePath)); // Generate and write BRU file const bruContent = generateBruFile(bruFile); await fs.writeFile(filePath, bruContent); return { success: true, path: filePath }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } /** * Load an existing .bru request file */ async loadRequest(filePath: string): Promise<BruFile> { try { const content = await fs.readFile(filePath, 'utf-8'); return this.parseBruFile(content); } catch (error) { throw new BruFileError( `Failed to load request from ${filePath}`, { originalError: error } ); } } /** * Update an existing request */ async updateRequest(filePath: string, updates: Partial<CreateRequestInput>): Promise<FileOperationResult> { try { // Load existing request const existingBru = await this.loadRequest(filePath); // Apply updates const updatedBru = this.applyUpdates(existingBru, updates); // Generate and write updated content const bruContent = generateBruFile(updatedBru); await fs.writeFile(filePath, bruContent); return { success: true, path: filePath }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } /** * Create multiple related requests (CRUD operations) */ async createCrudRequests( collectionPath: string, entityName: string, baseUrl: string, folder?: string ): Promise<FileOperationResult[]> { const results: FileOperationResult[] = []; const crudOperations = [ { name: `Get All ${entityName}`, method: 'GET' as HttpMethod, url: `${baseUrl}/${entityName.toLowerCase()}`, sequence: 1 }, { name: `Get ${entityName} by ID`, method: 'GET' as HttpMethod, url: `${baseUrl}/${entityName.toLowerCase()}/{{id}}`, sequence: 2 }, { name: `Create ${entityName}`, method: 'POST' as HttpMethod, url: `${baseUrl}/${entityName.toLowerCase()}`, body: { type: 'json' as BodyType, content: JSON.stringify({ name: `New ${entityName}`, description: `Description for ${entityName}` }, null, 2) }, headers: { 'Content-Type': 'application/json' }, sequence: 3 }, { name: `Update ${entityName}`, method: 'PUT' as HttpMethod, url: `${baseUrl}/${entityName.toLowerCase()}/{{id}}`, body: { type: 'json' as BodyType, content: JSON.stringify({ name: `Updated ${entityName}`, description: `Updated description for ${entityName}` }, null, 2) }, headers: { 'Content-Type': 'application/json' }, sequence: 4 }, { name: `Delete ${entityName}`, method: 'DELETE' as HttpMethod, url: `${baseUrl}/${entityName.toLowerCase()}/{{id}}`, sequence: 5 } ]; for (const operation of crudOperations) { const result = await this.createRequest({ collectionPath, ...operation, folder }); results.push(result); } return results; } /** * Create authentication test requests */ async createAuthRequests( collectionPath: string, baseUrl: string, authType: AuthType, folder = 'auth' ): Promise<FileOperationResult[]> { const results: FileOperationResult[] = []; const authRequests = [ { name: 'Login', method: 'POST' as HttpMethod, url: `${baseUrl}/auth/login`, body: { type: 'json' as BodyType, content: JSON.stringify({ username: '{{username}}', password: '{{password}}' }, null, 2) }, headers: { 'Content-Type': 'application/json' }, sequence: 1 }, { name: 'Get Profile', method: 'GET' as HttpMethod, url: `${baseUrl}/auth/profile`, auth: authType !== 'none' ? { type: authType, config: authType === 'bearer' ? { token: '{{token}}' } : { username: '{{username}}', password: '{{password}}' } } as { type: AuthType; config: Record<string, string> } : undefined, sequence: 2 }, { name: 'Refresh Token', method: 'POST' as HttpMethod, url: `${baseUrl}/auth/refresh`, body: { type: 'json' as BodyType, content: JSON.stringify({ refreshToken: '{{refreshToken}}' }, null, 2) }, headers: { 'Content-Type': 'application/json' }, sequence: 3 }, { name: 'Logout', method: 'POST' as HttpMethod, url: `${baseUrl}/auth/logout`, auth: authType !== 'none' ? { type: authType, config: authType === 'bearer' ? { token: '{{token}}' } : { username: '{{username}}', password: '{{password}}' } } as { type: AuthType; config: Record<string, string> } : undefined, sequence: 4 } ]; for (const authRequest of authRequests) { const result = await this.createRequest({ collectionPath, ...authRequest, folder }); results.push(result); } return results; } /** * Build BRU file structure from input */ private buildBruFile(input: CreateRequestInput): BruFile { const bruFile: BruFile = { meta: { name: input.name, type: 'http', seq: input.sequence }, http: { method: input.method, url: input.url, body: input.body?.type || 'none', auth: input.auth?.type || 'none' } }; // Add headers if provided if (input.headers && Object.keys(input.headers).length > 0) { bruFile.headers = input.headers; } // Add query parameters if provided if (input.query && Object.keys(input.query).length > 0) { bruFile.query = input.query; } // Add body if provided if (input.body && input.body.type !== 'none') { bruFile.body = { type: input.body.type, content: input.body.content }; // Handle form data if (input.body.formData) { bruFile.body.formData = input.body.formData.map(field => ({ name: field.name, value: field.value, type: field.type || 'text', enabled: true })); } } // Add authentication if provided if (input.auth && input.auth.type !== 'none') { bruFile.auth = { type: input.auth.type }; // Configure auth based on type switch (input.auth.type) { case 'bearer': bruFile.auth.bearer = { token: input.auth.config.token || '{{token}}' }; break; case 'basic': bruFile.auth.basic = { username: input.auth.config.username || '{{username}}', password: input.auth.config.password || '{{password}}' }; break; case 'api-key': bruFile.auth.apikey = { key: input.auth.config.key || 'X-API-Key', value: input.auth.config.value || '{{apiKey}}', in: (input.auth.config.in as 'header' | 'query') || 'header' }; break; } } return bruFile; } /** * Get file path for request */ private getRequestFilePath(input: CreateRequestInput): string { const fileName = this.sanitizeFileName(input.name) + '.bru'; if (input.folder) { return join(input.collectionPath, input.folder, fileName); } return join(input.collectionPath, fileName); } /** * Sanitize file name for filesystem */ private sanitizeFileName(name: string): string { return name .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') .trim(); } /** * Parse BRU file content (basic implementation) */ private parseBruFile(content: string): BruFile { // This is a simplified parser - in a production environment, // you'd want a more robust BRU parser const bruFile: BruFile = { meta: { name: 'Parsed Request', type: 'http' }, http: { method: 'GET', url: '', body: 'none', auth: 'none' } }; // Extract meta information const metaMatch = content.match(/meta\s*\{([^}]*)\}/s); if (metaMatch) { const metaContent = metaMatch[1]; const nameMatch = metaContent.match(/name:\s*'([^']*)'|name:\s*([^\n]*)/); if (nameMatch) { bruFile.meta.name = nameMatch[1] || nameMatch[2].trim(); } } // Extract HTTP method and URL const httpMethods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options']; for (const method of httpMethods) { const methodMatch = content.match(new RegExp(`${method}\\s*\\{([^}]*)\\}`, 's')); if (methodMatch) { bruFile.http.method = method.toUpperCase() as HttpMethod; const httpContent = methodMatch[1]; const urlMatch = httpContent.match(/url:\s*'([^']*)'|url:\s*([^\n]*)/); if (urlMatch) { bruFile.http.url = urlMatch[1] || urlMatch[2].trim(); } break; } } return bruFile; } /** * Apply updates to existing BRU file */ private applyUpdates(existingBru: BruFile, updates: Partial<CreateRequestInput>): BruFile { const updated = { ...existingBru }; if (updates.name) { updated.meta.name = updates.name; } if (updates.method) { updated.http.method = updates.method; } if (updates.url) { updated.http.url = updates.url; } if (updates.headers) { updated.headers = { ...updated.headers, ...updates.headers }; } if (updates.body) { updated.http.body = updates.body.type; updated.body = { type: updates.body.type, content: updates.body.content }; } if (updates.auth) { updated.http.auth = updates.auth.type; updated.auth = { type: updates.auth.type }; } return updated; } /** * Validate request input */ private validateRequestInput(input: CreateRequestInput): void { if (!input.name || input.name.trim().length === 0) { throw new BrunoError('Request name is required', 'VALIDATION_ERROR'); } if (!input.collectionPath || input.collectionPath.trim().length === 0) { throw new BrunoError('Collection path is required', 'VALIDATION_ERROR'); } if (!input.method) { throw new BrunoError('HTTP method is required', 'VALIDATION_ERROR'); } if (!input.url || input.url.trim().length === 0) { throw new BrunoError('URL is required', 'VALIDATION_ERROR'); } // Validate HTTP method const validMethods: HttpMethod[] = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']; if (!validMethods.includes(input.method)) { throw new BrunoError( `Invalid HTTP method: ${input.method}`, 'VALIDATION_ERROR' ); } // Validate auth configuration if provided if (input.auth && input.auth.type !== 'none') { this.validateAuthConfig(input.auth.type, input.auth.config); } } /** * Validate authentication configuration */ private validateAuthConfig(authType: AuthType, config: Record<string, string>): void { switch (authType) { case 'bearer': if (!config.token) { throw new BrunoError('Bearer token is required', 'VALIDATION_ERROR'); } break; case 'basic': if (!config.username || !config.password) { throw new BrunoError('Username and password are required for basic auth', 'VALIDATION_ERROR'); } break; case 'api-key': if (!config.key || !config.value) { throw new BrunoError('Key and value are required for API key auth', 'VALIDATION_ERROR'); } break; } } /** * Ensure directory exists */ private async ensureDirectory(dirPath: string): Promise<void> { try { await fs.access(dirPath); } catch { await fs.mkdir(dirPath, { recursive: true }); } } } /** * Create a new request builder instance */ export function createRequestBuilder(): RequestBuilder { return new RequestBuilder(); }

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

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