gemini-mcp-rust 0.1.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.
Files changed (2) hide show
  1. package/bin/cli.js +395 -0
  2. package/package.json +35 -0
package/bin/cli.js ADDED
@@ -0,0 +1,395 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import https from 'node:https';
6
+ import os from 'node:os';
7
+ import { execSync } from 'node:child_process';
8
+ import { select, confirm } from '@inquirer/prompts';
9
+
10
+ const REPO = 'pdxxxx/gemini-mcp-rust';
11
+ const BINARY_NAME = process.platform === 'win32' ? 'gemini-mcp.exe' : 'gemini-mcp';
12
+
13
+ // Default installation paths
14
+ const INSTALL_DIR = process.platform === 'win32'
15
+ ? path.join(process.env.LOCALAPPDATA || os.homedir(), 'Programs', 'gemini-mcp')
16
+ : path.join(os.homedir(), '.local', 'bin');
17
+
18
+ const INSTALL_PATH = path.join(INSTALL_DIR, BINARY_NAME);
19
+
20
+ // Colored logging utilities
21
+ const log = {
22
+ info: (msg) => console.log(`\x1b[36m[INFO]\x1b[0m ${msg}`),
23
+ success: (msg) => console.log(`\x1b[32m[SUCCESS]\x1b[0m ${msg}`),
24
+ warn: (msg) => console.log(`\x1b[33m[WARN]\x1b[0m ${msg}`),
25
+ error: (msg) => console.log(`\x1b[31m[ERROR]\x1b[0m ${msg}`),
26
+ };
27
+
28
+ // Allowed hosts for download redirects (security)
29
+ const ALLOWED_DOWNLOAD_HOSTS = [
30
+ 'github.com',
31
+ 'objects.githubusercontent.com',
32
+ 'github-releases.githubusercontent.com'
33
+ ];
34
+
35
+ // Platform detection and asset mapping
36
+ function getAssetDetails() {
37
+ const arch = process.arch;
38
+ const platform = process.platform;
39
+
40
+ // Supported architectures
41
+ const supportedArchs = ['x64', 'arm64'];
42
+ if (!supportedArchs.includes(arch)) {
43
+ throw new Error(`Unsupported architecture: ${arch}. Supported: x64, arm64`);
44
+ }
45
+
46
+ let assetKey = '';
47
+
48
+ if (platform === 'win32') {
49
+ // Windows ARM64 can run x64 binaries via emulation, and we don't build ARM64 Windows
50
+ if (arch === 'arm64') {
51
+ log.warn('Windows ARM64 detected. Using x64 binary (runs via emulation).');
52
+ }
53
+ assetKey = 'windows-amd64.exe';
54
+ } else if (platform === 'darwin') {
55
+ assetKey = arch === 'arm64' ? 'macos-arm64' : 'macos-amd64';
56
+ } else if (platform === 'linux') {
57
+ assetKey = arch === 'arm64' ? 'linux-arm64' : 'linux-amd64';
58
+ } else {
59
+ throw new Error(`Unsupported platform: ${platform}. Supported: win32, darwin, linux`);
60
+ }
61
+
62
+ return {
63
+ name: `gemini-mcp-${assetKey}`,
64
+ platform,
65
+ arch
66
+ };
67
+ }
68
+
69
+ // Fetch latest release from GitHub API
70
+ async function getLatestRelease() {
71
+ return new Promise((resolve, reject) => {
72
+ const options = {
73
+ hostname: 'api.github.com',
74
+ path: `/repos/${REPO}/releases/latest`,
75
+ headers: { 'User-Agent': 'gemini-mcp-installer' }
76
+ };
77
+
78
+ https.get(options, (res) => {
79
+ let data = '';
80
+ res.on('data', (chunk) => data += chunk);
81
+ res.on('end', () => {
82
+ if (res.statusCode === 200) {
83
+ try {
84
+ const release = JSON.parse(data);
85
+ resolve(release);
86
+ } catch (e) {
87
+ reject(new Error('Failed to parse GitHub API response'));
88
+ }
89
+ } else if (res.statusCode === 404) {
90
+ reject(new Error('No releases found. Please check the repository.'));
91
+ } else if (res.statusCode === 403) {
92
+ reject(new Error('GitHub API rate limit exceeded. Please try again later.'));
93
+ } else {
94
+ reject(new Error(`GitHub API returned status ${res.statusCode}`));
95
+ }
96
+ });
97
+ }).on('error', (e) => reject(new Error(`Network error: ${e.message}`)));
98
+ });
99
+ }
100
+
101
+ // Download file with redirect support and host validation
102
+ async function downloadFile(url, destPath) {
103
+ return new Promise((resolve, reject) => {
104
+ const request = (downloadUrl) => {
105
+ // Validate URL host for security
106
+ let urlObj;
107
+ try {
108
+ urlObj = new URL(downloadUrl);
109
+ } catch {
110
+ return reject(new Error(`Invalid URL: ${downloadUrl}`));
111
+ }
112
+
113
+ if (!ALLOWED_DOWNLOAD_HOSTS.includes(urlObj.hostname)) {
114
+ return reject(new Error(`Download blocked: ${urlObj.hostname} is not in allowed hosts`));
115
+ }
116
+
117
+ if (urlObj.protocol !== 'https:') {
118
+ return reject(new Error('Only HTTPS downloads are allowed'));
119
+ }
120
+
121
+ https.get(downloadUrl, (res) => {
122
+ // Handle redirects (GitHub releases use redirects)
123
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
124
+ return request(res.headers.location);
125
+ }
126
+
127
+ if (res.statusCode !== 200) {
128
+ return reject(new Error(`Download failed with status ${res.statusCode}`));
129
+ }
130
+
131
+ const totalSize = parseInt(res.headers['content-length'], 10);
132
+ let downloadedSize = 0;
133
+
134
+ const file = fs.createWriteStream(destPath);
135
+
136
+ res.on('data', (chunk) => {
137
+ downloadedSize += chunk.length;
138
+ if (totalSize) {
139
+ const percent = Math.round((downloadedSize / totalSize) * 100);
140
+ process.stdout.write(`\r\x1b[36m[INFO]\x1b[0m Downloading... ${percent}%`);
141
+ }
142
+ });
143
+
144
+ res.pipe(file);
145
+
146
+ file.on('finish', () => {
147
+ file.close();
148
+ console.log(); // New line after progress
149
+ resolve();
150
+ });
151
+
152
+ file.on('error', (err) => {
153
+ fs.unlink(destPath, () => {});
154
+ reject(err);
155
+ });
156
+ }).on('error', (e) => reject(new Error(`Download error: ${e.message}`)));
157
+ };
158
+ request(url);
159
+ });
160
+ }
161
+
162
+ // Check if claude CLI is available
163
+ function isClaudeAvailable() {
164
+ try {
165
+ execSync('claude --version', { stdio: 'ignore' });
166
+ return true;
167
+ } catch {
168
+ return false;
169
+ }
170
+ }
171
+
172
+ // Get installed version
173
+ function getInstalledVersion() {
174
+ if (!fs.existsSync(INSTALL_PATH)) {
175
+ return null;
176
+ }
177
+ try {
178
+ const output = execSync(`"${INSTALL_PATH}" --version`, { encoding: 'utf-8' });
179
+ const match = output.match(/(\d+\.\d+\.\d+)/);
180
+ return match ? match[1] : null;
181
+ } catch {
182
+ return null;
183
+ }
184
+ }
185
+
186
+ // Core actions
187
+ const Actions = {
188
+ async install() {
189
+ const assetDetails = getAssetDetails();
190
+ log.info(`Detected platform: ${assetDetails.platform} (${assetDetails.arch})`);
191
+
192
+ log.info('Fetching latest release...');
193
+ const release = await getLatestRelease();
194
+ const tag = release.tag_name;
195
+ log.info(`Latest version: ${tag}`);
196
+
197
+ // Find matching asset
198
+ const asset = release.assets.find(a => a.name === assetDetails.name);
199
+ if (!asset) {
200
+ throw new Error(`No compatible binary found for ${assetDetails.name}. Available: ${release.assets.map(a => a.name).join(', ')}`);
201
+ }
202
+
203
+ // Create installation directory
204
+ if (!fs.existsSync(INSTALL_DIR)) {
205
+ log.info(`Creating directory: ${INSTALL_DIR}`);
206
+ fs.mkdirSync(INSTALL_DIR, { recursive: true });
207
+ }
208
+
209
+ log.info(`Installing to: ${INSTALL_PATH}`);
210
+ await downloadFile(asset.browser_download_url, INSTALL_PATH);
211
+
212
+ // Set executable permissions on Unix-like systems
213
+ if (process.platform !== 'win32') {
214
+ fs.chmodSync(INSTALL_PATH, 0o755);
215
+ }
216
+
217
+ log.success(`Installed successfully: ${INSTALL_PATH}`);
218
+
219
+ // PATH warning
220
+ const pathEnv = process.env.PATH || '';
221
+ if (!pathEnv.split(path.delimiter).includes(INSTALL_DIR)) {
222
+ log.warn('Installation directory is not in your PATH.');
223
+ if (process.platform === 'win32') {
224
+ console.log(` Add "${INSTALL_DIR}" to your user environment variables.`);
225
+ } else {
226
+ console.log(` Add to your shell config (~/.zshrc or ~/.bashrc):`);
227
+ console.log(` export PATH="$PATH:${INSTALL_DIR}"`);
228
+ }
229
+ }
230
+
231
+ return INSTALL_PATH;
232
+ },
233
+
234
+ async configure() {
235
+ if (!fs.existsSync(INSTALL_PATH)) {
236
+ log.error(`Binary not found at ${INSTALL_PATH}`);
237
+ log.info('Please install first (option 1).');
238
+ return;
239
+ }
240
+
241
+ if (!isClaudeAvailable()) {
242
+ log.error('Claude CLI not found.');
243
+ log.info('Install Claude Code first: https://docs.anthropic.com/en/docs/claude-code');
244
+ return;
245
+ }
246
+
247
+ log.info('Configuring Claude Code...');
248
+ try {
249
+ // Remove existing config to avoid duplicates
250
+ try {
251
+ execSync('claude mcp remove gemini', { stdio: 'ignore' });
252
+ } catch {
253
+ // Ignore if not exists
254
+ }
255
+
256
+ execSync(`claude mcp add gemini "${INSTALL_PATH}"`, { stdio: 'inherit' });
257
+ log.success('Gemini MCP has been configured for Claude Code!');
258
+ } catch (e) {
259
+ log.error(`Configuration failed: ${e.message}`);
260
+ log.info(`You can manually run: claude mcp add gemini "${INSTALL_PATH}"`);
261
+ }
262
+ },
263
+
264
+ async update() {
265
+ const currentVersion = getInstalledVersion();
266
+
267
+ if (!currentVersion) {
268
+ log.warn('gemini-mcp is not installed.');
269
+ const doInstall = await confirm({ message: 'Install now?', default: true });
270
+ if (doInstall) {
271
+ return Actions.install();
272
+ }
273
+ return;
274
+ }
275
+
276
+ log.info(`Current version: v${currentVersion}`);
277
+ log.info('Checking for updates...');
278
+
279
+ const release = await getLatestRelease();
280
+ const latestVersion = release.tag_name.replace(/^v/, '');
281
+
282
+ if (currentVersion === latestVersion) {
283
+ log.success('Already on the latest version!');
284
+ const reinstall = await confirm({ message: 'Reinstall anyway?', default: false });
285
+ if (!reinstall) return;
286
+ } else {
287
+ log.info(`New version available: v${latestVersion}`);
288
+ const doUpdate = await confirm({ message: `Update to v${latestVersion}?`, default: true });
289
+ if (!doUpdate) return;
290
+ }
291
+
292
+ await Actions.install();
293
+ log.success(`Updated to v${latestVersion}!`);
294
+ },
295
+
296
+ async uninstall() {
297
+ let removed = false;
298
+
299
+ if (fs.existsSync(INSTALL_PATH)) {
300
+ const confirmDelete = await confirm({ message: `Delete ${INSTALL_PATH}?`, default: true });
301
+ if (confirmDelete) {
302
+ try {
303
+ fs.unlinkSync(INSTALL_PATH);
304
+ log.success('Binary removed.');
305
+ removed = true;
306
+ } catch (e) {
307
+ log.error(`Failed to remove binary: ${e.message}`);
308
+ }
309
+ }
310
+ } else {
311
+ log.warn('Binary not found (already removed or not installed).');
312
+ }
313
+
314
+ if (isClaudeAvailable()) {
315
+ const removeConfig = await confirm({ message: 'Remove from Claude configuration?', default: true });
316
+ if (removeConfig) {
317
+ try {
318
+ execSync('claude mcp remove gemini', { stdio: 'inherit' });
319
+ log.success('Claude configuration removed.');
320
+ removed = true;
321
+ } catch {
322
+ log.warn('Could not remove from Claude config (may not exist).');
323
+ }
324
+ }
325
+ }
326
+
327
+ if (removed) {
328
+ log.success('Uninstall complete!');
329
+ }
330
+ }
331
+ };
332
+
333
+ // Main menu
334
+ async function main() {
335
+ console.log();
336
+ console.log('\x1b[36m╔══════════════════════════════════════════╗\x1b[0m');
337
+ console.log('\x1b[36m║ Gemini MCP Server Manager ║\x1b[0m');
338
+ console.log('\x1b[36m╚══════════════════════════════════════════╝\x1b[0m');
339
+ console.log();
340
+
341
+ // Show current status
342
+ const installedVersion = getInstalledVersion();
343
+ if (installedVersion) {
344
+ log.info(`Installed version: v${installedVersion}`);
345
+ log.info(`Location: ${INSTALL_PATH}`);
346
+ } else {
347
+ log.info('gemini-mcp is not installed.');
348
+ }
349
+ console.log();
350
+
351
+ const choice = await select({
352
+ message: 'What would you like to do?',
353
+ choices: [
354
+ {
355
+ name: '1. Install gemini-mcp',
356
+ value: 'install',
357
+ description: 'Download and install the latest version'
358
+ },
359
+ {
360
+ name: '2. Configure Claude Code',
361
+ value: 'configure',
362
+ description: 'Register gemini-mcp with Claude CLI'
363
+ },
364
+ {
365
+ name: '3. Update gemini-mcp',
366
+ value: 'update',
367
+ description: 'Check for updates and upgrade'
368
+ },
369
+ {
370
+ name: '4. Uninstall',
371
+ value: 'uninstall',
372
+ description: 'Remove binary and configuration'
373
+ }
374
+ ],
375
+ });
376
+
377
+ console.log();
378
+
379
+ try {
380
+ await Actions[choice]();
381
+ } catch (error) {
382
+ log.error(error.message);
383
+ process.exit(1);
384
+ }
385
+ }
386
+
387
+ main().catch((error) => {
388
+ if (error.name === 'ExitPromptError') {
389
+ // User pressed Ctrl+C
390
+ console.log('\nCancelled.');
391
+ process.exit(0);
392
+ }
393
+ log.error(error.message);
394
+ process.exit(1);
395
+ });
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "gemini-mcp-rust",
3
+ "version": "0.1.0",
4
+ "description": "Interactive installer for Gemini MCP Server (Rust) - A high-performance MCP server wrapping Gemini CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "gemini-mcp-rust": "./bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18.0.0"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/pdxxxx/gemini-mcp-rust.git"
18
+ },
19
+ "keywords": [
20
+ "gemini",
21
+ "mcp",
22
+ "claude",
23
+ "cli",
24
+ "installer"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "bugs": {
29
+ "url": "https://github.com/pdxxxx/gemini-mcp-rust/issues"
30
+ },
31
+ "homepage": "https://github.com/pdxxxx/gemini-mcp-rust#readme",
32
+ "dependencies": {
33
+ "@inquirer/prompts": "^7.0.0"
34
+ }
35
+ }