mcp-gsheets 1.7.1 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +53 -1
  2. package/dist/index.js +733 -126
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ <!-- mcp-name: io.github.freema/mcp-gsheets -->
2
+
1
3
  # MCP Google Sheets Server
2
4
 
3
5
  <a href="https://glama.ai/mcp/servers/@freema/mcp-gsheets">
@@ -330,6 +332,8 @@ npm run dev # Watch mode with auto-reload
330
332
  | `sheets_append_values` | Append rows after the last row of an existing table. **Default `insertDataOption` is `OVERWRITE`** — set `INSERT_ROWS` to push existing rows down | `spreadsheetId`, `range`, `values`, `valueInputOption`, `insertDataOption` |
331
333
  | `sheets_clear_values` | Clear all values in a range (preserves formatting) | `spreadsheetId`, `range` |
332
334
  | `sheets_insert_rows` | Insert blank or pre-filled rows at a specific position | `spreadsheetId`, `range` (anchor), `rows`, `position` (BEFORE/AFTER), `values` |
335
+ | `sheets_delete_columns` | Delete one or more columns using a full-column A1 range | `spreadsheetId`, `range` (e.g. `Sheet1!B:D`) |
336
+ | `sheets_delete_rows` | Delete one or more rows using a full-row A1 range | `spreadsheetId`, `range` (e.g. `Sheet1!2:4`) |
333
337
  | `sheets_insert_link` | Insert a hyperlink formula into a cell | `spreadsheetId`, `range`, `url`, `label` |
334
338
  | `sheets_insert_date` | Insert a date/datetime value formatted correctly into a cell | `spreadsheetId`, `range`, `date`, `format` |
335
339
 
@@ -537,6 +541,54 @@ Insert new rows at a specific position in a spreadsheet with optional data.
537
541
  }
