google-tools-mcp 1.2.8 → 1.2.9
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/dist/auth.js +343 -325
- package/dist/clients.js +293 -213
- package/dist/index.js +85 -74
- package/dist/setup.js +422 -235
- package/dist/tools/sheets/deleteColumns.js +66 -0
- package/dist/tools/sheets/index.js +2 -0
- package/dist/tools/sheets/ungroupAllRows.js +6 -2
- package/package.json +1 -1
package/dist/setup.js
CHANGED
|
@@ -1,235 +1,422 @@
|
|
|
1
|
-
// Guided setup wizard for google-tools-mcp.
|
|
2
|
-
// Rich terminal UI using @clack/prompts.
|
|
3
|
-
import * as p from '@clack/prompts';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import * as fs from 'fs/promises';
|
|
6
|
-
import * as path from 'path';
|
|
7
|
-
import * as os from 'os';
|
|
8
|
-
import { exec, execSync } from 'child_process';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
p.log.message(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
p.log.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
p.
|
|
235
|
-
|
|
1
|
+
// Guided setup wizard for google-tools-mcp.
|
|
2
|
+
// Rich terminal UI using @clack/prompts.
|
|
3
|
+
import * as p from '@clack/prompts';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import * as fs from 'fs/promises';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
import { exec, execSync } from 'child_process';
|
|
9
|
+
import { google } from 'googleapis';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Config
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const APIS = [
|
|
15
|
+
'docs.googleapis.com',
|
|
16
|
+
'sheets.googleapis.com',
|
|
17
|
+
'drive.googleapis.com',
|
|
18
|
+
'gmail.googleapis.com',
|
|
19
|
+
'calendar-json.googleapis.com',
|
|
20
|
+
'forms.googleapis.com',
|
|
21
|
+
'slides.googleapis.com',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const ENABLE_APIS_URL =
|
|
25
|
+
`https://console.cloud.google.com/flows/enableapi?apiid=${APIS.join(',')}`;
|
|
26
|
+
|
|
27
|
+
const CREATE_CREDENTIALS_URL =
|
|
28
|
+
'https://console.cloud.google.com/apis/credentials/oauthclient';
|
|
29
|
+
|
|
30
|
+
const CONSENT_SCREEN_URL =
|
|
31
|
+
'https://console.cloud.google.com/apis/credentials/consent';
|
|
32
|
+
|
|
33
|
+
// Direct link to the Audience tab on the new "Google Auth Platform" UI — this
|
|
34
|
+
// is where the Test users section lives in the redesigned console.
|
|
35
|
+
const AUDIENCE_URL =
|
|
36
|
+
'https://console.cloud.google.com/auth/audience';
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Helpers
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
function openBrowser(url) {
|
|
42
|
+
const platform = process.platform;
|
|
43
|
+
let cmd;
|
|
44
|
+
if (platform === 'win32') {
|
|
45
|
+
cmd = `start "" "${url}"`;
|
|
46
|
+
} else if (platform === 'darwin') {
|
|
47
|
+
cmd = `open "${url}"`;
|
|
48
|
+
} else {
|
|
49
|
+
cmd = `xdg-open "${url}"`;
|
|
50
|
+
}
|
|
51
|
+
exec(cmd, () => {});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getConfigDir() {
|
|
55
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
56
|
+
const base = xdg || path.join(os.homedir(), '.config');
|
|
57
|
+
const baseDir = path.join(base, 'google-tools-mcp');
|
|
58
|
+
const profile = process.env.GOOGLE_MCP_PROFILE;
|
|
59
|
+
return profile ? path.join(baseDir, profile) : baseDir;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hasCli(name) {
|
|
63
|
+
try {
|
|
64
|
+
execSync(`${name} --version`, { stdio: 'ignore' });
|
|
65
|
+
return true;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function runCommand(cmd) {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
exec(cmd, (err, stdout, stderr) => {
|
|
74
|
+
if (err) reject(new Error(stderr || err.message));
|
|
75
|
+
else resolve(stdout);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function cancelled() {
|
|
81
|
+
p.cancel('Setup cancelled.');
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Pull the GCP project number out of an OAuth client ID.
|
|
86
|
+
// Format: <project_number>-<hash>.apps.googleusercontent.com
|
|
87
|
+
function extractProjectNumberFromClientId(clientId) {
|
|
88
|
+
const m = clientId.match(/^(\d+)-/);
|
|
89
|
+
return m ? m[1] : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Programmatically enable all required APIs in the user's project using the
|
|
93
|
+
// Service Usage API. Returns { ok: true } on success, or
|
|
94
|
+
// { ok: false, error } on failure (caller falls back to the manual URL).
|
|
95
|
+
async function enableApisProgrammatically(authClient, projectNumber) {
|
|
96
|
+
const serviceUsage = google.serviceusage({ version: 'v1', auth: authClient });
|
|
97
|
+
const batchRes = await serviceUsage.services.batchEnable({
|
|
98
|
+
parent: `projects/${projectNumber}`,
|
|
99
|
+
requestBody: { serviceIds: APIS },
|
|
100
|
+
});
|
|
101
|
+
const opName = batchRes.data?.name;
|
|
102
|
+
if (!opName) {
|
|
103
|
+
// Some implementations return done synchronously.
|
|
104
|
+
if (batchRes.data?.done) return { ok: true };
|
|
105
|
+
return { ok: false, error: new Error('batchEnable returned no operation name') };
|
|
106
|
+
}
|
|
107
|
+
// Poll the long-running operation. Enabling 7 APIs usually completes in
|
|
108
|
+
// 5-20s; we cap at 60s before giving up and falling back.
|
|
109
|
+
const deadline = Date.now() + 60_000;
|
|
110
|
+
while (Date.now() < deadline) {
|
|
111
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
112
|
+
const opRes = await serviceUsage.operations.get({ name: opName });
|
|
113
|
+
if (opRes.data?.done) {
|
|
114
|
+
if (opRes.data.error) {
|
|
115
|
+
return { ok: false, error: new Error(opRes.data.error.message || 'operation failed') };
|
|
116
|
+
}
|
|
117
|
+
return { ok: true };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { ok: false, error: new Error('enable operation timed out after 60s') };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Setup flow
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
export async function runSetup() {
|
|
127
|
+
console.clear();
|
|
128
|
+
|
|
129
|
+
p.intro(chalk.bgCyan.bold.white(' google-tools-mcp setup '));
|
|
130
|
+
|
|
131
|
+
// ── Step 1: Project setup ────────────────────────────────────────────
|
|
132
|
+
p.log.step(chalk.cyan.bold('Step 1') + chalk.dim(' · ') + 'Google Cloud project');
|
|
133
|
+
p.log.message([
|
|
134
|
+
chalk.dim('You need a Google Cloud project to host the OAuth client.'),
|
|
135
|
+
chalk.green('If you don\'t have one yet, the next page (consent screen) will prompt you to create one.'),
|
|
136
|
+
'',
|
|
137
|
+
chalk.dim('APIs to be enabled (we\'ll do this automatically after authentication):'),
|
|
138
|
+
chalk.dim(' ') + APIS.map(a => chalk.yellow(a.replace('.googleapis.com', ''))).join(chalk.dim(', ')),
|
|
139
|
+
].join('\n'));
|
|
140
|
+
|
|
141
|
+
const wantManualEnable = await p.confirm({
|
|
142
|
+
message: 'Pre-enable APIs manually now? (advanced — recommended: NO, we\'ll auto-enable later)',
|
|
143
|
+
active: 'yes, open manual page',
|
|
144
|
+
inactive: 'no, skip',
|
|
145
|
+
initialValue: false,
|
|
146
|
+
});
|
|
147
|
+
if (p.isCancel(wantManualEnable)) cancelled();
|
|
148
|
+
if (wantManualEnable) {
|
|
149
|
+
openBrowser(ENABLE_APIS_URL);
|
|
150
|
+
p.log.message(chalk.dim(ENABLE_APIS_URL));
|
|
151
|
+
const manualDone = await p.confirm({
|
|
152
|
+
message: 'Done with manual enable?',
|
|
153
|
+
active: 'yes, continue',
|
|
154
|
+
inactive: 'not yet',
|
|
155
|
+
});
|
|
156
|
+
if (p.isCancel(manualDone)) cancelled();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Step 2: OAuth consent screen ─────────────────────────────────────
|
|
160
|
+
p.log.step(chalk.cyan.bold('Step 2') + chalk.dim(' · ') + 'Configure OAuth consent screen');
|
|
161
|
+
p.log.message([
|
|
162
|
+
chalk.dim('Google has two versions of this UI — both lead to the same place:'),
|
|
163
|
+
'',
|
|
164
|
+
chalk.bold('New UI ("Google Auth Platform"):'),
|
|
165
|
+
`${chalk.white('›')} Click ${chalk.bold('"Get started"')} if prompted`,
|
|
166
|
+
`${chalk.white('›')} App name: anything ${chalk.dim('(e.g. "MCP")')}, support email: your own`,
|
|
167
|
+
`${chalk.white('›')} Audience: choose ${chalk.bold('"External"')}`,
|
|
168
|
+
`${chalk.white('›')} Click ${chalk.bold('Create')} / ${chalk.bold('Save and continue')} through each section`,
|
|
169
|
+
'',
|
|
170
|
+
chalk.bold('Old UI (wizard):'),
|
|
171
|
+
`${chalk.white('›')} User type: ${chalk.bold('"External"')}`,
|
|
172
|
+
`${chalk.white('›')} App name: anything ${chalk.dim('(e.g. "MCP")')}`,
|
|
173
|
+
`${chalk.white('›')} Click ${chalk.bold('Save and continue')} through Scopes (skip), then finish`,
|
|
174
|
+
'',
|
|
175
|
+
chalk.yellow('We will add your email as a test user in the NEXT step — don\'t worry about that here yet.'),
|
|
176
|
+
].join('\n'));
|
|
177
|
+
|
|
178
|
+
const ready2 = await p.confirm({
|
|
179
|
+
message: 'Ready? This will open your browser.',
|
|
180
|
+
active: 'open browser',
|
|
181
|
+
inactive: 'not yet',
|
|
182
|
+
});
|
|
183
|
+
if (p.isCancel(ready2)) cancelled();
|
|
184
|
+
openBrowser(CONSENT_SCREEN_URL);
|
|
185
|
+
p.log.message(chalk.dim(CONSENT_SCREEN_URL));
|
|
186
|
+
|
|
187
|
+
const step2 = await p.confirm({
|
|
188
|
+
message: 'Done configuring consent screen?',
|
|
189
|
+
active: 'yes, continue',
|
|
190
|
+
inactive: 'not yet',
|
|
191
|
+
});
|
|
192
|
+
if (p.isCancel(step2)) cancelled();
|
|
193
|
+
|
|
194
|
+
// ── Step 2b: Add test user ───────────────────────────────────────────
|
|
195
|
+
p.log.step(chalk.cyan.bold('Step 2b') + chalk.dim(' · ') + 'Set audience to External + add test user');
|
|
196
|
+
p.log.message([
|
|
197
|
+
chalk.yellow.bold('This step is the #1 cause of "Access blocked / app not verified" errors later.'),
|
|
198
|
+
'',
|
|
199
|
+
chalk.bold('On the Audience page, do BOTH of these:'),
|
|
200
|
+
'',
|
|
201
|
+
chalk.bold('1. Set User type to ') + chalk.bold.cyan('"External"'),
|
|
202
|
+
chalk.dim(' If it\'s currently "Internal" or unset, click ') + chalk.bold('"Make external"') + chalk.dim(' / change it to External.'),
|
|
203
|
+
chalk.dim(' (Internal only works for Google Workspace orgs and will block personal Gmail accounts.)'),
|
|
204
|
+
'',
|
|
205
|
+
chalk.bold('2. Add yourself under ') + chalk.bold.cyan('"Test users"'),
|
|
206
|
+
` ${chalk.white('›')} New UI: scroll down on the ${chalk.bold('Audience')} page → ${chalk.bold('Test users')} → ${chalk.bold('+ Add users')}`,
|
|
207
|
+
` ${chalk.white('›')} Old UI: OAuth consent screen page → ${chalk.bold('Test users')} section → ${chalk.bold('+ Add users')}`,
|
|
208
|
+
'',
|
|
209
|
+
chalk.bold.red('CRITICAL: '),
|
|
210
|
+
chalk.red('The email you add must EXACTLY match the Google account you\'ll sign in with in Step 4.'),
|
|
211
|
+
chalk.dim('(e.g. if you sign in as you@gmail.com, add you@gmail.com — not a work alias)'),
|
|
212
|
+
].join('\n'));
|
|
213
|
+
|
|
214
|
+
const authEmail = await p.text({
|
|
215
|
+
message: 'Which Google account will you authenticate with?',
|
|
216
|
+
placeholder: 'you@gmail.com',
|
|
217
|
+
validate: (v) => {
|
|
218
|
+
if (!v?.trim()) return 'Required — we need this so you can confirm it matches the test user';
|
|
219
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim())) return 'Doesn\'t look like an email address';
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
if (p.isCancel(authEmail)) cancelled();
|
|
223
|
+
const authEmailTrimmed = authEmail.trim();
|
|
224
|
+
|
|
225
|
+
p.log.message(chalk.bold(`→ Add ${chalk.cyan(authEmailTrimmed)} to the Test users list now.`));
|
|
226
|
+
|
|
227
|
+
const ready2b = await p.confirm({
|
|
228
|
+
message: 'Ready? This opens the Audience page directly.',
|
|
229
|
+
active: 'open browser',
|
|
230
|
+
inactive: 'not yet',
|
|
231
|
+
});
|
|
232
|
+
if (p.isCancel(ready2b)) cancelled();
|
|
233
|
+
openBrowser(AUDIENCE_URL);
|
|
234
|
+
p.log.message(chalk.dim(AUDIENCE_URL));
|
|
235
|
+
p.log.message(chalk.dim('If that page doesn\'t show Test users, try: ') + chalk.dim(CONSENT_SCREEN_URL));
|
|
236
|
+
|
|
237
|
+
const step2b = await p.confirm({
|
|
238
|
+
message: `Confirmed: audience is External AND ${authEmailTrimmed} is in the Test users list.`,
|
|
239
|
+
active: 'yes, both done',
|
|
240
|
+
inactive: 'not yet',
|
|
241
|
+
});
|
|
242
|
+
if (p.isCancel(step2b)) cancelled();
|
|
243
|
+
|
|
244
|
+
// ── Step 3: Create OAuth credentials ─────────────────────────────────
|
|
245
|
+
p.log.step(chalk.cyan.bold('Step 3') + chalk.dim(' · ') + 'Create OAuth Client ID');
|
|
246
|
+
p.log.message([
|
|
247
|
+
`${chalk.white('›')} Select ${chalk.bold('"Desktop application"')} as the type`,
|
|
248
|
+
`${chalk.white('›')} Click ${chalk.bold('Create')}`,
|
|
249
|
+
`${chalk.white('›')} Copy the ${chalk.bold('Client ID')} and ${chalk.bold('Client Secret')}`,
|
|
250
|
+
].join('\n'));
|
|
251
|
+
|
|
252
|
+
const ready3 = await p.confirm({
|
|
253
|
+
message: 'Ready? This will open your browser.',
|
|
254
|
+
active: 'open browser',
|
|
255
|
+
inactive: 'not yet',
|
|
256
|
+
});
|
|
257
|
+
if (p.isCancel(ready3)) cancelled();
|
|
258
|
+
openBrowser(CREATE_CREDENTIALS_URL);
|
|
259
|
+
p.log.message(chalk.dim(CREATE_CREDENTIALS_URL));
|
|
260
|
+
|
|
261
|
+
const credentials = await p.group({
|
|
262
|
+
clientId: () => p.text({
|
|
263
|
+
message: 'Client ID',
|
|
264
|
+
placeholder: 'xxxx.apps.googleusercontent.com',
|
|
265
|
+
validate: (v) => {
|
|
266
|
+
if (!v?.trim()) return 'Client ID is required';
|
|
267
|
+
},
|
|
268
|
+
}),
|
|
269
|
+
clientSecret: () => p.text({
|
|
270
|
+
message: 'Client Secret',
|
|
271
|
+
placeholder: 'GOCSPX-xxxx',
|
|
272
|
+
validate: (v) => {
|
|
273
|
+
if (!v?.trim()) return 'Client Secret is required';
|
|
274
|
+
},
|
|
275
|
+
}),
|
|
276
|
+
});
|
|
277
|
+
if (p.isCancel(credentials)) cancelled();
|
|
278
|
+
|
|
279
|
+
const clientId = credentials.clientId.trim();
|
|
280
|
+
const clientSecret = credentials.clientSecret.trim();
|
|
281
|
+
|
|
282
|
+
// ── Save credentials ─────────────────────────────────────────────────
|
|
283
|
+
const configDir = getConfigDir();
|
|
284
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
285
|
+
const envPath = path.join(configDir, '.env');
|
|
286
|
+
const envContent = `GOOGLE_CLIENT_ID=${clientId}\nGOOGLE_CLIENT_SECRET=${clientSecret}\n`;
|
|
287
|
+
await fs.writeFile(envPath, envContent);
|
|
288
|
+
const displayPath = envPath.replace(os.homedir(), '~');
|
|
289
|
+
p.log.success(`Credentials saved to ${chalk.dim(displayPath)}`);
|
|
290
|
+
|
|
291
|
+
// ── Step 4: Authenticate ─────────────────────────────────────────────
|
|
292
|
+
p.log.step(chalk.cyan.bold('Step 4') + chalk.dim(' · ') + 'Authenticate with Google');
|
|
293
|
+
p.log.message([
|
|
294
|
+
'Opening browser for OAuth consent...',
|
|
295
|
+
chalk.dim(`Sign in as ${chalk.bold(authEmailTrimmed)} (the test user you added in Step 2b).`),
|
|
296
|
+
].join('\n'));
|
|
297
|
+
|
|
298
|
+
process.env.GOOGLE_CLIENT_ID = clientId;
|
|
299
|
+
process.env.GOOGLE_CLIENT_SECRET = clientSecret;
|
|
300
|
+
|
|
301
|
+
const { runAuthFlow } = await import('./auth.js');
|
|
302
|
+
let authClient = null;
|
|
303
|
+
try {
|
|
304
|
+
authClient = await runAuthFlow();
|
|
305
|
+
} catch (err) {
|
|
306
|
+
const msg = err?.message || String(err);
|
|
307
|
+
const looksLikeAccessBlocked =
|
|
308
|
+
/access_denied|access blocked|not.*verified|has not completed.*verification|test users/i.test(msg);
|
|
309
|
+
if (looksLikeAccessBlocked) {
|
|
310
|
+
p.log.error(chalk.red.bold('Auth failed — likely test user mismatch.'));
|
|
311
|
+
p.log.message([
|
|
312
|
+
chalk.red('Google blocked the sign-in. This almost always means:'),
|
|
313
|
+
`${chalk.white('›')} The email you signed in with isn't in the Test users list, OR`,
|
|
314
|
+
`${chalk.white('›')} You added a different email than the one you actually signed in with`,
|
|
315
|
+
'',
|
|
316
|
+
`${chalk.bold('Fix:')} go back to ${chalk.cyan(AUDIENCE_URL)}, confirm ${chalk.cyan(authEmailTrimmed)} is listed under Test users, then re-run setup.`,
|
|
317
|
+
'',
|
|
318
|
+
chalk.dim('Original error: ') + chalk.dim(msg),
|
|
319
|
+
].join('\n'));
|
|
320
|
+
}
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
p.log.success('Authenticated with Google!');
|
|
325
|
+
|
|
326
|
+
// ── Step 4b: Auto-enable APIs ────────────────────────────────────────
|
|
327
|
+
p.log.step(chalk.cyan.bold('Step 4b') + chalk.dim(' · ') + 'Enabling Google APIs in your project');
|
|
328
|
+
const projectNumber = extractProjectNumberFromClientId(clientId);
|
|
329
|
+
if (!projectNumber) {
|
|
330
|
+
p.log.warn('Could not parse project number from Client ID — falling back to manual enable.');
|
|
331
|
+
openBrowser(ENABLE_APIS_URL);
|
|
332
|
+
p.log.message(chalk.dim('Manually enable APIs at: ') + chalk.dim(ENABLE_APIS_URL));
|
|
333
|
+
const fallback = await p.confirm({
|
|
334
|
+
message: 'Done enabling APIs manually?',
|
|
335
|
+
active: 'yes, continue',
|
|
336
|
+
inactive: 'not yet',
|
|
337
|
+
});
|
|
338
|
+
if (p.isCancel(fallback)) cancelled();
|
|
339
|
+
} else if (!authClient) {
|
|
340
|
+
p.log.warn('Auth client unavailable — falling back to manual enable.');
|
|
341
|
+
openBrowser(ENABLE_APIS_URL);
|
|
342
|
+
const fallback = await p.confirm({
|
|
343
|
+
message: 'Done enabling APIs manually?',
|
|
344
|
+
active: 'yes, continue',
|
|
345
|
+
inactive: 'not yet',
|
|
346
|
+
});
|
|
347
|
+
if (p.isCancel(fallback)) cancelled();
|
|
348
|
+
} else {
|
|
349
|
+
const s = p.spinner();
|
|
350
|
+
s.start(`Enabling ${APIS.length} APIs in project ${projectNumber}...`);
|
|
351
|
+
try {
|
|
352
|
+
const result = await enableApisProgrammatically(authClient, projectNumber);
|
|
353
|
+
if (result.ok) {
|
|
354
|
+
s.stop(chalk.green(`Enabled ${APIS.length} APIs in project ${projectNumber}`));
|
|
355
|
+
p.log.message(chalk.dim('(May take ~30s to propagate. The server will auto-open the enable page if any API is still disabled when you first use it.)'));
|
|
356
|
+
} else {
|
|
357
|
+
s.stop(chalk.yellow('Auto-enable failed — falling back to manual.'));
|
|
358
|
+
p.log.warn(`Error: ${result.error?.message || result.error}`);
|
|
359
|
+
openBrowser(ENABLE_APIS_URL);
|
|
360
|
+
p.log.message(chalk.dim('Open: ') + chalk.dim(ENABLE_APIS_URL));
|
|
361
|
+
const fallback = await p.confirm({
|
|
362
|
+
message: 'Done enabling APIs manually?',
|
|
363
|
+
active: 'yes, continue',
|
|
364
|
+
inactive: 'not yet',
|
|
365
|
+
});
|
|
366
|
+
if (p.isCancel(fallback)) cancelled();
|
|
367
|
+
}
|
|
368
|
+
} catch (err) {
|
|
369
|
+
s.stop(chalk.yellow('Auto-enable failed — falling back to manual.'));
|
|
370
|
+
p.log.warn(`Error: ${err.message || err}`);
|
|
371
|
+
openBrowser(ENABLE_APIS_URL);
|
|
372
|
+
p.log.message(chalk.dim('Open: ') + chalk.dim(ENABLE_APIS_URL));
|
|
373
|
+
const fallback = await p.confirm({
|
|
374
|
+
message: 'Done enabling APIs manually?',
|
|
375
|
+
active: 'yes, continue',
|
|
376
|
+
inactive: 'not yet',
|
|
377
|
+
});
|
|
378
|
+
if (p.isCancel(fallback)) cancelled();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── Step 5: Install ──────────────────────────────────────────────────
|
|
383
|
+
p.log.step(chalk.cyan.bold('Step 5') + chalk.dim(' · ') + 'Install MCP server');
|
|
384
|
+
|
|
385
|
+
const hasClaude = hasCli('claude');
|
|
386
|
+
if (hasClaude) {
|
|
387
|
+
const install = await p.confirm({
|
|
388
|
+
message: 'Add to Claude Code as a user-scope MCP server?',
|
|
389
|
+
active: 'yes',
|
|
390
|
+
inactive: 'no',
|
|
391
|
+
initialValue: true,
|
|
392
|
+
});
|
|
393
|
+
if (p.isCancel(install)) cancelled();
|
|
394
|
+
|
|
395
|
+
if (install) {
|
|
396
|
+
const s = p.spinner();
|
|
397
|
+
s.start('Adding to Claude Code...');
|
|
398
|
+
try {
|
|
399
|
+
await runCommand('claude mcp add -s user google -- npx -y google-tools-mcp');
|
|
400
|
+
s.stop('Added to Claude Code!');
|
|
401
|
+
} catch (err) {
|
|
402
|
+
s.stop('Failed to add automatically');
|
|
403
|
+
p.log.warn(`Error: ${err.message}`);
|
|
404
|
+
p.log.message(`Run manually:\n${chalk.cyan('claude mcp add -s user google -- npx -y google-tools-mcp')}`);
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
p.log.message(`To add later:\n${chalk.cyan('claude mcp add -s user google -- npx -y google-tools-mcp')}`);
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
p.log.message([
|
|
411
|
+
'Add to your MCP client:',
|
|
412
|
+
'',
|
|
413
|
+
chalk.dim('Claude Code:'),
|
|
414
|
+
chalk.cyan(' claude mcp add -s user google -- npx -y google-tools-mcp'),
|
|
415
|
+
'',
|
|
416
|
+
chalk.dim('Other clients') + chalk.dim(' (.mcp.json):'),
|
|
417
|
+
chalk.cyan(' { "mcpServers": { "google": { "command": "npx", "args": ["-y", "google-tools-mcp"] } } }'),
|
|
418
|
+
].join('\n'));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
p.outro(chalk.green.bold('Setup complete!') + chalk.dim(' You\'re ready to use google-tools-mcp.'));
|
|
422
|
+
}
|