gogcli-mcp-sheets 2.25.0 → 2.26.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -2
- package/SKILL.md +4 -0
- package/dist/index.js +164 -41
- package/manifest.json +17 -1
- package/mint.yaml +107 -0
- package/package.json +2 -2
- package/server.json +2 -2
- package/src/tools/sheets-extra.ts +134 -3
- package/tests/tools/sheets-extra.test.ts +179 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "Extended Google Sheets for Claude via gogcli — auth + full Sheets support",
|
|
10
|
-
"version": "2.
|
|
10
|
+
"version": "2.26.0"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "gogcli (Sheets)",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Extended Google Sheets for Claude via gogcli — auth + full Sheets support",
|
|
18
|
-
"version": "2.
|
|
18
|
+
"version": "2.26.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/README.md
CHANGED
|
@@ -44,9 +44,9 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
44
44
|
claude mcp add gogcli-sheets -- gogcli-mcp-sheets
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
## Extra Sheets Tools (
|
|
47
|
+
## Extra Sheets Tools (59)
|
|
48
48
|
|
|
49
|
-
Plus 8 auth tools and 8 base Sheets tools (get, update, append, clear, metadata, create, find-replace, run) —
|
|
49
|
+
Plus 8 auth tools and 8 base Sheets tools (get, update, append, clear, metadata, create, find-replace, run) — 75 in all.
|
|
50
50
|
|
|
51
51
|
| Tool | Description |
|
|
52
52
|
|------|-------------|
|
|
@@ -79,6 +79,10 @@ Plus 8 auth tools and 8 base Sheets tools (get, update, append, clear, metadata,
|
|
|
79
79
|
| `gog_sheets_datasource_table_list` | List anchored data-source tables (extracts) and their A1 anchors |
|
|
80
80
|
| `gog_sheets_datasource_table_describe` | Describe the extract anchored at an A1 cell |
|
|
81
81
|
| `gog_sheets_datasource_table_read` | Read a bounded number of rows out of an extract |
|
|
82
|
+
| `gog_sheets_datasource_add` | Add a BigQuery Connected Sheets data source (custom SQL or a native table) |
|
|
83
|
+
| `gog_sheets_datasource_update` | Repoint a data source's SQL, table or billing project |
|
|
84
|
+
| `gog_sheets_datasource_refresh` | Re-run a data source so its sheet and extracts pick up current data |
|
|
85
|
+
| `gog_sheets_datasource_delete` | Delete a data source, its linked sheet, and unlink dependent objects |
|
|
82
86
|
| `gog_sheets_list_tabs` | List tabs in a spreadsheet (sheetId, title, index, gridProperties) |
|
|
83
87
|
| `gog_sheets_copy_paste` | Copy a range's values/formulas/format to another range (tiles to fill down/across). |
|
|
84
88
|
| `gog_sheets_validation_get` | Read data-validation rules (dropdowns, checkboxes, conditions) on a range. |
|
package/SKILL.md
CHANGED
|
@@ -58,6 +58,10 @@ Extended Google Sheets MCP server via [gogcli](https://github.com/openclaw/gogcl
|
|
|
58
58
|
| `gog_sheets_datasource_table_list` | List anchored data-source tables (extracts) and their A1 anchors |
|
|
59
59
|
| `gog_sheets_datasource_table_describe` | Describe the extract anchored at an A1 cell |
|
|
60
60
|
| `gog_sheets_datasource_table_read` | Read a bounded number of rows out of an extract |
|
|
61
|
+
| `gog_sheets_datasource_add` | Add a BigQuery Connected Sheets data source (custom SQL or a native table) |
|
|
62
|
+
| `gog_sheets_datasource_update` | Repoint a data source's SQL, table or billing project |
|
|
63
|
+
| `gog_sheets_datasource_refresh` | Re-run a data source so its sheet and extracts pick up current data |
|
|
64
|
+
| `gog_sheets_datasource_delete` | Delete a data source, its linked sheet, and unlink dependent objects |
|
|
61
65
|
| `gog_sheets_named_ranges_list` | List named ranges |
|
|
62
66
|
| `gog_sheets_named_ranges_get` | Get a named range |
|
|
63
67
|
| `gog_sheets_named_ranges_add` | Create a named range |
|
package/dist/index.js
CHANGED
|
@@ -31000,9 +31000,82 @@ var StdioServerTransport = class {
|
|
|
31000
31000
|
}
|
|
31001
31001
|
};
|
|
31002
31002
|
|
|
31003
|
+
// ../../node_modules/@chrischall/mcp-utils/dist/errors/index.js
|
|
31004
|
+
var McpToolError = class extends Error {
|
|
31005
|
+
/** Actionable remediation text, when one applies. */
|
|
31006
|
+
hint;
|
|
31007
|
+
constructor(message, opts) {
|
|
31008
|
+
super(message, opts?.cause !== void 0 ? { cause: opts.cause } : void 0);
|
|
31009
|
+
this.name = "McpToolError";
|
|
31010
|
+
if (opts?.hint !== void 0)
|
|
31011
|
+
this.hint = opts.hint;
|
|
31012
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
31013
|
+
}
|
|
31014
|
+
};
|
|
31015
|
+
var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
31016
|
+
var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
|
|
31017
|
+
var BASIC_AUTH_RE = /(authorization\s*:\s*basic\s+)[A-Za-z0-9+/=_-]{6,}/gi;
|
|
31018
|
+
var SET_COOKIE_RE = /(\bset-cookie\s*:\s*)([^=;,\s]+)=[^;,\s]*/gi;
|
|
31019
|
+
var COOKIE_HEADER_RE = /((?<!set-)\bcookie\s*:\s*)((?:[^=;,\s]+=[^;,\s]*)(?:;\s*[^=;,\s]+=[^;,\s]*)*)/gi;
|
|
31020
|
+
var API_KEY_RE = new RegExp([
|
|
31021
|
+
"sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
|
|
31022
|
+
// OpenAI / Anthropic (incl. sk-ant-…)
|
|
31023
|
+
"gh[pousr]_[A-Za-z0-9]{36,}\\b",
|
|
31024
|
+
// GitHub ghp_/gho_/ghu_/ghs_/ghr_
|
|
31025
|
+
"xox[baprs]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])",
|
|
31026
|
+
// Slack
|
|
31027
|
+
"AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])",
|
|
31028
|
+
// Google API key (39 chars total)
|
|
31029
|
+
"AKIA[0-9A-Z]{16}\\b",
|
|
31030
|
+
// AWS access key id (20 chars total)
|
|
31031
|
+
"whsec_[A-Za-z0-9]{16,}\\b"
|
|
31032
|
+
// webhook signing secret (Stripe-style)
|
|
31033
|
+
].map((p) => `\\b${p}`).join("|"), "g");
|
|
31034
|
+
var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
|
|
31035
|
+
var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
|
|
31036
|
+
var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
|
|
31037
|
+
var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
|
|
31038
|
+
var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
|
|
31039
|
+
function redactSecrets(text) {
|
|
31040
|
+
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
|
|
31041
|
+
}
|
|
31042
|
+
|
|
31043
|
+
// ../../node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
31044
|
+
function rawTextResult(text) {
|
|
31045
|
+
return { content: [{ type: "text", text }] };
|
|
31046
|
+
}
|
|
31047
|
+
function errorResult(message) {
|
|
31048
|
+
return {
|
|
31049
|
+
content: [{ type: "text", text: redactSecrets(message) }],
|
|
31050
|
+
isError: true
|
|
31051
|
+
};
|
|
31052
|
+
}
|
|
31053
|
+
|
|
31003
31054
|
// ../../node_modules/@chrischall/mcp-utils/dist/server/index.js
|
|
31055
|
+
function hintResultOrRethrow(err) {
|
|
31056
|
+
if (err instanceof McpToolError && err.hint) {
|
|
31057
|
+
return errorResult(`${err.message}
|
|
31058
|
+
|
|
31059
|
+
Hint: ${err.hint}`);
|
|
31060
|
+
}
|
|
31061
|
+
throw err;
|
|
31062
|
+
}
|
|
31063
|
+
function surfaceToolHints(server) {
|
|
31064
|
+
const register = server.registerTool.bind(server);
|
|
31065
|
+
server.registerTool = (name, config2, cb) => register(name, config2, (...args) => {
|
|
31066
|
+
let result;
|
|
31067
|
+
try {
|
|
31068
|
+
result = cb(...args);
|
|
31069
|
+
} catch (err) {
|
|
31070
|
+
return hintResultOrRethrow(err);
|
|
31071
|
+
}
|
|
31072
|
+
return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
|
|
31073
|
+
});
|
|
31074
|
+
}
|
|
31004
31075
|
async function createMcpServer(opts) {
|
|
31005
31076
|
const server = new McpServer({ name: opts.name, version: opts.version });
|
|
31077
|
+
if (opts.surfaceHints !== false)
|
|
31078
|
+
surfaceToolHints(server);
|
|
31006
31079
|
if (opts.banner !== void 0) {
|
|
31007
31080
|
console.error(opts.banner);
|
|
31008
31081
|
}
|
|
@@ -31047,46 +31120,6 @@ async function runMcp(opts) {
|
|
|
31047
31120
|
return server;
|
|
31048
31121
|
}
|
|
31049
31122
|
|
|
31050
|
-
// ../../node_modules/@chrischall/mcp-utils/dist/errors/index.js
|
|
31051
|
-
var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
31052
|
-
var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
|
|
31053
|
-
var BASIC_AUTH_RE = /(authorization\s*:\s*basic\s+)[A-Za-z0-9+/=_-]{6,}/gi;
|
|
31054
|
-
var SET_COOKIE_RE = /(\bset-cookie\s*:\s*)([^=;,\s]+)=[^;,\s]*/gi;
|
|
31055
|
-
var COOKIE_HEADER_RE = /((?<!set-)\bcookie\s*:\s*)((?:[^=;,\s]+=[^;,\s]*)(?:;\s*[^=;,\s]+=[^;,\s]*)*)/gi;
|
|
31056
|
-
var API_KEY_RE = new RegExp([
|
|
31057
|
-
"sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
|
|
31058
|
-
// OpenAI / Anthropic (incl. sk-ant-…)
|
|
31059
|
-
"gh[pousr]_[A-Za-z0-9]{36,}\\b",
|
|
31060
|
-
// GitHub ghp_/gho_/ghu_/ghs_/ghr_
|
|
31061
|
-
"xox[baprs]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])",
|
|
31062
|
-
// Slack
|
|
31063
|
-
"AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])",
|
|
31064
|
-
// Google API key (39 chars total)
|
|
31065
|
-
"AKIA[0-9A-Z]{16}\\b",
|
|
31066
|
-
// AWS access key id (20 chars total)
|
|
31067
|
-
"whsec_[A-Za-z0-9]{16,}\\b"
|
|
31068
|
-
// webhook signing secret (Stripe-style)
|
|
31069
|
-
].map((p) => `\\b${p}`).join("|"), "g");
|
|
31070
|
-
var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
|
|
31071
|
-
var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
|
|
31072
|
-
var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
|
|
31073
|
-
var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
|
|
31074
|
-
var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
|
|
31075
|
-
function redactSecrets(text) {
|
|
31076
|
-
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
|
|
31077
|
-
}
|
|
31078
|
-
|
|
31079
|
-
// ../../node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
31080
|
-
function rawTextResult(text) {
|
|
31081
|
-
return { content: [{ type: "text", text }] };
|
|
31082
|
-
}
|
|
31083
|
-
function errorResult(message) {
|
|
31084
|
-
return {
|
|
31085
|
-
content: [{ type: "text", text: redactSecrets(message) }],
|
|
31086
|
-
isError: true
|
|
31087
|
-
};
|
|
31088
|
-
}
|
|
31089
|
-
|
|
31090
31123
|
// ../../node_modules/@chrischall/mcp-utils/dist/config/index.js
|
|
31091
31124
|
var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
|
|
31092
31125
|
function readEnvVar(key, opts = {}) {
|
|
@@ -31886,6 +31919,16 @@ function authToolsFor(defaultServices) {
|
|
|
31886
31919
|
return (server) => registerAuthToolsWith(server, defaultServices);
|
|
31887
31920
|
}
|
|
31888
31921
|
|
|
31922
|
+
// ../gogcli-mcp/src/tools/calendar.ts
|
|
31923
|
+
var reminderParams = {
|
|
31924
|
+
reminders: external_exports.array(external_exports.string()).max(5).optional().describe(
|
|
31925
|
+
`Reminders as method:duration, e.g. ["popup:30m", "email:1d"]. Method is popup or email; duration accepts m/h/d (max 40320 minutes = 4 weeks). Google allows at most 5. These REPLACE the event's reminders \u2014 on update, pass an EMPTY array to drop custom reminders and go back to the calendar's defaults. Cannot be combined with noReminders.`
|
|
31926
|
+
),
|
|
31927
|
+
noReminders: external_exports.boolean().optional().describe(
|
|
31928
|
+
"Give the event no reminders at all, overriding the calendar's defaults. Different from an empty reminders array, which RESTORES those defaults. Cannot be combined with reminders."
|
|
31929
|
+
)
|
|
31930
|
+
};
|
|
31931
|
+
|
|
31889
31932
|
// ../gogcli-mcp/src/attachments.ts
|
|
31890
31933
|
var MAX_INLINE_ATTACHMENT_BYTES = 8 * 1024 * 1024;
|
|
31891
31934
|
var RUNNER_MAX_BODY_BYTES = 32 * 1024 * 1024;
|
|
@@ -32074,7 +32117,7 @@ function registerSheetsTools(server) {
|
|
|
32074
32117
|
}
|
|
32075
32118
|
|
|
32076
32119
|
// ../gogcli-mcp/src/server.ts
|
|
32077
|
-
var VERSION = true ? "2.
|
|
32120
|
+
var VERSION = true ? "2.26.0" : "0.0.0";
|
|
32078
32121
|
|
|
32079
32122
|
// ../gogcli-mcp/src/auth-log.ts
|
|
32080
32123
|
var FAILURES = /* @__PURE__ */ new Set([
|
|
@@ -33509,6 +33552,86 @@ ${errorText(err)}`
|
|
|
33509
33552
|
if (render) args.push(`--render=${render}`);
|
|
33510
33553
|
return runOrDiagnose(args, { account });
|
|
33511
33554
|
});
|
|
33555
|
+
const bigQueryChargeNote = " COSTS MONEY AND RUNS ASYNCHRONOUSLY: this starts a BigQuery execution billed to the billing project, and returns once the execution has been REQUESTED \u2014 not once it has finished. Poll gog_sheets_datasource_describe until its dataExecutionStatus state is SUCCEEDED or FAILED before treating the data as current.";
|
|
33556
|
+
server.registerTool("gog_sheets_datasource_add", {
|
|
33557
|
+
description: "Add a BigQuery Connected Sheets data source to a spreadsheet, creating its linked DATA_SOURCE sheet. Point it at EITHER a custom SQL query (query) OR an existing native table (dataset + table) \u2014 never both. billingProject is the BigQuery project charged for the query; tableProject only names which project OWNS the table, and defaults to the billing project. Returns the new dataSourceId, which every other datasource tool takes." + bigQueryChargeNote + bigQueryScopeNote,
|
|
33558
|
+
inputSchema: {
|
|
33559
|
+
spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
|
|
33560
|
+
billingProject: external_exports.string().describe("Billing-enabled BigQuery project charged for this source's queries"),
|
|
33561
|
+
query: external_exports.string().optional().describe("BigQuery SQL to run. Mutually exclusive with dataset/table/tableProject."),
|
|
33562
|
+
dataset: external_exports.string().optional().describe("BigQuery dataset ID of an existing table. Requires table; mutually exclusive with query."),
|
|
33563
|
+
table: external_exports.string().optional().describe("BigQuery table ID. Requires dataset; mutually exclusive with query."),
|
|
33564
|
+
tableProject: external_exports.string().optional().describe("BigQuery project that owns the table (default: the billing project). Only valid with dataset/table."),
|
|
33565
|
+
account: accountParam
|
|
33566
|
+
}
|
|
33567
|
+
}, async ({ spreadsheetId, billingProject, query, dataset, table, tableProject, account }) => {
|
|
33568
|
+
const tableFlags = tableProject !== void 0 || dataset !== void 0 || table !== void 0;
|
|
33569
|
+
if (query !== void 0 && tableFlags) {
|
|
33570
|
+
throw new Error("query and the table fields (dataset, table, tableProject) are mutually exclusive \u2014 a data source is backed by one or the other.");
|
|
33571
|
+
}
|
|
33572
|
+
if (query === void 0 && !(dataset && table)) {
|
|
33573
|
+
throw new Error("Pass query, or both --dataset and --table (tableProject alone is not a table).");
|
|
33574
|
+
}
|
|
33575
|
+
const args = ["sheets", "datasource", "add", spreadsheetId, `--billing-project=${billingProject}`];
|
|
33576
|
+
if (query !== void 0) args.push(`--query=${query}`);
|
|
33577
|
+
if (tableProject) args.push(`--table-project=${tableProject}`);
|
|
33578
|
+
if (dataset) args.push(`--dataset=${dataset}`);
|
|
33579
|
+
if (table) args.push(`--table=${table}`);
|
|
33580
|
+
return runOrDiagnose(args, { account });
|
|
33581
|
+
});
|
|
33582
|
+
server.registerTool("gog_sheets_datasource_update", {
|
|
33583
|
+
description: "Repoint an existing BigQuery Connected Sheets data source: new SQL for a query-backed source, a new dataset/table for a table-backed one, or a new billing project for either. Only the fields you pass are changed. You cannot convert a source between the two kinds \u2014 gog refuses SQL on a table source and table fields on a query source \u2014 so read gog_sheets_datasource_describe first if you are unsure which kind it is. Replacing the SQL DISCARDS the source's current results." + bigQueryChargeNote + bigQueryScopeNote,
|
|
33584
|
+
annotations: { destructiveHint: true },
|
|
33585
|
+
inputSchema: {
|
|
33586
|
+
spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
|
|
33587
|
+
dataSourceId: external_exports.string().describe("Data source ID, as reported by gog_sheets_datasource_list"),
|
|
33588
|
+
billingProject: external_exports.string().optional().describe("New billing-enabled BigQuery project to charge executions to"),
|
|
33589
|
+
query: external_exports.string().optional().describe("Replacement SQL. Only valid on a query-backed source; mutually exclusive with the table fields."),
|
|
33590
|
+
dataset: external_exports.string().optional().describe("Replacement dataset ID. Only valid on a table-backed source."),
|
|
33591
|
+
table: external_exports.string().optional().describe("Replacement table ID. Only valid on a table-backed source."),
|
|
33592
|
+
tableProject: external_exports.string().optional().describe("Replacement project owning the table. Only valid on a table-backed source."),
|
|
33593
|
+
account: accountParam
|
|
33594
|
+
}
|
|
33595
|
+
}, async ({ spreadsheetId, dataSourceId, billingProject, query, dataset, table, tableProject, account }) => {
|
|
33596
|
+
const tableFlags = tableProject !== void 0 || dataset !== void 0 || table !== void 0;
|
|
33597
|
+
if (query !== void 0 && tableFlags) {
|
|
33598
|
+
throw new Error("query and the table fields (dataset, table, tableProject) are mutually exclusive \u2014 a data source is backed by one or the other.");
|
|
33599
|
+
}
|
|
33600
|
+
if (billingProject === void 0 && query === void 0 && !tableFlags) {
|
|
33601
|
+
throw new Error("nothing to update: pass billingProject, query, dataset, table or tableProject.");
|
|
33602
|
+
}
|
|
33603
|
+
const args = ["sheets", "datasource", "update", spreadsheetId, dataSourceId];
|
|
33604
|
+
if (billingProject !== void 0) args.push(`--billing-project=${billingProject}`);
|
|
33605
|
+
if (query !== void 0) args.push(`--query=${query}`);
|
|
33606
|
+
if (tableProject !== void 0) args.push(`--table-project=${tableProject}`);
|
|
33607
|
+
if (dataset !== void 0) args.push(`--dataset=${dataset}`);
|
|
33608
|
+
if (table !== void 0) args.push(`--table=${table}`);
|
|
33609
|
+
return runOrDiagnose(args, { account });
|
|
33610
|
+
});
|
|
33611
|
+
server.registerTool("gog_sheets_datasource_refresh", {
|
|
33612
|
+
description: "Re-run a Connected Sheets data source so its sheet and extracts pick up current BigQuery data. Google refuses to refresh a source whose previous execution FAILED; forceRefresh overrides that, which is what you want after fixing the underlying query or permissions." + bigQueryChargeNote + bigQueryScopeNote,
|
|
33613
|
+
inputSchema: {
|
|
33614
|
+
spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
|
|
33615
|
+
dataSourceId: external_exports.string().describe("Data source ID, as reported by gog_sheets_datasource_list"),
|
|
33616
|
+
forceRefresh: external_exports.boolean().optional().describe("Refresh even when the previous execution failed"),
|
|
33617
|
+
account: accountParam
|
|
33618
|
+
}
|
|
33619
|
+
}, async ({ spreadsheetId, dataSourceId, forceRefresh, account }) => {
|
|
33620
|
+
const args = ["sheets", "datasource", "refresh", spreadsheetId, dataSourceId];
|
|
33621
|
+
if (forceRefresh) args.push("--force-refresh");
|
|
33622
|
+
return runOrDiagnose(args, { account });
|
|
33623
|
+
});
|
|
33624
|
+
server.registerTool("gog_sheets_datasource_delete", {
|
|
33625
|
+
description: "Delete a Connected Sheets data source. This is WIDER than it sounds: it also deletes the source's linked DATA_SOURCE sheet, and unlinks every extract, chart and pivot table built on it elsewhere in the spreadsheet \u2014 objects on other tabs that the caller never named. Take gog_sheets_snapshot first if any of that matters. The BigQuery data itself is untouched; only the spreadsheet's connection to it goes away." + bigQueryScopeNote,
|
|
33626
|
+
annotations: { destructiveHint: true },
|
|
33627
|
+
inputSchema: {
|
|
33628
|
+
spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
|
|
33629
|
+
dataSourceId: external_exports.string().describe("Data source ID, as reported by gog_sheets_datasource_list"),
|
|
33630
|
+
account: accountParam
|
|
33631
|
+
}
|
|
33632
|
+
}, async ({ spreadsheetId, dataSourceId, account }) => {
|
|
33633
|
+
return runOrDiagnose(["sheets", "datasource", "delete", spreadsheetId, dataSourceId, "--force"], { account });
|
|
33634
|
+
});
|
|
33512
33635
|
}
|
|
33513
33636
|
|
|
33514
33637
|
// src/index.ts
|
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp-sheets",
|
|
5
5
|
"display_name": "gogcli (Sheets)",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.26.0",
|
|
7
7
|
"description": "Extended Google Sheets for Claude via gogcli — auth + full Sheets support",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
|
@@ -339,6 +339,22 @@
|
|
|
339
339
|
{
|
|
340
340
|
"name": "gog_sheets_datasource_table_read",
|
|
341
341
|
"description": "Read a bounded number of rows out of a data-source table (extract)"
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
"name": "gog_sheets_datasource_add",
|
|
345
|
+
"description": "Add a BigQuery Connected Sheets data source (custom SQL or a native table)"
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"name": "gog_sheets_datasource_update",
|
|
349
|
+
"description": "Repoint a Connected Sheets data source's SQL, table or billing project"
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"name": "gog_sheets_datasource_refresh",
|
|
353
|
+
"description": "Re-run a Connected Sheets data source against current BigQuery data"
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
"name": "gog_sheets_datasource_delete",
|
|
357
|
+
"description": "Delete a Connected Sheets data source and its linked sheet"
|
|
342
358
|
}
|
|
343
359
|
],
|
|
344
360
|
"compatibility": {
|
package/mint.yaml
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
name: Google Sheets (gogcli)
|
|
3
|
+
slug: gogcli-mcp-sheets
|
|
4
|
+
summary: >-
|
|
5
|
+
Extended Google Sheets MCP server via gogcli — all base tools plus full
|
|
6
|
+
Sheets support
|
|
7
|
+
command:
|
|
8
|
+
# This package publishes a single bin; naming it keeps the install
|
|
9
|
+
# unambiguous alongside its eight sibling packages.
|
|
10
|
+
bin: gogcli-mcp-sheets
|
|
11
|
+
env:
|
|
12
|
+
- name: GOG_CLIENT_ID
|
|
13
|
+
required: false
|
|
14
|
+
help: >-
|
|
15
|
+
Google OAuth client id. NOTE: on the local-spawn path the child `gog`
|
|
16
|
+
receives a sanitized env — runner.ts drops GOG_ACCESS_TOKEN and every
|
|
17
|
+
*_TOKEN / *_SECRET / *_API_KEY / *_PRIVATE_KEY variable — so the CLI
|
|
18
|
+
authenticates from its own stored credentials under $HOME (see
|
|
19
|
+
state.dataDir), not from these variables being passed through.
|
|
20
|
+
- name: GOG_CLIENT_SECRET
|
|
21
|
+
secret: true
|
|
22
|
+
required: false
|
|
23
|
+
help: >-
|
|
24
|
+
Google OAuth client secret. Stripped from the spawned CLI's environment
|
|
25
|
+
by runner.ts's *_SECRET rule — see GOG_CLIENT_ID.
|
|
26
|
+
- name: GOG_REFRESH_TOKEN
|
|
27
|
+
secret: true
|
|
28
|
+
required: false
|
|
29
|
+
help: >-
|
|
30
|
+
Google OAuth refresh token. Stripped from the spawned CLI's environment
|
|
31
|
+
by runner.ts's *_TOKEN rule — see GOG_CLIENT_ID. A hosted deployment must
|
|
32
|
+
therefore carry gog's authorised-account state in its persisted data dir,
|
|
33
|
+
or drive a remote runner via GOG_RUNNER_URL.
|
|
34
|
+
- name: GOG_ACCESS_TOKEN
|
|
35
|
+
secret: true
|
|
36
|
+
required: false
|
|
37
|
+
help: >-
|
|
38
|
+
Deliberately removed from the spawned CLI's environment by runner.ts, so
|
|
39
|
+
that a stale directly-passed token cannot shadow gog's stored refresh
|
|
40
|
+
credential. Leave unset.
|
|
41
|
+
- name: GOG_ACCOUNT
|
|
42
|
+
required: false
|
|
43
|
+
help: >-
|
|
44
|
+
Which configured Google account to act as, when more than one is
|
|
45
|
+
authorised. Defaults to the single/most recent account.
|
|
46
|
+
- name: GOG_READONLY
|
|
47
|
+
required: false
|
|
48
|
+
help: >-
|
|
49
|
+
Set to 1 to refuse every mutating operation. Recommended when the
|
|
50
|
+
connector is shared or you only need reads.
|
|
51
|
+
- name: GOG_PATH
|
|
52
|
+
required: false
|
|
53
|
+
help: >-
|
|
54
|
+
Path to the `gog` binary. Leave unset when the dependency below supplies
|
|
55
|
+
it; set it only to point at a binary you manage yourself.
|
|
56
|
+
- name: GOG_RUNNER_URL
|
|
57
|
+
required: false
|
|
58
|
+
help: >-
|
|
59
|
+
URL of a remote gog runner to execute against instead of spawning the
|
|
60
|
+
local binary. If you set this, add its host to egress.allow.
|
|
61
|
+
- name: GOG_RUNNER_KEY
|
|
62
|
+
secret: true
|
|
63
|
+
required: false
|
|
64
|
+
help: >-
|
|
65
|
+
Shared key authenticating calls to GOG_RUNNER_URL. Required whenever that
|
|
66
|
+
is set.
|
|
67
|
+
- name: GOG_TIMEZONE
|
|
68
|
+
required: false
|
|
69
|
+
help: >-
|
|
70
|
+
The IANA zone gog itself formats its naive timestamps in. The wrapper
|
|
71
|
+
reads it (naiveSourceTimeZone) to re-attach the correct offset, so it
|
|
72
|
+
should match gog's own configuration. Falls back to DISPLAY_TZ, then to
|
|
73
|
+
America/New_York.
|
|
74
|
+
- name: DISPLAY_TZ
|
|
75
|
+
required: false
|
|
76
|
+
help: >-
|
|
77
|
+
The IANA zone every rendered *Display field uses (displayTimeZone,
|
|
78
|
+
default America/New_York). It does not read GOG_TIMEZONE — the fallback
|
|
79
|
+
runs the other way, so this is also what GOG_TIMEZONE falls back to. An
|
|
80
|
+
invalid value degrades to the default rather than throwing.
|
|
81
|
+
dependencies:
|
|
82
|
+
# Every tool shells out to the `gog` CLI; without it the server starts and
|
|
83
|
+
# then fails on the first call. This tag must track
|
|
84
|
+
# packages/gogcli-mcp/src/runner.ts's MIN_GOG_VERSION (the floor the tools
|
|
85
|
+
# assume) and the fly-gog-runner/Dockerfile GOG_VERSION build arg. See
|
|
86
|
+
# CLAUDE.md "Required gog version" — bumping the floor means bumping these
|
|
87
|
+
# nine pins too.
|
|
88
|
+
- kind: github-release
|
|
89
|
+
repo: openclaw/gogcli
|
|
90
|
+
tag: v0.38.1
|
|
91
|
+
asset: "gogcli_*_linux_amd64.tar.gz"
|
|
92
|
+
bin: [gog]
|
|
93
|
+
state:
|
|
94
|
+
dataDir: true
|
|
95
|
+
reason: >-
|
|
96
|
+
`gog` keeps its authorised-account state and token cache under $HOME.
|
|
97
|
+
Without a persistent data dir every cold start has no account configured and
|
|
98
|
+
every tool call fails until the credentials are re-supplied.
|
|
99
|
+
egress:
|
|
100
|
+
allow:
|
|
101
|
+
# Google OAuth token exchange and the Google REST APIs the CLI calls.
|
|
102
|
+
- oauth2.googleapis.com
|
|
103
|
+
- www.googleapis.com
|
|
104
|
+
- googleapis.com
|
|
105
|
+
#
|
|
106
|
+
# NOTE: if you set GOG_RUNNER_URL to run against a remote gog runner, add
|
|
107
|
+
# that host here too — it is supplied at runtime and cannot be declared.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp-sheets",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.26.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/gogcli-mcp-sheets",
|
|
5
5
|
"description": "Extended Google Sheets MCP server via gogcli — all base tools plus full Sheets support",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"test:coverage": "vitest run --coverage"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@chrischall/mcp-utils": "^0.
|
|
27
|
+
"@chrischall/mcp-utils": "^0.15.0",
|
|
28
28
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
29
29
|
"zod": "^4.4.3"
|
|
30
30
|
},
|
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"source": "github",
|
|
8
8
|
"subfolder": "packages/gogcli-mcp-sheets"
|
|
9
9
|
},
|
|
10
|
-
"version": "2.
|
|
10
|
+
"version": "2.26.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp-sheets",
|
|
15
|
-
"version": "2.
|
|
15
|
+
"version": "2.26.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
|
@@ -1006,9 +1006,9 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
1006
1006
|
return runOrDiagnose(args, { account });
|
|
1007
1007
|
});
|
|
1008
1008
|
|
|
1009
|
-
// Connected Sheets (gog >= 0.37.0, openclaw/gogcli#938
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
1009
|
+
// Connected Sheets (gog >= 0.37.0 for the reads, openclaw/gogcli#938; >= 0.38.0
|
|
1010
|
+
// for the writes further down, #1025-#1028). The five tools in this first
|
|
1011
|
+
// group are read-only: none of them changes a data source or starts a query.
|
|
1012
1012
|
//
|
|
1013
1013
|
// The scope is the thing to know before calling any of them: Google requires
|
|
1014
1014
|
// https://www.googleapis.com/auth/bigquery.readonly whenever a Sheets
|
|
@@ -1111,4 +1111,135 @@ export function registerExtraSheetsTools(server: McpServer): void {
|
|
|
1111
1111
|
return runOrDiagnose(args, { account });
|
|
1112
1112
|
});
|
|
1113
1113
|
|
|
1114
|
+
|
|
1115
|
+
// Connected Sheets WRITES (gog >= 0.38.0, openclaw/gogcli#1025-#1028). The
|
|
1116
|
+
// 0.37.0 half of this surface could only look; these four change a source or
|
|
1117
|
+
// start a query, and they carry two costs the read tools do not:
|
|
1118
|
+
//
|
|
1119
|
+
// 1. Money. Every one of them (delete excepted) starts a BigQuery execution
|
|
1120
|
+
// billed to --billing-project, so a careless refresh of a wide query is a
|
|
1121
|
+
// real charge on someone's account.
|
|
1122
|
+
// 2. Asynchrony. add/update/refresh return a DataExecutionStatus, not data;
|
|
1123
|
+
// the query keeps running after the call returns. gog_sheets_datasource_describe
|
|
1124
|
+
// is how you find out whether it SUCCEEDED or FAILED.
|
|
1125
|
+
//
|
|
1126
|
+
// The BigQuery scope note applies here exactly as it does to the reads: the
|
|
1127
|
+
// mutation is a Sheets write, but the response still carries Connected Sheets
|
|
1128
|
+
// data, so Google demands bigquery.readonly for it too.
|
|
1129
|
+
const bigQueryChargeNote =
|
|
1130
|
+
' COSTS MONEY AND RUNS ASYNCHRONOUSLY: this starts a BigQuery execution billed to the billing project, and returns ' +
|
|
1131
|
+
'once the execution has been REQUESTED — not once it has finished. Poll gog_sheets_datasource_describe until its ' +
|
|
1132
|
+
'dataExecutionStatus state is SUCCEEDED or FAILED before treating the data as current.';
|
|
1133
|
+
|
|
1134
|
+
server.registerTool('gog_sheets_datasource_add', {
|
|
1135
|
+
description:
|
|
1136
|
+
'Add a BigQuery Connected Sheets data source to a spreadsheet, creating its linked DATA_SOURCE sheet. Point it at ' +
|
|
1137
|
+
'EITHER a custom SQL query (query) OR an existing native table (dataset + table) — never both. billingProject is the ' +
|
|
1138
|
+
'BigQuery project charged for the query; tableProject only names which project OWNS the table, and defaults to the ' +
|
|
1139
|
+
'billing project. Returns the new dataSourceId, which every other datasource tool takes.' +
|
|
1140
|
+
bigQueryChargeNote + bigQueryScopeNote,
|
|
1141
|
+
inputSchema: {
|
|
1142
|
+
spreadsheetId: z.string().describe('Spreadsheet ID'),
|
|
1143
|
+
billingProject: z.string().describe('Billing-enabled BigQuery project charged for this source\'s queries'),
|
|
1144
|
+
query: z.string().optional().describe('BigQuery SQL to run. Mutually exclusive with dataset/table/tableProject.'),
|
|
1145
|
+
dataset: z.string().optional().describe('BigQuery dataset ID of an existing table. Requires table; mutually exclusive with query.'),
|
|
1146
|
+
table: z.string().optional().describe('BigQuery table ID. Requires dataset; mutually exclusive with query.'),
|
|
1147
|
+
tableProject: z.string().optional().describe('BigQuery project that owns the table (default: the billing project). Only valid with dataset/table.'),
|
|
1148
|
+
account: accountParam,
|
|
1149
|
+
},
|
|
1150
|
+
}, async ({ spreadsheetId, billingProject, query, dataset, table, tableProject, account }) => {
|
|
1151
|
+
const tableFlags = tableProject !== undefined || dataset !== undefined || table !== undefined;
|
|
1152
|
+
if (query !== undefined && tableFlags) {
|
|
1153
|
+
throw new Error('query and the table fields (dataset, table, tableProject) are mutually exclusive — a data source is backed by one or the other.');
|
|
1154
|
+
}
|
|
1155
|
+
if (query === undefined && !(dataset && table)) {
|
|
1156
|
+
throw new Error('Pass query, or both --dataset and --table (tableProject alone is not a table).');
|
|
1157
|
+
}
|
|
1158
|
+
const args = ['sheets', 'datasource', 'add', spreadsheetId, `--billing-project=${billingProject}`];
|
|
1159
|
+
if (query !== undefined) args.push(`--query=${query}`);
|
|
1160
|
+
if (tableProject) args.push(`--table-project=${tableProject}`);
|
|
1161
|
+
if (dataset) args.push(`--dataset=${dataset}`);
|
|
1162
|
+
if (table) args.push(`--table=${table}`);
|
|
1163
|
+
return runOrDiagnose(args, { account });
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
server.registerTool('gog_sheets_datasource_update', {
|
|
1167
|
+
description:
|
|
1168
|
+
'Repoint an existing BigQuery Connected Sheets data source: new SQL for a query-backed source, a new dataset/table for ' +
|
|
1169
|
+
'a table-backed one, or a new billing project for either. Only the fields you pass are changed. You cannot convert a ' +
|
|
1170
|
+
'source between the two kinds — gog refuses SQL on a table source and table fields on a query source — so read ' +
|
|
1171
|
+
'gog_sheets_datasource_describe first if you are unsure which kind it is. Replacing the SQL DISCARDS the source\'s ' +
|
|
1172
|
+
'current results.' + bigQueryChargeNote + bigQueryScopeNote,
|
|
1173
|
+
annotations: { destructiveHint: true },
|
|
1174
|
+
inputSchema: {
|
|
1175
|
+
spreadsheetId: z.string().describe('Spreadsheet ID'),
|
|
1176
|
+
dataSourceId: z.string().describe('Data source ID, as reported by gog_sheets_datasource_list'),
|
|
1177
|
+
billingProject: z.string().optional().describe('New billing-enabled BigQuery project to charge executions to'),
|
|
1178
|
+
query: z.string().optional().describe('Replacement SQL. Only valid on a query-backed source; mutually exclusive with the table fields.'),
|
|
1179
|
+
dataset: z.string().optional().describe('Replacement dataset ID. Only valid on a table-backed source.'),
|
|
1180
|
+
table: z.string().optional().describe('Replacement table ID. Only valid on a table-backed source.'),
|
|
1181
|
+
tableProject: z.string().optional().describe('Replacement project owning the table. Only valid on a table-backed source.'),
|
|
1182
|
+
account: accountParam,
|
|
1183
|
+
},
|
|
1184
|
+
}, async ({ spreadsheetId, dataSourceId, billingProject, query, dataset, table, tableProject, account }) => {
|
|
1185
|
+
const tableFlags = tableProject !== undefined || dataset !== undefined || table !== undefined;
|
|
1186
|
+
if (query !== undefined && tableFlags) {
|
|
1187
|
+
throw new Error('query and the table fields (dataset, table, tableProject) are mutually exclusive — a data source is backed by one or the other.');
|
|
1188
|
+
}
|
|
1189
|
+
if (billingProject === undefined && query === undefined && !tableFlags) {
|
|
1190
|
+
throw new Error('nothing to update: pass billingProject, query, dataset, table or tableProject.');
|
|
1191
|
+
}
|
|
1192
|
+
const args = ['sheets', 'datasource', 'update', spreadsheetId, dataSourceId];
|
|
1193
|
+
if (billingProject !== undefined) args.push(`--billing-project=${billingProject}`);
|
|
1194
|
+
if (query !== undefined) args.push(`--query=${query}`);
|
|
1195
|
+
if (tableProject !== undefined) args.push(`--table-project=${tableProject}`);
|
|
1196
|
+
if (dataset !== undefined) args.push(`--dataset=${dataset}`);
|
|
1197
|
+
if (table !== undefined) args.push(`--table=${table}`);
|
|
1198
|
+
return runOrDiagnose(args, { account });
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
// Refresh is why MIN_GOG_VERSION is 0.38.1 (openclaw/gogcli#1037). On 0.38.0
|
|
1202
|
+
// a batchUpdate that came back with no RefreshDataSource reply produced an
|
|
1203
|
+
// empty status list and exit 0 — this tool would report a clean success for a
|
|
1204
|
+
// billable execution that may or may not have started, and the caller has no
|
|
1205
|
+
// way to tell the two apart. 0.38.1 fails instead, telling the caller to
|
|
1206
|
+
// inspect the source before retrying, which is the behaviour the description
|
|
1207
|
+
// below promises.
|
|
1208
|
+
server.registerTool('gog_sheets_datasource_refresh', {
|
|
1209
|
+
description:
|
|
1210
|
+
'Re-run a Connected Sheets data source so its sheet and extracts pick up current BigQuery data. Google refuses to ' +
|
|
1211
|
+
'refresh a source whose previous execution FAILED; forceRefresh overrides that, which is what you want after fixing ' +
|
|
1212
|
+
'the underlying query or permissions.' + bigQueryChargeNote + bigQueryScopeNote,
|
|
1213
|
+
inputSchema: {
|
|
1214
|
+
spreadsheetId: z.string().describe('Spreadsheet ID'),
|
|
1215
|
+
dataSourceId: z.string().describe('Data source ID, as reported by gog_sheets_datasource_list'),
|
|
1216
|
+
forceRefresh: z.boolean().optional().describe('Refresh even when the previous execution failed'),
|
|
1217
|
+
account: accountParam,
|
|
1218
|
+
},
|
|
1219
|
+
}, async ({ spreadsheetId, dataSourceId, forceRefresh, account }) => {
|
|
1220
|
+
const args = ['sheets', 'datasource', 'refresh', spreadsheetId, dataSourceId];
|
|
1221
|
+
if (forceRefresh) args.push('--force-refresh');
|
|
1222
|
+
return runOrDiagnose(args, { account });
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
server.registerTool('gog_sheets_datasource_delete', {
|
|
1226
|
+
description:
|
|
1227
|
+
'Delete a Connected Sheets data source. This is WIDER than it sounds: it also deletes the source\'s linked ' +
|
|
1228
|
+
'DATA_SOURCE sheet, and unlinks every extract, chart and pivot table built on it elsewhere in the spreadsheet — ' +
|
|
1229
|
+
'objects on other tabs that the caller never named. Take gog_sheets_snapshot first if any of that matters. The ' +
|
|
1230
|
+
'BigQuery data itself is untouched; only the spreadsheet\'s connection to it goes away.' + bigQueryScopeNote,
|
|
1231
|
+
annotations: { destructiveHint: true },
|
|
1232
|
+
inputSchema: {
|
|
1233
|
+
spreadsheetId: z.string().describe('Spreadsheet ID'),
|
|
1234
|
+
dataSourceId: z.string().describe('Data source ID, as reported by gog_sheets_datasource_list'),
|
|
1235
|
+
account: accountParam,
|
|
1236
|
+
},
|
|
1237
|
+
}, async ({ spreadsheetId, dataSourceId, account }) => {
|
|
1238
|
+
// gog gates this delete behind a confirmation; the runner injects
|
|
1239
|
+
// --no-input, so without --force it refuses at runtime. Verified live
|
|
1240
|
+
// against gog 0.38.0: without --force it answers "refusing to delete
|
|
1241
|
+
// Connected Sheets data source … without --force (non-interactive)".
|
|
1242
|
+
return runOrDiagnose(['sheets', 'datasource', 'delete', spreadsheetId, dataSourceId, '--force'], { account });
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1114
1245
|
}
|
|
@@ -1611,3 +1611,182 @@ describe('gog_sheets_datasource_table_read', () => {
|
|
|
1611
1611
|
expect(lib.runOrDiagnose).not.toHaveBeenCalled();
|
|
1612
1612
|
});
|
|
1613
1613
|
});
|
|
1614
|
+
|
|
1615
|
+
describe('gog_sheets_datasource_add', () => {
|
|
1616
|
+
it('adds a query-backed source', async () => {
|
|
1617
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
1618
|
+
const harness = await setupHandlers();
|
|
1619
|
+
await harness.callTool('gog_sheets_datasource_add', {
|
|
1620
|
+
spreadsheetId: 'sid', billingProject: 'my-proj', query: 'SELECT 1',
|
|
1621
|
+
});
|
|
1622
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1623
|
+
['sheets', 'datasource', 'add', 'sid', '--billing-project=my-proj', '--query=SELECT 1'],
|
|
1624
|
+
{ account: undefined },
|
|
1625
|
+
);
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
it('adds a table-backed source, including the optional table project', async () => {
|
|
1629
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
1630
|
+
const harness = await setupHandlers();
|
|
1631
|
+
await harness.callTool('gog_sheets_datasource_add', {
|
|
1632
|
+
spreadsheetId: 'sid', billingProject: 'billing', dataset: 'ds', table: 'tbl', tableProject: 'owner',
|
|
1633
|
+
});
|
|
1634
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1635
|
+
['sheets', 'datasource', 'add', 'sid', '--billing-project=billing', '--table-project=owner', '--dataset=ds', '--table=tbl'],
|
|
1636
|
+
{ account: undefined },
|
|
1637
|
+
);
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
it('refuses a call that mixes a query with table flags before spawning gog', async () => {
|
|
1641
|
+
const harness = await setupHandlers();
|
|
1642
|
+
const result = await harness.callTool('gog_sheets_datasource_add', {
|
|
1643
|
+
spreadsheetId: 'sid', billingProject: 'p', query: 'SELECT 1', dataset: 'ds', table: 'tbl',
|
|
1644
|
+
});
|
|
1645
|
+
expect(result.isError).toBe(true);
|
|
1646
|
+
expect(result.content[0].text).toContain('mutually exclusive');
|
|
1647
|
+
expect(lib.runOrDiagnose).not.toHaveBeenCalled();
|
|
1648
|
+
});
|
|
1649
|
+
|
|
1650
|
+
it('refuses a call that names neither a query nor a complete table', async () => {
|
|
1651
|
+
const harness = await setupHandlers();
|
|
1652
|
+
const result = await harness.callTool('gog_sheets_datasource_add', {
|
|
1653
|
+
spreadsheetId: 'sid', billingProject: 'p', dataset: 'ds',
|
|
1654
|
+
});
|
|
1655
|
+
expect(result.isError).toBe(true);
|
|
1656
|
+
expect(result.content[0].text).toContain('--dataset and --table');
|
|
1657
|
+
expect(lib.runOrDiagnose).not.toHaveBeenCalled();
|
|
1658
|
+
});
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
describe('gog_sheets_datasource_update', () => {
|
|
1662
|
+
it('sends only the fields the caller supplied', async () => {
|
|
1663
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
1664
|
+
const harness = await setupHandlers();
|
|
1665
|
+
await harness.callTool('gog_sheets_datasource_update', {
|
|
1666
|
+
spreadsheetId: 'sid', dataSourceId: 'ds1', query: 'SELECT 2',
|
|
1667
|
+
});
|
|
1668
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1669
|
+
['sheets', 'datasource', 'update', 'sid', 'ds1', '--query=SELECT 2'],
|
|
1670
|
+
{ account: undefined },
|
|
1671
|
+
);
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
it('updates the billing project and table fields together', async () => {
|
|
1675
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
1676
|
+
const harness = await setupHandlers();
|
|
1677
|
+
await harness.callTool('gog_sheets_datasource_update', {
|
|
1678
|
+
spreadsheetId: 'sid', dataSourceId: 'ds1', billingProject: 'new-proj', dataset: 'ds2', table: 't2',
|
|
1679
|
+
});
|
|
1680
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1681
|
+
['sheets', 'datasource', 'update', 'sid', 'ds1', '--billing-project=new-proj', '--dataset=ds2', '--table=t2'],
|
|
1682
|
+
{ account: undefined },
|
|
1683
|
+
);
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
it('moves a table-backed source to another owning project', async () => {
|
|
1687
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
1688
|
+
const harness = await setupHandlers();
|
|
1689
|
+
await harness.callTool('gog_sheets_datasource_update', {
|
|
1690
|
+
spreadsheetId: 'sid', dataSourceId: 'ds1', tableProject: 'other-owner',
|
|
1691
|
+
});
|
|
1692
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1693
|
+
['sheets', 'datasource', 'update', 'sid', 'ds1', '--table-project=other-owner'],
|
|
1694
|
+
{ account: undefined },
|
|
1695
|
+
);
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
it('refuses a call that mixes a query with table flags', async () => {
|
|
1699
|
+
const harness = await setupHandlers();
|
|
1700
|
+
const result = await harness.callTool('gog_sheets_datasource_update', {
|
|
1701
|
+
spreadsheetId: 'sid', dataSourceId: 'ds1', query: 'SELECT 2', table: 't2',
|
|
1702
|
+
});
|
|
1703
|
+
expect(result.isError).toBe(true);
|
|
1704
|
+
expect(result.content[0].text).toContain('mutually exclusive');
|
|
1705
|
+
expect(lib.runOrDiagnose).not.toHaveBeenCalled();
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
it('refuses a call with nothing to update', async () => {
|
|
1709
|
+
const harness = await setupHandlers();
|
|
1710
|
+
const result = await harness.callTool('gog_sheets_datasource_update', { spreadsheetId: 'sid', dataSourceId: 'ds1' });
|
|
1711
|
+
expect(result.isError).toBe(true);
|
|
1712
|
+
expect(result.content[0].text).toContain('nothing to update');
|
|
1713
|
+
expect(lib.runOrDiagnose).not.toHaveBeenCalled();
|
|
1714
|
+
});
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
describe('gog_sheets_datasource_refresh', () => {
|
|
1718
|
+
it('requests a refresh', async () => {
|
|
1719
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
1720
|
+
const harness = await setupHandlers();
|
|
1721
|
+
await harness.callTool('gog_sheets_datasource_refresh', { spreadsheetId: 'sid', dataSourceId: 'ds1' });
|
|
1722
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1723
|
+
['sheets', 'datasource', 'refresh', 'sid', 'ds1'],
|
|
1724
|
+
{ account: undefined },
|
|
1725
|
+
);
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
it('passes --force-refresh after a failed execution', async () => {
|
|
1729
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
1730
|
+
const harness = await setupHandlers();
|
|
1731
|
+
await harness.callTool('gog_sheets_datasource_refresh', {
|
|
1732
|
+
spreadsheetId: 'sid', dataSourceId: 'ds1', forceRefresh: true,
|
|
1733
|
+
});
|
|
1734
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1735
|
+
['sheets', 'datasource', 'refresh', 'sid', 'ds1', '--force-refresh'],
|
|
1736
|
+
{ account: undefined },
|
|
1737
|
+
);
|
|
1738
|
+
});
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
describe('gog_sheets_datasource_delete', () => {
|
|
1742
|
+
// Every tool a description NAMES has to exist in this package, and a name
|
|
1743
|
+
// that does not is worse than no advice: the model calls it, gets "unknown
|
|
1744
|
+
// tool", and may skip the step the description was insisting on — here, the
|
|
1745
|
+
// backup before an unlink-everything delete. gog_sheets_backup was such a
|
|
1746
|
+
// name; the snapshot tool is gog_sheets_snapshot. Checked across the WHOLE
|
|
1747
|
+
// package (index.ts composes auth + base sheets + these extras), because a
|
|
1748
|
+
// description may legitimately point at a tool another registrar supplies.
|
|
1749
|
+
it('names only tools this package actually registers', async () => {
|
|
1750
|
+
const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
|
|
1751
|
+
const server = new McpServer({ name: 'test', version: '0.0.0' });
|
|
1752
|
+
const configs = new Map<string, { description?: string }>();
|
|
1753
|
+
vi.spyOn(server, 'registerTool').mockImplementation((name, config) => {
|
|
1754
|
+
configs.set(name, config as { description?: string });
|
|
1755
|
+
return undefined as never;
|
|
1756
|
+
});
|
|
1757
|
+
lib.authToolsFor('sheets')(server);
|
|
1758
|
+
lib.registerSheetsTools(server);
|
|
1759
|
+
registerExtraSheetsTools(server);
|
|
1760
|
+
const names = [...configs.keys()];
|
|
1761
|
+
for (const [tool, config] of configs) {
|
|
1762
|
+
for (const [referenced] of (config.description ?? '').matchAll(/gog_[a-z_0-9]+/g)) {
|
|
1763
|
+
expect(names, `${tool} references ${referenced}`).toContain(referenced);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
// gog gates this behind a confirmation and the runner always injects
|
|
1769
|
+
// --no-input, so a missing --force fails at runtime against a real gog while
|
|
1770
|
+
// every mocked test still passes. Confirmed live on gog 0.38.0.
|
|
1771
|
+
it('appends --force so the confirmation gate does not refuse it', async () => {
|
|
1772
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
1773
|
+
const harness = await setupHandlers();
|
|
1774
|
+
await harness.callTool('gog_sheets_datasource_delete', { spreadsheetId: 'sid', dataSourceId: 'ds1' });
|
|
1775
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1776
|
+
['sheets', 'datasource', 'delete', 'sid', 'ds1', '--force'],
|
|
1777
|
+
{ account: undefined },
|
|
1778
|
+
);
|
|
1779
|
+
});
|
|
1780
|
+
|
|
1781
|
+
it('passes the account through', async () => {
|
|
1782
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
1783
|
+
const harness = await setupHandlers();
|
|
1784
|
+
await harness.callTool('gog_sheets_datasource_delete', {
|
|
1785
|
+
spreadsheetId: 'sid', dataSourceId: 'ds1', account: 'me@x.com',
|
|
1786
|
+
});
|
|
1787
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
1788
|
+
['sheets', 'datasource', 'delete', 'sid', 'ds1', '--force'],
|
|
1789
|
+
{ account: 'me@x.com' },
|
|
1790
|
+
);
|
|
1791
|
+
});
|
|
1792
|
+
});
|