538
542
  ```
539
543
 
544
+ ### sheets_delete_columns
545
+
546
+ Delete one or more columns from a sheet using a full-column A1 range.
547
+
548
+ **Parameters:**
549
+ - `spreadsheetId` (required): The ID of the spreadsheet
550
+ - `range` (required): Full-column A1 range to delete (e.g., "Sheet1!B:D" or "Sheet1!C:C")
551
+
552
+ **Examples:**
553
+
554
+ ```javascript
555
+ // Delete columns B through D from Sheet1
556
+ {
557
+ "spreadsheetId": "your-spreadsheet-id",
558
+ "range": "Sheet1!B:D"
559
+ }
560
+
561
+ // Delete a single column from the first sheet
562
+ {
563
+ "spreadsheetId": "your-spreadsheet-id",
564
+ "range": "C:C"
565
+ }
566
+ ```
567
+
568
+ ### sheets_delete_rows
569
+
570
+ Delete one or more rows from a sheet using a full-row A1 range.
571
+
572
+ **Parameters:**
573
+ - `spreadsheetId` (required): The ID of the spreadsheet
574
+ - `range` (required): Full-row A1 range to delete (e.g., "Sheet1!2:4" or "Sheet1!3:3")
575
+
576
+ **Examples:**
577
+
578
+ ```javascript
579
+ // Delete rows 2 through 4 from Sheet1
580
+ {
581
+ "spreadsheetId": "your-spreadsheet-id",
582
+ "range": "Sheet1!2:4"
583
+ }
584
+
585
+ // Delete a single row from the first sheet
586
+ {
587
+ "spreadsheetId": "your-spreadsheet-id",
588
+ "range": "3:3"
589
+ }
590
+ ```
591
+
540
592
  ## 📋 Changelog
541
593
 
542
594
  See [CHANGELOG.md](CHANGELOG.md) for a list of changes in each version.
@@ -556,4 +608,4 @@ See [CHANGELOG.md](CHANGELOG.md) for a list of changes in each version.
556
608
 
557
609
  ## 📄 License
558
610
 
559
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
611
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
package/dist/index.js CHANGED
@@ -18085,6 +18085,8 @@ var ERROR_MESSAGES = {
18085
18085
  REQUIRED_NON_NEGATIVE: (field) => `${field} must be a non-negative number`,
18086
18086
  // Range errors
18087
18087
  INVALID_RANGE: 'Invalid range format. Use A1 notation (e.g., "Sheet1!A1:B10")',
18088
+ INVALID_COLUMN_RANGE: 'Invalid column range format. Use a full-column A1 range (e.g., "Sheet1!B:D" or "Sheet1!C:C")',
18089
+ INVALID_ROW_RANGE: 'Invalid row range format. Use a full-row A1 range (e.g., "Sheet1!2:4" or "Sheet1!3:3")',
18088
18090
  RANGE_REQUIRED: "range is required and must be a string",
18089
18091
  // Spreadsheet errors
18090
18092
  SPREADSHEET_ID_REQUIRED: "spreadsheetId is required and must be a string",
@@ -18257,6 +18259,105 @@ function createSheetValidator(additionalValidation, defaults) {
18257
18259
  );
18258
18260
  }
18259
18261
 
18262
+ // src/utils/range-helpers.ts
18263
+ function findSheetOrThrow(sheets, sheetName) {
18264
+ const sheet = sheets.find((s) => s.properties?.title === sheetName);
18265
+ if (!sheet) {
18266
+ const available = sheets.map((s) => s.properties?.title).filter(Boolean).join(", ");
18267
+ throw new Error(`Sheet "${sheetName}" not found. Available: ${available}`);
18268
+ }
18269
+ return sheet;
18270
+ }
18271
+ function gridRangeToA1(range) {
18272
+ const startCol = range.startColumnIndex ?? 0;
18273
+ const startRow = (range.startRowIndex ?? 0) + 1;
18274
+ const endCol = (range.endColumnIndex ?? startCol + 1) - 1;
18275
+ const endRow = range.endRowIndex ?? startRow;
18276
+ return `${colIndexToLetter(startCol)}${startRow}:${colIndexToLetter(endCol)}${endRow}`;
18277
+ }
18278
+ function columnToIndex(column) {
18279
+ let index = 0;
18280
+ for (let i = 0; i < column.length; i++) {
18281
+ index = index * 26 + (column.charCodeAt(i) - "A".charCodeAt(0) + 1);
18282
+ }
18283
+ return index - 1;
18284
+ }
18285
+ function parseRange(range, sheetId) {
18286
+ const rangePart = range.includes("!") ? range.split("!")[1] : range;
18287
+ if (!rangePart) {
18288
+ throw new Error(`Invalid range format: ${range}`);
18289
+ }
18290
+ const singleCellMatch = rangePart.match(/^([A-Z]+)(\d+)$/);
18291
+ if (singleCellMatch?.[1] && singleCellMatch[2]) {
18292
+ const col = columnToIndex(singleCellMatch[1]);
18293
+ const row = parseInt(singleCellMatch[2]) - 1;
18294
+ return {
18295
+ sheetId: sheetId ?? null,
18296
+ startRowIndex: row,
18297
+ endRowIndex: row + 1,
18298
+ startColumnIndex: col,
18299
+ endColumnIndex: col + 1
18300
+ };
18301
+ }
18302
+ const rangeMatch = rangePart.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
18303
+ if (!rangeMatch?.[1] || !rangeMatch[2] || !rangeMatch[3] || !rangeMatch[4]) {
18304
+ throw new Error(`Invalid range format: ${range}`);
18305
+ }
18306
+ return {
18307
+ sheetId: sheetId ?? null,
18308
+ startRowIndex: parseInt(rangeMatch[2]) - 1,
18309
+ endRowIndex: parseInt(rangeMatch[4]),
18310
+ startColumnIndex: columnToIndex(rangeMatch[1]),
18311
+ endColumnIndex: columnToIndex(rangeMatch[3]) + 1
18312
+ };
18313
+ }
18314
+ async function getSheetId(sheets, spreadsheetId, sheetName) {
18315
+ const response = await sheets.spreadsheets.get({
18316
+ spreadsheetId,
18317
+ fields: "sheets.properties"
18318
+ });
18319
+ const sheetsData = response.data.sheets || [];
18320
+ if (sheetName) {
18321
+ const sheet = sheetsData.find((s) => s.properties?.title === sheetName);
18322
+ if (!sheet?.properties?.sheetId) {
18323
+ const availableSheets = sheetsData.map((s) => s.properties?.title).filter((title) => title).join(", ");
18324
+ throw new Error(`Sheet "${sheetName}" not found. Available sheets: ${availableSheets}`);
18325
+ }
18326
+ return sheet.properties.sheetId;
18327
+ }
18328
+ if (sheetsData.length > 0) {
18329
+ const firstSheet = sheetsData[0];
18330
+ if (firstSheet?.properties?.sheetId !== void 0 && firstSheet.properties.sheetId !== null) {
18331
+ return firstSheet.properties.sheetId;
18332
+ }
18333
+ }
18334
+ throw new Error("No sheets found in spreadsheet");
18335
+ }
18336
+ function extractSheetName(range) {
18337
+ if (range.includes("!")) {
18338
+ const parts = range.split("!");
18339
+ let sheetName = parts[0];
18340
+ const rangePart = parts[1] || "";
18341
+ if (sheetName) {
18342
+ if (sheetName.startsWith('"') && sheetName.endsWith('"') || sheetName.startsWith("'") && sheetName.endsWith("'")) {
18343
+ sheetName = sheetName.slice(1, -1);
18344
+ }
18345
+ return { sheetName, range: rangePart };
18346
+ }
18347
+ }
18348
+ return { range };
18349
+ }
18350
+ function colIndexToLetter(index) {
18351
+ let result = "";
18352
+ let n = index + 1;
18353
+ while (n > 0) {
18354
+ const rem = (n - 1) % 26;
18355
+ result = String.fromCharCode(65 + rem) + result;
18356
+ n = Math.floor((n - 1) / 26);
18357
+ }
18358
+ return result;
18359
+ }
18360
+
18260
18361
  // src/utils/validators.ts
18261
18362
  function validateRequiredString(value, fieldName) {
18262
18363
  if (!value || typeof value !== "string") {
@@ -18623,6 +18724,30 @@ function validateInsertRowsInput(input) {
18623
18724
  valueInputOption
18624
18725
  };
18625
18726
  }
18727
+ function validateDeleteColumnsInput(input) {
18728
+ validateSpreadsheetIdField(input.spreadsheetId);
18729
+ validateRangeField(input.range);
18730
+ const { range } = extractSheetName(input.range);
18731
+ if (!/^[A-Z]+:[A-Z]+$/i.test(range)) {
18732
+ throw new Error(ERROR_MESSAGES.INVALID_COLUMN_RANGE);
18733
+ }
18734
+ return {
18735
+ spreadsheetId: input.spreadsheetId,
18736
+ range: input.range
18737
+ };
18738
+ }
18739
+ function validateDeleteRowsInput(input) {
18740
+ validateSpreadsheetIdField(input.spreadsheetId);
18741
+ validateRangeField(input.range);
18742
+ const { range } = extractSheetName(input.range);
18743
+ if (!/^\d+:\d+$/.test(range)) {
18744
+ throw new Error(ERROR_MESSAGES.INVALID_ROW_RANGE);
18745
+ }
18746
+ return {
18747
+ spreadsheetId: input.spreadsheetId,
18748
+ range: input.range
18749
+ };
18750
+ }
18626
18751
 
18627
18752
  // src/utils/response-helpers.ts
18628
18753
  function createTextResponse(text) {
@@ -19471,105 +19596,6 @@ async function handleCopyTo(input) {
19471
19596
  }
19472
19597
  }
19473
19598
 
19474
- // src/utils/range-helpers.ts
19475
- function findSheetOrThrow(sheets, sheetName) {
19476
- const sheet = sheets.find((s) => s.properties?.title === sheetName);
19477
- if (!sheet) {
19478
- const available = sheets.map((s) => s.properties?.title).filter(Boolean).join(", ");
19479
- throw new Error(`Sheet "${sheetName}" not found. Available: ${available}`);
19480
- }
19481
- return sheet;
19482
- }
19483
- function gridRangeToA1(range) {
19484
- const startCol = range.startColumnIndex ?? 0;
19485
- const startRow = (range.startRowIndex ?? 0) + 1;
19486
- const endCol = (range.endColumnIndex ?? startCol + 1) - 1;
19487
- const endRow = range.endRowIndex ?? startRow;
19488
- return `${colIndexToLetter(startCol)}${startRow}:${colIndexToLetter(endCol)}${endRow}`;
19489
- }
19490
- function columnToIndex(column) {
19491
- let index = 0;
19492
- for (let i = 0; i < column.length; i++) {
19493
- index = index * 26 + (column.charCodeAt(i) - "A".charCodeAt(0) + 1);
19494
- }
19495
- return index - 1;
19496
- }
19497
- function parseRange(range, sheetId) {
19498
- const rangePart = range.includes("!") ? range.split("!")[1] : range;
19499
- if (!rangePart) {
19500
- throw new Error(`Invalid range format: ${range}`);
19501
- }
19502
- const singleCellMatch = rangePart.match(/^([A-Z]+)(\d+)$/);
19503
- if (singleCellMatch?.[1] && singleCellMatch[2]) {
19504
- const col = columnToIndex(singleCellMatch[1]);
19505
- const row = parseInt(singleCellMatch[2]) - 1;
19506
- return {
19507
- sheetId: sheetId ?? null,
19508
- startRowIndex: row,
19509
- endRowIndex: row + 1,
19510
- startColumnIndex: col,
19511
- endColumnIndex: col + 1
19512
- };
19513
- }
19514
- const rangeMatch = rangePart.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
19515
- if (!rangeMatch?.[1] || !rangeMatch[2] || !rangeMatch[3] || !rangeMatch[4]) {
19516
- throw new Error(`Invalid range format: ${range}`);
19517
- }
19518
- return {
19519
- sheetId: sheetId ?? null,
19520
- startRowIndex: parseInt(rangeMatch[2]) - 1,
19521
- endRowIndex: parseInt(rangeMatch[4]),
19522
- startColumnIndex: columnToIndex(rangeMatch[1]),
19523
- endColumnIndex: columnToIndex(rangeMatch[3]) + 1
19524
- };
19525
- }
19526
- async function getSheetId(sheets, spreadsheetId, sheetName) {
19527
- const response = await sheets.spreadsheets.get({
19528
- spreadsheetId,
19529
- fields: "sheets.properties"
19530
- });
19531
- const sheetsData = response.data.sheets || [];
19532
- if (sheetName) {
19533
- const sheet = sheetsData.find((s) => s.properties?.title === sheetName);
19534
- if (!sheet?.properties?.sheetId) {
19535
- const availableSheets = sheetsData.map((s) => s.properties?.title).filter((title) => title).join(", ");
19536
- throw new Error(`Sheet "${sheetName}" not found. Available sheets: ${availableSheets}`);
19537
- }
19538
- return sheet.properties.sheetId;
19539
- }
19540
- if (sheetsData.length > 0) {
19541
- const firstSheet = sheetsData[0];
19542
- if (firstSheet?.properties?.sheetId !== void 0 && firstSheet.properties.sheetId !== null) {
19543
- return firstSheet.properties.sheetId;
19544
- }
19545
- }
19546
- throw new Error("No sheets found in spreadsheet");
19547
- }
19548
- function extractSheetName(range) {
19549
- if (range.includes("!")) {
19550
- const parts = range.split("!");
19551
- let sheetName = parts[0];
19552
- const rangePart = parts[1] || "";
19553
- if (sheetName) {
19554
- if (sheetName.startsWith('"') && sheetName.endsWith('"') || sheetName.startsWith("'") && sheetName.endsWith("'")) {
19555
- sheetName = sheetName.slice(1, -1);
19556
- }
19557
- return { sheetName, range: rangePart };
19558
- }
19559
- }
19560
- return { range };
19561
- }
19562
- function colIndexToLetter(index) {
19563
- let result = "";
19564
- let n = index + 1;
19565
- while (n > 0) {
19566
- const rem = (n - 1) % 26;
19567
- result = String.fromCharCode(65 + rem) + result;
19568
- n = Math.floor((n - 1) / 26);
19569
- }
19570
- return result;
19571
- }
19572
-
19573
19599
  // src/utils/json-parser.ts
19574
19600
  function parseJsonInput(input, propertyName) {
19575
19601
  if (input && typeof input === "string") {
@@ -20614,29 +20640,23 @@ async function handleCreateChart(input) {
20614
20640
  const axes = [];
20615
20641
  if (validatedInput.domainAxis?.title) {
20616
20642
  const axis = {
20617
- position: "BOTTOM_AXIS"
20643
+ position: "BOTTOM_AXIS",
20644
+ title: validatedInput.domainAxis.title
20618
20645
  };
20619
- if (validatedInput.domainAxis.title !== void 0) {
20620
- axis.title = validatedInput.domainAxis.title;
20621
- }
20622
20646
  axes.push(axis);
20623
20647
  }
20624
20648
  if (validatedInput.leftAxis?.title) {
20625
20649
  const axis = {
20626
- position: "LEFT_AXIS"
20650
+ position: "LEFT_AXIS",
20651
+ title: validatedInput.leftAxis.title
20627
20652
  };
20628
- if (validatedInput.leftAxis.title !== void 0) {
20629
- axis.title = validatedInput.leftAxis.title;
20630
- }
20631
20653
  axes.push(axis);
20632
20654
  }
20633
20655
  if (validatedInput.rightAxis?.title) {
20634
20656
  const axis = {
20635
- position: "RIGHT_AXIS"
20657
+ position: "RIGHT_AXIS",
20658
+ title: validatedInput.rightAxis.title
20636
20659
  };
20637
- if (validatedInput.rightAxis.title !== void 0) {
20638
- axis.title = validatedInput.rightAxis.title;
20639
- }
20640
20660
  axes.push(axis);
20641
20661
  }
20642
20662
  if (axes.length > 0) {
@@ -21394,6 +21414,150 @@ async function handleInsertRows(input) {
21394
21414
  }
21395
21415
  }
21396
21416
 
21417
+ // src/tools/delete-columns.ts
21418
+ var deleteColumnsTool = {
21419
+ name: "sheets_delete_columns",
21420
+ description: "Delete one or more columns from a Google Sheet using a full-column A1 range",
21421
+ inputSchema: {
21422
+ type: "object",
21423
+ properties: {
21424
+ spreadsheetId: {
21425
+ type: "string",
21426
+ description: "The ID of the spreadsheet (found in the URL after /d/)"
21427
+ },
21428
+ range: {
21429
+ type: "string",
21430
+ description: 'Full-column A1 range to delete (e.g., "Sheet1!B:D" or "Sheet1!C:C"). If sheet name is omitted, the first sheet is used'
21431
+ }
21432
+ },
21433
+ required: ["spreadsheetId", "range"]
21434
+ }
21435
+ };
21436
+ function parseColumnRange(range) {
21437
+ const match = range.match(/^([A-Z]+):([A-Z]+)$/i);
21438
+ if (!match?.[1] || !match[2]) {
21439
+ throw new Error('Column range must use full-column A1 notation, e.g. "B:D" or "C:C"');
21440
+ }
21441
+ const startIndex = columnToIndex(match[1].toUpperCase());
21442
+ const endIndex = columnToIndex(match[2].toUpperCase()) + 1;
21443
+ if (endIndex <= startIndex) {
21444
+ throw new Error("Column range end must be greater than or equal to the start column");
21445
+ }
21446
+ return { startIndex, endIndex };
21447
+ }
21448
+ async function handleDeleteColumns(input) {
21449
+ try {
21450
+ const validatedInput = validateDeleteColumnsInput(input);
21451
+ const sheets = await getAuthenticatedClient();
21452
+ const { sheetName, range } = extractSheetName(validatedInput.range);
21453
+ const sheetId = await getSheetId(sheets, validatedInput.spreadsheetId, sheetName);
21454
+ const { startIndex, endIndex } = parseColumnRange(range);
21455
+ await sheets.spreadsheets.batchUpdate({
21456
+ spreadsheetId: validatedInput.spreadsheetId,
21457
+ requestBody: {
21458
+ requests: [
21459
+ {
21460
+ deleteDimension: {
21461
+ range: {
21462
+ sheetId,
21463
+ dimension: "COLUMNS",
21464
+ startIndex,
21465
+ endIndex
21466
+ }
21467
+ }
21468
+ }
21469
+ ]
21470
+ }
21471
+ });
21472
+ return formatToolResponse(
21473
+ `Successfully deleted ${endIndex - startIndex} columns in range ${validatedInput.range}`,
21474
+ {
21475
+ spreadsheetId: validatedInput.spreadsheetId,
21476
+ sheetId,
21477
+ deletedColumns: endIndex - startIndex,
21478
+ range: validatedInput.range
21479
+ }
21480
+ );
21481
+ } catch (error2) {
21482
+ return handleError(error2);
21483
+ }
21484
+ }
21485
+
21486
+ // src/tools/delete-rows.ts
21487
+ var deleteRowsTool = {
21488
+ name: "sheets_delete_rows",
21489
+ description: "Delete one or more rows from a Google Sheet using a full-row A1 range",
21490
+ inputSchema: {
21491
+ type: "object",
21492
+ properties: {
21493
+ spreadsheetId: {
21494
+ type: "string",
21495
+ description: "The ID of the spreadsheet (found in the URL after /d/)"
21496
+ },
21497
+ range: {
21498
+ type: "string",
21499
+ description: 'Full-row A1 range to delete (e.g., "Sheet1!2:4" or "Sheet1!3:3"). If sheet name is omitted, the first sheet is used'
21500
+ }
21501
+ },
21502
+ required: ["spreadsheetId", "range"]
21503
+ }
21504
+ };
21505
+ function parseRowRange(range) {
21506
+ const match = range.match(/^(\d+):(\d+)$/);
21507
+ if (!match?.[1] || !match[2]) {
21508
+ throw new Error('Row range must use full-row A1 notation, e.g. "2:4" or "3:3"');
21509
+ }
21510
+ const startRow = parseInt(match[1], 10);
21511
+ const endRow = parseInt(match[2], 10);
21512
+ if (Number.isNaN(startRow) || Number.isNaN(endRow) || startRow <= 0 || endRow <= 0) {
21513
+ throw new Error("Row numbers must be positive integers");
21514
+ }
21515
+ if (endRow < startRow) {
21516
+ throw new Error("Row range end must be greater than or equal to the start row");
21517
+ }
21518
+ return {
21519
+ startIndex: startRow - 1,
21520
+ endIndex: endRow
21521
+ };
21522
+ }
21523
+ async function handleDeleteRows(input) {
21524
+ try {
21525
+ const validatedInput = validateDeleteRowsInput(input);
21526
+ const sheets = await getAuthenticatedClient();
21527
+ const { sheetName, range } = extractSheetName(validatedInput.range);
21528
+ const sheetId = await getSheetId(sheets, validatedInput.spreadsheetId, sheetName);
21529
+ const { startIndex, endIndex } = parseRowRange(range);
21530
+ await sheets.spreadsheets.batchUpdate({
21531
+ spreadsheetId: validatedInput.spreadsheetId,
21532
+ requestBody: {
21533
+ requests: [
21534
+ {
21535
+ deleteDimension: {
21536
+ range: {
21537
+ sheetId,
21538
+ dimension: "ROWS",
21539
+ startIndex,
21540
+ endIndex
21541
+ }
21542
+ }
21543
+ }
21544
+ ]
21545
+ }
21546
+ });
21547
+ return formatToolResponse(
21548
+ `Successfully deleted ${endIndex - startIndex} rows in range ${validatedInput.range}`,
21549
+ {
21550
+ spreadsheetId: validatedInput.spreadsheetId,
21551
+ sheetId,
21552
+ deletedRows: endIndex - startIndex,
21553
+ range: validatedInput.range
21554
+ }
21555
+ );
21556
+ } catch (error2) {
21557
+ return handleError(error2);
21558
+ }
21559
+ }
21560
+
21397
21561
  // src/tools/get-merged-cells.ts
21398
21562
  var inputSchema = external_exports.object({
21399
21563
  spreadsheetId: external_exports.string(),
@@ -21600,14 +21764,91 @@ async function handleGetSheetFormatting(input) {
21600
21764
  }
21601
21765
  }
21602
21766
 
21767
+ // src/utils/formula-locale.ts
21768
+ function normalizeFormulaLocale(formula) {
21769
+ if (!formula?.startsWith("=")) {
21770
+ return { normalized: formula, raw: formula, localeDetected: "unknown" };
21771
+ }
21772
+ let result = "";
21773
+ let inString = false;
21774
+ let hasSemicolonSeparator = false;
21775
+ for (let i = 0; i < formula.length; i++) {
21776
+ const char = formula[i];
21777
+ if (char === '"') {
21778
+ if (inString && formula[i + 1] === '"') {
21779
+ result += '""';
21780
+ i++;
21781
+ continue;
21782
+ }
21783
+ inString = !inString;
21784
+ result += char;
21785
+ } else if (char === ";" && !inString) {
21786
+ hasSemicolonSeparator = true;
21787
+ result += ",";
21788
+ } else {
21789
+ result += char;
21790
+ }
21791
+ }
21792
+ const localeDetected = hasSemicolonSeparator ? "semicolon" : "comma";
21793
+ return { normalized: result, raw: formula, localeDetected };
21794
+ }
21795
+ function normalizeConditionalFormatFormulas(rule) {
21796
+ const result = { ...rule };
21797
+ if (result.booleanRule?.condition?.values) {
21798
+ const rawFormulas = [];
21799
+ result.booleanRule = {
21800
+ ...result.booleanRule,
21801
+ condition: {
21802
+ ...result.booleanRule.condition,
21803
+ values: result.booleanRule.condition.values.map((v) => {
21804
+ if (v.userEnteredValue?.startsWith?.("=")) {
21805
+ const { normalized, raw } = normalizeFormulaLocale(v.userEnteredValue);
21806
+ rawFormulas.push(raw);
21807
+ return { ...v, userEnteredValue: normalized };
21808
+ }
21809
+ return v;
21810
+ })
21811
+ }
21812
+ };
21813
+ if (rawFormulas.length > 0) {
21814
+ result._formulaLocaleRaw = rawFormulas;
21815
+ }
21816
+ }
21817
+ if (result.gradientRule) {
21818
+ const rawFormulas = [];
21819
+ const normalizeThreshold = (threshold) => {
21820
+ if (!threshold) {
21821
+ return threshold;
21822
+ }
21823
+ if (threshold.value?.startsWith?.("=")) {
21824
+ const { normalized, raw } = normalizeFormulaLocale(threshold.value);
21825
+ rawFormulas.push(raw);
21826
+ return { ...threshold, value: normalized };
21827
+ }
21828
+ return threshold;
21829
+ };
21830
+ result.gradientRule = {
21831
+ ...result.gradientRule,
21832
+ minpoint: normalizeThreshold(result.gradientRule.minpoint),
21833
+ midpoint: normalizeThreshold(result.gradientRule.midpoint),
21834
+ maxpoint: normalizeThreshold(result.gradientRule.maxpoint)
21835
+ };
21836
+ if (rawFormulas.length > 0) {
21837
+ result._formulaLocaleRaw = rawFormulas;
21838
+ }
21839
+ }
21840
+ return result;
21841
+ }
21842
+
21603
21843
  // src/tools/get-conditional-formatting-data.ts
21604
21844
  var inputSchema4 = external_exports.object({
21605
21845
  spreadsheetId: external_exports.string(),
21606
- sheetName: external_exports.string()
21846
+ sheetName: external_exports.string(),
21847
+ normalizeFormulas: external_exports.boolean().optional().default(true)
21607
21848
  });
21608
21849
  var getConditionalFormattingDataTool = {
21609
21850
  name: "sheets_get_conditional_formatting",
21610
- description: "Read conditional formatting rules and banded ranges (alternating row/column colors) for a sheet.",
21851
+ description: 'Read conditional formatting rules and banded ranges (alternating row/column colors) for a sheet. CF formulas are normalized to English locale (semicolons \u2192 commas) by default. Each rule with a formula includes a "_formulaLocaleRaw" field with the original unmodified formula. Set normalizeFormulas:false to get raw formulas as returned by the API.',
21611
21852
  inputSchema: {
21612
21853
  type: "object",
21613
21854
  properties: {
@@ -21618,6 +21859,10 @@ var getConditionalFormattingDataTool = {
21618
21859
  sheetName: {
21619
21860
  type: "string",
21620
21861
  description: "Name of the sheet (tab) to inspect"
21862
+ },
21863
+ normalizeFormulas: {
21864
+ type: "boolean",
21865
+ description: 'Default: true. Normalize formula separators to English locale (semicolons \u2192 commas). Each normalized rule includes "_formulaLocaleRaw" with the original formula. Set to false to get formulas exactly as returned by the Google Sheets API.'
21621
21866
  }
21622
21867
  },
21623
21868
  required: ["spreadsheetId", "sheetName"]
@@ -21625,18 +21870,21 @@ var getConditionalFormattingDataTool = {
21625
21870
  };
21626
21871
  async function handleGetConditionalFormattingData(input) {
21627
21872
  try {
21628
- const { spreadsheetId, sheetName } = inputSchema4.parse(input);
21873
+ const { spreadsheetId, sheetName, normalizeFormulas } = inputSchema4.parse(input);
21629
21874
  const sheets = await getAuthenticatedClient();
21630
21875
  const response = await sheets.spreadsheets.get({
21631
21876
  spreadsheetId,
21632
21877
  fields: "sheets.properties.title,sheets.properties.sheetId,sheets.conditionalFormats,sheets.bandedRanges"
21633
21878
  });
21634
21879
  const sheetData = findSheetOrThrow(response.data.sheets ?? [], sheetName);
21880
+ const rawFormats = sheetData.conditionalFormats ?? [];
21881
+ const conditionalFormats = normalizeFormulas ? rawFormats.map(normalizeConditionalFormatFormulas) : rawFormats;
21635
21882
  return formatSuccessResponse(
21636
21883
  {
21637
21884
  sheetName,
21638
21885
  sheetId: sheetData.properties?.sheetId,
21639
- conditionalFormats: sheetData.conditionalFormats ?? [],
21886
+ formulasNormalized: normalizeFormulas,
21887
+ conditionalFormats,
21640
21888
  bandedRanges: sheetData.bandedRanges ?? []
21641
21889
  },
21642
21890
  `Conditional formatting data for sheet "${sheetName}"`
@@ -21787,11 +22035,12 @@ var inputSchema5 = external_exports.object({
21787
22035
  includeFormattingRange: external_exports.string().optional(),
21788
22036
  useEffectiveFormat: external_exports.boolean().optional().default(false),
21789
22037
  fields: external_exports.array(external_exports.string()).optional(),
21790
- compactMode: external_exports.boolean().optional().default(false)
22038
+ compactMode: external_exports.boolean().optional().default(false),
22039
+ includeConditionalFormatting: external_exports.boolean().optional().default(true)
21791
22040
  });
21792
22041
  var getFullSheetSnapshotTool = {
21793
22042
  name: "sheets_get_full_sheet_snapshot",
21794
- description: "One-shot tool: reads all structural and formatting metadata for a sheet in a single API call. Returns: sheet properties (frozen rows/cols, dimensions, tab color), merged cells, column widths, row heights, conditional formatting, banded ranges, and optionally cell-level formatting for a specified range (includeFormattingRange). Use compactMode:true to collapse identical adjacent cells into range descriptors (90%+ smaller output). Use fields to limit which format properties are returned. Use this before programmatically recreating a sheet.",
22043
+ description: "One-shot tool: reads all structural and formatting metadata for a sheet in a single API call. Returns: sheet properties (frozen rows/cols, dimensions, tab color), merged cells, column widths, row heights, banded ranges, and optionally cell-level formatting for a specified range (includeFormattingRange). compactMode is OFF by default \u2014 full per-cell detail is returned unless compactMode:true is provided. When compactMode is ON, adjacent cells with identical formatting are collapsed into range descriptors (90%+ smaller output). Conditional formatting rules are included by default (includeConditionalFormatting:true); set to false to exclude them. CF formulas are normalized to English locale (commas). Use fields to limit which format properties are returned. Use this before programmatically recreating a sheet.",
21795
22044
  inputSchema: {
21796
22045
  type: "object",
21797
22046
  properties: {
@@ -21818,7 +22067,11 @@ var getFullSheetSnapshotTool = {
21818
22067
  },
21819
22068
  compactMode: {
21820
22069
  type: "boolean",
21821
- description: "When true, adjacent cells with identical formatting are collapsed into range descriptors (run-length encoded). Reduces cell formatting output by 90%+ for typical formatted sheets. Only applies when includeFormattingRange is set."
22070
+ description: "Default: false. Full per-cell formatting is returned unless compactMode:true is set. When true, adjacent cells with identical formatting are collapsed into range descriptors (run-length encoded), reducing output by 90%+ for typical formatted sheets. Only applies when includeFormattingRange is set."
22071
+ },
22072
+ includeConditionalFormatting: {
22073
+ type: "boolean",
22074
+ description: "Default: true. Include conditional formatting rules and banded ranges in the snapshot. CF formulas are normalized to English locale (semicolons \u2192 commas). Set to false to reduce output size when CF rules are not needed."
21822
22075
  }
21823
22076
  },
21824
22077
  required: ["spreadsheetId", "sheetName"]
@@ -21832,11 +22085,13 @@ async function handleGetFullSheetSnapshot(input) {
21832
22085
  includeFormattingRange,
21833
22086
  useEffectiveFormat,
21834
22087
  fields: formatFields,
21835
- compactMode
22088
+ compactMode,
22089
+ includeConditionalFormatting
21836
22090
  } = inputSchema5.parse(input);
21837
22091
  const sheets = await getAuthenticatedClient();
21838
22092
  const baseFields = "sheets.properties,sheets.merges,sheets.data.columnMetadata,sheets.data.rowMetadata,sheets.conditionalFormats,sheets.bandedRanges";
21839
22093
  const formatField = useEffectiveFormat ? "effectiveFormat" : "userEnteredFormat";
22094
+ const compact = compactMode === true;
21840
22095
  let cellDataFields;
21841
22096
  if (formatFields && formatFields.length > 0) {
21842
22097
  cellDataFields = formatFields.map((f) => `sheets.data.rowData.values.${formatField}.${f}`).join(",");
@@ -21880,7 +22135,7 @@ async function handleGetFullSheetSnapshot(input) {
21880
22135
  if (includeFormattingRange && gridData.rowData) {
21881
22136
  const startRow = gridData.startRow ?? 0;
21882
22137
  const startColumn = gridData.startColumn ?? 0;
21883
- if (compactMode) {
22138
+ if (compact) {
21884
22139
  cellFormatting = compactifyCellFormatting(
21885
22140
  gridData.rowData,
21886
22141
  startRow,
@@ -21904,6 +22159,11 @@ async function handleGetFullSheetSnapshot(input) {
21904
22159
  );
21905
22160
  }
21906
22161
  }
22162
+ const rawConditionalFormats = sheetData.conditionalFormats ?? [];
22163
+ const cfSection = includeConditionalFormatting ? {
22164
+ conditionalFormats: rawConditionalFormats.map(normalizeConditionalFormatFormulas),
22165
+ bandedRanges: sheetData.bandedRanges ?? []
22166
+ } : {};
21907
22167
  const snapshot = {
21908
22168
  sheetName,
21909
22169
  sheetId: props.sheetId,
@@ -21917,13 +22177,12 @@ async function handleGetFullSheetSnapshot(input) {
21917
22177
  merges,
21918
22178
  columns,
21919
22179
  rows,
21920
- conditionalFormats: sheetData.conditionalFormats ?? [],
21921
- bandedRanges: sheetData.bandedRanges ?? [],
22180
+ ...cfSection,
21922
22181
  ...cellFormatting !== null ? {
21923
22182
  cellFormatting: {
21924
22183
  range: `${sheetName}!${includeFormattingRange}`,
21925
22184
  formatType: formatField,
21926
- compact: compactMode,
22185
+ compact,
21927
22186
  data: cellFormatting
21928
22187
  }
21929
22188
  } : {}
@@ -22343,6 +22602,345 @@ async function handleGetBasicFilter(input) {
22343
22602
  }
22344
22603
  }
22345
22604
 
22605
+ // src/tools/get-border-map.ts
22606
+ var inputSchema10 = external_exports.object({
22607
+ spreadsheetId: external_exports.string(),
22608
+ range: external_exports.string().describe('Range with sheet prefix, e.g. "Sheet1!A1:F10"'),
22609
+ includeStyle: external_exports.boolean().optional().default(false)
22610
+ });
22611
+ var getBorderMapTool = {
22612
+ name: "sheets_get_border_map",
22613
+ description: 'Returns a visual tabular map of borders for a range. Instead of per-cell JSON with 4 separate border objects, returns compact grids showing which cells have top/bottom/left/right borders and their styles. Solves the ambiguity between "right border of cell N" vs "left border of cell N+1". Output: a horizontal-lines grid and a vertical-lines grid, each as a 2D array of line styles. Set includeStyle:true to include color and width details (larger output).',
22614
+ inputSchema: {
22615
+ type: "object",
22616
+ properties: {
22617
+ spreadsheetId: {
22618
+ type: "string",
22619
+ description: "The ID of the spreadsheet (found in the URL after /d/)"
22620
+ },
22621
+ range: {
22622
+ type: "string",
22623
+ description: 'Range with sheet prefix, e.g. "Sheet1!A1:F10". Sheet name is required to resolve the range correctly.'
22624
+ },
22625
+ includeStyle: {
22626
+ type: "boolean",
22627
+ description: "Default: false. When true, each border line includes color and width details. When false, only the style name (SOLID, DASHED, etc.) is shown \u2014 more compact."
22628
+ }
22629
+ },
22630
+ required: ["spreadsheetId", "range"]
22631
+ }
22632
+ };
22633
+ function encodeBorder(border, includeStyle) {
22634
+ if (!border || border.style === "NONE" || !border.style) {
22635
+ return null;
22636
+ }
22637
+ if (!includeStyle) {
22638
+ return border.style;
22639
+ }
22640
+ return {
22641
+ style: border.style,
22642
+ ...border.colorStyle ? { colorStyle: border.colorStyle } : {},
22643
+ ...border.color ? { color: border.color } : {},
22644
+ ...border.width !== void 0 ? { width: border.width } : {}
22645
+ };
22646
+ }
22647
+ async function handleGetBorderMap(input) {
22648
+ try {
22649
+ const { spreadsheetId, range, includeStyle } = inputSchema10.parse(input);
22650
+ if (!range.includes("!")) {
22651
+ throw new Error('Range must include sheet name prefix, e.g. "Sheet1!A1:F10"');
22652
+ }
22653
+ const { sheetName, range: rangeOnly } = extractSheetName(range);
22654
+ if (!sheetName) {
22655
+ throw new Error('Range must include sheet name prefix, e.g. "Sheet1!A1:F10"');
22656
+ }
22657
+ const rangeMatch = rangeOnly.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/i);
22658
+ if (!rangeMatch) {
22659
+ throw new Error(`Invalid range format: "${rangeOnly}". Expected e.g. "A1:F10".`);
22660
+ }
22661
+ const startCol = columnToIndex(rangeMatch[1].toUpperCase());
22662
+ const startRow = parseInt(rangeMatch[2]) - 1;
22663
+ const endCol = columnToIndex(rangeMatch[3].toUpperCase()) + 1;
22664
+ const endRow = parseInt(rangeMatch[4]);
22665
+ const numRows = endRow - startRow;
22666
+ const numCols = endCol - startCol;
22667
+ const sheets = await getAuthenticatedClient();
22668
+ const response = await sheets.spreadsheets.get({
22669
+ spreadsheetId,
22670
+ ranges: [range],
22671
+ includeGridData: true,
22672
+ fields: "sheets.properties.title,sheets.data.startRow,sheets.data.startColumn,sheets.data.rowData.values.userEnteredFormat.borders"
22673
+ });
22674
+ const sheetData = (response.data.sheets ?? []).find(
22675
+ (s) => s.properties?.title === sheetName
22676
+ );
22677
+ if (!sheetData) {
22678
+ const available = (response.data.sheets ?? []).map((s) => s.properties?.title).filter(Boolean).join(", ");
22679
+ throw new Error(`Sheet "${sheetName}" not found. Available: ${available}`);
22680
+ }
22681
+ const gridData = sheetData.data?.[0] ?? {};
22682
+ const rowData = gridData.rowData ?? [];
22683
+ const getBorder = (rowOffset, colOffset) => {
22684
+ const row = rowData[rowOffset];
22685
+ const cell = row?.values?.[colOffset];
22686
+ const borders = cell?.userEnteredFormat?.borders;
22687
+ return [
22688
+ borders?.top ?? null,
22689
+ borders?.bottom ?? null,
22690
+ borders?.left ?? null,
22691
+ borders?.right ?? null
22692
+ ];
22693
+ };
22694
+ const horizontalLines = [];
22695
+ for (let r = 0; r <= numRows; r++) {
22696
+ const row = [];
22697
+ for (let c = 0; c < numCols; c++) {
22698
+ let line = null;
22699
+ if (r > 0) {
22700
+ const [, bottom] = getBorder(r - 1, c);
22701
+ if (bottom?.style && bottom.style !== "NONE") {
22702
+ line = encodeBorder(bottom, includeStyle);
22703
+ }
22704
+ }
22705
+ if (r < numRows && line === null) {
22706
+ const [top] = getBorder(r, c);
22707
+ if (top?.style && top.style !== "NONE") {
22708
+ line = encodeBorder(top, includeStyle);
22709
+ }
22710
+ }
22711
+ row.push(line);
22712
+ }
22713
+ horizontalLines.push(row);
22714
+ }
22715
+ const verticalLines = [];
22716
+ for (let r = 0; r < numRows; r++) {
22717
+ const row = [];
22718
+ for (let c = 0; c <= numCols; c++) {
22719
+ let line = null;
22720
+ if (c < numCols) {
22721
+ const [, , left] = getBorder(r, c);
22722
+ if (left?.style && left.style !== "NONE") {
22723
+ line = encodeBorder(left, includeStyle);
22724
+ }
22725
+ }
22726
+ if (c > 0 && line === null) {
22727
+ const [, , , right] = getBorder(r, c - 1);
22728
+ if (right?.style && right.style !== "NONE") {
22729
+ line = encodeBorder(right, includeStyle);
22730
+ }
22731
+ }
22732
+ row.push(line);
22733
+ }
22734
+ verticalLines.push(row);
22735
+ }
22736
+ const colHeaders = Array.from({ length: numCols }, (_, i) => colIndexToLetter(startCol + i));
22737
+ const rowHeaders = Array.from({ length: numRows }, (_, i) => String(startRow + i + 1));
22738
+ return formatSuccessResponse(
22739
+ {
22740
+ range,
22741
+ sheetName,
22742
+ dimensions: { rows: numRows, cols: numCols },
22743
+ colHeaders,
22744
+ rowHeaders,
22745
+ // horizontalLines: (numRows+1) × numCols — line above each cell row
22746
+ horizontalLines: {
22747
+ description: "horizontalLines[r][c] = style of horizontal border line above row r at column c. r=0 \u2192 top edge, r=numRows \u2192 bottom edge.",
22748
+ rowCount: numRows + 1,
22749
+ colCount: numCols,
22750
+ data: horizontalLines
22751
+ },
22752
+ // verticalLines: numRows × (numCols+1) — line left of each cell column
22753
+ verticalLines: {
22754
+ description: "verticalLines[r][c] = style of vertical border line left of column c at row r. c=0 \u2192 left edge, c=numCols \u2192 right edge.",
22755
+ rowCount: numRows,
22756
+ colCount: numCols + 1,
22757
+ data: verticalLines
22758
+ }
22759
+ },
22760
+ `Border map for ${range} (${numRows}\xD7${numCols} cells)`
22761
+ );
22762
+ } catch (error2) {
22763
+ return handleError(error2);
22764
+ }
22765
+ }
22766
+
22767
+ // src/tools/compare-ranges.ts
22768
+ var inputSchema11 = external_exports.object({
22769
+ spreadsheetId: external_exports.string(),
22770
+ rangeA: external_exports.string().describe('First range with sheet prefix, e.g. "Sheet1!A6:Z6"'),
22771
+ rangeB: external_exports.string().describe('Second range with sheet prefix, e.g. "Sheet1!A7:Z7"'),
22772
+ fields: external_exports.array(external_exports.string()).optional(),
22773
+ useEffectiveFormat: external_exports.boolean().optional().default(false)
22774
+ });
22775
+ var compareRangesTool = {
22776
+ name: "sheets_compare_ranges",
22777
+ description: 'Compare cell formatting between two ranges of identical dimensions. Useful for verifying repeated patterns, e.g. "do all data rows 6\u201385 have identical formatting?" or "is row 10 formatted identically to the template row 5?". Returns a diff listing only the cells and properties that differ between the two ranges. Cells are compared position-by-position; rangeA and rangeB must have the same number of rows and columns. Use fields to restrict comparison to specific format properties.',
22778
+ inputSchema: {
22779
+ type: "object",
22780
+ properties: {
22781
+ spreadsheetId: {
22782
+ type: "string",
22783
+ description: "The ID of the spreadsheet (found in the URL after /d/)"
22784
+ },
22785
+ rangeA: {
22786
+ type: "string",
22787
+ description: 'First range with sheet prefix, e.g. "Sheet1!A6:Z6"'
22788
+ },
22789
+ rangeB: {
22790
+ type: "string",
22791
+ description: 'Second range with sheet prefix, e.g. "Sheet1!A7:Z7"'
22792
+ },
22793
+ fields: {
22794
+ type: "array",
22795
+ items: { type: "string" },
22796
+ description: 'Optional list of format property names to compare, e.g. ["backgroundColor", "textFormat"]. All format properties compared if omitted.'
22797
+ },
22798
+ useEffectiveFormat: {
22799
+ type: "boolean",
22800
+ description: "Default: false. Compare effectiveFormat (true) or userEnteredFormat (false). effectiveFormat includes conditional formatting overlays."
22801
+ }
22802
+ },
22803
+ required: ["spreadsheetId", "rangeA", "rangeB"]
22804
+ }
22805
+ };
22806
+ function parseRangeParts(range) {
22807
+ if (!range.includes("!")) {
22808
+ throw new Error(`Range must include sheet name prefix, e.g. "Sheet1!A1:F10". Got: "${range}"`);
22809
+ }
22810
+ const { sheetName, range: rangeOnly } = extractSheetName(range);
22811
+ if (!sheetName) {
22812
+ throw new Error(`Range must include sheet name prefix, e.g. "Sheet1!A1:F10". Got: "${range}"`);
22813
+ }
22814
+ const match = rangeOnly.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/i);
22815
+ if (!match) {
22816
+ throw new Error(`Invalid range format: "${rangeOnly}". Expected e.g. "A1:F10".`);
22817
+ }
22818
+ return {
22819
+ sheetName,
22820
+ startCol: columnToIndex(match[1].toUpperCase()),
22821
+ startRow: parseInt(match[2]) - 1,
22822
+ endCol: columnToIndex(match[3].toUpperCase()) + 1,
22823
+ // exclusive
22824
+ endRow: parseInt(match[4])
22825
+ // exclusive
22826
+ };
22827
+ }
22828
+ function getFmtKey(rowData, rowOffset, colOffset, useEffectiveFormat, fields) {
22829
+ const row = rowData[rowOffset];
22830
+ const cell = row?.values?.[colOffset];
22831
+ const rawFmt = useEffectiveFormat ? cell?.effectiveFormat : cell?.userEnteredFormat;
22832
+ if (!rawFmt) {
22833
+ return { key: "{}", fmt: {} };
22834
+ }
22835
+ const fmt = extractFormatFields(rawFmt, fields);
22836
+ return { key: JSON.stringify(fmt), fmt };
22837
+ }
22838
+ function deepDiffProperties(fmtA, fmtB) {
22839
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(fmtA), ...Object.keys(fmtB)]);
22840
+ const diffs = {};
22841
+ for (const key of allKeys) {
22842
+ const aVal = fmtA[key];
22843
+ const bVal = fmtB[key];
22844
+ if (JSON.stringify(aVal) !== JSON.stringify(bVal)) {
22845
+ diffs[key] = { a: aVal ?? null, b: bVal ?? null };
22846
+ }
22847
+ }
22848
+ return diffs;
22849
+ }
22850
+ async function handleCompareRanges(input) {
22851
+ try {
22852
+ let findGridData2 = function(parsed, rangeStr) {
22853
+ for (const s of allSheets) {
22854
+ if (s.properties?.title !== parsed.sheetName) {
22855
+ continue;
22856
+ }
22857
+ for (const gd of s.data ?? []) {
22858
+ const gdStartRow = gd.startRow ?? 0;
22859
+ const gdStartCol = gd.startColumn ?? 0;
22860
+ if (gdStartRow === parsed.startRow && gdStartCol === parsed.startCol) {
22861
+ return gd;
22862
+ }
22863
+ }
22864
+ }
22865
+ throw new Error(`Could not locate grid data for range "${rangeStr}". Check the sheet name.`);
22866
+ };
22867
+ var findGridData = findGridData2;
22868
+ const {
22869
+ spreadsheetId,
22870
+ rangeA,
22871
+ rangeB,
22872
+ fields: formatFields,
22873
+ useEffectiveFormat
22874
+ } = inputSchema11.parse(input);
22875
+ const parsedA = parseRangeParts(rangeA);
22876
+ const parsedB = parseRangeParts(rangeB);
22877
+ const numRowsA = parsedA.endRow - parsedA.startRow;
22878
+ const numColsA = parsedA.endCol - parsedA.startCol;
22879
+ const numRowsB = parsedB.endRow - parsedB.startRow;
22880
+ const numColsB = parsedB.endCol - parsedB.startCol;
22881
+ if (numColsA !== numColsB) {
22882
+ throw new Error(
22883
+ `Ranges must have the same number of columns. rangeA has ${numColsA} columns, rangeB has ${numColsB}.`
22884
+ );
22885
+ }
22886
+ if (numRowsA !== numRowsB) {
22887
+ throw new Error(
22888
+ `Ranges must have the same number of rows. rangeA has ${numRowsA} rows, rangeB has ${numRowsB}.`
22889
+ );
22890
+ }
22891
+ const formatField = useEffectiveFormat ? "effectiveFormat" : "userEnteredFormat";
22892
+ const sheets = await getAuthenticatedClient();
22893
+ const response = await sheets.spreadsheets.get({
22894
+ spreadsheetId,
22895
+ ranges: [rangeA, rangeB],
22896
+ includeGridData: true,
22897
+ fields: `sheets.properties.title,sheets.data.startRow,sheets.data.startColumn,sheets.data.rowData.values.${formatField}`
22898
+ });
22899
+ const allSheets = response.data.sheets ?? [];
22900
+ const gdA = findGridData2(parsedA, rangeA);
22901
+ const gdB = findGridData2(parsedB, rangeB);
22902
+ const rowDataA = gdA.rowData ?? [];
22903
+ const rowDataB = gdB.rowData ?? [];
22904
+ const diffs = [];
22905
+ let equalCells = 0;
22906
+ for (let r = 0; r < numRowsA; r++) {
22907
+ for (let c = 0; c < numColsA; c++) {
22908
+ const { fmt: fmtA } = getFmtKey(rowDataA, r, c, useEffectiveFormat, formatFields);
22909
+ const { fmt: fmtB } = getFmtKey(rowDataB, r, c, useEffectiveFormat, formatFields);
22910
+ const propDiffs = deepDiffProperties(fmtA, fmtB);
22911
+ if (Object.keys(propDiffs).length === 0) {
22912
+ equalCells++;
22913
+ continue;
22914
+ }
22915
+ diffs.push({
22916
+ cellA: `${colIndexToLetter(parsedA.startCol + c)}${parsedA.startRow + r + 1}`,
22917
+ cellB: `${colIndexToLetter(parsedB.startCol + c)}${parsedB.startRow + r + 1}`,
22918
+ diffs: propDiffs
22919
+ });
22920
+ }
22921
+ }
22922
+ const totalCells = numRowsA * numColsA;
22923
+ const identical = diffs.length === 0;
22924
+ return formatSuccessResponse(
22925
+ {
22926
+ rangeA,
22927
+ rangeB,
22928
+ dimensions: { rows: numRowsA, cols: numColsA, totalCells },
22929
+ formatType: formatField,
22930
+ fieldsCompared: formatFields ?? "all",
22931
+ identical,
22932
+ summary: identical ? `All ${totalCells} cells have identical formatting.` : `${diffs.length} of ${totalCells} cells differ. ${equalCells} cells are identical.`,
22933
+ equalCells,
22934
+ diffCount: diffs.length,
22935
+ diffs
22936
+ },
22937
+ identical ? `Ranges ${rangeA} and ${rangeB} have identical formatting` : `Found ${diffs.length} formatting difference(s) between ${rangeA} and ${rangeB}`
22938
+ );
22939
+ } catch (error2) {
22940
+ return handleError(error2);
22941
+ }
22942
+ }
22943
+
22346
22944
  // src/resources/index.ts
22347
22945
  function parseResourceUri(uri) {
22348
22946
  let path;
@@ -22780,6 +23378,8 @@ var toolHandlers = /* @__PURE__ */ new Map([
22780
23378
  ["sheets_insert_date", handleInsertDate],
22781
23379
  // Row operations
22782
23380
  ["sheets_insert_rows", handleInsertRows],
23381
+ ["sheets_delete_columns", handleDeleteColumns],
23382
+ ["sheets_delete_rows", handleDeleteRows],
22783
23383
  // READ / Snapshot tools
22784
23384
  ["sheets_get_merged_cells", handleGetMergedCells],
22785
23385
  ["sheets_get_sheet_dimensions", handleGetSheetDimensions],
@@ -22789,7 +23389,10 @@ var toolHandlers = /* @__PURE__ */ new Map([
22789
23389
  ["sheets_get_sheet_structure", handleGetSheetStructure],
22790
23390
  ["sheets_get_formatting_compact", handleGetFormattingCompact],
22791
23391
  ["sheets_get_data_validation", handleGetDataValidation],
22792
- ["sheets_get_basic_filter", handleGetBasicFilter]
23392
+ ["sheets_get_basic_filter", handleGetBasicFilter],
23393
+ // Border and comparison tools
23394
+ ["sheets_get_border_map", handleGetBorderMap],
23395
+ ["sheets_compare_ranges", handleCompareRanges]
22793
23396
  ]);
22794
23397
  var allTools = [
22795
23398
  checkAccessTool,
@@ -22819,6 +23422,8 @@ var allTools = [
22819
23422
  insertLinkTool,
22820
23423
  insertDateTool,
22821
23424
  insertRowsTool,
23425
+ deleteColumnsTool,
23426
+ deleteRowsTool,
22822
23427
  getMergedCellsTool,
22823
23428
  getSheetDimensionsTool,
22824
23429
  getSheetFormattingTool,
@@ -22827,7 +23432,9 @@ var allTools = [
22827
23432
  getSheetStructureTool,
22828
23433
  getFormattingCompactTool,
22829
23434
  getDataValidationTool,
22830
- getBasicFilterTool
23435
+ getBasicFilterTool,
23436
+ getBorderMapTool,
23437
+ compareRangesTool
22831
23438
  ];
22832
23439
  async function main() {
22833
23440
  try {
@@ -22839,7 +23446,7 @@ async function main() {
22839
23446
  const server = new Server(
22840
23447
  {
22841
23448
  name: "spreadsheet",
22842
- version: "1.7.1"
23449
+ version: "1.8.0"
22843
23450
  },
22844
23451
  {
22845
23452
  capabilities: {
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "mcp-gsheets",
3
- "version": "1.7.1",
3
+ "version": "1.8.1",
4
+ "mcpName": "io.github.freema/mcp-gsheets",
4
5
  "description": "Model Context Protocol (MCP) server for Google Sheets API integration",
5
6
  "author": "freema",
6
7
  "license": "MIT",