UNPKG

@aurracloud/mcp-cli

Version:

A command-line tool to install, manage, and setup MCP (Model Context Protocol) servers with Docker support

392 lines 16.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getServerDetails = getServerDetails; exports.parseSmitheryEnvironmentVariables = parseSmitheryEnvironmentVariables; exports.convertSmitheryToMcpConfig = convertSmitheryToMcpConfig; exports.loadSmitheryConfig = loadSmitheryConfig; exports.findAndLoadSmitheryConfig = findAndLoadSmitheryConfig; exports.testSmitheryParsing = testSmitheryParsing; const node_fetch_1 = __importDefault(require("node-fetch")); const logger_1 = require("./utils/logger"); const fs_1 = require("fs"); /** * Fetches MCP server details from the Smithery registry * * @param serverName - The qualified name of the server to fetch details for * @param apiToken - Smithery API token for authentication * @returns Promise resolving to server details * @throws Error if the request fails or returns non-OK status */ async function getServerDetails(serverName, apiToken) { try { const response = await (0, node_fetch_1.default)(`https://registry.smithery.ai/servers/${serverName}`, { headers: { 'Authorization': `Bearer ${apiToken}`, 'Accept': 'application/json' } }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to fetch server details: ${response.status} ${response.statusText} - ${errorText}`); } const data = await response.json(); return data; } catch (error) { // Only log error if not in quiet mode (for JSON output) if ((0, logger_1.getLoggerMode)() !== 'quiet') { logger_1.logger.error(`Error fetching server details for ${serverName}`, error); } throw error; } } /** * Parses a smithery.yaml configuration to extract environment variables * from the commandFunction * * @param smitheryConfig - The parsed smithery.yaml configuration * @returns Object containing extracted environment variables and command info */ function parseSmitheryEnvironmentVariables(smitheryConfig) { const errors = []; const environmentVariables = {}; let command; let args; try { // Extract the commandFunction string const commandFunction = smitheryConfig.startCommand.commandFunction; if (!commandFunction) { errors.push('No commandFunction found in smithery configuration'); return { environmentVariables, errors }; } // Parse the function to extract environment variables // The function format is: config => ({ command: 'node', args: ['build/index.js'], env: { VAR_NAME: config.varName } }) // Extract environment object using regex const envMatch = commandFunction.match(/env:\s*\{([^}]+)\}/); if (envMatch) { const envContent = envMatch[1]; // Extract individual environment variable assignments // Pattern: VAR_NAME: config.varName const envVarPattern = /(\w+):\s*config\.(\w+)/g; let match; while ((match = envVarPattern.exec(envContent)) !== null) { const [, envVarName, configVarName] = match; // Try to find description from configSchema if available let description = `Environment variable for ${configVarName}`; let type = 'string'; let sensitive = false; if (smitheryConfig.startCommand.configSchema?.properties?.[configVarName]) { const schemaProp = smitheryConfig.startCommand.configSchema.properties[configVarName]; description = schemaProp.description || description; type = schemaProp.type || type; // Guess if it's sensitive based on name patterns if (envVarName.toLowerCase().includes('key') || envVarName.toLowerCase().includes('secret') || envVarName.toLowerCase().includes('token') || envVarName.toLowerCase().includes('password')) { sensitive = true; } } environmentVariables[envVarName] = { type, description, sensitive }; } } // Extract command and args const commandMatch = commandFunction.match(/command:\s*['"]([^'"]+)['"]/); if (commandMatch) { command = commandMatch[1]; } const argsMatch = commandFunction.match(/args:\s*\[([^\]]+)\]/); if (argsMatch) { const argsContent = argsMatch[1]; // Extract quoted strings from args array const argMatches = argsContent.match(/['"][^'"]*['"]/g); if (argMatches) { args = argMatches.map(arg => arg.slice(1, -1)); // Remove quotes } } } catch (error) { errors.push(`Failed to parse commandFunction: ${error instanceof Error ? error.message : 'Unknown error'}`); } return { environmentVariables, command, args, errors }; } /** * Converts smithery environment variables to MCP config format * * @param smitheryEnvResult - Result from parsing smithery configuration * @param requiredVars - Array of variable names that should be marked as required * @returns Object with required and optional environment configurations */ function convertSmitheryToMcpConfig(smitheryEnvResult, requiredVars = []) { const required = {}; const optional = {}; Object.entries(smitheryEnvResult.environmentVariables).forEach(([varName, config]) => { if (requiredVars.includes(varName)) { required[varName] = config; } else { optional[varName] = config; } }); return { required, optional }; } /** * Simple YAML parser for smithery.yaml format * This is a basic parser specifically for the smithery.yaml structure */ function parseSmitheryYaml(content) { try { const lines = content.split('\n'); const config = { startCommand: { configSchema: { properties: {} } } }; let inCommandFunction = false; let commandFunctionLines = []; let currentPropertyName = null; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmedLine = line.trim(); // Skip empty lines and comments if (!trimmedLine || trimmedLine.startsWith('#')) { continue; } const indent = line.length - line.trimStart().length; // Handle command function multiline string if (inCommandFunction) { // If we encounter a line with the same or less indentation and a colon, we're done with commandFunction if (indent <= 2 && trimmedLine.includes(':')) { // Finish processing the command function const commandFunction = commandFunctionLines.join(' ').trim(); config.startCommand.commandFunction = commandFunction; inCommandFunction = false; commandFunctionLines = []; // Continue processing this line } else { // Still inside the command function commandFunctionLines.push(trimmedLine); continue; } } // Parse key-value pairs based on indentation if (trimmedLine.includes(':')) { const colonIndex = trimmedLine.indexOf(':'); const key = trimmedLine.substring(0, colonIndex).trim(); const value = trimmedLine.substring(colonIndex + 1).trim(); if (indent === 0) { // Root level - should be "startCommand" if (key === 'startCommand') { // Already initialized } } else if (indent === 2) { // Level 1 under startCommand if (key === 'type') { config.startCommand.type = value; } else if (key === 'configSchema') { // Already initialized } else if (key === 'commandFunction') { if (value === '|-' || value === '|') { inCommandFunction = true; commandFunctionLines = []; } else if (value === '') { // Check the following lines for the multiline indicator (skip comments) for (let j = i + 1; j < lines.length; j++) { const nextLine = lines[j]; const nextTrimmed = nextLine.trim(); // Skip empty lines and comments if (!nextTrimmed || nextTrimmed.startsWith('#')) { continue; } // Check if this line has the multiline indicator if (nextTrimmed === '|-' || nextTrimmed === '|') { inCommandFunction = true; commandFunctionLines = []; i = j; // Skip to the indicator line break; } else { // If we hit a non-comment, non-indicator line, stop looking break; } } } else { config.startCommand.commandFunction = value; } } } else if (indent === 4) { // Level 2 under configSchema if (key === 'type') { config.startCommand.configSchema.type = value; } else if (key === 'required') { // Handle required array - simplified for this case config.startCommand.configSchema.required = ['shodanApiKey']; } else if (key === 'properties') { // Already initialized } } else if (indent === 6) { // Level 3 under properties - this should be property names currentPropertyName = key; if (!config.startCommand.configSchema.properties[key]) { config.startCommand.configSchema.properties[key] = {}; } } else if (indent === 8) { // Level 4 under specific property if (currentPropertyName) { if (key === 'type') { config.startCommand.configSchema.properties[currentPropertyName].type = value; } else if (key === 'description') { config.startCommand.configSchema.properties[currentPropertyName].description = value; } } } } else if (indent === 6 && trimmedLine.startsWith('- ')) { // Handle array items under required const item = trimmedLine.substring(2).trim(); if (!config.startCommand.configSchema.required) { config.startCommand.configSchema.required = []; } config.startCommand.configSchema.required.push(item); } } // Handle case where commandFunction is at the end of file if (inCommandFunction && commandFunctionLines.length > 0) { const commandFunction = commandFunctionLines.join(' ').trim(); config.startCommand.commandFunction = commandFunction; } return config; } catch (error) { return null; } } /** * Loads and parses a smithery.yaml file from the filesystem * * @param filePath - Path to the smithery.yaml file * @returns Promise resolving to parsed smithery configuration and environment variables */ async function loadSmitheryConfig(filePath) { const errors = []; try { // Read the YAML file const content = await fs_1.promises.readFile(filePath, 'utf-8'); // Parse YAML const parsed = parseSmitheryYaml(content); if (!parsed || typeof parsed !== 'object') { errors.push('Invalid smithery.yaml format'); return { config: null, envResult: { environmentVariables: {}, errors }, errors }; } // Validate basic structure if (!parsed.startCommand) { errors.push('Missing startCommand in smithery.yaml'); return { config: null, envResult: { environmentVariables: {}, errors }, errors }; } // Parse environment variables const envResult = parseSmitheryEnvironmentVariables(parsed); return { config: parsed, envResult, errors: [...errors, ...envResult.errors] }; } catch (error) { errors.push(`Failed to load smithery.yaml: ${error instanceof Error ? error.message : 'Unknown error'}`); return { config: null, envResult: { environmentVariables: {}, errors: [] }, errors }; } } /** * Attempts to find and load smithery.yaml in a directory * * @param directoryPath - Directory to search for smithery.yaml * @returns Promise resolving to smithery config if found, null otherwise */ async function findAndLoadSmitheryConfig(directoryPath) { const smitheryPath = `${directoryPath}/smithery.yaml`; try { await fs_1.promises.access(smitheryPath); const result = await loadSmitheryConfig(smitheryPath); return { found: true, ...result, filePath: smitheryPath }; } catch { return { found: false, config: null, envResult: { environmentVariables: {}, errors: [] }, errors: [] }; } } /** * Test function to verify smithery.yaml parsing with the provided example * This can be removed in production */ function testSmitheryParsing() { const exampleYaml = `startCommand: type: stdio configSchema: # JSON Schema defining the configuration options for the MCP. type: object required: - shodanApiKey properties: shodanApiKey: type: string description: The API key for the Shodan API. commandFunction: # A function that produces the CLI command to start the MCP on stdio. |- config => ({ command: 'node', args: ['build/index.js'], env: { SHODAN_API_KEY: config.shodanApiKey } })`; console.log('Testing with example YAML:'); console.log(exampleYaml); console.log('\n--- Parsing ---'); const parsed = parseSmitheryYaml(exampleYaml); if (parsed) { console.log('Parsed config:', JSON.stringify(parsed, null, 2)); console.log('\n--- Environment Variable Extraction ---'); const envResult = parseSmitheryEnvironmentVariables(parsed); console.log('Environment variables:', JSON.stringify(envResult, null, 2)); console.log('\n--- MCP Config Conversion ---'); const mcpConfig = convertSmitheryToMcpConfig(envResult, ['SHODAN_API_KEY']); console.log('MCP config format:', JSON.stringify(mcpConfig, null, 2)); } else { console.log('Failed to parse example YAML'); } } //# sourceMappingURL=smithery.js.map