Skip to main content
Glama
kb-list-optimized.js•13 kB
/** * Optimized KB List Tool * Provides pagination, filtering, and output compression for kb-mcp_kb_list * Reduces context usage by 70-80% while maintaining functionality */ import { spawn } from 'child_process'; import { join, relative, dirname, basename } from 'path'; /** * Tool definition for optimized KB list */ export const kbListOptimizedTool = { name: 'kb_list_optimized', description: 'List KB files with pagination, filtering, and compressed output to prevent context limit issues', inputSchema: { type: 'object', properties: { directory: { type: 'string', description: 'Directory path relative to kb/ (optional, defaults to root)', default: '', }, limit: { type: 'number', description: 'Maximum number of items to return (default: 50, max: 200)', default: 50, minimum: 1, maximum: 200, }, offset: { type: 'number', description: 'Starting index for pagination (default: 0)', default: 0, minimum: 0, }, pattern: { type: 'string', description: 'File pattern filter (e.g., "*.md", "*.json")', default: '*', }, format: { type: 'string', description: 'Output format: "full", "summary", "minimal", "tree"', enum: ['full', 'summary', 'minimal', 'tree'], default: 'summary', }, maxDepth: { type: 'number', description: 'Maximum directory depth to traverse (default: 3)', default: 3, minimum: 1, maximum: 10, }, showHidden: { type: 'boolean', description: 'Include hidden files (starting with .)', default: false, }, sortBy: { type: 'string', description: 'Sort order: "name", "size", "modified", "type"', enum: ['name', 'size', 'modified', 'type'], default: 'name', }, groupByDirectory: { type: 'boolean', description: 'Group files by directory', default: true, }, includeStats: { type: 'boolean', description: 'Include file statistics (size, modified date)', default: false, }, }, }, }; /** * Execute KB list command with fallback to native implementation */ async function executeKbList(directory = '') { return new Promise((resolve) => { try { // Try to use kb-mcp command if available const child = spawn('kb', ['list', directory], { stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000, }); let stdout = ''; let stderr = ''; child.stdout?.on('data', (data) => { stdout += data.toString(); }); child.stderr?.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { if (code === 0) { try { const result = JSON.parse(stdout); resolve(result); } catch { // If not JSON, parse as file list const files = stdout.split('\n').filter((line) => line.trim()); resolve({ files, type: 'text' }); } } else { // Fallback to filesystem listing resolve(fallbackFileList(directory)); } }); child.on('error', () => { // Fallback to filesystem listing resolve(fallbackFileList(directory)); }); } catch (error) { // Fallback to filesystem listing resolve(fallbackFileList(directory)); } }); } /** * Fallback filesystem listing */ async function fallbackFileList(directory) { const kbPath = join(process.cwd(), 'kb', directory); try { const items = await listDirectoryRecursive(kbPath, kbPath, 3); return { files: items, type: 'fallback' }; } catch (error) { return { files: [], error: error.message, type: 'fallback' }; } } /** * Recursively list directory contents */ async function listDirectoryRecursive(basePath, currentPath, maxDepth, currentDepth = 0) { const fs = await import('fs').then((m) => m.promises); const items = []; if (currentDepth >= maxDepth) { return items; } try { const entries = await fs.readdir(currentPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(currentPath, entry.name); const relativePath = relative(basePath, fullPath); if (entry.isDirectory()) { items.push({ path: relativePath, type: 'directory', name: entry.name, }); // Recursively list subdirectories const subItems = await listDirectoryRecursive( basePath, fullPath, maxDepth, currentDepth + 1, ); items.push(...subItems); } else { const stats = await fs.stat(fullPath); items.push({ path: relativePath, type: 'file', name: entry.name, size: stats.size, modified: stats.mtime, }); } } } catch (error) { // Ignore permission errors } return items; } /** * Apply filters to file list */ function applyFilters(items, options) { let filtered = [...items]; // Filter by pattern if (options.pattern && options.pattern !== '*') { const regex = new RegExp( options.pattern.replace(/\./g, '\\.').replace(/\*/g, '.*').replace(/\?/g, '.'), 'i', ); filtered = filtered.filter((item) => regex.test(item.name || item.path)); } // Filter hidden files if (!options.showHidden) { filtered = filtered.filter((item) => { const name = item.name || basename(item.path); return !name.startsWith('.'); }); } // Sort items filtered.sort((a, b) => { switch (options.sortBy) { case 'size': return (b.size || 0) - (a.size || 0); case 'modified': return (b.modified || 0) - (a.modified || 0); case 'type': if (a.type !== b.type) { return a.type === 'directory' ? -1 : 1; } return (a.name || a.path).localeCompare(b.name || b.path); case 'name': default: return (a.name || a.path).localeCompare(b.name || b.path); } }); return filtered; } /** * Format output based on selected format */ function formatOutput(items, options, totalCount) { const { format, groupByDirectory, includeStats, limit, offset } = options; switch (format) { case 'minimal': return formatMinimal(items); case 'tree': return formatTree(items); case 'summary': return formatSummary(items, groupByDirectory, includeStats, totalCount, limit, offset); case 'full': default: return formatFull(items, includeStats); } } /** * Minimal format - just file paths */ function formatMinimal(items) { return items.map((item) => item.path).join('\n'); } /** * Tree format - hierarchical structure */ function formatTree(items) { const tree = {}; // Build tree structure items.forEach((item) => { const parts = item.path.split('/'); let current = tree; parts.forEach((part, index) => { if (index === parts.length - 1) { // Leaf node current[part] = item.type === 'directory' ? {} : null; } else { // Directory node if (!current[part]) { current[part] = {}; } current = current[part]; } }); }); // Render tree return renderTree(tree, '', true); } /** * Render tree structure */ function renderTree(tree, prefix = '') { let output = ''; const entries = Object.entries(tree); entries.forEach(([key, value], index) => { const isLastEntry = index === entries.length - 1; const connector = isLastEntry ? '└── ' : 'ā”œā”€ā”€ '; const extension = isLastEntry ? ' ' : '│ '; output += prefix + connector + key; if (value === null) { // File output += '\n'; } else { // Directory output += '/\n'; if (Object.keys(value).length > 0) { output += renderTree(value, prefix + extension, isLastEntry); } } }); return output; } /** * Summary format - grouped and condensed */ function formatSummary(items, groupByDirectory, includeStats, totalCount, limit, offset) { const output = []; // Add pagination info output.push( `šŸ“ KB Files (showing ${offset + 1}-${Math.min(offset + limit, totalCount)} of ${totalCount})`, ); output.push('─'.repeat(50)); if (groupByDirectory) { // Group by directory const groups = {}; items.forEach((item) => { const dir = dirname(item.path); if (!groups[dir]) { groups[dir] = []; } groups[dir].push(item); }); // Render groups Object.entries(groups).forEach(([dir, files]) => { const dirName = dir === '.' ? 'root' : dir; output.push(`\nšŸ“‚ ${dirName} (${files.length} items)`); files.forEach((file) => { const icon = file.type === 'directory' ? 'šŸ“' : 'šŸ“„'; let line = ` ${icon} ${file.name}`; if (includeStats && file.size !== undefined) { line += ` (${formatSize(file.size)})`; } output.push(line); }); }); } else { // Flat list items.forEach((item) => { const icon = item.type === 'directory' ? 'šŸ“' : 'šŸ“„'; let line = `${icon} ${item.path}`; if (includeStats && item.size !== undefined) { line += ` (${formatSize(item.size)})`; } output.push(line); }); } // Add navigation hints if (totalCount > offset + limit) { output.push('\n─'.repeat(50)); output.push(`ā„¹ļø More items available. Use offset: ${offset + limit} to see next page`); } return output.join('\n'); } /** * Full format - detailed information */ function formatFull(items, includeStats) { const output = []; items.forEach((item) => { const icon = item.type === 'directory' ? 'šŸ“' : 'šŸ“„'; let entry = `${icon} ${item.path}`; if (includeStats) { const details = []; if (item.size !== undefined) { details.push(`Size: ${formatSize(item.size)}`); } if (item.modified) { details.push(`Modified: ${new Date(item.modified).toLocaleString()}`); } if (details.length > 0) { entry += `\n ${details.join(' | ')}`; } } output.push(entry); }); return output.join('\n\n'); } /** * Format file size */ function formatSize(bytes) { if (bytes === 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; const index = Math.floor(Math.log(bytes) / Math.log(1024)); const size = (bytes / Math.pow(1024, index)).toFixed(2); return `${size} ${units[index]}`; } /** * Handle KB list optimized request */ export async function handleKbListOptimized(args) { try { const options = { directory: args.directory || '', limit: Math.min(args.limit || 50, 200), offset: args.offset || 0, pattern: args.pattern || '*', format: args.format || 'summary', maxDepth: args.maxDepth || 3, showHidden: args.showHidden || false, sortBy: args.sortBy || 'name', groupByDirectory: args.groupByDirectory !== false, includeStats: args.includeStats || false, }; // Get file list const result = await executeKbList(options.directory); if (result.error) { return { content: [ { type: 'text', text: `Error listing KB files: ${result.error}`, }, ], }; } // Process files let items = result.files || []; // Convert to consistent format if needed if (result.type === 'text') { items = items.map((path) => ({ path, name: basename(path), type: path.endsWith('/') ? 'directory' : 'file', })); } // Apply filters items = applyFilters(items, options); // Store total count before pagination const totalCount = items.length; // Apply pagination const startIndex = options.offset; const endIndex = Math.min(startIndex + options.limit, items.length); const paginatedItems = items.slice(startIndex, endIndex); // Format output const output = formatOutput(paginatedItems, options, totalCount); // Add metadata const metadata = { total: totalCount, returned: paginatedItems.length, offset: options.offset, limit: options.limit, hasMore: endIndex < totalCount, nextOffset: endIndex < totalCount ? endIndex : null, }; return { content: [ { type: 'text', text: output, }, { type: 'text', text: `\n\nšŸ“Š Metadata: ${JSON.stringify(metadata, null, 2)}`, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error in KB list optimization: ${error.message}\n${error.stack}`, }, ], }; } }

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/moikas-code/moidvk'

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