Skip to main content
Glama
extract-docs.js•11.4 kB
#!/usr/bin/env node import fs from 'fs'; import path from 'path'; import { execSync } from 'child_process'; /** * Extracts MCP tool documentation from the main index.js file * This parses the tools array in setupToolHandlers() and extracts structured information */ function extractToolsFromCode() { // Tools are now defined in the tool registry, not in index.js const registryPath = path.resolve('lib/tools/tool-registry.js'); if (!fs.existsSync(registryPath)) { throw new Error('Could not find tool registry at lib/tools/tool-registry.js'); } const content = fs.readFileSync(registryPath, 'utf8'); // Extract all tool arrays (DATABASE_TOOLS, DATA_TOOLS, etc.) const toolArrays = extractToolArrays(content); const allTools = []; toolArrays.forEach(toolArray => { const tools = parseToolArray(toolArray); allTools.push(...tools); }); return allTools; } // Extract tool arrays from the registry content function extractToolArrays(content) { const toolArrays = []; // Look for const TOOL_NAME = [ ... ]; patterns const arrayRegex = /const\s+(\w*TOOLS?)\s*=\s*\[(.*?)\];/gs; let match; while ((match = arrayRegex.exec(content)) !== null) { const arrayName = match[1]; const arrayContent = match[2]; console.log(`Found tool array: ${arrayName}`); toolArrays.push(arrayContent); } return toolArrays; } // Parse individual tool array content function parseToolArray(arrayContent) { const tools = []; const toolParts = splitToolObjects(arrayContent); toolParts.forEach(toolPart => { const tool = parseIndividualTool(toolPart); if (tool) { tools.push(tool); } }); return tools; } function splitToolObjects(content) { // Split by tool object boundaries, but be careful with nested braces const parts = []; let current = ''; let braceDepth = 0; let inString = false; let stringChar = ''; for (let i = 0; i < content.length; i++) { const char = content[i]; const prevChar = i > 0 ? content[i - 1] : ''; current += char; if (!inString && (char === '"' || char === "'" || char === '`')) { inString = true; stringChar = char; } else if (inString && char === stringChar && prevChar !== '\\') { inString = false; stringChar = ''; } else if (!inString) { if (char === '{') { braceDepth++; } else if (char === '}') { braceDepth--; // If we're back to depth 0, we've completed a tool object if (braceDepth === 0) { parts.push(current.trim()); current = ''; } } } } // Add any remaining content if (current.trim()) { parts.push(current.trim()); } return parts.filter(part => part.includes('name:') && part.includes('description:')); } function parseIndividualTool(toolContent) { // Extract name const nameRegex = /name:\s*['"`]([^'"`]+)['"`]/; const nameMatch = toolContent.match(nameRegex); // Extract description const descriptionRegex = /description:\s*['"`]([^'"`]+)['"`]/; const descriptionMatch = toolContent.match(descriptionRegex); if (!nameMatch || !descriptionMatch) { return null; } const name = nameMatch[1]; const description = descriptionMatch[1]; // Extract input schema const inputSchemaRegex = /inputSchema:\s*\{([\s\S]*?)\}\s*$/; const inputSchemaMatch = toolContent.match(inputSchemaRegex); let properties = {}; let required = []; if (inputSchemaMatch) { const schemaContent = inputSchemaMatch[1]; properties = parseInputSchemaProperties(schemaContent); required = parseRequiredFields(schemaContent); } return { name, description, parameters: properties, required: required }; } function parseInputSchemaProperties(schemaContent) { const properties = {}; // First, extract the entire properties block const propertiesRegex = /properties:\s*\{([\s\S]*?)\}(?:\s*,?\s*required|\s*$)/; const propertiesMatch = schemaContent.match(propertiesRegex); if (!propertiesMatch) { return properties; } const propertiesContent = propertiesMatch[1]; // Now parse individual property objects by splitting them properly const propObjects = splitPropertyObjects(propertiesContent); propObjects.forEach(propObj => { const parsed = parseIndividualProperty(propObj); if (parsed) { properties[parsed.name] = { type: parsed.type, description: parsed.description }; } }); return properties; } function splitPropertyObjects(content) { const parts = []; let current = ''; let braceDepth = 0; let inString = false; let stringChar = ''; for (let i = 0; i < content.length; i++) { const char = content[i]; const prevChar = i > 0 ? content[i - 1] : ''; if (!inString && (char === '"' || char === "'" || char === '`')) { inString = true; stringChar = char; } else if (inString && char === stringChar && prevChar !== '\\') { inString = false; stringChar = ''; } if (!inString) { if (char === '{') { braceDepth++; current += char; } else if (char === '}') { braceDepth--; current += char; // If we're back to depth 0, we've completed a property object if (braceDepth === 0) { parts.push(current.trim()); current = ''; } } else { current += char; } } else { current += char; } } // Add any remaining content if (current.trim()) { parts.push(current.trim()); } return parts.filter(part => part.includes(':') && part.includes('{')); } function parseIndividualProperty(propContent) { // Extract property name (the key before the colon) const nameRegex = /(\w+):\s*\{/; const nameMatch = propContent.match(nameRegex); // Extract type const typeRegex = /type:\s*['"`]([^'"`]+)['"`]/; const typeMatch = propContent.match(typeRegex); // Extract description const descRegex = /description:\s*['"`]([^'"`]+)['"`]/; const descMatch = propContent.match(descRegex); if (!nameMatch || !typeMatch || !descMatch) { return null; } return { name: nameMatch[1], type: typeMatch[1], description: descMatch[1] }; } function parseRequiredFields(schemaContent) { const requiredRegex = /required:\s*\[([\s\S]*?)\]/; const requiredMatch = schemaContent.match(requiredRegex); if (!requiredMatch) { return []; } const requiredContent = requiredMatch[1]; const fieldRegex = /['"`]([^'"`]+)['"`]/g; const required = []; let fieldMatch; while ((fieldMatch = fieldRegex.exec(requiredContent)) !== null) { required.push(fieldMatch[1]); } return required; } function generateExamples(toolName, parameters, required) { const examples = { basic: {}, advanced: {} }; // Generate basic example with only required parameters required.forEach(param => { if (parameters[param]) { switch (parameters[param].type) { case 'string': if (param.includes('query')) { examples.basic[param] = 'SELECT * FROM your_table'; } else if (param.includes('table')) { examples.basic[param] = 'your_table_name'; } else if (param.includes('database')) { examples.basic[param] = 'your_database'; } else { examples.basic[param] = 'example_value'; } break; case 'number': examples.basic[param] = param === 'limit' ? 100 : 1; break; case 'boolean': examples.basic[param] = false; break; default: examples.basic[param] = 'example_value'; } } }); // Generate advanced example with optional parameters examples.advanced = { ...examples.basic }; Object.keys(parameters).forEach(param => { if (!required.includes(param)) { switch (parameters[param].type) { case 'string': if (param === 'database') { examples.advanced[param] = 'MyDatabase'; } else if (param === 'schema') { examples.advanced[param] = 'dbo'; } else if (param === 'where') { examples.advanced[param] = 'id > 100'; } else { examples.advanced[param] = 'optional_value'; } break; case 'number': examples.advanced[param] = param === 'limit' ? 50 : 1; break; case 'boolean': examples.advanced[param] = true; break; } } }); return examples; } function generateToolsDocumentation() { console.log('Extracting MCP tools documentation...'); try { const tools = extractToolsFromCode(); console.log(`Found ${tools.length} MCP tools`); // Add examples to each tool const enhancedTools = tools.map(tool => ({ ...tool, examples: generateExamples(tool.name, tool.parameters, tool.required) })); // Generate the documentation data (candidate) const docData = { version: getPackageVersion(), generatedAt: new Date().toISOString(), toolsCount: enhancedTools.length, tools: enhancedTools }; // Save to a JSON file that can be used by the GitHub Actions workflow const outputDir = 'docs-data'; if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } const outPath = path.join(outputDir, 'tools.json'); // Avoid trivial PRs: if the only change is generatedAt, keep the previous timestamp try { if (fs.existsSync(outPath)) { const prev = JSON.parse(fs.readFileSync(outPath, 'utf8')); const normalize = obj => { const copy = JSON.parse(JSON.stringify(obj)); delete copy.generatedAt; return copy; }; const prevNorm = normalize(prev); const nextNorm = normalize(docData); if (JSON.stringify(prevNorm) === JSON.stringify(nextNorm)) { // Preserve previous generatedAt to keep file identical docData.generatedAt = prev.generatedAt || docData.generatedAt; } } } catch { // Non-fatal: proceed with current docData } fs.writeFileSync(outPath, JSON.stringify(docData, null, 2)); // Format the generated JSON with Prettier to ensure consistency try { execSync('npx prettier --write docs-data/tools.json', { stdio: 'inherit' }); console.log('Documentation data saved and formatted: docs-data/tools.json'); } catch { console.log('Documentation data saved to docs-data/tools.json (formatting skipped)'); } return docData; } catch (error) { console.error('Error extracting documentation:', error.message); process.exit(1); } } function getPackageVersion() { try { const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); return packageJson.version; } catch { return '1.0.0'; } } // Run if executed directly if (import.meta.url === `file://${process.argv[1]}`) { const docData = generateToolsDocumentation(); // Output summary for GitHub Actions console.log('\n📊 Documentation Summary:'); console.log(`Version: ${docData.version}`); console.log(`Tools: ${docData.toolsCount}`); docData.tools.forEach(tool => { console.log(` • ${tool.name}: ${tool.description}`); }); } export { generateToolsDocumentation };

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/egarcia74/warp-sql-server-mcp'

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