@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
JavaScript
;
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