storage describe-column writes column descriptions where the MCP server never reads them
kbagent: v0.86.0
keboola-mcp-server: 1.74.6
Impact: column descriptions written by kbagent are invisible to every AI client using the Keboola MCP server, while kbagent storage table-detail reports them as present.
Summary
There are two incompatible conventions for storing column descriptions:
|
Key |
Location |
| kbagent writes |
KBC.column.{name}.description |
flat entry in table metadata |
| MCP server reads |
KBC.description |
inside columnMetadata[{name}] |
They never meet. Descriptions written via kbagent storage describe-column (or the columns: section of storage describe-batch) are silently invisible to get_table_detail / search in the MCP server — which is precisely the audience those descriptions are written for.
The failure is silent in both directions: kbagent storage table-detail reads back its own convention, so the descriptions look correctly applied.
Root cause
client/storage_tables.py:126-129 documents the assumption behind the current behaviour:
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).
That assumption is incorrect. The same endpoint kbagent already uses, POST /v2/storage/tables/{id}/metadata, accepts a columnsMetadata payload with provider: user. The MCP server does exactly this in clients/storage.py:944-953.
The mirrored read path has the same problem — services/storage_service.py:664-668 populates col_descriptions only from flat KBC.column.* keys, ignoring KBC.description inside columnMetadata, even though columnMetadata is already loaded on line 654.
Reproduction
Verified live on project 9432 (out.c-out-reporting.customer).
Writing through the columnsMetadata payload:
payload = {
"provider": "user",
"columnsMetadata": {
"customer_id": [{"key": "KBC.description", "value": "Customer ID from the ERP system."}]
},
}
client._request("POST", "/v2/storage/tables/out.c-out-reporting.customer/metadata", json=payload)
GET /v2/storage/tables/{id} afterwards:
customer_id [... , ('KBC.description', 'Customer ID from the ERP system.', 'user')]
The write succeeds with provider: user, contradicting the docstring.
Reading the same table back through kbagent after 70 columns were described this way:
$ kbagent --json storage table-detail --project <alias> --table-id out.c-out-reporting.price_history_today
description: "Today's price snapshot: ..." # table description: read back fine
column_metadata: 0 # <- the 70 column descriptions are not seen
column_details with a description: 0 of 70
Where the MCP server reads from
tools/storage/tools.py:793-799 — column description resolved from columnMetadata[col] via MetadataField.DESCRIPTION, with a fallback to sourceTable.columnMetadata
tools/storage/search.py:107-110 — same lookup for search matching
Neither path inspects KBC.column.* keys.
Suggested fix
set_table_metadata (or a sibling method): send column descriptions as a columnsMetadata JSON payload with provider: user instead of flat metadata[i][key] entries.
storage_service.py:664-668: read KBC.description from columnMetadata[col] as the primary source, keeping the KBC.column.* lookup as a fallback for already-written data.
- Correct the docstrings in
client/storage_tables.py:126-129 and services/storage_service.py:2534-2538, plus skills/kbagent/references/storage-describe-workflow.md, which currently states that descriptions written this way will be visible in MCP get_tables.
A migration for existing KBC.column.* entries would be useful — any project already documented this way is silently invisible to its AI clients.
Workaround
Bypass the CLI and post columnsMetadata through keboola_agent_cli.lib.Client(...).raw:
payload = {"provider": "user", "columnsMetadata": {col: [{"key": "KBC.description", "value": desc}] for col, desc in cols.items()}}
client._request("POST", f"/v2/storage/tables/{quote(table_id, safe='')}/metadata", json=payload)
Applied to 529 columns across 20 tables in project 9432; all became visible to the MCP server.
storage describe-columnwrites column descriptions where the MCP server never reads themkbagent: v0.86.0
keboola-mcp-server: 1.74.6
Impact: column descriptions written by kbagent are invisible to every AI client using the Keboola MCP server, while
kbagent storage table-detailreports them as present.Summary
There are two incompatible conventions for storing column descriptions:
KBC.column.{name}.descriptionKBC.descriptioncolumnMetadata[{name}]They never meet. Descriptions written via
kbagent storage describe-column(or thecolumns:section ofstorage describe-batch) are silently invisible toget_table_detail/searchin the MCP server — which is precisely the audience those descriptions are written for.The failure is silent in both directions:
kbagent storage table-detailreads back its own convention, so the descriptions look correctly applied.Root cause
client/storage_tables.py:126-129documents the assumption behind the current behaviour:That assumption is incorrect. The same endpoint kbagent already uses,
POST /v2/storage/tables/{id}/metadata, accepts acolumnsMetadatapayload withprovider: user. The MCP server does exactly this inclients/storage.py:944-953.The mirrored read path has the same problem —
services/storage_service.py:664-668populatescol_descriptionsonly from flatKBC.column.*keys, ignoringKBC.descriptioninsidecolumnMetadata, even thoughcolumnMetadatais already loaded on line 654.Reproduction
Verified live on project 9432 (
out.c-out-reporting.customer).Writing through the
columnsMetadatapayload:GET /v2/storage/tables/{id}afterwards:The write succeeds with
provider: user, contradicting the docstring.Reading the same table back through kbagent after 70 columns were described this way:
Where the MCP server reads from
tools/storage/tools.py:793-799— column description resolved fromcolumnMetadata[col]viaMetadataField.DESCRIPTION, with a fallback tosourceTable.columnMetadatatools/storage/search.py:107-110— same lookup for search matchingNeither path inspects
KBC.column.*keys.Suggested fix
set_table_metadata(or a sibling method): send column descriptions as acolumnsMetadataJSON payload withprovider: userinstead of flatmetadata[i][key]entries.storage_service.py:664-668: readKBC.descriptionfromcolumnMetadata[col]as the primary source, keeping theKBC.column.*lookup as a fallback for already-written data.client/storage_tables.py:126-129andservices/storage_service.py:2534-2538, plusskills/kbagent/references/storage-describe-workflow.md, which currently states that descriptions written this way will be visible in MCPget_tables.A migration for existing
KBC.column.*entries would be useful — any project already documented this way is silently invisible to its AI clients.Workaround
Bypass the CLI and post
columnsMetadatathroughkeboola_agent_cli.lib.Client(...).raw:Applied to 529 columns across 20 tables in project 9432; all became visible to the MCP server.