File size: 5,943 Bytes
7dc28be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import type { FastMCP } from 'fastmcp';
import { UserError } from 'fastmcp';
import { z } from 'zod';
import { getDocsClient } from '../../clients.js';
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
import { DocumentIdParameter } from '../../types.js';
import { getTableById } from './structureHelpers.js';
import { replaceTableRowData as replaceTableRowDataInternal } from './tableRowDataHelpers.js';

export function register(server: FastMCP) {
  server.addTool({
    name: 'appendTableRows',
    description:
      'Appends one or more plain-text rows to the end of an existing Google Docs table while preserving the table structure.',
    parameters: DocumentIdParameter.extend({
      tableId: z
        .string()
        .min(1)
        .describe('The MCP table ID returned by listDocumentTables, for example "table:body:0".'),
      rows: z
        .array(z.array(z.string()).max(50))
        .min(1)
        .max(200)
        .describe('Rows to append. Each inner array is the plain-text cell values for one row.'),
      tabId: z
        .string()
        .optional()
        .describe(
          'The ID of the specific tab containing the table. If not specified, uses the first tab or legacy document body.'
        ),
    }),
    execute: async (args, { log }) => {
      const docs = await getDocsClient();
      log.info(
        `Appending ${args.rows.length} row(s) to ${args.tableId} in doc ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`
      );

      try {
        const res = await docs.documents.get({
          documentId: args.documentId,
          includeTabsContent: true,
          fields:
            'body(content(startIndex,endIndex,table(tableRows(tableCells(startIndex,endIndex,content(startIndex,endIndex,paragraph(elements(startIndex,endIndex,textRun(content))))))))),tabs(tabProperties(tabId,title),documentTab(body(content(startIndex,endIndex,table(tableRows(tableCells(startIndex,endIndex,content(startIndex,endIndex,paragraph(elements(startIndex,endIndex,textRun(content)))))))))))',
        });

        const table = getTableById(res.data, args.tableId, args.tabId);
        if (!table) {
          throw new UserError(`Table "${args.tableId}" not found in document.`);
        }
        if (table.startIndex == null) {
          throw new UserError(`Table "${args.tableId}" does not expose a valid table start index.`);
        }

        for (const [offset, rowValues] of args.rows.entries()) {
          if (rowValues.length > table.columnCount) {
            throw new UserError(
              `Row ${offset} has ${rowValues.length} values, but table ${args.tableId} only has ${table.columnCount} columns.`
            );
          }
        }

        const insertRequests = args.rows.map(() =>
          GDocsHelpers.buildInsertTableRowRequest(
            table.startIndex!,
            table.rowCount - 1,
            true,
            args.tabId
          )
        );
        await GDocsHelpers.executeBatchUpdateWithSplitting(
          docs,
          args.documentId,
          insertRequests,
          log
        );

        const refreshed = await docs.documents.get({
          documentId: args.documentId,
          includeTabsContent: true,
          fields:
            'body(content(startIndex,endIndex,table(tableRows(tableCells(startIndex,endIndex,content(startIndex,endIndex,paragraph(elements(startIndex,endIndex,textRun(content))))))))),tabs(tabProperties(tabId,title),documentTab(body(content(startIndex,endIndex,table(tableRows(tableCells(startIndex,endIndex,content(startIndex,endIndex,paragraph(elements(startIndex,endIndex,textRun(content)))))))))))',
        });

        const updatedTable = getTableById(refreshed.data, args.tableId, args.tabId);
        if (!updatedTable) {
          throw new UserError(`Table "${args.tableId}" could not be found after appending rows.`);
        }

        const firstAppendedRowIndex = table.rowCount;
        for (const [offset, rowValues] of args.rows.entries()) {
          const currentTable =
            offset === 0
              ? updatedTable
              : getTableById(
                  (
                    await docs.documents.get({
                      documentId: args.documentId,
                      includeTabsContent: true,
                      fields:
                        'body(content(startIndex,endIndex,table(tableRows(tableCells(startIndex,endIndex,content(startIndex,endIndex,paragraph(elements(startIndex,endIndex,textRun(content))))))))),tabs(tabProperties(tabId,title),documentTab(body(content(startIndex,endIndex,table(tableRows(tableCells(startIndex,endIndex,content(startIndex,endIndex,paragraph(elements(startIndex,endIndex,textRun(content)))))))))))',
                    })
                  ).data,
                  args.tableId,
                  args.tabId
                );
          if (!currentTable) {
            throw new UserError(
              `Table "${args.tableId}" could not be re-fetched while populating appended rows.`
            );
          }
          await replaceTableRowDataInternal(
            docs,
            args.documentId,
            currentTable,
            firstAppendedRowIndex + offset,
            rowValues,
            args.tabId
          );
        }

        return `Successfully appended ${args.rows.length} row(s) to table ${args.tableId}.`;
      } catch (error: any) {
        log.error(
          `Error appending rows to ${args.tableId} in doc ${args.documentId}: ${error.message || error}`
        );
        if (error instanceof UserError) throw error;
        if (error.code === 404) throw new UserError(`Document not found (ID: ${args.documentId}).`);
        if (error.code === 403)
          throw new UserError(`Permission denied for document (ID: ${args.documentId}).`);
        throw new UserError(`Failed to append table rows: ${error.message || 'Unknown error'}`);
      }
    },
  });
}