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.
- package/README.md +66 -0
- package/index.js +247 -0
- 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
|
+
}
|