/**
* Response formatters for Metabase MCP server.
* Converts verbose JSON API responses into concise markdown tables
* to reduce token consumption in MCP conversations.
*/
// --- Query Results ---
interface QueryResultData {
data?: {
rows: any[][];
cols: Array<{ name: string; display_name: string; base_type: string }>;
};
status?: string;
row_count?: number;
running_time?: number;
database_id?: number;
error?: string;
error_type?: string;
}
export function formatQueryResult(result: QueryResultData, maxRows: number = 50): string {
if (result.error || result.error_type) {
let out = `Query error (${result.error_type || "unknown"}): ${result.error || "No message"}`;
if (result.status) out += `\nStatus: ${result.status}`;
return out;
}
const meta = [
`Status: ${result.status || "unknown"}`,
`Rows: ${result.row_count ?? 0}`,
`Time: ${result.running_time ?? 0}ms`,
];
if (!result.data?.cols || !result.data?.rows || result.data.rows.length === 0) {
return meta.join(" | ") + "\n\nNo rows returned.";
}
const { rows, cols } = result.data;
const headers = cols.map(c => c.display_name || c.name);
const displayRows = maxRows > 0 ? rows.slice(0, maxRows) : rows;
const truncated = maxRows > 0 && rows.length > maxRows;
let out = meta.join(" | ") + "\n\n";
out += "| " + headers.join(" | ") + " |\n";
out += "|" + headers.map(() => "---").join("|") + "|\n";
for (const row of displayRows) {
const cells = row.map(cell => {
if (cell === null || cell === undefined) return "null";
if (typeof cell === "object") return JSON.stringify(cell);
return String(cell);
});
out += "| " + cells.join(" | ") + " |\n";
}
if (truncated) {
out += `\n*Showing ${maxRows} of ${rows.length} rows. Use max_rows to see more.*\n`;
}
return out;
}
// --- List: Databases ---
export function formatDatabases(data: any): string {
const databases: any[] = Array.isArray(data) ? data : (data?.data || []);
if (databases.length === 0) return "No databases found.";
let out = `Databases (${databases.length})\n\n`;
out += "| ID | Name | Engine | Sync Status |\n";
out += "|---|---|---|---|\n";
for (const db of databases) {
const flags: string[] = [];
if (db.is_sample) flags.push("sample");
if (db.is_audit) flags.push("audit");
const status = db.initial_sync_status || "unknown";
const statusStr = flags.length > 0 ? `${status} (${flags.join(", ")})` : status;
out += `| ${db.id} | ${db.name} | ${db.engine} | ${statusStr} |\n`;
}
return out;
}
// --- List: Dashboards ---
export function formatDashboards(dashboards: any[]): string {
if (!Array.isArray(dashboards) || dashboards.length === 0) return "No dashboards found.";
let out = `Dashboards (${dashboards.length})\n\n`;
out += "| ID | Name | Collection | Views | Archived |\n";
out += "|---|---|---|---|---|\n";
for (const d of dashboards) {
const coll = d.collection_id ?? "root";
const views = d.view_count ?? 0;
const archived = d.archived ? "yes" : "";
out += `| ${d.id} | ${d.name} | ${coll} | ${views} | ${archived} |\n`;
}
return out;
}
// --- List: Cards ---
export function formatCards(cards: any[]): string {
if (!Array.isArray(cards) || cards.length === 0) return "No cards found.";
let out = `Cards (${cards.length})\n\n`;
out += "| ID | Name | Collection | Display | Archived |\n";
out += "|---|---|---|---|---|\n";
for (const c of cards) {
const coll = c.collection_id ?? "root";
const display = c.display || c.query_type || "unknown";
const archived = c.archived ? "yes" : "";
out += `| ${c.id} | ${c.name} | ${coll} | ${display} | ${archived} |\n`;
}
return out;
}
// --- Dashboard Cards ---
export function formatDashboardCards(cards: any[]): string {
if (!Array.isArray(cards) || cards.length === 0) return "No cards in this dashboard.";
let out = `Dashboard cards (${cards.length})\n\n`;
out += "| Dashcard ID | Card ID | Name | Display | Size |\n";
out += "|---|---|---|---|---|\n";
for (const dc of cards) {
const card = dc.card || {};
const cardId = card.id ?? "—";
const name = card.name || "Untitled";
const display = card.display || "—";
const size = `${dc.size_x ?? 4}x${dc.size_y ?? 4}`;
out += `| ${dc.id} | ${cardId} | ${name} | ${display} | ${size} |\n`;
}
return out;
}
// --- List: Collections ---
export function formatCollections(collections: any[]): string {
if (!Array.isArray(collections) || collections.length === 0) return "No collections found.";
let out = `Collections (${collections.length})\n\n`;
out += "| ID | Name | Parent | Archived |\n";
out += "|---|---|---|---|\n";
for (const c of collections) {
const parent = c.parent_id ?? "root";
const archived = c.archived ? "yes" : "";
out += `| ${c.id} | ${c.name} | ${parent} | ${archived} |\n`;
}
return out;
}
// --- List: Users ---
export function formatUsers(data: any): string {
const users: any[] = Array.isArray(data) ? data : (data?.data || []);
if (users.length === 0) return "No users found.";
let out = `Users (${users.length})\n\n`;
out += "| ID | Email | Name | Active |\n";
out += "|---|---|---|---|\n";
for (const u of users) {
const name = [u.first_name, u.last_name].filter(Boolean).join(" ") || "—";
const active = u.is_active === false ? "no" : "yes";
out += `| ${u.id} | ${u.email} | ${name} | ${active} |\n`;
}
return out;
}
// --- List: Permission Groups ---
export function formatPermissionGroups(groups: any[]): string {
if (!Array.isArray(groups) || groups.length === 0) return "No permission groups found.";
let out = `Permission Groups (${groups.length})\n\n`;
out += "| ID | Name | Member Count |\n";
out += "|---|---|---|\n";
for (const g of groups) {
const members = g.member_count ?? "—";
out += `| ${g.id} | ${g.name} | ${members} |\n`;
}
return out;
}