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
JavaScript
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 '📄';
}
}