UNPKG

@debugg-ai/debugg-ai-mcp

Version:

Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.

96 lines (95 loc) 3.86 kB
/** * Shared tunnel and URL resolution context used by all MCP tools. * * Centralizes: * - resolving user input url to a concrete URL * - creating / reusing ngrok tunnels after the backend returns a tunnelKey * - sanitizing backend responses so callers only ever see the original URL */ import { tunnelManager } from '../services/ngrok/tunnelManager.js'; import { isLocalhostUrl, replaceTunnelUrls, extractLocalhostPort } from './urlParser.js'; // ─── URL resolution ────────────────────────────────────────────────────────── /** * Resolve tool input to a concrete URL string. */ export function resolveTargetUrl(input) { return input.url; } /** * Build a TunnelContext for a resolved URL. * Call this right after resolving the target URL — before any backend call. */ export function buildContext(originalUrl) { return { originalUrl, isLocalhost: isLocalhostUrl(originalUrl), }; } // ─── Tunnel creation ───────────────────────────────────────────────────────── /** * Check whether an active tunnel already exists for the same local port. * If found, touches its timer and returns an enriched context pointing at it. * Returns null for public URLs or when no tunnel is active for that port. * * Call this BEFORE provisioning a new key — if it returns a context, skip the provision. */ export function findExistingTunnel(ctx) { if (!ctx.isLocalhost) return null; const port = extractLocalhostPort(ctx.originalUrl); if (!port) return null; const existing = tunnelManager.getTunnelForPort(port); if (!existing) return null; tunnelManager.touchTunnel(existing.tunnelId); return { ...ctx, tunnelId: existing.tunnelId, targetUrl: existing.publicUrl }; } /** * Create (or reuse) a tunnel for a localhost URL. * * Call this AFTER the backend returns a `tunnelKey` and `tunnelId`. * No-op for public URLs. * * @param ctx - Context built from `buildContext()` * @param tunnelKey - Auth token from the backend (short-lived ngrok key) * @param tunnelId - ID to use as the ngrok subdomain * @param keyId - Backend key ID; stored on the tunnel so it is revoked on stop * @param revokeKey - Callback that revokes the backend key (called when tunnel stops) */ export async function ensureTunnel(ctx, tunnelKey, tunnelId, keyId, revokeKey) { if (!ctx.isLocalhost) return ctx; const result = await tunnelManager.processUrl(ctx.originalUrl, tunnelKey, tunnelId, keyId, revokeKey); return { ...ctx, tunnelId: result.tunnelId, targetUrl: result.url }; } /** * Stop the tunnel associated with a context (fire-and-forget safe). */ export async function releaseTunnel(ctx) { if (ctx.tunnelId) { await tunnelManager.stopTunnel(ctx.tunnelId); } } /** * Touch a tunnel's timer by ID to prevent auto-shutoff during active use. * Safe to call with undefined (no-op). */ export function touchTunnelById(tunnelId) { if (tunnelId) { tunnelManager.touchTunnel(tunnelId); } } // ─── Response sanitization ─────────────────────────────────────────────────── /** * Replace any tunnel URLs in a backend response with the original localhost origin. * No-op when the original URL was not localhost. * * Handles nested objects, arrays, and strings recursively. */ export function sanitizeResponseUrls(value, ctx) { if (!ctx.isLocalhost) return value; const origin = new URL(ctx.originalUrl).origin; return replaceTunnelUrls(value, origin); }