Skip to content

Commit 967de79

Browse files
karthikcsqclaude
andcommitted
Rich terminal UI for setup wizard using @clack/prompts
Replaces plain console.log with @clack/prompts for styled steps, spinners, confirm dialogs, and text input. Browser now opens only after user reads instructions and confirms, fixing focus-steal issue. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e26c8f7 commit 967de79

3 files changed

Lines changed: 867 additions & 102 deletions

File tree

dist/setup.js

Lines changed: 124 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Guided setup wizard for google-tools-mcp.
2-
// Opens the right Google Cloud Console URLs and saves credentials.
3-
import * as readline from 'readline';
2+
// Rich terminal UI using @clack/prompts.
3+
import * as p from '@clack/prompts';
4+
import chalk from 'chalk';
45
import * as fs from 'fs/promises';
56
import * as path from 'path';
67
import * as os from 'os';
@@ -44,10 +45,6 @@ function openBrowser(url) {
4445
exec(cmd, () => {});
4546
}
4647

47-
function prompt(rl, question) {
48-
return new Promise((resolve) => rl.question(question, resolve));
49-
}
50-
5148
function getConfigDir() {
5249
const xdg = process.env.XDG_CONFIG_HOME;
5350
const base = xdg || path.join(os.homedir(), '.config');
@@ -74,106 +71,162 @@ function runCommand(cmd) {
7471
});
7572
}
7673

