import type { FastMCP } from 'fastmcp'; import { UserError } from 'fastmcp'; import { z } from 'zod'; import { getDocsClient } from '../../clients.js'; import { DocumentIdParameter, MarkdownConversionError } from '../../types.js'; import * as GDocsHelpers from '../../googleDocsApiHelpers.js'; import { insertMarkdown, formatInsertResult } from '../../markdown-transformer/index.js'; export function register(server: FastMCP) { server.addTool({ name: 'replaceRangeWithMarkdown', description: "Replaces a specific character range in a document with formatted markdown content. Use readDocument with format='json' to determine the start and end indices of the content you want to replace. Supports headings, bold, italic, strikethrough, links, bullet/numbered lists, code blocks, horizontal rules, and tables.", parameters: DocumentIdParameter.extend({ startIndex: z .number() .int() .min(1) .describe( "1-based character index where the replacement range begins (inclusive). Use readDocument with format='json' to find the correct index." ), endIndex: z .number() .int() .min(1) .describe( "1-based character index where the replacement range ends (exclusive). Use readDocument with format='json' to find the correct index." ), markdown: z .string() .min(1) .describe('The markdown content to insert in place of the deleted range.'), tabId: z .string() .optional() .describe( 'The ID of the specific tab to modify. If not specified, modifies the first tab.' ), }).refine((data) => data.endIndex > data.startIndex, { message: 'endIndex must be greater than startIndex', path: ['endIndex'], }), execute: async (args, { log }) => { const docs = await getDocsClient(); log.info( `Replacing range ${args.startIndex}-${args.endIndex} in doc ${args.documentId} with markdown (${args.markdown.length} chars)${args.tabId ? ` in tab ${args.tabId}` : ''}` ); try { // 1. Delete the existing content in the specified range const deleteRange: any = { startIndex: args.startIndex, endIndex: args.endIndex, }; if (args.tabId) { deleteRange.tabId = args.tabId; } log.info(`Deleting content from index ${args.startIndex} to ${args.endIndex}`); await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [ { deleteContentRange: { range: deleteRange }, }, ]); log.info(`Delete complete.`); // 2. Insert markdown at the start position (which is now where the deleted content was) log.info(`Inserting markdown at index ${args.startIndex}`); const result = await insertMarkdown(docs, args.documentId, args.markdown, { startIndex: args.startIndex, tabId: args.tabId, }); const debugSummary = formatInsertResult(result); log.info(debugSummary); return `Successfully replaced range ${args.startIndex}-${args.endIndex} with ${args.markdown.length} characters of markdown.\n\n${debugSummary}`; } catch (error: any) { log.error(`Error replacing range with markdown: ${error.message}`); if (error instanceof UserError || error instanceof MarkdownConversionError) { throw error; } throw new UserError( `Failed to replace range with markdown: ${error.message || 'Unknown error'}` ); } }, }); }