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

284 lines (283 loc) 12.1 kB
import { z } from 'zod'; import { google } from 'googleapis'; import { getValidCredentials } from '../auth.js'; export const name = 'gdrive_explore_directory'; export const description = 'Recursively explore a Google Drive directory structure with configurable depth and filtering options'; export const inputSchema = z.object({ folderId: z.string().describe('The Google Drive folder ID to explore from'), maxDepth: z.number().min(1).max(10).optional().default(3).describe('Maximum depth to explore (1-10)'), includeFiles: z.boolean().optional().default(true).describe('Include files in the exploration'), includeFolders: z.boolean().optional().default(true).describe('Include folders in the exploration'), fileTypeFilter: z.array(z.string()).optional().describe('Filter by file types (e.g., ["pdf", "docx", "folder"])'), showSizes: z.boolean().optional().default(false).describe('Show file sizes'), showCounts: z.boolean().optional().default(true).describe('Show file/folder counts per directory'), maxItemsPerFolder: z.number().min(1).max(1000).optional().default(50).describe('Maximum items to show per folder') }); export const schema = { name, description, inputSchema: { type: 'object', properties: { folderId: { type: 'string', description: 'The Google Drive folder ID to explore from' }, maxDepth: { type: 'number', description: 'Maximum depth to explore (1-10)', default: 3, minimum: 1, maximum: 10 }, includeFiles: { type: 'boolean', description: 'Include files in the exploration', default: true }, includeFolders: { type: 'boolean', description: 'Include folders in the exploration', default: true }, fileTypeFilter: { type: 'array', items: { type: 'string' }, description: 'Filter by file types' }, showSizes: { type: 'boolean', description: 'Show file sizes', default: false }, showCounts: { type: 'boolean', description: 'Show file/folder counts per directory', default: true }, maxItemsPerFolder: { type: 'number', description: 'Maximum items to show per folder', default: 50, minimum: 1, maximum: 1000 } }, required: ['folderId'] } }; export async function gdrive_explore_directory(args) { try { const authClient = await getValidCredentials(); const drive = google.drive({ version: 'v3', auth: authClient }); // First verify the 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 }; } // Start exploration const rootNode = await exploreFolder(drive, args.folderId, 0, '', args, new Set() // To prevent infinite loops ); if (!rootNode) { return { content: [{ type: 'text', text: 'Error: Unable to explore the specified folder' }], isError: true }; } // Format the output const output = formatExplorationResults(rootNode, args); return { content: [{ type: 'text', text: output }], isError: false }; } catch (error) { return { content: [{ type: 'text', text: `Error exploring directory: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true }; } } async function exploreFolder(drive, folderId, currentDepth, currentPath, args, visitedFolders) { // Prevent infinite loops if (visitedFolders.has(folderId)) { return null; } visitedFolders.add(folderId); try { // Get folder information const folderInfo = await drive.files.get({ fileId: folderId, fields: 'id,name,mimeType,parents,webViewLink' }); const folderItem = { id: folderInfo.data.id, name: folderInfo.data.name, mimeType: folderInfo.data.mimeType, parents: folderInfo.data.parents, webViewLink: folderInfo.data.webViewLink, isFolder: true }; // Build query for folder contents let query = `'${folderId}' in parents and trashed=false`; if (!args.includeFiles && args.includeFolders) { query += " and mimeType='application/vnd.google-apps.folder'"; } else if (args.includeFiles && !args.includeFolders) { query += " and mimeType!='application/vnd.google-apps.folder'"; } // Add file type filter if specified if (args.fileTypeFilter && args.fileTypeFilter.length > 0) { const typeFilters = args.fileTypeFilter.map(type => { if (type === 'folder') { return "mimeType='application/vnd.google-apps.folder'"; } else if (type === 'pdf') { return "mimeType='application/pdf'"; } else if (type === 'doc' || type === 'docx') { return "mimeType contains 'document'"; } else if (type === 'sheet' || type === 'xlsx') { return "mimeType contains 'spreadsheet'"; } else if (type === 'image') { return "mimeType contains 'image'"; } else { return `name contains '.${type}'`; } }); query += ` and (${typeFilters.join(' or ')})`; } const fields = args.showSizes ? 'files(id,name,mimeType,size,parents,webViewLink)' : 'files(id,name,mimeType,parents,webViewLink)'; // Get folder contents const response = await drive.files.list({ q: query, orderBy: 'folder,name', pageSize: args.maxItemsPerFolder, fields: fields }); const items = (response.data.files || []).map((file) => ({ id: file.id, name: file.name, mimeType: file.mimeType, size: file.size, parents: file.parents, webViewLink: file.webViewLink, isFolder: file.mimeType === 'application/vnd.google-apps.folder' })); // Create the node const node = { item: folderItem, depth: currentDepth, path: currentPath ? `${currentPath}/${folderItem.name}` : folderItem.name, children: [], fileCount: items.filter((item) => !item.isFolder).length, folderCount: items.filter((item) => item.isFolder).length }; // If we haven't reached max depth, explore subfolders if (currentDepth < (args.maxDepth || 3)) { const subfolders = items.filter((item) => item.isFolder); for (const subfolder of subfolders) { const childNode = await exploreFolder(drive, subfolder.id, currentDepth + 1, node.path, args, new Set(visitedFolders) // Create a new set to avoid shared state issues ); if (childNode) { node.children.push(childNode); } } } // Add non-folder items as leaf nodes if including files if (args.includeFiles) { const files = items.filter((item) => !item.isFolder); for (const file of files) { const fileNode = { item: file, depth: currentDepth + 1, path: `${node.path}/${file.name}`, children: [], fileCount: 0, folderCount: 0 }; node.children.push(fileNode); } } return node; } catch (error) { console.error(`Error exploring folder ${folderId}:`, error); return null; } } function formatExplorationResults(rootNode, args) { const output = []; output.push(`📁 Directory Exploration: ${rootNode.item.name}`); output.push(`🔍 Max Depth: ${args.maxDepth} | Items per folder: ${args.maxItemsPerFolder}`); output.push(`📊 Filters: ${args.includeFiles ? 'Files' : ''}${args.includeFiles && args.includeFolders ? '+' : ''}${args.includeFolders ? 'Folders' : ''}`); if (args.fileTypeFilter && args.fileTypeFilter.length > 0) { output.push(`🎯 File Types: ${args.fileTypeFilter.join(', ')}`); } output.push(''); formatNode(rootNode, output, '', args); // Add summary statistics const stats = calculateStats(rootNode); output.push(''); output.push('📈 Summary:'); output.push(` Total Folders: ${stats.totalFolders}`); output.push(` Total Files: ${stats.totalFiles}`); output.push(` Max Depth Reached: ${stats.maxDepth}`); if (args.showSizes && stats.totalSize > 0) { const sizeMB = Math.round(stats.totalSize / 1024 / 1024); output.push(` Total Size: ${sizeMB.toLocaleString()} MB`); } return output.join('\n'); } function formatNode(node, output, prefix, args) { const isRoot = node.depth === 0; const icon = node.item.isFolder ? '📁' : getFileIcon(node.item.mimeType); let line = `${prefix}${icon} ${node.item.name}`; if (args.showCounts && node.item.isFolder && (node.fileCount > 0 || node.folderCount > 0)) { line += ` (${node.folderCount} folders, ${node.fileCount} files)`; } if (args.showSizes && node.item.size && !node.item.isFolder) { const sizeKB = Math.round(parseInt(node.item.size) / 1024); line += ` [${sizeKB.toLocaleString()} KB]`; } output.push(line); // Add children node.children.forEach((child, index) => { const isLast = index === node.children.length - 1; const childPrefix = prefix + (isRoot ? '' : (isLast ? ' ' : '│ ')); const connector = isRoot ? '' : (isLast ? '└── ' : '├── '); formatNode(child, output, childPrefix + connector, args); }); } function getFileIcon(mimeType) { if (mimeType.includes('pdf')) return '📄'; if (mimeType.includes('image')) return '🖼️'; if (mimeType.includes('video')) return '🎥'; if (mimeType.includes('audio')) return '🎵'; if (mimeType.includes('spreadsheet')) return '📊'; if (mimeType.includes('document')) return '📝'; if (mimeType.includes('presentation')) return '📽️'; if (mimeType.includes('zip') || mimeType.includes('archive')) return '🗜️'; return '📄'; } function calculateStats(node) { let totalFolders = node.item.isFolder ? 1 : 0; let totalFiles = node.item.isFolder ? 0 : 1; let maxDepth = node.depth; let totalSize = node.item.size ? parseInt(node.item.size) : 0; for (const child of node.children) { const childStats = calculateStats(child); totalFolders += childStats.totalFolders; totalFiles += childStats.totalFiles; maxDepth = Math.max(maxDepth, childStats.maxDepth); totalSize += childStats.totalSize; } return { totalFolders, totalFiles, maxDepth, totalSize }; }