google-sheet-mcp 1.0.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.
@@ -0,0 +1,415 @@
1
+ /**
2
+ * init — Interactive setup wizard.
3
+ *
4
+ * Two auth modes:
5
+ * service-account (default): JSON key + share sheet
6
+ * oauth: OAuth2 browser flow → refresh token
7
+ *
8
+ * OAuth2 flow:
9
+ * 1. User pastes client_id + client_secret
10
+ * 2. CLI prints auth URL
11
+ * 3. User opens URL, grants access, copies code
12
+ * 4. CLI exchanges code for refresh_token
13
+ * 5. Token auto-refreshes forever
14
+ */
15
+
16
+ import chalk from "chalk";
17
+ import ora from "ora";
18
+ import inquirer from "inquirer";
19
+ import http from "http";
20
+ import { URL } from "url";
21
+ import { loadConfig, saveConfig, extractSpreadsheetId } from "../../config/config.mjs";
22
+ import {
23
+ createServiceAccountSheetsClient,
24
+ createOAuth2SheetsClient,
25
+ } from "../../server/sheets-client.mjs";
26
+ import {
27
+ generateAuthUrl,
28
+ exchangeCodeForTokens,
29
+ } from "../../server/oauth2-client.mjs";
30
+ import { showMCPConfig } from "./helpers/show-mcp-config.mjs";
31
+
32
+ export async function initCommand(options) {
33
+ const authType = options.auth || "service-account";
34
+ const isOAuth = authType === "oauth";
35
+
36
+ const authLabel = isOAuth
37
+ ? "OAuth2 (Personal Google Account)"
38
+ : "Service Account";
39
+
40
+ console.log(chalk.bold.cyan(`\nšŸ”— Google Sheet MCP — Setup Wizard`));
41
+ console.log(chalk.gray(` Auth mode: ${authLabel}`));
42
+ console.log();
43
+
44
+ // Check for existing config
45
+ const existing = loadConfig();
46
+ if (existing) {
47
+ console.log(
48
+ chalk.yellow(
49
+ `āš ļø Already configured (${existing.source}): ${existing.spreadsheetId} (${existing.authType || "service-account"})`
50
+ )
51
+ );
52
+ const { overwrite } = await inquirer.prompt([
53
+ {
54
+ type: "confirm",
55
+ name: "overwrite",
56
+ message: "Overwrite existing configuration?",
57
+ default: false,
58
+ },
59
+ ]);
60
+ if (!overwrite) {
61
+ console.log(chalk.gray(" Setup cancelled."));
62
+ return;
63
+ }
64
+ console.log();
65
+ }
66
+
67
+ // Step 1: Google Sheet URL
68
+ console.log(chalk.bold("Step 1/4: Google Sheet"));
69
+ const { sheetUrl } = await inquirer.prompt([
70
+ {
71
+ type: "input",
72
+ name: "sheetUrl",
73
+ message: "Paste Google Sheet URL:",
74
+ validate: (input) => {
75
+ const id = extractSpreadsheetId(input);
76
+ if (!id)
77
+ return "Invalid Google Sheet URL. Expected: https://docs.google.com/spreadsheets/d/<ID>/edit";
78
+ return true;
79
+ },
80
+ },
81
+ ]);
82
+ const spreadsheetId = extractSpreadsheetId(sheetUrl);
83
+ console.log(chalk.green(` āœ… Extracted ID: ${spreadsheetId}`));
84
+ console.log();
85
+
86
+ if (isOAuth) {
87
+ // ─── OAuth2 Flow ──────────────────────────────────────────────────────
88
+ await setupOAuth2(spreadsheetId);
89
+ } else {
90
+ // ─── Service Account Flow (default) ───────────────────────────────────
91
+ await setupServiceAccount(spreadsheetId);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Service Account setup.
97
+ */
98
+ async function setupServiceAccount(spreadsheetId) {
99
+ console.log(chalk.bold("Step 2/4: Service Account Key"));
100
+ console.log(
101
+ chalk.gray(" If you haven't created one yet, see: docs/setup-google.md")
102
+ );
103
+ const { credentialsPath } = await inquirer.prompt([
104
+ {
105
+ type: "input",
106
+ name: "credentialsPath",
107
+ message: "Path to service account JSON key:",
108
+ default: "./credentials.json",
109
+ },
110
+ ]);
111
+ console.log(chalk.green(` āœ… Credentials: ${credentialsPath}`));
112
+ console.log();
113
+
114
+ // Step 3: Verify connection
115
+ console.log(chalk.bold("Step 3/4: Verify connection"));
116
+ const spinner = ora("Connecting to Google Sheets...").start();
117
+
118
+ try {
119
+ const sheets = createServiceAccountSheetsClient({ credentialsPath });
120
+ const res = await sheets.spreadsheets.get({
121
+ spreadsheetId,
122
+ fields: "properties.title,sheets.properties",
123
+ });
124
+
125
+ const title = res.data.properties.title;
126
+ const sheetList = (res.data.sheets ?? []).map(
127
+ (s) => s.properties?.title || "(unnamed)"
128
+ );
129
+
130
+ spinner.succeed(`Connected to "${chalk.bold(title)}"`);
131
+ console.log(
132
+ chalk.green(
133
+ ` Found ${sheetList.length} sheet(s): ${sheetList.join(", ")}`
134
+ )
135
+ );
136
+ console.log();
137
+
138
+ // Save config
139
+ const configPath = saveConfig({
140
+ spreadsheetId,
141
+ authType: "service-account",
142
+ credentialsPath,
143
+ sheets: sheetList,
144
+ });
145
+
146
+ printSuccess(configPath, sheetList);
147
+ } catch (err) {
148
+ spinner.fail(`Connection failed: ${err.message}`);
149
+ printServiceAccountTroubleshooting();
150
+ }
151
+ }
152
+
153
+ /**
154
+ * OAuth2 setup flow.
155
+ */
156
+ async function setupOAuth2(spreadsheetId) {
157
+ console.log(chalk.bold("Step 2/4: OAuth2 Credentials"));
158
+ console.log(
159
+ chalk.gray(
160
+ " Get these from Google Cloud Console → APIs & Services → Credentials → Create OAuth client ID → Desktop app."
161
+ )
162
+ );
163
+
164
+ const { clientId, clientSecret } = await inquirer.prompt([
165
+ {
166
+ type: "input",
167
+ name: "clientId",
168
+ message: "Client ID:",
169
+ validate: (input) =>
170
+ input.length > 10
171
+ ? true
172
+ : "Invalid Client ID. Should be a long string ending with .apps.googleusercontent.com",
173
+ },
174
+ {
175
+ type: "input",
176
+ name: "clientSecret",
177
+ message: "Client Secret:",
178
+ validate: (input) =>
179
+ input.length > 5 ? true : "Invalid Client Secret.",
180
+ },
181
+ ]);
182
+
183
+ console.log(chalk.green(` āœ… Credentials accepted`));
184
+ console.log();
185
+
186
+ // Step 3: Browser OAuth flow
187
+ console.log(chalk.bold("Step 3/4: Authorize Access"));
188
+
189
+ const authUrl = generateAuthUrl(clientId, clientSecret);
190
+ console.log();
191
+ console.log(
192
+ chalk.white(" 1. Open this URL in your browser:")
193
+ );
194
+ console.log(chalk.cyan(` ${authUrl}`));
195
+ console.log();
196
+ console.log(
197
+ chalk.white(
198
+ " 2. Sign in with your Google account"
199
+ )
200
+ );
201
+ console.log(
202
+ chalk.white(
203
+ " 3. Grant access to Google Sheets"
204
+ )
205
+ );
206
+ console.log(
207
+ chalk.white(
208
+ " 4. You'll be redirected to localhost — copy the 'code' parameter from the URL"
209
+ )
210
+ );
211
+ console.log();
212
+
213
+ // Try to auto-capture the code via local HTTP server
214
+ const code = await captureOAuthCode();
215
+
216
+ if (!code) {
217
+ // Fallback: manual entry
218
+ const { manualCode } = await inquirer.prompt([
219
+ {
220
+ type: "input",
221
+ name: "manualCode",
222
+ message:
223
+ "Paste the authorization code from the redirect URL (?code=...):",
224
+ validate: (input) =>
225
+ input.length > 5 ? true : "Code seems too short.",
226
+ },
227
+ ]);
228
+ return await completeOAuthSetup(spreadsheetId, clientId, clientSecret, manualCode);
229
+ }
230
+
231
+ return await completeOAuthSetup(spreadsheetId, clientId, clientSecret, code);
232
+ }
233
+
234
+ /**
235
+ * Start a local HTTP server to capture the OAuth redirect.
236
+ * Returns the authorization code or null (if user wants manual entry).
237
+ */
238
+ function captureOAuthCode() {
239
+ return new Promise((resolve) => {
240
+ const server = http.createServer((req, res) => {
241
+ const parsed = new URL(req.url, "http://localhost:3000");
242
+ const code = parsed.searchParams.get("code");
243
+
244
+ if (code) {
245
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
246
+ res.end(`
247
+ <html>
248
+ <body style="font-family: sans-serif; text-align: center; padding-top: 60px;">
249
+ <h1>āœ… Authorization Successful</h1>
250
+ <p>You can close this tab and return to the terminal.</p>
251
+ </body>
252
+ </html>
253
+ `);
254
+ server.close();
255
+ resolve(code);
256
+ } else {
257
+ const error = parsed.searchParams.get("error");
258
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
259
+ res.end(`
260
+ <html>
261
+ <body style="font-family: sans-serif; text-align: center; padding-top: 60px;">
262
+ <h1>āŒ Authorization Failed</h1>
263
+ <p>Error: ${error || "unknown"}</p>
264
+ </body>
265
+ </html>
266
+ `);
267
+ server.close();
268
+ resolve(null);
269
+ }
270
+ });
271
+
272
+ server.listen(3000, () => {
273
+ console.log(
274
+ chalk.gray(" Waiting for browser redirect on http://localhost:3000 ...")
275
+ );
276
+ });
277
+
278
+ // Timeout after 2 minutes
279
+ setTimeout(() => {
280
+ server.close();
281
+ resolve(null);
282
+ }, 120_000);
283
+ });
284
+ }
285
+
286
+ /**
287
+ * Exchange code for tokens and test connection.
288
+ */
289
+ async function completeOAuthSetup(spreadsheetId, clientId, clientSecret, code) {
290
+ console.log();
291
+ const exchangeSpinner = ora("Exchanging code for tokens...").start();
292
+
293
+ let tokens;
294
+ try {
295
+ tokens = await exchangeCodeForTokens(clientId, clientSecret, code);
296
+ exchangeSpinner.succeed("Tokens obtained");
297
+ } catch (err) {
298
+ exchangeSpinner.fail(`Failed: ${err.message}`);
299
+ console.log(
300
+ chalk.yellow(
301
+ " The authorization code may have expired. Run the init command again."
302
+ )
303
+ );
304
+ return;
305
+ }
306
+
307
+ if (!tokens.refresh_token) {
308
+ console.log(
309
+ chalk.red(
310
+ " āŒ No refresh token returned. Try again — make sure to approve all permissions."
311
+ )
312
+ );
313
+ return;
314
+ }
315
+
316
+ console.log(
317
+ chalk.green(
318
+ ` āœ… Refresh token: ${tokens.refresh_token.substring(0, 12)}...`
319
+ )
320
+ );
321
+ console.log();
322
+
323
+ // Step 4: Test connection
324
+ console.log(chalk.bold("Step 4/4: Verify connection"));
325
+ const spinner = ora("Connecting to Google Sheets...").start();
326
+
327
+ try {
328
+ const oauth2Config = {
329
+ client_id: clientId,
330
+ client_secret: clientSecret,
331
+ refresh_token: tokens.refresh_token,
332
+ };
333
+
334
+ const sheets = createOAuth2SheetsClient({ oauth2: oauth2Config });
335
+ const res = await sheets.spreadsheets.get({
336
+ spreadsheetId,
337
+ fields: "properties.title,sheets.properties",
338
+ });
339
+
340
+ const title = res.data.properties.title;
341
+ const sheetList = (res.data.sheets ?? []).map(
342
+ (s) => s.properties?.title || "(unnamed)"
343
+ );
344
+
345
+ spinner.succeed(`Connected to "${chalk.bold(title)}" (as you)`);
346
+ console.log(
347
+ chalk.green(
348
+ ` Found ${sheetList.length} sheet(s): ${sheetList.join(", ")}`
349
+ )
350
+ );
351
+ console.log();
352
+
353
+ // Save config
354
+ const configPath = saveConfig({
355
+ spreadsheetId,
356
+ authType: "oauth2",
357
+ oauth2: oauth2Config,
358
+ sheets: sheetList,
359
+ });
360
+
361
+ printSuccess(configPath, sheetList);
362
+ } catch (err) {
363
+ spinner.fail(`Connection failed: ${err.message}`);
364
+ printOAuthTroubleshooting();
365
+ }
366
+ }
367
+
368
+ function printSuccess(configPath, sheetList) {
369
+ console.log(chalk.green(`āœ… Configuration saved to ${configPath}`));
370
+ console.log();
371
+
372
+ console.log(chalk.bold("šŸš€ You're ready!"));
373
+ console.log();
374
+ console.log(" Test connection:");
375
+ console.log(chalk.cyan(" npx google-sheet-mcp test"));
376
+ console.log();
377
+ console.log(" Check token health (OAuth2 only):");
378
+ console.log(chalk.cyan(" npx google-sheet-mcp token-status"));
379
+ console.log();
380
+ console.log(" List sheets:");
381
+ console.log(chalk.cyan(" npx google-sheet-mcp list"));
382
+ console.log();
383
+ console.log(" Read data:");
384
+ console.log(
385
+ chalk.cyan(` npx google-sheet-mcp read -s "${sheetList[0]}"`)
386
+ );
387
+ console.log();
388
+
389
+ showMCPConfig();
390
+ }
391
+
392
+ function printServiceAccountTroubleshooting() {
393
+ console.log();
394
+ console.log(chalk.yellow("Common issues:"));
395
+ console.log(" 1. Did you share the sheet with the service account email?");
396
+ console.log(
397
+ " 2. Is the Sheets API enabled in your Google Cloud project?"
398
+ );
399
+ console.log(" 3. Is the JSON key file path correct?");
400
+ console.log();
401
+ console.log(chalk.gray(" Run the command again to retry."));
402
+ }
403
+
404
+ function printOAuthTroubleshooting() {
405
+ console.log();
406
+ console.log(chalk.yellow("Common issues:"));
407
+ console.log(" 1. Do you have access to this spreadsheet?");
408
+ console.log(" 2. Did you grant the full spreadsheets scope?");
409
+ console.log(
410
+ " 3. Is the Sheets API enabled in your Google Cloud project?"
411
+ );
412
+ console.log(" 4. Try running `npx google-sheet-mcp token-status`");
413
+ console.log();
414
+ console.log(chalk.gray(" Run the command again to retry."));
415
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * list — Display all sheets in the connected spreadsheet.
3
+ */
4
+
5
+ import chalk from "chalk";
6
+ import ora from "ora";
7
+ import { loadConfig } from "../../config/config.mjs";
8
+ import { createSheetsClientFromConfig } from "../../server/sheets-client.mjs";
9
+
10
+ export async function listCommand() {
11
+ const config = loadConfig();
12
+ if (!config) {
13
+ console.error(chalk.red("āŒ No configuration found. Run `npx google-sheet-mcp init` first."));
14
+ process.exit(1);
15
+ }
16
+
17
+ const spinner = ora("Fetching sheets...").start();
18
+
19
+ try {
20
+ const sheets = createSheetsClientFromConfig(config);
21
+ const res = await sheets.spreadsheets.get({
22
+ spreadsheetId: config.spreadsheetId,
23
+ fields: "properties.title,sheets.properties",
24
+ });
25
+
26
+ spinner.succeed(`"${res.data.properties.title}"`);
27
+ console.log();
28
+
29
+ const sheetList = res.data.sheets ?? [];
30
+ sheetList.forEach((s) => {
31
+ const p = s.properties || {};
32
+ const title = p.title || "(unnamed)";
33
+ const rows = p.gridProperties?.rowCount || "?";
34
+ const cols = p.gridProperties?.columnCount || "?";
35
+ console.log(
36
+ chalk.green(` šŸ“„ ${title}`) +
37
+ chalk.gray(` ${rows}r Ɨ ${cols}c`)
38
+ );
39
+ });
40
+
41
+ console.log();
42
+ console.log(chalk.gray(` Total: ${sheetList.length} sheet(s)`));
43
+ console.log();
44
+ console.log(` Read data: ${chalk.cyan(`npx google-sheet-mcp read -s "${sheetList[0]?.properties?.title || 'Sheet1'}"`)}`);
45
+
46
+ } catch (err) {
47
+ spinner.fail(`Failed: ${err.message}`);
48
+ process.exit(1);
49
+ }
50
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * read — Read data from a specific sheet.
3
+ */
4
+
5
+ import chalk from "chalk";
6
+ import ora from "ora";
7
+ import { loadConfig } from "../../config/config.mjs";
8
+ import { createSheetsClientFromConfig } from "../../server/sheets-client.mjs";
9
+
10
+ export async function readCommand(options) {
11
+ const config = loadConfig();
12
+ if (!config) {
13
+ console.error(chalk.red("āŒ No configuration found. Run `npx google-sheet-mcp init` first."));
14
+ process.exit(1);
15
+ }
16
+
17
+ const range = options.range
18
+ ? `${options.sheet}!${options.range}`
19
+ : `${options.sheet}`;
20
+
21
+ const spinner = ora(`Reading "${range}"...`).start();
22
+
23
+ try {
24
+ const sheets = createSheetsClientFromConfig(config);
25
+ const res = await sheets.spreadsheets.values.get({
26
+ spreadsheetId: config.spreadsheetId,
27
+ range,
28
+ valueRenderOption: "FORMATTED_VALUE",
29
+ dateTimeRenderOption: "FORMATTED_STRING",
30
+ });
31
+
32
+ const rows = res.data.values || [];
33
+ spinner.succeed(`Read ${rows.length} rows from "${range.split("!")[0]}"`);
34
+
35
+ if (rows.length === 0) {
36
+ console.log(chalk.yellow(" (empty sheet)"));
37
+ return;
38
+ }
39
+
40
+ if (options.raw) {
41
+ // Raw 2D array
42
+ rows.forEach((row) => {
43
+ console.log(JSON.stringify(row));
44
+ });
45
+ } else {
46
+ // First row = headers, rest = objects
47
+ const [headers, ...data] = rows;
48
+ data.forEach((row) => {
49
+ const obj = {};
50
+ headers.forEach((h, j) => {
51
+ obj[h] = row[j] ?? "";
52
+ });
53
+ console.log(JSON.stringify(obj));
54
+ });
55
+ }
56
+
57
+ console.log();
58
+ console.log(chalk.gray(` Total: ${options.raw ? rows.length : rows.length - 1} data row(s)`));
59
+
60
+ } catch (err) {
61
+ spinner.fail(`Failed to read: ${err.message}`);
62
+ process.exit(1);
63
+ }
64
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * test — Verify connection to the configured sheet.
3
+ */
4
+
5
+ import chalk from "chalk";
6
+ import ora from "ora";
7
+ import { loadConfig } from "../../config/config.mjs";
8
+ import { createSheetsClientFromConfig } from "../../server/sheets-client.mjs";
9
+
10
+ export async function testCommand(options) {
11
+ const config = loadConfig();
12
+ if (!config) {
13
+ console.error(
14
+ chalk.red(
15
+ "āŒ No configuration found.\n" +
16
+ ' Run "npx google-sheet-mcp init" first, or set env vars:\n' +
17
+ " Service Account: GOOGLE_SPREADSHEET_ID + GOOGLE_APPLICATION_CREDENTIALS\n" +
18
+ " OAuth2: GOOGLE_SPREADSHEET_ID + GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET + GOOGLE_REFRESH_TOKEN"
19
+ )
20
+ );
21
+ process.exit(1);
22
+ }
23
+
24
+ console.log(chalk.bold.cyan("\nšŸ”— Google Sheet MCP — Connection Test\n"));
25
+ console.log(chalk.gray(` Config source: ${config.source}`));
26
+ console.log(chalk.gray(` Spreadsheet ID: ${config.spreadsheetId}`));
27
+ console.log(chalk.gray(` Auth type: ${config.authType || "service-account"}`));
28
+ if (config.authType === "service-account") {
29
+ console.log(chalk.gray(` Key file: ${config.credentialsPath}`));
30
+ } else {
31
+ console.log(chalk.gray(` Client ID: ${config.oauth2?.client_id?.substring(0, 16)}...`));
32
+ }
33
+ console.log();
34
+
35
+ const spinner = ora("Testing connection...").start();
36
+
37
+ try {
38
+ const sheets = createSheetsClientFromConfig(config);
39
+ const res = await sheets.spreadsheets.get({
40
+ spreadsheetId: config.spreadsheetId,
41
+ fields: "properties.title,sheets.properties",
42
+ });
43
+
44
+ const title = res.data.properties.title;
45
+ const sheetList = (res.data.sheets ?? []).map(
46
+ (s) => ({
47
+ title: s.properties?.title || "(unnamed)",
48
+ rows: s.properties?.gridProperties?.rowCount,
49
+ cols: s.properties?.gridProperties?.columnCount,
50
+ })
51
+ );
52
+
53
+ spinner.succeed(`Connected to "${chalk.bold(title)}"`);
54
+ console.log();
55
+
56
+ // If --sheet option, try reading it
57
+ if (options.sheet) {
58
+ const readSpinner = ora(`Reading sheet "${options.sheet}"...`).start();
59
+ try {
60
+ const data = await sheets.spreadsheets.values.get({
61
+ spreadsheetId: config.spreadsheetId,
62
+ range: `${options.sheet}!A1:Z5`,
63
+ });
64
+ const rows = data.data.values || [];
65
+ readSpinner.succeed(`Read ${rows.length} rows from "${options.sheet}"`);
66
+ console.log();
67
+ console.log(chalk.bold(" Preview:"));
68
+ rows.slice(0, 5).forEach((row, i) => {
69
+ console.log(chalk.gray(` ${i === 0 ? "→" : " "} ${row.slice(0, 6).join(" | ")}`));
70
+ });
71
+ } catch (e) {
72
+ readSpinner.fail(`Cannot read "${options.sheet}": ${e.message}`);
73
+ }
74
+ } else {
75
+ // List all sheets
76
+ console.log(chalk.bold("Sheets:"));
77
+ sheetList.forEach((s) => {
78
+ console.log(chalk.green(` āœ“ ${s.title}`) + chalk.gray(` (${s.rows} rows Ɨ ${s.cols} cols)`));
79
+ });
80
+ }
81
+
82
+ console.log();
83
+ console.log(chalk.green("āœ… Connection OK — ready to use from AI agents!"));
84
+
85
+ } catch (err) {
86
+ spinner.fail(`Connection failed: ${err.message}`);
87
+ console.log();
88
+ console.log(chalk.yellow("Troubleshooting:"));
89
+ if (config.authType === "oauth2") {
90
+ console.log(" 1. Run: npx google-sheet-mcp token-status (check token health)");
91
+ console.log(" 2. Re-run: npx google-sheet-mcp init --auth oauth (replace token)");
92
+ } else {
93
+ console.log(" 1. Is the service account email added as Editor on the sheet?");
94
+ console.log(" 2. Is the Sheets API enabled?");
95
+ }
96
+ console.log(" 3. Run: npx google-sheet-mcp config (to verify settings)");
97
+ process.exit(1);
98
+ }
99
+ }