// Binary file tools - upload and download binary files (images, PDFs, etc.)
import { z } from 'zod';
import * as fs from 'fs/promises';
import * as path from 'path';
import type { DriveService } from '../services/drive.js';
import { getMimeTypeFromFilename } from '../utils/mime.js';
// Tool definitions for MCP
export const binaryTools = [
{
name: 'uploadBinaryFile',
description: 'Upload a binary file (image, PDF, etc.) to Google Drive from base64 content or local file path',
inputSchema: {
type: 'object' as const,
properties: {
name: { type: 'string', description: 'File name (e.g., "photo.jpg", "document.pdf")' },
content: { type: 'string', description: 'Base64-encoded file content (use this OR localPath)' },
localPath: { type: 'string', description: 'Local file path to upload (use this OR content)' },
mimeType: { type: 'string', description: 'MIME type (optional, auto-detected from name)' },
folderId: { type: 'string', description: 'Parent folder ID or path (optional)' },
},
required: ['name'],
},
},
{
name: 'downloadBinaryFile',
description: 'Download a binary file from Google Drive and save to local path or return as base64',
inputSchema: {
type: 'object' as const,
properties: {
fileId: { type: 'string', description: 'Google Drive file ID' },
localPath: { type: 'string', description: 'Local path to save file (optional, returns base64 if not provided)' },
},
required: ['fileId'],
},
},
];
// Schemas for validation
export const UploadBinaryFileSchema = z.object({
name: z.string().describe('File name'),
content: z.string().optional().describe('Base64-encoded content'),
localPath: z.string().optional().describe('Local file path'),
mimeType: z.string().optional().describe('MIME type'),
folderId: z.string().optional().describe('Parent folder'),
}).refine(
(data) => data.content || data.localPath,
{ message: 'Either content (base64) or localPath must be provided' }
);
export const DownloadBinaryFileSchema = z.object({
fileId: z.string().describe('File ID'),
localPath: z.string().optional().describe('Local path to save'),
});
// Handler factory
export function createBinaryHandlers(driveService: DriveService) {
return {
uploadBinaryFile: async (args: z.infer<typeof UploadBinaryFileSchema>) => {
const params = UploadBinaryFileSchema.parse(args);
let buffer: Buffer;
if (params.localPath) {
// Read from local file
const absolutePath = path.resolve(params.localPath);
buffer = await fs.readFile(absolutePath);
} else if (params.content) {
// Decode base64
buffer = Buffer.from(params.content, 'base64');
} else {
throw new Error('Either content (base64) or localPath must be provided');
}
// Auto-detect MIME type if not provided
const mimeType = params.mimeType || getMimeTypeFromFilename(params.name) || 'application/octet-stream';
const file = await driveService.uploadBinaryFile(
params.name,
buffer,
mimeType,
params.folderId
);
return {
id: file.id,
name: file.name,
mimeType: file.mimeType,
size: file.size,
webViewLink: file.webViewLink,
};
},
downloadBinaryFile: async (args: z.infer<typeof DownloadBinaryFileSchema>) => {
const params = DownloadBinaryFileSchema.parse(args);
const buffer = await driveService.downloadFile(params.fileId);
if (params.localPath) {
// Save to local file
const absolutePath = path.resolve(params.localPath);
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
await fs.writeFile(absolutePath, buffer);
return {
saved: true,
localPath: absolutePath,
size: buffer.length,
};
} else {
// Return as base64
return {
content: buffer.toString('base64'),
size: buffer.length,
};
}
},
};
}