Skip to main content
Glama

Frida MCP Server

by nonsleepr
helpers.ts8.84 kB
/** * Helper functions for Frida operations */ import * as frida from 'frida'; import { logger } from './logger.js'; import { sessions, scripts, scriptMessages } from './state.js'; import type { ScriptMessage } from './types.js'; /** * Get a Frida device by ID or use default detection. * * Device selection priority: * 1. If device_id is specified: * - Connection strings (hostname:port or hostname) are added as remote devices * - Standard device IDs ("local", "usb", etc.) are resolved via getDevice() * 2. If FRIDA_REMOTE_HOST env var is set, use remote device * 3. Try USB device (for mobile device debugging) * 4. Fall back to local device (for local process instrumentation) * * @param deviceId - Optional device ID or connection string (hostname:port or hostname) * @returns The selected Frida device * * Environment Variables: * FRIDA_REMOTE_HOST: Remote Frida server hostname/IP * FRIDA_REMOTE_PORT: Remote Frida server port (default: 27042) */ export async function getDevice(deviceId?: string): Promise<frida.Device> { // Priority 1: Explicit device ID provided if (deviceId) { // Check if it's a connection string by attempting to parse as URL // Skip standard device keywords if (!['local', 'usb', 'remote'].includes(deviceId.toLowerCase())) { try { // Try parsing as URL with dummy protocol const url = new URL(`tcp://${deviceId}`); const host = url.hostname; const port = url.port ? parseInt(url.port, 10) : 27042; // If we successfully parsed hostname, treat as connection string if (host) { const remoteAddress = `${host}:${port}`; logger.info(`Adding remote device: ${remoteAddress}`); const deviceManager = frida.getDeviceManager(); return await deviceManager.addRemoteDevice(remoteAddress); } } catch { // Not a valid URL format, fall through to standard device lookup } } // Standard device ID return await frida.getDevice(deviceId); } // Priority 2: Remote device via environment variables const remoteHost = process.env.FRIDA_REMOTE_HOST; if (remoteHost) { const remotePort = parseInt(process.env.FRIDA_REMOTE_PORT || '27042', 10); const deviceManager = frida.getDeviceManager(); const remoteAddress = `${remoteHost}:${remotePort}`; logger.info(`Using remote device: ${remoteAddress}`); return await deviceManager.addRemoteDevice(remoteAddress); } // Priority 3: USB device (for mobile debugging) try { return await frida.getUsbDevice(); } catch (error) { // Priority 4: Local device (fallback) return await frida.getLocalDevice(); } } /** * Execute a Frida script and wait for results. * * @param session - Active Frida session * @param scriptCode - JavaScript code to execute * @param timeout - Milliseconds to wait for results (default: 200ms) * @returns Array of payloads received from send() calls */ export async function executeScriptAndWait( session: frida.Session, scriptCode: string, timeout: number = 200 ): Promise<any[]> { const script = await session.createScript(scriptCode); const results: any[] = []; script.message.connect((message: frida.Message) => { if (message.type === 'send') { results.push(message.payload); } }); await script.load(); // Wait for the specified timeout await sleep(timeout); await script.unload(); return results; } /** * Clean up a detached or invalid session. * * @param sessionId - Session ID to clean up */ export function cleanupSession(sessionId: string): void { // Detach session if exists const session = sessions.get(sessionId); if (session) { try { session.detach(); } catch (error) { // Ignore errors during cleanup } sessions.delete(sessionId); } // Unload and clean up scripts const sessionScripts = scripts.get(sessionId); if (sessionScripts) { for (const script of sessionScripts) { try { script.unload(); } catch (error) { // Ignore errors during cleanup } } scripts.delete(sessionId); } // Clear message queue if (scriptMessages.has(sessionId)) { scriptMessages.delete(sessionId); } } /** * Retrieve and clear messages from persistent scripts. * * @param sessionId - Session ID to retrieve messages from * @param timeout - Maximum milliseconds to wait for messages (default: 5000ms) * @returns Object with status and messages list */ export async function getSessionMessagesAsync( sessionId: string, timeout: number = 5000 ): Promise<{ status: string; session_id?: string; messages?: ScriptMessage[]; messages_retrieved?: number; elapsed_seconds?: number; error?: string; info?: string; }> { logger.info(`Retrieving messages for session ${sessionId}`); const startTime = Date.now(); try { // Validation checks const session = sessions.get(sessionId); if (!session) { logger.warning(`Session ${sessionId} not in sessions`); const sessionScripts = scripts.get(sessionId); if (sessionScripts && sessionScripts.length === 0) { return { status: 'success', session_id: sessionId, messages: [], messages_retrieved: 0, info: 'Session scripts finished or detached.' }; } return { status: 'error', error: `Session ${sessionId} not found.` }; } const messageQueue = scriptMessages.get(sessionId); if (!messageQueue) { return { status: 'error', error: `Message queue not found for session ${sessionId}.` }; } const messages: ScriptMessage[] = []; const deadline = Date.now() + timeout; // Collect all available messages with timeout while (Date.now() < deadline) { const remaining = deadline - Date.now(); if (remaining <= 0) { break; } // Since we're using an array-based queue, just drain it if (messageQueue.length > 0) { messages.push(...messageQueue.splice(0, messageQueue.length)); break; } // Small delay before checking again await sleep(Math.min(100, remaining)); } const elapsed = (Date.now() - startTime) / 1000; logger.info(`Retrieved ${messages.length} messages in ${elapsed.toFixed(3)}s`); return { status: 'success', session_id: sessionId, messages, messages_retrieved: messages.length, elapsed_seconds: Math.round(elapsed * 1000) / 1000 }; } catch (error) { const elapsed = (Date.now() - startTime) / 1000; logger.error(`Exception retrieving messages: ${error}`); return { status: 'error', error: `Failed to retrieve messages: ${error instanceof Error ? error.message : String(error)}`, session_id: sessionId, elapsed_seconds: Math.round(elapsed * 1000) / 1000 }; } } /** * Sleep for specified milliseconds (utility function) */ export function sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Execute a promise with timeout protection. * * @param promise - Promise to execute * @param timeoutMs - Maximum milliseconds to wait * @param errorContext - Context string for timeout error message * @returns Promise that resolves with the result or rejects on timeout */ export async function withTimeout<T>( promise: Promise<T>, timeoutMs: number, errorContext: string = 'Operation' ): Promise<T> { return Promise.race([ promise, new Promise<T>((_, reject) => setTimeout( () => reject(new Error(`${errorContext} timed out after ${timeoutMs}ms`)), timeoutMs ) ) ]); } /** * Generate a unique session ID */ export function generateSessionId(processId: number): string { return `session_${processId}_${Math.floor(Date.now() / 1000)}`; }

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/nonsleepr/frida-mcp.ts'

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