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: 'searchDriveFiles', description: 'Searches across all file types in Google Drive by name or content. ' + 'Unlike searchDocuments (which only searches Google Docs), this tool finds Sheets, PDFs, ' + 'presentations, folders, and any other Drive file. Supports filtering by MIME type, ' + 'scoping to a specific folder subtree, controllable sort order, and pagination via pageToken.', parameters: z.strictObject({ query: z.string().min(1).describe('Search term to find in file names or content.'), searchIn: z .enum(['name', 'content', 'both']) .optional() .default('both') .describe( 'Where to search: "name" matches file titles only, "content" searches inside files, ' + '"both" searches names and content (default).' ), mimeType: z .string() .optional() .describe( 'Restrict search to a specific file type. ' + 'Shortcuts: "document", "spreadsheet", "presentation", "folder", "form", "pdf", "zip". ' + 'Or pass a full MIME type string.' ), folderId: z .string() .optional() .describe( 'Restrict search to files inside this folder (and its subfolders). ' + 'Use "root" for the top-level Drive. Omit to search all of Drive.' ), orderBy: z .enum(['name', 'modifiedTime', 'createdTime']) .optional() .default('modifiedTime') .describe('Field to sort results by.'), sortDirection: z .enum(['asc', 'desc']) .optional() .default('desc') .describe('Sort direction: "asc" for oldest first, "desc" for newest first (default).'), maxResults: z .number() .int() .min(1) .max(100) .optional() .default(10) .describe('Maximum number of results to return per page (1-100).'), modifiedAfter: z .string() .optional() .describe( 'Only return files modified after this date (ISO 8601 format, e.g. "2024-01-01").' ), pageToken: z .string() .optional() .describe( 'Pagination token from a previous searchDriveFiles response. ' + 'Pass this to retrieve the next page of results.' ), }), execute: async (args, { log }) => { const drive = await getDriveClient(); log.info( `Searching Drive files for: "${args.query}" in ${args.searchIn}, ` + `mimeType=${args.mimeType || 'any'}, folder=${args.folderId || 'all'}, ` + `orderBy=${args.orderBy} ${args.sortDirection}` ); try { const conditions: string[] = ['trashed=false']; // Search term if (args.searchIn === 'name') { conditions.push(`name contains '${escapeDriveQuery(args.query)}'`); } else if (args.searchIn === 'content') { conditions.push(`fullText contains '${escapeDriveQuery(args.query)}'`); } else { conditions.push( `(name contains '${escapeDriveQuery(args.query)}' or fullText contains '${escapeDriveQuery(args.query)}')` ); } // 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 (searches within the folder subtree via fullText, // but Drive API does not support recursive parent filtering natively — using // ancestor query instead which covers all descendants) if (args.folderId) { conditions.push(`'${escapeDriveQuery(args.folderId)}' in ancestors`); } // 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, pageToken: args.pageToken, fields: 'nextPageToken,files(id,name,mimeType,size,modifiedTime,createdTime,webViewLink,owners(displayName,emailAddress),parents)', 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, })); const result: Record = { files, total: files.length }; if (response.data.nextPageToken) { result.nextPageToken = response.data.nextPageToken; result.hasMore = true; } else { result.hasMore = false; } return JSON.stringify(result, null, 2); } catch (error: any) { log.error(`Error searching 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 search files: ${error.message || 'Unknown error'}`); } }, }); }