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.
- package/README.md +53 -1
- package/dist/index.js +733 -126
- 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:
|
|
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
|
-
|
|
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,
|
|
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)
|
|
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 (
|
|
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
|
-
|
|
21921
|
-
bandedRanges: sheetData.bandedRanges ?? [],
|
|
22180
|
+
...cfSection,
|
|
21922
22181
|
...cellFormatting !== null ? {
|
|
21923
22182
|
cellFormatting: {
|
|
21924
22183
|
range: `${sheetName}!${includeFormattingRange}`,
|
|
21925
22184
|
formatType: formatField,
|
|
21926
|
-
compact
|
|
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.
|
|
23449
|
+
version: "1.8.0"
|
|
22843
23450
|
},
|
|
22844
23451
|
{
|
|
22845
23452
|
capabilities: {
|
package/package.json
CHANGED