import { Readable } from 'stream';
import { getGoogleAPIs } from '../auth/google-auth.js';
import {
getLogger,
validateInput,
DriveSearchSchema,
DriveCreateFolderSchema,
DriveSetPermissionSchema,
DriveCopyFileSchema,
DriveUploadFileSchema,
DriveDownloadFileSchema,
DriveDeleteFileSchema,
DriveMoveFileSchema,
DriveGetFileSchema,
isOperationAllowed,
isFolderAllowed,
OperationNotAllowedError,
FolderNotAllowedError,
GoogleAPIError,
withErrorHandling,
type DriveSearch,
type DriveCreateFolder,
type DriveSetPermission,
type DriveCopyFile,
type DriveUploadFile,
type DriveDownloadFile,
type DriveDeleteFile,
type DriveMoveFile,
type DriveGetFile,
} from '@company-mcp/core';
const logger = getLogger();
// Types
export interface DriveFile {
id: string;
name: string;
mimeType: string;
modifiedTime: string;
parents?: string[];
}
export interface DriveSearchResult {
files: DriveFile[];
nextPageToken?: string;
}
export interface DriveFolder {
id: string;
name: string;
}
// Helper to check folder allowlist
async function checkFolderAllowlist(
folderId: string | undefined,
operation: string
): Promise<void> {
if (!folderId) return;
if (!isFolderAllowed(folderId)) {
logger.audit(operation, 'blocked', {
args: { folderId },
result: 'failure',
error: 'Folder not in allowlist',
});
throw new FolderNotAllowedError(folderId);
}
}
// Search files
export async function driveSearchFiles(
input: unknown
): Promise<DriveSearchResult> {
return withErrorHandling('drive_search_files', async () => {
const validation = validateInput(DriveSearchSchema, input);
if (!validation.success) {
throw new GoogleAPIError(validation.errors.join(', '), 400);
}
const params = validation.data as DriveSearch;
const startTime = Date.now();
const { drive } = getGoogleAPIs();
const response = await drive.files.list({
q: params.q,
pageSize: params.pageSize,
pageToken: params.pageToken,
fields: 'nextPageToken, files(id, name, mimeType, modifiedTime, parents)',
});
const files: DriveFile[] = (response.data.files || []).map((file) => ({
id: file.id!,
name: file.name!,
mimeType: file.mimeType!,
modifiedTime: file.modifiedTime!,
parents: file.parents || undefined,
}));
logger.audit('drive_search_files', 'search', {
args: { q: params.q, pageSize: params.pageSize },
result: 'success',
duration_ms: Date.now() - startTime,
});
return {
files,
nextPageToken: response.data.nextPageToken || undefined,
};
});
}
// Create folder
export async function driveCreateFolder(
input: unknown
): Promise<DriveFolder> {
return withErrorHandling('drive_create_folder', async () => {
// Check if write is allowed
if (!isOperationAllowed('drive_write')) {
throw new OperationNotAllowedError('drive_write');
}
const validation = validateInput(DriveCreateFolderSchema, input);
if (!validation.success) {
throw new GoogleAPIError(validation.errors.join(', '), 400);
}
const params = validation.data as DriveCreateFolder;
const startTime = Date.now();
// Check folder allowlist
await checkFolderAllowlist(params.parentFolderId, 'drive_create_folder');
const { drive } = getGoogleAPIs();
const requestBody: { name: string; mimeType: string; parents?: string[] } = {
name: params.name,
mimeType: 'application/vnd.google-apps.folder',
};
if (params.parentFolderId) {
requestBody.parents = [params.parentFolderId];
}
const response = await drive.files.create({
requestBody,
fields: 'id, name',
});
logger.audit('drive_create_folder', 'create', {
args: { name: params.name, parentFolderId: params.parentFolderId },
result: 'success',
duration_ms: Date.now() - startTime,
});
return {
id: response.data.id!,
name: response.data.name!,
};
});
}
// Set permission
export async function driveSetPermission(
input: unknown
): Promise<{ ok: true }> {
return withErrorHandling('drive_set_permission', async () => {
// Check if write is allowed
if (!isOperationAllowed('drive_write')) {
throw new OperationNotAllowedError('drive_write');
}
const validation = validateInput(DriveSetPermissionSchema, input);
if (!validation.success) {
throw new GoogleAPIError(validation.errors.join(', '), 400);
}
const params = validation.data as DriveSetPermission;
const startTime = Date.now();
const { drive } = getGoogleAPIs();
// Get file info to check parent folder
const fileInfo = await drive.files.get({
fileId: params.fileId,
fields: 'parents',
});
// Check if parent folder is in allowlist
const parentId = fileInfo.data.parents?.[0];
await checkFolderAllowlist(parentId, 'drive_set_permission');
await drive.permissions.create({
fileId: params.fileId,
requestBody: {
type: 'user',
role: params.role,
emailAddress: params.email,
},
});
logger.audit('drive_set_permission', 'create', {
args: {
fileId: params.fileId,
email: params.email,
role: params.role,
},
result: 'success',
duration_ms: Date.now() - startTime,
});
return { ok: true };
});
}
// Copy file
export async function driveCopyFile(
input: unknown
): Promise<{ id: string; name: string }> {
return withErrorHandling('drive_copy_file', async () => {
// Check if write is allowed
if (!isOperationAllowed('drive_write')) {
throw new OperationNotAllowedError('drive_write');
}
const validation = validateInput(DriveCopyFileSchema, input);
if (!validation.success) {
throw new GoogleAPIError(validation.errors.join(', '), 400);
}
const params = validation.data as DriveCopyFile;
const startTime = Date.now();
// Check folder allowlist for destination
await checkFolderAllowlist(params.parentFolderId, 'drive_copy_file');
const { drive } = getGoogleAPIs();
const requestBody: { name?: string; parents?: string[] } = {};
if (params.newName) {
requestBody.name = params.newName;
}
if (params.parentFolderId) {
requestBody.parents = [params.parentFolderId];
}
const response = await drive.files.copy({
fileId: params.fileId,
requestBody,
fields: 'id, name',
});
logger.audit('drive_copy_file', 'copy', {
args: {
fileId: params.fileId,
newName: params.newName,
parentFolderId: params.parentFolderId,
},
result: 'success',
duration_ms: Date.now() - startTime,
});
return {
id: response.data.id!,
name: response.data.name!,
};
});
}
// Upload file
export async function driveUploadFile(
input: unknown
): Promise<{ id: string; name: string; mimeType: string }> {
return withErrorHandling('drive_upload_file', async () => {
// Check if write is allowed
if (!isOperationAllowed('drive_write')) {
throw new OperationNotAllowedError('drive_write');
}
const validation = validateInput(DriveUploadFileSchema, input);
if (!validation.success) {
throw new GoogleAPIError(validation.errors.join(', '), 400);
}
const params = validation.data as DriveUploadFile;
const startTime = Date.now();
// Check folder allowlist for parent folder
await checkFolderAllowlist(params.parentFolderId, 'drive_upload_file');
const { drive } = getGoogleAPIs();
// Decode base64 content
const buffer = Buffer.from(params.content, 'base64');
const requestBody: { name: string; mimeType: string; parents?: string[] } = {
name: params.name,
mimeType: params.mimeType,
};
if (params.parentFolderId) {
requestBody.parents = [params.parentFolderId];
}
const response = await drive.files.create({
requestBody,
media: {
mimeType: params.mimeType,
body: Readable.from(buffer),
},
fields: 'id, name, mimeType',
});
logger.audit('drive_upload_file', 'create', {
args: {
name: params.name,
mimeType: params.mimeType,
parentFolderId: params.parentFolderId,
contentSize: buffer.length,
},
result: 'success',
duration_ms: Date.now() - startTime,
});
return {
id: response.data.id!,
name: response.data.name!,
mimeType: response.data.mimeType!,
};
});
}
// Download file
export async function driveDownloadFile(
input: unknown
): Promise<{ content: string; mimeType: string; name: string }> {
return withErrorHandling('drive_download_file', async () => {
const validation = validateInput(DriveDownloadFileSchema, input);
if (!validation.success) {
throw new GoogleAPIError(validation.errors.join(', '), 400);
}
const params = validation.data as DriveDownloadFile;
const startTime = Date.now();
const { drive } = getGoogleAPIs();
// Get file metadata first
const fileInfo = await drive.files.get({
fileId: params.fileId,
fields: 'id, name, mimeType, parents',
});
// Check if parent folder is in allowlist
const parentId = fileInfo.data.parents?.[0];
await checkFolderAllowlist(parentId, 'drive_download_file');
// Download file content
const response = await drive.files.get(
{
fileId: params.fileId,
alt: 'media',
},
{ responseType: 'arraybuffer' }
);
const content = Buffer.from(response.data as ArrayBuffer).toString('base64');
logger.audit('drive_download_file', 'download', {
args: { fileId: params.fileId },
result: 'success',
duration_ms: Date.now() - startTime,
});
return {
content,
mimeType: fileInfo.data.mimeType!,
name: fileInfo.data.name!,
};
});
}
// Delete file
export async function driveDeleteFile(
input: unknown
): Promise<{ ok: true }> {
return withErrorHandling('drive_delete_file', async () => {
// Check if delete is allowed
if (!isOperationAllowed('drive_delete')) {
throw new OperationNotAllowedError('drive_delete');
}
const validation = validateInput(DriveDeleteFileSchema, input);
if (!validation.success) {
throw new GoogleAPIError(validation.errors.join(', '), 400);
}
const params = validation.data as DriveDeleteFile;
const startTime = Date.now();
const { drive } = getGoogleAPIs();
// Get file info to check parent folder
const fileInfo = await drive.files.get({
fileId: params.fileId,
fields: 'parents',
});
// Check if parent folder is in allowlist
const parentId = fileInfo.data.parents?.[0];
await checkFolderAllowlist(parentId, 'drive_delete_file');
if (params.permanent) {
// Permanently delete the file
await drive.files.delete({
fileId: params.fileId,
});
} else {
// Move to trash
await drive.files.update({
fileId: params.fileId,
requestBody: {
trashed: true,
},
});
}
logger.audit('drive_delete_file', params.permanent ? 'delete' : 'trash', {
args: { fileId: params.fileId, permanent: params.permanent },
result: 'success',
duration_ms: Date.now() - startTime,
});
return { ok: true };
});
}
// Move file
export async function driveMoveFile(
input: unknown
): Promise<{ id: string; name: string; parents: string[] }> {
return withErrorHandling('drive_move_file', async () => {
// Check if write is allowed
if (!isOperationAllowed('drive_write')) {
throw new OperationNotAllowedError('drive_write');
}
const validation = validateInput(DriveMoveFileSchema, input);
if (!validation.success) {
throw new GoogleAPIError(validation.errors.join(', '), 400);
}
const params = validation.data as DriveMoveFile;
const startTime = Date.now();
// Check destination folder allowlist
await checkFolderAllowlist(params.newParentFolderId, 'drive_move_file');
const { drive } = getGoogleAPIs();
// Get current parents
const fileInfo = await drive.files.get({
fileId: params.fileId,
fields: 'parents',
});
const currentParents = fileInfo.data.parents || [];
// Check current parent folder allowlist
const currentParentId = currentParents[0];
await checkFolderAllowlist(currentParentId, 'drive_move_file');
const updateParams: {
fileId: string;
addParents: string;
removeParents?: string;
fields: string;
} = {
fileId: params.fileId,
addParents: params.newParentFolderId,
fields: 'id, name, parents',
};
if (params.removeFromCurrentParent && currentParents.length > 0) {
updateParams.removeParents = currentParents.join(',');
}
const response = await drive.files.update(updateParams);
logger.audit('drive_move_file', 'move', {
args: {
fileId: params.fileId,
newParentFolderId: params.newParentFolderId,
removeFromCurrentParent: params.removeFromCurrentParent,
},
result: 'success',
duration_ms: Date.now() - startTime,
});
return {
id: response.data.id!,
name: response.data.name!,
parents: response.data.parents || [],
};
});
}
// Get file metadata
export async function driveGetFile(
input: unknown
): Promise<DriveFile> {
return withErrorHandling('drive_get_file', async () => {
const validation = validateInput(DriveGetFileSchema, input);
if (!validation.success) {
throw new GoogleAPIError(validation.errors.join(', '), 400);
}
const params = validation.data as DriveGetFile;
const startTime = Date.now();
const { drive } = getGoogleAPIs();
const response = await drive.files.get({
fileId: params.fileId,
fields: 'id, name, mimeType, modifiedTime, parents',
});
// Check if parent folder is in allowlist
const parentId = response.data.parents?.[0];
await checkFolderAllowlist(parentId, 'drive_get_file');
logger.audit('drive_get_file', 'get', {
args: { fileId: params.fileId },
result: 'success',
duration_ms: Date.now() - startTime,
});
return {
id: response.data.id!,
name: response.data.name!,
mimeType: response.data.mimeType!,
modifiedTime: response.data.modifiedTime!,
parents: response.data.parents || undefined,
};
});
}
// Tool definitions for MCP
export const driveTools = [
{
name: 'drive_search_files',
description:
'Search files in Google Drive using Drive query syntax. Returns file metadata.',
inputSchema: {
type: 'object',
properties: {
q: {
type: 'string',
description:
'Drive search query (e.g., "name contains \'report\' and mimeType=\'application/pdf\'")',
},
pageSize: {
type: 'number',
description: 'Maximum number of results (1-1000, default: 10)',
default: 10,
},
pageToken: {
type: 'string',
description: 'Page token for pagination',
},
},
required: ['q'],
},
},
{
name: 'drive_create_folder',
description:
'Create a new folder in Google Drive. Requires DRIVE_WRITE_ENABLED=true.',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Name of the folder to create',
},
parentFolderId: {
type: 'string',
description: 'ID of the parent folder (optional, defaults to root)',
},
},
required: ['name'],
},
},
{
name: 'drive_set_permission',
description:
'Share a file or folder with another user. Requires DRIVE_WRITE_ENABLED=true.',
inputSchema: {
type: 'object',
properties: {
fileId: {
type: 'string',
description: 'ID of the file or folder to share',
},
email: {
type: 'string',
description: 'Email address of the user to share with',
},
role: {
type: 'string',
enum: ['reader', 'writer'],
description: 'Permission role (reader or writer)',
},
},
required: ['fileId', 'email', 'role'],
},
},
{
name: 'drive_copy_file',
description:
'Copy a file in Google Drive. Requires DRIVE_WRITE_ENABLED=true.',
inputSchema: {
type: 'object',
properties: {
fileId: {
type: 'string',
description: 'ID of the file to copy',
},
newName: {
type: 'string',
description: 'Name for the copy (optional)',
},
parentFolderId: {
type: 'string',
description: 'ID of the destination folder (optional)',
},
},
required: ['fileId'],
},
},
{
name: 'drive_upload_file',
description:
'Upload a file to Google Drive with base64 encoded content. Requires DRIVE_WRITE_ENABLED=true.',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Name of the file to create',
},
content: {
type: 'string',
description: 'Base64 encoded file content',
},
mimeType: {
type: 'string',
description: 'MIME type of the file (e.g., "application/pdf", "image/png")',
},
parentFolderId: {
type: 'string',
description: 'ID of the parent folder (optional, defaults to root)',
},
},
required: ['name', 'content', 'mimeType'],
},
},
{
name: 'drive_download_file',
description:
'Download a file from Google Drive. Returns base64 encoded content.',
inputSchema: {
type: 'object',
properties: {
fileId: {
type: 'string',
description: 'ID of the file to download',
},
},
required: ['fileId'],
},
},
{
name: 'drive_delete_file',
description:
'Delete or trash a file in Google Drive. Requires DRIVE_DELETE_ENABLED=true.',
inputSchema: {
type: 'object',
properties: {
fileId: {
type: 'string',
description: 'ID of the file to delete',
},
permanent: {
type: 'boolean',
description: 'If true, permanently delete; if false (default), move to trash',
default: false,
},
},
required: ['fileId'],
},
},
{
name: 'drive_move_file',
description:
'Move a file to another folder in Google Drive. Requires DRIVE_WRITE_ENABLED=true.',
inputSchema: {
type: 'object',
properties: {
fileId: {
type: 'string',
description: 'ID of the file to move',
},
newParentFolderId: {
type: 'string',
description: 'ID of the destination folder',
},
removeFromCurrentParent: {
type: 'boolean',
description: 'If true (default), remove from current parent; if false, add to new parent without removing',
default: true,
},
},
required: ['fileId', 'newParentFolderId'],
},
},
{
name: 'drive_get_file',
description:
'Get metadata for a file in Google Drive.',
inputSchema: {
type: 'object',
properties: {
fileId: {
type: 'string',
description: 'ID of the file',
},
},
required: ['fileId'],
},
},
];
// Tool handlers
export const driveHandlers: Record<
string,
(input: unknown) => Promise<unknown>
> = {
drive_search_files: driveSearchFiles,
drive_create_folder: driveCreateFolder,
drive_set_permission: driveSetPermission,
drive_copy_file: driveCopyFile,
drive_upload_file: driveUploadFile,
drive_download_file: driveDownloadFile,
drive_delete_file: driveDeleteFile,
drive_move_file: driveMoveFile,
drive_get_file: driveGetFile,
};