File size: 5,827 Bytes
7dc28be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
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'}`);
      }
    },
  });
}