import type { FastMCP } from 'fastmcp'; import { UserError } from 'fastmcp'; import { z } from 'zod'; import { getDriveClient } from '../../clients.js'; import { escapeDriveQuery } from '../../driveQueryUtils.js'; /** * Convenience shortcuts for common MIME types. * Users can also pass any full MIME type string directly. */ const MIME_TYPE_SHORTCUTS: Record = { document: 'application/vnd.google-apps.document', spreadsheet: 'application/vnd.google-apps.spreadsheet', presentation: 'application/vnd.google-apps.presentation', folder: 'application/vnd.google-apps.folder', form: 'application/vnd.google-apps.form', pdf: 'application/pdf', zip: 'application/zip', }; export function register(server: FastMCP) { server.addTool({ name: 'listDriveFiles', description: 'Lists files across Google Drive with optional filtering by type, folder, and ownership. ' + 'Unlike listDocuments (which only returns Google Docs), this tool works with all file types ' + '(Sheets, PDFs, images, folders, etc.) and supports sort direction and size-based ordering. ' + 'Use mimeType shortcuts: "document", "spreadsheet", "presentation", "folder", "form", "pdf", "zip" ' + 'or pass any full MIME type string.', parameters: z.strictObject({ maxResults: z .number() .int() .min(1) .max(100) .optional() .default(20) .describe('Maximum number of files to return (1-100).'), mimeType: z .string() .optional() .describe( 'Filter by file type. Shortcuts: "document", "spreadsheet", "presentation", ' + '"folder", "form", "pdf", "zip". Or pass a full MIME type (e.g. "image/png").' ), folderId: z .string() .optional() .describe( 'Only return files directly inside this folder. Use "root" for the top-level Drive. ' + 'Omit to search across all folders.' ), orderBy: z .enum(['name', 'modifiedTime', 'createdTime', 'quotaBytesUsed']) .optional() .default('modifiedTime') .describe( 'Field to sort results by. "quotaBytesUsed" sorts by file size (note: Google-native files report 0).' ), sortDirection: z .enum(['asc', 'desc']) .optional() .default('desc') .describe( 'Sort direction: "asc" for oldest/smallest first, "desc" for newest/largest first.' ), ownedByMe: z .boolean() .optional() .describe('If true, only return files owned by the authenticated user.'), sharedWithMe: z .boolean() .optional() .describe( 'If true, only return files shared with the authenticated user (excludes files they own). ' + 'Cannot be combined with ownedByMe.' ), modifiedAfter: z .string() .optional() .describe( 'Only return files modified after this date (ISO 8601 format, e.g. "2024-01-01").' ), }), execute: async (args, { log }) => { if (args.ownedByMe && args.sharedWithMe) { throw new UserError('ownedByMe and sharedWithMe cannot both be true.'); } const drive = await getDriveClient(); log.info( `Listing Drive files. mimeType=${args.mimeType || 'any'}, folder=${args.folderId || 'all'}, ` + `orderBy=${args.orderBy} ${args.sortDirection}, ownedByMe=${args.ownedByMe}, sharedWithMe=${args.sharedWithMe}` ); try { const conditions: string[] = ['trashed=false']; // Resolve MIME type shortcut or use value as-is if (args.mimeType) { const resolved = MIME_TYPE_SHORTCUTS[args.mimeType] ?? args.mimeType; conditions.push(`mimeType='${escapeDriveQuery(resolved)}'`); } // Scope to a specific folder if (args.folderId) { conditions.push(`'${escapeDriveQuery(args.folderId)}' in parents`); } // Ownership filter if (args.ownedByMe) { conditions.push(`'me' in owners`); } else if (args.sharedWithMe) { conditions.push(`sharedWithMe=true`); } // Date filter if (args.modifiedAfter) { const cutoff = new Date(args.modifiedAfter).toISOString(); conditions.push(`modifiedTime > '${escapeDriveQuery(cutoff)}'`); } const queryString = conditions.join(' and '); const orderByParam = args.sortDirection === 'desc' ? `${args.orderBy} desc` : args.orderBy; const response = await drive.files.list({ q: queryString, pageSize: args.maxResults, orderBy: orderByParam, fields: 'files(id,name,mimeType,size,modifiedTime,createdTime,webViewLink,owners(displayName,emailAddress))', supportsAllDrives: true, includeItemsFromAllDrives: true, }); const files = (response.data.files || []).map((file) => ({ id: file.id, name: file.name, mimeType: file.mimeType, size: file.size != null ? Number(file.size) : null, modifiedTime: file.modifiedTime, createdTime: file.createdTime, owner: file.owners?.[0]?.displayName || null, url: file.webViewLink, })); return JSON.stringify({ files, total: files.length }, null, 2); } catch (error: any) { log.error(`Error listing Drive files: ${error.message || error}`); if (error.code === 403) throw new UserError( 'Permission denied. Make sure you have granted Google Drive access to the application.' ); throw new UserError(`Failed to list files: ${error.message || 'Unknown error'}`); } }, }); }