mcp-gsheets 1.8.1 → 1.10.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 +65 -0
  2. package/dist/index.js +641 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -312,6 +312,62 @@ cp .env.example .env
312
312
  npm run dev # Watch mode with auto-reload
313
313
  ```
314
314
 
315
+ ## 🎚️ Reducing context cost with toolsets
316
+
317
+ All 44 tools together cost about **9,900 tokens of context in every session**,
318
+ before the model does anything. Most workflows need a fraction of that.
319
+ `GSHEETS_TOOLSETS` limits which tools the server exposes:
320
+
321
+ ```json
322
+ {
323
+ "mcpServers": {
324
+ "gsheets": {
325
+ "command": "npx",
326
+ "args": ["mcp-gsheets"],
327
+ "env": {
328
+ "GOOGLE_PROJECT_ID": "your-project-id",
329
+ "GOOGLE_APPLICATION_CREDENTIALS": "/path/to/key.json",
330
+ "GSHEETS_TOOLSETS": "core,charts"
331
+ }
332
+ }
333
+ }
334
+ }
335
+ ```
336
+
337
+ | Toolset | Tools | What it covers |
338
+ |---|---|---|
339
+ | `core` | 11 | Read/write values, metadata, sheet structure, create spreadsheet |
340
+ | `sheets` | 9 | Sheet lifecycle, rows and columns |
341
+ | `formatting` | 15 | Colours, borders, merges, conditional rules, links, dates |
342
+ | `charts` | 3 | Create, update, delete charts |
343
+ | `tables` | 4 | Native tables |
344
+ | `analysis` | 2 | Full-sheet snapshot, range comparison |
345
+
346
+ Measured `tools/list` cost:
347
+
348
+ | Configuration | Tools | ≈ Tokens |
349
+ |---|---|---|
350
+ | _unset_ (default, all toolsets) | 44 | 9,875 |
351
+ | `GSHEETS_TOOLSETS=core` | 11 | 1,986 |
352
+ | `GSHEETS_TOOLSETS=core,sheets` | 20 | 3,548 |
353
+ | `GSHEETS_READ_ONLY=true` | 16 | 3,599 |
354
+ | `GSHEETS_TOOLSETS=core` + read-only | 6 | 912 |
355
+
356
+ Notes:
357
+
358
+ - **The default is unchanged** — leave `GSHEETS_TOOLSETS` unset and you get
359
+ every tool, exactly as before.
360
+ - **`core` is always included.** `GSHEETS_TOOLSETS=charts` means "charts as
361
+ well as core", not "charts only" — without core the server cannot read a cell.
362
+ - **A typo is a startup error**, not a silently smaller tool list.
363
+ - `GSHEETS_READ_ONLY=true` drops every writing tool and can be combined with
364
+ `GSHEETS_TOOLSETS`. It is enforced when a tool is called, not just when the
365
+ list is built, so a client cannot write by naming a hidden tool.
366
+
367
+ All tools also carry MCP annotations (`readOnlyHint`, `destructiveHint`,
368
+ `idempotentHint`), so clients can skip confirmation prompts on reads and warn
369
+ before destructive operations.
370
+
315
371
  ## 📋 Available Tools
316
372
 
317
373
  ### Reading Data
@@ -360,6 +416,15 @@ npm run dev # Watch mode with auto-reload
360
416
  | `sheets_unmerge_cells` | Unmerge previously merged cells in a range | `spreadsheetId`, `range` |
361
417
  | `sheets_add_conditional_formatting` | Add a conditional formatting rule (gradient or boolean) to a range | `spreadsheetId`, `range`, `rule` |
362
418
 
419
+ ### Native Tables
420
+
421
+ | Tool | Description | Key Parameters |
422
+ |------|-------------|----------------|
423
+ | `sheets_add_table` | Create a native Google Sheets table with typed columns and optional dropdown values | `spreadsheetId`, `sheetName`, `range`, `name`, `columns` |
424
+ | `sheets_update_table` | Update an existing native Google Sheets table by table ID using an explicit field mask | `spreadsheetId`, `tableId`, `fields`, `name`, `range`, `columns` |
425
+ | `sheets_delete_table` | Delete a native Google Sheets table by table ID | `spreadsheetId`, `tableId` |
426
+ | `sheets_get_tables` | Read native tables for a spreadsheet or a specific sheet | `spreadsheetId`, `sheetName` |
427
+
363
428
  ### Charts
364
429
 
365
430
  | Tool | Description | Key Parameters |
package/dist/index.js CHANGED
@@ -18036,6 +18036,141 @@ function validateAuth() {
18036
18036
  }
18037
18037
  }
18038
18038
 
18039
+ // src/config/toolsets.ts
18040
+ var TOOLSET_NAMES = [
18041
+ "core",
18042
+ "sheets",
18043
+ "formatting",
18044
+ "charts",
18045
+ "tables",
18046
+ "analysis"
18047
+ ];
18048
+ var TOOLSETS = {
18049
+ /** Reading and writing cells, plus the metadata needed to do it safely. */
18050
+ core: [
18051
+ "sheets_check_access",
18052
+ "sheets_get_metadata",
18053
+ "sheets_get_sheet_structure",
18054
+ "sheets_get_sheet_dimensions",
18055
+ "sheets_get_values",
18056
+ "sheets_batch_get_values",
18057
+ "sheets_update_values",
18058
+ "sheets_batch_update_values",
18059
+ "sheets_append_values",
18060
+ "sheets_clear_values",
18061
+ "sheets_create_spreadsheet"
18062
+ ],
18063
+ /** Sheet lifecycle and row/column structure. */
18064
+ sheets: [
18065
+ "sheets_insert_sheet",
18066
+ "sheets_delete_sheet",
18067
+ "sheets_duplicate_sheet",
18068
+ "sheets_copy_to",
18069
+ "sheets_update_sheet_properties",
18070
+ "sheets_batch_delete_sheets",
18071
+ "sheets_insert_rows",
18072
+ "sheets_delete_rows",
18073
+ "sheets_delete_columns"
18074
+ ],
18075
+ /** Cell appearance: colours, borders, merges, conditional rules, links. */
18076
+ formatting: [
18077
+ "sheets_format_cells",
18078
+ "sheets_batch_format_cells",
18079
+ "sheets_update_borders",
18080
+ "sheets_get_border_map",
18081
+ "sheets_merge_cells",
18082
+ "sheets_unmerge_cells",
18083
+ "sheets_get_merged_cells",
18084
+ "sheets_get_sheet_formatting",
18085
+ "sheets_get_formatting_compact",
18086
+ "sheets_add_conditional_formatting",
18087
+ "sheets_get_conditional_formatting",
18088
+ "sheets_get_data_validation",
18089
+ "sheets_get_basic_filter",
18090
+ "sheets_insert_link",
18091
+ "sheets_insert_date"
18092
+ ],
18093
+ charts: ["sheets_create_chart", "sheets_update_chart", "sheets_delete_chart"],
18094
+ tables: ["sheets_add_table", "sheets_update_table", "sheets_delete_table", "sheets_get_tables"],
18095
+ /** Whole-sheet reads and diffing — powerful, but the heaviest schemas. */
18096
+ analysis: ["sheets_get_full_sheet_snapshot", "sheets_compare_ranges"]
18097
+ };
18098
+ var READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
18099
+ "sheets_check_access",
18100
+ "sheets_get_metadata",
18101
+ "sheets_get_sheet_structure",
18102
+ "sheets_get_sheet_dimensions",
18103
+ "sheets_get_values",
18104
+ "sheets_batch_get_values",
18105
+ "sheets_get_border_map",
18106
+ "sheets_get_merged_cells",
18107
+ "sheets_get_sheet_formatting",
18108
+ "sheets_get_formatting_compact",
18109
+ "sheets_get_conditional_formatting",
18110
+ "sheets_get_data_validation",
18111
+ "sheets_get_basic_filter",
18112
+ "sheets_get_tables",
18113
+ "sheets_get_full_sheet_snapshot",
18114
+ "sheets_compare_ranges"
18115
+ ]);
18116
+ var DESTRUCTIVE_TOOLS = /* @__PURE__ */ new Set([
18117
+ "sheets_clear_values",
18118
+ "sheets_delete_sheet",
18119
+ "sheets_batch_delete_sheets",
18120
+ "sheets_delete_rows",
18121
+ "sheets_delete_columns",
18122
+ "sheets_delete_chart",
18123
+ "sheets_delete_table",
18124
+ "sheets_unmerge_cells"
18125
+ ]);
18126
+ var ToolsetConfigError = class extends Error {
18127
+ };
18128
+ function parseList(raw) {
18129
+ return raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
18130
+ }
18131
+ function resolveToolsets(env = process.env) {
18132
+ const raw = env.GSHEETS_TOOLSETS?.trim();
18133
+ const readOnly = env.GSHEETS_READ_ONLY?.trim().toLowerCase() === "true";
18134
+ let enabled;
18135
+ if (!raw || raw.toLowerCase() === "all") {
18136
+ enabled = [...TOOLSET_NAMES];
18137
+ } else {
18138
+ const requested = parseList(raw);
18139
+ const unknown2 = requested.filter((n) => !TOOLSET_NAMES.includes(n));
18140
+ if (unknown2.length > 0) {
18141
+ throw new ToolsetConfigError(
18142
+ `Unknown toolset${unknown2.length > 1 ? "s" : ""}: ${unknown2.join(", ")}. Valid toolsets: ${TOOLSET_NAMES.join(", ")}, all.`
18143
+ );
18144
+ }
18145
+ const set = new Set(requested);
18146
+ set.add("core");
18147
+ enabled = TOOLSET_NAMES.filter((n) => set.has(n));
18148
+ }
18149
+ const allowed = /* @__PURE__ */ new Set();
18150
+ for (const name of enabled) {
18151
+ for (const tool of TOOLSETS[name]) {
18152
+ if (readOnly && !READ_ONLY_TOOLS.has(tool)) {
18153
+ continue;
18154
+ }
18155
+ allowed.add(tool);
18156
+ }
18157
+ }
18158
+ return { enabled, readOnly, allowed };
18159
+ }
18160
+ function annotationsFor(toolName) {
18161
+ if (READ_ONLY_TOOLS.has(toolName)) {
18162
+ return { readOnlyHint: true, idempotentHint: true };
18163
+ }
18164
+ const annotations = {};
18165
+ if (!DESTRUCTIVE_TOOLS.has(toolName)) {
18166
+ annotations.destructiveHint = false;
18167
+ }
18168
+ if (toolName !== "sheets_append_values") {
18169
+ annotations.idempotentHint = true;
18170
+ }
18171
+ return annotations;
18172
+ }
18173
+
18039
18174
  // src/utils/error-handler.ts
18040
18175
  function handleError(error2) {
18041
18176
  console.error("Error in Google Sheets operation:", error2);
@@ -18319,7 +18454,7 @@ async function getSheetId(sheets, spreadsheetId, sheetName) {
18319
18454
  const sheetsData = response.data.sheets || [];
18320
18455
  if (sheetName) {
18321
18456
  const sheet = sheetsData.find((s) => s.properties?.title === sheetName);
18322
- if (!sheet?.properties?.sheetId) {
18457
+ if (sheet?.properties?.sheetId === void 0 || sheet.properties.sheetId === null) {
18323
18458
  const availableSheets = sheetsData.map((s) => s.properties?.title).filter((title) => title).join(", ");
18324
18459
  throw new Error(`Sheet "${sheetName}" not found. Available sheets: ${availableSheets}`);
18325
18460
  }
@@ -20130,6 +20265,480 @@ async function addConditionalFormattingHandler(input) {
20130
20265
  }
20131
20266
  }
20132
20267
 
20268
+ // src/utils/table-helpers.ts
20269
+ var TABLE_COLUMN_TYPES = [
20270
+ "COLUMN_TYPE_UNSPECIFIED",
20271
+ "DOUBLE",
20272
+ "CURRENCY",
20273
+ "PERCENT",
20274
+ "DATE",
20275
+ "TIME",
20276
+ "DATE_TIME",
20277
+ "TEXT",
20278
+ "BOOLEAN",
20279
+ "DROPDOWN",
20280
+ "FILES_CHIP",
20281
+ "PEOPLE_CHIP",
20282
+ "FINANCE_CHIP",
20283
+ "PLACE_CHIP",
20284
+ "RATINGS_CHIP"
20285
+ ];
20286
+ function resolveTableRangeInput(sheetName, range) {
20287
+ const extracted = extractSheetName(range);
20288
+ if (extracted.sheetName && extracted.sheetName !== sheetName) {
20289
+ throw new Error(`Range sheet "${extracted.sheetName}" does not match sheetName "${sheetName}"`);
20290
+ }
20291
+ return {
20292
+ sheetName: extracted.sheetName ?? sheetName,
20293
+ range: extracted.range
20294
+ };
20295
+ }
20296
+ function buildTableColumnProperties(columns) {
20297
+ return columns.map((column, index) => {
20298
+ const columnProperties = {
20299
+ columnIndex: index,
20300
+ columnName: column.name,
20301
+ columnType: column.columnType
20302
+ };
20303
+ if (column.dropdownValues && column.dropdownValues.length > 0) {
20304
+ columnProperties.dataValidationRule = {
20305
+ condition: {
20306
+ type: "ONE_OF_LIST",
20307
+ values: column.dropdownValues.map((value) => ({ userEnteredValue: value }))
20308
+ }
20309
+ };
20310
+ }
20311
+ return columnProperties;
20312
+ });
20313
+ }
20314
+ function getRangeColumnCount(range) {
20315
+ if (range.startColumnIndex === void 0 || range.startColumnIndex === null || range.endColumnIndex === void 0 || range.endColumnIndex === null) {
20316
+ return void 0;
20317
+ }
20318
+ return range.endColumnIndex - range.startColumnIndex;
20319
+ }
20320
+ function validateTableGridRange(range, rangeText) {
20321
+ const startRow = range.startRowIndex;
20322
+ const endRow = range.endRowIndex;
20323
+ const startColumn = range.startColumnIndex;
20324
+ const endColumn = range.endColumnIndex;
20325
+ if (startRow === void 0 || startRow === null || endRow === void 0 || endRow === null || startColumn === void 0 || startColumn === null || endColumn === void 0 || endColumn === null || startRow < 0 || startColumn < 0 || endRow <= startRow || endColumn <= startColumn) {
20326
+ throw new Error(`Invalid table range: ${rangeText}. Use a rectangular A1 range like "A1:D20".`);
20327
+ }
20328
+ }
20329
+ function validateColumnCount(range, columns, rangeText) {
20330
+ const columnCount = getRangeColumnCount(range);
20331
+ if (columnCount !== void 0 && columnCount !== columns.length) {
20332
+ throw new Error(
20333
+ `Column count mismatch: range ${rangeText} spans ${columnCount} column(s), but ${columns.length} column definition(s) were provided`
20334
+ );
20335
+ }
20336
+ }
20337
+ function rangesOverlap(first, second) {
20338
+ if (first.sheetId !== void 0 && first.sheetId !== null && second.sheetId !== void 0 && second.sheetId !== null && first.sheetId !== second.sheetId) {
20339
+ return false;
20340
+ }
20341
+ const firstStartRow = first.startRowIndex ?? 0;
20342
+ const firstEndRow = first.endRowIndex ?? Number.POSITIVE_INFINITY;
20343
+ const firstStartColumn = first.startColumnIndex ?? 0;
20344
+ const firstEndColumn = first.endColumnIndex ?? Number.POSITIVE_INFINITY;
20345
+ const secondStartRow = second.startRowIndex ?? 0;
20346
+ const secondEndRow = second.endRowIndex ?? Number.POSITIVE_INFINITY;
20347
+ const secondStartColumn = second.startColumnIndex ?? 0;
20348
+ const secondEndColumn = second.endColumnIndex ?? Number.POSITIVE_INFINITY;
20349
+ return firstStartRow < secondEndRow && firstEndRow > secondStartRow && firstStartColumn < secondEndColumn && firstEndColumn > secondStartColumn;
20350
+ }
20351
+ function ensureNoOverlappingTable(tables, range, sheetName, excludeTableId) {
20352
+ const overlappingTable = tables.find(
20353
+ (table) => table.tableId !== excludeTableId && table.range && rangesOverlap(table.range, range)
20354
+ );
20355
+ if (!overlappingTable) {
20356
+ return;
20357
+ }
20358
+ const existingRange = overlappingTable.range ? gridRangeToA1(overlappingTable.range) : "unknown";
20359
+ const requestedRange = gridRangeToA1(range);
20360
+ const tableLabel = overlappingTable.name || overlappingTable.tableId || "unnamed table";
20361
+ throw new Error(
20362
+ `Table range ${sheetName}!${requestedRange} overlaps existing table "${tableLabel}" (${sheetName}!${existingRange}). Choose a non-overlapping range or update/delete the existing table.`
20363
+ );
20364
+ }
20365
+ function formatTableForResponse(table, sheetName) {
20366
+ const range = table.range ? gridRangeToA1(table.range) : null;
20367
+ return {
20368
+ tableId: table.tableId ?? null,
20369
+ name: table.name ?? null,
20370
+ range: range && sheetName ? `${sheetName}!${range}` : range,
20371
+ gridRange: table.range ?? null,
20372
+ columnProperties: table.columnProperties ?? []
20373
+ };
20374
+ }
20375
+
20376
+ // src/tools/add-table.ts
20377
+ var columnSchema = external_exports.object({
20378
+ name: external_exports.string().min(1),
20379
+ columnType: external_exports.enum(TABLE_COLUMN_TYPES),
20380
+ dropdownValues: external_exports.array(external_exports.string()).optional()
20381
+ });
20382
+ var addTableInputSchema = external_exports.object({
20383
+ spreadsheetId: external_exports.string().min(1),
20384
+ sheetName: external_exports.string().min(1),
20385
+ range: external_exports.string().min(1),
20386
+ name: external_exports.string().min(1),
20387
+ columns: external_exports.array(columnSchema).min(1)
20388
+ });
20389
+ var addTableTool = {
20390
+ name: "sheets_add_table",
20391
+ description: "Create a native Google Sheets table with typed columns and optional dropdown values",
20392
+ inputSchema: {
20393
+ type: "object",
20394
+ properties: {
20395
+ spreadsheetId: {
20396
+ type: "string",
20397
+ description: "The ID of the spreadsheet (found in the URL after /d/)"
20398
+ },
20399
+ sheetName: {
20400
+ type: "string",
20401
+ description: "Name of the sheet (tab) where the table will be created"
20402
+ },
20403
+ range: {
20404
+ type: "string",
20405
+ description: 'A1 notation range for the table, e.g. "A1:D20" or "Sheet1!A1:D20"'
20406
+ },
20407
+ name: {
20408
+ type: "string",
20409
+ description: "Unique table name within the spreadsheet"
20410
+ },
20411
+ columns: {
20412
+ type: "array",
20413
+ description: "Column definitions for the table",
20414
+ items: {
20415
+ type: "object",
20416
+ properties: {
20417
+ name: { type: "string", description: "Column name" },
20418
+ columnType: {
20419
+ type: "string",
20420
+ enum: [...TABLE_COLUMN_TYPES],
20421
+ description: "Google Sheets table column type"
20422
+ },
20423
+ dropdownValues: {
20424
+ type: "array",
20425
+ items: { type: "string" },
20426
+ description: "Allowed values for DROPDOWN columns"
20427
+ }
20428
+ },
20429
+ required: ["name", "columnType"]
20430
+ }
20431
+ }
20432
+ },
20433
+ required: ["spreadsheetId", "sheetName", "range", "name", "columns"]
20434
+ }
20435
+ };
20436
+ async function addTableHandler(input) {
20437
+ try {
20438
+ const validatedInput = addTableInputSchema.parse(input);
20439
+ const sheets = await getAuthenticatedClient();
20440
+ const { sheetName, range } = resolveTableRangeInput(
20441
+ validatedInput.sheetName,
20442
+ validatedInput.range
20443
+ );
20444
+ const sheetId = await getSheetId(sheets, validatedInput.spreadsheetId, sheetName);
20445
+ const gridRange = parseRange(range, sheetId);
20446
+ validateTableGridRange(gridRange, `${sheetName}!${range}`);
20447
+ validateColumnCount(gridRange, validatedInput.columns, `${sheetName}!${range}`);
20448
+ const metadataResponse = await sheets.spreadsheets.get({
20449
+ spreadsheetId: validatedInput.spreadsheetId,
20450
+ fields: "sheets.properties.title,sheets.properties.sheetId,sheets.tables"
20451
+ });
20452
+ const sheetData = findSheetOrThrow(metadataResponse.data.sheets ?? [], sheetName);
20453
+ ensureNoOverlappingTable(sheetData.tables ?? [], gridRange, sheetName);
20454
+ const table = {
20455
+ name: validatedInput.name,
20456
+ range: gridRange,
20457
+ columnProperties: buildTableColumnProperties(validatedInput.columns)
20458
+ };
20459
+ const response = await sheets.spreadsheets.batchUpdate({
20460
+ spreadsheetId: validatedInput.spreadsheetId,
20461
+ requestBody: {
20462
+ requests: [
20463
+ {
20464
+ addTable: {
20465
+ table
20466
+ }
20467
+ }
20468
+ ]
20469
+ }
20470
+ });
20471
+ const addedTable = response.data.replies?.[0]?.addTable?.table ?? table;
20472
+ return formatToolResponse(`Successfully created table "${validatedInput.name}"`, {
20473
+ spreadsheetId: response.data.spreadsheetId,
20474
+ table: formatTableForResponse(addedTable, sheetName)
20475
+ });
20476
+ } catch (error2) {
20477
+ return handleError(error2);
20478
+ }
20479
+ }
20480
+
20481
+ // src/tools/update-table.ts
20482
+ var columnSchema2 = external_exports.object({
20483
+ name: external_exports.string().min(1),
20484
+ columnType: external_exports.enum(TABLE_COLUMN_TYPES),
20485
+ dropdownValues: external_exports.array(external_exports.string()).optional()
20486
+ });
20487
+ var updateTableInputSchema = external_exports.object({
20488
+ spreadsheetId: external_exports.string().min(1),
20489
+ tableId: external_exports.string().min(1),
20490
+ fields: external_exports.string().min(1),
20491
+ sheetName: external_exports.string().min(1).optional(),
20492
+ range: external_exports.string().min(1).optional(),
20493
+ name: external_exports.string().min(1).optional(),
20494
+ columns: external_exports.array(columnSchema2).min(1).optional()
20495
+ }).refine((value) => value.name || value.range || value.columns, {
20496
+ message: "At least one of name, range, or columns must be provided"
20497
+ });
20498
+ var updateTableTool = {
20499
+ name: "sheets_update_table",
20500
+ description: "Update an existing native Google Sheets table by tableId",
20501
+ inputSchema: {
20502
+ type: "object",
20503
+ properties: {
20504
+ spreadsheetId: {
20505
+ type: "string",
20506
+ description: "The ID of the spreadsheet (found in the URL after /d/)"
20507
+ },
20508
+ tableId: {
20509
+ type: "string",
20510
+ description: "The ID of the table to update"
20511
+ },
20512
+ fields: {
20513
+ type: "string",
20514
+ description: 'Required field mask for the table update, e.g. "name", "range", "columnProperties", or "name,range"'
20515
+ },
20516
+ sheetName: {
20517
+ type: "string",
20518
+ description: "Name of the target sheet when updating the table range"
20519
+ },
20520
+ range: {
20521
+ type: "string",
20522
+ description: 'Optional new A1 notation range for the table, e.g. "A1:D20"'
20523
+ },
20524
+ name: {
20525
+ type: "string",
20526
+ description: "Optional new table name"
20527
+ },
20528
+ columns: {
20529
+ type: "array",
20530
+ description: "Optional replacement column definitions for the table",
20531
+ items: {
20532
+ type: "object",
20533
+ properties: {
20534
+ name: { type: "string", description: "Column name" },
20535
+ columnType: {
20536
+ type: "string",
20537
+ enum: [...TABLE_COLUMN_TYPES],
20538
+ description: "Google Sheets table column type"
20539
+ },
20540
+ dropdownValues: {
20541
+ type: "array",
20542
+ items: { type: "string" },
20543
+ description: "Allowed values for DROPDOWN columns"
20544
+ }
20545
+ },
20546
+ required: ["name", "columnType"]
20547
+ }
20548
+ }
20549
+ },
20550
+ required: ["spreadsheetId", "tableId", "fields"]
20551
+ }
20552
+ };
20553
+ async function updateTableHandler(input) {
20554
+ try {
20555
+ const validatedInput = updateTableInputSchema.parse(input);
20556
+ const sheets = await getAuthenticatedClient();
20557
+ const metadataResponse = await sheets.spreadsheets.get({
20558
+ spreadsheetId: validatedInput.spreadsheetId,
20559
+ fields: "sheets.properties.title,sheets.properties.sheetId,sheets.tables"
20560
+ });
20561
+ const allSheets = metadataResponse.data.sheets ?? [];
20562
+ const existingTable = allSheets.flatMap((sheet) => sheet.tables ?? []).find((table2) => table2.tableId === validatedInput.tableId);
20563
+ if (!existingTable) {
20564
+ throw new Error(`Table "${validatedInput.tableId}" not found`);
20565
+ }
20566
+ const table = {
20567
+ tableId: validatedInput.tableId
20568
+ };
20569
+ if (validatedInput.name) {
20570
+ table.name = validatedInput.name;
20571
+ }
20572
+ if (validatedInput.columns) {
20573
+ table.columnProperties = buildTableColumnProperties(validatedInput.columns);
20574
+ }
20575
+ let responseSheetName;
20576
+ if (validatedInput.range) {
20577
+ const extracted = extractSheetName(validatedInput.range);
20578
+ const sheetName = validatedInput.sheetName ?? extracted.sheetName;
20579
+ if (!sheetName) {
20580
+ throw new Error("sheetName is required when updating a table range");
20581
+ }
20582
+ const resolvedRange = resolveTableRangeInput(sheetName, validatedInput.range);
20583
+ responseSheetName = resolvedRange.sheetName;
20584
+ const sheetId = await getSheetId(
20585
+ sheets,
20586
+ validatedInput.spreadsheetId,
20587
+ resolvedRange.sheetName
20588
+ );
20589
+ const gridRange = parseRange(resolvedRange.range, sheetId);
20590
+ validateTableGridRange(gridRange, `${resolvedRange.sheetName}!${resolvedRange.range}`);
20591
+ if (validatedInput.columns) {
20592
+ validateColumnCount(
20593
+ gridRange,
20594
+ validatedInput.columns,
20595
+ `${resolvedRange.sheetName}!${resolvedRange.range}`
20596
+ );
20597
+ }
20598
+ const sheetData = findSheetOrThrow(allSheets, resolvedRange.sheetName);
20599
+ ensureNoOverlappingTable(
20600
+ sheetData.tables ?? [],
20601
+ gridRange,
20602
+ resolvedRange.sheetName,
20603
+ validatedInput.tableId
20604
+ );
20605
+ table.range = gridRange;
20606
+ }
20607
+ const response = await sheets.spreadsheets.batchUpdate({
20608
+ spreadsheetId: validatedInput.spreadsheetId,
20609
+ requestBody: {
20610
+ requests: [
20611
+ {
20612
+ updateTable: {
20613
+ table,
20614
+ fields: validatedInput.fields
20615
+ }
20616
+ }
20617
+ ]
20618
+ }
20619
+ });
20620
+ return formatToolResponse(`Successfully updated table "${validatedInput.tableId}"`, {
20621
+ spreadsheetId: response.data.spreadsheetId,
20622
+ table: formatTableForResponse({ ...existingTable, ...table }, responseSheetName),
20623
+ fields: validatedInput.fields
20624
+ });
20625
+ } catch (error2) {
20626
+ return handleError(error2);
20627
+ }
20628
+ }
20629
+
20630
+ // src/tools/delete-table.ts
20631
+ var deleteTableInputSchema = external_exports.object({
20632
+ spreadsheetId: external_exports.string().min(1),
20633
+ tableId: external_exports.string().min(1)
20634
+ });
20635
+ var deleteTableTool = {
20636
+ name: "sheets_delete_table",
20637
+ description: "Delete a native Google Sheets table by tableId",
20638
+ inputSchema: {
20639
+ type: "object",
20640
+ properties: {
20641
+ spreadsheetId: {
20642
+ type: "string",
20643
+ description: "The ID of the spreadsheet (found in the URL after /d/)"
20644
+ },
20645
+ tableId: {
20646
+ type: "string",
20647
+ description: "The ID of the table to delete"
20648
+ }
20649
+ },
20650
+ required: ["spreadsheetId", "tableId"]
20651
+ }
20652
+ };
20653
+ async function deleteTableHandler(input) {
20654
+ try {
20655
+ const validatedInput = deleteTableInputSchema.parse(input);
20656
+ const sheets = await getAuthenticatedClient();
20657
+ const metadataResponse = await sheets.spreadsheets.get({
20658
+ spreadsheetId: validatedInput.spreadsheetId,
20659
+ fields: "sheets.tables.tableId"
20660
+ });
20661
+ const tableExists = (metadataResponse.data.sheets ?? []).flatMap((sheet) => sheet.tables ?? []).some((table) => table.tableId === validatedInput.tableId);
20662
+ if (!tableExists) {
20663
+ throw new Error(`Table "${validatedInput.tableId}" not found`);
20664
+ }
20665
+ const response = await sheets.spreadsheets.batchUpdate({
20666
+ spreadsheetId: validatedInput.spreadsheetId,
20667
+ requestBody: {
20668
+ requests: [
20669
+ {
20670
+ deleteTable: {
20671
+ tableId: validatedInput.tableId
20672
+ }
20673
+ }
20674
+ ]
20675
+ }
20676
+ });
20677
+ return formatToolResponse(`Successfully deleted table "${validatedInput.tableId}"`, {
20678
+ spreadsheetId: response.data.spreadsheetId,
20679
+ tableId: validatedInput.tableId
20680
+ });
20681
+ } catch (error2) {
20682
+ return handleError(error2);
20683
+ }
20684
+ }
20685
+
20686
+ // src/tools/get-tables.ts
20687
+ var getTablesInputSchema = external_exports.object({
20688
+ spreadsheetId: external_exports.string().min(1),
20689
+ sheetName: external_exports.string().min(1).optional()
20690
+ });
20691
+ var getTablesTool = {
20692
+ name: "sheets_get_tables",
20693
+ description: "Read native Google Sheets tables for a spreadsheet or a specific sheet",
20694
+ inputSchema: {
20695
+ type: "object",
20696
+ properties: {
20697
+ spreadsheetId: {
20698
+ type: "string",
20699
+ description: "The ID of the spreadsheet (found in the URL after /d/)"
20700
+ },
20701
+ sheetName: {
20702
+ type: "string",
20703
+ description: "Optional name of the sheet (tab) to inspect"
20704
+ }
20705
+ },
20706
+ required: ["spreadsheetId"]
20707
+ }
20708
+ };
20709
+ async function getTablesHandler(input) {
20710
+ try {
20711
+ const validatedInput = getTablesInputSchema.parse(input);
20712
+ const sheets = await getAuthenticatedClient();
20713
+ const response = await sheets.spreadsheets.get({
20714
+ spreadsheetId: validatedInput.spreadsheetId,
20715
+ fields: "sheets.properties.title,sheets.properties.sheetId,sheets.tables"
20716
+ });
20717
+ const spreadsheetSheets = response.data.sheets ?? [];
20718
+ const targetSheets = validatedInput.sheetName ? [findSheetOrThrow(spreadsheetSheets, validatedInput.sheetName)] : spreadsheetSheets;
20719
+ const tables = targetSheets.flatMap((sheet) => {
20720
+ const sheetName = sheet.properties?.title ?? void 0;
20721
+ return (sheet.tables ?? []).map((table) => ({
20722
+ sheetName: sheetName ?? null,
20723
+ sheetId: sheet.properties?.sheetId ?? null,
20724
+ ...formatTableForResponse(table, sheetName)
20725
+ }));
20726
+ });
20727
+ const context = validatedInput.sheetName ? ` in sheet "${validatedInput.sheetName}"` : " in spreadsheet";
20728
+ return formatSuccessResponse(
20729
+ {
20730
+ spreadsheetId: validatedInput.spreadsheetId,
20731
+ sheetName: validatedInput.sheetName,
20732
+ tableCount: tables.length,
20733
+ tables
20734
+ },
20735
+ `Found ${tables.length} table(s)${context}`
20736
+ );
20737
+ } catch (error2) {
20738
+ return handleError(error2);
20739
+ }
20740
+ }
20741
+
20133
20742
  // src/tools/check-access.ts
20134
20743
  var CheckAccessSchema = external_exports.object({
20135
20744
  spreadsheetId: external_exports.string().describe("The ID of the spreadsheet to check access for")
@@ -23366,6 +23975,10 @@ var toolHandlers = /* @__PURE__ */ new Map([
23366
23975
  ["sheets_merge_cells", mergeCellsHandler],
23367
23976
  ["sheets_unmerge_cells", unmergeCellsHandler],
23368
23977
  ["sheets_add_conditional_formatting", addConditionalFormattingHandler],
23978
+ ["sheets_add_table", addTableHandler],
23979
+ ["sheets_update_table", updateTableHandler],
23980
+ ["sheets_delete_table", deleteTableHandler],
23981
+ ["sheets_get_tables", getTablesHandler],
23369
23982
  // Batch operations
23370
23983
  ["sheets_batch_delete_sheets", handleBatchDeleteSheets],
23371
23984
  ["sheets_batch_format_cells", handleBatchFormatCells],
@@ -23414,6 +24027,10 @@ var allTools = [
23414
24027
  mergeCellsTool,
23415
24028
  unmergeCellsTool,
23416
24029
  addConditionalFormattingTool,
24030
+ addTableTool,
24031
+ updateTableTool,
24032
+ deleteTableTool,
24033
+ getTablesTool,
23417
24034
  batchDeleteSheetsTool,
23418
24035
  batchFormatCellsTool,
23419
24036
  createChartTool,
@@ -23443,6 +24060,23 @@ async function main() {
23443
24060
  console.error("Authentication Error:", error2.message);
23444
24061
  process.exit(1);
23445
24062
  }
24063
+ let toolsetConfig;
24064
+ try {
24065
+ toolsetConfig = resolveToolsets();
24066
+ } catch (error2) {
24067
+ if (error2 instanceof ToolsetConfigError) {
24068
+ console.error(`Configuration Error: ${error2.message}`);
24069
+ process.exit(1);
24070
+ }
24071
+ throw error2;
24072
+ }
24073
+ const { enabled, readOnly, allowed } = toolsetConfig;
24074
+ const exposedTools = allTools.filter((tool) => allowed.has(tool.name)).map((tool) => ({ ...tool, annotations: annotationsFor(tool.name) }));
24075
+ if (enabled.length < Object.keys(TOOLSETS).length || readOnly) {
24076
+ console.error(
24077
+ `Toolsets: ${enabled.join(", ")}${readOnly ? " (read-only)" : ""} \u2014 ${exposedTools.length}/${allTools.length} tools`
24078
+ );
24079
+ }
23446
24080
  const server = new Server(
23447
24081
  {
23448
24082
  name: "spreadsheet",
@@ -23458,11 +24092,16 @@ async function main() {
23458
24092
  );
23459
24093
  server.setRequestHandler(ListToolsRequestSchema, async () => {
23460
24094
  return {
23461
- tools: allTools
24095
+ tools: exposedTools
23462
24096
  };
23463
24097
  });
23464
24098
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
23465
24099
  const { name, arguments: args } = request.params;
24100
+ if (!allowed.has(name)) {
24101
+ throw new Error(
24102
+ toolHandlers.has(name) ? `Tool ${name} is disabled by the current GSHEETS_TOOLSETS/GSHEETS_READ_ONLY configuration` : `Unknown tool: ${name}`
24103
+ );
24104
+ }
23466
24105
  const handler = toolHandlers.get(name);
23467
24106
  if (!handler) {
23468
24107
  throw new Error(`Unknown tool: ${name}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-gsheets",
3
- "version": "1.8.1",
3
+ "version": "1.10.0",
4
4
  "mcpName": "io.github.freema/mcp-gsheets",
5
5
  "description": "Model Context Protocol (MCP) server for Google Sheets API integration",
6
6
  "author": "freema",