Spaces:
Sleeping
Sleeping
| import type { FastMCP } from 'fastmcp'; | |
| import { UserError } from 'fastmcp'; | |
| import { z } from 'zod'; | |
| import { docs_v1 } from 'googleapis'; | |
| import { getDocsClient } from '../../clients.js'; | |
| import { DocumentIdParameter } from '../../types.js'; | |
| import * as GDocsHelpers from '../../googleDocsApiHelpers.js'; | |
| export interface UpdateSectionStyleBuilderInput { | |
| startIndex: number; | |
| endIndex: number; | |
| flipPageOrientation?: boolean; | |
| sectionType?: 'SECTION_TYPE_UNSPECIFIED' | 'CONTINUOUS' | 'NEXT_PAGE'; | |
| marginTop?: number; | |
| marginBottom?: number; | |
| marginLeft?: number; | |
| marginRight?: number; | |
| pageNumberStart?: number; | |
| tabId?: string; | |
| } | |
| export function buildUpdateSectionStyleRequest( | |
| params: UpdateSectionStyleBuilderInput | |
| ): { request: docs_v1.Schema$Request; fields: string[] } | null { | |
| const sectionStyle: docs_v1.Schema$SectionStyle = {}; | |
| const fields: string[] = []; | |
| if (params.flipPageOrientation !== undefined) { | |
| sectionStyle.flipPageOrientation = params.flipPageOrientation; | |
| fields.push('flipPageOrientation'); | |
| } | |
| if (params.sectionType !== undefined) { | |
| sectionStyle.sectionType = params.sectionType; | |
| fields.push('sectionType'); | |
| } | |
| if (params.marginTop !== undefined) { | |
| sectionStyle.marginTop = { magnitude: params.marginTop, unit: 'PT' }; | |
| fields.push('marginTop'); | |
| } | |
| if (params.marginBottom !== undefined) { | |
| sectionStyle.marginBottom = { magnitude: params.marginBottom, unit: 'PT' }; | |
| fields.push('marginBottom'); | |
| } | |
| if (params.marginLeft !== undefined) { | |
| sectionStyle.marginLeft = { magnitude: params.marginLeft, unit: 'PT' }; | |
| fields.push('marginLeft'); | |
| } | |
| if (params.marginRight !== undefined) { | |
| sectionStyle.marginRight = { magnitude: params.marginRight, unit: 'PT' }; | |
| fields.push('marginRight'); | |
| } | |
| if (params.pageNumberStart !== undefined) { | |
| sectionStyle.pageNumberStart = params.pageNumberStart; | |
| fields.push('pageNumberStart'); | |
| } | |
| if (fields.length === 0) { | |
| return null; | |
| } | |
| const range: any = { | |
| startIndex: params.startIndex, | |
| endIndex: params.endIndex, | |
| }; | |
| if (params.tabId) { | |
| range.tabId = params.tabId; | |
| } | |
| return { | |
| request: { | |
| updateSectionStyle: { | |
| range, | |
| sectionStyle, | |
| fields: fields.join(','), | |
| }, | |
| }, | |
| fields, | |
| }; | |
| } | |
| export function register(server: FastMCP) { | |
| server.addTool({ | |
| name: 'updateSectionStyle', | |
| description: | |
| 'Updates the style of a section identified by a character range. The range must cover the section whose style is changing — typically from the character right after an inserted section break to any index inside that section. Supports flipping page orientation (landscape <-> portrait), setting margins, and changing the section type. Common workflow for a landscape page: call insertSectionBreak with sectionType="NEXT_PAGE" before the landscape content, call insertSectionBreak again after it, then call updateSectionStyle on the range between the two breaks with flipPageOrientation=true.', | |
| parameters: DocumentIdParameter.extend({ | |
| startIndex: z | |
| .number() | |
| .int() | |
| .min(1) | |
| .describe( | |
| 'The starting index of the range covering the section to style (inclusive, 1-based). Typically the index immediately after the section break that opened this section.' | |
| ), | |
| endIndex: z | |
| .number() | |
| .int() | |
| .min(1) | |
| .describe( | |
| 'The ending index of the range covering the section to style (exclusive). Any index inside the section works — the style applies to the whole section.' | |
| ), | |
| flipPageOrientation: z | |
| .boolean() | |
| .optional() | |
| .describe( | |
| 'If true, swaps the section page width and height (effectively toggling between portrait and landscape). If false, keeps the default document orientation.' | |
| ), | |
| sectionType: z | |
| .enum(['SECTION_TYPE_UNSPECIFIED', 'CONTINUOUS', 'NEXT_PAGE']) | |
| .optional() | |
| .describe( | |
| 'Changes the section type after the break. Usually set at insert time via insertSectionBreak, but can be updated here.' | |
| ), | |
| marginTop: z | |
| .number() | |
| .nonnegative() | |
| .optional() | |
| .describe('Top margin of the section in points (1 inch = 72 points).'), | |
| marginBottom: z | |
| .number() | |
| .nonnegative() | |
| .optional() | |
| .describe('Bottom margin of the section in points.'), | |
| marginLeft: z | |
| .number() | |
| .nonnegative() | |
| .optional() | |
| .describe('Left margin of the section in points.'), | |
| marginRight: z | |
| .number() | |
| .nonnegative() | |
| .optional() | |
| .describe('Right margin of the section in points.'), | |
| pageNumberStart: z | |
| .number() | |
| .int() | |
| .min(1) | |
| .optional() | |
| .describe( | |
| 'Page number to start the section from. If unset, numbering continues from the previous section.' | |
| ), | |
| tabId: z | |
| .string() | |
| .optional() | |
| .describe( | |
| 'The ID of the specific tab to update. Use listDocumentTabs to get tab IDs. If not specified, targets 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( | |
| `Updating section style in doc ${args.documentId} for range ${args.startIndex}-${args.endIndex}${args.tabId ? ` (tab: ${args.tabId})` : ''}` | |
| ); | |
| try { | |
| if (args.tabId) { | |
| const docInfo = await docs.documents.get({ | |
| documentId: args.documentId, | |
| includeTabsContent: true, | |
| fields: 'tabs(tabProperties,documentTab(body))', | |
| }); | |
| 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).` | |
| ); | |
| } | |
| } | |
| const built = buildUpdateSectionStyleRequest({ | |
| startIndex: args.startIndex, | |
| endIndex: args.endIndex, | |
| flipPageOrientation: args.flipPageOrientation, | |
| sectionType: args.sectionType, | |
| marginTop: args.marginTop, | |
| marginBottom: args.marginBottom, | |
| marginLeft: args.marginLeft, | |
| marginRight: args.marginRight, | |
| pageNumberStart: args.pageNumberStart, | |
| tabId: args.tabId, | |
| }); | |
| if (!built) { | |
| throw new UserError( | |
| 'No section style options were provided. Set at least one of: flipPageOrientation, sectionType, marginTop, marginBottom, marginLeft, marginRight, pageNumberStart.' | |
| ); | |
| } | |
| await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [built.request]); | |
| return `Successfully updated section style (${built.fields.join(', ')}) for range ${args.startIndex}-${args.endIndex}${args.tabId ? ` in tab ${args.tabId}` : ''}.`; | |
| } catch (error: any) { | |
| log.error( | |
| `Error updating section style in doc ${args.documentId}: ${error.message || error}` | |
| ); | |
| if (error instanceof UserError) throw error; | |
| throw new UserError(`Failed to update section style: ${error.message || 'Unknown error'}`); | |
| } | |
| }, | |
| }); | |
| } | |