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,524 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Google Sheet MCP Server
|
|
4
|
+
*
|
|
5
|
+
* stdio-based MCP server for AI agents.
|
|
6
|
+
* Launched by IDE (Cursor, VS Code, Claude, Codex) via npx.
|
|
7
|
+
*
|
|
8
|
+
* Auth: Service Account (JSON key) or OAuth2 (refresh token with auto-refresh).
|
|
9
|
+
* Config: .google-sheet-mcp.json or env vars.
|
|
10
|
+
*
|
|
11
|
+
* Tools:
|
|
12
|
+
* sheets_list_tabs — List all sheet tabs
|
|
13
|
+
* sheets_read_range — Read data from a range
|
|
14
|
+
* sheets_get_sheet — Get spreadsheet metadata
|
|
15
|
+
* sheets_write_range — Write values to a range
|
|
16
|
+
* sheets_create_tab — Create a new sheet tab
|
|
17
|
+
* sheets_append_row — Append a row (header-aware)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
21
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
|
+
import {
|
|
23
|
+
CallToolRequestSchema,
|
|
24
|
+
ListToolsRequestSchema,
|
|
25
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
26
|
+
import { readFileSync, existsSync } from "fs";
|
|
27
|
+
import { resolve } from "path";
|
|
28
|
+
import { homedir } from "os";
|
|
29
|
+
import { createSheetsClientFromConfig } from "./sheets-client.mjs";
|
|
30
|
+
|
|
31
|
+
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
const CONFIG_FILENAME = ".google-sheet-mcp.json";
|
|
34
|
+
|
|
35
|
+
function loadServerConfig() {
|
|
36
|
+
// 1. ENV vars — service account
|
|
37
|
+
const envId = process.env.GOOGLE_SPREADSHEET_ID;
|
|
38
|
+
if (!envId) return null;
|
|
39
|
+
|
|
40
|
+
const saCreds = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
41
|
+
if (saCreds) {
|
|
42
|
+
return { spreadsheetId: envId, authType: "service-account", credentialsPath: saCreds };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// OAuth2
|
|
46
|
+
const clientId = process.env.GOOGLE_CLIENT_ID;
|
|
47
|
+
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
|
|
48
|
+
const refreshToken = process.env.GOOGLE_REFRESH_TOKEN;
|
|
49
|
+
if (clientId && clientSecret && refreshToken) {
|
|
50
|
+
return {
|
|
51
|
+
spreadsheetId: envId,
|
|
52
|
+
authType: "oauth2",
|
|
53
|
+
oauth2: { client_id: clientId, client_secret: clientSecret, refresh_token: refreshToken },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function loadFileConfig() {
|
|
61
|
+
const paths = [
|
|
62
|
+
resolve(process.cwd(), CONFIG_FILENAME),
|
|
63
|
+
resolve(homedir(), CONFIG_FILENAME),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
for (const p of paths) {
|
|
67
|
+
if (existsSync(p)) {
|
|
68
|
+
try {
|
|
69
|
+
const c = JSON.parse(readFileSync(p, "utf-8"));
|
|
70
|
+
if (c.spreadsheetId && isValidConfig(c)) return c;
|
|
71
|
+
} catch { /* skip malformed */ }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isValidConfig(c) {
|
|
79
|
+
if (c.authType === "oauth2") {
|
|
80
|
+
return !!(c.oauth2?.client_id && c.oauth2?.client_secret && c.oauth2?.refresh_token);
|
|
81
|
+
}
|
|
82
|
+
return !!c.credentialsPath;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Sheets Client ────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
let _sheets = null;
|
|
88
|
+
let _config = null;
|
|
89
|
+
|
|
90
|
+
function getSheets() {
|
|
91
|
+
if (_sheets) return _sheets;
|
|
92
|
+
|
|
93
|
+
_config = loadServerConfig() || loadFileConfig();
|
|
94
|
+
if (!_config) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
"No configuration. Run `npx google-sheet-mcp init` or set env vars."
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_sheets = createSheetsClientFromConfig(_config);
|
|
101
|
+
return _sheets;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getSpreadsheetId() {
|
|
105
|
+
if (_config) return _config.spreadsheetId;
|
|
106
|
+
_config = loadServerConfig() || loadFileConfig();
|
|
107
|
+
return _config?.spreadsheetId;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
function rowsToObjects(rows, skipHeader = false) {
|
|
113
|
+
if (!rows || rows.length === 0) return [];
|
|
114
|
+
if (skipHeader) return rows;
|
|
115
|
+
const [headers, ...data] = rows;
|
|
116
|
+
return data.map((row) => {
|
|
117
|
+
const obj = {};
|
|
118
|
+
headers.forEach((h, i) => {
|
|
119
|
+
obj[h] = row[i] ?? "";
|
|
120
|
+
});
|
|
121
|
+
return obj;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── Tool implementations ────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
async function listTabs({ spreadsheet }) {
|
|
128
|
+
const sheets = getSheets();
|
|
129
|
+
const id = spreadsheet || getSpreadsheetId();
|
|
130
|
+
|
|
131
|
+
const res = await sheets.spreadsheets.get({
|
|
132
|
+
spreadsheetId: id,
|
|
133
|
+
fields: "properties.title,sheets.properties",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const tabs = (res.data.sheets ?? []).map((s) => ({
|
|
137
|
+
title: s.properties?.title || "(unnamed)",
|
|
138
|
+
sheetId: s.properties?.sheetId,
|
|
139
|
+
rowCount: s.properties?.gridProperties?.rowCount,
|
|
140
|
+
columnCount: s.properties?.gridProperties?.columnCount,
|
|
141
|
+
hidden: s.properties?.hidden ?? false,
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
spreadsheetId: id,
|
|
146
|
+
title: res.data.properties?.title,
|
|
147
|
+
tabs,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function readRange({
|
|
152
|
+
spreadsheet,
|
|
153
|
+
range,
|
|
154
|
+
skip_header = false,
|
|
155
|
+
value_render = "FORMATTED_VALUE",
|
|
156
|
+
}) {
|
|
157
|
+
const sheets = getSheets();
|
|
158
|
+
const id = spreadsheet || getSpreadsheetId();
|
|
159
|
+
|
|
160
|
+
const res = await sheets.spreadsheets.values.get({
|
|
161
|
+
spreadsheetId: id,
|
|
162
|
+
range,
|
|
163
|
+
valueRenderOption: value_render,
|
|
164
|
+
dateTimeRenderOption: "FORMATTED_STRING",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const rows = res.data.values;
|
|
168
|
+
const data = rowsToObjects(rows, skip_header);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
spreadsheetId: id,
|
|
172
|
+
range: res.data.range,
|
|
173
|
+
totalRows: rows ? rows.length : 0,
|
|
174
|
+
rows: data,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function getSheet({ spreadsheet }) {
|
|
179
|
+
const sheets = getSheets();
|
|
180
|
+
const id = spreadsheet || getSpreadsheetId();
|
|
181
|
+
|
|
182
|
+
const res = await sheets.spreadsheets.get({
|
|
183
|
+
spreadsheetId: id,
|
|
184
|
+
fields: "spreadsheetId,spreadsheetUrl,properties,sheets.properties",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const tabs = (res.data.sheets ?? []).map((s) => ({
|
|
188
|
+
title: s.properties?.title,
|
|
189
|
+
sheetId: s.properties?.sheetId,
|
|
190
|
+
rowCount: s.properties?.gridProperties?.rowCount,
|
|
191
|
+
columnCount: s.properties?.gridProperties?.columnCount,
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
spreadsheetId: res.data.spreadsheetId,
|
|
196
|
+
url: res.data.spreadsheetUrl,
|
|
197
|
+
title: res.data.properties?.title,
|
|
198
|
+
locale: res.data.properties?.locale,
|
|
199
|
+
tabs,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function writeRange({ spreadsheet, range, values }) {
|
|
204
|
+
const sheets = getSheets();
|
|
205
|
+
const id = spreadsheet || getSpreadsheetId();
|
|
206
|
+
|
|
207
|
+
const res = await sheets.spreadsheets.values.update({
|
|
208
|
+
spreadsheetId: id,
|
|
209
|
+
range,
|
|
210
|
+
valueInputOption: "USER_ENTERED",
|
|
211
|
+
requestBody: { values },
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
spreadsheetId: id,
|
|
216
|
+
updatedRange: res.data.updatedRange,
|
|
217
|
+
updatedRows: res.data.updatedRows,
|
|
218
|
+
updatedColumns: res.data.updatedColumns,
|
|
219
|
+
updatedCells: res.data.updatedCells,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function createTab({ spreadsheet, sheetName }) {
|
|
224
|
+
const sheets = getSheets();
|
|
225
|
+
const id = spreadsheet || getSpreadsheetId();
|
|
226
|
+
|
|
227
|
+
await sheets.spreadsheets.batchUpdate({
|
|
228
|
+
spreadsheetId: id,
|
|
229
|
+
requestBody: {
|
|
230
|
+
requests: [
|
|
231
|
+
{
|
|
232
|
+
addSheet: {
|
|
233
|
+
properties: { title: sheetName },
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
spreadsheetId: id,
|
|
242
|
+
sheetName,
|
|
243
|
+
created: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function appendRow({ spreadsheet, sheet, row }) {
|
|
248
|
+
const sheets = getSheets();
|
|
249
|
+
const id = spreadsheet || getSpreadsheetId();
|
|
250
|
+
|
|
251
|
+
// Get headers to align columns
|
|
252
|
+
const headerRes = await sheets.spreadsheets.values.get({
|
|
253
|
+
spreadsheetId: id,
|
|
254
|
+
range: `${sheet}!1:1`,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const headers = headerRes.data.values?.[0] || Object.keys(row);
|
|
258
|
+
const values = headers.map((h) => row[h] ?? "");
|
|
259
|
+
|
|
260
|
+
const res = await sheets.spreadsheets.values.append({
|
|
261
|
+
spreadsheetId: id,
|
|
262
|
+
range: `${sheet}!A1`,
|
|
263
|
+
valueInputOption: "USER_ENTERED",
|
|
264
|
+
requestBody: { values: [values] },
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
spreadsheetId: id,
|
|
269
|
+
sheet,
|
|
270
|
+
updatedRange: res.data.updates?.updatedRange,
|
|
271
|
+
row,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ─── Tool definitions ─────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
const TOOLS = [
|
|
278
|
+
{
|
|
279
|
+
name: "sheets_list_tabs",
|
|
280
|
+
description:
|
|
281
|
+
"List all sheet tabs in the connected Google Spreadsheet. " +
|
|
282
|
+
"Use this to discover available sheets before reading or writing data.",
|
|
283
|
+
inputSchema: {
|
|
284
|
+
type: "object",
|
|
285
|
+
properties: {
|
|
286
|
+
spreadsheet: {
|
|
287
|
+
type: "string",
|
|
288
|
+
description:
|
|
289
|
+
"Optional spreadsheet ID. Uses the configured default if omitted.",
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: "sheets_read_range",
|
|
296
|
+
description:
|
|
297
|
+
"Read data from a Google Sheet range. " +
|
|
298
|
+
"Range format: 'SheetName!A1:Z100' or just 'SheetName' for the whole sheet. " +
|
|
299
|
+
"First row is treated as headers and data is returned as an array of objects.",
|
|
300
|
+
inputSchema: {
|
|
301
|
+
type: "object",
|
|
302
|
+
properties: {
|
|
303
|
+
spreadsheet: {
|
|
304
|
+
type: "string",
|
|
305
|
+
description: "Optional spreadsheet ID.",
|
|
306
|
+
},
|
|
307
|
+
range: {
|
|
308
|
+
type: "string",
|
|
309
|
+
description:
|
|
310
|
+
"Range in A1 notation. Examples: 'Users!A1:Z500', 'Orders'.",
|
|
311
|
+
},
|
|
312
|
+
skip_header: {
|
|
313
|
+
type: "boolean",
|
|
314
|
+
description:
|
|
315
|
+
"If true, returns raw 2D array instead of objects with header keys.",
|
|
316
|
+
default: false,
|
|
317
|
+
},
|
|
318
|
+
value_render: {
|
|
319
|
+
type: "string",
|
|
320
|
+
enum: ["FORMATTED_VALUE", "UNFORMATTED_VALUE", "FORMULA"],
|
|
321
|
+
description:
|
|
322
|
+
"How to render values. FORMATTED_VALUE = as seen in UI, UNFORMATTED_VALUE = raw numbers, FORMULA = show formulas.",
|
|
323
|
+
default: "FORMATTED_VALUE",
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
required: ["range"],
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: "sheets_get_sheet",
|
|
331
|
+
description:
|
|
332
|
+
"Get spreadsheet metadata: title, URL, locale, and all tabs with row/column counts.",
|
|
333
|
+
inputSchema: {
|
|
334
|
+
type: "object",
|
|
335
|
+
properties: {
|
|
336
|
+
spreadsheet: {
|
|
337
|
+
type: "string",
|
|
338
|
+
description: "Optional spreadsheet ID.",
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
name: "sheets_write_range",
|
|
345
|
+
description:
|
|
346
|
+
"Write values to a Google Sheet range. Values should be a 2D array: " +
|
|
347
|
+
"each inner array is a row. Example: [['name','age'],['Anton','30']]",
|
|
348
|
+
inputSchema: {
|
|
349
|
+
type: "object",
|
|
350
|
+
properties: {
|
|
351
|
+
spreadsheet: {
|
|
352
|
+
type: "string",
|
|
353
|
+
description: "Optional spreadsheet ID.",
|
|
354
|
+
},
|
|
355
|
+
range: {
|
|
356
|
+
type: "string",
|
|
357
|
+
description: "Range to write, e.g., 'Users!A1'.",
|
|
358
|
+
},
|
|
359
|
+
values: {
|
|
360
|
+
type: "array",
|
|
361
|
+
items: { type: "array" },
|
|
362
|
+
description: "2D array of values. First array = first row.",
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
required: ["range", "values"],
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
name: "sheets_create_tab",
|
|
370
|
+
description: "Create a new sheet tab in the spreadsheet.",
|
|
371
|
+
inputSchema: {
|
|
372
|
+
type: "object",
|
|
373
|
+
properties: {
|
|
374
|
+
spreadsheet: {
|
|
375
|
+
type: "string",
|
|
376
|
+
description: "Optional spreadsheet ID.",
|
|
377
|
+
},
|
|
378
|
+
sheetName: {
|
|
379
|
+
type: "string",
|
|
380
|
+
description: "Name of the new sheet tab.",
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
required: ["sheetName"],
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: "sheets_append_row",
|
|
388
|
+
description:
|
|
389
|
+
"Append a row to a sheet. The row is a JSON object with column names as keys. " +
|
|
390
|
+
"The tool automatically reads the header row to align columns. " +
|
|
391
|
+
'Example: {"name": "Anton", "email": "anton@example.com"}',
|
|
392
|
+
inputSchema: {
|
|
393
|
+
type: "object",
|
|
394
|
+
properties: {
|
|
395
|
+
spreadsheet: {
|
|
396
|
+
type: "string",
|
|
397
|
+
description: "Optional spreadsheet ID.",
|
|
398
|
+
},
|
|
399
|
+
sheet: {
|
|
400
|
+
type: "string",
|
|
401
|
+
description: "Sheet name (tab name).",
|
|
402
|
+
},
|
|
403
|
+
row: {
|
|
404
|
+
type: "object",
|
|
405
|
+
description:
|
|
406
|
+
"Row data as JSON object. Keys = column headers, values = cell values.",
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
required: ["sheet", "row"],
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
// ─── Server ───────────────────────────────────────────────────────────────────
|
|
415
|
+
|
|
416
|
+
const server = new Server(
|
|
417
|
+
{
|
|
418
|
+
name: "google-sheet-mcp",
|
|
419
|
+
version: "1.0.0",
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
capabilities: {
|
|
423
|
+
tools: {},
|
|
424
|
+
},
|
|
425
|
+
}
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
429
|
+
tools: TOOLS,
|
|
430
|
+
}));
|
|
431
|
+
|
|
432
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
433
|
+
const { name, arguments: args } = request.params;
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
let result;
|
|
437
|
+
|
|
438
|
+
switch (name) {
|
|
439
|
+
case "sheets_list_tabs":
|
|
440
|
+
result = await listTabs(args);
|
|
441
|
+
break;
|
|
442
|
+
case "sheets_read_range":
|
|
443
|
+
result = await readRange(args);
|
|
444
|
+
break;
|
|
445
|
+
case "sheets_get_sheet":
|
|
446
|
+
result = await getSheet(args);
|
|
447
|
+
break;
|
|
448
|
+
case "sheets_write_range":
|
|
449
|
+
result = await writeRange(args);
|
|
450
|
+
break;
|
|
451
|
+
case "sheets_create_tab":
|
|
452
|
+
result = await createTab(args);
|
|
453
|
+
break;
|
|
454
|
+
case "sheets_append_row":
|
|
455
|
+
result = await appendRow(args);
|
|
456
|
+
break;
|
|
457
|
+
default:
|
|
458
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
content: [
|
|
463
|
+
{
|
|
464
|
+
type: "text",
|
|
465
|
+
text: JSON.stringify(result, null, 2),
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
};
|
|
469
|
+
} catch (error) {
|
|
470
|
+
const isOAuth = _config?.authType === "oauth2";
|
|
471
|
+
const hint = isOAuth
|
|
472
|
+
? "Check token health: `npx google-sheet-mcp token-status`. Re-auth: `npx google-sheet-mcp init --auth oauth`."
|
|
473
|
+
: "Check that the spreadsheet is shared with the service account email.";
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
content: [
|
|
477
|
+
{
|
|
478
|
+
type: "text",
|
|
479
|
+
text: JSON.stringify(
|
|
480
|
+
{
|
|
481
|
+
error: error.message,
|
|
482
|
+
authType: _config?.authType || "unknown",
|
|
483
|
+
hint,
|
|
484
|
+
},
|
|
485
|
+
null,
|
|
486
|
+
2
|
|
487
|
+
),
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
isError: true,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// ─── Start ────────────────────────────────────────────────────────────────────
|
|
496
|
+
|
|
497
|
+
async function main() {
|
|
498
|
+
// Check we have a valid config before starting
|
|
499
|
+
const config = loadServerConfig() || loadFileConfig();
|
|
500
|
+
if (!config) {
|
|
501
|
+
console.error(
|
|
502
|
+
"[google-sheet-mcp] ⚠️ No configuration found. " +
|
|
503
|
+
"Run `npx google-sheet-mcp init` or set env vars."
|
|
504
|
+
);
|
|
505
|
+
} else {
|
|
506
|
+
console.error(
|
|
507
|
+
`[google-sheet-mcp] ✅ Configured: ${config.spreadsheetId} (${config.authType || "service-account"})`
|
|
508
|
+
);
|
|
509
|
+
if (config.authType === "oauth2") {
|
|
510
|
+
console.error(
|
|
511
|
+
`[google-sheet-mcp] 🔑 OAuth2 mode — token auto-refreshes. Check health: npx google-sheet-mcp token-status`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const transport = new StdioServerTransport();
|
|
517
|
+
await server.connect(transport);
|
|
518
|
+
console.error("[google-sheet-mcp] Server started (stdio) — waiting for IDE connection...");
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
main().catch((err) => {
|
|
522
|
+
console.error("[google-sheet-mcp] Fatal error:", err);
|
|
523
|
+
process.exit(1);
|
|
524
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Sheets client — dual auth: service account + OAuth2.
|
|
3
|
+
*
|
|
4
|
+
* Service Account: uses JSON key file (GOOGLE_APPLICATION_CREDENTIALS).
|
|
5
|
+
* OAuth2: uses refresh token (GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET + GOOGLE_REFRESH_TOKEN).
|
|
6
|
+
*
|
|
7
|
+
* Auto-refresh: OAuth2 access tokens are refreshed transparently by googleapis.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { google } from "googleapis";
|
|
11
|
+
import { existsSync } from "fs";
|
|
12
|
+
import { resolve } from "path";
|
|
13
|
+
import { createOAuth2Client } from "./oauth2-client.mjs";
|
|
14
|
+
|
|
15
|
+
let _sheetsClient = null;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create sheets client from loaded config.
|
|
19
|
+
* Detects auth type automatically.
|
|
20
|
+
*/
|
|
21
|
+
export function createSheetsClientFromConfig(config) {
|
|
22
|
+
const { authType } = config;
|
|
23
|
+
|
|
24
|
+
if (authType === "oauth2") {
|
|
25
|
+
return createOAuth2SheetsClient(config);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Default: service account
|
|
29
|
+
return createServiceAccountSheetsClient(config);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Service Account: create authenticated Google Sheets client from JSON key path.
|
|
34
|
+
*/
|
|
35
|
+
export function createServiceAccountSheetsClient(config) {
|
|
36
|
+
const credentialsPath = config.credentialsPath;
|
|
37
|
+
if (!credentialsPath) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
"No credentials path. Use GOOGLE_APPLICATION_CREDENTIALS or `npx google-sheet-mcp init`."
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const resolvedPath = resolve(credentialsPath);
|
|
44
|
+
if (!existsSync(resolvedPath)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Credentials file not found: ${resolvedPath}\n` +
|
|
47
|
+
`Get one from Google Cloud Console → APIs & Services → Credentials → Create Service Account → JSON key.`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const auth = new google.auth.GoogleAuth({
|
|
52
|
+
keyFile: resolvedPath,
|
|
53
|
+
scopes: ["https://www.googleapis.com/auth/spreadsheets"],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return google.sheets({ version: "v4", auth });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* OAuth2: create authenticated Google Sheets client from refresh token.
|
|
61
|
+
* Access token is auto-refreshed by googleapis.
|
|
62
|
+
*/
|
|
63
|
+
export function createOAuth2SheetsClient(config) {
|
|
64
|
+
const { oauth2 } = config;
|
|
65
|
+
if (!oauth2 || !oauth2.client_id || !oauth2.client_secret || !oauth2.refresh_token) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
"Missing OAuth2 credentials. Run `npx google-sheet-mcp init --auth oauth` to configure."
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const auth = createOAuth2Client(oauth2);
|
|
72
|
+
return google.sheets({ version: "v4", auth });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Backward-compatible: createSheetsClient(credentialsPath).
|
|
77
|
+
* Used by CLI commands that take a direct path.
|
|
78
|
+
*/
|
|
79
|
+
export function createSheetsClient(credentialsPath) {
|
|
80
|
+
return createServiceAccountSheetsClient({ credentialsPath });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get or create cached client.
|
|
85
|
+
*/
|
|
86
|
+
let _cachedConfig = null;
|
|
87
|
+
|
|
88
|
+
export function getSheetsClient(configOrPath) {
|
|
89
|
+
if (_sheetsClient) return _sheetsClient;
|
|
90
|
+
|
|
91
|
+
if (typeof configOrPath === "string") {
|
|
92
|
+
_sheetsClient = createSheetsClient(configOrPath);
|
|
93
|
+
} else if (configOrPath) {
|
|
94
|
+
_sheetsClient = createSheetsClientFromConfig(configOrPath);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return _sheetsClient;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Reset cached client (for switching auth).
|
|
102
|
+
*/
|
|
103
|
+
export function resetClient() {
|
|
104
|
+
_sheetsClient = null;
|
|
105
|
+
_cachedConfig = null;
|
|
106
|
+
}
|