Skip to main content
Glama
bruno-parser.ts20.5 kB
import fs from "fs-extra"; import * as path from "path"; import axios from "axios"; import debug from "debug"; import { bruToJson, envToJson, collectionBruToJson, } from "./bruno-lang/brulang.js"; import { applyAuthToParsedRequest } from "./auth/index.js"; const log = debug("bruno-parser"); const debugReq = debug("bruno-request"); // Match {{baseUrl}} or any other template variable {{varName}} const TEMPLATE_VAR_REGEX = /{{([^}]+)}}/g; interface BrunoResponse { status: number; headers: any; data: any; isJson?: boolean; error?: boolean; } export interface ParsedRequest { name: string; method: string; url: string; rawRequest: any; headers: Record<string, string>; queryParams: Record<string, string>; body?: { type: string; content: any; }; filePath?: string; } export interface EnvironmentData { name: string; variables: Record<string, string>; rawData: any; } export class BrunoParser { collectionPath: string; basePath: string; envVars: Record<string, string> = {}; environment?: string; availableEnvironments: Map<string, EnvironmentData> = new Map(); parsedRequests: Map<string, any> = new Map(); parsedCollection: any = null; constructor(collectionPath: string, environment?: string) { this.collectionPath = collectionPath; this.basePath = path.dirname(collectionPath); this.environment = environment; } async init() { // Check if the collection path exists try { await fs.access(this.collectionPath); } catch (error: unknown) { throw new Error(`Collection path does not exist: ${this.collectionPath}`); } try { // Load all available environments await this.loadAllEnvironments(); // Load the collection try { this.parsedCollection = await this.parseCollection(); } catch (error) { log(`Error parsing collection: ${error}`); this.parsedCollection = { meta: { name: "collection", type: "collection" }, }; } // Load all request files await this.loadAllRequests(); // Set the active environment if specified if (this.environment) { this.setEnvironment(this.environment); } } catch (error: unknown) { log(`Error during parser initialization: ${error}`); throw error; } } async loadAllEnvironments() { const envPath = path.join(this.basePath, "environments"); try { // Check if the environments directory exists if (await fs.pathExists(envPath)) { const files = await fs.readdir(envPath); const envFiles = files.filter( (file) => file.endsWith(".env") || file.endsWith(".bru") ); // Load all environment files for (const envFile of envFiles) { const envName = path.basename( envFile, envFile.endsWith(".bru") ? ".bru" : ".env" ); const envFilePath = path.join(envPath, envFile); const envContent = await fs.readFile(envFilePath, "utf-8"); try { const envData = envToJson(envContent); const variables: Record<string, string> = {}; // Extract variables to our simplified format if (envData) { if (envData.vars) { // Legacy .env format Object.entries(envData.vars).forEach(([name, value]) => { variables[name] = String(value); }); } else if (envData.variables) { // New .bru format envData.variables.forEach((variable: any) => { if (variable.enabled && variable.name) { variables[variable.name] = variable.value || ""; } }); } } // Store the environment data this.availableEnvironments.set(envName, { name: envName, variables, rawData: envData, }); log(`Environment loaded: ${envName}`); // If this is the first environment and no specific one was requested, // set it as the default if (!this.environment && this.availableEnvironments.size === 1) { this.environment = envName; this.envVars = { ...variables }; log(`Set default environment: ${envName}`); } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); log( `Error parsing environment file ${envFilePath}: ${errorMessage}` ); } } log( "Available environments:", Array.from(this.availableEnvironments.keys()) ); log("Current environment variables:", this.envVars); } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); log(`Error loading environments: ${errorMessage}`); } } setEnvironment(envName: string): boolean { const env = this.availableEnvironments.get(envName); if (env) { this.environment = envName; this.envVars = { ...env.variables }; log(`Environment set to: ${envName}`); return true; } log(`Environment not found: ${envName}`); return false; } getAvailableEnvironments(): string[] { return Array.from(this.availableEnvironments.keys()); } getEnvironment(envName: string): EnvironmentData | undefined { return this.availableEnvironments.get(envName); } getCurrentEnvironment(): EnvironmentData | undefined { return this.environment ? this.availableEnvironments.get(this.environment) : undefined; } async loadAllRequests() { try { log(`Loading request files from ${this.basePath}`); const files = await fs.readdir(this.basePath); log(`Found ${files.length} files in directory:`, files); const requestFiles = files.filter( (file) => file.endsWith(".bru") && file !== path.basename(this.collectionPath) && !file.includes("env") ); log(`Filtered request files: ${requestFiles.length}`, requestFiles); for (const file of requestFiles) { const requestPath = path.join(this.basePath, file); try { log(`Loading request from ${requestPath}`); const content = await fs.readFile(requestPath, "utf-8"); const parsed = bruToJson(content); const requestName = path.basename(file, ".bru"); this.parsedRequests.set(requestName, parsed); log(`Request loaded: ${requestName}`); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); log(`Error parsing request file ${file}: ${errorMessage}`); } } log(`Loaded ${this.parsedRequests.size} requests`); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); log(`Error loading request files: ${errorMessage}`); } } getAvailableRequests(): string[] { return Array.from(this.parsedRequests.keys()); } getRawRequest(requestName: string): any | undefined { return this.parsedRequests.get(requestName); } async parseCollection(): Promise<any> { try { const content = await fs.readFile(this.collectionPath, "utf-8"); return collectionBruToJson(content); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); log(`Error parsing collection file: ${errorMessage}`); throw error; } } getCollection(): any { return this.parsedCollection; } async parseRequest(requestInput: string): Promise<ParsedRequest> { let rawRequest; let requestName; let filePath = requestInput; // If the input is a name and not a path, get the request from loaded requests if (!requestInput.includes(path.sep) && !requestInput.endsWith(".bru")) { requestName = requestInput; rawRequest = this.getRawRequest(requestName); if (!rawRequest) { throw new Error(`Request not found: ${requestName}`); } } else { // Input is a file path requestName = path.basename(requestInput, ".bru"); try { const content = await fs.readFile(filePath, "utf-8"); rawRequest = bruToJson(content); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error( `Error parsing request file ${filePath}: ${errorMessage}` ); } } // Extract HTTP method and URL let method = "GET"; let url = ""; if (rawRequest.http && rawRequest.http.method) { method = rawRequest.http.method.toUpperCase(); } if (rawRequest.http && rawRequest.http.url) { // Store the original URL without processing variables url = rawRequest.http.url; } // Parse headers const headers: Record<string, string> = {}; // Handle auth inheritance if ( rawRequest.http && rawRequest.http.auth === "inherit" && this.parsedCollection ) { const collectionAuth = this.parsedCollection.auth; if (collectionAuth && collectionAuth.mode === "apikey") { const apiKeyAuth = collectionAuth.apikey; if ( apiKeyAuth && (!apiKeyAuth.addTo || apiKeyAuth.addTo === "header") ) { headers[apiKeyAuth.key] = this.processTemplateVariables( apiKeyAuth.value || "" ); } } } // Parse request-specific headers from headers section if (rawRequest.headers) { for (const header of rawRequest.headers) { if (header.enabled !== false && header.name) { headers[header.name] = this.processTemplateVariables( header.value || "" ); } } } // Parse request-specific headers from http.headers (for backward compatibility) if (rawRequest.http && rawRequest.http.headers) { for (const header of rawRequest.http.headers) { if (header.enabled !== false && header.name) { headers[header.name] = this.processTemplateVariables( header.value || "" ); } } } // Parse query parameters const queryParams: Record<string, string> = {}; // Parse from params:query section (new format) if (rawRequest.params) { // Check if params is an array (from paramsquery handler) if (Array.isArray(rawRequest.params)) { // Find query parameters in params array const queryParamsArray = rawRequest.params.filter( (param: any) => param.type === "query" ); for (const param of queryParamsArray) { if (param.enabled !== false && param.name) { queryParams[param.name] = this.processTemplateVariables( param.value || "" ); } } } else if (rawRequest.params.query) { // Handle legacy structure if (Array.isArray(rawRequest.params.query)) { for (const param of rawRequest.params.query) { if (param.enabled !== false && param.name) { queryParams[param.name] = this.processTemplateVariables( param.value || "" ); } } } else if (typeof rawRequest.params.query === "object") { Object.entries(rawRequest.params.query).forEach(([name, value]) => { queryParams[name] = this.processTemplateVariables(String(value)); }); } } } // Parse from http.query section (backward compatibility) if (rawRequest.http && rawRequest.http.query) { for (const param of rawRequest.http.query) { if (param.enabled !== false && param.name) { queryParams[param.name] = this.processTemplateVariables( param.value || "" ); } } } // Handle query parameter auth if ( rawRequest.http && rawRequest.http.auth === "inherit" && this.parsedCollection ) { const collectionAuth = this.parsedCollection.auth; if (collectionAuth && collectionAuth.mode === "apikey") { const apiKeyAuth = collectionAuth.apikey; if (apiKeyAuth && apiKeyAuth.addTo === "queryParams") { queryParams[apiKeyAuth.key] = this.processTemplateVariables( apiKeyAuth.value || "" ); log( `Added auth query param: ${apiKeyAuth.key}=${ queryParams[apiKeyAuth.key] }` ); } } } // Parse body content let body; if (rawRequest.http && rawRequest.http.body) { const bodyContent = rawRequest.http.body; const bodyMode = bodyContent.mode || "json"; // Process body content based on mode if (bodyMode === "json" && bodyContent.json) { try { // If it's a string, try to parse it as JSON let processedContent = this.processTemplateVariables( bodyContent.json ); let jsonContent; try { jsonContent = JSON.parse(processedContent); } catch (e) { // If not valid JSON, use as is jsonContent = processedContent; } body = { type: "json", content: jsonContent, }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); log(`Error processing JSON body: ${errorMessage}`); body = { type: "json", content: bodyContent.json, }; } } else if (bodyMode === "text" && bodyContent.text) { body = { type: "text", content: this.processTemplateVariables(bodyContent.text), }; } else if (bodyMode === "form-urlencoded" && bodyContent.formUrlEncoded) { const formData: Record<string, string> = {}; for (const param of bodyContent.formUrlEncoded) { if (param.enabled !== false && param.name) { formData[param.name] = this.processTemplateVariables( param.value || "" ); } } body = { type: "form-urlencoded", content: formData, }; } else { // For other body types, store as is body = { type: bodyMode, content: bodyContent[bodyMode], }; } } return { name: requestName, method, url, rawRequest, headers, queryParams, body, filePath, }; } processTemplateVariables(input: string): string { if (!input || typeof input !== "string") { return input; } return input.replace( TEMPLATE_VAR_REGEX, (match: string, varName: string) => { const trimmedVarName = varName.trim(); return this.envVars[trimmedVarName] !== undefined ? this.envVars[trimmedVarName] : match; } ); } extractTemplateVariables(input: string): string[] { if (!input || typeof input !== "string") { return []; } const variables: string[] = []; let match; while ((match = TEMPLATE_VAR_REGEX.exec(input)) !== null) { variables.push(match[1].trim()); } return variables; } async executeRequest( parsedRequest: ParsedRequest, params: { variables?: Record<string, any>; query?: Record<string, string>; body?: any; } = {} ): Promise<BrunoResponse> { // Create a temporary copy of environment variables const originalEnvVars = { ...this.envVars }; console.log("originalEnvVars", originalEnvVars); try { const { method, body, queryParams, rawRequest } = parsedRequest; const { variables, query, ...requestParams } = params; // Apply any custom variables if provided if (variables && typeof variables === "object") { debugReq(`Applying temporary variables: ${JSON.stringify(variables)}`); // Temporarily override environment variables Object.entries(variables).forEach(([key, value]) => { this.envVars[key] = String(value); // If a variable matches a query parameter name, update the query parameter as well if (Object.prototype.hasOwnProperty.call(queryParams, key)) { queryParams[key] = String(value); } }); } // Get the original URL from rawRequest instead of using the pre-processed URL const originalUrl = rawRequest?.http?.url || parsedRequest.url; // Process template variables in the URL with current environment variables let finalUrl = this.processTemplateVariables(originalUrl); debugReq(`Final URL: ${finalUrl}`); // Add query parameters that are not already in the URL const urlObj = new URL(finalUrl); // Apply authentication using our new auth module const authResult = applyAuthToParsedRequest( rawRequest, this.parsedCollection, this.envVars ); // Merge any headers from auth with existing headers from parsedRequest const headers = { ...parsedRequest.headers, ...authResult.headers, }; // Apply parameters to query parameters if (queryParams) { Object.entries(requestParams).forEach(([key, value]) => { if (Object.prototype.hasOwnProperty.call(queryParams, key)) { queryParams[key] = String(value); } }); } // Add dedicated query parameters if provided if (query && typeof query === "object") { debugReq( `Applying dedicated query parameters: ${JSON.stringify(query)}` ); Object.entries(query).forEach(([key, value]) => { queryParams[key] = String(value); }); } // Add all query parameters to URL, including those from auth // First add existing query params from the request Object.entries(queryParams).forEach(([key, value]) => { urlObj.searchParams.set(key, value); }); // Then add auth query params if any if (authResult.queryParams) { Object.entries(authResult.queryParams).forEach(([key, value]) => { urlObj.searchParams.set(key, value); }); } finalUrl = urlObj.toString(); // Process body content with parameters if it's JSON let requestData = params.body; debugReq(`Executing ${method} request to ${finalUrl}`); debugReq(`Headers: ${JSON.stringify(headers)}`); if (requestData) { debugReq( `Body: ${ typeof requestData === "object" ? JSON.stringify(requestData) : requestData }` ); } // Send the request const response = await axios({ method, url: finalUrl, headers, data: requestData, validateStatus: () => true, // Don't throw on any status code }); // Log response status debugReq(`Response status: ${response.status}`); // Check if the response is JSON by examining the content-type header const contentType = response.headers["content-type"] || ""; const isJson = contentType.includes("application/json"); if (!isJson) { debugReq( `Warning: Response is not JSON (content-type: ${contentType})` ); } console.log("response.data", response.data); // Return structured response return { status: response.status, headers: response.headers, data: response.data, isJson, }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); debugReq(`Error executing request: ${errorMessage}`); return { status: 0, headers: {}, data: errorMessage, error: true, }; } finally { // Restore original environment variables this.envVars = originalEnvVars; } } hasTemplateVariable(url: string, varName: string): boolean { const templateVars = this.extractTemplateVariables(url); return templateVars.includes(varName); } }

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

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