Skip to main content
Glama
DynamicToolFactory.ts5.43 kB
import { z, ZodTypeAny } from 'zod'; import { spawn, ChildProcess } from 'child_process'; import { ParamConfig, ToolConfig } from '../types'; import { createOutputCollector, formatTruncationSuffix } from './outputCollector'; /** * JSON Schema type for MCP tool input (with index signature for SDK compatibility) */ export interface JsonSchema { type: 'object'; properties: Record<string, { type: string; description: string; }>; required: string[]; [key: string]: unknown; } /** * Maps config type strings to Zod schema builders */ function getZodType(type: ParamConfig['type']): ZodTypeAny { switch (type) { case 'string': return z.string(); case 'number': return z.number(); case 'boolean': return z.boolean(); default: throw new Error(`Unsupported parameter type: ${type}`); } } /** * Builds a Zod schema from parameter configurations */ export function buildZodSchema(parameters: ParamConfig[]): z.ZodObject<Record<string, ZodTypeAny>> { const schemaShape: Record<string, ZodTypeAny> = {}; for (const param of parameters) { let zodType = getZodType(param.type).describe(param.description); if (!param.required) { zodType = zodType.optional(); } schemaShape[param.name] = zodType; } return z.object(schemaShape); } /** * Builds a JSON Schema from parameter configurations (for MCP tool definition) */ export function buildJsonSchema(parameters: ParamConfig[]): JsonSchema { const properties: JsonSchema['properties'] = {}; const required: string[] = []; for (const param of parameters) { properties[param.name] = { type: param.type, description: param.description, }; if (param.required) { required.push(param.name); } } return { type: 'object', properties, required, }; } /** * Formats command result with actual stdout, stderr, and signal info */ function formatResult( stdout: string, stderr: string, stdoutTruncated: boolean, stderrTruncated: boolean, exitCode: number | null, signal: NodeJS.Signals | null ): string { // If killed by signal, treat as error if (signal) { return [ 'Error:', `Stdout: ${stdout || '(empty)'}${formatTruncationSuffix(stdoutTruncated)}`, `Stderr: ${stderr || '(empty)'}${formatTruncationSuffix(stderrTruncated)}`, `Exit code: (killed by signal)`, `Signal: ${signal}`, ].join('\n'); } const code = exitCode ?? 0; const status = code === 0 ? 'Success' : 'Error'; return [ `${status}:`, `Stdout: ${stdout || '(empty)'}${formatTruncationSuffix(stdoutTruncated)}`, `Stderr: ${stderr || '(empty)'}${formatTruncationSuffix(stderrTruncated)}`, `Exit code: ${code}`, ].join('\n'); } /** * Formats spawn error (e.g., command not found) */ function formatSpawnError(error: Error): string { return [ 'Error:', `Stdout: `, `Stderr: `, `Exit code: unknown`, `Message: ${error.message}`, ].join('\n'); } /** * Builds an array of arguments from input and parameter configs. * Preserves declaration order so flags and positionals stay interleaved as defined. */ export function buildArgs( paramConfigs: ParamConfig[], input: Record<string, unknown> ): string[] { const args: string[] = []; // Process parameters in declaration order to preserve interleaving for (const param of paramConfigs) { const value = input[param.name]; // Skip undefined optional parameters if (value === undefined) { continue; } if (param.flag) { // Flagged parameter if (param.type === 'boolean') { // Boolean flags: include flag only if true if (value === true) { args.push(param.flag); } } else { // String/number flags: include flag and value as separate args args.push(param.flag, String(value)); } } else { // Positional parameter args.push(String(value)); } } return args; } /** * Executes a dynamic tool based on config using async spawn for safe argument passing. * Returns a Promise to avoid blocking the event loop. */ export function executeDynamicTool( baseCli: string, config: ToolConfig, input: Record<string, unknown> ): Promise<string> { return new Promise((resolve) => { // Build args array preserving declaration order const paramArgs = buildArgs(config.parameters, input); // Split subcommand into parts, trim whitespace and filter empty strings // This handles " status " -> ["status"] correctly const subcommandParts = config.subcommand ? config.subcommand.trim().split(/\s+/).filter(Boolean) : []; // Combine: subcommand parts + parameter args (in declaration order) const allArgs = [...subcommandParts, ...paramArgs]; const child: ChildProcess = spawn(baseCli, allArgs); const stdoutCollector = createOutputCollector(child.stdout); const stderrCollector = createOutputCollector(child.stderr); child.on('error', (error: Error) => { resolve(formatSpawnError(error)); }); child.on('close', (code: number | null, signal: NodeJS.Signals | null) => { resolve( formatResult( stdoutCollector.getText(), stderrCollector.getText(), stdoutCollector.isTruncated(), stderrCollector.isTruncated(), code, signal ) ); }); }); }

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/pyrex41/mcp-cli'

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