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,114 @@
1
+ /**
2
+ * token-status — Check OAuth2 refresh token health.
3
+ *
4
+ * Validates the refresh token by attempting to get a fresh access token.
5
+ * Useful for:
6
+ * - Debugging "access denied" errors
7
+ * - Checking if token was revoked
8
+ * - Pre-flight check before CI/CD usage
9
+ */
10
+
11
+ import chalk from "chalk";
12
+ import ora from "ora";
13
+ import { loadConfig } from "../../config/config.mjs";
14
+ import {
15
+ validateRefreshToken,
16
+ getTokenInfo,
17
+ } from "../../server/oauth2-client.mjs";
18
+
19
+ export async function tokenStatusCommand() {
20
+ console.log(chalk.bold.cyan("\nšŸ”‘ Google Sheet MCP — Token Status\n"));
21
+
22
+ const config = loadConfig();
23
+ if (!config) {
24
+ console.error(
25
+ chalk.red(
26
+ "āŒ No configuration found. Run `npx google-sheet-mcp init --auth oauth` first."
27
+ )
28
+ );
29
+ process.exit(1);
30
+ }
31
+
32
+ console.log(chalk.gray(` Config source: ${config.source}`));
33
+ console.log(chalk.gray(` Auth type: ${config.authType || "service-account"}`));
34
+ console.log();
35
+
36
+ if (config.authType !== "oauth2") {
37
+ console.log(
38
+ chalk.yellow(
39
+ "āš ļø Token-status is only relevant for OAuth2 authentication.\n" +
40
+ " Service accounts use JSON keys that don't expire."
41
+ )
42
+ );
43
+ return;
44
+ }
45
+
46
+ // Show stored token info
47
+ const info = getTokenInfo(config.oauth2);
48
+ console.log(chalk.bold("Stored token:"));
49
+ console.log(chalk.white(` Status: ${info.status}`));
50
+ if (info.clientId) {
51
+ console.log(chalk.white(` Client ID: ${info.clientId}`));
52
+ }
53
+ if (info.refreshTokenPrefix) {
54
+ console.log(
55
+ chalk.white(` Refresh: ${info.refreshTokenPrefix}`)
56
+ );
57
+ }
58
+ if (info.issues) {
59
+ for (const issue of info.issues) {
60
+ console.log(chalk.red(` āŒ ${issue}`));
61
+ }
62
+ }
63
+ console.log();
64
+
65
+ if (info.status !== "configured") {
66
+ console.log(
67
+ chalk.red(
68
+ "āŒ Token is incomplete. Run `npx google-sheet-mcp init --auth oauth` to reconfigure."
69
+ )
70
+ );
71
+ process.exit(1);
72
+ }
73
+
74
+ // Validate by refreshing
75
+ const spinner = ora("Validating refresh token...").start();
76
+
77
+ try {
78
+ const result = await validateRefreshToken(config.oauth2);
79
+
80
+ if (result.valid) {
81
+ spinner.succeed("Token is valid");
82
+ console.log();
83
+ console.log(chalk.green("āœ… Refresh token is healthy"));
84
+ console.log(chalk.gray(` Access token expires: ${result.expiresAt}`));
85
+ console.log(chalk.gray(` Scopes: ${result.scopes}`));
86
+ console.log();
87
+ console.log(
88
+ chalk.white(
89
+ " AI agents can now access your Google Sheets on your behalf."
90
+ )
91
+ );
92
+ } else {
93
+ spinner.fail(`Token is invalid: ${result.error}`);
94
+ console.log();
95
+ console.log(chalk.yellow(` Hint: ${result.hint}`));
96
+ console.log();
97
+ console.log(
98
+ chalk.white(" To replace the token, run:")
99
+ );
100
+ console.log(
101
+ chalk.cyan(" npx google-sheet-mcp init --auth oauth")
102
+ );
103
+ console.log(
104
+ chalk.gray(
105
+ " This will walk you through the OAuth flow and save a new refresh token."
106
+ )
107
+ );
108
+ process.exit(1);
109
+ }
110
+ } catch (err) {
111
+ spinner.fail(`Validation error: ${err.message}`);
112
+ process.exit(1);
113
+ }
114
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Configuration management.
3
+ *
4
+ * Supports two auth types:
5
+ * 1. Service Account: GOOGLE_SPREADSHEET_ID + GOOGLE_APPLICATION_CREDENTIALS
6
+ * 2. OAuth2: GOOGLE_SPREADSHEET_ID + GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET + GOOGLE_REFRESH_TOKEN
7
+ *
8
+ * Resolution order:
9
+ * 1. ENV vars
10
+ * 2. .google-sheet-mcp.json in CWD
11
+ * 3. ~/.google-sheet-mcp.json (global)
12
+ */
13
+
14
+ import { readFileSync, writeFileSync, existsSync } from "fs";
15
+ import { homedir } from "os";
16
+ import { join, resolve } from "path";
17
+
18
+ const CONFIG_FILENAME = ".google-sheet-mcp.json";
19
+
20
+ /**
21
+ * Detect auth type from env vars.
22
+ */
23
+ function detectAuthFromEnv() {
24
+ const spreadsheetId = process.env.GOOGLE_SPREADSHEET_ID;
25
+ if (!spreadsheetId) return null;
26
+
27
+ // Service account
28
+ const saCreds = process.env.GOOGLE_APPLICATION_CREDENTIALS;
29
+ if (saCreds) {
30
+ return {
31
+ spreadsheetId,
32
+ authType: "service-account",
33
+ credentialsPath: saCreds,
34
+ source: "env",
35
+ };
36
+ }
37
+
38
+ // OAuth2
39
+ const clientId = process.env.GOOGLE_CLIENT_ID;
40
+ const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
41
+ const refreshToken = process.env.GOOGLE_REFRESH_TOKEN;
42
+ if (clientId && clientSecret && refreshToken) {
43
+ return {
44
+ spreadsheetId,
45
+ authType: "oauth2",
46
+ oauth2: {
47
+ client_id: clientId,
48
+ client_secret: clientSecret,
49
+ refresh_token: refreshToken,
50
+ },
51
+ source: "env",
52
+ };
53
+ }
54
+
55
+ return null;
56
+ }
57
+
58
+ /**
59
+ * Validate a config object has all required fields.
60
+ */
61
+ function isValidConfig(config) {
62
+ if (!config || !config.spreadsheetId) return false;
63
+
64
+ if (config.authType === "service-account") {
65
+ return !!config.credentialsPath;
66
+ }
67
+
68
+ if (config.authType === "oauth2") {
69
+ return !!(
70
+ config.oauth2?.client_id &&
71
+ config.oauth2?.client_secret &&
72
+ config.oauth2?.refresh_token
73
+ );
74
+ }
75
+
76
+ return false;
77
+ }
78
+
79
+ /**
80
+ * Load config from local file, global file, or env vars.
81
+ */
82
+ export function loadConfig() {
83
+ // 1. ENV vars — highest priority
84
+ const envConfig = detectAuthFromEnv();
85
+ if (envConfig) return envConfig;
86
+
87
+ // 2. Local config (.google-sheet-mcp.json in CWD)
88
+ const localPath = resolve(process.cwd(), CONFIG_FILENAME);
89
+ if (existsSync(localPath)) {
90
+ const config = JSON.parse(readFileSync(localPath, "utf-8"));
91
+ if (isValidConfig(config)) {
92
+ config._path = localPath;
93
+ config.source = "local";
94
+ return config;
95
+ }
96
+ }
97
+
98
+ // 3. Global config (~/.google-sheet-mcp.json)
99
+ const globalPath = join(homedir(), CONFIG_FILENAME);
100
+ if (existsSync(globalPath)) {
101
+ const config = JSON.parse(readFileSync(globalPath, "utf-8"));
102
+ if (isValidConfig(config)) {
103
+ config._path = globalPath;
104
+ config.source = "global";
105
+ return config;
106
+ }
107
+ }
108
+
109
+ return null;
110
+ }
111
+
112
+ /**
113
+ * Save config to local file (CWD) or global (~/).
114
+ *
115
+ * Config shape:
116
+ * Service Account: { spreadsheetId, authType: "service-account", credentialsPath, sheets }
117
+ * OAuth2: { spreadsheetId, authType: "oauth2", oauth2: { client_id, client_secret, refresh_token }, sheets }
118
+ */
119
+ export function saveConfig(
120
+ { spreadsheetId, authType, credentialsPath, oauth2, sheets },
121
+ global = false
122
+ ) {
123
+ const configPath = global
124
+ ? join(homedir(), CONFIG_FILENAME)
125
+ : resolve(process.cwd(), CONFIG_FILENAME);
126
+
127
+ const existing = existsSync(configPath)
128
+ ? JSON.parse(readFileSync(configPath, "utf-8"))
129
+ : {};
130
+
131
+ const config = {
132
+ ...existing,
133
+ spreadsheetId,
134
+ authType: authType || existing.authType || "service-account",
135
+ sheets: sheets || existing.sheets || [],
136
+ };
137
+
138
+ if (authType === "oauth2" || existing.authType === "oauth2") {
139
+ config.oauth2 = oauth2 || existing.oauth2 || {};
140
+ delete config.credentialsPath;
141
+ } else {
142
+ config.credentialsPath = credentialsPath || existing.credentialsPath;
143
+ delete config.oauth2;
144
+ }
145
+
146
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
147
+ return configPath;
148
+ }
149
+
150
+ /**
151
+ * Extract spreadsheetId from Google Sheets URL.
152
+ */
153
+ export function extractSpreadsheetId(url) {
154
+ // Formats:
155
+ // https://docs.google.com/spreadsheets/d/<ID>/edit
156
+ // https://docs.google.com/spreadsheets/d/<ID>/edit#gid=0
157
+ const match = url.match(
158
+ /\/spreadsheets\/d\/([a-zA-Z0-9_-]+)/
159
+ );
160
+ if (match) return match[1];
161
+
162
+ // Maybe it's already a raw ID
163
+ if (/^[a-zA-Z0-9_-]{20,}$/.test(url)) return url;
164
+
165
+ return null;
166
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * OAuth2 Google Sheets client.
3
+ *
4
+ * Uses refresh token — no browser login needed after initial setup.
5
+ * Token auto-refreshes via googleapis library.
6
+ *
7
+ * Setup flow (one-time):
8
+ * 1. Create OAuth2 credentials in Google Cloud Console
9
+ * 2. Run `npx google-sheet-mcp init --auth oauth`
10
+ * 3. Follow browser OAuth flow
11
+ * 4. Refresh token is saved and auto-refreshed forever
12
+ *
13
+ * Token health:
14
+ * - `npx google-sheet-mcp token-status` — check if token is valid
15
+ * - Invalid token → run `npx google-sheet-mcp init --auth oauth` again
16
+ */
17
+
18
+ import { google } from "googleapis";
19
+
20
+ /**
21
+ * Create OAuth2 client from stored refresh token.
22
+ * Automatically refreshes access token if expired.
23
+ */
24
+ export function createOAuth2Client(credentials) {
25
+ const { client_id, client_secret, refresh_token } = credentials;
26
+
27
+ if (!client_id || !client_secret) {
28
+ throw new Error(
29
+ "Missing OAuth2 credentials: client_id and client_secret are required.\n" +
30
+ "Get them from Google Cloud Console → APIs & Services → Credentials → Create OAuth client ID."
31
+ );
32
+ }
33
+
34
+ if (!refresh_token) {
35
+ throw new Error(
36
+ "Missing refresh_token. Run `npx google-sheet-mcp init --auth oauth` to complete OAuth2 setup."
37
+ );
38
+ }
39
+
40
+ const auth = new google.auth.OAuth2(client_id, client_secret);
41
+ auth.setCredentials({ refresh_token });
42
+
43
+ // Auto-refresh: googleapis handles this transparently.
44
+ // Every API call checks if the access token is expired and refreshes if needed.
45
+
46
+ return auth;
47
+ }
48
+
49
+ /**
50
+ * Create sheets client using OAuth2.
51
+ */
52
+ export function createOAuth2SheetsClient(credentials) {
53
+ const auth = createOAuth2Client(credentials);
54
+ return google.sheets({ version: "v4", auth });
55
+ }
56
+
57
+ /**
58
+ * Generate the OAuth2 authorization URL.
59
+ * User visits this URL, grants access, gets a code.
60
+ */
61
+ export function generateAuthUrl(client_id, client_secret) {
62
+ const auth = new google.auth.OAuth2(
63
+ client_id,
64
+ client_secret,
65
+ "http://localhost:3000/oauth2callback"
66
+ );
67
+
68
+ return auth.generateAuthUrl({
69
+ access_type: "offline", // ← CRITICAL: forces refresh_token to be returned
70
+ prompt: "consent", // ← CRITICAL: always show consent (ensures refresh_token every time)
71
+ scope: ["https://www.googleapis.com/auth/spreadsheets"],
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Exchange authorization code for tokens.
77
+ * Returns { access_token, refresh_token, expiry_date }.
78
+ */
79
+ export async function exchangeCodeForTokens(
80
+ client_id,
81
+ client_secret,
82
+ code
83
+ ) {
84
+ const auth = new google.auth.OAuth2(
85
+ client_id,
86
+ client_secret,
87
+ "http://localhost:3000/oauth2callback"
88
+ );
89
+
90
+ const { tokens } = await auth.getToken(code);
91
+ return tokens;
92
+ }
93
+
94
+ /**
95
+ * Validate a refresh token by trying to get a fresh access token.
96
+ *
97
+ * Returns { valid: true } or { valid: false, error: string }.
98
+ */
99
+ export async function validateRefreshToken(credentials) {
100
+ const { client_id, client_secret, refresh_token } = credentials;
101
+
102
+ if (!client_id || !client_secret || !refresh_token) {
103
+ return {
104
+ valid: false,
105
+ error: "Missing credentials (client_id, client_secret, or refresh_token).",
106
+ };
107
+ }
108
+
109
+ try {
110
+ const auth = new google.auth.OAuth2(client_id, client_secret);
111
+ auth.setCredentials({ refresh_token });
112
+
113
+ // Force token refresh — this will fail if the refresh_token is invalid
114
+ const { credentials: newCreds } = await auth.refreshAccessToken();
115
+
116
+ return {
117
+ valid: true,
118
+ expiresAt: newCreds.expiry_date
119
+ ? new Date(newCreds.expiry_date).toISOString()
120
+ : "unknown",
121
+ scopes: newCreds.scope || "unknown",
122
+ };
123
+ } catch (err) {
124
+ return {
125
+ valid: false,
126
+ error: err.message,
127
+ hint:
128
+ "The refresh token is invalid or revoked. Run `npx google-sheet-mcp init --auth oauth` to re-authenticate.",
129
+ };
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Get token info without making an API call.
135
+ * Just checks if the stored token exists and looks valid.
136
+ */
137
+ export function getTokenInfo(credentials) {
138
+ const { client_id, client_secret, refresh_token } = credentials;
139
+
140
+ const issues = [];
141
+
142
+ if (!client_id) issues.push("Missing client_id");
143
+ if (!client_secret) issues.push("Missing client_secret");
144
+ if (!refresh_token) issues.push("Missing refresh_token");
145
+
146
+ if (issues.length > 0) {
147
+ return { status: "missing", issues };
148
+ }
149
+
150
+ return {
151
+ status: "configured",
152
+ clientId: `${client_id.substring(0, 12)}...`,
153
+ refreshTokenPrefix: `${refresh_token.substring(0, 12)}...`,
154
+ };
155
+ }