import type { FastMCP } from 'fastmcp'; import { UserError } from 'fastmcp'; import { z } from 'zod'; import { getDocsClient, getDriveClient, getScriptClient } from '../../clients.js'; import { DocumentIdParameter } from '../../types.js'; import * as GDocsHelpers from '../../googleDocsApiHelpers.js'; import { logger } from '../../logger.js'; export function register(server: FastMCP) { server.addTool({ name: 'insertImage', description: 'Inserts an inline image into a Google Document. Provide either a publicly accessible URL or a local file path. Local files are automatically uploaded to Google Drive before insertion.', parameters: DocumentIdParameter.extend({ imageUrl: z .string() .url() .optional() .describe('Publicly accessible URL to the image (http:// or https://).'), localImagePath: z .string() .optional() .describe( 'Absolute path to a local image file (supports .jpg, .jpeg, .png, .gif, .bmp, .webp, .svg). The file will be uploaded to Google Drive.' ), index: z .number() .int() .min(1) .describe( "1-based character index in the document body where the image should be inserted. Use readDocument with format='json' to inspect indices." ), width: z.number().min(1).optional().describe('Width of the image in points.'), height: z.number().min(1).optional().describe('Height of the image in points.'), tabId: z .string() .optional() .describe( 'The ID of the specific tab to insert into. Use listDocumentTabs to get tab IDs. If not specified, inserts into the first tab.' ), }) .refine((data) => data.imageUrl || data.localImagePath, { message: 'Either imageUrl or localImagePath must be provided.', }) .refine((data) => !(data.imageUrl && data.localImagePath), { message: 'Provide only one of imageUrl or localImagePath, not both.', }), execute: async (args, { log }) => { const docs = await getDocsClient(); const appsScriptDeploymentId = process.env.APPS_SCRIPT_DEPLOYMENT_ID; try { if (args.tabId) { const docInfo = await docs.documents.get({ documentId: args.documentId, includeTabsContent: true, fields: 'tabs(tabProperties,documentTab(body(content(endIndex))))', }); const targetTab = GDocsHelpers.findTabById(docInfo.data, args.tabId); if (!targetTab) { throw new UserError(`Tab with ID "${args.tabId}" not found in document.`); } if (!targetTab.documentTab) { throw new UserError( `Tab "${args.tabId}" does not have content (may not be a document tab).` ); } } // --- Apps Script path: local files when APPS_SCRIPT_DEPLOYMENT_ID is set --- if (args.localImagePath && appsScriptDeploymentId) { const drive = await getDriveClient(); const scriptClient = await getScriptClient(); log.info(`[AppsScript] Uploading ${args.localImagePath} to Drive (no public sharing)`); let parentFolderId: string | undefined; try { const docInfo = await drive.files.get({ fileId: args.documentId, fields: 'parents', supportsAllDrives: true, }); if (docInfo.data.parents && docInfo.data.parents.length > 0) { parentFolderId = docInfo.data.parents[0]; } } catch (folderError) { log.warn( `Could not determine document's parent folder, using Drive root: ${folderError}` ); } const driveFileId = await GDocsHelpers.uploadImageToDrive( drive, args.localImagePath, parentFolderId, true // skipPublicSharing ); log.info( `[AppsScript] Inserting image via marker at index ${args.index} (fileId: ${driveFileId})` ); await GDocsHelpers.insertImageViaAppsScript( docs, scriptClient, appsScriptDeploymentId, args.documentId, driveFileId, args.index, args.tabId ); return `Successfully inserted local image at index ${args.index} via Apps Script${args.tabId ? ` in tab ${args.tabId}` : ''}.`; } // --- Standard path: public URL insertion via Docs API --- let resolvedUrl: string; if (args.localImagePath) { const drive = await getDriveClient(); log.info( `Uploading local image ${args.localImagePath} and inserting at index ${args.index} in doc ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}` ); let parentFolderId: string | undefined; try { const docInfo = await drive.files.get({ fileId: args.documentId, fields: 'parents', supportsAllDrives: true, }); if (docInfo.data.parents && docInfo.data.parents.length > 0) { parentFolderId = docInfo.data.parents[0]; } } catch (folderError) { log.warn( `Could not determine document's parent folder, using Drive root: ${folderError}` ); } resolvedUrl = await GDocsHelpers.uploadImageToDrive( drive, args.localImagePath, parentFolderId, false // explicit: needs public URL for Docs API insertion ); log.info(`Image uploaded successfully, URL: ${resolvedUrl}`); } else { resolvedUrl = args.imageUrl!; log.info( `Inserting image from URL ${resolvedUrl} at index ${args.index} in doc ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}` ); } await GDocsHelpers.insertInlineImage( docs, args.documentId, resolvedUrl, args.index, args.width, args.height, args.tabId ); let sizeInfo = ''; if (args.width && args.height) { sizeInfo = ` with size ${args.width}x${args.height}pt`; } return `Successfully inserted image at index ${args.index}${sizeInfo}${args.tabId ? ` in tab ${args.tabId}` : ''}.`; } catch (error: any) { log.error(`Error inserting image in doc ${args.documentId}: ${error.message || error}`); if (error instanceof UserError) throw error; throw new UserError(`Failed to insert image: ${error.message || 'Unknown error'}`); } }, }); }