Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"plugins": [
{
"name": "kbagent",
"version": "0.87.0",
"version": "0.87.1",
"source": "./plugins/kbagent",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, sync configs as files, manage dev branches, and debug SQL in workspaces",
"category": "development"
Expand Down
2 changes: 1 addition & 1 deletion plugins/kbagent/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kbagent",
"version": "0.87.0",
"version": "0.87.1",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, sync configs as files, manage dev branches, and debug SQL in workspaces",
"author": {
"name": "Keboola",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ Requires a **super-admin** Manage API token (same kind as `org setup`). Same def
- `storage snapshot-delete --project NAME --snapshot-id ID [--snapshot-id ...] [--dry-run] [--yes]` (since v0.75.0) -- delete snapshots (destructive: forecloses restores; source tables untouched). Batch-tolerant, exit 1 when any ID failed
- `storage describe-bucket --project NAME --bucket-id ID [--text STR | --file PATH | --stdin] [--branch ID]` -- set a bucket description (stored as `KBC.description` in bucket metadata, upsert). Provide exactly one of `--text`, `--file`, `--stdin`. Read back via `storage bucket-detail`
- `storage describe-table --project NAME --table-id ID [--text STR | --file PATH | --stdin] [--branch ID]` -- set a table description (stored as `KBC.description` in table metadata, upsert). Provide exactly one of `--text`, `--file`, `--stdin`. Read back via `storage table-detail`
- `storage describe-column --project NAME --table-id ID --column NAME=DESCRIPTION [--column ...] [--branch ID]` -- set one or more column descriptions. Stored as `KBC.column.{name}.description` keys in the table's metadata (Keboola has no user-writable column-metadata endpoint). Read back in `storage table-detail` under `column_details[].description`
- `storage describe-column --project NAME --table-id ID --column NAME=DESCRIPTION [--column ...] [--branch ID]` -- set one or more column descriptions. Stored as `KBC.description` inside the table's native `columnMetadata` store (since v0.87.1; `POST .../metadata` accepts a `columnsMetadata` payload with `provider: user`) -- the store the Keboola UI and the Keboola MCP server actually read. Read back in `storage table-detail` under `column_details[].description`. kbagent `<= 0.86.0` wrote flat `KBC.column.{name}.description` table metadata that MCP never read (#624); re-run this command on an affected table to migrate it
- `storage describe-batch --project NAME --from-file PATH [--branch ID]` -- apply bucket/table/column descriptions from a YAML file (top-level `buckets`, `tables`, `columns` sections, all optional). Partial-failure tolerant: per-item errors are collected and reported, the batch does not abort. Non-zero exit only when at least one item failed

## Storage Files
Expand Down
26 changes: 17 additions & 9 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -2622,15 +2622,23 @@ so setting a branch's description will **not** update the dashboard.
`kbagent storage describe-bucket / describe-table / describe-column / describe-batch`
write descriptive metadata onto storage objects. Three behaviors are easy to miss:

- **Column descriptions use a metadata-key convention, not a column endpoint.**
The Keboola Storage API has no user-writable column-level metadata endpoint,
so `describe-column` stores each description as a `KBC.column.{name}.description`
entry on the **table's** metadata (upsert). `storage table-detail` reads them
back via the same key and surfaces them under `column_details[].description`.
Renaming or deleting a column does NOT automatically clean these entries up
(they remain on the table's metadata under the old name). Same convention for
table and bucket descriptions: stored as `KBC.description` (provider=user) on
the object's metadata.
- **Column descriptions written before v0.87.1 are invisible to MCP clients.**
`describe-column` used to store each description as a flat
`KBC.column.{name}.description` entry on the **table's** metadata, on the
mistaken assumption that Keboola had no user-writable column-metadata
endpoint. It does: `POST /v2/storage/tables/{id}/metadata` accepts a
`columnsMetadata` payload with `provider: user`, and that native
`columnMetadata` store is where the Keboola UI and the Keboola MCP server
(`get_table_detail`, `search`) read column descriptions from. **(since
v0.87.1)** `describe-column` / `describe-batch` write there. The failure was
silent in both directions — `storage table-detail` read back kbagent's own
convention, so the descriptions looked correctly applied while no AI client
could see them (#624). kbagent still reads the flat keys as a fallback, and
`columnMetadata` wins when both carry a value, so **re-running
`describe-column` on an affected table is the migration** — there is no bulk
migration command. Renaming or deleting a column does NOT clean the old
entries up under either convention. Table and bucket descriptions are
unaffected: still `KBC.description` (provider=user) on the object's metadata.
- **`describe-batch` is partial-failure-tolerant.** Item-level errors are
collected into `result.errors[]` but the batch keeps processing the remaining
items. The CLI exits non-zero only if `error_count > 0`, so in scripts always
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,16 @@ Descriptions are stored as metadata entries on the object:

- **Bucket description** -- `KBC.description` (provider=user) on bucket metadata
- **Table description** -- `KBC.description` (provider=user) on table metadata
- **Column description** -- `KBC.column.{column_name}.description` on the
**table's** metadata. Keboola has no user-writable column-metadata endpoint,
so this key convention is the storage layer for column descriptions. Read
- **Column description** -- `KBC.description` (provider=user) inside the
table's **`columnMetadata`** store, written through the `columnsMetadata`
payload of `POST /v2/storage/tables/{id}/metadata`. This is the store the
Keboola UI and the Keboola MCP server read column descriptions from. Read
them back via `storage table-detail` (`column_details[].description`).
**(since v0.87.1)** -- kbagent `<= 0.86.0` wrote flat
`KBC.column.{name}.description` keys on the **table's** metadata instead,
which no MCP consumer ever read (#624). kbagent still reads that convention
as a fallback, so old descriptions stay visible here, but they remain
invisible to MCP until you re-run `describe-column` on the table.

Descriptions are `upsert`: calling `describe-*` with a new text replaces
whatever was there before. There is no append mode.
Expand Down Expand Up @@ -106,10 +112,12 @@ kbagent --json storage describe-column \
--column "created_at=Server-side creation timestamp (UTC)"
```

Column descriptions live under `KBC.column.{name}.description` on the
**table's** metadata -- they are NOT attached to the column record itself.
If you rename or delete a column, the old key lingers until you manually
clean it up (there is no `--delete-column-description` command today).
Column descriptions live under `KBC.description` inside the table's
`columnMetadata` store, keyed by column name (since v0.87.1; before that they
were flat `KBC.column.{name}.description` keys on the table's metadata that MCP
never read -- see #624). If you rename or delete a column, the old entry
lingers until you manually clean it up (there is no
`--delete-column-description` command today).

Read back via `storage table-detail`:

Expand Down Expand Up @@ -246,8 +254,10 @@ entries with `provider="user"` are considered the canonical description.
## Key behaviors

- `describe-*` is **upsert** -- no append mode; re-running replaces the value.
- Column descriptions piggy-back on table metadata via the
`KBC.column.{name}.description` key convention.
- Column descriptions are written as `KBC.description` into the table's
`columnMetadata` store (since v0.87.1), so MCP clients see them. Values
written by an older kbagent under `KBC.column.{name}.description` are still
read here but are invisible to MCP -- re-run `describe-column` to migrate.
- `describe-batch` is **partial-failure-tolerant** -- check `errors[]` even
on exit code 0.
- All commands support `--branch ID` to target a dev branch.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "keboola-cli"
version = "0.87.0"
version = "0.87.1"
description = "AI-friendly CLI for managing Keboola projects"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
20 changes: 20 additions & 0 deletions src/keboola_agent_cli/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@

# Ordered newest-first. Each value is a list of brief one-line descriptions.
CHANGELOG: dict[str, list[str]] = {
"0.87.1": [
"Fix (#624): `storage describe-column` / `describe-batch` now write column "
"descriptions where every Keboola consumer actually reads them. They used to be "
"stored as flat `KBC.column.{name}.description` entries on the TABLE's metadata, on "
"the documented but incorrect assumption that Keboola exposes no user-writable "
"column-metadata endpoint. It does: the same `POST /v2/storage/tables/{id}/metadata` "
"kbagent already called accepts a `columnsMetadata` payload with `provider: user`, "
"and that native `columnMetadata` store is what the Keboola UI and the Keboola MCP "
"server (`get_table_detail`, `search`) read. Nothing ever read the flat keys, so "
"column descriptions written by kbagent were invisible to every AI client using the "
"MCP server -- the exact audience they are written for. The failure was silent in "
"BOTH directions: `storage table-detail` read back kbagent's own convention, so the "
"descriptions were reported as correctly applied. `table-detail` now reads "
"`KBC.description` from `columnMetadata` first and keeps the flat keys as a fallback, "
"so descriptions written by an older kbagent stay visible here; when both carry a "
"value, `columnMetadata` wins. There is NO bulk migration -- re-running "
"`describe-column` (or `describe-batch`) on an affected table rewrites it into the "
"right place, and until you do, those tables remain invisible to MCP clients. Table "
"and bucket descriptions are unaffected.",
],
"0.87.0": [
"New (#626): `data-app create` gains `--workspace / --no-workspace` and grants "
"Storage access BY DEFAULT. The flag writes `runtime.workspace.enabled: true`, which "
Expand Down
57 changes: 53 additions & 4 deletions src/keboola_agent_cli/client/storage_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,10 @@ def set_table_metadata(
POST /v2/storage/tables/{id}/metadata

Provider is always ``"user"`` for CLI-originated descriptions.
Column-level descriptions use the namespaced key convention
``KBC.column.{colname}.description`` stored at table-metadata level
(Keboola Storage API does not expose a user-writable column-metadata
endpoint; ``columnMetadata`` is populated exclusively by components).
This method writes **table-level** metadata only. For per-column
descriptions use :meth:`set_table_column_metadata`, which targets the
native ``columnMetadata`` store the platform (and the Keboola MCP
server) actually reads.

Args:
table_id: Full table ID (e.g. "in.c-bucket.table").
Expand All @@ -145,6 +145,55 @@ def set_table_metadata(
response = self._request("POST", f"{prefix}/tables/{safe_id}/metadata", data=form)
return response.json()

def set_table_column_metadata(
self,
table_id: str,
columns: dict[str, list[tuple[str, str]]],
branch_id: int | None = None,
) -> dict[str, Any]:
"""Upsert per-column metadata on a storage table.

POST /v2/storage/tables/{id}/metadata with a JSON ``columnsMetadata``
payload.

The same endpoint that takes table-level ``metadata`` also accepts a
``columnsMetadata`` object with ``provider: user``, writing into the
native per-column store that ``GET /v2/storage/tables/{id}`` returns as
``columnMetadata``. That is the store the platform UI and the Keboola
MCP server read column descriptions from; the older flat
``KBC.column.{name}.description`` table-metadata convention is invisible
to them (issue #624).

Unlike the table-level sibling this sends JSON rather than form data --
the nested per-column shape has no form-encoded equivalent.

Args:
table_id: Full table ID (e.g. "in.c-bucket.table").
columns: Mapping of column name -> ordered ``(key, value)`` tuples.
branch_id: If set, target a specific dev branch.

Returns:
Response dict from the API, carrying ``metadata`` and
``columnsMetadata`` for the table after the upsert.
"""
prefix = f"/v2/storage/branch/{branch_id}" if branch_id else "/v2/storage"
safe_id = quote(table_id, safe="")
payload: dict[str, Any] = {
"provider": "user",
"columnsMetadata": {
name: [
# ``columnName`` is redundant with the mapping key but the
# platform's own clients send it; mirror them rather than
# rely on the server inferring it.
{"key": key, "value": value, "columnName": name}
for key, value in entries
]
for name, entries in columns.items()
},
}
response = self._request("POST", f"{prefix}/tables/{safe_id}/metadata", json=payload)
return response.json()

def get_bucket_detail(
self,
bucket_id: str,
Expand Down
6 changes: 4 additions & 2 deletions src/keboola_agent_cli/commands/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,8 +724,10 @@
Set the KBC.description metadata on a table (upsert). Readable via table-detail --json .data.description.

kbagent storage describe-column --project NAME --table-id ID --column NAME=DESC [--column ...] [--branch ID]
Set per-column descriptions stored as KBC.column.{{name}}.description in table metadata (upsert).
Readable via table-detail --json .data.column_details[].description.
Set per-column descriptions as KBC.description in the table's columnMetadata (upsert) -- the store
the Keboola UI and MCP server read. Readable via table-detail --json .data.column_details[].description.
kbagent <= 0.86.0 wrote flat KBC.column.{{name}}.description table metadata that MCP never read (#624);
re-run describe-column on an affected table to repair it.

kbagent storage describe-batch --project NAME --from-file YAML [--branch ID]
Apply bucket/table/column descriptions from a YAML file. Sections: buckets, tables, columns (all optional).
Expand Down
11 changes: 7 additions & 4 deletions src/keboola_agent_cli/commands/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -1864,10 +1864,13 @@ def storage_describe_column(
) -> None:
"""Set descriptions on one or more columns of a storage table.

Descriptions are stored as KBC.column.{name}.description keys in table
metadata (upsert). Keboola Storage does not expose a user-writable
column-level metadata endpoint; this convention lets you annotate columns
and read them back via 'storage table-detail'.
Descriptions are stored as KBC.description in the table's per-column
metadata (upsert) -- the same place the Keboola UI and the Keboola MCP
server read them from. Read them back via 'storage table-detail'.

kbagent <= 0.86.0 wrote them as flat KBC.column.{name}.description table
metadata instead, which no MCP consumer ever read; re-run this command on
an affected table to repair it.

Example:

Expand Down
Loading
Loading