Skip to main content
Glama

Frida MCP Server

by nonsleepr
resources.ts19.5 kB
/** * MCP resource registration for Frida */ import * as frida from 'frida'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { getDevice, getSessionMessagesAsync, cleanupSession } from './helpers.js'; import { logger } from './logger.js'; import { sessions, scripts, scriptMessages } from './state.js'; import type { SessionInfo } from './types.js'; /** * Register MCP resources with the server */ export function registerResources(server: McpServer): void { // Devices resource (static list) server.registerResource( 'frida-devices', 'frida://devices', { name: 'Frida Devices', description: 'List all connected Frida devices', mimeType: 'application/json' }, async () => { const devices = await frida.enumerateDevices(); const deviceList = devices.map(d => ({ id: d.id, name: d.name, type: d.type })); return { contents: [{ uri: 'frida://devices', text: JSON.stringify(deviceList, null, 2), mimeType: 'application/json' }] }; } ); // Device processes resource template server.registerResource( 'device-processes', new ResourceTemplate('frida://devices/{device_id}/processes', { list: undefined }), { name: 'Device Processes', description: 'List processes on a specific Frida device. Use "local", "usb", or "remote" for automatic device selection, provide a specific device ID, or use a connection string (hostname:port or hostname).', mimeType: 'application/json' }, async (uri, { device_id }) => { const deviceIdParam = String(device_id || ''); // Map special keywords to device selection let deviceId: string | undefined; if (deviceIdParam === 'local' || deviceIdParam === 'usb' || deviceIdParam === 'remote') { deviceId = undefined; // Let getDevice handle auto-selection } else { // Pass through - getDevice will handle connection strings deviceId = deviceIdParam; } try { const device = await getDevice(deviceId); const processes = await device.enumerateProcesses(); const processList = processes.map(p => ({ pid: p.pid, name: p.name })); return { contents: [{ uri: uri.href, text: JSON.stringify(processList, null, 2), mimeType: 'application/json' }] }; } catch (error) { const errorResult = { error: `Failed to enumerate processes: ${error instanceof Error ? error.message : String(error)}`, device_id: deviceIdParam }; return { contents: [{ uri: uri.href, text: JSON.stringify(errorResult, null, 2), mimeType: 'application/json' }] }; } } ); // Sessions resource server.registerResource( 'frida-sessions', 'frida://sessions', { name: 'Frida Sessions', description: 'List all active Frida sessions and their statuses', mimeType: 'application/json' }, async () => { const sessionsInfo: SessionInfo[] = []; for (const [sessionId, session] of sessions) { try { const isDetached = session.isDetached(); const scriptCount = scripts.get(sessionId)?.length || 0; const messageQueue = scriptMessages.get(sessionId); const messageCount = messageQueue ? messageQueue.length : 0; sessionsInfo.push({ session_id: sessionId, is_alive: !isDetached, is_detached: isDetached, active_scripts: scriptCount, pending_messages: messageCount }); } catch (error) { sessionsInfo.push({ session_id: sessionId, is_alive: false, is_detached: true, error: error instanceof Error ? error.message : String(error), active_scripts: 0, pending_messages: 0 }); } } const result = { total_sessions: sessionsInfo.length, sessions: sessionsInfo }; return { contents: [{ uri: 'frida://sessions', text: JSON.stringify(result, null, 2), mimeType: 'application/json' }] }; } ); // Device info resource template server.registerResource( 'device-info', new ResourceTemplate('frida://devices/{device_id}', { list: undefined }), { name: 'Device Info', description: 'Get detailed information about a specific device by ID', mimeType: 'application/json' }, async (uri, { device_id }) => { const deviceId = String(device_id || ''); try { const device = await frida.getDevice(deviceId); const result = { id: device.id, name: device.name, type: device.type }; return { contents: [{ uri: uri.href, text: JSON.stringify(result, null, 2), mimeType: 'application/json' }] }; } catch (error) { const errorResult = { error: `Device with ID ${deviceId} not found`, device_id: deviceId }; return { contents: [{ uri: uri.href, text: JSON.stringify(errorResult, null, 2), mimeType: 'application/json' }] }; } } ); // Process by name resource template server.registerResource( 'process-by-name', new ResourceTemplate('frida://devices/{device_id}/processes/by-name/{process_name}', { list: undefined }), { name: 'Process by Name', description: 'Find a process by name (case-insensitive partial match) on a specific device. Supports connection strings (hostname:port or hostname).', mimeType: 'application/json' }, async (uri, { device_id, process_name }) => { const deviceIdParam = String(device_id || ''); const processName = decodeURIComponent(String(process_name || '')); // Map special keywords to device selection let deviceId: string | undefined; if (deviceIdParam === 'local' || deviceIdParam === 'usb' || deviceIdParam === 'remote') { deviceId = undefined; } else { // Pass through - getDevice will handle connection strings deviceId = deviceIdParam; } try { const device = await getDevice(deviceId); const processes = await device.enumerateProcesses(); for (const proc of processes) { if (proc.name.toLowerCase().includes(processName.toLowerCase())) { const result = { pid: proc.pid, name: proc.name, found: true }; return { contents: [{ uri: uri.href, text: JSON.stringify(result, null, 2), mimeType: 'application/json' }] }; } } const result = { found: false, error: `Process '${processName}' not found`, device_id: deviceIdParam }; return { contents: [{ uri: uri.href, text: JSON.stringify(result, null, 2), mimeType: 'application/json' }] }; } catch (error) { const errorResult = { found: false, error: `Failed to search for process: ${error instanceof Error ? error.message : String(error)}`, device_id: deviceIdParam, process_name: processName }; return { contents: [{ uri: uri.href, text: JSON.stringify(errorResult, null, 2), mimeType: 'application/json' }] }; } } ); // Process module path resource template server.registerResource( 'process-module-path', new ResourceTemplate('frida://devices/{device_id}/processes/{pid}/module', { list: undefined }), { name: 'Process Module Path', description: 'Get main module information for a process (path, base address, size). Supports connection strings (hostname:port or hostname).', mimeType: 'application/json' }, async (uri, { device_id, pid }) => { const deviceIdParam = String(device_id || ''); const pidStr = String(pid || ''); const pidNum = parseInt(pidStr, 10); if (isNaN(pidNum)) { const errorResult = { status: 'error', error: `Invalid PID: ${pidStr}` }; return { contents: [{ uri: uri.href, text: JSON.stringify(errorResult, null, 2), mimeType: 'application/json' }] }; } // Map special keywords to device selection let deviceId: string | undefined; if (deviceIdParam === 'local' || deviceIdParam === 'usb' || deviceIdParam === 'remote') { deviceId = undefined; } else { // Pass through - getDevice will handle connection strings deviceId = deviceIdParam; } try { const device = await getDevice(deviceId); const session = await device.attach(pidNum); const script = await session.createScript(` var mainModule = Process.enumerateModules()[0]; send({ name: mainModule.name, path: mainModule.path, base: mainModule.base.toString(), size: mainModule.size }); `); const results: any[] = []; script.message.connect((message: frida.Message) => { if (message.type === 'send') { results.push(message.payload); } }); await script.load(); // Wait for result await new Promise(resolve => setTimeout(resolve, 100)); await script.unload(); await session.detach(); if (results.length > 0) { const result = { status: 'success', pid: pidNum, ...results[0] }; return { contents: [{ uri: uri.href, text: JSON.stringify(result, null, 2), mimeType: 'application/json' }] }; } else { const result = { status: 'error', error: 'Failed to get module information', pid: pidNum }; return { contents: [{ uri: uri.href, text: JSON.stringify(result, null, 2), mimeType: 'application/json' }] }; } } catch (error) { const result = { status: 'error', error: error instanceof Error ? error.message : String(error), pid: pidNum }; return { contents: [{ uri: uri.href, text: JSON.stringify(result, null, 2), mimeType: 'application/json' }] }; } } ); // Session messages resource template with optional limit server.registerResource( 'session-messages', new ResourceTemplate('frida://sessions/{sessionId}/messages', { list: undefined }), { name: 'Session Messages', description: 'Retrieve messages from persistent scripts. Append /last:N to limit results (e.g., /last:10 for last 10 messages) or /all for unlimited. Default limit is 100 messages. Messages are consumed when retrieved.', mimeType: 'application/json' }, async (uri, { sessionId }) => { const sessionIdStr = String(sessionId || ''); let limit: number | undefined = 100; // Default limit // Check for limit parameter in URI path const pathParts = uri.pathname.split('/').filter(Boolean); if (pathParts[3]) { const limitPart = pathParts[3]; if (limitPart.startsWith('last:')) { const limitValue = parseInt(limitPart.substring(5), 10); if (!isNaN(limitValue) && limitValue > 0) { limit = limitValue; } } else if (limitPart === 'all') { limit = undefined; // No limit } } logger.info(`Message retrieval requested for session ${sessionIdStr}, limit=${limit || 'unlimited'}`); const startTime = Date.now(); try { // Validate session exists and is alive const session = sessions.get(sessionIdStr); if (session) { const isDetached = session.isDetached(); logger.debug(`Session found, is_detached=${isDetached}`); if (isDetached) { logger.warning(`Session ${sessionIdStr} is detached, cleaning up`); cleanupSession(sessionIdStr); const result = { status: 'error', error: `Session ${sessionIdStr} is detached and has been cleaned up`, session_id: sessionIdStr }; return { contents: [{ uri: uri.href, text: JSON.stringify(result, null, 2), mimeType: 'application/json' }] }; } } else { logger.warning(`Session ${sessionIdStr} not in sessions dict`); } // Use the async function with timeout protection (50s to leave margin) logger.debug('Calling getSessionMessagesAsync with 50s timeout'); const result = await Promise.race([ getSessionMessagesAsync(sessionIdStr, 50000), new Promise<any>((_, reject) => setTimeout(() => reject(new Error('Timeout')), 55000) ) ]); // Apply limit if specified if (result.status === 'success' && result.messages && limit !== undefined) { const originalCount = result.messages.length; result.messages = result.messages.slice(-limit); result.messages_retrieved = result.messages.length; if (result.messages.length < originalCount) { result.info = `Retrieved last ${result.messages.length} of ${originalCount} available messages`; } } const elapsed = (Date.now() - startTime) / 1000; logger.info(`Success in ${elapsed.toFixed(3)}s, returning ${result.messages_retrieved || 0} messages`); return { contents: [{ uri: uri.href, text: JSON.stringify(result, null, 2), mimeType: 'application/json' }] }; } catch (error) { const elapsed = (Date.now() - startTime) / 1000; logger.error(`Exception after ${elapsed.toFixed(3)}s: ${error}`); const result = { status: 'error', error: `Failed to retrieve messages: ${error instanceof Error ? error.message : String(error)}`, session_id: sessionIdStr, elapsed_seconds: Math.round(elapsed * 1000) / 1000 }; return { contents: [{ uri: uri.href, text: JSON.stringify(result, null, 2), mimeType: 'application/json' }] }; } } ); }

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