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