Skip to content

storage describe-column writes column descriptions where the MCP server never reads them #624

Description

@frantisekrehor

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

  1. 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.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions