UNPKG

@emit-ia/gdrive-mcp-server

Version:

Google Drive MCP Server - Model Context Protocol server for Google Drive and Gmail APIs

519 lines 19.7 kB
/** * Google Drive Service for MCP Server * * This service provides comprehensive Google Drive operations using Service Account * authentication for reliable, non-expiring access. It includes file management, * folder operations, sharing/permissions, search, comments, and more. */ import { google } from "googleapis"; import { config, validateCredentials } from "./config.js"; /** * Google Drive service class that handles all Drive API operations * * Uses Service Account authentication for reliable access without token expiration. * Provides a complete set of file and folder management operations. */ export class GoogleDriveService { auth; drive; constructor() { // Validate credentials before attempting to initialize validateCredentials(); // Set up authentication and initialize the Drive API client this.initializeAuth(); this.drive = google.drive({ version: "v3", auth: this.auth }); } /** * Initialize Service Account authentication for Google Drive * * Service Account authentication is preferred over OAuth2 for Google Drive * because it provides reliable, non-expiring access without user intervention. */ initializeAuth() { // Require Service Account credentials for Google Drive access if (!config.googleServiceAccountEmail || !config.googlePrivateKey) { throw new Error("Google Drive requires Service Account authentication. Please set:\n" + "- GOOGLE_SERVICE_ACCOUNT_EMAIL\n" + "- GOOGLE_PRIVATE_KEY\n\n" + "Service accounts provide reliable, non-expiring access to Google Drive."); } // Create JWT client with Drive API scope this.auth = new google.auth.JWT(config.googleServiceAccountEmail, undefined, config.googlePrivateKey, ['https://www.googleapis.com/auth/drive']); } /** * List files and folders in Google Drive * * @param args - Search parameters including folder, query, limits, and sharing options * @returns Promise resolving to file list with metadata */ async listFiles(args = {}) { const { folderId, query, maxResults = 100, includeShared = true, } = args; // Build Google Drive search query based on parameters let searchQuery = "trashed=false"; if (folderId) { searchQuery += ` and '${folderId}' in parents`; } if (query) { searchQuery += ` and (name contains '${query}' or fullText contains '${query}')`; } if (!includeShared) { searchQuery += " and 'me' in owners"; } try { const response = await this.drive.files.list({ q: searchQuery, pageSize: Math.min(maxResults, 1000), fields: "nextPageToken, files(id, name, mimeType, size, modifiedTime, createdTime, parents, owners, shared, webViewLink, webContentLink)", orderBy: "modifiedTime desc", }); return { files: response.data.files || [], nextPageToken: response.data.nextPageToken, totalCount: response.data.files?.length || 0, }; } catch (error) { throw new Error(`Failed to list files: ${error instanceof Error ? error.message : String(error)}`); } } /** * Get detailed information about a specific file * * @param args - File ID and optional content inclusion flag * @returns Promise resolving to file metadata and optionally content */ async getFile(args) { const { fileId, includeContent = false } = args; try { const response = await this.drive.files.get({ fileId, fields: "id, name, mimeType, size, modifiedTime, createdTime, parents, owners, shared, webViewLink, webContentLink, description, properties", }); const file = response.data; // Optionally include file content for text files if (includeContent && this.isTextFile(file.mimeType)) { try { const contentResponse = await this.drive.files.get({ fileId, alt: "media", }); file.content = contentResponse.data; } catch (contentError) { file.contentError = "Failed to retrieve file content"; } } return file; } catch (error) { throw new Error(`Failed to get file: ${error instanceof Error ? error.message : String(error)}`); } } /** * Download file content, handling both regular files and Google Workspace documents * * @param args - File ID and optional export format for Google Workspace files * @returns Promise resolving to file content and metadata */ async downloadFile(args) { const { fileId, format } = args; try { const fileInfo = await this.drive.files.get({ fileId, fields: "mimeType, name", }); let response; // Handle Google Workspace files that need to be exported if (this.isGoogleWorkspaceFile(fileInfo.data.mimeType)) { const exportFormat = format || this.getDefaultExportFormat(fileInfo.data.mimeType); response = await this.drive.files.export({ fileId, mimeType: exportFormat, }); } else { // Regular files can be downloaded directly response = await this.drive.files.get({ fileId, alt: "media", }); } return { fileName: fileInfo.data.name, mimeType: fileInfo.data.mimeType, content: response.data, size: response.data.length || 0, }; } catch (error) { throw new Error(`Failed to download file: ${error instanceof Error ? error.message : String(error)}`); } } /** * Upload a new file to Google Drive * * @param args - File name, content, MIME type, and optional parent folder * @returns Promise resolving to uploaded file metadata */ async uploadFile(args) { const { name, content, mimeType = "text/plain", parentId } = args; const fileMetadata = { name, }; if (parentId) { fileMetadata.parents = [parentId]; } const media = { mimeType, body: content, }; try { const response = await this.drive.files.create({ resource: fileMetadata, media, fields: "id, name, mimeType, size, webViewLink", }); return response.data; } catch (error) { throw new Error(`Failed to upload file: ${error instanceof Error ? error.message : String(error)}`); } } async updateFile(args) { const { fileId, name, content, mimeType } = args; const fileMetadata = {}; if (name) fileMetadata.name = name; const media = {}; if (content !== undefined) { media.body = content; if (mimeType) media.mimeType = mimeType; } try { const response = await this.drive.files.update({ fileId, resource: fileMetadata, media: Object.keys(media).length > 0 ? media : undefined, fields: "id, name, mimeType, size, modifiedTime, webViewLink", }); return response.data; } catch (error) { throw new Error(`Failed to update file: ${error instanceof Error ? error.message : String(error)}`); } } async deleteFile(args) { const { fileId, permanent = false } = args; try { if (permanent) { await this.drive.files.delete({ fileId }); return { success: true, message: "File permanently deleted" }; } else { await this.drive.files.update({ fileId, resource: { trashed: true }, }); return { success: true, message: "File moved to trash" }; } } catch (error) { throw new Error(`Failed to delete file: ${error instanceof Error ? error.message : String(error)}`); } } async copyFile(args) { const { fileId, name, parentId } = args; const fileMetadata = {}; if (name) fileMetadata.name = name; if (parentId) fileMetadata.parents = [parentId]; try { const response = await this.drive.files.copy({ fileId, resource: fileMetadata, fields: "id, name, mimeType, parents, webViewLink", }); return response.data; } catch (error) { throw new Error(`Failed to copy file: ${error instanceof Error ? error.message : String(error)}`); } } async moveFile(args) { const { fileId, newParentId, removeFromParents } = args; try { // Get current parents if we need to remove from specific ones let removeParents = removeFromParents; if (!removeParents) { const file = await this.drive.files.get({ fileId, fields: "parents", }); removeParents = file.data.parents?.join(","); } const response = await this.drive.files.update({ fileId, addParents: newParentId, removeParents, fields: "id, name, parents", }); return response.data; } catch (error) { throw new Error(`Failed to move file: ${error instanceof Error ? error.message : String(error)}`); } } async createFolder(args) { const { name, parentId } = args; const fileMetadata = { name, mimeType: "application/vnd.google-apps.folder", }; if (parentId) { fileMetadata.parents = [parentId]; } try { const response = await this.drive.files.create({ resource: fileMetadata, fields: "id, name, mimeType, parents, webViewLink", }); return response.data; } catch (error) { throw new Error(`Failed to create folder: ${error instanceof Error ? error.message : String(error)}`); } } async getFolderInfo(args) { const { folderId, includeFiles = true } = args; try { // Get folder metadata const folderResponse = await this.drive.files.get({ fileId: folderId, fields: "id, name, mimeType, modifiedTime, createdTime, parents, shared, webViewLink", }); const folder = folderResponse.data; if (includeFiles) { // Get folder contents const filesResponse = await this.listFiles({ folderId }); folder.files = filesResponse.files; folder.fileCount = filesResponse.totalCount; } return folder; } catch (error) { throw new Error(`Failed to get folder info: ${error instanceof Error ? error.message : String(error)}`); } } async shareFile(args) { const { fileId, email, role = "reader", type = "user", sendNotificationEmail = true, } = args; const permission = { role, type, }; if (email && type === "user") { permission.emailAddress = email; } try { const response = await this.drive.permissions.create({ fileId, resource: permission, sendNotificationEmail, fields: "id, role, type, emailAddress, displayName", }); return response.data; } catch (error) { throw new Error(`Failed to share file: ${error instanceof Error ? error.message : String(error)}`); } } async getPermissions(args) { const { fileId } = args; try { const response = await this.drive.permissions.list({ fileId, fields: "permissions(id, role, type, emailAddress, displayName, expirationTime)", }); return { permissions: response.data.permissions || [], }; } catch (error) { throw new Error(`Failed to get permissions: ${error instanceof Error ? error.message : String(error)}`); } } async removePermission(args) { const { fileId, permissionId } = args; try { await this.drive.permissions.delete({ fileId, permissionId, }); return { success: true, message: "Permission removed" }; } catch (error) { throw new Error(`Failed to remove permission: ${error instanceof Error ? error.message : String(error)}`); } } async search(args = {}) { const { query, mimeType, modifiedTime, owner, maxResults = 100, } = args; let searchQuery = "trashed=false"; if (query) { searchQuery += ` and (name contains '${query}' or fullText contains '${query}')`; } if (mimeType) { searchQuery += ` and mimeType='${mimeType}'`; } if (modifiedTime) { searchQuery += ` and modifiedTime > '${modifiedTime}'`; } if (owner) { searchQuery += ` and '${owner}' in owners`; } try { const response = await this.drive.files.list({ q: searchQuery, pageSize: Math.min(maxResults, 1000), fields: "nextPageToken, files(id, name, mimeType, size, modifiedTime, createdTime, owners, webViewLink)", orderBy: "relevance", }); return { files: response.data.files || [], query: searchQuery, totalCount: response.data.files?.length || 0, }; } catch (error) { throw new Error(`Failed to search files: ${error instanceof Error ? error.message : String(error)}`); } } async getRecentFiles(args = {}) { const { maxResults = 20, daysBack = 7 } = args; const date = new Date(); date.setDate(date.getDate() - daysBack); const modifiedTime = date.toISOString(); return this.search({ modifiedTime, maxResults, }); } async getComments(args) { const { fileId, includeDeleted = false } = args; try { const response = await this.drive.comments.list({ fileId, includeDeleted, fields: "comments(id, content, createdTime, modifiedTime, author, replies)", }); return { comments: response.data.comments || [], }; } catch (error) { throw new Error(`Failed to get comments: ${error instanceof Error ? error.message : String(error)}`); } } async addComment(args) { const { fileId, content, anchor } = args; const comment = { content, }; if (anchor) { comment.anchor = anchor; } try { const response = await this.drive.comments.create({ fileId, resource: comment, fields: "id, content, createdTime, author", }); return response.data; } catch (error) { throw new Error(`Failed to add comment: ${error instanceof Error ? error.message : String(error)}`); } } async getRevisions(args) { const { fileId } = args; try { const response = await this.drive.revisions.list({ fileId, fields: "revisions(id, modifiedTime, size, originalFilename, mimeType)", }); return { revisions: response.data.revisions || [], }; } catch (error) { throw new Error(`Failed to get revisions: ${error instanceof Error ? error.message : String(error)}`); } } async getAbout() { try { const response = await this.drive.about.get({ fields: "user, storageQuota, importFormats, exportFormats, maxImportSizes, maxUploadSize", }); return response.data; } catch (error) { throw new Error(`Failed to get account info: ${error instanceof Error ? error.message : String(error)}`); } } async emptyTrash() { try { await this.drive.files.emptyTrash(); return { success: true, message: "Trash emptied successfully" }; } catch (error) { throw new Error(`Failed to empty trash: ${error instanceof Error ? error.message : String(error)}`); } } // Helper methods for file type detection and format conversion /** * Check if a MIME type represents a text file that can be read as content * * @param mimeType - The MIME type to check * @returns True if the file is a readable text file */ isTextFile(mimeType) { const textMimeTypes = [ "text/plain", "text/html", "text/css", "text/javascript", "application/json", "application/xml", "text/xml", "text/csv", "text/markdown", ]; return textMimeTypes.includes(mimeType) || mimeType.startsWith("text/"); } /** * Check if a file is a Google Workspace document that requires export * * @param mimeType - The MIME type to check * @returns True if the file is a Google Workspace document */ isGoogleWorkspaceFile(mimeType) { return mimeType.startsWith("application/vnd.google-apps."); } /** * Get the default export format for Google Workspace files * * @param mimeType - The Google Workspace MIME type * @returns The appropriate export format MIME type */ getDefaultExportFormat(mimeType) { const exportFormats = { "application/vnd.google-apps.document": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", // Word .docx "application/vnd.google-apps.spreadsheet": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // Excel .xlsx "application/vnd.google-apps.presentation": "application/vnd.openxmlformats-officedocument.presentationml.presentation", // PowerPoint .pptx "application/vnd.google-apps.drawing": "image/png", // PNG image "application/vnd.google-apps.script": "application/vnd.google-apps.script+json", // Apps Script JSON }; return exportFormats[mimeType] || "application/pdf"; // Default to PDF } } //# sourceMappingURL=google-drive.js.map