Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 50 additions & 14 deletions src/client/metabase-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,15 +261,20 @@ export class MetabaseClient {
return response.data;
}

async updateDashboardCards(dashboardId: number, cards: any[]): Promise<any> {
async updateDashboardCards(dashboardId: number, cards: any[], tabs?: any[]): Promise<any> {
// Get current dashboard to preserve existing properties
const dashboard = await this.getDashboard(dashboardId);

// Replace all dashcards with the provided cards while preserving other properties
const response = await this.axiosInstance.put(`/api/dashboard/${dashboardId}`, {
// If tabs are provided, override those too (use [] to remove all tabs)
const payload: any = {
...dashboard,
dashcards: cards
});
};
if (tabs !== undefined) {
payload.tabs = tabs;
}
const response = await this.axiosInstance.put(`/api/dashboard/${dashboardId}`, payload);
return response.data;
}

Expand Down Expand Up @@ -416,28 +421,59 @@ export class MetabaseClient {

async moveCards(cardIds: number[], collectionId?: number, dashboardId?: number): Promise<any> {
const data: any = { card_ids: cardIds };
if (collectionId) {

if (collectionId !== undefined) {
data.collection_id = collectionId;
}
if (dashboardId) {
if (dashboardId !== undefined) {
data.dashboard_id = dashboardId;
}

const response = await this.axiosInstance.post("/api/cards/move", data);
return response.data;

try {
const response = await this.axiosInstance.post("/api/cards/move", data);
return response.data;
} catch (error: any) {
// Dashboard Questions (cards created directly on a dashboard) cannot be
// moved via the bulk endpoint. Fall back to individual PUT calls that
// detach dashboard_id so the collection_id can be set.
const msg = error?.response?.data?.message ?? error?.message ?? "";
if (msg.includes("Dashboard Question") && collectionId !== undefined) {
const results = [];
for (const cardId of cardIds) {
const update: any = { collection_id: collectionId, dashboard_id: null };
const res = await this.axiosInstance.put(`/api/card/${cardId}`, update);
results.push(res.data);
}
return results;
}
throw error;
}
}

// Card collection operations
async moveCardsToCollection(cardIds: number[], collectionId?: number): Promise<any> {
const requestBody: any = { card_ids: cardIds };

if (collectionId !== undefined) {
requestBody.collection_id = collectionId;
}

const response = await this.axiosInstance.post("/api/card/collections", requestBody);
return response.data;

try {
const response = await this.axiosInstance.post("/api/card/collections", requestBody);
return response.data;
} catch (error: any) {
const msg = error?.response?.data?.message ?? error?.message ?? "";
if (msg.includes("Dashboard Question") && collectionId !== undefined) {
const results = [];
for (const cardId of cardIds) {
const update: any = { collection_id: collectionId, dashboard_id: null };
const res = await this.axiosInstance.put(`/api/card/${cardId}`, update);
results.push(res.data);
}
return results;
}
throw error;
}
}

// Card embeddable operations
Expand Down
13 changes: 11 additions & 2 deletions src/tools/additional-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,16 @@ export function addAdditionalTools(server: any, metabaseClient: MetabaseClient)
collection_id: number | null;
}) => {
try {
// For cards, also clear dashboard_id so Dashboard Questions (cards
// created directly on a dashboard) can be moved to a collection.
const payload: Record<string, any> = { collection_id: args.collection_id };
if (args.item_type === "card") {
payload.dashboard_id = null;
}
const result = await metabaseClient.apiCall(
"PUT",
`/api/${args.item_type}/${args.item_id}`,
{ collection_id: args.collection_id }
payload
);
return JSON.stringify(result, null, 2);
} catch (error) {
Expand Down Expand Up @@ -211,8 +217,9 @@ export function addAdditionalTools(server: any, metabaseClient: MetabaseClient)
description: z.string().optional().describe("New description for the collection"),
parent_id: z.number().optional().describe("New parent collection ID"),
color: z.string().optional().describe("New color for the collection"),
archived: z.boolean().optional().describe("Archive (true) or unarchive (false) the collection"),
}).strict(),
execute: async (args: { collection_id: number; name?: string; description?: string; parent_id?: number; color?: string }) => {
execute: async (args: { collection_id: number; name?: string; description?: string; parent_id?: number; color?: string; archived?: boolean }) => {
try {
const { collection_id, ...updates } = args;
const collection = await metabaseClient.updateCollection(collection_id, updates);
Expand Down Expand Up @@ -242,6 +249,8 @@ export function addAdditionalTools(server: any, metabaseClient: MetabaseClient)
}).strict(),
execute: async (args: { collection_id: number }) => {
try {
// Metabase requires collections to be archived (trashed) before deletion
await metabaseClient.updateCollection(args.collection_id, { archived: true });
await metabaseClient.deleteCollection(args.collection_id);
return JSON.stringify({
collection_id: args.collection_id,
Expand Down
2 changes: 1 addition & 1 deletion src/tools/card-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function addCardTools(server: any, metabaseClient: MetabaseClient) {
parameters: z.object({
name: z.string().describe("Card name"),
description: z.string().optional().describe("Description"),
dataset_query: z.unknown().optional().describe("Dataset query object - fully preserved including nested MBQL arrays"),
dataset_query: z.record(z.string(), z.any()).optional().describe("Dataset query object - fully preserved including nested MBQL arrays"),
display: z.string().optional().describe("Visualization type"),
visualization_settings: z
.object({})
Expand Down
18 changes: 14 additions & 4 deletions src/tools/dashboard-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ export function addDashboardTools(server: any, metabaseClient: MetabaseClient) {
.array(z.object({
parameter_id: z.string().describe("The parameter ID to map"),
card_id: z.number().describe("The card ID this mapping applies to"),
target: z.array(z.unknown()).describe("Target specification - e.g. ['dimension', ['field', field_id, {...}]] or ['variable', ['template-tag', name]]"),
target: z.array(z.any()).describe("Target specification - e.g. ['dimension', ['field', field_id, {...}]] or ['variable', ['template-tag', name]]"),
}).passthrough())
.optional()
.describe("Parameter mappings for the card - connects dashboard filters to card fields"),
Expand Down Expand Up @@ -804,12 +804,22 @@ export function addDashboardTools(server: any, metabaseClient: MetabaseClient) {
.passthrough()
)
.describe("Array of card configurations"),
tabs: z
.array(
z.object({
id: z.number().optional().describe("Tab ID (omit for new tabs)"),
name: z.string().describe("Tab name"),
}).passthrough()
)
.optional()
.describe("Dashboard tabs. Pass [] to remove all tabs. Omit to preserve existing tabs."),
}).strict(),
execute: async (args: { dashboard_id: number; cards: any[] }) => {
execute: async (args: { dashboard_id: number; cards: any[]; tabs?: any[] }) => {
try {
const result = await metabaseClient.updateDashboardCards(
args.dashboard_id,
args.cards
args.cards,
args.tabs
);
return JSON.stringify(result, null, 2);
} catch (error) {
Expand Down Expand Up @@ -1094,7 +1104,7 @@ export function addDashboardTools(server: any, metabaseClient: MetabaseClient) {
parameter_mappings: z.array(z.object({
parameter_id: z.string().describe("The parameter ID to map"),
card_id: z.number().describe("The card ID this mapping applies to"),
target: z.array(z.unknown()).describe("Target specification - e.g. ['dimension', ['field', field_id, {...}]] or ['variable', ['template-tag', name]]"),
target: z.array(z.any()).describe("Target specification - e.g. ['dimension', ['field', field_id, {...}]] or ['variable', ['template-tag', name]]"),
}).passthrough()).optional().describe("Parameter mappings for connecting filters"),
visualization_settings: z.object({}).passthrough().optional().describe("Visualization settings"),
row: z.number().optional().describe("Row position"),
Expand Down
4 changes: 2 additions & 2 deletions src/tools/database-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,8 +354,8 @@ export function addDatabaseTools(server: any, metabaseClient: MetabaseClient) {
query: z.string().describe("The SQL query to execute"),
parameters: z.array(z.object({
type: z.string().optional().describe("Parameter type (e.g. 'category', 'date')"),
target: z.array(z.unknown()).optional().describe("Target specification"),
value: z.unknown().describe("Parameter value"),
target: z.array(z.any()).optional().describe("Target specification"),
value: z.any().describe("Parameter value"),
}).passthrough()).optional().describe("Optional query parameters for parameterized queries"),
}).strict(),
execute: async (args: { database_id: number; query: string; parameters?: any[] }) => {
Expand Down
4 changes: 2 additions & 2 deletions src/tools/table-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function addTableTools(server: any, metabaseClient: MetabaseClient) {
caveats: z.string().optional().describe("Caveats"),
points_of_interest: z.string().optional().describe("Points of interest"),
visibility_type: z.enum(["technical", "hidden", "cruft"]).optional().describe("Table visibility type"),
data_authority: z.unknown().optional().describe("Data authority settings (see Metabase OpenAPI spec)"),
data_authority: z.record(z.string(), z.any()).optional().describe("Data authority settings (see Metabase OpenAPI spec)"),
data_layer: z.string().optional().describe("Data layer"),
data_source: z.string().optional().describe("Data source"),
owner_email: z.string().optional().describe("Owner email"),
Expand Down Expand Up @@ -178,7 +178,7 @@ export function addTableTools(server: any, metabaseClient: MetabaseClient) {
points_of_interest: z.string().optional().describe("Points of interest"),
visibility_type: z.enum(["technical", "hidden", "cruft"]).optional().describe("Table visibility type"),
field_order: z.enum(["alphabetical", "custom", "database", "smart"]).optional().describe("Field ordering"),
data_authority: z.unknown().optional().describe("Data authority settings (see Metabase OpenAPI spec)"),
data_authority: z.record(z.string(), z.any()).optional().describe("Data authority settings (see Metabase OpenAPI spec)"),
data_layer: z.string().optional().describe("Data layer"),
data_source: z.string().optional().describe("Data source"),
owner_email: z.string().optional().describe("Owner email"),
Expand Down