UNPKG

@foundrole/ai-job-search-mcp

Version:

Easy-to-use MCP proxy server that connects AI assistants like Claude to job search services. Simply run with npx for instant job search capabilities in your AI conversations.

274 lines 10.5 kB
import findProcess from "find-process"; import { PROXY_NAME, PROXY_VERSION } from "./constants.js"; import { VersionExtractor } from "./version-extractor.js"; /** * Gets OS-friendly name from platform */ function getOSName() { switch (process.platform) { case "darwin": return "macOS"; case "win32": return "Windows"; case "linux": return "Linux"; default: return process.platform; } } /** * Extracts app name from executable path */ function extractAppName(executablePath, processName) { if (process.platform === "darwin") { // Extract app name from macOS .app bundle const appMatch = executablePath.match(/([^/]+)\.app$/); if (appMatch && appMatch[1]) { return appMatch[1]; } // Extract service name from macOS .xpc bundle const xpcMatch = executablePath.match(/([^/]+)\.xpc$/); if (xpcMatch && xpcMatch[1]) { return xpcMatch[1]; } } if (process.platform === "win32") { // Extract executable name without .exe extension const exeMatch = executablePath.match(/([^/\\]+)\.exe$/i); if (exeMatch && exeMatch[1]) { return exeMatch[1]; } } // For Linux or fallback, use process name return processName; } /** * Extracts executable path from process command line */ function extractExecutablePath(cmd, processName) { // For macOS, look for .app and .xpc bundle paths if (process.platform === "darwin") { // Strategy: Find bundle patterns and extract the containing bundle path // Look for paths that end with .app or .xpc (before / or space that starts a new argument) // Try to find patterns like /path/to/App Name.app or /path/to/ServiceName.xpc // Look for the full path to the bundle, handling spaces in directory names // Match complete bundle paths that end with .app or .xpc (before /Contents or end) const bundlePathMatch = cmd.match(/(.*?\.(?:app|xpc))(?=\/Contents|\/|$)/i); if (bundlePathMatch && bundlePathMatch[1]) { return bundlePathMatch[1]; } // Fallback: if no clear bundle/Contents pattern, look for any .app or .xpc const simpleBundleMatch = cmd.match(/([^"\s]+\.(?:app|xpc))/i); if (simpleBundleMatch && simpleBundleMatch[1]) { return simpleBundleMatch[1]; } } // For Windows, look for .exe paths if (process.platform === "win32") { // Handle quoted paths, single-quoted paths, and unquoted paths const exeMatch = cmd.match(/"([^"]+\.exe)"|'([^']+\.exe)'|([^\s]+\.exe)/i); if (exeMatch) { return exeMatch[1] || exeMatch[2] || exeMatch[3] || null; } } // Generic approach: first word in command is usually the executable const parts = cmd.trim().split(/\s+/); if (parts.length > 0 && parts[0] && parts[0] !== processName) { return parts[0]; } return null; } /** * Extracts version from process command line arguments */ function extractVersionFromCommand(cmd) { // Look for common version patterns in command line const versionPatterns = [ /--version[=\s]+([0-9]+\.[0-9]+\.[0-9]+)/i, /-v[=\s]+([0-9]+\.[0-9]+\.[0-9]+)/i, /version[=:\s]+([0-9]+\.[0-9]+\.[0-9]+)/i, /([0-9]+\.[0-9]+\.[0-9]+)/i, // Generic semver pattern ]; for (const pattern of versionPatterns) { const match = cmd.match(pattern); if (match && match[1]) { return match[1]; } } return null; } /** * Identifies client from process information using direct app name extraction */ async function identifyClientFromProcess(process) { const { cmd, name } = process; console.error(`[CLIENT-DETECTOR] Analyzing process: ${name} (${cmd})`); try { // Extract executable path from command const executablePath = extractExecutablePath(cmd, name); if (!executablePath) { console.error("[CLIENT-DETECTOR] Could not extract executable path, using proxy identity"); return { name: PROXY_NAME, version: PROXY_VERSION, }; } console.error(`[CLIENT-DETECTOR] Extracted executable path: ${executablePath}`); // Extract app name from executable path const appName = extractAppName(executablePath, name); const osName = getOSName(); const clientName = `${osName}//${appName}`; console.error(`[CLIENT-DETECTOR] Extracted app name: ${appName}`); // Try to extract version from executable let version = "unknown"; // Default to unknown when app is identified but version fails try { console.error(`[CLIENT-DETECTOR] Extracting version from: ${executablePath}`); const versionResult = await VersionExtractor.extractVersion(executablePath, name); if (versionResult.version) { version = versionResult.version; console.error(`[CLIENT-DETECTOR] Version extracted from ${versionResult.source}: ${version}`); } else { console.error(`[CLIENT-DETECTOR] Version extraction failed: ${versionResult.error}`); // Try extracting from command line as fallback const cmdVersion = extractVersionFromCommand(cmd); if (cmdVersion) { version = cmdVersion; console.error(`[CLIENT-DETECTOR] Version extracted from command: ${version}`); } else { console.error(`[CLIENT-DETECTOR] No version found, using 'unknown'`); } } } catch (error) { console.error(`[CLIENT-DETECTOR] Version extraction error: ${String(error)}`); // Try command line extraction as final fallback const cmdVersion = extractVersionFromCommand(cmd); if (cmdVersion) { version = cmdVersion; console.error(`[CLIENT-DETECTOR] Version extracted from command: ${version}`); } } console.error(`[CLIENT-DETECTOR] Identified as ${clientName}@${version}`); return { name: clientName, version, }; } catch (error) { console.error(`[CLIENT-DETECTOR] Error during client identification: ${String(error)}`); console.error("[CLIENT-DETECTOR] Using proxy identity as fallback"); return { name: PROXY_NAME, version: PROXY_VERSION, }; } } /** * Checks if a process is likely an intermediary (package manager, runtime, shell, etc.) * that we should skip when looking for the real MCP client */ function isIntermediaryProcess(process) { const { cmd, name } = process; // Common intermediary process names const intermediaryNames = [ "npm", "npx", "yarn", "pnpm", "bun", "node", "deno", "sh", "bash", "zsh", "fish", "launchd", "systemd", "init", "exec", "spawn", ]; // Check process name const lowerName = name.toLowerCase(); if (intermediaryNames.some((inter) => lowerName.includes(inter))) { return true; } // Check command line for package manager patterns const lowerCmd = cmd.toLowerCase(); if (lowerCmd.includes("npm exec") || lowerCmd.includes("npx ") || lowerCmd.includes("yarn exec") || lowerCmd.includes("pnpm exec")) { return true; } return false; } /** * Traverses up the process tree to find the real MCP client, * skipping intermediary processes like npm, npx, node, etc. */ async function findRealClient(currentPid, depth = 0) { const MAX_DEPTH = 5; // Prevent infinite traversal if (depth >= MAX_DEPTH) { console.error(`[CLIENT-DETECTOR] Reached max traversal depth ${MAX_DEPTH}, using fallback`); return { name: PROXY_NAME, version: PROXY_VERSION, }; } try { console.error(`[CLIENT-DETECTOR] Traversing process tree, depth ${depth}, PID: ${currentPid}`); const processes = await (findProcess.default ?? findProcess)("pid", currentPid); if (processes.length === 0) { console.error(`[CLIENT-DETECTOR] Process ${currentPid} not found at depth ${depth}`); return { name: PROXY_NAME, version: PROXY_VERSION, }; } const currentProcess = processes[0]; console.error(`[CLIENT-DETECTOR] Found process: ${currentProcess.name} (${currentProcess.cmd})`); // Check if this is an intermediary process we should skip if (isIntermediaryProcess(currentProcess)) { console.error(`[CLIENT-DETECTOR] Process '${currentProcess.name}' is intermediary, checking parent`); if (!currentProcess.ppid) { console.error(`[CLIENT-DETECTOR] No parent PID available for intermediary process`); return { name: PROXY_NAME, version: PROXY_VERSION, }; } // Recursively check parent process return await findRealClient(currentProcess.ppid, depth + 1); } // This looks like a real client process console.error(`[CLIENT-DETECTOR] Found real client process: ${currentProcess.name}`); return await identifyClientFromProcess(currentProcess); } catch (error) { console.error(`[CLIENT-DETECTOR] Error during traversal at depth ${depth}: ${String(error)}`); return { name: PROXY_NAME, version: PROXY_VERSION, }; } } /** * Extracts original client info from parent process */ export async function extractClientInfoFromParent() { const parentPid = process.ppid; if (!parentPid) { console.error("[CLIENT-DETECTOR] No parent PID available"); return { name: PROXY_NAME, version: PROXY_VERSION, }; } console.error(`[CLIENT-DETECTOR] Looking up parent process PID: ${parentPid}`); // Use the new traversal logic to find the real client return await findRealClient(parentPid); } //# sourceMappingURL=client-detector.js.map