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.
- package/LICENSE +21 -0
- package/README.md +210 -0
- package/docs/setup-google.md +122 -0
- package/docs/setup-oauth2.md +177 -0
- package/examples/claude-mcp.json +12 -0
- package/examples/codex-mcp.json +12 -0
- package/examples/cursor-mcp.json +12 -0
- package/examples/vscode-mcp.json +13 -0
- package/package.json +53 -0
- package/src/cli/cli.mjs +87 -0
- package/src/cli/commands/append.mjs +55 -0
- package/src/cli/commands/config.mjs +93 -0
- package/src/cli/commands/create.mjs +45 -0
- package/src/cli/commands/helpers/show-mcp-config.mjs +71 -0
- package/src/cli/commands/init.mjs +415 -0
- package/src/cli/commands/list.mjs +50 -0
- package/src/cli/commands/read.mjs +64 -0
- package/src/cli/commands/test.mjs +99 -0
- package/src/cli/commands/token-status.mjs +114 -0
- package/src/config/config.mjs +166 -0
- package/src/server/oauth2-client.mjs +155 -0
- package/src/server/server.mjs +524 -0
- package/src/server/sheets-client.mjs +106 -0
|
@@ -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
|
+
}
|