Skip to main content
Glama

Google Drive MCP Server

by ducla5
file-manager.ts16.3 kB
import { google, drive_v3 } from 'googleapis'; import { AuthService } from '../auth/auth-service.js'; import { FileManager, DriveFile, FileMetadata, User, Permission, FileCapabilities } from '../types/drive.js'; import { SearchQuery } from '../types/search.js'; import { ResilientExecutor } from '../utils/retry-handler.js'; import { InvalidFileIdError, PermissionDeniedError, FileTooLargeError, UnsupportedFileTypeError } from '../types/errors.js'; /** * Google Drive File Manager * Handles file operations with Google Drive API including download, metadata retrieval, and folder listing */ export class GoogleDriveFileManager implements FileManager { private driveApi: drive_v3.Drive; private authService: AuthService; private resilientExecutor: ResilientExecutor; constructor( authService: AuthService, resilientExecutor?: ResilientExecutor ) { this.authService = authService; this.driveApi = google.drive({ version: 'v3', auth: authService.getOAuthManager().getOAuth2Client() }); this.resilientExecutor = resilientExecutor || new ResilientExecutor(); } /** * Search files on Google Drive */ async searchFiles(query: SearchQuery): Promise<DriveFile[]> { return this.resilientExecutor.execute(async () => { return this.authService.executeWithAuth(async () => { const searchQuery = this.buildSearchQuery(query); const response = await this.driveApi.files.list({ q: searchQuery, pageSize: query.limit || 100, fields: 'nextPageToken, files(id, name, mimeType, size, modifiedTime, createdTime, owners, permissions, webViewLink, webContentLink, parents)', orderBy: query.orderBy ? `${query.orderBy} ${query.orderDirection || 'desc'}` : 'modifiedTime desc', supportsAllDrives: true, includeItemsFromAllDrives: true }); if (!response.data.files) { return []; } return response.data.files.map(file => this.mapGoogleFileTodriveFile(file)); }, 'searchFiles'); }, { operation: 'searchFiles', query }); } /** * Get file information */ async getFile(fileId: string): Promise<DriveFile> { // Validate file ID format if (!this.isValidFileId(fileId)) { throw new InvalidFileIdError(`Invalid Google Drive file ID format: ${fileId}`); } return this.resilientExecutor.execute(async () => { return this.authService.executeWithAuth(async () => { try { const response = await this.driveApi.files.get({ fileId, fields: 'id, name, mimeType, size, modifiedTime, createdTime, owners, permissions, webViewLink, webContentLink, parents', supportsAllDrives: true }); return this.mapGoogleFileTodriveFile(response.data); } catch (error: any) { if (error.status === 404) { throw new InvalidFileIdError(`File not found: ${fileId}. The file may have been deleted or you may not have access to it.`); } if (error.status === 403) { throw new PermissionDeniedError(`Access denied to file: ${fileId}. You may not have permission to view this file.`); } throw error; } }, 'getFile'); }, { operation: 'getFile', fileId }); } /** * Download file content */ async downloadFile(fileId: string): Promise<Buffer> { // Validate file ID format if (!this.isValidFileId(fileId)) { throw new InvalidFileIdError(`Invalid Google Drive file ID format: ${fileId}`); } return this.resilientExecutor.execute(async () => { return this.authService.executeWithAuth(async () => { try { // First get file metadata to determine how to download const fileMetadata = await this.driveApi.files.get({ fileId, fields: 'mimeType, name, size', supportsAllDrives: true }); const mimeType = fileMetadata.data.mimeType; const fileSize = parseInt(fileMetadata.data.size || '0'); // Check file size limits (100MB default) const maxFileSize = 100 * 1024 * 1024; // 100MB if (fileSize > maxFileSize) { throw new FileTooLargeError( `File is too large (${Math.round(fileSize / 1024 / 1024)}MB). Maximum supported size is ${Math.round(maxFileSize / 1024 / 1024)}MB.`, { fileId, fileSize, maxFileSize } ); } // Handle Google Workspace files (need to export) if (mimeType?.startsWith('application/vnd.google-apps.')) { return this.exportGoogleWorkspaceFile(fileId, mimeType); } // Check if file type is supported if (!this.isSupportedFileType(mimeType || undefined)) { throw new UnsupportedFileTypeError( `File type not supported: ${mimeType}. Supported types include PDF, Word documents, Google Docs, and plain text files.`, { fileId, mimeType } ); } // Handle regular files (direct download) const response = await this.driveApi.files.get({ fileId, alt: 'media', supportsAllDrives: true }, { responseType: 'arraybuffer' }); return Buffer.from(response.data as ArrayBuffer); } catch (error: any) { if (error.status === 404) { throw new InvalidFileIdError(`File not found: ${fileId}`); } if (error.status === 403) { throw new PermissionDeniedError(`Access denied to file: ${fileId}`); } throw error; } }, 'downloadFile'); }, { operation: 'downloadFile', fileId }); } /** * Get comprehensive file metadata */ async getFileMetadata(fileId: string): Promise<FileMetadata> { return this.authService.executeWithAuth(async () => { const response = await this.driveApi.files.get({ fileId, fields: 'id, name, mimeType, size, modifiedTime, createdTime, owners, permissions, webViewLink, webContentLink, parents, description, starred, trashed, version, lastModifyingUser, sharingUser, capabilities', supportsAllDrives: true }); return this.mapGoogleFileToFileMetadata(response.data); }, 'getFileMetadata'); } /** * List folder contents */ async listFolder(folderId: string): Promise<DriveFile[]> { return this.authService.executeWithAuth(async () => { const response = await this.driveApi.files.list({ q: `'${folderId}' in parents and trashed = false`, fields: 'nextPageToken, files(id, name, mimeType, size, modifiedTime, createdTime, owners, permissions, webViewLink, webContentLink, parents)', orderBy: 'folder, name', supportsAllDrives: true, includeItemsFromAllDrives: true }); if (!response.data.files) { return []; } return response.data.files.map(file => this.mapGoogleFileTodriveFile(file)); }, 'listFolder'); } /** * Build Google Drive search query from SearchQuery object */ private buildSearchQuery(query: SearchQuery): string { const conditions: string[] = []; // Text search in name and content if (query.text) { conditions.push(`(name contains '${this.escapeQueryString(query.text)}' or fullText contains '${this.escapeQueryString(query.text)}')`); } // File type filter if (query.fileType && query.fileType.length > 0) { const mimeTypeConditions = query.fileType.map((type: string) => { // Handle common file type shortcuts const mimeTypeMap: { [key: string]: string } = { 'pdf': 'application/pdf', 'doc': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'gdoc': 'application/vnd.google-apps.document', 'gsheet': 'application/vnd.google-apps.spreadsheet', 'gslide': 'application/vnd.google-apps.presentation', 'folder': 'application/vnd.google-apps.folder', 'image': 'image/', 'video': 'video/', 'audio': 'audio/' }; const mimeType = mimeTypeMap[type.toLowerCase()] || type; // For partial mime types (like 'image/'), use contains if (mimeType.endsWith('/')) { return `mimeType contains '${mimeType}'`; } return `mimeType = '${mimeType}'`; }); conditions.push(`(${mimeTypeConditions.join(' or ')})`); } // Folder filter if (query.folderId) { conditions.push(`'${query.folderId}' in parents`); } // Date filters if (query.modifiedAfter) { conditions.push(`modifiedTime > '${query.modifiedAfter.toISOString()}'`); } if (query.modifiedBefore) { conditions.push(`modifiedTime < '${query.modifiedBefore.toISOString()}'`); } // Owner filter if (query.owner) { conditions.push(`'${this.escapeQueryString(query.owner)}' in owners`); } // Always exclude trashed files unless explicitly requested conditions.push('trashed = false'); return conditions.join(' and '); } /** * Export Google Workspace files to appropriate formats */ private async exportGoogleWorkspaceFile(fileId: string, mimeType: string): Promise<Buffer> { let exportMimeType: string; // Determine export format based on Google Workspace file type switch (mimeType) { case 'application/vnd.google-apps.document': exportMimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; // .docx break; case 'application/vnd.google-apps.spreadsheet': exportMimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; // .xlsx break; case 'application/vnd.google-apps.presentation': exportMimeType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; // .pptx break; case 'application/vnd.google-apps.drawing': exportMimeType = 'image/png'; break; default: // Fallback to plain text for other Google Workspace files exportMimeType = 'text/plain'; } const response = await this.driveApi.files.export({ fileId, mimeType: exportMimeType }, { responseType: 'arraybuffer' }); return Buffer.from(response.data as ArrayBuffer); } /** * Map Google Drive API file object to our DriveFile interface */ private mapGoogleFileTodriveFile(file: drive_v3.Schema$File): DriveFile { return { id: file.id!, name: file.name || 'Untitled', mimeType: file.mimeType || 'application/octet-stream', size: parseInt(file.size || '0'), modifiedTime: new Date(file.modifiedTime || Date.now()), createdTime: new Date(file.createdTime || Date.now()), owners: (file.owners || []).map(owner => this.mapGoogleUserToUser(owner)), permissions: (file.permissions || []).map(permission => this.mapGooglePermissionToPermission(permission)), webViewLink: file.webViewLink || '', webContentLink: file.webContentLink || '', parents: file.parents || [] }; } /** * Map Google Drive API file object to our FileMetadata interface */ private mapGoogleFileToFileMetadata(file: drive_v3.Schema$File): FileMetadata { return { id: file.id!, name: file.name || 'Untitled', mimeType: file.mimeType || 'application/octet-stream', size: parseInt(file.size || '0'), modifiedTime: new Date(file.modifiedTime || Date.now()), createdTime: new Date(file.createdTime || Date.now()), owners: (file.owners || []).map(owner => this.mapGoogleUserToUser(owner)), permissions: (file.permissions || []).map(permission => this.mapGooglePermissionToPermission(permission)), webViewLink: file.webViewLink || '', webContentLink: file.webContentLink || '', parents: file.parents || [], description: file.description || '', starred: file.starred || false, trashed: file.trashed || false, version: file.version || '1', lastModifyingUser: this.mapGoogleUserToUser(file.lastModifyingUser || {}), sharingUser: this.mapGoogleUserToUser(file.sharingUser || {}), capabilities: this.mapGoogleCapabilitiesToFileCapabilities(file.capabilities || {}) }; } /** * Map Google Drive API user object to our User interface */ private mapGoogleUserToUser(user: drive_v3.Schema$User): User { return { displayName: user.displayName || 'Unknown User', emailAddress: user.emailAddress || '', photoLink: user.photoLink || '', me: user.me || false }; } /** * Map Google Drive API permission object to our Permission interface */ private mapGooglePermissionToPermission(permission: drive_v3.Schema$Permission): Permission { return { id: permission.id!, type: (permission.type as any) || 'user', role: (permission.role as any) || 'reader', emailAddress: permission.emailAddress || '', domain: permission.domain || '', displayName: permission.displayName || '' }; } /** * Map Google Drive API capabilities to our FileCapabilities interface */ private mapGoogleCapabilitiesToFileCapabilities(capabilities: any): FileCapabilities { return { canAddChildren: capabilities.canAddChildren || false, canChangeCopyRequiresWriterPermission: capabilities.canChangeCopyRequiresWriterPermission || false, canChangeViewersCanCopyContent: capabilities.canChangeViewersCanCopyContent || false, canComment: capabilities.canComment || false, canCopy: capabilities.canCopy || false, canDelete: capabilities.canDelete || false, canDownload: capabilities.canDownload || false, canEdit: capabilities.canEdit || false, canListChildren: capabilities.canListChildren || false, canMoveItemIntoTeamDrive: capabilities.canMoveItemIntoTeamDrive || false, canMoveItemOutOfTeamDrive: capabilities.canMoveItemOutOfTeamDrive || false, canMoveItemWithinTeamDrive: capabilities.canMoveItemWithinTeamDrive || false, canReadRevisions: capabilities.canReadRevisions || false, canRemoveChildren: capabilities.canRemoveChildren || false, canRename: capabilities.canRename || false, canShare: capabilities.canShare || false, canTrash: capabilities.canTrash || false, canUntrash: capabilities.canUntrash || false }; } /** * Escape special characters in search query strings */ private escapeQueryString(query: string): string { // Escape single quotes and backslashes for Google Drive search return query.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); } /** * Validate Google Drive file ID format */ private isValidFileId(fileId: string): boolean { // Google Drive file IDs are typically 28-44 characters long and contain alphanumeric characters, hyphens, and underscores const fileIdRegex = /^[a-zA-Z0-9_-]{10,50}$/; return fileIdRegex.test(fileId); } /** * Check if file type is supported for processing */ private isSupportedFileType(mimeType?: string): boolean { if (!mimeType) return false; const supportedTypes = [ // Google Workspace files 'application/vnd.google-apps.document', 'application/vnd.google-apps.spreadsheet', 'application/vnd.google-apps.presentation', // Microsoft Office files 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx 'application/msword', // .doc // PDF files 'application/pdf', // Text files 'text/plain', 'text/markdown', 'text/html', 'text/csv', // Other common formats 'application/rtf', 'application/json' ]; return supportedTypes.includes(mimeType) || mimeType.startsWith('text/'); } }

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/ducla5/gdriver-mcp'

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