Skip to main content
Glama

NTFY MCP Server

tree.ts9.72 kB
#!/usr/bin/env node /** * Generate Tree Script * =================== * * Description: * A utility script that generates a visual tree representation of your project's directory structure. * The script respects .gitignore patterns and applies common exclusions like node_modules. * The tree is saved as a markdown file by default in the docs directory. * * Usage: * - Add to package.json: "tree": "ts-node --esm scripts/tree.ts" * - Run directly: npm run tree * - Specify custom output path: ts-node --esm scripts/tree.ts ./documentation/structure.md * - Specify max depth: ts-node --esm scripts/tree.ts --depth=3 * - Get help: ts-node --esm scripts/tree.ts --help * * Features: * - Automatically excludes directories listed in .gitignore * - Handles directory sorting (folders first) * - Supports custom output path * - Works on all platforms * - Can limit directory depth */ import fs from 'fs/promises'; import path from 'path'; // Define the project root directory robustly const projectRoot = process.cwd(); // Process command line arguments const args = process.argv.slice(2); let outputPath = 'docs/tree.md'; // Default output path relative to project root let maxDepth = Infinity; /** * Interface for gitignore pattern */ interface GitignorePattern { pattern: string; negated: boolean; regex: string; } // Handle command line options if (args.includes('--help')) { console.log(` Generate Tree - Project directory structure visualization tool Usage: ts-node --esm scripts/tree.ts [output-path] [--depth=<number>] [--help] Options: output-path Custom file path for the tree output (relative to project root, default: docs/tree.md) --depth=<number> Maximum directory depth to display (default: unlimited) --help Show this help message `); process.exit(0); } // Default patterns to always ignore const DEFAULT_IGNORE_PATTERNS: string[] = ['.git', 'node_modules', '.DS_Store', 'dist', 'build']; /** * Loads patterns from the .gitignore file */ async function loadGitignorePatterns(): Promise<GitignorePattern[]> { const gitignorePath = path.join(projectRoot, '.gitignore'); try { // Security: Ensure we read only from within the project root if (!path.resolve(gitignorePath).startsWith(projectRoot + path.sep)) { console.warn('Attempted to read .gitignore outside project root. Using default patterns only.'); return []; } const gitignoreContent = await fs.readFile(gitignorePath, 'utf-8'); return gitignoreContent .split('\n') .map(line => line.trim()) // Remove comments, empty lines, and lines with just whitespace .filter(line => line && !line.startsWith('#') && line.trim() !== '') // Process each pattern .map(pattern => ({ pattern: pattern.startsWith('!') ? pattern.slice(1) : pattern, negated: pattern.startsWith('!'), // Convert glob patterns to regex-compatible strings (simplified approach) regex: pattern .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars first .replace(/\\\*/g, '.*') // Convert \* to .* .replace(/\\\?/g, '.') // Convert \? to . .replace(/\/$/, '(/.*)?') // Handle directory indicators })); } catch (error: any) { if (error.code === 'ENOENT') { console.warn('No .gitignore file found at project root, using default patterns only'); } else { console.error(`Error reading .gitignore: ${error.message}`); } return []; } } /** * Checks if a path should be ignored based on patterns */ function isIgnored(entryPath: string, ignorePatterns: GitignorePattern[]): boolean { const relativePath = path.relative(projectRoot, entryPath); // Use relative path for matching // Always check default patterns first using relative path if (DEFAULT_IGNORE_PATTERNS.some(pattern => relativePath.startsWith(pattern))) { return true; } let ignored = false; for (const { pattern, negated, regex } of ignorePatterns) { // Match against the relative path const regexPattern = new RegExp(`^${regex}$|^${regex}/`); // Match full path or directory start if (regexPattern.test(relativePath)) { ignored = !negated; } } return ignored; } /** * Generates a tree representation of the directory structure */ async function generateTree( dir: string, ignorePatterns: GitignorePattern[], prefix = '', isLast = true, currentDepth = 0 ): Promise<string> { // Security Check: Ensure the directory being read is within the project root const resolvedDir = path.resolve(dir); if (!resolvedDir.startsWith(projectRoot + path.sep) && resolvedDir !== projectRoot) { console.warn(`Skipping directory outside project root: ${resolvedDir}`); return ''; // Prevent traversal outside root } let entries; try { entries = await fs.readdir(resolvedDir, { withFileTypes: true }); // Use resolvedDir } catch (error: any) { console.error(`Error reading directory ${resolvedDir}: ${error.message}`); return ''; // Stop processing this branch on error } let output = ''; // Filter and sort entries const filteredEntries = entries .filter(entry => { const entryAbsolutePath = path.join(resolvedDir, entry.name); // Use absolute path for ignore check return !isIgnored(entryAbsolutePath, ignorePatterns); }) .sort((a, b) => { // Directories first, then files if (a.isDirectory() && !b.isDirectory()) return -1; if (!a.isDirectory() && b.isDirectory()) return 1; return a.name.localeCompare(b.name); }); for (let i = 0; i < filteredEntries.length; i++) { const entry = filteredEntries[i]; const isLastEntry = i === filteredEntries.length - 1; const newPrefix = prefix + (isLast ? ' ' : '│ '); output += prefix + (isLastEntry ? '└── ' : '├── ') + entry.name + '\n'; // Only traverse deeper if we haven't reached maxDepth if (entry.isDirectory() && currentDepth < maxDepth) { output += await generateTree( path.join(resolvedDir, entry.name), // Pass resolved path for next level ignorePatterns, newPrefix, isLastEntry, currentDepth + 1 ); } } return output; } // Process command line arguments for custom configurations for (const arg of args) { if (arg.startsWith('--depth=')) { const depthValue = arg.split('=')[1]; const parsedDepth = parseInt(depthValue, 10); if (isNaN(parsedDepth) || parsedDepth < 1) { console.error('Invalid depth value. Using unlimited depth.'); maxDepth = Infinity; } else { maxDepth = parsedDepth; } } else if (!arg.startsWith('--')) { // If it's not an option flag, assume it's the output path outputPath = arg; } } /** * Main function to write the tree to a file */ const writeTree = async (): Promise<void> => { try { const projectName = path.basename(projectRoot); const ignorePatterns = await loadGitignorePatterns(); // --- Security Validation for Output Path --- const resolvedOutputFile = path.resolve(projectRoot, outputPath); if (!resolvedOutputFile.startsWith(projectRoot + path.sep)) { console.error(`Error: Output path "${outputPath}" resolves outside the project directory: ${resolvedOutputFile}`); process.exit(1); } const resolvedOutputDir = path.dirname(resolvedOutputFile); // Double-check directory path as well if (!resolvedOutputDir.startsWith(projectRoot + path.sep) && resolvedOutputDir !== projectRoot) { console.error(`Error: Output directory "${resolvedOutputDir}" is outside the project directory.`); process.exit(1); } // --- End Security Validation --- console.log(`Generating directory tree for: ${projectName}`); console.log(`Output path: ${resolvedOutputFile}`); // Log resolved path if (maxDepth !== Infinity) { console.log(`Maximum depth: ${maxDepth}`); } // Generate the tree structure starting from the project root const treeContent = await generateTree(projectRoot, ignorePatterns, '', true, 0); // Ensure output directory exists (use validated path) try { await fs.access(resolvedOutputDir); // Use validated resolvedOutputDir } catch { console.log(`Creating directory: ${resolvedOutputDir}`); try { await fs.mkdir(resolvedOutputDir, { recursive: true }); // Use validated resolvedOutputDir } catch (mkdirError: any) { console.error(`Error creating directory ${resolvedOutputDir}: ${mkdirError.message}`); process.exit(1); } } // Write tree to file (use validated path) const timestamp = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, ''); const content = `# ${projectName} - Directory Structure Generated on: ${timestamp} ${maxDepth !== Infinity ? `_Depth limited to ${maxDepth} levels_\n\n` : ''} \`\`\` ${projectName} ${treeContent} \`\`\` _Note: This tree excludes files and directories matched by .gitignore and common patterns like node_modules._ `; try { await fs.writeFile( resolvedOutputFile, // Use validated resolvedOutputFile content ); } catch (writeFileError: any) { console.error(`Error writing to file ${resolvedOutputFile}: ${writeFileError.message}`); process.exit(1); } console.log(`✓ Successfully generated tree structure in ${resolvedOutputFile}`); } catch (error) { console.error(`× Error generating tree: ${error instanceof Error ? error.message : error}`); process.exit(1); } }; // Execute the write tree function writeTree();

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/cyanheads/ntfy-mcp-server'

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