load_test
Launch concurrent HTTP requests to measure average response times, percentiles, and error rates.
Instructions
Lanza N requests concurrentes al mismo endpoint y mide tiempos promedio, percentiles y tasa de errores.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| method | Yes | HTTP method | |
| url | Yes | URL del endpoint | |
| headers | No | Headers HTTP | |
| body | No | Body del request | |
| query | No | Query parameters | |
| auth | No | Autenticación | |
| concurrent | Yes | Número de requests concurrentes a lanzar (max: 100) | |
| timeout | No | Timeout por request en ms (default: 30000) |
Implementation Reference
- src/server.ts:12-12 (registration)Import of the registerLoadTestTool function from the load-test module.
import { registerLoadTestTool } from './tools/load-test.js' - src/server.ts:70-70 (registration)Registration of the load_test tool on the MCP server via registerLoadTestTool(server, storage).
registerLoadTestTool(server, storage) - src/tools/load-test.ts:10-141 (handler)Full implementation of registerLoadTestTool: defines the load_test tool schema (method, url, headers, body, query, auth, concurrent, timeout) and handler. The handler resolves variables, executes concurrent HTTP requests via executeRequest, collects timing data, and returns a text report with percentiles (p50, p95, p99), avg/min/max, RPS, status code distribution, and error summaries.
export function registerLoadTestTool(server: McpServer, storage: Storage): void { server.tool( 'load_test', 'Lanza N requests concurrentes al mismo endpoint y mide tiempos promedio, percentiles y tasa de errores.', { method: HttpMethodSchema.describe('HTTP method'), url: z.string().describe('URL del endpoint'), headers: z.record(z.string()).optional().describe('Headers HTTP'), body: z.any().optional().describe('Body del request'), query: z.record(z.string()).optional().describe('Query parameters'), auth: AuthSchema.optional().describe('Autenticación'), concurrent: z .number() .describe('Número de requests concurrentes a lanzar (max: 100)'), timeout: z .number() .optional() .describe('Timeout por request en ms (default: 30000)'), }, async (params) => { try { const concurrentCount = Math.min(Math.max(params.concurrent, 1), 100) const variables = await storage.getActiveVariables() const resolvedUrl = resolveUrl(params.url, variables) const baseConfig: RequestConfig = { method: params.method, url: resolvedUrl, headers: params.headers, body: params.body, query: params.query, auth: params.auth, timeout: params.timeout, } const interpolated = interpolateRequest(baseConfig, variables) const startTotal = performance.now() const promises = Array.from({ length: concurrentCount }, () => executeRequest(interpolated) .then((response) => ({ status: response.status, timing: response.timing.total_ms, error: undefined as string | undefined, })) .catch((error) => ({ status: 0, timing: 0, error: error instanceof Error ? error.message : String(error), })), ) const results = await Promise.all(promises) const endTotal = performance.now() const wallTime = Math.round((endTotal - startTotal) * 100) / 100 const successful = results.filter((r) => !r.error) const failed = results.filter((r) => r.error) const timings = successful.map((r) => r.timing).sort((a, b) => a - b) const statusCounts: Record<number, number> = {} for (const r of successful) { statusCounts[r.status] = (statusCounts[r.status] ?? 0) + 1 } const avg = timings.length > 0 ? Math.round((timings.reduce((s, t) => s + t, 0) / timings.length) * 100) / 100 : 0 const min = timings.length > 0 ? timings[0] : 0 const max = timings.length > 0 ? timings[timings.length - 1] : 0 const p50 = timings.length > 0 ? timings[Math.floor(timings.length * 0.5)] : 0 const p95 = timings.length > 0 ? timings[Math.floor(timings.length * 0.95)] : 0 const p99 = timings.length > 0 ? timings[Math.floor(timings.length * 0.99)] : 0 const rps = wallTime > 0 ? Math.round((successful.length / (wallTime / 1000)) * 100) / 100 : 0 const lines: string[] = [ `📊 LOAD TEST — ${params.method} ${params.url}`, '', `Requests: ${concurrentCount} concurrentes`, `Exitosos: ${successful.length} | Fallidos: ${failed.length}`, `Tiempo total: ${wallTime}ms`, `Requests/segundo: ${rps}`, '', '⏱️ Tiempos de respuesta:', ` Min: ${min}ms`, ` Avg: ${avg}ms`, ` p50: ${p50}ms`, ` p95: ${p95}ms`, ` p99: ${p99}ms`, ` Max: ${max}ms`, ] if (Object.keys(statusCounts).length > 0) { lines.push('') lines.push('📋 Status codes:') for (const [status, count] of Object.entries(statusCounts)) { const pct = Math.round((count / concurrentCount) * 100) lines.push(` ${status}: ${count} (${pct}%)`) } } if (failed.length > 0) { lines.push('') lines.push('❌ Errores:') const errorCounts: Record<string, number> = {} for (const r of failed) { const errMsg = r.error ?? 'Unknown' errorCounts[errMsg] = (errorCounts[errMsg] ?? 0) + 1 } for (const [err, count] of Object.entries(errorCounts)) { lines.push(` ${err}: ${count}x`) } } return { content: [{ type: 'text' as const, text: lines.join('\n') }], isError: failed.length > successful.length, } } catch (error) { const message = error instanceof Error ? error.message : String(error) return { content: [{ type: 'text' as const, text: `Error: ${message}` }], isError: true, } } }, ) } - src/tools/load-test.ts:14-28 (schema)Zod schema for the load_test tool parameters: method (HttpMethodSchema), url (string), headers (optional record), body (optional any), query (optional record), auth (optional AuthSchema), concurrent (number 1-100), and timeout (optional number).
{ method: HttpMethodSchema.describe('HTTP method'), url: z.string().describe('URL del endpoint'), headers: z.record(z.string()).optional().describe('Headers HTTP'), body: z.any().optional().describe('Body del request'), query: z.record(z.string()).optional().describe('Query parameters'), auth: AuthSchema.optional().describe('Autenticación'), concurrent: z .number() .describe('Número de requests concurrentes a lanzar (max: 100)'), timeout: z .number() .optional() .describe('Timeout por request en ms (default: 30000)'), }, - src/lib/http-client.ts:57-142 (helper)executeRequest function used by load_test to perform each individual HTTP request with timeout, auth, and timing measurement.
export async function executeRequest(config: RequestConfig): Promise<RequestResponse> { const timeout = config.timeout ?? DEFAULT_TIMEOUT // Construir URL con query params const url = buildUrl(config.url, config.query) // Preparar headers — Accept: application/json por defecto let headers: Record<string, string> = { Accept: 'application/json', ...config.headers, } // Aplicar auth if (config.auth) { headers = applyAuth(headers, config.auth) } // Preparar body let body: string | undefined if (config.body !== undefined && config.body !== null) { if (typeof config.body === 'string') { body = config.body } else { body = JSON.stringify(config.body) // Solo añadir Content-Type si no está definido if (!headers['Content-Type'] && !headers['content-type']) { headers['Content-Type'] = 'application/json' } } } // AbortController para timeout const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), timeout) // Medir timing const startTime = performance.now() try { const response = await fetch(url, { method: config.method, headers, body, signal: controller.signal, }) const endTime = performance.now() const totalMs = Math.round((endTime - startTime) * 100) / 100 // Parsear response body const responseText = await response.text() let responseBody: unknown try { responseBody = JSON.parse(responseText) } catch { responseBody = responseText } // Convertir headers a Record const responseHeaders: Record<string, string> = {} response.headers.forEach((value, key) => { responseHeaders[key] = value }) // Calcular tamaño const sizeBytes = Number(response.headers.get('content-length')) || Buffer.byteLength(responseText, 'utf-8') return { status: response.status, statusText: response.statusText, headers: responseHeaders, body: responseBody, timing: { total_ms: totalMs }, size_bytes: sizeBytes, } } catch (error) { if (error instanceof Error && error.name === 'AbortError') { throw new Error(`Request timeout: superado el límite de ${timeout}ms`) } throw error } finally { clearTimeout(timeoutId) } }