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/LICENSE +21 -0
- package/README.md +273 -0
- package/dist/index.js +7573 -0
- package/package.json +83 -0
- package/scripts/setup-mcp-config.js +260 -0
- package/scripts/test-sheets-tools.js +416 -0
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
|
+
});
|