UNPKG

mcp-gdrive-enhanced-markov

Version:

MCP server for Google Drive and Sheets with write capabilities, AI-powered PDF analysis, SQLite document indexing, and advanced directory exploration tools

278 lines (277 loc) 10.7 kB
import { z } from 'zod'; import { google } from 'googleapis'; import { getValidCredentials } from '../auth.js'; export const name = 'gdrive_get_folder_tree'; export const description = 'Generate a visual tree structure of Google Drive folders and files with customizable display options'; export const inputSchema = z.object({ folderId: z.string().describe('The Google Drive folder ID to generate tree from'), maxDepth: z.number().min(1).max(8).optional().default(4).describe('Maximum depth for the tree (1-8)'), showFiles: z.boolean().optional().default(false).describe('Include files in the tree (folders only by default)'), showIds: z.boolean().optional().default(false).describe('Show Google Drive IDs next to names'), showCounts: z.boolean().optional().default(true).describe('Show item counts in parentheses'), collapseEmpty: z.boolean().optional().default(true).describe('Collapse empty folders in the display'), sortOrder: z.enum(['name', 'modified', 'created']).optional().default('name').describe('Sort order for items'), maxWidth: z.number().min(40).max(200).optional().default(120).describe('Maximum width for tree display') }); export const schema = { name, description, inputSchema: { type: 'object', properties: { folderId: { type: 'string', description: 'The Google Drive folder ID to generate tree from' }, maxDepth: { type: 'number', description: 'Maximum depth for the tree (1-8)', default: 4, minimum: 1, maximum: 8 }, showFiles: { type: 'boolean', description: 'Include files in the tree', default: false }, showIds: { type: 'boolean', description: 'Show Google Drive IDs', default: false }, showCounts: { type: 'boolean', description: 'Show item counts', default: true }, collapseEmpty: { type: 'boolean', description: 'Collapse empty folders', default: true }, sortOrder: { type: 'string', enum: ['name', 'modified', 'created'], description: 'Sort order for items', default: 'name' }, maxWidth: { type: 'number', description: 'Maximum width for tree display', default: 120, minimum: 40, maximum: 200 } }, required: ['folderId'] } }; export async function gdrive_get_folder_tree(args) { try { const authClient = await getValidCredentials(); const drive = google.drive({ version: 'v3', auth: authClient }); // Verify root folder exists try { const folderInfo = await drive.files.get({ fileId: args.folderId, fields: 'id,name,mimeType' }); if (folderInfo.data.mimeType !== 'application/vnd.google-apps.folder') { return { content: [{ type: 'text', text: `Error: ${args.folderId} is not a folder (type: ${folderInfo.data.mimeType})` }], isError: true }; } } catch (error) { return { content: [{ type: 'text', text: `Error: Folder ${args.folderId} not found or not accessible` }], isError: true }; } // Build the tree const rootNode = await buildFolderTree(drive, args.folderId, 0, args, new Set()); if (!rootNode) { return { content: [{ type: 'text', text: 'Error: Unable to build folder tree' }], isError: true }; } // Generate the visual tree const treeOutput = generateTreeVisualization(rootNode, args); return { content: [{ type: 'text', text: treeOutput }], isError: false }; } catch (error) { return { content: [{ type: 'text', text: `Error generating folder tree: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true }; } } async function buildFolderTree(drive, folderId, depth, args, visited) { if (visited.has(folderId) || depth > (args.maxDepth || 4)) { return null; } visited.add(folderId); try { // Get folder info const folderInfo = await drive.files.get({ fileId: folderId, fields: 'id,name,mimeType,modifiedTime,createdTime' }); // Determine sort order let orderBy = 'folder,name'; if (args.sortOrder === 'modified') { orderBy = 'folder,modifiedTime desc'; } else if (args.sortOrder === 'created') { orderBy = 'folder,createdTime desc'; } // Get folder contents let query = `'${folderId}' in parents and trashed=false`; if (!args.showFiles) { query += " and mimeType='application/vnd.google-apps.folder'"; } const response = await drive.files.list({ q: query, orderBy: orderBy, pageSize: 1000, fields: 'files(id,name,mimeType,size,modifiedTime,createdTime)' }); const items = response.data.files || []; const node = { id: folderInfo.data.id, name: folderInfo.data.name, isFolder: true, children: [], fileCount: 0, folderCount: 0, totalSize: 0, modifiedTime: folderInfo.data.modifiedTime, createdTime: folderInfo.data.createdTime, isEmpty: items.length === 0 }; // Process children for (const item of items) { const isFolder = item.mimeType === 'application/vnd.google-apps.folder'; if (isFolder) { // Recursively build subfolder tree const childNode = await buildFolderTree(drive, item.id, depth + 1, args, new Set(visited)); if (childNode) { node.children.push(childNode); node.folderCount += 1 + childNode.folderCount; node.fileCount += childNode.fileCount; node.totalSize += childNode.totalSize; } } else if (args.showFiles) { // Add file as leaf node const fileNode = { id: item.id, name: item.name, isFolder: false, children: [], fileCount: 0, folderCount: 0, totalSize: item.size ? parseInt(item.size) : 0, modifiedTime: item.modifiedTime, createdTime: item.createdTime, isEmpty: false }; node.children.push(fileNode); node.fileCount += 1; node.totalSize += fileNode.totalSize; } else { // Count files but don't add to tree node.fileCount += 1; node.totalSize += item.size ? parseInt(item.size) : 0; } } // Update isEmpty status node.isEmpty = node.children.length === 0 && node.fileCount === 0; return node; } catch (error) { console.error(`Error building tree for folder ${folderId}:`, error); return null; } } function generateTreeVisualization(rootNode, args) { const lines = []; // Header lines.push('📁 Google Drive Folder Tree'); lines.push('═'.repeat(Math.min(args.maxWidth || 120, 50))); lines.push(`📂 Root: ${rootNode.name}${args.showIds ? ` (${rootNode.id})` : ''}`); if (args.showCounts) { lines.push(`📊 Total: ${rootNode.folderCount} folders, ${rootNode.fileCount} files`); } if (rootNode.totalSize > 0) { const sizeMB = Math.round(rootNode.totalSize / 1024 / 1024); lines.push(`💾 Size: ${sizeMB.toLocaleString()} MB`); } lines.push(''); // Generate tree structure if (rootNode.children.length > 0) { renderTreeNode(rootNode, '', true, lines, args, 0); } else { lines.push(' (Empty folder)'); } // Add legend lines.push(''); lines.push('Legend:'); lines.push(' 📁 Folder 📄 File'); if (args.showCounts) { lines.push(' (n,m) = n folders, m files'); } return lines.join('\n'); } function renderTreeNode(node, prefix, isLast, lines, args, depth) { // Skip empty folders if collapse option is enabled if (args.collapseEmpty && node.isEmpty && depth > 0) { return; } const connector = depth === 0 ? '' : (isLast ? '└── ' : '├── '); const icon = node.isFolder ? '📁' : getFileIcon(node.name); let line = prefix + connector + icon + ' ' + node.name; // Add ID if requested if (args.showIds) { line += ` (${node.id})`; } // Add counts for folders if (args.showCounts && node.isFolder && (node.folderCount > 0 || node.fileCount > 0)) { line += ` (${node.folderCount},${node.fileCount})`; } // Truncate if too long const maxWidth = args.maxWidth || 120; if (line.length > maxWidth) { line = line.substring(0, maxWidth - 3) + '...'; } lines.push(line); // Add children if (node.children.length > 0) { const childPrefix = prefix + (depth === 0 ? '' : (isLast ? ' ' : '│ ')); node.children.forEach((child, index) => { const isChildLast = index === node.children.length - 1; renderTreeNode(child, childPrefix, isChildLast, lines, args, depth + 1); }); } } function getFileIcon(fileName) { const ext = fileName.split('.').pop()?.toLowerCase() || ''; switch (ext) { case 'pdf': return '📄'; case 'doc': case 'docx': return '📝'; case 'xls': case 'xlsx': return '📊'; case 'ppt': case 'pptx': return '📽️'; case 'jpg': case 'jpeg': case 'png': case 'gif': case 'bmp': return '🖼️'; case 'mp4': case 'avi': case 'mov': case 'wmv': return '🎥'; case 'mp3': case 'wav': case 'flac': return '🎵'; case 'zip': case 'rar': case '7z': return '🗜️'; case 'txt': return '📄'; default: return '📄'; } }