UNPKG

mcp-google-drive

Version:

Advanced MCP server for Google Drive integration with full CRUD operations, file management, and sharing capabilities. Supports both OAuth2 and Service Account authentication.

1,109 lines 43.9 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { google } from 'googleapis'; import { z } from 'zod'; // Enhanced logging const log = (message, level = 'info') => { const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] [${level.toUpperCase()}] MCP-Google-Drive: ${message}`; if (level === 'error') { console.error(logMessage); } else if (process.env.LOG_LEVEL === 'debug' || level === 'info') { console.error(logMessage); // Use stderr for MCP logging } }; // Google Drive API setup with enhanced error handling let auth; let drive; let isInitialized = false; // Retry utility with exponential backoff async function retryWithBackoff(operation, maxRetries = 3, initialDelay = 1000) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await operation(); } catch (error) { if (attempt === maxRetries) { throw error; } // Check if error is retryable (rate limits, network issues) const isRetryable = error.code === 403 || error.code === 429 || error.code === 500 || error.code === 502 || error.code === 503 || error.code === 504 || error.message?.includes('rate limit') || error.message?.includes('quota'); if (!isRetryable) { throw error; } const delay = initialDelay * Math.pow(2, attempt - 1) + Math.random() * 1000; log(`Attempt ${attempt} failed, retrying in ${Math.round(delay)}ms: ${error.message}`, 'debug'); await new Promise(resolve => setTimeout(resolve, delay)); } } throw new Error('Max retries exceeded'); } // Simple in-memory cache for frequently accessed data const cache = new Map(); function getCachedData(key) { const cached = cache.get(key); if (!cached) return null; if (Date.now() - cached.timestamp > cached.ttl) { cache.delete(key); return null; } return cached.data; } function setCachedData(key, data, ttl = 300000) { cache.set(key, { data, timestamp: Date.now(), ttl }); } function initializeGoogleAuth() { try { log('Initializing Google Drive authentication...', 'debug'); // Check if OAuth2 credentials are provided if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { log('Using OAuth2 authentication...', 'info'); const oauth2Client = new google.auth.OAuth2(process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, 'urn:ietf:wg:oauth:2.0:oob'); if (process.env.GOOGLE_REFRESH_TOKEN) { oauth2Client.setCredentials({ refresh_token: process.env.GOOGLE_REFRESH_TOKEN }); auth = oauth2Client; log('OAuth2 authentication configured with refresh token', 'info'); } else { log('OAuth2 refresh token not provided, falling back to Service Account', 'info'); throw new Error('OAuth2 refresh token required'); } } else if (process.env.GOOGLE_SERVICE_ACCOUNT_KEY) { log('Using Service Account authentication...', 'info'); let credentials; try { // Try to parse as JSON if it's a JSON string const serviceAccountKey = process.env.GOOGLE_SERVICE_ACCOUNT_KEY; // Handle escaped newlines in private key const cleanedKey = serviceAccountKey.replace(/\\n/g, '\n'); credentials = JSON.parse(cleanedKey); log('Successfully parsed Service Account credentials as JSON', 'debug'); // Validate credentials structure if (!credentials.private_key || !credentials.client_email) { throw new Error('Invalid Service Account credentials structure'); } // Ensure private key is properly formatted if (!credentials.private_key.includes('-----BEGIN PRIVATE KEY-----')) { throw new Error('Invalid private key format'); } } catch (parseError) { log('Failed to parse Service Account credentials as JSON, treating as file path', 'debug'); // If parsing fails, treat as file path auth = new google.auth.GoogleAuth({ keyFile: process.env.GOOGLE_SERVICE_ACCOUNT_KEY, scopes: [ 'https://www.googleapis.com/auth/drive.readonly', 'https://www.googleapis.com/auth/drive.metadata.readonly', 'https://www.googleapis.com/auth/drive.file', ], }); isInitialized = true; log('Google Drive API initialized successfully with keyFile', 'info'); return; } auth = new google.auth.GoogleAuth({ credentials: credentials, scopes: [ 'https://www.googleapis.com/auth/drive.readonly', 'https://www.googleapis.com/auth/drive.metadata.readonly', 'https://www.googleapis.com/auth/drive.file', ], }); } else { throw new Error('No authentication credentials provided. Please set either GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET or GOOGLE_SERVICE_ACCOUNT_KEY'); } log('Google Drive authentication configured successfully', 'info'); } catch (error) { log(`Failed to initialize Google Drive API: ${error}`, 'error'); throw error; } } // Initialize authentication with retry let retryCount = 0; const maxRetries = 3; async function initializeWithRetry() { try { initializeGoogleAuth(); drive = google.drive({ version: 'v3', auth }); isInitialized = true; log('Google Drive API fully initialized', 'info'); } catch (error) { retryCount++; if (retryCount < maxRetries) { log(`Authentication failed, retrying... (${retryCount}/${maxRetries})`, 'info'); await new Promise(resolve => setTimeout(resolve, 1000 * retryCount)); return initializeWithRetry(); } else { log(`Authentication failed after ${maxRetries} attempts`, 'error'); throw error; } } } // Initialize authentication immediately when module loads (async () => { try { await initializeWithRetry(); } catch (error) { log(`Critical: Failed to initialize Google Drive API: ${error}`, 'error'); } })(); // Tool schemas const SearchFilesSchema = z.object({ query: z.string().describe('Search query for files in Google Drive'), maxResults: z.number().optional().default(20).describe('Maximum number of results to return'), fileType: z .string() .optional() .describe("Filter by file type (e.g., 'application/vnd.google-apps.spreadsheet')"), orderBy: z.string().optional().describe("Order by field (e.g., 'name', 'modifiedTime', 'size')"), includeTrashed: z.boolean().optional().default(false).describe('Include trashed files'), }); const GetFileSchema = z.object({ fileId: z.string().describe('ID of the file to retrieve'), includeContent: z.boolean().optional().default(false).describe('Whether to include file content'), includePermissions: z.boolean().optional().default(false).describe('Include file permissions'), }); const ListFilesSchema = z.object({ pageSize: z.number().optional().default(20).describe('Number of files to return'), pageToken: z.string().optional().describe('Token for pagination'), orderBy: z.string().optional().describe("Order by field (e.g., 'name', 'modifiedTime', 'size')"), q: z.string().optional().describe('Query string to filter files'), driveId: z.string().optional().describe('ID of the shared drive'), includeItemsFromAllDrives: z .boolean() .optional() .default(false) .describe('Include items from all drives'), }); const GetFileContentSchema = z.object({ fileId: z.string().describe('ID of the file to get content from'), mimeType: z .string() .optional() .describe("MIME type for export (e.g., 'text/plain', 'application/pdf')"), encoding: z.string().optional().describe("Encoding for text files (e.g., 'utf-8', 'latin1')"), }); const CreateFileSchema = z.object({ name: z.string().describe('Name of the file to create'), mimeType: z.string().describe('MIME type of the file'), content: z.string().describe('Content of the file'), parentId: z.string().optional().describe('ID of the parent folder'), description: z.string().optional().describe('Description of the file'), }); const UpdateFileSchema = z.object({ fileId: z.string().describe('ID of the file to update'), name: z.string().optional().describe('New name for the file'), description: z.string().optional().describe('New description for the file'), content: z.string().optional().describe('New content for the file'), }); const DeleteFileSchema = z.object({ fileId: z.string().describe('ID of the file to delete'), permanent: z.boolean().optional().default(false).describe('Permanently delete the file'), }); const CopyFileSchema = z.object({ fileId: z.string().describe('ID of the file to copy'), name: z.string().optional().describe('Name for the copied file'), parentId: z.string().optional().describe('ID of the destination folder'), }); const MoveFileSchema = z.object({ fileId: z.string().describe('ID of the file to move'), parentId: z.string().describe('ID of the destination folder'), removeFromParents: z.boolean().optional().default(true).describe('Remove from current parent'), }); const CreateFolderSchema = z.object({ name: z.string().describe('Name of the folder to create'), parentId: z.string().optional().describe('ID of the parent folder'), description: z.string().optional().describe('Description of the folder'), }); const GetFilePermissionsSchema = z.object({ fileId: z.string().describe('ID of the file to get permissions for'), }); const ShareFileSchema = z.object({ fileId: z.string().describe('ID of the file to share'), email: z.string().describe('Email address to share with'), role: z.enum(['reader', 'writer', 'commenter', 'owner']).describe('Role for the user'), message: z.string().optional().describe('Message to include in sharing email'), }); const GetDriveInfoSchema = z.object({ driveId: z.string().optional().describe("ID of the drive (defaults to 'root')"), }); const ListSharedDrivesSchema = z.object({ pageSize: z.number().optional().default(20).describe('Number of drives to return'), pageToken: z.string().optional().describe('Token for pagination'), }); const GetFileRevisionsSchema = z.object({ fileId: z.string().describe('ID of the file to get revisions for'), maxResults: z.number().optional().default(10).describe('Maximum number of revisions to return'), }); // Tools const tools = [ { name: 'search_files', description: "Search for files in Google Drive using Google's search syntax", inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query for files in Google Drive' }, maxResults: { type: 'number', description: 'Maximum number of results to return', default: 20, }, fileType: { type: 'string', description: "Filter by file type (e.g., 'application/vnd.google-apps.spreadsheet')", }, orderBy: { type: 'string', description: "Order by field (e.g., 'name', 'modifiedTime', 'size')", }, includeTrashed: { type: 'boolean', description: 'Include trashed files', default: false }, }, required: ['query'], }, }, { name: 'get_file', description: 'Get file metadata and optionally content', inputSchema: { type: 'object', properties: { fileId: { type: 'string', description: 'ID of the file to retrieve' }, includeContent: { type: 'boolean', description: 'Whether to include file content', default: false, }, includePermissions: { type: 'boolean', description: 'Include file permissions', default: false, }, }, required: ['fileId'], }, }, { name: 'list_files', description: 'List files in Google Drive with optional filtering', inputSchema: { type: 'object', properties: { pageSize: { type: 'number', description: 'Number of files to return', default: 20 }, pageToken: { type: 'string', description: 'Token for pagination' }, orderBy: { type: 'string', description: "Order by field (e.g., 'name', 'modifiedTime', 'size')", }, q: { type: 'string', description: 'Query string to filter files' }, driveId: { type: 'string', description: 'ID of the shared drive' }, includeItemsFromAllDrives: { type: 'boolean', description: 'Include items from all drives', default: false, }, }, }, }, { name: 'get_file_content', description: 'Get file content in various formats', inputSchema: { type: 'object', properties: { fileId: { type: 'string', description: 'ID of the file to get content from' }, mimeType: { type: 'string', description: "MIME type for export (e.g., 'text/plain', 'application/pdf')", }, encoding: { type: 'string', description: "Encoding for text files (e.g., 'utf-8', 'latin1')", }, }, required: ['fileId'], }, }, { name: 'create_file', description: 'Create a new file in Google Drive', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Name of the file to create' }, mimeType: { type: 'string', description: 'MIME type of the file' }, content: { type: 'string', description: 'Content of the file' }, parentId: { type: 'string', description: 'ID of the parent folder' }, description: { type: 'string', description: 'Description of the file' }, }, required: ['name', 'mimeType', 'content'], }, }, { name: 'update_file', description: 'Update an existing file in Google Drive', inputSchema: { type: 'object', properties: { fileId: { type: 'string', description: 'ID of the file to update' }, name: { type: 'string', description: 'New name for the file' }, description: { type: 'string', description: 'New description for the file' }, content: { type: 'string', description: 'New content for the file' }, }, required: ['fileId'], }, }, { name: 'delete_file', description: 'Delete a file from Google Drive', inputSchema: { type: 'object', properties: { fileId: { type: 'string', description: 'ID of the file to delete' }, permanent: { type: 'boolean', description: 'Permanently delete the file', default: false }, }, required: ['fileId'], }, }, { name: 'copy_file', description: 'Copy a file to a new location', inputSchema: { type: 'object', properties: { fileId: { type: 'string', description: 'ID of the file to copy' }, name: { type: 'string', description: 'Name for the copied file' }, parentId: { type: 'string', description: 'ID of the destination folder' }, }, required: ['fileId'], }, }, { name: 'move_file', description: 'Move a file to a new location', inputSchema: { type: 'object', properties: { fileId: { type: 'string', description: 'ID of the file to move' }, parentId: { type: 'string', description: 'ID of the destination folder' }, removeFromParents: { type: 'boolean', description: 'Remove from current parent', default: true, }, }, required: ['fileId', 'parentId'], }, }, { name: 'create_folder', description: 'Create a new folder in Google Drive', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Name of the folder to create' }, parentId: { type: 'string', description: 'ID of the parent folder' }, description: { type: 'string', description: 'Description of the folder' }, }, required: ['name'], }, }, { name: 'get_file_permissions', description: 'Get permissions for a file', inputSchema: { type: 'object', properties: { fileId: { type: 'string', description: 'ID of the file to get permissions for' }, }, required: ['fileId'], }, }, { name: 'share_file', description: 'Share a file with another user', inputSchema: { type: 'object', properties: { fileId: { type: 'string', description: 'ID of the file to share' }, email: { type: 'string', description: 'Email address to share with' }, role: { type: 'string', enum: ['reader', 'writer', 'commenter', 'owner'], description: 'Role for the user', }, message: { type: 'string', description: 'Message to include in sharing email' }, }, required: ['fileId', 'email', 'role'], }, }, { name: 'get_drive_info', description: 'Get information about a drive', inputSchema: { type: 'object', properties: { driveId: { type: 'string', description: "ID of the drive (defaults to 'root')" }, }, }, }, { name: 'list_shared_drives', description: 'List all shared drives', inputSchema: { type: 'object', properties: { pageSize: { type: 'number', description: 'Number of drives to return', default: 20 }, pageToken: { type: 'string', description: 'Token for pagination' }, }, }, }, { name: 'get_file_revisions', description: 'Get revision history of a file', inputSchema: { type: 'object', properties: { fileId: { type: 'string', description: 'ID of the file to get revisions for' }, maxResults: { type: 'number', description: 'Maximum number of revisions to return', default: 10, }, }, required: ['fileId'], }, }, ]; // Tool implementations // Vietnamese text normalization utilities function removeVietnameseDiacritics(str) { const map = { 'à': 'a', 'á': 'a', 'ạ': 'a', 'ả': 'a', 'ã': 'a', 'â': 'a', 'ầ': 'a', 'ấ': 'a', 'ậ': 'a', 'ẩ': 'a', 'ẫ': 'a', 'ă': 'a', 'ằ': 'a', 'ắ': 'a', 'ặ': 'a', 'ẳ': 'a', 'ẵ': 'a', 'è': 'e', 'é': 'e', 'ẹ': 'e', 'ẻ': 'e', 'ẽ': 'e', 'ê': 'e', 'ề': 'e', 'ế': 'e', 'ệ': 'e', 'ể': 'e', 'ễ': 'e', 'ì': 'i', 'í': 'i', 'ị': 'i', 'ỉ': 'i', 'ĩ': 'i', 'ò': 'o', 'ó': 'o', 'ọ': 'o', 'ỏ': 'o', 'õ': 'o', 'ô': 'o', 'ồ': 'o', 'ố': 'o', 'ộ': 'o', 'ổ': 'o', 'ỗ': 'o', 'ơ': 'o', 'ờ': 'o', 'ớ': 'o', 'ợ': 'o', 'ở': 'o', 'ỡ': 'o', 'ù': 'u', 'ú': 'u', 'ụ': 'u', 'ủ': 'u', 'ũ': 'u', 'ư': 'u', 'ừ': 'u', 'ứ': 'u', 'ự': 'u', 'ử': 'u', 'ữ': 'u', 'ỳ': 'y', 'ý': 'y', 'ỵ': 'y', 'ỷ': 'y', 'ỹ': 'y', 'đ': 'd', 'À': 'A', 'Á': 'A', 'Ạ': 'A', 'Ả': 'A', 'Ã': 'A', 'Â': 'A', 'Ầ': 'A', 'Ấ': 'A', 'Ậ': 'A', 'Ẩ': 'A', 'Ẫ': 'A', 'Ă': 'A', 'Ằ': 'A', 'Ắ': 'A', 'Ặ': 'A', 'Ẳ': 'A', 'Ẵ': 'A', 'È': 'E', 'É': 'E', 'Ẹ': 'E', 'Ẻ': 'E', 'Ẽ': 'E', 'Ê': 'E', 'Ề': 'E', 'Ế': 'E', 'Ệ': 'E', 'Ể': 'E', 'Ễ': 'E', 'Ì': 'I', 'Í': 'I', 'Ị': 'I', 'Ỉ': 'I', 'Ĩ': 'I', 'Ò': 'O', 'Ó': 'O', 'Ọ': 'O', 'Ỏ': 'O', 'Õ': 'O', 'Ô': 'O', 'Ồ': 'O', 'Ố': 'O', 'Ộ': 'O', 'Ổ': 'O', 'Ỗ': 'O', 'Ơ': 'O', 'Ờ': 'O', 'Ớ': 'O', 'Ợ': 'O', 'Ở': 'O', 'Ỡ': 'O', 'Ù': 'U', 'Ú': 'U', 'Ụ': 'U', 'Ủ': 'U', 'Ũ': 'U', 'Ư': 'U', 'Ừ': 'U', 'Ứ': 'U', 'Ự': 'U', 'Ử': 'U', 'Ữ': 'U', 'Ỳ': 'Y', 'Ý': 'Y', 'Ỵ': 'Y', 'Ỷ': 'Y', 'Ỹ': 'Y', 'Đ': 'D' }; return str.replace(/[^\u0000-\u007E]/g, (char) => map[char] || char); } function generateSearchVariations(searchTerm) { const variations = new Set(); // Original term variations.add(searchTerm); // Case variations variations.add(searchTerm.toLowerCase()); variations.add(searchTerm.toUpperCase()); variations.add(searchTerm.charAt(0).toUpperCase() + searchTerm.slice(1).toLowerCase()); // Without diacritics const withoutDiacritics = removeVietnameseDiacritics(searchTerm); variations.add(withoutDiacritics); variations.add(withoutDiacritics.toLowerCase()); variations.add(withoutDiacritics.toUpperCase()); // Split terms for partial matching const words = searchTerm.split(/[\s\-_]+/).filter(w => w.length > 1); words.forEach(word => { variations.add(word); variations.add(removeVietnameseDiacritics(word)); }); return Array.from(variations); } async function searchFiles(args) { try { log(`Searching files with args: ${JSON.stringify(args)}`, 'debug'); // Check if the query looks like a file ID (length ~44 chars, alphanumeric with hyphens/underscores) const fileIdPattern = /^[a-zA-Z0-9_-]{25,}$/; if (fileIdPattern.test(args.query.trim())) { log(`Query "${args.query}" looks like a file ID, attempting direct access`, 'info'); try { const fileResponse = await drive.files.get({ fileId: args.query.trim(), fields: 'id,name,mimeType,modifiedTime,size,webViewLink,parents,description,owners,permissions', supportsAllDrives: true, }); if (fileResponse.data) { log(`Successfully found file by ID: ${fileResponse.data.name}`, 'info'); return { files: [fileResponse.data], nextPageToken: null, totalResults: 1, query: args.query, cached: false, foundByFileId: true }; } } catch (fileIdError) { log(`File ID access failed, falling back to search: ${fileIdError}`, 'debug'); } } // Create cache key const cacheKey = `search:${JSON.stringify(args)}`; // Check cache first for non-real-time queries const cachedResult = getCachedData(cacheKey); if (cachedResult && !args.query.includes('modifiedTime')) { log('Returning cached search result', 'debug'); return cachedResult; } // Generate search variations for Vietnamese text const searchVariations = generateSearchVariations(args.query); log(`Generated ${searchVariations.length} search variations: ${searchVariations.join(', ')}`, 'debug'); let allFoundFiles = new Map(); // Use Map to avoid duplicates by ID // Try multiple search approaches const searchApproaches = [ // Exact matches with variations ...searchVariations.map(term => `name contains "${term}"`), // Partial matches ...searchVariations.map(term => `fullText contains "${term}"`), // Without case sensitivity (Google Drive handles this automatically) ...searchVariations.slice(0, 3).map(term => `name = "${term}"`) ]; // Add file type and trash filters to all approaches const baseFilters = []; if (!args.includeTrashed) { baseFilters.push('trashed = false'); } if (args.fileType) { baseFilters.push(`mimeType='${args.fileType}'`); } const searchOperation = async () => { // Try each search approach for (let i = 0; i < Math.min(searchApproaches.length, 5); i++) { // Limit to prevent too many API calls const searchQuery = searchApproaches[i]; const fullQuery = baseFilters.length > 0 ? `${searchQuery} and ${baseFilters.join(' and ')}` : searchQuery; log(`Trying search query ${i + 1}: ${fullQuery}`, 'debug'); try { const response = await drive.files.list({ q: fullQuery, pageSize: args.maxResults || 20, fields: 'files(id,name,mimeType,modifiedTime,size,webViewLink,parents,description,owners,permissions),nextPageToken', orderBy: args.orderBy || 'modifiedTime desc', includeItemsFromAllDrives: true, supportsAllDrives: true, }); if (response.data?.files && Array.isArray(response.data.files)) { response.data.files.forEach((file) => { if (!allFoundFiles.has(file.id)) { allFoundFiles.set(file.id, file); } }); log(`Search query ${i + 1} found ${response.data.files.length} files`, 'debug'); // If we found files and they seem relevant, we can break early if (response.data.files.length > 0) { const relevantFiles = response.data.files.filter((file) => searchVariations.some(term => file.name.toLowerCase().includes(term.toLowerCase()))); if (relevantFiles.length > 0) { log(`Found ${relevantFiles.length} highly relevant files, stopping search`, 'info'); break; } } } } catch (searchError) { log(`Search query ${i + 1} failed: ${searchError}`, 'debug'); // Continue with next approach } } const files = Array.from(allFoundFiles.values()); log(`Total unique files found across all searches: ${files.length}`, 'info'); const result = { files: files, nextPageToken: null, // We're combining results, so no pagination token totalResults: files.length, query: args.query, searchVariations: searchVariations, cached: false }; // Cache the result if successful if (files.length >= 0) { setCachedData(cacheKey, result, 60000); // 1 minute cache for searches } return result; }; return await retryWithBackoff(searchOperation, 3, 1000); } catch (error) { log(`Failed to search files: ${error}`, 'error'); throw new Error(`Failed to search files: ${error}`); } } async function getFile(args) { try { let fields = 'id,name,mimeType,modifiedTime,size,webViewLink,parents,description,owners,createdTime,lastModifyingUser'; if (args.includePermissions) { fields += ',permissions'; } const response = await drive.files.get({ fileId: args.fileId, fields, supportsAllDrives: true, }); let content = null; if (args.includeContent && response.data.mimeType?.includes('text')) { try { const contentResponse = await drive.files.get({ fileId: args.fileId, alt: 'media', supportsAllDrives: true, }); content = contentResponse.data; } catch (contentError) { console.error('Failed to get file content:', contentError); // eslint-disable-line no-undef } } return { file: response.data, content, }; } catch (error) { throw new Error(`Failed to get file: ${error}`); } } async function listFiles(args) { try { log(`Listing files with args: ${JSON.stringify(args)}`, 'debug'); const response = await drive.files.list({ pageSize: args.pageSize, pageToken: args.pageToken, orderBy: args.orderBy || 'modifiedTime desc', q: args.q, fields: 'files(id,name,mimeType,modifiedTime,size,webViewLink,parents,description,owners),nextPageToken', driveId: args.driveId, includeItemsFromAllDrives: args.includeItemsFromAllDrives, supportsAllDrives: true, }); log(`Google Drive API response: ${JSON.stringify(response.data)}`, 'debug'); if (!response.data) { throw new Error('Google Drive API returned no data'); } // Handle different response structures const files = Array.isArray(response.data.files) ? response.data.files : []; log(`Listed ${files.length} files`, 'info'); return { files: files, nextPageToken: response.data.nextPageToken || null, totalResults: files.length, }; } catch (error) { log(`Failed to list files: ${error}`, 'error'); throw new Error(`Failed to list files: ${error}`); } } async function getFileContent(args) { try { let content; if (args.mimeType) { // Export file to specific format const response = await drive.files.export({ fileId: args.fileId, mimeType: args.mimeType, }); content = response.data; } else { // Get raw content const response = await drive.files.get({ fileId: args.fileId, alt: 'media', supportsAllDrives: true, }); content = response.data; } return { content, mimeType: args.mimeType || 'raw', encoding: args.encoding || 'utf-8', }; } catch (error) { throw new Error(`Failed to get file content: ${error}`); } } async function createFile(args) { try { const fileMetadata = { name: args.name, description: args.description, parents: args.parentId ? [args.parentId] : undefined, }; const media = { mimeType: args.mimeType, body: Buffer.from(args.content, 'utf-8'), // eslint-disable-line no-undef }; const response = await drive.files.create({ requestBody: fileMetadata, media, fields: 'id,name,mimeType,webViewLink,size', supportsAllDrives: true, }); return { file: response.data, message: 'File created successfully', }; } catch (error) { throw new Error(`Failed to create file: ${error}`); } } async function updateFile(args) { try { const fileMetadata = {}; if (args.name) fileMetadata.name = args.name; if (args.description) fileMetadata.description = args.description; let media; if (args.content) { media = { body: Buffer.from(args.content, 'utf-8'), // eslint-disable-line no-undef }; } const response = await drive.files.update({ fileId: args.fileId, requestBody: fileMetadata, media, fields: 'id,name,mimeType,webViewLink,size,modifiedTime', supportsAllDrives: true, }); return { file: response.data, message: 'File updated successfully', }; } catch (error) { throw new Error(`Failed to update file: ${error}`); } } async function deleteFile(args) { try { if (args.permanent) { await drive.files.delete({ fileId: args.fileId, supportsAllDrives: true, }); } else { await drive.files.update({ fileId: args.fileId, requestBody: { trashed: true }, supportsAllDrives: true, }); } return { message: args.permanent ? 'File permanently deleted' : 'File moved to trash', fileId: args.fileId, }; } catch (error) { throw new Error(`Failed to delete file: ${error}`); } } async function copyFile(args) { try { const copyMetadata = {}; if (args.name) copyMetadata.name = args.name; if (args.parentId) copyMetadata.parents = [args.parentId]; const response = await drive.files.copy({ fileId: args.fileId, requestBody: copyMetadata, fields: 'id,name,mimeType,webViewLink,size', supportsAllDrives: true, }); return { file: response.data, message: 'File copied successfully', }; } catch (error) { throw new Error(`Failed to copy file: ${error}`); } } async function moveFile(args) { try { // First, get current parents const currentFile = await drive.files.get({ fileId: args.fileId, fields: 'parents', supportsAllDrives: true, }); const currentParents = currentFile.data.parents || []; let newParents = [...currentParents]; // Add new parent if (!newParents.includes(args.parentId)) { newParents.push(args.parentId); } // Remove from current parent if requested if (args.removeFromParents && currentParents.length > 0) { newParents = newParents.filter(id => id !== 'root'); } const response = await drive.files.update({ fileId: args.fileId, requestBody: { parents: newParents, }, fields: 'id,name,parents,webViewLink', supportsAllDrives: true, }); return { file: response.data, message: 'File moved successfully', }; } catch (error) { throw new Error(`Failed to move file: ${error}`); } } async function createFolder(args) { try { const folderMetadata = { name: args.name, description: args.description, mimeType: 'application/vnd.google-apps.folder', parents: args.parentId ? [args.parentId] : undefined, }; const response = await drive.files.create({ requestBody: folderMetadata, fields: 'id,name,mimeType,webViewLink', supportsAllDrives: true, }); return { folder: response.data, message: 'Folder created successfully', }; } catch (error) { throw new Error(`Failed to create folder: ${error}`); } } async function getFilePermissions(args) { try { const response = await drive.permissions.list({ fileId: args.fileId, fields: 'permissions(id,emailAddress,role,displayName,type,deleted)', supportsAllDrives: true, }); return { permissions: response.data.permissions || [], totalResults: response.data.permissions?.length || 0, }; } catch (error) { throw new Error(`Failed to get file permissions: ${error}`); } } async function shareFile(args) { try { const permission = { type: 'user', role: args.role, emailAddress: args.email, }; const response = await drive.permissions.create({ fileId: args.fileId, requestBody: permission, emailMessage: args.message, fields: 'id,emailAddress,role', supportsAllDrives: true, }); return { permission: response.data, message: 'File shared successfully', }; } catch (error) { throw new Error(`Failed to share file: ${error}`); } } async function getDriveInfo(args) { try { const driveId = args.driveId || 'root'; const response = await drive.drives.get({ driveId, fields: 'id,name,capabilities,restrictions,createdTime', }); return { drive: response.data, }; } catch (error) { throw new Error(`Failed to get drive info: ${error}`); } } async function listSharedDrives(args) { try { const response = await drive.drives.list({ pageSize: args.pageSize, pageToken: args.pageToken, fields: 'drives(id,name,capabilities,restrictions,createdTime),nextPageToken', }); return { drives: response.data.drives || [], nextPageToken: response.data.nextPageToken, totalResults: response.data.drives?.length || 0, }; } catch (error) { throw new Error(`Failed to list shared drives: ${error}`); } } async function getFileRevisions(args) { try { const response = await drive.revisions.list({ fileId: args.fileId, fields: 'revisions(id,mimeType,modifiedTime,size,originalFilename,keepForever,published,publishedOutsideDomain)', pageSize: args.maxResults, }); return { revisions: response.data.revisions || [], totalResults: response.data.revisions?.length || 0, }; } catch (error) { throw new Error(`Failed to get file revisions: ${error}`); } } // MCP Server setup with enhanced error handling const server = new Server({ name: 'mcp-google-drive-server', version: '1.4.1', capabilities: { tools: {}, }, }); server.setRequestHandler(ListToolsRequestSchema, async () => { log('ListTools request received', 'debug'); return { tools }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // Check if authentication is ready if (!isInitialized) { log('Authentication not ready, waiting...', 'debug'); // Wait for authentication to complete let waitCount = 0; while (!isInitialized && waitCount < 30) { await new Promise(resolve => setTimeout(resolve, 100)); waitCount++; } if (!isInitialized) { throw new Error('Authentication not ready after 3 seconds'); } } log(`Tool call: ${name}`, 'debug'); try { let result; switch (name) { case 'search_files': result = await searchFiles(SearchFilesSchema.parse(args)); break; case 'get_file': result = await getFile(GetFileSchema.parse(args)); break; case 'list_files': result = await listFiles(ListFilesSchema.parse(args)); break; case 'get_file_content': result = await getFileContent(GetFileContentSchema.parse(args)); break; case 'create_file': result = await createFile(CreateFileSchema.parse(args)); break; case 'update_file': result = await updateFile(UpdateFileSchema.parse(args)); break; case 'delete_file': result = await deleteFile(DeleteFileSchema.parse(args)); break; case 'copy_file': result = await copyFile(CopyFileSchema.parse(args)); break; case 'move_file': result = await moveFile(MoveFileSchema.parse(args)); break; case 'create_folder': result = await createFolder(CreateFolderSchema.parse(args)); break; case 'get_file_permissions': result = await getFilePermissions(GetFilePermissionsSchema.parse(args)); break; case 'share_file': result = await shareFile(ShareFileSchema.parse(args)); break; case 'get_drive_info': result = await getDriveInfo(GetDriveInfoSchema.parse(args)); break; case 'list_shared_drives': result = await listSharedDrives(ListSharedDrivesSchema.parse(args)); break; case 'get_file_revisions': result = await getFileRevisions(GetFileRevisionsSchema.parse(args)); break; default: throw new Error(`Unknown tool: ${name}`); } log(`Tool ${name} executed successfully`, 'debug'); return result; } catch (error) { log(`Tool ${name} failed: ${error}`, 'error'); throw new Error(`Tool execution failed: ${error}`); } }); // Start server with error handling try { const transport = new StdioServerTransport(); await server.connect(transport); log('MCP Google Drive server started successfully', 'info'); log('Server ready to handle requests', 'info'); } catch (error) { log(`Failed to start MCP server: ${error}`, 'error'); process.exit(1); } //# sourceMappingURL=index.js.map