Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## [Unreleased]

## [0.1.4] - 2026-03-12

- Add dashboard inspection commands for listing dashboards, viewing tabs and dashcards, and summarizing dashboard dependencies
- Expand card inspection with `card get --full`, dashboard parameter lookup, and parameterized card/dashboard execution
- Improve dashboard query safety with clearer error messages and redaction-aware parameterized query handling

## [0.1.3] - 2026-03-05

- PII redaction enabled by default: query result columns with Metabase PII semantic types (Email, Name, Phone, etc.) are replaced with `[REDACTED]`
Expand Down
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,39 @@ mb-cli card list
# Get card details
mb-cli card get 10

# Get the full card definition, including dataset_query and template tags
mb-cli card get 10 --full

# Execute a saved question
mb-cli card run 10

# Execute a saved question with parameters
mb-cli card run 10 --param timeframe_days=14
```

### Dashboards

```bash
# List all dashboards
mb-cli dashboard list

# Get dashboard metadata, tabs, filters, and grouped cards
mb-cli dashboard get 298

# List the saved questions used by a dashboard
mb-cli dashboard cards 298

# Discover valid values for a dashboard filter
mb-cli dashboard params values 298 merchant_name

# Search dashboard filter values
mb-cli dashboard params search 298 merchant_name acme

# Execute a dashboard card with dashboard parameters applied
mb-cli dashboard run-card 298 1201 398 --param merchant_name="Acme Corp"

# Summarize tabs, parameter mappings, and source-card dependencies
mb-cli dashboard analyze 298
```

### Search
Expand Down Expand Up @@ -175,8 +206,48 @@ mb-cli table metadata 42

# 4. Query data
mb-cli query sql --db 1 --sql "SELECT id, email FROM users WHERE created_at > '2024-01-01' LIMIT 10"

# 5. Inspect a dashboard that depends on saved questions
mb-cli dashboard analyze 298
```

## Dashboard analysis workflow

Example flow for dashboard `298`:

```bash
# 1. Inspect dashboard structure
mb-cli dashboard get 298

# 2. List the saved questions behind the dashboard
mb-cli dashboard cards 298

# 3. Inspect a card's full SQL or MBQL definition
mb-cli card get 398 --full

# 4. Discover valid parameter values
mb-cli dashboard params values 298 merchant_name

# 5. Summarize dependencies and assumption-backed cards
mb-cli dashboard analyze 298
```

## API coverage

The dashboard and parameter workflows use these Metabase endpoints:

| Command | Endpoint |
|---------|----------|
| `dashboard list` | `GET /api/dashboard/` |
| `dashboard get` | `GET /api/dashboard/:id` |
| `dashboard cards` | `GET /api/dashboard/:id` |
| `dashboard params values` | `GET /api/dashboard/:id/params/:param-key/values` |
| `dashboard params search` | `GET /api/dashboard/:id/params/:param-key/search/:query` |
| `dashboard run-card` | `POST /api/dashboard/:dashboard-id/dashcard/:dashcard-id/card/:card-id/query` |
| `card get --full` | `GET /api/card/:id` |
| `card run --param` | `POST /api/card/:card-id/query` |
| `dashboard analyze` | `GET /api/dashboard/:id`, `GET /api/card/:id` |

## PII Redaction

When AI agents use mb-cli directly (via shell commands), query results containing PII (emails, names, phone numbers) flow through stdout into the model's context. This feature prevents agents from seeing sensitive data by redacting PII columns before data leaves the client layer. Agents can still cross-reference records using IDs; for actual PII values, the user can check directly in Metabase.
Expand Down
79 changes: 77 additions & 2 deletions internal/cli/card.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package cli

import (
"fmt"
"os"
"strconv"
"strings"

"github.com/andreagrandi/mb-cli/internal/client"
"github.com/andreagrandi/mb-cli/internal/formatter"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -34,14 +37,27 @@ var cardRunCmd = &cobra.Command{
RunE: runCardRun,
}

type cardSummary struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
DatabaseID int `json:"database_id"`
Display string `json:"display"`
QueryType string `json:"query_type,omitempty"`
CollectionID *int `json:"collection_id,omitempty"`
Archived bool `json:"archived"`
}

func init() {
rootCmd.AddCommand(cardCmd)

cardCmd.AddCommand(cardListCmd)
cardCmd.AddCommand(cardGetCmd)
cardCmd.AddCommand(cardRunCmd)

cardGetCmd.Flags().Bool("full", false, "Include the full query definition and card metadata")
cardRunCmd.Flags().String("fields", "", "Comma-separated list of columns to include in output")
cardRunCmd.Flags().StringSlice("param", nil, "Parameter in key=value format (repeatable)")
}

func runCardList(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -74,7 +90,12 @@ func runCardGet(cmd *cobra.Command, args []string) error {
return err
}

return formatter.Output(cmd, card)
full, _ := cmd.Flags().GetBool("full")
if full {
return formatter.Output(cmd, card)
}

return formatter.Output(cmd, summarizeCard(card))
}

