blixify-charts-mcp 0.1.2 → 0.1.3
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/build/index.js +429 -196
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// 为老版本 Node.js 添加 AbortController polyfill
|
|
3
|
-
import AbortController from
|
|
3
|
+
import AbortController from "abort-controller";
|
|
4
4
|
global.AbortController = global.AbortController || AbortController;
|
|
5
5
|
/**
|
|
6
6
|
* Metabase MCP 服务器
|
|
@@ -13,9 +13,9 @@ global.AbortController = global.AbortController || AbortController;
|
|
|
13
13
|
*/
|
|
14
14
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
15
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
-
import { ListResourcesRequestSchema, ReadResourceRequestSchema,
|
|
17
|
-
import { z } from "zod";
|
|
16
|
+
import { CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
18
17
|
import axios from "axios";
|
|
18
|
+
import { z } from "zod";
|
|
19
19
|
// 自定义错误枚举
|
|
20
20
|
var ErrorCode;
|
|
21
21
|
(function (ErrorCode) {
|
|
@@ -38,15 +38,16 @@ const METABASE_URL = process.env.METABASE_URL;
|
|
|
38
38
|
const METABASE_USERNAME = process.env.METABASE_USERNAME;
|
|
39
39
|
const METABASE_PASSWORD = process.env.METABASE_PASSWORD;
|
|
40
40
|
const METABASE_API_KEY = process.env.METABASE_API_KEY;
|
|
41
|
-
if (!METABASE_URL ||
|
|
41
|
+
if (!METABASE_URL ||
|
|
42
|
+
(!METABASE_API_KEY && (!METABASE_USERNAME || !METABASE_PASSWORD))) {
|
|
42
43
|
throw new Error("Either (METABASE_URL and METABASE_API_KEY) or (METABASE_URL, METABASE_USERNAME, and METABASE_PASSWORD) environment variables are required");
|
|
43
44
|
}
|
|
44
45
|
// 创建自定义 Schema 对象,使用 z.object
|
|
45
46
|
const ListResourceTemplatesRequestSchema = z.object({
|
|
46
|
-
method: z.literal("resources/list_templates")
|
|
47
|
+
method: z.literal("resources/list_templates"),
|
|
47
48
|
});
|
|
48
49
|
const ListToolsRequestSchema = z.object({
|
|
49
|
-
method: z.literal("tools/list")
|
|
50
|
+
method: z.literal("tools/list"),
|
|
50
51
|
});
|
|
51
52
|
class MetabaseServer {
|
|
52
53
|
server;
|
|
@@ -69,28 +70,29 @@ class MetabaseServer {
|
|
|
69
70
|
},
|
|
70
71
|
});
|
|
71
72
|
if (METABASE_API_KEY) {
|
|
72
|
-
this.logInfo(
|
|
73
|
-
this.axiosInstance.defaults.headers.common[
|
|
73
|
+
this.logInfo("Using Metabase API Key for authentication.");
|
|
74
|
+
this.axiosInstance.defaults.headers.common["X-API-Key"] =
|
|
75
|
+
METABASE_API_KEY;
|
|
74
76
|
this.sessionToken = "api_key_used"; // Indicate API key is in use
|
|
75
77
|
}
|
|
76
78
|
else if (METABASE_USERNAME && METABASE_PASSWORD) {
|
|
77
|
-
this.logInfo(
|
|
79
|
+
this.logInfo("Using Metabase username/password for authentication.");
|
|
78
80
|
// Existing session token logic will apply
|
|
79
81
|
}
|
|
80
82
|
else {
|
|
81
83
|
// This case should ideally be caught by the initial environment variable check
|
|
82
84
|
// but as a safeguard:
|
|
83
|
-
this.logError(
|
|
85
|
+
this.logError("Metabase authentication credentials not configured properly.", {});
|
|
84
86
|
throw new Error("Metabase authentication credentials not provided or incomplete.");
|
|
85
87
|
}
|
|
86
88
|
this.setupResourceHandlers();
|
|
87
89
|
this.setupToolHandlers();
|
|
88
90
|
// Enhanced error handling with logging
|
|
89
91
|
this.server.onerror = (error) => {
|
|
90
|
-
this.logError(
|
|
92
|
+
this.logError("Server Error", error);
|
|
91
93
|
};
|
|
92
|
-
process.on(
|
|
93
|
-
this.logInfo(
|
|
94
|
+
process.on("SIGINT", async () => {
|
|
95
|
+
this.logInfo("Shutting down server...");
|
|
94
96
|
await this.server.close();
|
|
95
97
|
process.exit(0);
|
|
96
98
|
});
|
|
@@ -99,9 +101,9 @@ class MetabaseServer {
|
|
|
99
101
|
logInfo(message, data) {
|
|
100
102
|
const logMessage = {
|
|
101
103
|
timestamp: new Date().toISOString(),
|
|
102
|
-
level:
|
|
104
|
+
level: "info",
|
|
103
105
|
message,
|
|
104
|
-
data
|
|
106
|
+
data,
|
|
105
107
|
};
|
|
106
108
|
console.error(JSON.stringify(logMessage));
|
|
107
109
|
// MCP SDK changed, can't directly access session
|
|
@@ -118,15 +120,15 @@ class MetabaseServer {
|
|
|
118
120
|
const apiError = error;
|
|
119
121
|
const logMessage = {
|
|
120
122
|
timestamp: new Date().toISOString(),
|
|
121
|
-
level:
|
|
123
|
+
level: "error",
|
|
122
124
|
message,
|
|
123
|
-
error: errorObj.message ||
|
|
124
|
-
stack: errorObj.stack
|
|
125
|
+
error: errorObj.message || "Unknown error",
|
|
126
|
+
stack: errorObj.stack,
|
|
125
127
|
};
|
|
126
128
|
console.error(JSON.stringify(logMessage));
|
|
127
129
|
// MCP SDK changed, can't directly access session
|
|
128
130
|
try {
|
|
129
|
-
console.error(`ERROR: ${message} - ${errorObj.message ||
|
|
131
|
+
console.error(`ERROR: ${message} - ${errorObj.message || "Unknown error"}`);
|
|
130
132
|
}
|
|
131
133
|
catch (e) {
|
|
132
134
|
// Ignore if session not available
|
|
@@ -136,25 +138,27 @@ class MetabaseServer {
|
|
|
136
138
|
* 获取 Metabase 会话令牌
|
|
137
139
|
*/
|
|
138
140
|
async getSessionToken() {
|
|
139
|
-
if (this.sessionToken) {
|
|
141
|
+
if (this.sessionToken) {
|
|
142
|
+
// Handles both API key ("api_key_used") and actual session tokens
|
|
140
143
|
return this.sessionToken;
|
|
141
144
|
}
|
|
142
145
|
// This part should only be reached if using username/password and sessionToken is null
|
|
143
|
-
this.logInfo(
|
|
146
|
+
this.logInfo("Authenticating with Metabase using username/password...");
|
|
144
147
|
try {
|
|
145
|
-
const response = await this.axiosInstance.post(
|
|
148
|
+
const response = await this.axiosInstance.post("/api/session", {
|
|
146
149
|
username: METABASE_USERNAME,
|
|
147
150
|
password: METABASE_PASSWORD,
|
|
148
151
|
});
|
|
149
152
|
this.sessionToken = response.data.id;
|
|
150
153
|
// 设置默认请求头
|
|
151
|
-
this.axiosInstance.defaults.headers.common[
|
|
152
|
-
|
|
154
|
+
this.axiosInstance.defaults.headers.common["X-Metabase-Session"] =
|
|
155
|
+
this.sessionToken;
|
|
156
|
+
this.logInfo("Successfully authenticated with Metabase");
|
|
153
157
|
return this.sessionToken;
|
|
154
158
|
}
|
|
155
159
|
catch (error) {
|
|
156
|
-
this.logError(
|
|
157
|
-
throw new McpError(ErrorCode.InternalError,
|
|
160
|
+
this.logError("Authentication failed", error);
|
|
161
|
+
throw new McpError(ErrorCode.InternalError, "Failed to authenticate with Metabase");
|
|
158
162
|
}
|
|
159
163
|
}
|
|
160
164
|
/**
|
|
@@ -162,27 +166,31 @@ class MetabaseServer {
|
|
|
162
166
|
*/
|
|
163
167
|
setupResourceHandlers() {
|
|
164
168
|
this.server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
|
|
165
|
-
this.logInfo(
|
|
169
|
+
this.logInfo("Listing resources...", {
|
|
170
|
+
requestStructure: JSON.stringify(request),
|
|
171
|
+
});
|
|
166
172
|
if (!METABASE_API_KEY) {
|
|
167
173
|
await this.getSessionToken();
|
|
168
174
|
}
|
|
169
175
|
try {
|
|
170
176
|
// 获取仪表板列表
|
|
171
|
-
const dashboardsResponse = await this.axiosInstance.get(
|
|
172
|
-
this.logInfo(
|
|
177
|
+
const dashboardsResponse = await this.axiosInstance.get("/api/dashboard");
|
|
178
|
+
this.logInfo("Successfully listed resources", {
|
|
179
|
+
count: dashboardsResponse.data.length,
|
|
180
|
+
});
|
|
173
181
|
// 将仪表板作为资源返回
|
|
174
182
|
return {
|
|
175
183
|
resources: dashboardsResponse.data.map((dashboard) => ({
|
|
176
184
|
uri: `metabase://dashboard/${dashboard.id}`,
|
|
177
185
|
mimeType: "application/json",
|
|
178
186
|
name: dashboard.name,
|
|
179
|
-
description: `Metabase dashboard: ${dashboard.name}
|
|
180
|
-
}))
|
|
187
|
+
description: `Metabase dashboard: ${dashboard.name}`,
|
|
188
|
+
})),
|
|
181
189
|
};
|
|
182
190
|
}
|
|
183
191
|
catch (error) {
|
|
184
|
-
this.logError(
|
|
185
|
-
throw new McpError(ErrorCode.InternalError,
|
|
192
|
+
this.logError("Failed to list resources", error);
|
|
193
|
+
throw new McpError(ErrorCode.InternalError, "Failed to list Metabase resources");
|
|
186
194
|
}
|
|
187
195
|
});
|
|
188
196
|
// 资源模板
|
|
@@ -190,29 +198,31 @@ class MetabaseServer {
|
|
|
190
198
|
return {
|
|
191
199
|
resourceTemplates: [
|
|
192
200
|
{
|
|
193
|
-
uriTemplate:
|
|
194
|
-
name:
|
|
195
|
-
mimeType:
|
|
196
|
-
description:
|
|
201
|
+
uriTemplate: "metabase://dashboard/{id}",
|
|
202
|
+
name: "Dashboard by ID",
|
|
203
|
+
mimeType: "application/json",
|
|
204
|
+
description: "Get a Metabase dashboard by its ID",
|
|
197
205
|
},
|
|
198
206
|
{
|
|
199
|
-
uriTemplate:
|
|
200
|
-
name:
|
|
201
|
-
mimeType:
|
|
202
|
-
description:
|
|
207
|
+
uriTemplate: "metabase://card/{id}",
|
|
208
|
+
name: "Card by ID",
|
|
209
|
+
mimeType: "application/json",
|
|
210
|
+
description: "Get a Metabase question/card by its ID",
|
|
203
211
|
},
|
|
204
212
|
{
|
|
205
|
-
uriTemplate:
|
|
206
|
-
name:
|
|
207
|
-
mimeType:
|
|
208
|
-
description:
|
|
213
|
+
uriTemplate: "metabase://database/{id}",
|
|
214
|
+
name: "Database by ID",
|
|
215
|
+
mimeType: "application/json",
|
|
216
|
+
description: "Get a Metabase database by its ID",
|
|
209
217
|
},
|
|
210
218
|
],
|
|
211
219
|
};
|
|
212
220
|
});
|
|
213
221
|
// 读取资源
|
|
214
222
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
215
|
-
this.logInfo(
|
|
223
|
+
this.logInfo("Reading resource...", {
|
|
224
|
+
requestStructure: JSON.stringify(request),
|
|
225
|
+
});
|
|
216
226
|
if (!METABASE_API_KEY) {
|
|
217
227
|
await this.getSessionToken();
|
|
218
228
|
}
|
|
@@ -224,11 +234,13 @@ class MetabaseServer {
|
|
|
224
234
|
const dashboardId = match[1];
|
|
225
235
|
const response = await this.axiosInstance.get(`/api/dashboard/${dashboardId}`);
|
|
226
236
|
return {
|
|
227
|
-
contents: [
|
|
237
|
+
contents: [
|
|
238
|
+
{
|
|
228
239
|
uri: request.params?.uri,
|
|
229
240
|
mimeType: "application/json",
|
|
230
|
-
text: JSON.stringify(response.data, null, 2)
|
|
231
|
-
}
|
|
241
|
+
text: JSON.stringify(response.data, null, 2),
|
|
242
|
+
},
|
|
243
|
+
],
|
|
232
244
|
};
|
|
233
245
|
}
|
|
234
246
|
// 处理问题/卡片资源
|
|
@@ -236,11 +248,13 @@ class MetabaseServer {
|
|
|
236
248
|
const cardId = match[1];
|
|
237
249
|
const response = await this.axiosInstance.get(`/api/card/${cardId}`);
|
|
238
250
|
return {
|
|
239
|
-
contents: [
|
|
251
|
+
contents: [
|
|
252
|
+
{
|
|
240
253
|
uri: request.params?.uri,
|
|
241
254
|
mimeType: "application/json",
|
|
242
|
-
text: JSON.stringify(response.data, null, 2)
|
|
243
|
-
}
|
|
255
|
+
text: JSON.stringify(response.data, null, 2),
|
|
256
|
+
},
|
|
257
|
+
],
|
|
244
258
|
};
|
|
245
259
|
}
|
|
246
260
|
// 处理数据库资源
|
|
@@ -248,11 +262,13 @@ class MetabaseServer {
|
|
|
248
262
|
const databaseId = match[1];
|
|
249
263
|
const response = await this.axiosInstance.get(`/api/database/${databaseId}`);
|
|
250
264
|
return {
|
|
251
|
-
contents: [
|
|
265
|
+
contents: [
|
|
266
|
+
{
|
|
252
267
|
uri: request.params?.uri,
|
|
253
268
|
mimeType: "application/json",
|
|
254
|
-
text: JSON.stringify(response.data, null, 2)
|
|
255
|
-
}
|
|
269
|
+
text: JSON.stringify(response.data, null, 2),
|
|
270
|
+
},
|
|
271
|
+
],
|
|
256
272
|
};
|
|
257
273
|
}
|
|
258
274
|
else {
|
|
@@ -280,8 +296,8 @@ class MetabaseServer {
|
|
|
280
296
|
description: "List all dashboards in Metabase",
|
|
281
297
|
inputSchema: {
|
|
282
298
|
type: "object",
|
|
283
|
-
properties: {}
|
|
284
|
-
}
|
|
299
|
+
properties: {},
|
|
300
|
+
},
|
|
285
301
|
},
|
|
286
302
|
{
|
|
287
303
|
name: "list_cards",
|
|
@@ -291,18 +307,46 @@ class MetabaseServer {
|
|
|
291
307
|
properties: {
|
|
292
308
|
f: {
|
|
293
309
|
type: "string",
|
|
294
|
-
description: "Optional filter function, possible values: archived, table, database, using_model, bookmarked, using_segment, all, mine"
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
310
|
+
description: "Optional filter function, possible values: archived, table, database, using_model, bookmarked, using_segment, all, mine",
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
},
|
|
298
314
|
},
|
|
299
315
|
{
|
|
300
316
|
name: "list_databases",
|
|
301
317
|
description: "List all databases in Metabase",
|
|
302
318
|
inputSchema: {
|
|
303
319
|
type: "object",
|
|
304
|
-
properties: {}
|
|
305
|
-
}
|
|
320
|
+
properties: {},
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: "get_database",
|
|
325
|
+
description: "Get detailed information about a specific Metabase database including tables and schema",
|
|
326
|
+
inputSchema: {
|
|
327
|
+
type: "object",
|
|
328
|
+
properties: {
|
|
329
|
+
database_id: {
|
|
330
|
+
type: "number",
|
|
331
|
+
description: "ID of the database",
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
required: ["database_id"],
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: "get_database_metadata",
|
|
339
|
+
description: "Get complete metadata for a database including all tables, fields, and schema information",
|
|
340
|
+
inputSchema: {
|
|
341
|
+
type: "object",
|
|
342
|
+
properties: {
|
|
343
|
+
database_id: {
|
|
344
|
+
type: "number",
|
|
345
|
+
description: "ID of the database",
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
required: ["database_id"],
|
|
349
|
+
},
|
|
306
350
|
},
|
|
307
351
|
{
|
|
308
352
|
name: "execute_card",
|
|
@@ -312,15 +356,15 @@ class MetabaseServer {
|
|
|
312
356
|
properties: {
|
|
313
357
|
card_id: {
|
|
314
358
|
type: "number",
|
|
315
|
-
description: "ID of the card/question to execute"
|
|
359
|
+
description: "ID of the card/question to execute",
|
|
316
360
|
},
|
|
317
361
|
parameters: {
|
|
318
362
|
type: "object",
|
|
319
|
-
description: "Optional parameters for the query"
|
|
320
|
-
}
|
|
363
|
+
description: "Optional parameters for the query",
|
|
364
|
+
},
|
|
321
365
|
},
|
|
322
|
-
required: ["card_id"]
|
|
323
|
-
}
|
|
366
|
+
required: ["card_id"],
|
|
367
|
+
},
|
|
324
368
|
},
|
|
325
369
|
{
|
|
326
370
|
name: "get_dashboard_cards",
|
|
@@ -330,36 +374,40 @@ class MetabaseServer {
|
|
|
330
374
|
properties: {
|
|
331
375
|
dashboard_id: {
|
|
332
376
|
type: "number",
|
|
333
|
-
description: "ID of the dashboard"
|
|
334
|
-
}
|
|
377
|
+
description: "ID of the dashboard",
|
|
378
|
+
},
|
|
335
379
|
},
|
|
336
|
-
required: ["dashboard_id"]
|
|
337
|
-
}
|
|
380
|
+
required: ["dashboard_id"],
|
|
381
|
+
},
|
|
338
382
|
},
|
|
339
383
|
{
|
|
340
384
|
name: "execute_query",
|
|
341
|
-
description: "Execute a SQL query against a Metabase database",
|
|
385
|
+
description: "Execute a SQL query against a Metabase database, or MongoDB aggregation pipeline against MongoDB databases",
|
|
342
386
|
inputSchema: {
|
|
343
387
|
type: "object",
|
|
344
388
|
properties: {
|
|
345
389
|
database_id: {
|
|
346
390
|
type: "number",
|
|
347
|
-
description: "ID of the database to query"
|
|
391
|
+
description: "ID of the database to query",
|
|
348
392
|
},
|
|
349
393
|
query: {
|
|
350
394
|
type: "string",
|
|
351
|
-
description: "SQL query
|
|
395
|
+
description: "SQL query for SQL databases, or MongoDB aggregation pipeline as JSON string (e.g., '[{\"$limit\": 10}]') for MongoDB databases",
|
|
396
|
+
},
|
|
397
|
+
collection: {
|
|
398
|
+
type: "string",
|
|
399
|
+
description: "MongoDB collection name (required for MongoDB databases, e.g., 'kpj-user-profiles'). Ignored for SQL databases.",
|
|
352
400
|
},
|
|
353
401
|
native_parameters: {
|
|
354
402
|
type: "array",
|
|
355
403
|
description: "Optional parameters for the query",
|
|
356
404
|
items: {
|
|
357
|
-
type: "object"
|
|
358
|
-
}
|
|
359
|
-
}
|
|
405
|
+
type: "object",
|
|
406
|
+
},
|
|
407
|
+
},
|
|
360
408
|
},
|
|
361
|
-
required: ["database_id", "query"]
|
|
362
|
-
}
|
|
409
|
+
required: ["database_id", "query"],
|
|
410
|
+
},
|
|
363
411
|
},
|
|
364
412
|
{
|
|
365
413
|
name: "create_card",
|
|
@@ -368,14 +416,34 @@ class MetabaseServer {
|
|
|
368
416
|
type: "object",
|
|
369
417
|
properties: {
|
|
370
418
|
name: { type: "string", description: "Name of the card" },
|
|
371
|
-
dataset_query: {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
419
|
+
dataset_query: {
|
|
420
|
+
type: "object",
|
|
421
|
+
description: "The query for the card (e.g., MBQL or native query)",
|
|
422
|
+
},
|
|
423
|
+
display: {
|
|
424
|
+
type: "string",
|
|
425
|
+
description: "Display type (e.g., 'table', 'line', 'bar')",
|
|
426
|
+
},
|
|
427
|
+
visualization_settings: {
|
|
428
|
+
type: "object",
|
|
429
|
+
description: "Settings for the visualization",
|
|
430
|
+
},
|
|
431
|
+
collection_id: {
|
|
432
|
+
type: "number",
|
|
433
|
+
description: "Optional ID of the collection to save the card in",
|
|
434
|
+
},
|
|
435
|
+
description: {
|
|
436
|
+
type: "string",
|
|
437
|
+
description: "Optional description for the card",
|
|
438
|
+
},
|
|
376
439
|
},
|
|
377
|
-
required: [
|
|
378
|
-
|
|
440
|
+
required: [
|
|
441
|
+
"name",
|
|
442
|
+
"dataset_query",
|
|
443
|
+
"display",
|
|
444
|
+
"visualization_settings",
|
|
445
|
+
],
|
|
446
|
+
},
|
|
379
447
|
},
|
|
380
448
|
{
|
|
381
449
|
name: "update_card",
|
|
@@ -383,17 +451,32 @@ class MetabaseServer {
|
|
|
383
451
|
inputSchema: {
|
|
384
452
|
type: "object",
|
|
385
453
|
properties: {
|
|
386
|
-
card_id: {
|
|
454
|
+
card_id: {
|
|
455
|
+
type: "number",
|
|
456
|
+
description: "ID of the card to update",
|
|
457
|
+
},
|
|
387
458
|
name: { type: "string", description: "New name for the card" },
|
|
388
|
-
dataset_query: {
|
|
459
|
+
dataset_query: {
|
|
460
|
+
type: "object",
|
|
461
|
+
description: "New query for the card",
|
|
462
|
+
},
|
|
389
463
|
display: { type: "string", description: "New display type" },
|
|
390
|
-
visualization_settings: {
|
|
391
|
-
|
|
464
|
+
visualization_settings: {
|
|
465
|
+
type: "object",
|
|
466
|
+
description: "New visualization settings",
|
|
467
|
+
},
|
|
468
|
+
collection_id: {
|
|
469
|
+
type: "number",
|
|
470
|
+
description: "New collection ID",
|
|
471
|
+
},
|
|
392
472
|
description: { type: "string", description: "New description" },
|
|
393
|
-
archived: {
|
|
473
|
+
archived: {
|
|
474
|
+
type: "boolean",
|
|
475
|
+
description: "Set to true to archive the card",
|
|
476
|
+
},
|
|
394
477
|
},
|
|
395
|
-
required: ["card_id"]
|
|
396
|
-
}
|
|
478
|
+
required: ["card_id"],
|
|
479
|
+
},
|
|
397
480
|
},
|
|
398
481
|
{
|
|
399
482
|
name: "delete_card",
|
|
@@ -401,11 +484,18 @@ class MetabaseServer {
|
|
|
401
484
|
inputSchema: {
|
|
402
485
|
type: "object",
|
|
403
486
|
properties: {
|
|
404
|
-
card_id: {
|
|
405
|
-
|
|
487
|
+
card_id: {
|
|
488
|
+
type: "number",
|
|
489
|
+
description: "ID of the card to delete",
|
|
490
|
+
},
|
|
491
|
+
hard_delete: {
|
|
492
|
+
type: "boolean",
|
|
493
|
+
description: "Set to true for hard delete, false (default) for archive",
|
|
494
|
+
default: false,
|
|
495
|
+
},
|
|
406
496
|
},
|
|
407
|
-
required: ["card_id"]
|
|
408
|
-
}
|
|
497
|
+
required: ["card_id"],
|
|
498
|
+
},
|
|
409
499
|
},
|
|
410
500
|
{
|
|
411
501
|
name: "create_dashboard",
|
|
@@ -414,12 +504,22 @@ class MetabaseServer {
|
|
|
414
504
|
type: "object",
|
|
415
505
|
properties: {
|
|
416
506
|
name: { type: "string", description: "Name of the dashboard" },
|
|
417
|
-
description: {
|
|
418
|
-
|
|
419
|
-
|
|
507
|
+
description: {
|
|
508
|
+
type: "string",
|
|
509
|
+
description: "Optional description for the dashboard",
|
|
510
|
+
},
|
|
511
|
+
parameters: {
|
|
512
|
+
type: "array",
|
|
513
|
+
description: "Optional parameters for the dashboard",
|
|
514
|
+
items: { type: "object" },
|
|
515
|
+
},
|
|
516
|
+
collection_id: {
|
|
517
|
+
type: "number",
|
|
518
|
+
description: "Optional ID of the collection to save the dashboard in",
|
|
519
|
+
},
|
|
420
520
|
},
|
|
421
|
-
required: ["name"]
|
|
422
|
-
}
|
|
521
|
+
required: ["name"],
|
|
522
|
+
},
|
|
423
523
|
},
|
|
424
524
|
{
|
|
425
525
|
name: "update_dashboard",
|
|
@@ -427,15 +527,34 @@ class MetabaseServer {
|
|
|
427
527
|
inputSchema: {
|
|
428
528
|
type: "object",
|
|
429
529
|
properties: {
|
|
430
|
-
dashboard_id: {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
530
|
+
dashboard_id: {
|
|
531
|
+
type: "number",
|
|
532
|
+
description: "ID of the dashboard to update",
|
|
533
|
+
},
|
|
534
|
+
name: {
|
|
535
|
+
type: "string",
|
|
536
|
+
description: "New name for the dashboard",
|
|
537
|
+
},
|
|
538
|
+
description: {
|
|
539
|
+
type: "string",
|
|
540
|
+
description: "New description for the dashboard",
|
|
541
|
+
},
|
|
542
|
+
parameters: {
|
|
543
|
+
type: "array",
|
|
544
|
+
description: "New parameters for the dashboard",
|
|
545
|
+
items: { type: "object" },
|
|
546
|
+
},
|
|
547
|
+
collection_id: {
|
|
548
|
+
type: "number",
|
|
549
|
+
description: "New collection ID",
|
|
550
|
+
},
|
|
551
|
+
archived: {
|
|
552
|
+
type: "boolean",
|
|
553
|
+
description: "Set to true to archive the dashboard",
|
|
554
|
+
},
|
|
436
555
|
},
|
|
437
|
-
required: ["dashboard_id"]
|
|
438
|
-
}
|
|
556
|
+
required: ["dashboard_id"],
|
|
557
|
+
},
|
|
439
558
|
},
|
|
440
559
|
{
|
|
441
560
|
name: "delete_dashboard",
|
|
@@ -443,48 +562,107 @@ class MetabaseServer {
|
|
|
443
562
|
inputSchema: {
|
|
444
563
|
type: "object",
|
|
445
564
|
properties: {
|
|
446
|
-
dashboard_id: {
|
|
447
|
-
|
|
565
|
+
dashboard_id: {
|
|
566
|
+
type: "number",
|
|
567
|
+
description: "ID of the dashboard to delete",
|
|
568
|
+
},
|
|
569
|
+
hard_delete: {
|
|
570
|
+
type: "boolean",
|
|
571
|
+
description: "Set to true for hard delete, false (default) for archive",
|
|
572
|
+
default: false,
|
|
573
|
+
},
|
|
448
574
|
},
|
|
449
|
-
required: ["dashboard_id"]
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
]
|
|
575
|
+
required: ["dashboard_id"],
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
],
|
|
453
579
|
};
|
|
454
580
|
});
|
|
455
581
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
456
|
-
this.logInfo(
|
|
582
|
+
this.logInfo("Calling tool...", {
|
|
583
|
+
requestStructure: JSON.stringify(request),
|
|
584
|
+
});
|
|
457
585
|
if (!METABASE_API_KEY) {
|
|
458
586
|
await this.getSessionToken();
|
|
459
587
|
}
|
|
460
588
|
try {
|
|
461
589
|
switch (request.params?.name) {
|
|
462
590
|
case "list_dashboards": {
|
|
463
|
-
const response = await this.axiosInstance.get(
|
|
591
|
+
const response = await this.axiosInstance.get("/api/dashboard");
|
|
464
592
|
return {
|
|
465
|
-
content: [
|
|
593
|
+
content: [
|
|
594
|
+
{
|
|
466
595
|
type: "text",
|
|
467
|
-
text: JSON.stringify(response.data, null, 2)
|
|
468
|
-
}
|
|
596
|
+
text: JSON.stringify(response.data, null, 2),
|
|
597
|
+
},
|
|
598
|
+
],
|
|
469
599
|
};
|
|
470
600
|
}
|
|
471
601
|
case "list_cards": {
|
|
472
602
|
const f = request.params?.arguments?.f || "all";
|
|
473
603
|
const response = await this.axiosInstance.get(`/api/card?f=${f}`);
|
|
474
604
|
return {
|
|
475
|
-
content: [
|
|
605
|
+
content: [
|
|
606
|
+
{
|
|
476
607
|
type: "text",
|
|
477
|
-
text: JSON.stringify(response.data, null, 2)
|
|
478
|
-
}
|
|
608
|
+
text: JSON.stringify(response.data, null, 2),
|
|
609
|
+
},
|
|
610
|
+
],
|
|
479
611
|
};
|
|
480
612
|
}
|
|
481
613
|
case "list_databases": {
|
|
482
|
-
const response = await this.axiosInstance.get(
|
|
614
|
+
const response = await this.axiosInstance.get("/api/database");
|
|
483
615
|
return {
|
|
484
|
-
content: [
|
|
616
|
+
content: [
|
|
617
|
+
{
|
|
485
618
|
type: "text",
|
|
486
|
-
text: JSON.stringify(response.data, null, 2)
|
|
487
|
-
}
|
|
619
|
+
text: JSON.stringify(response.data, null, 2),
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
case "get_database": {
|
|
625
|
+
const databaseId = request.params?.arguments?.database_id;
|
|
626
|
+
if (!databaseId) {
|
|
627
|
+
throw new McpError(ErrorCode.InvalidParams, "Database ID is required");
|
|
628
|
+
}
|
|
629
|
+
const response = await this.axiosInstance.get(`/api/database/${databaseId}`);
|
|
630
|
+
return {
|
|
631
|
+
content: [
|
|
632
|
+
{
|
|
633
|
+
type: "text",
|
|
634
|
+
text: JSON.stringify(response.data, null, 2),
|
|
635
|
+
},
|
|
636
|
+
],
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
case "get_database_metadata": {
|
|
640
|
+
const databaseId = request.params?.arguments?.database_id;
|
|
641
|
+
if (!databaseId) {
|
|
642
|
+
throw new McpError(ErrorCode.InvalidParams, "Database ID is required");
|
|
643
|
+
}
|
|
644
|
+
const response = await this.axiosInstance.get(`/api/database/${databaseId}/metadata`);
|
|
645
|
+
// Filter to only include table names/IDs and field names/IDs
|
|
646
|
+
const filteredData = {
|
|
647
|
+
id: response.data.id,
|
|
648
|
+
name: response.data.name,
|
|
649
|
+
tables: response.data.tables?.map((table) => ({
|
|
650
|
+
id: table.id,
|
|
651
|
+
name: table.name,
|
|
652
|
+
fields: table.fields?.map((field) => ({
|
|
653
|
+
id: field.id,
|
|
654
|
+
name: field.name,
|
|
655
|
+
database_type: field.database_type,
|
|
656
|
+
})) || [],
|
|
657
|
+
})) || [],
|
|
658
|
+
};
|
|
659
|
+
return {
|
|
660
|
+
content: [
|
|
661
|
+
{
|
|
662
|
+
type: "text",
|
|
663
|
+
text: JSON.stringify(filteredData, null, 2),
|
|
664
|
+
},
|
|
665
|
+
],
|
|
488
666
|
};
|
|
489
667
|
}
|
|
490
668
|
case "execute_card": {
|
|
@@ -495,10 +673,12 @@ class MetabaseServer {
|
|
|
495
673
|
const parameters = request.params?.arguments?.parameters || {};
|
|
496
674
|
const response = await this.axiosInstance.post(`/api/card/${cardId}/query`, { parameters });
|
|
497
675
|
return {
|
|
498
|
-
content: [
|
|
676
|
+
content: [
|
|
677
|
+
{
|
|
499
678
|
type: "text",
|
|
500
|
-
text: JSON.stringify(response.data, null, 2)
|
|
501
|
-
}
|
|
679
|
+
text: JSON.stringify(response.data, null, 2),
|
|
680
|
+
},
|
|
681
|
+
],
|
|
502
682
|
};
|
|
503
683
|
}
|
|
504
684
|
case "get_dashboard_cards": {
|
|
@@ -508,43 +688,74 @@ class MetabaseServer {
|
|
|
508
688
|
}
|
|
509
689
|
const response = await this.axiosInstance.get(`/api/dashboard/${dashboardId}`);
|
|
510
690
|
return {
|
|
511
|
-
content: [
|
|
691
|
+
content: [
|
|
692
|
+
{
|
|
512
693
|
type: "text",
|
|
513
|
-
text: JSON.stringify(response.data.cards, null, 2)
|
|
514
|
-
}
|
|
694
|
+
text: JSON.stringify(response.data.cards, null, 2),
|
|
695
|
+
},
|
|
696
|
+
],
|
|
515
697
|
};
|
|
516
698
|
}
|
|
517
699
|
case "execute_query": {
|
|
518
700
|
const databaseId = request.params?.arguments?.database_id;
|
|
519
701
|
const query = request.params?.arguments?.query;
|
|
702
|
+
const collectionParam = request.params?.arguments?.collection;
|
|
520
703
|
const nativeParameters = request.params?.arguments?.native_parameters || [];
|
|
521
704
|
if (!databaseId) {
|
|
522
705
|
throw new McpError(ErrorCode.InvalidParams, "Database ID is required");
|
|
523
706
|
}
|
|
524
707
|
if (!query) {
|
|
525
|
-
throw new McpError(ErrorCode.InvalidParams, "
|
|
708
|
+
throw new McpError(ErrorCode.InvalidParams, "Query is required");
|
|
526
709
|
}
|
|
527
|
-
//
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
710
|
+
// Get database details to check engine type
|
|
711
|
+
const dbResponse = await this.axiosInstance.get(`/api/database/${databaseId}`);
|
|
712
|
+
const dbEngine = dbResponse.data.engine;
|
|
713
|
+
let queryData;
|
|
714
|
+
if (dbEngine === "mongo") {
|
|
715
|
+
// MongoDB query format
|
|
716
|
+
if (!collectionParam) {
|
|
717
|
+
throw new McpError(ErrorCode.InvalidParams, "Collection name is required for MongoDB queries");
|
|
718
|
+
}
|
|
719
|
+
const queryStr = String(query);
|
|
720
|
+
queryData = {
|
|
721
|
+
type: "native",
|
|
722
|
+
native: {
|
|
723
|
+
collection: collectionParam,
|
|
724
|
+
query: queryStr,
|
|
725
|
+
template_tags: {},
|
|
726
|
+
},
|
|
727
|
+
parameters: nativeParameters,
|
|
728
|
+
database: databaseId,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
// SQL query format
|
|
733
|
+
queryData = {
|
|
734
|
+
type: "native",
|
|
735
|
+
native: {
|
|
736
|
+
query: query,
|
|
737
|
+
template_tags: {},
|
|
738
|
+
},
|
|
739
|
+
parameters: nativeParameters,
|
|
740
|
+
database: databaseId,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
const response = await this.axiosInstance.post("/api/dataset", queryData);
|
|
538
744
|
return {
|
|
539
|
-
content: [
|
|
745
|
+
content: [
|
|
746
|
+
{
|
|
540
747
|
type: "text",
|
|
541
|
-
text: JSON.stringify(response.data, null, 2)
|
|
542
|
-
}
|
|
748
|
+
text: JSON.stringify(response.data, null, 2),
|
|
749
|
+
},
|
|
750
|
+
],
|
|
543
751
|
};
|
|
544
752
|
}
|
|
545
753
|
case "create_card": {
|
|
546
|
-
const { name, dataset_query, display, visualization_settings, collection_id, description } = request.params?.arguments || {};
|
|
547
|
-
if (!name ||
|
|
754
|
+
const { name, dataset_query, display, visualization_settings, collection_id, description, } = request.params?.arguments || {};
|
|
755
|
+
if (!name ||
|
|
756
|
+
!dataset_query ||
|
|
757
|
+
!display ||
|
|
758
|
+
!visualization_settings) {
|
|
548
759
|
throw new McpError(ErrorCode.InvalidParams, "Missing required fields for create_card: name, dataset_query, display, visualization_settings");
|
|
549
760
|
}
|
|
550
761
|
const createCardBody = {
|
|
@@ -557,12 +768,14 @@ class MetabaseServer {
|
|
|
557
768
|
createCardBody.collection_id = collection_id;
|
|
558
769
|
if (description !== undefined)
|
|
559
770
|
createCardBody.description = description;
|
|
560
|
-
const response = await this.axiosInstance.post(
|
|
771
|
+
const response = await this.axiosInstance.post("/api/card", createCardBody);
|
|
561
772
|
return {
|
|
562
|
-
content: [
|
|
773
|
+
content: [
|
|
774
|
+
{
|
|
563
775
|
type: "text",
|
|
564
|
-
text: JSON.stringify(response.data, null, 2)
|
|
565
|
-
}
|
|
776
|
+
text: JSON.stringify(response.data, null, 2),
|
|
777
|
+
},
|
|
778
|
+
],
|
|
566
779
|
};
|
|
567
780
|
}
|
|
568
781
|
case "update_card": {
|
|
@@ -575,10 +788,12 @@ class MetabaseServer {
|
|
|
575
788
|
}
|
|
576
789
|
const response = await this.axiosInstance.put(`/api/card/${card_id}`, updateFields);
|
|
577
790
|
return {
|
|
578
|
-
content: [
|
|
791
|
+
content: [
|
|
792
|
+
{
|
|
579
793
|
type: "text",
|
|
580
|
-
text: JSON.stringify(response.data, null, 2)
|
|
581
|
-
}
|
|
794
|
+
text: JSON.stringify(response.data, null, 2),
|
|
795
|
+
},
|
|
796
|
+
],
|
|
582
797
|
};
|
|
583
798
|
}
|
|
584
799
|
case "delete_card": {
|
|
@@ -589,22 +804,28 @@ class MetabaseServer {
|
|
|
589
804
|
if (hard_delete) {
|
|
590
805
|
await this.axiosInstance.delete(`/api/card/${card_id}`);
|
|
591
806
|
return {
|
|
592
|
-
content: [
|
|
807
|
+
content: [
|
|
808
|
+
{
|
|
593
809
|
type: "text",
|
|
594
|
-
text: `Card ${card_id} permanently deleted
|
|
595
|
-
}
|
|
810
|
+
text: `Card ${card_id} permanently deleted.`,
|
|
811
|
+
},
|
|
812
|
+
],
|
|
596
813
|
};
|
|
597
814
|
}
|
|
598
815
|
else {
|
|
599
816
|
// Soft delete (archive)
|
|
600
817
|
const response = await this.axiosInstance.put(`/api/card/${card_id}`, { archived: true });
|
|
601
818
|
return {
|
|
602
|
-
content: [
|
|
819
|
+
content: [
|
|
820
|
+
{
|
|
603
821
|
type: "text",
|
|
604
822
|
// Metabase might return the updated card object or just a success status.
|
|
605
823
|
// If response.data is available and meaningful, include it. Otherwise, a generic success message.
|
|
606
|
-
text: response.data
|
|
607
|
-
|
|
824
|
+
text: response.data
|
|
825
|
+
? `Card ${card_id} archived. Details: ${JSON.stringify(response.data, null, 2)}`
|
|
826
|
+
: `Card ${card_id} archived.`,
|
|
827
|
+
},
|
|
828
|
+
],
|
|
608
829
|
};
|
|
609
830
|
}
|
|
610
831
|
}
|
|
@@ -620,12 +841,14 @@ class MetabaseServer {
|
|
|
620
841
|
createDashboardBody.parameters = parameters;
|
|
621
842
|
if (collection_id !== undefined)
|
|
622
843
|
createDashboardBody.collection_id = collection_id;
|
|
623
|
-
const response = await this.axiosInstance.post(
|
|
844
|
+
const response = await this.axiosInstance.post("/api/dashboard", createDashboardBody);
|
|
624
845
|
return {
|
|
625
|
-
content: [
|
|
846
|
+
content: [
|
|
847
|
+
{
|
|
626
848
|
type: "text",
|
|
627
|
-
text: JSON.stringify(response.data, null, 2)
|
|
628
|
-
}
|
|
849
|
+
text: JSON.stringify(response.data, null, 2),
|
|
850
|
+
},
|
|
851
|
+
],
|
|
629
852
|
};
|
|
630
853
|
}
|
|
631
854
|
case "update_dashboard": {
|
|
@@ -638,10 +861,12 @@ class MetabaseServer {
|
|
|
638
861
|
}
|
|
639
862
|
const response = await this.axiosInstance.put(`/api/dashboard/${dashboard_id}`, updateFields);
|
|
640
863
|
return {
|
|
641
|
-
content: [
|
|
864
|
+
content: [
|
|
865
|
+
{
|
|
642
866
|
type: "text",
|
|
643
|
-
text: JSON.stringify(response.data, null, 2)
|
|
644
|
-
}
|
|
867
|
+
text: JSON.stringify(response.data, null, 2),
|
|
868
|
+
},
|
|
869
|
+
],
|
|
645
870
|
};
|
|
646
871
|
}
|
|
647
872
|
case "delete_dashboard": {
|
|
@@ -652,20 +877,26 @@ class MetabaseServer {
|
|
|
652
877
|
if (hard_delete) {
|
|
653
878
|
await this.axiosInstance.delete(`/api/dashboard/${dashboard_id}`);
|
|
654
879
|
return {
|
|
655
|
-
content: [
|
|
880
|
+
content: [
|
|
881
|
+
{
|
|
656
882
|
type: "text",
|
|
657
|
-
text: `Dashboard ${dashboard_id} permanently deleted
|
|
658
|
-
}
|
|
883
|
+
text: `Dashboard ${dashboard_id} permanently deleted.`,
|
|
884
|
+
},
|
|
885
|
+
],
|
|
659
886
|
};
|
|
660
887
|
}
|
|
661
888
|
else {
|
|
662
889
|
// Soft delete (archive)
|
|
663
890
|
const response = await this.axiosInstance.put(`/api/dashboard/${dashboard_id}`, { archived: true });
|
|
664
891
|
return {
|
|
665
|
-
content: [
|
|
892
|
+
content: [
|
|
893
|
+
{
|
|
666
894
|
type: "text",
|
|
667
|
-
text: response.data
|
|
668
|
-
|
|
895
|
+
text: response.data
|
|
896
|
+
? `Dashboard ${dashboard_id} archived. Details: ${JSON.stringify(response.data, null, 2)}`
|
|
897
|
+
: `Dashboard ${dashboard_id} archived.`,
|
|
898
|
+
},
|
|
899
|
+
],
|
|
669
900
|
};
|
|
670
901
|
}
|
|
671
902
|
}
|
|
@@ -674,21 +905,23 @@ class MetabaseServer {
|
|
|
674
905
|
content: [
|
|
675
906
|
{
|
|
676
907
|
type: "text",
|
|
677
|
-
text: `Unknown tool: ${request.params?.name}
|
|
678
|
-
}
|
|
908
|
+
text: `Unknown tool: ${request.params?.name}`,
|
|
909
|
+
},
|
|
679
910
|
],
|
|
680
|
-
isError: true
|
|
911
|
+
isError: true,
|
|
681
912
|
};
|
|
682
913
|
}
|
|
683
914
|
}
|
|
684
915
|
catch (error) {
|
|
685
916
|
if (axios.isAxiosError(error)) {
|
|
686
917
|
return {
|
|
687
|
-
content: [
|
|
918
|
+
content: [
|
|
919
|
+
{
|
|
688
920
|
type: "text",
|
|
689
|
-
text: `Metabase API error: ${error.response?.data?.message || error.message}
|
|
690
|
-
}
|
|
691
|
-
|
|
921
|
+
text: `Metabase API error: ${error.response?.data?.message || error.message}`,
|
|
922
|
+
},
|
|
923
|
+
],
|
|
924
|
+
isError: true,
|
|
692
925
|
};
|
|
693
926
|
}
|
|
694
927
|
throw error;
|
|
@@ -697,35 +930,35 @@ class MetabaseServer {
|
|
|
697
930
|
}
|
|
698
931
|
async run() {
|
|
699
932
|
try {
|
|
700
|
-
this.logInfo(
|
|
933
|
+
this.logInfo("Starting Metabase MCP server...");
|
|
701
934
|
const transport = new StdioServerTransport();
|
|
702
935
|
await this.server.connect(transport);
|
|
703
|
-
this.logInfo(
|
|
936
|
+
this.logInfo("Metabase MCP server running on stdio");
|
|
704
937
|
}
|
|
705
938
|
catch (error) {
|
|
706
|
-
this.logError(
|
|
939
|
+
this.logError("Failed to start server", error);
|
|
707
940
|
throw error;
|
|
708
941
|
}
|
|
709
942
|
}
|
|
710
943
|
}
|
|
711
944
|
// Add global error handlers
|
|
712
|
-
process.on(
|
|
945
|
+
process.on("uncaughtException", (error) => {
|
|
713
946
|
console.error(JSON.stringify({
|
|
714
947
|
timestamp: new Date().toISOString(),
|
|
715
|
-
level:
|
|
716
|
-
message:
|
|
948
|
+
level: "fatal",
|
|
949
|
+
message: "Uncaught Exception",
|
|
717
950
|
error: error.message,
|
|
718
|
-
stack: error.stack
|
|
951
|
+
stack: error.stack,
|
|
719
952
|
}));
|
|
720
953
|
process.exit(1);
|
|
721
954
|
});
|
|
722
|
-
process.on(
|
|
955
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
723
956
|
const errorMessage = reason instanceof Error ? reason.message : String(reason);
|
|
724
957
|
console.error(JSON.stringify({
|
|
725
958
|
timestamp: new Date().toISOString(),
|
|
726
|
-
level:
|
|
727
|
-
message:
|
|
728
|
-
error: errorMessage
|
|
959
|
+
level: "fatal",
|
|
960
|
+
message: "Unhandled Rejection",
|
|
961
|
+
error: errorMessage,
|
|
729
962
|
}));
|
|
730
963
|
});
|
|
731
964
|
const server = new MetabaseServer();
|