MCPcopy
hub / github.com/promptfoo/promptfoo / maybeLoadToolsFromExternalFile

Function maybeLoadToolsFromExternalFile

src/util/file.ts:487–627  ·  view source on GitHub ↗
(
  tools: any,
  vars?: Record<string, VarValue>,
)

Source from the content-addressed store, hash-verified

485 * @throws {Error} If the loaded tools are in an invalid format
486 */
487export async function maybeLoadToolsFromExternalFile(
488 tools: any,
489 vars?: Record<string, VarValue>,
490): Promise<any> {
491 const rendered = renderVarsInObject(tools, vars);
492
493 // Check if this is a Python/JS file reference with function name
494 // These need special handling to execute the function and get the result
495 if (typeof rendered === 'string' && rendered.startsWith('file://')) {
496 const { filePath, functionName } = parseFileUrl(rendered);
497
498 if (functionName && (filePath.endsWith('.py') || isJavascriptFile(filePath))) {
499 // Execute the function to get tool definitions
500 const fileType = filePath.endsWith('.py') ? 'Python' : 'JavaScript';
501 logger.debug(
502 `[maybeLoadToolsFromExternalFile] Loading tools from ${fileType} file: ${filePath}:${functionName}`,
503 );
504
505 try {
506 let toolDefinitions: any;
507
508 if (filePath.endsWith('.py')) {
509 // Resolve Python path relative to config base directory (same as JavaScript)
510 const absPath = safeResolve(cliState.basePath || process.cwd(), filePath);
511 logger.debug(`[maybeLoadToolsFromExternalFile] Resolved Python path: ${absPath}`);
512 toolDefinitions = await runPython(absPath, functionName, []);
513 } else {
514 // Use safeResolve for security (prevents path traversal)
515 const absPath = safeResolve(cliState.basePath || process.cwd(), filePath);
516 logger.debug(`[maybeLoadToolsFromExternalFile] Resolved JavaScript path: ${absPath}`);
517
518 const module = await importModule(absPath);
519 const fn = module[functionName] || module.default?.[functionName];
520
521 if (typeof fn !== 'function') {
522 const availableExports = Object.keys(module).filter((k) => k !== 'default');
523 const basePath = cliState.basePath || process.cwd();
524 throw new Error(
525 `Function "${functionName}" not found in ${filePath}. ` +
526 `Available exports: ${availableExports.length > 0 ? availableExports.join(', ') : '(none)'}\n` +
527 `Resolved from: ${basePath}`,
528 );
529 }
530
531 // Call the function - handle both sync and async functions
532 toolDefinitions = await Promise.resolve(fn());
533 }
534
535 // Validate the result - must be array or object, not primitive
536 if (
537 !toolDefinitions ||
538 typeof toolDefinitions === 'string' ||
539 typeof toolDefinitions === 'number' ||
540 typeof toolDefinitions === 'boolean'
541 ) {
542 throw new Error(
543 `Function "${functionName}" must return an array or object of tool definitions, ` +
544 `but returned: ${toolDefinitions === null ? 'null' : typeof toolDefinitions}`,

Callers 15

file.test.tsFile · 0.90
callApiInternalMethod · 0.90
callApiInternalMethod · 0.85
buildToolConfigMethod · 0.85
index.tsFile · 0.85
callApiMethod · 0.85
buildResponsesBodyMethod · 0.85
getOpenAiBodyMethod · 0.85
getAzureResponsesBodyMethod · 0.85
callApiInternalMethod · 0.85
buildSessionConfigMethod · 0.85

Calls 9

renderVarsInObjectFunction · 0.90
parseFileUrlFunction · 0.90
isJavascriptFileFunction · 0.90
safeResolveFunction · 0.90
runPythonFunction · 0.90
importModuleFunction · 0.90
resolveMethod · 0.80
fnFunction · 0.50

Tested by

no test coverage detected

Used in the wild real call sites across dependent graphs

searching dependent graphs…