@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
JavaScript
/**
* 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);
}