Spaces:
Sleeping
Sleeping
| 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<string, string> = { | |
| 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'}`); | |
| } | |
| }, | |
| }); | |
| } | |