mcp-gsheets 1.7.0 → 1.8.0

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 +55 -3
  2. package/dist/index.js +780 -269
  3. package/package.json +2 -2
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,90 +19596,6 @@ async function handleCopyTo(input) {
19471
19596
  }
19472
19597
  }
19473
19598
 
19474
- // src/utils/range-helpers.ts
19475
- function columnToIndex(column) {
19476
- let index = 0;
19477
- for (let i = 0; i < column.length; i++) {
19478
- index = index * 26 + (column.charCodeAt(i) - "A".charCodeAt(0) + 1);
19479
- }
19480
- return index - 1;
19481
- }
19482
- function parseRange(range, sheetId) {
19483
- const rangePart = range.includes("!") ? range.split("!")[1] : range;
19484
- if (!rangePart) {
19485
- throw new Error(`Invalid range format: ${range}`);
19486
- }
19487
- const singleCellMatch = rangePart.match(/^([A-Z]+)(\d+)$/);
19488
- if (singleCellMatch?.[1] && singleCellMatch[2]) {
19489
- const col = columnToIndex(singleCellMatch[1]);
19490
- const row = parseInt(singleCellMatch[2]) - 1;
19491
- return {
19492
- sheetId: sheetId ?? null,
19493
- startRowIndex: row,
19494
- endRowIndex: row + 1,
19495
- startColumnIndex: col,
19496
- endColumnIndex: col + 1
19497
- };
19498
- }
19499
- const rangeMatch = rangePart.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
19500
- if (!rangeMatch?.[1] || !rangeMatch[2] || !rangeMatch[3] || !rangeMatch[4]) {
19501
- throw new Error(`Invalid range format: ${range}`);
19502
- }
19503
- return {
19504
- sheetId: sheetId ?? null,
19505
- startRowIndex: parseInt(rangeMatch[2]) - 1,
19506
- endRowIndex: parseInt(rangeMatch[4]),
19507
- startColumnIndex: columnToIndex(rangeMatch[1]),
19508
- endColumnIndex: columnToIndex(rangeMatch[3]) + 1
19509
- };
19510
- }
19511
- async function getSheetId(sheets, spreadsheetId, sheetName) {
19512
- const response = await sheets.spreadsheets.get({
19513
- spreadsheetId,
19514
- fields: "sheets.properties"
19515
- });
19516
- const sheetsData = response.data.sheets || [];
19517
- if (sheetName) {
19518
- const sheet = sheetsData.find((s) => s.properties?.title === sheetName);
19519
- if (!sheet?.properties?.sheetId) {
19520
- const availableSheets = sheetsData.map((s) => s.properties?.title).filter((title) => title).join(", ");
19521
- throw new Error(`Sheet "${sheetName}" not found. Available sheets: ${availableSheets}`);
19522
- }
19523
- return sheet.properties.sheetId;
19524
- }
19525
- if (sheetsData.length > 0) {
19526
- const firstSheet = sheetsData[0];
19527
- if (firstSheet?.properties?.sheetId !== void 0 && firstSheet.properties.sheetId !== null) {
19528
- return firstSheet.properties.sheetId;
19529
- }
19530
- }
19531
- throw new Error("No sheets found in spreadsheet");
19532
- }
19533
- function extractSheetName(range) {
19534
- if (range.includes("!")) {
19535
- const parts = range.split("!");
19536
- let sheetName = parts[0];
19537
- const rangePart = parts[1] || "";
19538
- if (sheetName) {
19539
- if (sheetName.startsWith('"') && sheetName.endsWith('"') || sheetName.startsWith("'") && sheetName.endsWith("'")) {
19540
- sheetName = sheetName.slice(1, -1);
19541
- }
19542
- return { sheetName, range: rangePart };
19543
- }
19544
- }
19545
- return { range };
19546
- }
19547
- function colIndexToLetter(index) {
19548
- let result = "";
19549
- let n = index + 1;
19550
- while (n > 0) {
19551
- const rem = (n - 1) % 26;
19552
- result = String.fromCharCode(65 + rem) + result;
19553
- n = Math.floor((n - 1) / 26);
19554
- }
19555
- return result;
19556
- }
19557
-
19558
19599
  // src/utils/json-parser.ts
