Skip to main content
Glama

Salesforce CLI MCP Server

utils.ts9.81 kB
/** * Utility functions for working with the Salesforce CLI */ /** * Parse a user message to look for project directory specification * @param message A message from the user that might contain project directory specification * @returns The extracted directory path, or null if none found */ export function extractProjectDirectoryFromMessage(message: string): string | null { if (!message) return null; // Common patterns for specifying project directories const patterns = [ // "Execute in /path/to/project" /[Ee]xecute\s+(?:in|from)\s+(['"]?)([\/~][^\n'"]+)\1/, // "Run in /path/to/project" /[Rr]un\s+(?:in|from)\s+(['"]?)([\/~][^\n'"]+)\1/, // "Use project in /path/to/project" /[Uu]se\s+project\s+(?:in|from|at)\s+(['"]?)([\/~][^\n'"]+)\1/, // "Set project directory to /path/to/project" /[Ss]et\s+project\s+directory\s+(?:to|as)\s+(['"]?)([\/~][^\n'"]+)\1/, // "Project is at /path/to/project" /[Pp]roject\s+(?:is|located)\s+(?:at|in)\s+(['"]?)([\/~][^\n'"]+)\1/, // "My project is in /path/to/project" /[Mm]y\s+project\s+is\s+(?:at|in)\s+(['"]?)([\/~][^\n'"]+)\1/, // "/path/to/project is my project" /(['"]?)([\/~][^\n'"]+)\1\s+is\s+my\s+(?:project|directory)/, ]; for (const pattern of patterns) { const match = message.match(pattern); if (match) { return match[2]; } } return null; } /** * Formats an object as a string representation of CLI flags * @param flags Key-value pairs of flag names and values * @returns Formatted flags string suitable for command line */ export function formatFlags(flags: Record<string, any>): string { if (!flags) return ''; return Object.entries(flags) .map(([key, value]) => { // Skip undefined/null values if (value === undefined || value === null) return ''; // Handle boolean flags if (typeof value === 'boolean') { return value ? `--${key}` : ''; } // Handle arrays (space-separated multi-values) if (Array.isArray(value)) { return value.map((v) => `--${key}=${escapeValue(v)}`).join(' '); } // Handle objects (JSON stringify) if (typeof value === 'object') { return `--${key}=${escapeValue(JSON.stringify(value))}`; } // Regular values return `--${key}=${escapeValue(value)}`; }) .filter(Boolean) .join(' '); } /** * Escapes values for command line usage */ function escapeValue(value: any): string { const stringValue = String(value); // If value contains spaces, wrap in quotes if (stringValue.includes(' ')) { // Escape any existing quotes return `"${stringValue.replace(/"/g, '\\"')}"`; } return stringValue; } /** * Parses help text to extract structured information about commands or flags * @param helpText Help text from Salesforce CLI * @returns Structured information extracted from help text */ export function parseHelpText(helpText: string): { description: string; examples: string[]; flags: Record< string, { name: string; description: string; required: boolean; type: string; char?: string; } >; } { const description: string[] = []; const examples: string[] = []; const flags: Record<string, any> = {}; // Split by sections const sections = helpText.split(/\n\s*\n/); // Extract description (usually the first section, skipping DESCRIPTION header if present) if (sections.length > 0) { let firstSection = sections[0].trim(); if (firstSection.toUpperCase().startsWith('DESCRIPTION')) { firstSection = firstSection.substring(firstSection.indexOf('\n') + 1).trim(); } description.push(firstSection); } // Look for a description section if the first section wasn't clear if (description[0]?.length < 10 || description[0]?.toUpperCase().includes('USAGE')) { const descSection = sections.find( (section) => section.toUpperCase().startsWith('DESCRIPTION') || section.toUpperCase().includes('\nDESCRIPTION\n') ); if (descSection) { const descContent = descSection.replace(/DESCRIPTION/i, '').trim(); if (descContent) { description.push(descContent); } } } // Look for examples section with improved pattern matching const examplePatterns = [/EXAMPLES?/i, /USAGE/i]; for (const pattern of examplePatterns) { const exampleSection = sections.find((section) => pattern.test(section)); if (exampleSection) { // Extract examples - look for command lines that start with $ or sf const exampleLines = exampleSection .split('\n') .filter((line) => { const trimmed = line.trim(); return trimmed.startsWith('$') || trimmed.startsWith('sf ') || /^\s*\d+\.\s+sf\s+/.test(line); // Numbered examples: "1. sf ..." }) .map((line) => line.trim().replace(/^\d+\.\s+/, '')); // Remove numbering if present examples.push(...exampleLines); } } // Look for flags section with improved pattern matching const flagPatterns = [/FLAGS/i, /OPTIONS/i, /PARAMETERS/i, /ARGUMENTS/i]; for (const pattern of flagPatterns) { const flagSections = sections.filter((section) => pattern.test(section)); for (const flagSection of flagSections) { // Skip the section header line const sectionLines = flagSection.split('\n').slice(1); // Different patterns for flag lines const flagPatterns = [ // Pattern 1: Classic -c, --char=<value> Description /^\s*(?:-([a-zA-Z]),\s+)?--([a-zA-Z][a-zA-Z0-9-]+)(?:=<?([a-zA-Z0-9_\-\[\]|]+)>?)?\s+(.+)$/, // Pattern 2: Indented flag with details (common in newer SF CLI) /^\s+(?:-([a-zA-Z]),\s+)?--([a-zA-Z][a-zA-Z0-9-]+)(?:\s+|\=)(?:<([a-zA-Z0-9_\-\[\]|]+)>)?\s*\n\s+(.+)/, // Pattern 3: Simple flag with no/minimal formatting /^\s*(?:-([a-zA-Z]),\s*)?--([a-zA-Z][a-zA-Z0-9-]+)(?:\s+|\=)?(?:\s*<([a-zA-Z0-9_\-\[\]|]+)>)?\s+(.+)$/, ]; // Process the flag section let i = 0; while (i < sectionLines.length) { const line = sectionLines[i]; const nextLine = i < sectionLines.length - 1 ? sectionLines[i + 1] : ''; const combinedLines = line + '\n' + nextLine; let matched = false; // Try all patterns for (const pattern of flagPatterns) { const match = line.match(pattern) || combinedLines.match(pattern); if (match) { matched = true; const char = match[1]; const name = match[2]; const type = match[3] || 'boolean'; const description = match[4].trim(); // Check if this flag is required const required = description.toLowerCase().includes('(required)') || description.toLowerCase().includes('[required]') || description.toLowerCase().includes('required:') || description.toLowerCase().includes('required -'); // Normalize the type let normalizedType = type.toLowerCase(); if (normalizedType.includes('number') || normalizedType.includes('int')) { normalizedType = 'number'; } else if (normalizedType.includes('boolean') || normalizedType === 'flag') { normalizedType = 'boolean'; } else if (normalizedType.includes('array') || normalizedType.includes('[]')) { normalizedType = 'array'; } else if (normalizedType.includes('json') || normalizedType.includes('object')) { normalizedType = 'json'; } else { normalizedType = 'string'; } flags[name] = { name, char, description: description .replace(/\([Rr]equired\)|\[[Rr]equired\]|[Rr]equired:?/g, '') .trim(), required, type: normalizedType, }; // Skip the next line if we matched against a two-line pattern if (combinedLines.match(pattern) && !line.match(pattern)) { i++; } break; } } // If no pattern matched and this line looks like it might be a flag if (!matched && (line.includes('--') || line.trim().startsWith('-'))) { console.error(`No pattern matched for potential flag line: "${line.trim()}"`); } i++; } } } return { description: description.join('\n\n'), examples, flags, }; }

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/codefriar/sf-mcp'

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