74+
function cancelled() {
75+
p.cancel('Setup cancelled.');
76+
process.exit(0);
77+
}
78+
7779
// ---------------------------------------------------------------------------
7880
// Setup flow
7981
// ---------------------------------------------------------------------------
8082
export async function runSetup() {
81-
const rl = readline.createInterface({
82-
input: process.stdin,
83-
output: process.stdout,
83+
console.clear();
84+
85+
p.intro(chalk.bgCyan.bold.white(' google-tools-mcp setup '));
86+
87+
// ── Step 1: Enable APIs ──────────────────────────────────────────────
88+
p.log.step(chalk.cyan.bold('Step 1') + chalk.dim(' · ') + 'Enable Google APIs');
89+
p.log.message([
90+
'This will open Google Cloud Console to enable all required APIs.',
91+
chalk.dim('If you don\'t have a project yet, it will ask you to create one.'),
92+
'',
93+
chalk.dim('APIs: ') + APIS.map(a => chalk.yellow(a.replace('.googleapis.com', ''))).join(chalk.dim(', ')),
94+
].join('\n'));
95+
96+
const ready1 = await p.confirm({
97+
message: 'Ready? This will open your browser.',
98+
active: 'open browser',
99+
inactive: 'not yet',
84100
});
85-
86-
console.log('\n🔧 google-tools-mcp setup\n');
87-
88-
// Step 1: Enable APIs
89-
console.log('Step 1: Enable Google APIs');
90-
console.log('─────────────────────────');
91-
console.log('Opening Google Cloud Console to enable all required APIs.');
92-
console.log('If you don\'t have a project yet, it will ask you to create one.\n');
101+
if (p.isCancel(ready1)) cancelled();
93102
openBrowser(ENABLE_APIS_URL);
94-
await prompt(rl, 'Press Enter when done...');
95-
96-
// Step 2: OAuth consent screen
97-
console.log('\nStep 2: Configure OAuth consent screen');
98-
console.log('──────────────────────────────────────');
99-
console.log('Opening the OAuth consent screen configuration.');
100-
console.log(' • Choose "External" for the user type');
101-
console.log(' • Fill in the app name (anything is fine, e.g. "MCP")');
102-
console.log(' • Add your email as a test user\n');
103+
104+
const step1 = await p.confirm({
105+
message: 'Done enabling APIs?',
106+
active: 'yes, continue',
107+
inactive: 'not yet',
108+
});
109+
if (p.isCancel(step1)) cancelled();
110+
111+
// ── Step 2: OAuth consent screen ─────────────────────────────────────
112+
p.log.step(chalk.cyan.bold('Step 2') + chalk.dim(' · ') + 'Configure OAuth consent screen');
113+
p.log.message([
114+
`${chalk.white('›')} Choose ${chalk.bold('"External"')} for the user type`,
115+
`${chalk.white('›')} Fill in the app name ${chalk.dim('(anything works, e.g. "MCP")')}`,
116+
`${chalk.white('›')} Add your email as a ${chalk.bold('test user')}`,
117+
].join('\n'));
118+
119+
const ready2 = await p.confirm({
120+
message: 'Ready? This will open your browser.',
121+
active: 'open browser',
122+
inactive: 'not yet',
123+
});
124+
if (p.isCancel(ready2)) cancelled();
103125
openBrowser(CONSENT_SCREEN_URL);
104-
await prompt(rl, 'Press Enter when done...');
105-
106-
// Step 3: Create OAuth credentials
107-
console.log('\nStep 3: Create OAuth Client ID');
108-
console.log('──────────────────────────────');
109-
console.log('Opening the credentials page.');
110-
console.log(' • Select "Desktop application" as the type');
111-
console.log(' • Click Create, then copy the Client ID and Client Secret\n');
126+
127+
const step2 = await p.confirm({
128+
message: 'Done configuring consent screen?',
129+
active: 'yes, continue',
130+
inactive: 'not yet',
131+
});
132+
if (p.isCancel(step2)) cancelled();
133+
134+
// ── Step 3: Create OAuth credentials ─────────────────────────────────
135+
p.log.step(chalk.cyan.bold('Step 3') + chalk.dim(' · ') + 'Create OAuth Client ID');
136+
p.log.message([
137+
`${chalk.white('›')} Select ${chalk.bold('"Desktop application"')} as the type`,
138+
`${chalk.white('›')} Click ${chalk.bold('Create')}`,
139+
`${chalk.white('›')} Copy the ${chalk.bold('Client ID')} and ${chalk.bold('Client Secret')}`,
140+
].join('\n'));
141+
142+
const ready3 = await p.confirm({
143+
message: 'Ready? This will open your browser.',
144+
active: 'open browser',
145+
inactive: 'not yet',
146+
});
147+
if (p.isCancel(ready3)) cancelled();
112148
openBrowser(CREATE_CREDENTIALS_URL);
113-
await prompt(rl, 'Press Enter when you have your Client ID and Secret...');
114149

115-
// Step 4: Collect credentials
116-
console.log('');
117-
const clientId = (await prompt(rl, 'Client ID: ')).trim();
118-
const clientSecret = (await prompt(rl, 'Client Secret: ')).trim();
150+
const credentials = await p.group({
151+
clientId: () => p.text({
152+
message: 'Client ID',
153+
placeholder: 'xxxx.apps.googleusercontent.com',
154+
validate: (v) => {
155+
if (!v?.trim()) return 'Client ID is required';
156+
},
157+
}),
158+
clientSecret: () => p.text({
159+
message: 'Client Secret',
160+
placeholder: 'GOCSPX-xxxx',
161+
validate: (v) => {
162+
if (!v?.trim()) return 'Client Secret is required';
163+
},
164+
}),
165+
});
166+
if (p.isCancel(credentials)) cancelled();
119167

120-
if (!clientId || !clientSecret) {
121-
rl.close();
122-
throw new Error('Client ID and Client Secret are required.');
123-
}
168+
const clientId = credentials.clientId.trim();
169+
const clientSecret = credentials.clientSecret.trim();
124170

125-
// Save to config dir
171+
// ── Save credentials ─────────────────────────────────────────────────
126172
const configDir = getConfigDir();
127173
await fs.mkdir(configDir, { recursive: true });
128174
const envPath = path.join(configDir, '.env');
129175
const envContent = `GOOGLE_CLIENT_ID=${clientId}\nGOOGLE_CLIENT_SECRET=${clientSecret}\n`;
130176
await fs.writeFile(envPath, envContent);
131177
const displayPath = envPath.replace(os.homedir(), '~');
132-
console.log(`\nCredentials saved to ${displayPath}`);
178+
p.log.success(`Credentials saved to ${chalk.dim(displayPath)}`);
133179

134-
// Step 5: Run OAuth flow
135-
console.log('\nStep 4: Authenticate with Google');
136-
console.log('────────────────────────────────');
137-
console.log('Opening browser for OAuth consent...\n');
180+
// ── Step 4: Authenticate ─────────────────────────────────────────────
181+
p.log.step(chalk.cyan.bold('Step 4') + chalk.dim(' · ') + 'Authenticate with Google');
182+
p.log.message('Opening browser for OAuth consent...');
138183

139-
// Set env vars so auth picks them up immediately
140184
process.env.GOOGLE_CLIENT_ID = clientId;
141185
process.env.GOOGLE_CLIENT_SECRET = clientSecret;
142186

143187
const { runAuthFlow } = await import('./auth.js');
144188
await runAuthFlow();
145189

146-
// Step 6: Install into MCP client
147-
console.log('\nStep 5: Install');
148-
console.log('───────────────');
190+
p.log.success('Authenticated with Google!');
191+
192+
// ── Step 5: Install ──────────────────────────────────────────────────
193+
p.log.step(chalk.cyan.bold('Step 5') + chalk.dim(' · ') + 'Install MCP server');
149194

150195
const hasClaude = hasCli('claude');
151196
if (hasClaude) {
152-
const answer = (await prompt(rl, 'Add to Claude Code as a user-scope MCP server? (Y/n) ')).trim().toLowerCase();
153-
if (answer === '' || answer === 'y' || answer === 'yes') {
154-
console.log('Running: claude mcp add -s user google -- npx -y google-tools-mcp');
197+
const install = await p.confirm({
198+
message: 'Add to Claude Code as a user-scope MCP server?',
199+
active: 'yes',
200+
inactive: 'no',
201+
initialValue: true,
202+
});
203+
if (p.isCancel(install)) cancelled();
204+
205+
if (install) {
206+
const s = p.spinner();
207+
s.start('Adding to Claude Code...');
155208
try {
156209
await runCommand('claude mcp add -s user google -- npx -y google-tools-mcp');
157-
console.log('Added to Claude Code!');
210+
s.stop('Added to Claude Code!');
158211
} catch (err) {
159-
console.error('Failed to add:', err.message);
160-
console.log('You can add it manually:');
161-
console.log(' claude mcp add -s user google -- npx -y google-tools-mcp');
212+
s.stop('Failed to add automatically');
213+
p.log.warn(`Error: ${err.message}`);
214+
p.log.message(`Run manually:\n${chalk.cyan('claude mcp add -s user google -- npx -y google-tools-mcp')}`);
162215
}
163216
} else {
164-
console.log('\nTo add it later:');
165-
console.log(' claude mcp add -s user google -- npx -y google-tools-mcp');
217+
p.log.message(`To add later:\n${chalk.cyan('claude mcp add -s user google -- npx -y google-tools-mcp')}`);
166218
}
167219
} else {
168-
console.log('Add google-tools-mcp to your MCP client config:');
169-
console.log('');
170-
console.log(' Claude Code:');
171-
console.log(' claude mcp add -s user google -- npx -y google-tools-mcp');
172-
console.log('');
173-
console.log(' Other clients (.mcp.json / claude_desktop_config.json):');
174-
console.log(' { "mcpServers": { "google": { "command": "npx", "args": ["-y", "google-tools-mcp"] } } }');
220+
p.log.message([
221+
'Add to your MCP client:',
222+
'',
223+
chalk.dim('Claude Code:'),
224+
chalk.cyan(' claude mcp add -s user google -- npx -y google-tools-mcp'),
225+
'',
226+
chalk.dim('Other clients') + chalk.dim(' (.mcp.json):'),
227+
chalk.cyan(' { "mcpServers": { "google": { "command": "npx", "args": ["-y", "google-tools-mcp"] } } }'),
228+
].join('\n'));
175229
}
176230

177-
rl.close();
178-
console.log('\n✅ Setup complete! You\'re ready to use google-tools-mcp.\n');
231+
p.outro(chalk.green.bold('Setup complete!') + chalk.dim(' You\'re ready to use google-tools-mcp.'));
179232
}

0 commit comments

Comments
 (0)