func runCardRun(cmd *cobra.Command, args []string) error {
Expand All @@ -88,11 +109,54 @@ func runCardRun(cmd *cobra.Command, args []string) error {
return err
}

result, err := c.RunCard(id)
params, err := parseNamedParams(cmd)
if err != nil {
return err
}

result, err := c.RunCardWithParams(id, params)
if err != nil {
return wrapParameterizedRunError(err)
}

return formatQueryResultOutput(cmd, result)
}

func summarizeCard(card *client.Card) cardSummary {
return cardSummary{
ID: card.ID,
Name: card.Name,
Description: card.Description,
DatabaseID: card.DatabaseID,
Display: card.Display,
QueryType: card.QueryType,
CollectionID: card.CollectionID,
Archived: card.Archived,
}
}

func parseNamedParams(cmd *cobra.Command) (map[string]string, error) {
values, err := cmd.Flags().GetStringSlice("param")
if err != nil {
return nil, err
}
if len(values) == 0 {
return nil, nil
}

params := make(map[string]string, len(values))
for _, value := range values {
parts := strings.SplitN(value, "=", 2)
if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" {
return nil, fmt.Errorf("invalid parameter %q: expected key=value", value)
}
params[strings.TrimSpace(parts[0])] = parts[1]
}

return params, nil
}

func formatQueryResultOutput(cmd *cobra.Command, result *client.QueryResult) error {
format, _ := cmd.Flags().GetString("format")
fields, _ := cmd.Flags().GetString("fields")

Expand All @@ -104,3 +168,14 @@ func runCardRun(cmd *cobra.Command, args []string) error {
columns, rows := formatter.FilterColumns(columns, result.Data.Rows, fields)
return formatter.FormatQueryResults(format, columns, rows, os.Stdout)
}

func wrapParameterizedRunError(err error) error {
message := err.Error()
if strings.Contains(message, "API request failed with status 400") {
return fmt.Errorf("parameterized query failed: check parameter keys and values (%w)", err)
}
if strings.Contains(message, "API request failed with status 404") {
return fmt.Errorf("query target was not found (%w)", err)
}
return err
}
27 changes: 27 additions & 0 deletions internal/cli/context_embed.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ Set both environment variables (required):
| `card list` | List saved questions | none | none |
| `card get <id>` | Get card details | id (positional) | none |
| `card run <id>` | Execute a saved question | id (positional) | none |
| `dashboard list` | List dashboards | none | none |
| `dashboard get <id>` | Get dashboard details | id (positional) | none |
| `dashboard cards <id>` | List cards used by a dashboard | id (positional) | none |
| `dashboard analyze <id>` | Summarize dashboard dependencies | id (positional) | none |
| `dashboard run-card <dashboard-id> <dashcard-id> <card-id>` | Execute a dashboard card | dashboard-id, dashcard-id, card-id (positional) | none |
| `dashboard params values <dashboard-id> <param-key>` | List valid dashboard parameter values | dashboard-id, param-key (positional) | none |
| `dashboard params search <dashboard-id> <param-key> <query>` | Search dashboard parameter values | dashboard-id, param-key, query (positional) | none |
| `search <query>` | Search across Metabase items | query (positional) | none |
| `context` | Print this agent context document | none | none |
| `version` | Print version | none | none |
Expand Down Expand Up @@ -69,6 +76,18 @@ Set both environment variables (required):
| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--fields` | string | no | | Comma-separated columns to include in output |
| `--param` | string[] | no | | Parameter in `key=value` format (repeatable) |

### `card get`
| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--full` | bool | no | false | Include `dataset_query`, template tags, result metadata, and visualization settings |

### `dashboard run-card`
| Flag | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `--fields` | string | no | | Comma-separated columns to include in output |
| `--param` | string[] | no | | Parameter in `key=value` format (repeatable) |

### `search`
| Flag | Type | Required | Default | Description |
Expand Down Expand Up @@ -118,6 +137,8 @@ When stdout is not a TTY (piped to another program), the default format is `json

Query result commands (`query sql`, `query filter`, `card run`, `table data`) format output as column/row tables in both formats.

Dashboard inspection commands default to concise summaries in table mode. Use `--format json` for full raw dashboard or analysis payloads.

## Structured Error Output

Use `--error-format json` to get machine-readable errors on stderr:
Expand Down Expand Up @@ -183,6 +204,12 @@ mb-cli query filter --db 1 --table products --where "id=prod_1234" --export csv
mb-cli card list
mb-cli card run 5

# Inspect dashboard structure and dependencies
mb-cli dashboard get 298
mb-cli dashboard cards 298
mb-cli dashboard params values 298 merchant_name
mb-cli dashboard analyze 298

# Get table output for terminal reading
mb-cli database list --format table
```
Loading
Loading