Self-serve
AI Endpoints

AI Endpoints

⚠️

This feature is still in Beta. APIs and behaviour may change. Reach out to the Embeddable team if you have questions or run into issues.

Embeddable exposes a set of API endpoints under /ai/v1/* that allow you to connect an AI agent to your users' data and dashboards. The agent can query data models, inspect the charts already on a user's canvas, and create or modify charts on their behalf — without the user having to build anything manually.

This is useful when you want to enable conversational insights (i.e. ask questions about the data without building charts) or allow end users to describe what they want in natural language — "show me sales performance this quarter" — and have your AI translate that into fully rendered charts on their Custom Canvas.

💡

End users in this context are your customers — the people using the product you've built on top of Embeddable.

Setup

Getting started with the AI endpoints is straightforward but requires the following initial steps. For a more complete description, see our Custom Canvas documentation.

Enable Custom Canvas in the builder

Simply toggle the "Custom Canvas" option in your dashboard's settings. This allows your end users to create and manage their own charts.

Image 0

Add datasets the user and AI can use for Custom Canvas

Add datasets to your dashboard and enable them for Custom Canvas. The AI agent will only be able to query datasets that are enabled for Custom Canvas.

Image 0

Create Templates with widgets the end user or AI can use for Custom Canvas

To give the ai agent more options, add templates for many types of charts and leave them open (no set dataset or filters)

Image 0

Publish custom canvas

You'll need to publish your dashboard with the Custom Canvas enabled and at least one dataset defined and enabled within Custom Canvas in order to proceed. Otherwise, the API endpoints won’t be able to query your data!

Image 0

Create a security token

You can learn more about creating a security token on our Tokens Api Page.

⚠️

Important note: you must have Custom Canvas enabled on your dashboard and at least one dataset defined and enabled within Custom Canvas in order to proceed. Otherwise, the API endpoints won’t be able to query your data!

Base URL

EU:  https://api.eu.embeddable.com
US:  https://api.us.embeddable.com

All endpoint paths below are relative to this base.

Authorization

All /ai/v1/* endpoints are authenticated using the same security token you already generate for your end users. No new authentication mechanism is required.

Pass it as a Bearer token on every request:

Authorization: Bearer <security-token-jwt>

The token encodes:

  • Which data models the user is allowed to see (via row-level security)
  • Which Custom Canvas chart types are available to the user
  • The embeddable context the token is scoped to

Generate a token server-side for the end user and pass it to your AI agent. Every response is automatically scoped to that user's security context.

// Always generate tokens server-side — never from client code
const response = await fetch('https://api.eu.embeddable.com/api/v1/security-token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${apiKey}`
  },
  body: JSON.stringify({
    embeddableId: '<dashboard-id>',
    savedVersion: 'production',
    user: 'user@example.com',
    // ... other fields
  })
});
const { token } = await response.json();
// Pass `token` to your AI agent for all /ai/v1/* calls

See the Tokens API for the full list of token parameters.

How it works

A typical agent flow looks like this:

End User
  │  "Add a KPI chart showing sales performance this quarter in terms of dollars"

Customer's AI Agent
  │  GET  /ai/v1/embeddable/metadata          → available chart types & datasets
  │  GET  /ai/v1/models                       → data model schema
  │  POST /ai/v1/query                        → run and verify a data query
  │  GET  /ai/v1/custom-canvas/request-state  → current canvas contents
  │  POST /ai/v1/custom-canvas/create         → place chart on canvas

Embeddable API  (authenticated via Security Token)
  │  Applies row-level security
  │  Writes chart to end user's Custom Canvas

End User sees the chart on their canvas

The AI reasoning stays with your agent. Embeddable provides the tools it needs: data access, chart rendering, and canvas management.

You can also use the agent without rendering a chart, simply asking a question that would return an answer based on the data. For example, "Show me sales performance this quarter" might return a table of results or a summary of the data, without creating a chart on the canvas. Here is an example of that flow:

End User
  │  "Show me sales performance this quarter"

Customer's AI Agent
  │  GET  /ai/v1/embeddable/metadata          → available chart types & datasets
  │  GET  /ai/v1/models                       → data model schema
  │  POST /ai/v1/query                        → run and verify a data query

Embeddable API  (authenticated via Security Token)
  │  Applies row-level security
  │  Returns data to the agent

End User sees the data in the agent's response (table, summary, etc.)

Read endpoints

GET /ai/v1/embeddable/metadata

Returns the full configuration for the embeddable — widgets, datasets, variables, and layout parameters. Use this as the starting point for any agent that needs to understand what chart types and data sources are available.

Key relationships:

  • datasets[].id is the datasetId required by POST /ai/v1/query
  • templateWidgets[].id is the templateId used in POST /ai/v1/custom-canvas/create
  • starterCanvasWidgets are pre-built layout presets the agent can use to seed the canvas

Response 200 OK

{
  "widgets": [
    {
      "id": "<uuid>",
      "name": "string",
      "inputConfiguration": { "<inputName>": "<value>" }
    }
  ],
  "selfServeWidgets": [ { "..." } ],
  "templateWidgets": [
    {
      "id": "<uuid>",
      "name": "string",
      "inputConfiguration": { "..." }
    }
  ],
  "starterCanvasWidgets": [
    {
      "id": "<uuid>",
      "templateId": "<uuid>",
      "inputConfiguration": { "..." },
      "filters": {},
      "limit": {},
      "order": {},
      "w": 4,
      "h": 3
    }
  ],
  "variables": [
    {
      "id": "<uuid>",
      "name": "string",
      "type": "string",
      "defaultValue": "string",
      "array": false
    }
  ],
  "variableListeners": { "<variableId>": ["<dependentVariableId>"] },
  "dashboardParams": { "..." },
  "datasets": [
    {
      "id": "<uuid>",
      "name": "string",
      "model": "string",
      "availableInCustomCanvas": true
    }
  ],
  "bundleHash": "string"
}

GET /ai/v1/models

Returns all available Cube.js data models — the schema the agent needs to construct valid queries (dimension and measure names, types, and descriptions).

Key notes:

  • name on dimensions and measures follows the pattern <modelName>.<fieldName> — use this exact string in query bodies
  • __type__ is "dimension" or "measure"

Response 200 OK

[
  {
    "name": "orders",
    "title": "Orders",
    "connectedComponent": 1,
    "dimensions": [
      {
        "name": "orders.status",
        "title": "Status",
        "nativeType": "string",
        "description": "Order status",
        "meta": null,
        "__type__": "dimension"
      }
    ],
    "measures": [
      {
        "name": "orders.count",
        "title": "Count",
        "nativeType": "number",
        "description": null,
        "meta": null,
        "__type__": "measure"
      }
    ]
  }
]

POST /ai/v1/query

Executes a structured data query against a dataset. Returns raw rows and an optional total count.

Request headers

HeaderRequiredDescription
AuthorizationBearer <security-token-jwt>
External-System-Request-IdClient-assigned idempotency UUID. Defaults to a random UUID.
External-System-Request-Sequence-NumberSequence number for ordering requests within a logical operation. Defaults to 1.

Request body

FieldTypeRequiredDescription
datasetIdUUIDID of the dataset to query (from GET /ai/v1/embeddable/metadata)
dimensionsstring[]Dimension names in model.field format
measuresstring[]Measure names in model.field format
filtersFilter[]Array of filter conditions (see below)
timeDimensionsTimeDimension[]Time-based filters with optional granularity (see below)
orderstring[][]Sort order: [["model.field", "asc"]]
limitintegerMax rows to return
offsetlongRows to skip (for pagination)
timezonestringIANA timezone, e.g. "America/New_York"
countRowsbooleanIf true, include total row count in response. Default: false

Filter object

FieldTypeRequiredDescription
memberstringDimension or measure name (model.field)
operatorstringOne of: equals, notEquals, contains, notContains, startsWith, endsWith, gt, gte, lt, lte, set, notSet, inDateRange, notInDateRange, beforeDate, afterDate, measureFilter
valuesany[]Filter values. Omit for set / notSet operators.

TimeDimension object

FieldTypeRequiredDescription
dimensionstringDate/time dimension name
dateRangestring or string[2]Named range (e.g. "last 7 days") or ["YYYY-MM-DD", "YYYY-MM-DD"]
granularitystringsecond, minute, hour, day, week, month, quarter, year

Example request

{
  "datasetId": "<uuid>",
  "dimensions": ["orders.status", "orders.createdAt"],
  "measures": ["orders.count", "orders.revenue"],
  "filters": [
    { "member": "orders.status", "operator": "equals", "values": ["completed"] }
  ],
  "timeDimensions": [
    {
      "dimension": "orders.createdAt",
      "dateRange": ["2024-01-01", "2024-12-31"],
      "granularity": "month"
    }
  ],
  "order": [["orders.createdAt", "asc"]],
  "limit": 100,
  "offset": 0,
  "timezone": "America/New_York",
  "countRows": false
}

Response 200 OK

{
  "cubeRequestId": "<uuid>",
  "externalRequestId": "<uuid>",
  "requestSequenceNumber": 1,
  "data": [
    { "orders.status": "completed", "orders.count": 142 }
  ],
  "total": 142,
  "error": null,
  "preAggregationTriggered": false
}
FieldDescription
dataResult rows. Keys are model.field strings.
totalTotal row count. Only present when countRows: true.
errorError message if the query failed; otherwise null.
cubeRequestIdInternal request ID assigned by Cube.
externalRequestIdEchoes the External-System-Request-Id header.

GET /ai/v1/custom-canvas/request-state

Returns the current state of the end user's Custom Canvas — all widgets currently placed, including their positions, sizes, configurations, and applied filters. This is per-user, scoped to the security token.

Key notes:

  • version increments on every mutation — use it to detect stale reads
  • components[].id is the widget instance UUID required by the update, delete, resize, and reorder endpoints

Response 200 OK

{
  "components": [
    {
      "id": "<uuid>",
      "templateId": "<uuid>",
      "key": "string",
      "inputConfiguration": { "<inputName>": "<value>" },
      "subInputConfiguration": {
        "<parentInputName>": {
          "<parentValue>": { "<subInputName>": "<value>" }
        }
      },
      "filters": {
        "<datasetName>": [
          {
            "member": "model.field",
            "operator": "equals",
            "value": "x",
            "valueType": "string",
            "nativeDataType": "string"
          }
        ]
      },
      "limit": { "<datasetName>": 100 },
      "order": { "<datasetName>": ["model.field asc"] },
      "w": 4,
      "h": 3,
      "heightResolution": null,
      "widthResolution": null
    }
  ],
  "version": 5
}

Custom Canvas mutation endpoints

These endpoints let your AI agent create, update, and manage charts on the end user's Custom Canvas. Charts created via the API appear in the same place as manually built charts.

All mutation endpoints require a requestId — a client-assigned UUID used as an idempotency key.

⚠️

Documentation gap: It is not yet documented how the frontend is notified when the AI agent mutates the Custom Canvas, so that the embedded dashboard re-renders to show new or updated charts.

POST /ai/v1/custom-canvas/create

Add a new widget to the custom canvas.

Request body

FieldTypeRequiredConstraintsDescription
idUUIDClient-assigned UUID for the new widget instance
templateIdUUIDUUID of a template widget (from templateWidgets in metadata)
requestIdUUIDIdempotency key
inputConfigurationMap<string, any>Input values keyed by input name. Key names come from the template's definition — check templateWidgets[].inputConfiguration in the metadata response. Member values (dimensions, measures) must be full member objects, not plain strings (see example below).
subInputConfigurationMap<string, Map<string, Map<string, any>>>Nested per-value input overrides
filtersMap<string, Filter[]>Per-dataset filter overrides
limitMap<string, int>Per-dataset row limit
orderMap<string, string[]>Per-dataset sort order
winteger1–16Width in grid columns. Default: 1
hinteger1–12Height in grid rows. Default: 1
heightResolutioninteger1–4Height resolution multiplier
widthResolutioninteger1–2Width resolution multiplier

The inputConfiguration key for a measure input varies by chart type — for example "measure" (KPI, Donut) or "measures" (Bar Chart, Line Chart). Check the template's definition in GET /ai/v1/embeddable/metadata to confirm the correct key for your chart type. Member values must be full member objects as returned by GET /ai/v1/models, not plain "model.field" strings.

Example request

{
  "id": "<uuid>",
  "templateId": "<uuid>",
  "requestId": "<uuid>",
  "inputConfiguration": {
    "datasetId": "<uuid>",
    "measure": {
      "name": "orders.count",
      "title": "Count",
      "nativeType": "number",
      "description": null,
      "meta": null,
      "__type__": "measure"
    }
  },
  "filters": {},
  "limit": {},
  "order": {},
  "w": 4,
  "h": 3
}

Response 201 Created — empty body


PUT /ai/v1/custom-canvas/update

Replace the configuration of an existing canvas widget. All supplied fields overwrite the current values.

Request body

FieldTypeRequiredDescription
idUUIDUUID of the widget to update (from GET /ai/v1/custom-canvas/request-state)
requestIdUUIDIdempotency key
inputConfigurationMap<string, any>Full replacement input configuration
subInputConfigurationMap<string, Map<string, Map<string, any>>>Full replacement sub-input configuration
filtersMap<string, Filter[]>Full replacement per-dataset filters
limitMap<string, int>Full replacement per-dataset limits
orderMap<string, string[]>Full replacement per-dataset sort orders

Example request

{
  "id": "<uuid>",
  "requestId": "<uuid>",
  "inputConfiguration": {
    "datasetId": "<uuid>",
    "measure": {
      "name": "orders.revenue",
      "title": "Revenue",
      "nativeType": "number",
      "description": null,
      "meta": null,
      "__type__": "measure"
    }
  },
  "filters": {},
  "limit": {},
  "order": {}
}

Response 204 No Content


DELETE /ai/v1/custom-canvas/delete

Remove a widget from the canvas.

Request body

FieldTypeRequiredDescription
idUUIDUUID of the widget to delete (from GET /ai/v1/custom-canvas/request-state)
requestIdUUIDIdempotency key

Example request

{
  "id": "<uuid>",
  "requestId": "<uuid>"
}

Response 204 No Content


POST /ai/v1/custom-canvas/reorder

Change the display order of widgets on the canvas.

Request body

FieldTypeRequiredDescription
updatesMap<UUID, int>Map of widget UUID → new zero-based display index
requestIdUUIDIdempotency key

Example request

{
  "updates": {
    "<widget-uuid>": 0,
    "<widget-uuid-2>": 1
  },
  "requestId": "<uuid>"
}

Response 204 No Content


POST /ai/v1/custom-canvas/resize

Change the dimensions of a canvas widget within the grid.

Request body

FieldTypeRequiredConstraintsDescription
idUUIDUUID of the widget to resize
requestIdUUIDIdempotency key
winteger1–16New width in grid columns
hinteger1–12New height in grid rows
heightResolutioninteger1–4Height resolution multiplier
widthResolutioninteger1–2Width resolution multiplier

Example request

{
  "id": "<uuid>",
  "requestId": "<uuid>",
  "w": 8,
  "h": 4
}

Response 204 No Content


Error handling

All errors return 400 Bad Request with the following body:

{
  "errorType": "PROBLEM_LOADING_DATA",
  "message": "...",
  "retryMessage": "Try again later"
}

Scenarios

Using the AI endpoints without an existing embeddable

If you want to use the data querying endpoints without having set up an embeddable for your end users, create a minimal embeddable in your workspace and use it solely to generate security tokens for your AI agent.

Multiple embeddables on the same page

If you render multiple embeddables simultaneously on a single page, each has its own security token. Pass all tokens to your AI agent. The agent should identify the correct token for each operation — for example by correlating available chart types and data models to the right embeddable.

Customer's AI Agent
  │  Receives tokens: [token_A (embeddable A), token_B (embeddable B)]

  ├── GET /ai/v1/embeddable/metadata (token_A) → chart types for embeddable A
  ├── GET /ai/v1/embeddable/metadata (token_B) → chart types for embeddable B

  │  AI identifies which embeddable is relevant for the operation

  ├── POST /ai/v1/custom-canvas/create (token_A) → chart on embeddable A canvas
  └── POST /ai/v1/custom-canvas/create (token_B) → chart on embeddable B canvas

Example MCP Code

A ready-to-use MCP (opens in a new tab) server (Python / FastMCP) implementing all tools above is available below. It exposes the full /ai/v1/* surface as MCP tools that any MCP-compatible AI agent can call.

Dependencies

pip install fastmcp httpx

Environment variables

VariableDescription
EMBEDDABLE_BASE_URLBase URL of your Embeddable instance (e.g. https://api.eu.embeddable.com)
EMBEDDABLE_TOKENSecurity token JWT for the end user

Run

python mcp_ai_agent.py

Available MCP tools

ToolEndpoint
get_embeddable_metadataGET /ai/v1/embeddable/metadata
get_datasetsGET /ai/v1/embeddable/metadata (returns datasets only)
get_modelsGET /ai/v1/models
execute_queryPOST /ai/v1/query
get_canvas_stateGET /ai/v1/custom-canvas/request-state
create_canvas_widgetPOST /ai/v1/custom-canvas/create
update_canvas_widgetPUT /ai/v1/custom-canvas/update
delete_canvas_widgetDELETE /ai/v1/custom-canvas/delete
reorder_canvas_widgetsPOST /ai/v1/custom-canvas/reorder
resize_canvas_widgetPOST /ai/v1/custom-canvas/resize

mcp_ai_agent.py

import os
import json
import httpx
from mcp.server.fastmcp import FastMCP
 
mcp = FastMCP("embeddable-ai-agent")
 
BASE_URL = os.getenv("EMBEDDABLE_BASE_URL", "")
TOKEN = os.getenv("EMBEDDABLE_TOKEN", "")
 
 
def headers():
    if not TOKEN:
        raise ValueError("EMBEDDABLE_TOKEN is not set. Please set it via environment variable or ask the user.")
    return {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
 
 
@mcp.tool()
def get_models() -> str:
    """Returns available Cube data models with their dimensions and measures."""
    with httpx.Client() as client:
        response = client.get(f"{BASE_URL}/ai/v1/models", headers=headers())
        response.raise_for_status()
        return json.dumps(response.json(), indent=2)
 
 
@mcp.tool()
def execute_query(
    dataset_id: str,
    dimensions: list[str] | None = None,
    measures: list[str] | None = None,
    filters: list[dict] | None = None,
    time_dimensions: list[dict] | None = None,
    order: list[list[str]] | None = None,
    limit: int | None = None,
    offset: int | None = None,
    timezone: str | None = None,
    count_rows: bool = False,
) -> str:
    """
    Execute a data query against the embeddable's model.
 
    dataset_id: UUID of the dataset to query against.
 
    filters format: [{"member": "model.field", "operator": "equals", "values": ["value"]}]
    Operators: equals, notEquals, contains, notContains, startsWith, endsWith, gt, gte, lt, lte, set, notSet, inDateRange, notInDateRange, beforeDate, afterDate, measureFilter
 
    time_dimensions format: [{"dimension": "model.date", "dateRange": ["YYYY-MM-DD", "YYYY-MM-DD"], "granularity": "day"}]
    Granularity options: second, minute, hour, day, week, month, quarter, year
 
    order format: [["model.field", "asc"]]
    """
    body = {k: v for k, v in {
        "datasetId": dataset_id,
        "dimensions": dimensions,
        "measures": measures,
        "filters": filters,
        "timeDimensions": time_dimensions,
        "order": order,
        "limit": limit,
        "offset": offset,
        "timezone": timezone,
        "countRows": count_rows,
    }.items() if v is not None}
 
    with httpx.Client() as client:
        response = client.post(f"{BASE_URL}/ai/v1/query", headers=headers(), json=body)
        response.raise_for_status()
        return json.dumps(response.json(), indent=2)
 
 
@mcp.tool()
def get_datasets() -> str:
    """Returns available datasets with their id, name, model, and availableInCustomCanvas flag. Use this to resolve a datasetId before calling execute_query."""
    with httpx.Client() as client:
        response = client.get(f"{BASE_URL}/ai/v1/embeddable/metadata", headers=headers())
        response.raise_for_status()
        return json.dumps(response.json().get("datasets", []), indent=2)
 
 
@mcp.tool()
def get_embeddable_metadata() -> str:
    """
    Returns full embeddable config including:
    - widgets: configured chart/table widgets with their inputConfiguration
    - selfServeWidgets: self-serve widgets available for the canvas
    - templateWidgets: reusable template chart definitions
    - starterCanvasWidgets: pre-built starter layout widgets (each has id, templateId, inputConfiguration, filters, limit, order, w, h)
    - variables: dashboard variables (id, name, type, defaultValue, array)
    - variableListeners: variable dependency graph
    - dashboardParams: global dashboard parameters
    - datasets: available datasets (id, name, model, availableInCustomCanvas)
    - bundleHash: current bundle version hash
    """
    with httpx.Client() as client:
        response = client.get(f"{BASE_URL}/ai/v1/embeddable/metadata", headers=headers())
        response.raise_for_status()
        return json.dumps(response.json(), indent=2)
 
 
@mcp.tool()
def get_canvas_state() -> str:
    """
    Returns the current custom canvas state including:
    - components: list of widgets on the canvas, each with:
        - id (UUID), templateId (UUID), key (string)
        - inputConfiguration: Map<string, any> of input values
        - subInputConfiguration: nested per-value input overrides
        - filters: Map<datasetName, list of filters> where each filter has member, operator, value, valueType, nativeDataType
        - limit: Map<datasetName, int>
        - order: Map<datasetName, list of strings>
        - w (1-16), h (1-12), heightResolution (1-4), widthResolution (1-2)
    - version: canvas version number
    """
    with httpx.Client() as client:
        response = client.get(f"{BASE_URL}/ai/v1/custom-canvas/request-state", headers=headers())
        response.raise_for_status()
        return json.dumps(response.json(), indent=2)
 
 
@mcp.tool()
def create_canvas_widget(
    id: str,
    template_id: str,
    request_id: str,
    input_configuration: dict,
    sub_input_configuration: dict | None = None,
    filters: dict | None = None,
    limit: dict | None = None,
    order: dict | None = None,
    w: int = 1,
    h: int = 1,
    height_resolution: int | None = None,
    width_resolution: int | None = None,
) -> str:
    """
    Add a new widget to the custom canvas.
 
    id: UUID for the new widget instance.
    template_id: UUID of the template chart to instantiate (from get_embeddable_metadata templateWidgets).
    request_id: Unique UUID for this request (for idempotency).
    input_configuration: Map of input name -> value. Key names come from the template definition (e.g. "measure" for KPI/Donut, "measures" for Bar/Line). Member values must be full member objects: {"name": "model.field", "title": "...", "nativeType": "...", "__type__": "measure"}.
    sub_input_configuration: Optional nested per-value input overrides. Key is parent input name (e.g. "columns"),
        value is Map<parentValue, Map<inputName, any>>.
    filters: Optional Map<datasetName, list of filters>. Each filter: {"member": "model.field", "operator": "equals", "value": "x", "valueType": "...", "nativeDataType": "..."}.
    limit: Optional Map<datasetName, int> row limits per dataset.
    order: Optional Map<datasetName, list of strings> sort order per dataset.
    w: Width in grid columns (1-16, default 1).
    h: Height in grid rows (1-12, default 1).
    height_resolution: Optional height resolution multiplier (1-4).
    width_resolution: Optional width resolution multiplier (1-2).
    """
    body = {k: v for k, v in {
        "id": id,
        "templateId": template_id,
        "requestId": request_id,
        "inputConfiguration": input_configuration,
        "subInputConfiguration": sub_input_configuration,
        "filters": filters,
        "limit": limit,
        "order": order,
        "w": w,
        "h": h,
        "heightResolution": height_resolution,
        "widthResolution": width_resolution,
    }.items() if v is not None}
    with httpx.Client() as client:
        response = client.post(f"{BASE_URL}/ai/v1/custom-canvas/create", headers=headers(), json=body)
        response.raise_for_status()
        return json.dumps(response.json(), indent=2)
 
 
@mcp.tool()
def update_canvas_widget(
    id: str,
    request_id: str,
    input_configuration: dict,
    sub_input_configuration: dict | None = None,
    filters: dict | None = None,
    limit: dict | None = None,
    order: dict | None = None,
) -> str:
    """
    Update an existing widget on the custom canvas.
 
    id: UUID of the widget to update (from get_canvas_state components).
    request_id: Unique UUID for this request (for idempotency).
    input_configuration: Full replacement Map of input name -> value.
    sub_input_configuration: Optional nested per-value input overrides (replaces existing).
    filters: Optional Map<datasetName, list of filters> (replaces existing).
    limit: Optional Map<datasetName, int> (replaces existing).
    order: Optional Map<datasetName, list of strings> (replaces existing).
    """
    body = {k: v for k, v in {
        "id": id,
        "requestId": request_id,
        "inputConfiguration": input_configuration,
        "subInputConfiguration": sub_input_configuration,
        "filters": filters,
        "limit": limit,
        "order": order,
    }.items() if v is not None}
    with httpx.Client() as client:
        response = client.put(f"{BASE_URL}/ai/v1/custom-canvas/update", headers=headers(), json=body)
        response.raise_for_status()
        return json.dumps(response.json(), indent=2)
 
 
@mcp.tool()
def delete_canvas_widget(id: str, request_id: str) -> str:
    """
    Remove a widget from the custom canvas.
 
    id: UUID of the widget to delete (from get_canvas_state components).
    request_id: Unique UUID for this request (for idempotency).
    """
    body = {"id": id, "requestId": request_id}
    with httpx.Client() as client:
        response = client.request("DELETE", f"{BASE_URL}/ai/v1/custom-canvas/delete", headers=headers(), json=body)
        response.raise_for_status()
        return "Deleted successfully"
 
 
@mcp.tool()
def reorder_canvas_widgets(updates: dict, request_id: str) -> str:
    """
    Reorder widgets on the custom canvas.
 
    updates: Map of widget UUID -> new zero-based index position (e.g. {"<uuid>": 0, "<uuid2>": 1}).
    request_id: Unique UUID for this request (for idempotency).
    """
    body = {"updates": updates, "requestId": request_id}
    with httpx.Client() as client:
        response = client.post(f"{BASE_URL}/ai/v1/custom-canvas/reorder", headers=headers(), json=body)
        response.raise_for_status()
        return "Reordered successfully"
 
 
@mcp.tool()
def resize_canvas_widget(
    id: str,
    request_id: str,
    w: int,
    h: int,
    height_resolution: int | None = None,
    width_resolution: int | None = None,
) -> str:
    """
    Resize a widget on the custom canvas.
 
    id: UUID of the widget to resize (from get_canvas_state components).
    request_id: Unique UUID for this request (for idempotency).
    w: New width in grid columns (1-16).
    h: New height in grid rows (1-12).
    height_resolution: Optional height resolution multiplier (1-4).
    width_resolution: Optional width resolution multiplier (1-2).
    """
    body = {k: v for k, v in {
        "id": id,
        "requestId": request_id,
        "w": w,
        "h": h,
        "heightResolution": height_resolution,
        "widthResolution": width_resolution,
    }.items() if v is not None}
    with httpx.Client() as client:
        response = client.post(f"{BASE_URL}/ai/v1/custom-canvas/resize", headers=headers(), json=body)
        response.raise_for_status()
        return "Resized successfully"
 
 
if __name__ == "__main__":
    mcp.run()