blixify-charts-mcp 0.1.2 → 0.1.4
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 +583 -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,197 @@ 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
|
+
{
|
|
579
|
+
name: "add_card_to_dashboard",
|
|
580
|
+
description: "Add an existing card to a dashboard.",
|
|
581
|
+
inputSchema: {
|
|
582
|
+
type: "object",
|
|
583
|
+
properties: {
|
|
584
|
+
dashboard_id: {
|
|
585
|
+
type: "number",
|
|
586
|
+
description: "ID of the dashboard to add the card to",
|
|
587
|
+
},
|
|
588
|
+
card_id: {
|
|
589
|
+
type: "number",
|
|
590
|
+
description: "ID of the card to add",
|
|
591
|
+
},
|
|
592
|
+
row: {
|
|
593
|
+
type: "number",
|
|
594
|
+
description: "Row position (default: 0)",
|
|
595
|
+
default: 0,
|
|
596
|
+
},
|
|
597
|
+
col: {
|
|
598
|
+
type: "number",
|
|
599
|
+
description: "Column position (default: 0)",
|
|
600
|
+
default: 0,
|
|
601
|
+
},
|
|
602
|
+
size_x: {
|
|
603
|
+
type: "number",
|
|
604
|
+
description: "Width in grid units (default: 4)",
|
|
605
|
+
default: 4,
|
|
606
|
+
},
|
|
607
|
+
size_y: {
|
|
608
|
+
type: "number",
|
|
609
|
+
description: "Height in grid units (default: 4)",
|
|
610
|
+
default: 4,
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
required: ["dashboard_id", "card_id"],
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
name: "remove_card_from_dashboard",
|
|
618
|
+
description: "Remove a card from a dashboard (does not delete the card itself, just removes it from the dashboard).",
|
|
619
|
+
inputSchema: {
|
|
620
|
+
type: "object",
|
|
621
|
+
properties: {
|
|
622
|
+
dashboard_id: {
|
|
623
|
+
type: "number",
|
|
624
|
+
description: "ID of the dashboard",
|
|
625
|
+
},
|
|
626
|
+
dashcard_id: {
|
|
627
|
+
type: "number",
|
|
628
|
+
description: "ID of the dashboard card (dashcard) to remove. Use get_dashboard_cards to find this ID.",
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
required: ["dashboard_id", "dashcard_id"],
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: "update_dashboard_card",
|
|
636
|
+
description: "Update the position or size of a card in a dashboard.",
|
|
637
|
+
inputSchema: {
|
|
638
|
+
type: "object",
|
|
639
|
+
properties: {
|
|
640
|
+
dashboard_id: {
|
|
641
|
+
type: "number",
|
|
642
|
+
description: "ID of the dashboard",
|
|
643
|
+
},
|
|
644
|
+
dashcard_id: {
|
|
645
|
+
type: "number",
|
|
646
|
+
description: "ID of the dashboard card to update",
|
|
647
|
+
},
|
|
648
|
+
row: {
|
|
649
|
+
type: "number",
|
|
650
|
+
description: "New row position",
|
|
651
|
+
},
|
|
652
|
+
col: {
|
|
653
|
+
type: "number",
|
|
654
|
+
description: "New column position",
|
|
655
|
+
},
|
|
656
|
+
size_x: {
|
|
657
|
+
type: "number",
|
|
658
|
+
description: "New width in grid units",
|
|
659
|
+
},
|
|
660
|
+
size_y: {
|
|
661
|
+
type: "number",
|
|
662
|
+
description: "New height in grid units",
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
required: ["dashboard_id", "dashcard_id"],
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
],
|
|
453
669
|
};
|
|
454
670
|
});
|
|
455
671
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
456
|
-
this.logInfo(
|
|
672
|
+
this.logInfo("Calling tool...", {
|
|
673
|
+
requestStructure: JSON.stringify(request),
|
|
674
|
+
});
|
|
457
675
|
if (!METABASE_API_KEY) {
|
|
458
676
|
await this.getSessionToken();
|
|
459
677
|
}
|
|
460
678
|
try {
|
|
461
679
|
switch (request.params?.name) {
|
|
462
680
|
case "list_dashboards": {
|
|
463
|
-
const response = await this.axiosInstance.get(
|
|
681
|
+
const response = await this.axiosInstance.get("/api/dashboard");
|
|
464
682
|
return {
|
|
465
|
-
content: [
|
|
683
|
+
content: [
|
|
684
|
+
{
|
|
466
685
|
type: "text",
|
|
467
|
-
text: JSON.stringify(response.data, null, 2)
|
|
468
|
-
}
|
|
686
|
+
text: JSON.stringify(response.data, null, 2),
|
|
687
|
+
},
|
|
688
|
+
],
|
|
469
689
|
};
|
|
470
690
|
}
|
|
471
691
|
case "list_cards": {
|
|
472
692
|
const f = request.params?.arguments?.f || "all";
|
|
473
693
|
const response = await this.axiosInstance.get(`/api/card?f=${f}`);
|
|
474
694
|
return {
|
|
475
|
-
content: [
|
|
695
|
+
content: [
|
|
696
|
+
{
|
|
476
697
|
type: "text",
|
|
477
|
-
text: JSON.stringify(response.data, null, 2)
|
|
478
|
-
}
|
|
698
|
+
text: JSON.stringify(response.data, null, 2),
|
|
699
|
+
},
|
|
700
|
+
],
|
|
479
701
|
};
|
|
480
702
|
}
|
|
481
703
|
case "list_databases": {
|
|
482
|
-
const response = await this.axiosInstance.get(
|
|
704
|
+
const response = await this.axiosInstance.get("/api/database");
|
|
483
705
|
return {
|
|
484
|
-
content: [
|
|
706
|
+
content: [
|
|
707
|
+
{
|
|
485
708
|
type: "text",
|
|
486
|
-
text: JSON.stringify(response.data, null, 2)
|
|
487
|
-
}
|
|
709
|
+
text: JSON.stringify(response.data, null, 2),
|
|
710
|
+
},
|
|
711
|
+
],
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
case "get_database": {
|
|
715
|
+
const databaseId = request.params?.arguments?.database_id;
|
|
716
|
+
if (!databaseId) {
|
|
717
|
+
throw new McpError(ErrorCode.InvalidParams, "Database ID is required");
|
|
718
|
+
}
|
|
719
|
+
const response = await this.axiosInstance.get(`/api/database/${databaseId}`);
|
|
720
|
+
return {
|
|
721
|
+
content: [
|
|
722
|
+
{
|
|
723
|
+
type: "text",
|
|
724
|
+
text: JSON.stringify(response.data, null, 2),
|
|
725
|
+
},
|
|
726
|
+
],
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
case "get_database_metadata": {
|
|
730
|
+
const databaseId = request.params?.arguments?.database_id;
|
|
731
|
+
if (!databaseId) {
|
|
732
|
+
throw new McpError(ErrorCode.InvalidParams, "Database ID is required");
|
|
733
|
+
}
|
|
734
|
+
const response = await this.axiosInstance.get(`/api/database/${databaseId}/metadata`);
|
|
735
|
+
// Filter to only include table names/IDs and field names/IDs
|
|
736
|
+
const filteredData = {
|
|
737
|
+
id: response.data.id,
|
|
738
|
+
name: response.data.name,
|
|
739
|
+
tables: response.data.tables?.map((table) => ({
|
|
740
|
+
id: table.id,
|
|
741
|
+
name: table.name,
|
|
742
|
+
fields: table.fields?.map((field) => ({
|
|
743
|
+
id: field.id,
|
|
744
|
+
name: field.name,
|
|
745
|
+
database_type: field.database_type,
|
|
746
|
+
})) || [],
|
|
747
|
+
})) || [],
|
|
748
|
+
};
|
|
749
|
+
return {
|
|
750
|
+
content: [
|
|
751
|
+
{
|
|
752
|
+
type: "text",
|
|
753
|
+
text: JSON.stringify(filteredData, null, 2),
|
|
754
|
+
},
|
|
755
|
+
],
|
|
488
756
|
};
|
|
489
757
|
}
|
|
490
758
|
case "execute_card": {
|
|
@@ -495,10 +763,12 @@ class MetabaseServer {
|
|
|
495
763
|
const parameters = request.params?.arguments?.parameters || {};
|
|
496
764
|
const response = await this.axiosInstance.post(`/api/card/${cardId}/query`, { parameters });
|
|
497
765
|
return {
|
|
498
|
-
content: [
|
|
766
|
+
content: [
|
|
767
|
+
{
|
|
499
768
|
type: "text",
|
|
500
|
-
text: JSON.stringify(response.data, null, 2)
|
|
501
|
-
}
|
|
769
|
+
text: JSON.stringify(response.data, null, 2),
|
|
770
|
+
},
|
|
771
|
+
],
|
|
502
772
|
};
|
|
503
773
|
}
|
|
504
774
|
case "get_dashboard_cards": {
|
|
@@ -508,43 +778,74 @@ class MetabaseServer {
|
|
|
508
778
|
}
|
|
509
779
|
const response = await this.axiosInstance.get(`/api/dashboard/${dashboardId}`);
|
|
510
780
|
return {
|
|
511
|
-
content: [
|
|
781
|
+
content: [
|
|
782
|
+
{
|
|
512
783
|
type: "text",
|
|
513
|
-
text: JSON.stringify(response.data.cards, null, 2)
|
|
514
|
-
}
|
|
784
|
+
text: JSON.stringify(response.data.cards, null, 2),
|
|
785
|
+
},
|
|
786
|
+
],
|
|
515
787
|
};
|
|
516
788
|
}
|
|
517
789
|
case "execute_query": {
|
|
518
790
|
const databaseId = request.params?.arguments?.database_id;
|
|
519
791
|
const query = request.params?.arguments?.query;
|
|
792
|
+
const collectionParam = request.params?.arguments?.collection;
|
|
520
793
|
const nativeParameters = request.params?.arguments?.native_parameters || [];
|
|
521
794
|
if (!databaseId) {
|
|
522
795
|
throw new McpError(ErrorCode.InvalidParams, "Database ID is required");
|
|
523
796
|
}
|
|
524
797
|
if (!query) {
|
|
525
|
-
throw new McpError(ErrorCode.InvalidParams, "
|
|
798
|
+
throw new McpError(ErrorCode.InvalidParams, "Query is required");
|
|
526
799
|
}
|
|
527
|
-
//
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
800
|
+
// Get database details to check engine type
|
|
801
|
+
const dbResponse = await this.axiosInstance.get(`/api/database/${databaseId}`);
|
|
802
|
+
const dbEngine = dbResponse.data.engine;
|
|
803
|
+
let queryData;
|
|
804
|
+
if (dbEngine === "mongo") {
|
|
805
|
+
// MongoDB query format
|
|
806
|
+
if (!collectionParam) {
|
|
807
|
+
throw new McpError(ErrorCode.InvalidParams, "Collection name is required for MongoDB queries");
|
|
808
|
+
}
|
|
809
|
+
const queryStr = String(query);
|
|
810
|
+
queryData = {
|
|
811
|
+
type: "native",
|
|
812
|
+
native: {
|
|
813
|
+
collection: collectionParam,
|
|
814
|
+
query: queryStr,
|
|
815
|
+
template_tags: {},
|
|
816
|
+
},
|
|
817
|
+
parameters: nativeParameters,
|
|
818
|
+
database: databaseId,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
// SQL query format
|
|
823
|
+
queryData = {
|
|
824
|
+
type: "native",
|
|
825
|
+
native: {
|
|
826
|
+
query: query,
|
|
827
|
+
template_tags: {},
|
|
828
|
+
},
|
|
829
|
+
parameters: nativeParameters,
|
|
830
|
+
database: databaseId,
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
const response = await this.axiosInstance.post("/api/dataset", queryData);
|
|
538
834
|
return {
|
|
539
|
-
content: [
|
|
835
|
+
content: [
|
|
836
|
+
{
|
|
540
837
|
type: "text",
|
|
541
|
-
text: JSON.stringify(response.data, null, 2)
|
|
542
|
-
}
|
|
838
|
+
text: JSON.stringify(response.data, null, 2),
|
|
839
|
+
},
|
|
840
|
+
],
|
|
543
841
|
};
|
|
544
842
|
}
|
|
545
843
|
case "create_card": {
|
|
546
|
-
const { name, dataset_query, display, visualization_settings, collection_id, description } = request.params?.arguments || {};
|
|
547
|
-
if (!name ||
|
|
844
|
+
const { name, dataset_query, display, visualization_settings, collection_id, description, } = request.params?.arguments || {};
|
|
845
|
+
if (!name ||
|
|
846
|
+
!dataset_query ||
|
|
847
|
+
!display ||
|
|
848
|
+
!visualization_settings) {
|
|
548
849
|
throw new McpError(ErrorCode.InvalidParams, "Missing required fields for create_card: name, dataset_query, display, visualization_settings");
|
|
549
850
|
}
|
|
550
851
|
const createCardBody = {
|
|
@@ -557,12 +858,14 @@ class MetabaseServer {
|
|
|
557
858
|
createCardBody.collection_id = collection_id;
|
|
558
859
|
if (description !== undefined)
|
|
559
860
|
createCardBody.description = description;
|
|
560
|
-
const response = await this.axiosInstance.post(
|
|
861
|
+
const response = await this.axiosInstance.post("/api/card", createCardBody);
|
|
561
862
|
return {
|
|
562
|
-
content: [
|
|
863
|
+
content: [
|
|
864
|
+
{
|
|
563
865
|
type: "text",
|
|
564
|
-
text: JSON.stringify(response.data, null, 2)
|
|
565
|
-
}
|
|
866
|
+
text: JSON.stringify(response.data, null, 2),
|
|
867
|
+
},
|
|
868
|
+
],
|
|
566
869
|
};
|
|
567
870
|
}
|
|
568
871
|
case "update_card": {
|
|
@@ -575,10 +878,12 @@ class MetabaseServer {
|
|
|
575
878
|
}
|
|
576
879
|
const response = await this.axiosInstance.put(`/api/card/${card_id}`, updateFields);
|
|
577
880
|
return {
|
|
578
|
-
content: [
|
|
881
|
+
content: [
|
|
882
|
+
{
|
|
579
883
|
type: "text",
|
|
580
|
-
text: JSON.stringify(response.data, null, 2)
|
|
581
|
-
}
|
|
884
|
+
text: JSON.stringify(response.data, null, 2),
|
|
885
|
+
},
|
|
886
|
+
],
|
|
582
887
|
};
|
|
583
888
|
}
|
|
584
889
|
case "delete_card": {
|
|
@@ -589,22 +894,28 @@ class MetabaseServer {
|
|
|
589
894
|
if (hard_delete) {
|
|
590
895
|
await this.axiosInstance.delete(`/api/card/${card_id}`);
|
|
591
896
|
return {
|
|
592
|
-
content: [
|
|
897
|
+
content: [
|
|
898
|
+
{
|
|
593
899
|
type: "text",
|
|
594
|
-
text: `Card ${card_id} permanently deleted
|
|
595
|
-
}
|
|
900
|
+
text: `Card ${card_id} permanently deleted.`,
|
|
901
|
+
},
|
|
902
|
+
],
|
|
596
903
|
};
|
|
597
904
|
}
|
|
598
905
|
else {
|
|
599
906
|
// Soft delete (archive)
|
|
600
907
|
const response = await this.axiosInstance.put(`/api/card/${card_id}`, { archived: true });
|
|
601
908
|
return {
|
|
602
|
-
content: [
|
|
909
|
+
content: [
|
|
910
|
+
{
|
|
603
911
|
type: "text",
|
|
604
912
|
// Metabase might return the updated card object or just a success status.
|
|
605
913
|
// If response.data is available and meaningful, include it. Otherwise, a generic success message.
|
|
606
|
-
text: response.data
|
|
607
|
-
|
|
914
|
+
text: response.data
|
|
915
|
+
? `Card ${card_id} archived. Details: ${JSON.stringify(response.data, null, 2)}`
|
|
916
|
+
: `Card ${card_id} archived.`,
|
|
917
|
+
},
|
|
918
|
+
],
|
|
608
919
|
};
|
|
609
920
|
}
|
|
610
921
|
}
|
|
@@ -620,12 +931,14 @@ class MetabaseServer {
|
|
|
620
931
|
createDashboardBody.parameters = parameters;
|
|
621
932
|
if (collection_id !== undefined)
|
|
622
933
|
createDashboardBody.collection_id = collection_id;
|
|
623
|
-
const response = await this.axiosInstance.post(
|
|
934
|
+
const response = await this.axiosInstance.post("/api/dashboard", createDashboardBody);
|
|
624
935
|
return {
|
|
625
|
-
content: [
|
|
936
|
+
content: [
|
|
937
|
+
{
|
|
626
938
|
type: "text",
|
|
627
|
-
text: JSON.stringify(response.data, null, 2)
|
|
628
|
-
}
|
|
939
|
+
text: JSON.stringify(response.data, null, 2),
|
|
940
|
+
},
|
|
941
|
+
],
|
|
629
942
|
};
|
|
630
943
|
}
|
|
631
944
|
case "update_dashboard": {
|
|
@@ -638,10 +951,12 @@ class MetabaseServer {
|
|
|
638
951
|
}
|
|
639
952
|
const response = await this.axiosInstance.put(`/api/dashboard/${dashboard_id}`, updateFields);
|
|
640
953
|
return {
|
|
641
|
-
content: [
|
|
954
|
+
content: [
|
|
955
|
+
{
|
|
642
956
|
type: "text",
|
|
643
|
-
text: JSON.stringify(response.data, null, 2)
|
|
644
|
-
}
|
|
957
|
+
text: JSON.stringify(response.data, null, 2),
|
|
958
|
+
},
|
|
959
|
+
],
|
|
645
960
|
};
|
|
646
961
|
}
|
|
647
962
|
case "delete_dashboard": {
|
|
@@ -652,43 +967,115 @@ class MetabaseServer {
|
|
|
652
967
|
if (hard_delete) {
|
|
653
968
|
await this.axiosInstance.delete(`/api/dashboard/${dashboard_id}`);
|
|
654
969
|
return {
|
|
655
|
-
content: [
|
|
970
|
+
content: [
|
|
971
|
+
{
|
|
656
972
|
type: "text",
|
|
657
|
-
text: `Dashboard ${dashboard_id} permanently deleted
|
|
658
|
-
}
|
|
973
|
+
text: `Dashboard ${dashboard_id} permanently deleted.`,
|
|
974
|
+
},
|
|
975
|
+
],
|
|
659
976
|
};
|
|
660
977
|
}
|
|
661
978
|
else {
|
|
662
979
|
// Soft delete (archive)
|
|
663
980
|
const response = await this.axiosInstance.put(`/api/dashboard/${dashboard_id}`, { archived: true });
|
|
664
981
|
return {
|
|
665
|
-
content: [
|
|
982
|
+
content: [
|
|
983
|
+
{
|
|
666
984
|
type: "text",
|
|
667
|
-
text: response.data
|
|
668
|
-
|
|
985
|
+
text: response.data
|
|
986
|
+
? `Dashboard ${dashboard_id} archived. Details: ${JSON.stringify(response.data, null, 2)}`
|
|
987
|
+
: `Dashboard ${dashboard_id} archived.`,
|
|
988
|
+
},
|
|
989
|
+
],
|
|
669
990
|
};
|
|
670
991
|
}
|
|
671
992
|
}
|
|
993
|
+
case "add_card_to_dashboard": {
|
|
994
|
+
const { dashboard_id, card_id, row = 0, col = 0, size_x = 4, size_y = 4, } = request.params?.arguments || {};
|
|
995
|
+
if (!dashboard_id) {
|
|
996
|
+
throw new McpError(ErrorCode.InvalidParams, "Dashboard ID is required for add_card_to_dashboard");
|
|
997
|
+
}
|
|
998
|
+
if (!card_id) {
|
|
999
|
+
throw new McpError(ErrorCode.InvalidParams, "Card ID is required for add_card_to_dashboard");
|
|
1000
|
+
}
|
|
1001
|
+
const addCardBody = {
|
|
1002
|
+
cardId: card_id,
|
|
1003
|
+
row,
|
|
1004
|
+
col,
|
|
1005
|
+
size_x,
|
|
1006
|
+
size_y,
|
|
1007
|
+
};
|
|
1008
|
+
const response = await this.axiosInstance.post(`/api/dashboard/${dashboard_id}/cards`, addCardBody);
|
|
1009
|
+
return {
|
|
1010
|
+
content: [
|
|
1011
|
+
{
|
|
1012
|
+
type: "text",
|
|
1013
|
+
text: JSON.stringify(response.data, null, 2),
|
|
1014
|
+
},
|
|
1015
|
+
],
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
case "remove_card_from_dashboard": {
|
|
1019
|
+
const { dashboard_id, dashcard_id } = request.params?.arguments || {};
|
|
1020
|
+
if (!dashboard_id) {
|
|
1021
|
+
throw new McpError(ErrorCode.InvalidParams, "Dashboard ID is required for remove_card_from_dashboard");
|
|
1022
|
+
}
|
|
1023
|
+
if (!dashcard_id) {
|
|
1024
|
+
throw new McpError(ErrorCode.InvalidParams, "Dashcard ID is required for remove_card_from_dashboard");
|
|
1025
|
+
}
|
|
1026
|
+
await this.axiosInstance.delete(`/api/dashboard/${dashboard_id}/cards/${dashcard_id}`);
|
|
1027
|
+
return {
|
|
1028
|
+
content: [
|
|
1029
|
+
{
|
|
1030
|
+
type: "text",
|
|
1031
|
+
text: `Card ${dashcard_id} removed from dashboard ${dashboard_id}.`,
|
|
1032
|
+
},
|
|
1033
|
+
],
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
case "update_dashboard_card": {
|
|
1037
|
+
const { dashboard_id, dashcard_id, ...updateFields } = request.params?.arguments || {};
|
|
1038
|
+
if (!dashboard_id) {
|
|
1039
|
+
throw new McpError(ErrorCode.InvalidParams, "Dashboard ID is required for update_dashboard_card");
|
|
1040
|
+
}
|
|
1041
|
+
if (!dashcard_id) {
|
|
1042
|
+
throw new McpError(ErrorCode.InvalidParams, "Dashcard ID is required for update_dashboard_card");
|
|
1043
|
+
}
|
|
1044
|
+
if (Object.keys(updateFields).length === 0) {
|
|
1045
|
+
throw new McpError(ErrorCode.InvalidParams, "No fields provided for update_dashboard_card");
|
|
1046
|
+
}
|
|
1047
|
+
const response = await this.axiosInstance.put(`/api/dashboard/${dashboard_id}/cards/${dashcard_id}`, updateFields);
|
|
1048
|
+
return {
|
|
1049
|
+
content: [
|
|
1050
|
+
{
|
|
1051
|
+
type: "text",
|
|
1052
|
+
text: JSON.stringify(response.data, null, 2),
|
|
1053
|
+
},
|
|
1054
|
+
],
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
672
1057
|
default:
|
|
673
1058
|
return {
|
|
674
1059
|
content: [
|
|
675
1060
|
{
|
|
676
1061
|
type: "text",
|
|
677
|
-
text: `Unknown tool: ${request.params?.name}
|
|
678
|
-
}
|
|
1062
|
+
text: `Unknown tool: ${request.params?.name}`,
|
|
1063
|
+
},
|
|
679
1064
|
],
|
|
680
|
-
isError: true
|
|
1065
|
+
isError: true,
|
|
681
1066
|
};
|
|
682
1067
|
}
|
|
683
1068
|
}
|
|
684
1069
|
catch (error) {
|
|
685
1070
|
if (axios.isAxiosError(error)) {
|
|
686
1071
|
return {
|
|
687
|
-
content: [
|
|
1072
|
+
content: [
|
|
1073
|
+
{
|
|
688
1074
|
type: "text",
|
|
689
|
-
text: `Metabase API error: ${error.response?.data?.message || error.message}
|
|
690
|
-
}
|
|
691
|
-
|
|
1075
|
+
text: `Metabase API error: ${error.response?.data?.message || error.message}`,
|
|
1076
|
+
},
|
|
1077
|
+
],
|
|
1078
|
+
isError: true,
|
|
692
1079
|
};
|
|
693
1080
|
}
|
|
694
1081
|
throw error;
|
|
@@ -697,35 +1084,35 @@ class MetabaseServer {
|
|
|
697
1084
|
}
|
|
698
1085
|
async run() {
|
|
699
1086
|
try {
|
|
700
|
-
this.logInfo(
|
|
1087
|
+
this.logInfo("Starting Metabase MCP server...");
|
|
701
1088
|
const transport = new StdioServerTransport();
|
|
702
1089
|
await this.server.connect(transport);
|
|
703
|
-
this.logInfo(
|
|
1090
|
+
this.logInfo("Metabase MCP server running on stdio");
|
|
704
1091
|
}
|
|
705
1092
|
catch (error) {
|
|
706
|
-
this.logError(
|
|
1093
|
+
this.logError("Failed to start server", error);
|
|
707
1094
|
throw error;
|
|
708
1095
|
}
|
|
709
1096
|
}
|
|
710
1097
|
}
|
|
711
1098
|
// Add global error handlers
|
|
712
|
-
process.on(
|
|
1099
|
+
process.on("uncaughtException", (error) => {
|
|
713
1100
|
console.error(JSON.stringify({
|
|
714
1101
|
timestamp: new Date().toISOString(),
|
|
715
|
-
level:
|
|
716
|
-
message:
|
|
1102
|
+
level: "fatal",
|
|
1103
|
+
message: "Uncaught Exception",
|
|
717
1104
|
error: error.message,
|
|
718
|
-
stack: error.stack
|
|
1105
|
+
stack: error.stack,
|
|
719
1106
|
}));
|
|
720
1107
|
process.exit(1);
|
|
721
1108
|
});
|
|
722
|
-
process.on(
|
|
1109
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
723
1110
|
const errorMessage = reason instanceof Error ? reason.message : String(reason);
|
|
724
1111
|
console.error(JSON.stringify({
|
|
725
1112
|
timestamp: new Date().toISOString(),
|
|
726
|
-
level:
|
|
727
|
-
message:
|
|
728
|
-
error: errorMessage
|
|
1113
|
+
level: "fatal",
|
|
1114
|
+
message: "Unhandled Rejection",
|
|
1115
|
+
error: errorMessage,
|
|
729
1116
|
}));
|
|
730
1117
|
});
|
|
731
1118
|
const server = new MetabaseServer();
|