twaddan-metabase-mcp 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.
Files changed (3) hide show
  1. package/README.md +66 -0
  2. package/index.js +247 -0
  3. package/package.json +30 -0
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # twaddan-metabase-mcp (Metabase MCP for Claude Code)
2
+
3
+ MCP server that connects Claude Code to your Metabase instance — query databases, explore tables, run SQL, and search saved questions.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx twaddan-metabase-mcp setup
9
+ ```
10
+
11
+ This will:
12
+ 1. Prompt for your Metabase URL and API key
13
+ 2. Validate the connection
14
+ 3. Register the MCP server with Claude Code
15
+ 4. Auto-allow all Metabase tools (no permission prompts)
16
+
17
+ Then just start Claude Code — all Metabase tools are ready.
18
+
19
+ ## Available Tools
20
+
21
+ | Tool | Description |
22
+ |------|-------------|
23
+ | `list_databases` | List all databases connected to Metabase |
24
+ | `list_tables` | List all tables in a specific database |
25
+ | `get_table_metadata` | Get column names, types, and details for a table |
26
+ | `run_sql_query` | Run a native SQL SELECT query against a database |
27
+ | `search_metabase` | Search for saved questions, dashboards, tables, and collections |
28
+ | `run_saved_question` | Execute a saved question/card by its ID |
29
+
30
+ ## Getting a Metabase API Key
31
+
32
+ 1. Log in to your Metabase instance
33
+ 2. Go to **Settings** → **Authentication** → **API Keys**
34
+ 3. Click **Create API Key**
35
+ 4. Copy the generated key
36
+
37
+ ## Manual Setup
38
+
39
+ If you prefer to configure manually:
40
+
41
+ ```bash
42
+ claude mcp add -s user \
43
+ -e METABASE_URL=https://metabase.example.com \
44
+ -e METABASE_API_KEY=your_api_key \
45
+ metabase -- npx twaddan-metabase-mcp
46
+ ```
47
+
48
+ Then add `"mcp__metabase"` to `~/.claude/settings.json` under `permissions.allow` to skip permission prompts.
49
+
50
+ ## Troubleshooting
51
+
52
+ **"METABASE_URL environment variable is required"**
53
+ The MCP server was started without environment variables. Re-run `npx twaddan-metabase-mcp setup` or check your Claude Code MCP configuration.
54
+
55
+ **"Could not connect"**
56
+ - Verify your Metabase URL is accessible from your machine
57
+ - Check that the API key is valid and has not expired
58
+ - Ensure the URL does not have a trailing slash
59
+
60
+ **Tools not appearing in Claude Code**
61
+ - Run `claude mcp list` to verify the server is registered
62
+ - Restart Claude Code after setup
63
+
64
+ ## License
65
+
66
+ MIT
package/index.js ADDED
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ── Setup CLI: `npx twaddan-metabase-mcp setup` ──
4
+ if (process.argv[2] === 'setup') {
5
+ const readline = await import('readline');
6
+ const { execSync } = await import('child_process');
7
+ const fs = await import('fs');
8
+ const path = await import('path');
9
+ const os = await import('os');
10
+
11
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
12
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
13
+
14
+ console.log('\n🔧 Metabase MCP Server — Setup\n');
15
+
16
+ const url = (await ask('Metabase URL (e.g. https://metabase.example.com): ')).replace(/\/+$/, '');
17
+ if (!url) { console.error('URL is required.'); process.exit(1); }
18
+
19
+ const apiKey = await ask('Metabase API key: ');
20
+ if (!apiKey) { console.error('API key is required.'); process.exit(1); }
21
+
22
+ rl.close();
23
+
24
+ // Validate connection
25
+ console.log('\nValidating connection...');
26
+ try {
27
+ const res = await fetch(`${url}/api/database`, {
28
+ headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
29
+ });
30
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
31
+ const data = await res.json();
32
+ const count = data.data?.length ?? 0;
33
+ console.log(`✅ Connected — found ${count} database${count !== 1 ? 's' : ''}\n`);
34
+ } catch (err) {
35
+ console.error(`❌ Could not connect to ${url}: ${err.message}`);
36
+ process.exit(1);
37
+ }
38
+
39
+ // Register MCP server with Claude Code
40
+ console.log('Registering MCP server with Claude Code...');
41
+ try {
42
+ execSync(
43
+ `claude mcp add -s user -e METABASE_URL=${url} -e METABASE_API_KEY=${apiKey} metabase -- npx twaddan-metabase-mcp`,
44
+ { stdio: 'inherit' }
45
+ );
46
+ console.log('✅ MCP server registered\n');
47
+ } catch {
48
+ console.error('❌ Failed to register MCP server. Make sure Claude Code CLI is installed.');
49
+ process.exit(1);
50
+ }
51
+
52
+ // Auto-allow metabase tools in Claude Code settings
53
+ console.log('Configuring permissions...');
54
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
55
+ try {
56
+ let settings = {};
57
+ if (fs.existsSync(settingsPath)) {
58
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
59
+ }
60
+ if (!settings.permissions) settings.permissions = {};
61
+ if (!Array.isArray(settings.permissions.allow)) settings.permissions.allow = [];
62
+
63
+ const rule = 'mcp__metabase';
64
+ if (!settings.permissions.allow.includes(rule)) {
65
+ settings.permissions.allow.push(rule);
66
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
67
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
68
+ console.log('✅ Auto-allowed all Metabase tools (mcp__metabase)\n');
69
+ } else {
70
+ console.log('✅ Metabase tools already allowed\n');
71
+ }
72
+ } catch (err) {
73
+ console.warn(`⚠️ Could not update settings: ${err.message}`);
74
+ console.warn(' You may need to manually allow "mcp__metabase" tools in ~/.claude/settings.json\n');
75
+ }
76
+
77
+ console.log('🎉 Setup complete! Start Claude Code and your Metabase tools are ready.\n');
78
+ process.exit(0);
79
+ }
80
+
81
+ // ── MCP Server Mode ──
82
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
83
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
84
+ import { z } from 'zod';
85
+
86
+ const METABASE_URL = process.env.METABASE_URL;
87
+ const METABASE_API_KEY = process.env.METABASE_API_KEY;
88
+
89
+ if (!METABASE_URL) {
90
+ console.error('METABASE_URL environment variable is required');
91
+ process.exit(1);
92
+ }
93
+
94
+ if (!METABASE_API_KEY) {
95
+ console.error('METABASE_API_KEY environment variable is required');
96
+ process.exit(1);
97
+ }
98
+
99
+ const headers = {
100
+ 'x-api-key': METABASE_API_KEY,
101
+ 'Content-Type': 'application/json',
102
+ };
103
+
104
+ async function metabaseRequest(method, path, body) {
105
+ const res = await fetch(`${METABASE_URL}${path}`, {
106
+ method,
107
+ headers,
108
+ body: body ? JSON.stringify(body) : undefined,
109
+ });
110
+
111
+ if (!res.ok) {
112
+ const text = await res.text().catch(() => '');
113
+ throw new Error(`Metabase API error (${res.status}): ${text || res.statusText}`);
114
+ }
115
+
116
+ return res.json();
117
+ }
118
+
119
+ function formatDataset(data) {
120
+ const columns = data.data.cols.map(c => c.name);
121
+ const rows = data.data.rows.map(row => {
122
+ const obj = {};
123
+ columns.forEach((col, i) => { obj[col] = row[i]; });
124
+ return obj;
125
+ });
126
+ return { columns, rows, row_count: rows.length };
127
+ }
128
+
129
+ // Create MCP server
130
+ const server = new McpServer({
131
+ name: 'metabase',
132
+ version: '1.0.0',
133
+ });
134
+
135
+ // Tool: List databases
136
+ server.tool(
137
+ 'list_databases',
138
+ 'List all databases connected to Metabase',
139
+ {},
140
+ async () => {
141
+ const data = await metabaseRequest('GET', '/api/database');
142
+ const databases = data.data.map(db => ({
143
+ id: db.id,
144
+ name: db.name,
145
+ engine: db.engine,
146
+ }));
147
+ return { content: [{ type: 'text', text: JSON.stringify(databases, null, 2) }] };
148
+ }
149
+ );
150
+
151
+ // Tool: List tables
152
+ server.tool(
153
+ 'list_tables',
154
+ 'List all tables in a specific database',
155
+ { database_id: z.number().describe('The Metabase database ID') },
156
+ async ({ database_id }) => {
157
+ const data = await metabaseRequest('GET', `/api/database/${database_id}/metadata`);
158
+ const tables = data.tables.map(t => ({
159
+ id: t.id,
160
+ name: t.name,
161
+ schema: t.schema,
162
+ description: t.description,
163
+ }));
164
+ return { content: [{ type: 'text', text: JSON.stringify(tables, null, 2) }] };
165
+ }
166
+ );
167
+
168
+ // Tool: Get table metadata (columns, types)
169
+ server.tool(
170
+ 'get_table_metadata',
171
+ 'Get column names, types, and details for a specific table',
172
+ { table_id: z.number().describe('The Metabase table ID') },
173
+ async ({ table_id }) => {
174
+ const data = await metabaseRequest('GET', `/api/table/${table_id}/query_metadata`);
175
+ const result = {
176
+ name: data.name,
177
+ schema: data.schema,
178
+ description: data.description,
179
+ columns: data.fields.map(f => ({
180
+ name: f.name,
181
+ type: f.database_type,
182
+ description: f.description,
183
+ semantic_type: f.semantic_type,
184
+ })),
185
+ };
186
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
187
+ }
188
+ );
189
+
190
+ // Tool: Run native SQL query
191
+ server.tool(
192
+ 'run_sql_query',
193
+ 'Run a native SQL SELECT query against a database via Metabase. Only SELECT/WITH queries allowed.',
194
+ {
195
+ database_id: z.number().describe('The Metabase database ID'),
196
+ sql: z.string().describe('The SQL SELECT query to run'),
197
+ },
198
+ async ({ database_id, sql }) => {
199
+ const trimmed = sql.replace(/--.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '').trim();
200
+ const upper = trimmed.toUpperCase();
201
+ if (!upper.startsWith('SELECT') && !upper.startsWith('WITH')) {
202
+ return { content: [{ type: 'text', text: 'Error: Only SELECT/WITH queries are allowed.' }], isError: true };
203
+ }
204
+
205
+ const data = await metabaseRequest('POST', '/api/dataset', {
206
+ database: database_id,
207
+ type: 'native',
208
+ native: { query: sql },
209
+ });
210
+ const result = formatDataset(data);
211
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
212
+ }
213
+ );
214
+
215
+ // Tool: Search Metabase
216
+ server.tool(
217
+ 'search_metabase',
218
+ 'Search for saved questions, dashboards, tables, and collections in Metabase',
219
+ { query: z.string().describe('Search query string') },
220
+ async ({ query }) => {
221
+ const data = await metabaseRequest('GET', `/api/search?q=${encodeURIComponent(query)}`);
222
+ const results = data.data.map(item => ({
223
+ id: item.id,
224
+ name: item.name,
225
+ description: item.description,
226
+ model: item.model,
227
+ collection: item.collection?.name || null,
228
+ }));
229
+ return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
230
+ }
231
+ );
232
+
233
+ // Tool: Run a saved question
234
+ server.tool(
235
+ 'run_saved_question',
236
+ 'Execute an existing saved question/card in Metabase by its ID',
237
+ { card_id: z.number().describe('The saved question/card ID') },
238
+ async ({ card_id }) => {
239
+ const data = await metabaseRequest('POST', `/api/card/${card_id}/query`);
240
+ const result = formatDataset(data);
241
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
242
+ }
243
+ );
244
+
245
+ // Start server
246
+ const transport = new StdioServerTransport();
247
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "twaddan-metabase-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for querying Metabase from Claude Code — databases, tables, SQL, saved questions & search",
5
+ "type": "module",
6
+ "bin": {
7
+ "twaddan-metabase-mcp": "index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "README.md"
12
+ ],
13
+ "keywords": [
14
+ "mcp",
15
+ "metabase",
16
+ "claude",
17
+ "claude-code",
18
+ "ai",
19
+ "analytics",
20
+ "sql",
21
+ "model-context-protocol"
22
+ ],
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.0.0"
29
+ }
30
+ }