Skip to main content
Glama
publicApi.ts9.04 kB
/** * @fileOverview: Public API surface detection with signatures for server-only exports * @module: PublicApi * @keyFunctions: * - isPublicSurface(): Check if file should be included in public API analysis * - extractApiSignature(): Extract function signature from AST-like parsing * - formatSignature(): Format export with parameters for agent consumption * @context: Focuses on server-side programmatic APIs that agents can understand and use */ import { isServerishPath, toPosix } from './pathUtils'; export type ApiSymbol = { name: string; kind: 'function' | 'class' | 'const' | 'interface' | 'type'; file: string; line: number; params?: number; // parameter count from AST signature?: string; // formatted signature role?: string; // inferred role (handler, validator, etc.) }; /** * Check if a file path represents a public API surface * Only server-side files should be included in public API analysis */ export function isPublicSurface(posixPath: string): boolean { // Must be server-side code if (!isServerishPath(posixPath)) { return false; } // Exclude test files if (/(\/|^)(__tests__|tests)\//i.test(posixPath)) { return false; } if (/\.(test|spec)\./i.test(posixPath)) { return false; } // Exclude build artifacts and generated files if (/(\/|^)(dist|build|out|coverage|\.next)\//i.test(posixPath)) { return false; } return true; } /** * Extract function signature information from code content */ export function extractApiSignature( content: string, exportName: string, kind: string ): { params?: number; signature?: string } { const lines = content.split('\n'); // Find the export line const exportPatterns = [ new RegExp(`export\\s+(?:async\\s+)?function\\s+${exportName}\\s*\\(([^)]*)\\)`, 'g'), new RegExp( `export\\s+const\\s+${exportName}\\s*=\\s*(?:async\\s+)?(?:function\\s*)?\\(([^)]*)\\)`, 'g' ), new RegExp(`${exportName}\\s*:\\s*(?:async\\s+)?function\\s*\\(([^)]*)\\)`, 'g'), new RegExp(`function\\s+${exportName}\\s*\\(([^)]*)\\)`, 'g'), // For function declarations that might be exported later ]; for (const line of lines) { for (const pattern of exportPatterns) { const match = pattern.exec(line); if (match) { const paramString = match[1] || ''; const params = countParameters(paramString); const signature = formatSignatureFromParams(exportName, paramString, kind); return { params, signature }; } } } // Fallback for classes if (kind === 'class') { const classPattern = new RegExp( `class\\s+${exportName}(?:\\s*<[^>]*>)?(?:\\s+extends\\s+\\w+)?\\s*\\{`, 'g' ); if (classPattern.test(content)) { return { signature: `${exportName} (class)` }; } } // Fallback for interfaces/types if (kind === 'interface' || kind === 'type') { return { signature: `${exportName} (${kind})` }; } return {}; } /** * Count parameters in a parameter string, handling complex cases */ function countParameters(paramString: string): number { if (!paramString.trim()) return 0; // Remove default values and type annotations for counting const cleaned = paramString .replace(/:\s*[^,=)]+/g, '') // Remove type annotations .replace(/=\s*[^,)]+/g, '') // Remove default values .replace(/\s+/g, '') // Remove whitespace .trim(); if (!cleaned) return 0; // Count commas + 1, but handle destructured parameters let count = 1; let braceDepth = 0; let bracketDepth = 0; for (const char of cleaned) { if (char === '{') braceDepth++; else if (char === '}') braceDepth--; else if (char === '[') bracketDepth++; else if (char === ']') bracketDepth--; else if (char === ',' && braceDepth === 0 && bracketDepth === 0) { count++; } } return count; } /** * Format a signature from parameter string */ function formatSignatureFromParams(name: string, paramString: string, kind: string): string { if (kind === 'class') { return `${name} (class)`; } if (kind === 'interface' || kind === 'type') { return `${name} (${kind})`; } const paramCount = countParameters(paramString); if (paramCount === 0) { return `${name}()`; } // Extract parameter names for a more informative signature const paramNames = extractParameterNames(paramString); if (paramNames.length > 0 && paramNames.length <= 4) { return `${name}(${paramNames.join(', ')})`; } // Fallback to count return `${name}(${paramCount})`; } /** * Extract parameter names from parameter string */ function extractParameterNames(paramString: string): string[] { if (!paramString.trim()) return []; const params = paramString.split(',').map(p => p.trim()); const names: string[] = []; for (const param of params) { // Extract the parameter name (before : or =) let name = param.split(/[:=]/)[0].trim(); // Handle destructuring if (name.startsWith('{') && name.endsWith('}')) { names.push('{ ... }'); } else if (name.startsWith('[') && name.endsWith(']')) { names.push('[ ... ]'); } else if (name.includes('...')) { names.push(`...${name.replace(/^\.\.\./, '')}`); } else { // Clean up the name name = name.replace(/[{}[\]]/g, '').trim(); if (name && name.length > 0) { names.push(name); } } } return names.slice(0, 4); // Limit to 4 parameters for readability } /** * Format a complete signature for an API symbol */ export function formatSignature(symbol: ApiSymbol): string { if (symbol.signature) { return symbol.signature; } if (symbol.params !== undefined) { return `${symbol.name}(${symbol.params})`; } return symbol.name; } /** * Infer the role/purpose of an export based on its name and kind */ export function inferExportRole(name: string, kind: string, filePath?: string): string { const nameLower = name.toLowerCase(); // Role inference based on name patterns if (/^(init|initialize|bootstrap|setup|configure)/.test(nameLower)) return 'initializer'; if (/^(create|build|make|generate)/.test(nameLower)) return 'factory'; if (/(handler|handle)/.test(nameLower)) return 'handler'; if (/(validate|verify|check|assert)/.test(nameLower)) return 'validator'; if (/(parse|transform|convert|format)/.test(nameLower)) return 'transformer'; if (/(search|query|find|filter)/.test(nameLower)) return 'search'; if (/(save|store|persist|write|insert|update)/.test(nameLower)) return 'storage'; if (/(load|read|fetch|get|retrieve)/.test(nameLower)) return 'retrieval'; if (/(connect|client|pool|database)/.test(nameLower)) return 'connection'; if (/(middleware|guard|auth|authorize)/.test(nameLower)) return 'middleware'; if (/(tool|command|execute)/.test(nameLower)) return 'tool'; if (/(service|provider|manager)/.test(nameLower)) return 'service'; if (/(router|route|endpoint)/.test(nameLower)) return 'routing'; // Role inference based on file path context if (filePath) { const posixPath = toPosix(filePath); if (/\/handlers?\//.test(posixPath)) return 'handler'; if (/\/middleware\//.test(posixPath)) return 'middleware'; if (/\/services?\//.test(posixPath)) return 'service'; if (/\/utils?\//.test(posixPath)) return 'utility'; if (/\/tools?\//.test(posixPath)) return 'tool'; if (/\/validators?\//.test(posixPath)) return 'validator'; if (/\/models?\//.test(posixPath)) return 'model'; if (/\/controllers?\//.test(posixPath)) return 'controller'; if (/(api|routes?)\//.test(posixPath)) return 'api'; } // Default role based on kind return kind; } /** * Check if an export should be excluded from public API (noise filtering) */ export function shouldExcludeFromPublicApi(name: string, kind: string, filePath: string): boolean { const nameLower = name.toLowerCase(); const posixPath = toPosix(filePath); // Exclude overly generic names unless in meaningful contexts const genericNames = new Set([ 'default', 'index', 'config', 'props', 'children', 'value', 'data', 'item', 'element', 'component', 'wrapper', 'container', 'render', ]); if (genericNames.has(nameLower)) { // Allow in server contexts if (!isServerishPath(posixPath)) { return true; } } // Exclude obvious UI components if (/^(button|card|modal|dialog|input|form|label|text|icon|image)$/i.test(name)) { return true; } // Exclude React patterns if (/^(use[A-Z]|with[A-Z]|create[A-Z].*Component)/.test(name)) { return true; } // Exclude HTTP method exports that are route handlers (they belong in routes section) const httpMethods = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']); if (httpMethods.has(name.toUpperCase())) { if (posixPath.includes('/api/') || posixPath.match(/\/app\/.*\/route\.(ts|js)$/)) { return true; // This is a route handler, not a public API export } } return false; }

Latest Blog Posts

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/sbarron/AmbianceMCP'

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