mcp-gsheets 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/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "mcp-gsheets",
3
+ "version": "1.0.0",
4
+ "description": "Model Context Protocol (MCP) server for Google Sheets API integration",
5
+ "author": "freema",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "bin": {
11
+ "mcp-gsheets": "./dist/index.js"
12
+ },
13
+ "scripts": {
14
+ "dev": "tsx watch src/index.ts",
15
+ "build": "tsup",
16
+ "start": "node dist/index.js",
17
+ "setup": "node scripts/setup-mcp-config.js",
18
+ "clean": "rm -rf dist",
19
+ "typecheck": "tsc --noEmit",
20
+ "lint": "eslint src --ext .ts",
21
+ "lint:fix": "eslint src --ext .ts --fix",
22
+ "format": "prettier --write \"src/**/*.ts\"",
23
+ "format:check": "prettier --check \"src/**/*.ts\"",
24
+ "check": "npm run typecheck && npm run lint && npm run format:check",
25
+ "check:all": "npm run check && npm run test:run && npm run build",
26
+ "prepublishOnly": "npm run clean && npm run build",
27
+ "inspector": "npx @modelcontextprotocol/inspector node dist/index.js",
28
+ "inspector:dev": "NODE_ENV=development npx @modelcontextprotocol/inspector npx tsx src/index.ts",
29
+ "test:sheets": "node scripts/test-sheets-tools.js",
30
+ "test": "vitest",
31
+ "test:run": "vitest run",
32
+ "test:coverage": "vitest run --coverage",
33
+ "test:watch": "vitest watch",
34
+ "test:ui": "vitest --ui"
35
+ },
36
+ "keywords": [
37
+ "mcp",
38
+ "model-context-protocol",
39
+ "google-sheets",
40
+ "spreadsheet",
41
+ "api",
42
+ "claude",
43
+ "ai"
44
+ ],
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ },
48
+ "dependencies": {
49
+ "@modelcontextprotocol/sdk": "^0.5.0",
50
+ "googleapis": "^140.0.0",
51
+ "zod": "^3.22.4"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^20.11.0",
55
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
56
+ "@typescript-eslint/parser": "^6.21.0",
57
+ "@vitest/coverage-v8": "^3.1.4",
58
+ "@vitest/ui": "^3.1.4",
59
+ "dotenv": "^16.3.1",
60
+ "eslint": "^8.57.1",
61
+ "eslint-config-prettier": "^10.1.5",
62
+ "eslint-plugin-prettier": "^5.4.0",
63
+ "prettier": "^3.5.3",
64
+ "tsup": "^8.0.0",
65
+ "tsx": "^4.7.0",
66
+ "typescript": "^5.3.3",
67
+ "vitest": "^3.1.4"
68
+ },
69
+ "files": [
70
+ "dist",
71
+ "README.md",
72
+ "LICENSE",
73
+ "scripts"
74
+ ],
75
+ "repository": {
76
+ "type": "git",
77
+ "url": "git+https://github.com/freema/mcp-gsheets.git"
78
+ },
79
+ "bugs": {
80
+ "url": "https://github.com/freema/mcp-gsheets/issues"
81
+ },
82
+ "homepage": "https://github.com/freema/mcp-gsheets#readme"
83
+ }
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import readline from 'readline';
6
+ import os from 'os';
7
+ import { fileURLToPath } from 'url';
8
+ import { execSync } from 'child_process';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ const rl = readline.createInterface({
14
+ input: process.stdin,
15
+ output: process.stdout
16
+ });
17
+
18
+ // Colors for terminal output
19
+ const colors = {
20
+ reset: '\x1b[0m',
21
+ bright: '\x1b[1m',
22
+ green: '\x1b[32m',
23
+ yellow: '\x1b[33m',
24
+ blue: '\x1b[34m',
25
+ red: '\x1b[31m'
26
+ };
27
+
28
+ function question(query) {
29
+ return new Promise(resolve => rl.question(query, resolve));
30
+ }
31
+
32
+ function detectNodePath() {
33
+ try {
34
+ // First try to get current Node.js path
35
+ const nodeVersion = process.version;
36
+ console.log(`${colors.green}✓ Detected Node.js version: ${nodeVersion}${colors.reset}`);
37
+
38
+ // Check if using nvm
39
+ const nvmDir = process.env.NVM_DIR;
40
+ if (nvmDir) {
41
+ // Try to get the current version from nvm
42
+ try {
43
+ const currentVersion = execSync('nvm current', { encoding: 'utf8' }).trim();
44
+ const nodePath = path.join(nvmDir, 'versions', 'node', currentVersion, 'bin', 'node');
45
+ if (fs.existsSync(nodePath)) {
46
+ console.log(`${colors.green}✓ Using nvm Node.js at: ${nodePath}${colors.reset}`);
47
+ return nodePath;
48
+ }
49
+ } catch (e) {
50
+ }
51
+ }
52
+
53
+ // Use the current Node.js executable path
54
+ console.log(`${colors.green}✓ Using Node.js at: ${process.execPath}${colors.reset}`);
55
+ return process.execPath;
56
+ } catch (error) {
57
+ console.log(`${colors.yellow}⚠️ Could not detect Node.js path, using default${colors.reset}`);
58
+ return 'node';
59
+ }
60
+ }
61
+
62
+ async function main() {
63
+ console.log(`${colors.bright}${colors.blue}🚀 MCP Google Sheets Configuration Setup${colors.reset}\n`);
64
+
65
+ // Get project path
66
+ const projectPath = path.resolve(__dirname, '..');
67
+ const distIndexPath = path.join(projectPath, 'dist', 'index.js');
68
+
69
+ // Check if we need to build first
70
+ const srcIndexPath = path.join(projectPath, 'src', 'index.ts');
71
+ if (!fs.existsSync(distIndexPath) && fs.existsSync(srcIndexPath)) {
72
+ console.log(`${colors.yellow}⚠️ Dist file not found. Building project...${colors.reset}`);
73
+ try {
74
+ execSync('npm run build', { cwd: projectPath, stdio: 'inherit' });
75
+ console.log(`${colors.green}✓ Build completed${colors.reset}\n`);
76
+ } catch (error) {
77
+ console.log(`${colors.red}❌ Build failed. Please run 'npm run build' manually${colors.reset}`);
78
+ process.exit(1);
79
+ }
80
+ }
81
+
82
+ if (!fs.existsSync(distIndexPath)) {
83
+ console.log(`${colors.red}❌ Dist file not found at: ${distIndexPath}${colors.reset}`);
84
+ console.log(`${colors.yellow}Please run 'npm run build' first${colors.reset}`);
85
+ process.exit(1);
86
+ }
87
+
88
+ console.log(`${colors.green}✓ Found server dist at: ${distIndexPath}${colors.reset}\n`);
89
+
90
+ // Get Google Cloud info
91
+ console.log(`${colors.bright}Google Cloud Setup:${colors.reset}`);
92
+ const projectId = await question('Google Cloud Project ID: ');
93
+ const credentialsPath = await question('Path to service account JSON file: ');
94
+
95
+ // Validate credentials file
96
+ const absoluteCredPath = path.resolve(credentialsPath);
97
+ if (!fs.existsSync(absoluteCredPath)) {
98
+ console.log(`${colors.red}❌ Credentials file not found: ${absoluteCredPath}${colors.reset}`);
99
+ process.exit(1);
100
+ }
101
+
102
+ // Read service account email
103
+ let serviceAccountEmail = '';
104
+ try {
105
+ const credentials = JSON.parse(fs.readFileSync(absoluteCredPath, 'utf8'));
106
+ serviceAccountEmail = credentials.client_email;
107
+ console.log(`${colors.green}✓ Service account: ${serviceAccountEmail}${colors.reset}\n`);
108
+ } catch (error) {
109
+ console.log(`${colors.yellow}⚠️ Could not read service account email${colors.reset}\n`);
110
+ }
111
+
112
+ // Detect Node.js path
113
+ const nodePath = detectNodePath();
114
+
115
+ // Ask user about Node.js configuration
116
+ console.log(`\n${colors.bright}Node.js Configuration:${colors.reset}`);
117
+ console.log(`1. Use detected Node.js: ${nodePath}`);
118
+ console.log(`2. Specify custom Node.js path`);
119
+ console.log(`3. Use system default (node)`);
120
+
121
+ const nodeChoice = await question('\nSelect option (1-3): ');
122
+ let finalNodePath = nodePath;
123
+
124
+ switch (nodeChoice) {
125
+ case '2':
126
+ finalNodePath = await question('Enter full path to Node.js executable: ');
127
+ if (!fs.existsSync(finalNodePath)) {
128
+ console.log(`${colors.yellow}⚠️ Path not found, using detected path${colors.reset}`);
129
+ finalNodePath = nodePath;
130
+ }
131
+ break;
132
+ case '3':
133
+ finalNodePath = 'node';
134
+ break;
135
+ }
136
+
137
+ // Create MCP config
138
+ const mcpConfig = {
139
+ mcpServers: {
140
+ "mcp-gsheets": {
141
+ command: finalNodePath,
142
+ args: [distIndexPath],
143
+ env: {
144
+ GOOGLE_PROJECT_ID: projectId,
145
+ GOOGLE_APPLICATION_CREDENTIALS: absoluteCredPath
146
+ }
147
+ }
148
+ }
149
+ };
150
+
151
+ // Determine config path
152
+ const platform = os.platform();
153
+ let configPath;
154
+ let configDir;
155
+
156
+ if (platform === 'darwin') {
157
+ configDir = path.join(os.homedir(), 'Library', 'Application Support', 'Claude');
158
+ configPath = path.join(configDir, 'claude_desktop_config.json');
159
+ } else if (platform === 'win32') {
160
+ configDir = path.join(os.homedir(), 'AppData', 'Roaming', 'Claude');
161
+ configPath = path.join(configDir, 'claude_desktop_config.json');
162
+ } else {
163
+ configDir = path.join(os.homedir(), '.config', 'claude');
164
+ configPath = path.join(configDir, 'claude_desktop_config.json');
165
+ }
166
+
167
+ console.log(`${colors.bright}Configuration Options:${colors.reset}`);
168
+ console.log('1. Save to Claude Desktop config');
169
+ console.log('2. Display config (copy manually)');
170
+ console.log('3. Save to custom file');
171
+
172
+ const choice = await question('\nSelect option (1-3): ');
173
+
174
+ switch (choice) {
175
+ case '1':
176
+ // Check if config exists
177
+ let existingConfig = {};
178
+ if (fs.existsSync(configPath)) {
179
+ try {
180
+ existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
181
+ if (existingConfig.mcpServers && (existingConfig.mcpServers.gsheets || existingConfig.mcpServers['mcp-gsheets'])) {
182
+ const overwrite = await question(`\n${colors.yellow}⚠️ 'mcp-gsheets' server already exists. Overwrite? (y/n): ${colors.reset}`);
183
+ if (overwrite.toLowerCase() !== 'y') {
184
+ console.log('Cancelled.');
185
+ process.exit(0);
186
+ }
187
+ if (existingConfig.mcpServers.gsheets) {
188
+ delete existingConfig.mcpServers.gsheets;
189
+ }
190
+ }
191
+ } catch (error) {
192
+ console.log(`${colors.yellow}⚠️ Could not read existing config, will create new one${colors.reset}`);
193
+ }
194
+ }
195
+
196
+ // Merge configs
197
+ const finalConfig = {
198
+ ...existingConfig,
199
+ mcpServers: {
200
+ ...existingConfig.mcpServers,
201
+ ...mcpConfig.mcpServers
202
+ }
203
+ };
204
+
205
+ if (!fs.existsSync(configDir)) {
206
+ fs.mkdirSync(configDir, { recursive: true });
207
+ }
208
+
209
+ fs.writeFileSync(configPath, JSON.stringify(finalConfig, null, 2));
210
+ console.log(`\n${colors.green}✅ Configuration saved to: ${configPath}${colors.reset}`);
211
+ console.log(`\n${colors.yellow}⚠️ Restart Claude Desktop to apply changes${colors.reset}`);
212
+ break;
213
+
214
+ case '2':
215
+ console.log(`\n${colors.bright}Copy this configuration to your Claude Desktop config:${colors.reset}\n`);
216
+ console.log(JSON.stringify(mcpConfig, null, 2));
217
+ break;
218
+
219
+ case '3':
220
+ const customPath = await question('Enter file path: ');
221
+ fs.writeFileSync(customPath, JSON.stringify(mcpConfig, null, 2));
222
+ console.log(`\n${colors.green}✅ Configuration saved to: ${customPath}${colors.reset}`);
223
+ break;
224
+
225
+ default:
226
+ console.log(`${colors.red}Invalid option${colors.reset}`);
227
+ }
228
+
229
+ // Show next steps
230
+ console.log(`\n${colors.bright}${colors.blue}Next Steps:${colors.reset}`);
231
+ console.log(`1. Share your Google Sheets with: ${colors.green}${serviceAccountEmail}${colors.reset}`);
232
+ console.log(`2. Restart Claude Desktop`);
233
+ console.log(`3. Test with: "Read data from spreadsheet [YOUR_SPREADSHEET_ID]"`);
234
+
235
+ // Create .env file option
236
+ const createEnv = await question(`\n${colors.yellow}Create .env file for development? (y/n): ${colors.reset}`);
237
+ if (createEnv.toLowerCase() === 'y') {
238
+ const envContent = `# Google Cloud configuration
239
+ GOOGLE_PROJECT_ID=${projectId}
240
+ GOOGLE_APPLICATION_CREDENTIALS=${absoluteCredPath}
241
+
242
+ # Test spreadsheet ID (optional)
243
+ TEST_SPREADSHEET_ID=
244
+
245
+ # Service account email (for reference)
246
+ SERVICE_ACCOUNT_EMAIL=${serviceAccountEmail}
247
+ `;
248
+
249
+ const envPath = path.join(projectPath, '.env');
250
+ fs.writeFileSync(envPath, envContent);
251
+ console.log(`\n${colors.green}✅ Created .env file for development${colors.reset}`);
252
+ }
253
+
254
+ rl.close();
255
+ }
256
+
257
+ main().catch(error => {
258
+ console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
259
+ process.exit(1);
260
+ });