19559
19600
  function parseJsonInput(input, propertyName) {
19560
19601
  if (input && typeof input === "string") {
@@ -20599,29 +20640,23 @@ async function handleCreateChart(input) {
20599
20640
  const axes = [];
20600
20641
  if (validatedInput.domainAxis?.title) {
20601
20642
  const axis = {
20602
- position: "BOTTOM_AXIS"
20643
+ position: "BOTTOM_AXIS",
20644
+ title: validatedInput.domainAxis.title
20603
20645
  };
20604
- if (validatedInput.domainAxis.title !== void 0) {
20605
- axis.title = validatedInput.domainAxis.title;
20606
- }
20607
20646
  axes.push(axis);
20608
20647
  }
20609
20648
  if (validatedInput.leftAxis?.title) {
20610
20649
  const axis = {
20611
- position: "LEFT_AXIS"
20650
+ position: "LEFT_AXIS",
20651
+ title: validatedInput.leftAxis.title
20612
20652
  };
20613
- if (validatedInput.leftAxis.title !== void 0) {
20614
- axis.title = validatedInput.leftAxis.title;
20615
- }
20616
20653
  axes.push(axis);
20617
20654
  }
20618
20655
  if (validatedInput.rightAxis?.title) {
20619
20656
  const axis = {
20620
- position: "RIGHT_AXIS"
20657
+ position: "RIGHT_AXIS",
20658
+ title: validatedInput.rightAxis.title
20621
20659
  };
20622
- if (validatedInput.rightAxis.title !== void 0) {
20623
- axis.title = validatedInput.rightAxis.title;
20624
- }
20625
20660
  axes.push(axis);
20626
20661
  }
20627
20662
  if (axes.length > 0) {
@@ -21379,14 +21414,10 @@ async function handleInsertRows(input) {
21379
21414
  }
21380
21415
  }
21381
21416
 
21382
- // src/tools/get-merged-cells.ts
21383
- var inputSchema = external_exports.object({
21384
- spreadsheetId: external_exports.string(),
21385
- sheetName: external_exports.string()
21386
- });
21387
- var getMergedCellsTool = {
21388
- name: "sheets_get_merged_cells",
21389
- description: "Get all merged cell ranges for a specific sheet. Returns each merge as A1 notation and GridRange coordinates.",
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",
21390
21421
  inputSchema: {
21391
21422
  type: "object",
21392
21423
  properties: {
@@ -21394,66 +21425,186 @@ var getMergedCellsTool = {
21394
21425
  type: "string",
21395
21426
  description: "The ID of the spreadsheet (found in the URL after /d/)"
21396
21427
  },
21397
- sheetName: {
21428
+ range: {
21398
21429
  type: "string",
21399
- description: "Name of the sheet (tab) to inspect"
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'
21400
21431
  }
21401
21432
  },
21402
- required: ["spreadsheetId", "sheetName"]
21433
+ required: ["spreadsheetId", "range"]
21403
21434
  }
21404
21435
  };
21405
- function colIndexToLetter2(index) {
21406
- let result = "";
21407
- let n = index + 1;
21408
- while (n > 0) {
21409
- const rem = (n - 1) % 26;
21410
- result = String.fromCharCode(65 + rem) + result;
21411
- n = Math.floor((n - 1) / 26);
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"');
21412
21440
  }
21413
- return result;
21414
- }
21415
- function gridRangeToA1(startRowIndex, endRowIndex, startColumnIndex, endColumnIndex) {
21416
- const startCol = colIndexToLetter2(startColumnIndex);
21417
- const endCol = colIndexToLetter2(endColumnIndex - 1);
21418
- const startRow = startRowIndex + 1;
21419
- const endRow = endRowIndex;
21420
- return `${startCol}${startRow}:${endCol}${endRow}`;
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 };
21421
21447
  }
21422
- async function handleGetMergedCells(input) {
21448
+ async function handleDeleteColumns(input) {
21423
21449
  try {
21424
- const { spreadsheetId, sheetName } = inputSchema.parse(input);
21450
+ const validatedInput = validateDeleteColumnsInput(input);
21425
21451
  const sheets = await getAuthenticatedClient();
21426
- const response = await sheets.spreadsheets.get({
21427
- spreadsheetId,
21428
- fields: "sheets.properties.title,sheets.properties.sheetId,sheets.merges"
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
+ }
21429
21471
  });
21430
- const sheetData = (response.data.sheets ?? []).find(
21431
- (s) => s.properties?.title === sheetName
21432
- );
21433
- if (!sheetData) {
21434
- const available = (response.data.sheets ?? []).map((s) => s.properties?.title).filter(Boolean).join(", ");
21435
- throw new Error(`Sheet "${sheetName}" not found. Available: ${available}`);
21436
- }
21437
- const merges = (sheetData.merges ?? []).map((m) => ({
21438
- a1Notation: gridRangeToA1(
21439
- m.startRowIndex ?? 0,
21440
- m.endRowIndex ?? 0,
21441
- m.startColumnIndex ?? 0,
21442
- m.endColumnIndex ?? 0
21443
- ),
21444
- startRowIndex: m.startRowIndex,
21445
- endRowIndex: m.endRowIndex,
21446
- startColumnIndex: m.startColumnIndex,
21447
- endColumnIndex: m.endColumnIndex
21448
- }));
21449
- return formatSuccessResponse(
21472
+ return formatToolResponse(
21473
+ `Successfully deleted ${endIndex - startIndex} columns in range ${validatedInput.range}`,
21450
21474
  {
21451
- sheetName,
21452
- sheetId: sheetData.properties?.sheetId,
21453
- mergeCount: merges.length,
21454
- merges
21455
- },
21456
- `Found ${merges.length} merged range(s) in sheet "${sheetName}"`
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
+
21561
+ // src/tools/get-merged-cells.ts
21562
+ var inputSchema = external_exports.object({
21563
+ spreadsheetId: external_exports.string(),
21564
+ sheetName: external_exports.string()
21565
+ });
21566
+ var getMergedCellsTool = {
21567
+ name: "sheets_get_merged_cells",
21568
+ description: "Get all merged cell ranges for a specific sheet. Returns each merge as A1 notation and GridRange coordinates.",
21569
+ inputSchema: {
21570
+ type: "object",
21571
+ properties: {
21572
+ spreadsheetId: {
21573
+ type: "string",
21574
+ description: "The ID of the spreadsheet (found in the URL after /d/)"
21575
+ },
21576
+ sheetName: {
21577
+ type: "string",
21578
+ description: "Name of the sheet (tab) to inspect"
21579
+ }
21580
+ },
21581
+ required: ["spreadsheetId", "sheetName"]
21582
+ }
21583
+ };
21584
+ async function handleGetMergedCells(input) {
21585
+ try {
21586
+ const { spreadsheetId, sheetName } = inputSchema.parse(input);
21587
+ const sheets = await getAuthenticatedClient();
21588
+ const response = await sheets.spreadsheets.get({
21589
+ spreadsheetId,
21590
+ fields: "sheets.properties.title,sheets.properties.sheetId,sheets.merges"
21591
+ });
21592
+ const sheetData = findSheetOrThrow(response.data.sheets ?? [], sheetName);
21593
+ const merges = (sheetData.merges ?? []).map((m) => ({
21594
+ a1Notation: gridRangeToA1(m),
21595
+ startRowIndex: m.startRowIndex,
21596
+ endRowIndex: m.endRowIndex,
21597
+ startColumnIndex: m.startColumnIndex,
21598
+ endColumnIndex: m.endColumnIndex
21599
+ }));
21600
+ return formatSuccessResponse(
21601
+ {
21602
+ sheetName,
21603
+ sheetId: sheetData.properties?.sheetId,
21604
+ mergeCount: merges.length,
21605
+ merges
21606
+ },
21607
+ `Found ${merges.length} merged range(s) in sheet "${sheetName}"`
21457
21608
  );
21458
21609
  } catch (error2) {
21459
21610
  return handleError(error2);
@@ -21491,13 +21642,7 @@ async function handleGetSheetDimensions(input) {
21491
21642
  spreadsheetId,
21492
21643
  fields: "sheets.properties,sheets.data.columnMetadata,sheets.data.rowMetadata"
21493
21644
  });
21494
- const sheetData = (response.data.sheets ?? []).find(
21495
- (s) => s.properties?.title === sheetName
21496
- );
21497
- if (!sheetData) {
21498
- const available = (response.data.sheets ?? []).map((s) => s.properties?.title).filter(Boolean).join(", ");
21499
- throw new Error(`Sheet "${sheetName}" not found. Available: ${available}`);
21500
- }
21645
+ const sheetData = findSheetOrThrow(response.data.sheets ?? [], sheetName);
21501
21646
  const gridProps = sheetData.properties?.gridProperties ?? {};
21502
21647
  const gridData = sheetData.data?.[0] ?? {};
21503
21648
  const columns = (gridData.columnMetadata ?? []).map(
@@ -21619,14 +21764,91 @@ async function handleGetSheetFormatting(input) {
21619
21764
  }
21620
21765
  }
21621
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
+
21622
21843
  // src/tools/get-conditional-formatting-data.ts
21623
21844
  var inputSchema4 = external_exports.object({
21624
21845
  spreadsheetId: external_exports.string(),
21625
- sheetName: external_exports.string()
21846
+ sheetName: external_exports.string(),
21847
+ normalizeFormulas: external_exports.boolean().optional().default(true)
21626
21848
  });
21627
21849
  var getConditionalFormattingDataTool = {
21628
21850
  name: "sheets_get_conditional_formatting",
21629
- 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.',
21630
21852
  inputSchema: {
21631
21853
  type: "object",
21632
21854
  properties: {
@@ -21637,6 +21859,10 @@ var getConditionalFormattingDataTool = {
21637
21859
  sheetName: {
21638
21860
  type: "string",
21639
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.'
21640
21866
  }
21641
21867
  },
21642
21868
  required: ["spreadsheetId", "sheetName"]
@@ -21644,24 +21870,21 @@ var getConditionalFormattingDataTool = {
21644
21870
  };
21645
21871
  async function handleGetConditionalFormattingData(input) {
21646
21872
  try {
21647
- const { spreadsheetId, sheetName } = inputSchema4.parse(input);
21873
+ const { spreadsheetId, sheetName, normalizeFormulas } = inputSchema4.parse(input);
21648
21874
  const sheets = await getAuthenticatedClient();
21649
21875
  const response = await sheets.spreadsheets.get({
21650
21876
  spreadsheetId,
21651
21877
  fields: "sheets.properties.title,sheets.properties.sheetId,sheets.conditionalFormats,sheets.bandedRanges"
21652
21878
  });
21653
- const sheetData = (response.data.sheets ?? []).find(
21654
- (s) => s.properties?.title === sheetName
21655
- );
21656
- if (!sheetData) {
21657
- const available = (response.data.sheets ?? []).map((s) => s.properties?.title).filter(Boolean).join(", ");
21658
- throw new Error(`Sheet "${sheetName}" not found. Available: ${available}`);
21659
- }
21879
+ const sheetData = findSheetOrThrow(response.data.sheets ?? [], sheetName);
21880
+ const rawFormats = sheetData.conditionalFormats ?? [];
21881
+ const conditionalFormats = normalizeFormulas ? rawFormats.map(normalizeConditionalFormatFormulas) : rawFormats;
21660
21882
  return formatSuccessResponse(
21661
21883
  {
21662
21884
  sheetName,
21663
21885
  sheetId: sheetData.properties?.sheetId,
21664
- conditionalFormats: sheetData.conditionalFormats ?? [],
21886
+ formulasNormalized: normalizeFormulas,
21887
+ conditionalFormats,
21665
21888
  bandedRanges: sheetData.bandedRanges ?? []
21666
21889
  },
21667
21890
  `Conditional formatting data for sheet "${sheetName}"`
@@ -21812,11 +22035,12 @@ var inputSchema5 = external_exports.object({
21812
22035
  includeFormattingRange: external_exports.string().optional(),
21813
22036
  useEffectiveFormat: external_exports.boolean().optional().default(false),
21814
22037
  fields: external_exports.array(external_exports.string()).optional(),
21815
- compactMode: external_exports.boolean().optional().default(false)
22038
+ compactMode: external_exports.boolean().optional().default(false),
22039
+ includeConditionalFormatting: external_exports.boolean().optional().default(true)
21816
22040
  });
21817
22041
  var getFullSheetSnapshotTool = {
21818
22042
  name: "sheets_get_full_sheet_snapshot",
21819
- 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.",
21820
22044
  inputSchema: {
21821
22045
  type: "object",
21822
22046
  properties: {
@@ -21843,15 +22067,16 @@ var getFullSheetSnapshotTool = {
21843
22067
  },
21844
22068
  compactMode: {
21845
22069
  type: "boolean",
21846
- 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."
21847
22075
  }
21848
22076
  },
21849
22077
  required: ["spreadsheetId", "sheetName"]
21850
22078
  }
21851
22079
  };
21852
- function gridRangeToA12(startRowIndex, endRowIndex, startColumnIndex, endColumnIndex) {
21853
- return `${colIndexToLetter(startColumnIndex)}${startRowIndex + 1}:${colIndexToLetter(endColumnIndex - 1)}${endRowIndex}`;
21854
- }
21855
22080
  async function handleGetFullSheetSnapshot(input) {
21856
22081
  try {
21857
22082
  const {
@@ -21860,11 +22085,13 @@ async function handleGetFullSheetSnapshot(input) {
21860
22085
  includeFormattingRange,
21861
22086
  useEffectiveFormat,
21862
22087
  fields: formatFields,
21863
- compactMode
22088
+ compactMode,
22089
+ includeConditionalFormatting
21864
22090
  } = inputSchema5.parse(input);
21865
22091
  const sheets = await getAuthenticatedClient();
21866
22092
  const baseFields = "sheets.properties,sheets.merges,sheets.data.columnMetadata,sheets.data.rowMetadata,sheets.conditionalFormats,sheets.bandedRanges";
21867
22093
  const formatField = useEffectiveFormat ? "effectiveFormat" : "userEnteredFormat";
22094
+ const compact = compactMode === true;
21868
22095
  let cellDataFields;
21869
22096
  if (formatFields && formatFields.length > 0) {
21870
22097
  cellDataFields = formatFields.map((f) => `sheets.data.rowData.values.${formatField}.${f}`).join(",");
@@ -21879,23 +22106,12 @@ async function handleGetFullSheetSnapshot(input) {
21879
22106
  includeGridData: !!includeFormattingRange,
21880
22107
  fields: baseFields + cellFormattingFields
21881
22108
  });
21882
- const sheetData = (response.data.sheets ?? []).find(
21883
- (s) => s.properties?.title === sheetName
21884
- );
21885
- if (!sheetData) {
21886
- const available = (response.data.sheets ?? []).map((s) => s.properties?.title).filter(Boolean).join(", ");
21887
- throw new Error(`Sheet "${sheetName}" not found. Available: ${available}`);
21888
- }
22109
+ const sheetData = findSheetOrThrow(response.data.sheets ?? [], sheetName);
21889
22110
  const props = sheetData.properties;
21890
22111
  const gridProps = props.gridProperties ?? {};
21891
22112
  const gridData = sheetData.data?.[0] ?? {};
21892
22113
  const merges = (sheetData.merges ?? []).map((m) => ({
21893
- a1Notation: gridRangeToA12(
21894
- m.startRowIndex ?? 0,
21895
- m.endRowIndex ?? 0,
21896
- m.startColumnIndex ?? 0,
21897
- m.endColumnIndex ?? 0
21898
- ),
22114
+ a1Notation: gridRangeToA1(m),
21899
22115
  startRowIndex: m.startRowIndex,
21900
22116
  endRowIndex: m.endRowIndex,
21901
22117
  startColumnIndex: m.startColumnIndex,
@@ -21919,7 +22135,7 @@ async function handleGetFullSheetSnapshot(input) {
21919
22135
  if (includeFormattingRange && gridData.rowData) {
21920
22136
  const startRow = gridData.startRow ?? 0;
21921
22137
  const startColumn = gridData.startColumn ?? 0;
21922
- if (compactMode) {
22138
+ if (compact) {
21923
22139
  cellFormatting = compactifyCellFormatting(
21924
22140
  gridData.rowData,
21925
22141
  startRow,
@@ -21943,6 +22159,11 @@ async function handleGetFullSheetSnapshot(input) {
21943
22159
  );
21944
22160
  }
21945
22161
  }
22162
+ const rawConditionalFormats = sheetData.conditionalFormats ?? [];
22163
+ const cfSection = includeConditionalFormatting ? {
22164
+ conditionalFormats: rawConditionalFormats.map(normalizeConditionalFormatFormulas),
22165
+ bandedRanges: sheetData.bandedRanges ?? []
22166
+ } : {};
21946
22167
  const snapshot = {
21947
22168
  sheetName,
21948
22169
  sheetId: props.sheetId,
@@ -21956,13 +22177,12 @@ async function handleGetFullSheetSnapshot(input) {
21956
22177
  merges,
21957
22178
  columns,
21958
22179
  rows,
21959
- conditionalFormats: sheetData.conditionalFormats ?? [],
21960
- bandedRanges: sheetData.bandedRanges ?? [],
22180
+ ...cfSection,
21961
22181
  ...cellFormatting !== null ? {
21962
22182
  cellFormatting: {
21963
22183
  range: `${sheetName}!${includeFormattingRange}`,
21964
22184
  formatType: formatField,
21965
- compact: compactMode,
22185
+ compact,
21966
22186
  data: cellFormatting
21967
22187
  }
21968
22188
  } : {}
@@ -22008,13 +22228,7 @@ async function handleGetSheetStructure(input) {
22008
22228
  fields: "sheets.properties,sheets.merges,sheets.data.columnMetadata,sheets.data.rowMetadata"
22009
22229
  });
22010
22230
  const allSheets = response.data.sheets ?? [];
22011
- const sheetData = allSheets.find(
22012
- (s) => s.properties?.title === sheetName
22013
- );
22014
- if (!sheetData) {
22015
- const available = allSheets.map((s) => s.properties?.title).filter(Boolean).join(", ");
22016
- throw new Error(`Sheet "${sheetName}" not found. Available: ${available}`);
22017
- }
22231
+ const sheetData = findSheetOrThrow(allSheets, sheetName);
22018
22232
  const props = sheetData.properties;
22019
22233
  const gridProps = props.gridProperties ?? {};
22020
22234
  const gridData = sheetData.data?.[0] ?? {};
@@ -22029,13 +22243,9 @@ async function handleGetSheetStructure(input) {
22029
22243
  (row) => row.pixelSize ?? null
22030
22244
  );
22031
22245
  const hiddenRows = (gridData.rowMetadata ?? []).map((row, i) => row.hiddenByUser ? i : -1).filter((i) => i >= 0);
22032
- const merges = (sheetData.merges ?? []).map((m) => {
22033
- const sc = m.startColumnIndex ?? 0;
22034
- const ec = (m.endColumnIndex ?? 1) - 1;
22035
- const sr = (m.startRowIndex ?? 0) + 1;
22036
- const er = m.endRowIndex ?? 1;
22037
- return `${colIndexToLetter(sc)}${sr}:${colIndexToLetter(ec)}${er}`;
22038
- });
22246
+ const merges = (sheetData.merges ?? []).map(
22247
+ (m) => gridRangeToA1(m)
22248
+ );
22039
22249
  return formatSuccessResponse(
22040
22250
  {
22041
22251
  sheetName,
@@ -22122,13 +22332,7 @@ async function handleGetFormattingCompact(input) {
22122
22332
  fields: `sheets.properties.title,${cellDataFields},sheets.data.startRow,sheets.data.startColumn`
22123
22333
  });
22124
22334
  const allSheets = response.data.sheets ?? [];
22125
- const sheetData = allSheets.find(
22126
- (s) => s.properties?.title === sheetName
22127
- );
22128
- if (!sheetData) {
22129
- const available = allSheets.map((s) => s.properties?.title).filter(Boolean).join(", ");
22130
- throw new Error(`Sheet "${sheetName}" not found. Available: ${available}`);
22131
- }
22335
+ const sheetData = findSheetOrThrow(allSheets, sheetName);
22132
22336
  const gridData = sheetData.data?.[0];
22133
22337
  if (!gridData?.rowData) {
22134
22338
  return formatSuccessResponse(
@@ -22188,15 +22392,6 @@ var getDataValidationTool = {
22188
22392
  required: ["spreadsheetId", "sheetName"]
22189
22393
  }
22190
22394
  };
22191
- function colToLetter(col) {
22192
- let letter = "";
22193
- let c = col;
22194
- while (c >= 0) {
22195
- letter = String.fromCharCode(c % 26 + 65) + letter;
22196
- c = Math.floor(c / 26) - 1;
22197
- }
22198
- return letter;
22199
- }
22200
22395
  function validationKey(dv) {
22201
22396
  return JSON.stringify({
22202
22397
  type: dv.condition?.type ?? null,
@@ -22272,8 +22467,8 @@ function compactifyValidation(rowData, startRow, startCol) {
22272
22467
  }
22273
22468
  }
22274
22469
  }
22275
- const topLeft = `${colToLetter(c + minCol)}${r + minRow + 1}`;
22276
- const bottomRight = `${colToLetter(endC + minCol)}${endR + minRow + 1}`;
22470
+ const topLeft = `${colIndexToLetter(c + minCol)}${r + minRow + 1}`;
22471
+ const bottomRight = `${colIndexToLetter(endC + minCol)}${endR + minRow + 1}`;
22277
22472
  ranges.push(topLeft === bottomRight ? topLeft : `${topLeft}:${bottomRight}`);
22278
22473
  }
22279
22474
  }
@@ -22298,13 +22493,7 @@ async function handleGetDataValidation(input) {
22298
22493
  includeGridData: true,
22299
22494
  fields: "sheets.data.rowData.values.dataValidation,sheets.data.startRow,sheets.data.startColumn,sheets.properties.title,sheets.properties.sheetId"
22300
22495
  });
22301
- const sheetData = (response.data.sheets ?? []).find(
22302
- (s) => s.properties?.title === sheetName
22303
- );
22304
- if (!sheetData) {
22305
- const available = (response.data.sheets ?? []).map((s) => s.properties?.title).filter(Boolean).join(", ");
22306
- throw new Error(`Sheet "${sheetName}" not found. Available: ${available}`);
22307
- }
22496
+ const sheetData = findSheetOrThrow(response.data.sheets ?? [], sheetName);
22308
22497
  const gridData = sheetData.data?.[0];
22309
22498
  if (!gridData?.rowData) {
22310
22499
  return formatSuccessResponse(
@@ -22314,11 +22503,7 @@ async function handleGetDataValidation(input) {
22314
22503
  }
22315
22504
  const startRow = gridData.startRow ?? 0;
22316
22505
  const startCol = gridData.startColumn ?? 0;
22317
- const validationRules = compactifyValidation(
22318
- gridData.rowData,
22319
- startRow,
22320
- startCol
22321
- );
22506
+ const validationRules = compactifyValidation(gridData.rowData, startRow, startCol);
22322
22507
  return formatSuccessResponse(
22323
22508
  { sheetName, validationRules },
22324
22509
  `Data validation for sheet "${sheetName}" \u2014 ${validationRules.length} unique rule(s)`
@@ -22351,22 +22536,6 @@ var getBasicFilterTool = {
22351
22536
  required: ["spreadsheetId", "sheetName"]
22352
22537
  }
22353
22538
  };
22354
- function colToLetter2(col) {
22355
- let letter = "";
22356
- let c = col;
22357
- while (c >= 0) {
22358
- letter = String.fromCharCode(c % 26 + 65) + letter;
22359
- c = Math.floor(c / 26) - 1;
22360
- }
22361
- return letter;
22362
- }
22363
- function gridRangeToA13(range) {
22364
- const startCol = range.startColumnIndex ?? 0;
22365
- const startRow = (range.startRowIndex ?? 0) + 1;
22366
- const endCol = (range.endColumnIndex ?? startCol + 1) - 1;
22367
- const endRow = range.endRowIndex ?? startRow;
22368
- return `${colToLetter2(startCol)}${startRow}:${colToLetter2(endCol)}${endRow}`;
22369
- }
22370
22539
  async function handleGetBasicFilter(input) {
22371
22540
  try {
22372
22541
  const { spreadsheetId, sheetName } = inputSchema9.parse(input);
@@ -22375,13 +22544,7 @@ async function handleGetBasicFilter(input) {
22375
22544
  spreadsheetId,
22376
22545
  fields: "sheets.properties.title,sheets.properties.sheetId,sheets.basicFilter"
22377
22546
  });
22378
- const sheetData = (response.data.sheets ?? []).find(
22379
- (s) => s.properties?.title === sheetName
22380
- );
22381
- if (!sheetData) {
22382
- const available = (response.data.sheets ?? []).map((s) => s.properties?.title).filter(Boolean).join(", ");
22383
- throw new Error(`Sheet "${sheetName}" not found. Available: ${available}`);
22384
- }
22547
+ const sheetData = findSheetOrThrow(response.data.sheets ?? [], sheetName);
22385
22548
  const bf = sheetData.basicFilter;
22386
22549
  if (!bf) {
22387
22550
  return formatSuccessResponse(
@@ -22396,7 +22559,7 @@ async function handleGetBasicFilter(input) {
22396
22559
  const fc = spec.filterCriteria;
22397
22560
  filterCriteria.push({
22398
22561
  columnIndex: colIdx,
22399
- columnLetter: colToLetter2(colIdx),
22562
+ columnLetter: colIndexToLetter(colIdx),
22400
22563
  hiddenValues: fc?.hiddenValues ?? [],
22401
22564
  condition: fc?.condition ?? null,
22402
22565
  visibleBackgroundColor: fc?.visibleBackgroundColor ?? null,
@@ -22409,7 +22572,7 @@ async function handleGetBasicFilter(input) {
22409
22572
  const criteria = fc;
22410
22573
  filterCriteria.push({
22411
22574
  columnIndex: idx,
22412
- columnLetter: colToLetter2(idx),
22575
+ columnLetter: colIndexToLetter(idx),
22413
22576
  hiddenValues: criteria.hiddenValues ?? [],
22414
22577
  condition: criteria.condition ?? null,
22415
22578
  visibleBackgroundColor: criteria.visibleBackgroundColor ?? null,
@@ -22417,7 +22580,7 @@ async function handleGetBasicFilter(input) {
22417
22580
  });
22418
22581
  }
22419
22582
  }
22420
- const rangeA1 = bf.range ? gridRangeToA13(bf.range) : null;
22583
+ const rangeA1 = bf.range ? gridRangeToA1(bf.range) : null;
22421
22584
  return formatSuccessResponse(
22422
22585
  {
22423
22586
  sheetName,
@@ -22426,7 +22589,7 @@ async function handleGetBasicFilter(input) {
22426
22589
  range: rangeA1,
22427
22590
  sortSpecs: (bf.sortSpecs ?? []).map((s) => ({
22428
22591
  columnIndex: s.dimensionIndex,
22429
- columnLetter: s.dimensionIndex !== null && s.dimensionIndex !== void 0 ? colToLetter2(s.dimensionIndex) : null,
22592
+ columnLetter: s.dimensionIndex !== null && s.dimensionIndex !== void 0 ? colIndexToLetter(s.dimensionIndex) : null,
22430
22593
  sortOrder: s.sortOrder ?? null
22431
22594
  })),
22432
22595
  filterCriteria
@@ -22439,6 +22602,345 @@ async function handleGetBasicFilter(input) {
22439
22602
  }
22440
22603
  }
22441
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
+
22442
22944
  // src/resources/index.ts
22443
22945
  function parseResourceUri(uri) {
22444
22946
  let path;
@@ -22876,6 +23378,8 @@ var toolHandlers = /* @__PURE__ */ new Map([
22876
23378
  ["sheets_insert_date", handleInsertDate],
22877
23379
  // Row operations
22878
23380
  ["sheets_insert_rows", handleInsertRows],
23381
+ ["sheets_delete_columns", handleDeleteColumns],
23382
+ ["sheets_delete_rows", handleDeleteRows],
22879
23383
  // READ / Snapshot tools
22880
23384
  ["sheets_get_merged_cells", handleGetMergedCells],
22881
23385
  ["sheets_get_sheet_dimensions", handleGetSheetDimensions],
@@ -22885,7 +23389,10 @@ var toolHandlers = /* @__PURE__ */ new Map([
22885
23389
  ["sheets_get_sheet_structure", handleGetSheetStructure],
22886
23390
  ["sheets_get_formatting_compact", handleGetFormattingCompact],
22887
23391
  ["sheets_get_data_validation", handleGetDataValidation],
22888
- ["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]
22889
23396
  ]);
22890
23397
  var allTools = [
22891
23398
  checkAccessTool,
@@ -22915,6 +23422,8 @@ var allTools = [
22915
23422
  insertLinkTool,
22916
23423
  insertDateTool,
22917
23424
  insertRowsTool,
23425
+ deleteColumnsTool,
23426
+ deleteRowsTool,
22918
23427
  getMergedCellsTool,
22919
23428
  getSheetDimensionsTool,
22920
23429
  getSheetFormattingTool,
@@ -22923,7 +23432,9 @@ var allTools = [
22923
23432
  getSheetStructureTool,
22924
23433
  getFormattingCompactTool,
22925
23434
  getDataValidationTool,
22926
- getBasicFilterTool
23435
+ getBasicFilterTool,
23436
+ getBorderMapTool,
23437
+ compareRangesTool
22927
23438
  ];
22928
23439
  async function main() {
22929
23440
  try {
@@ -22935,7 +23446,7 @@ async function main() {
22935
23446
  const server = new Server(
22936
23447
  {
22937
23448
  name: "spreadsheet",
22938
- version: "1.6.0"
23449
+ version: "1.8.0"
22939
23450
  },
22940
23451
  {
22941
23452
  capabilities: {