From 33bb4641026d4cc05ba9c66905ac0d5d73dbfd02 Mon Sep 17 00:00:00 2001 From: Bill Ballou Date: Mon, 26 Jan 2026 15:18:11 -0500 Subject: [PATCH] feat: add label parameter to add_column and modify_column tools Allow setting a human-readable display label for columns, separate from the column_id used in formulas and API calls. The label defaults to the column_id if not provided. --- src/grist_mcp/grist_client.py | 8 ++++++- src/grist_mcp/server.py | 6 +++++- src/grist_mcp/tools/schema.py | 10 ++++++--- tests/unit/test_grist_client.py | 37 ++++++++++++++++++++++++++++++++ tests/unit/test_tools_schema.py | 38 +++++++++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 5 deletions(-) diff --git a/src/grist_mcp/grist_client.py b/src/grist_mcp/grist_client.py index 8fc4ad4..7c307f7 100644 --- a/src/grist_mcp/grist_client.py +++ b/src/grist_mcp/grist_client.py @@ -203,11 +203,14 @@ class GristClient: column_id: str, column_type: str, formula: str | None = None, + label: str | None = None, ) -> str: """Add a column to a table. Returns column ID.""" fields = {"type": column_type} if formula: fields["formula"] = formula + if label: + fields["label"] = label payload = {"columns": [{"id": column_id, "fields": fields}]} data = await self._request("POST", f"/tables/{table}/columns", json=payload) @@ -219,13 +222,16 @@ class GristClient: column_id: str, type: str | None = None, formula: str | None = None, + label: str | None = None, ) -> None: - """Modify a column's type or formula.""" + """Modify a column's type, formula, or label.""" fields = {} if type is not None: fields["type"] = type if formula is not None: fields["formula"] = formula + if label is not None: + fields["label"] = label payload = {"columns": [{"id": column_id, "fields": fields}]} await self._request("PATCH", f"/tables/{table}/columns", json=payload) diff --git a/src/grist_mcp/server.py b/src/grist_mcp/server.py index 70353a3..5b5903f 100644 --- a/src/grist_mcp/server.py +++ b/src/grist_mcp/server.py @@ -186,13 +186,14 @@ def create_server( "column_id": {"type": "string"}, "column_type": {"type": "string"}, "formula": {"type": "string"}, + "label": {"type": "string", "description": "Display label for the column"}, }, "required": ["document", "table", "column_id", "column_type"], }, ), Tool( name="modify_column", - description="Modify a column's type or formula", + description="Modify a column's type, formula, or label", inputSchema={ "type": "object", "properties": { @@ -201,6 +202,7 @@ def create_server( "column_id": {"type": "string"}, "type": {"type": "string"}, "formula": {"type": "string"}, + "label": {"type": "string", "description": "Display label for the column"}, }, "required": ["document", "table", "column_id"], }, @@ -311,6 +313,7 @@ def create_server( _current_agent, auth, arguments["document"], arguments["table"], arguments["column_id"], arguments["column_type"], formula=arguments.get("formula"), + label=arguments.get("label"), ) elif name == "modify_column": result = await _modify_column( @@ -318,6 +321,7 @@ def create_server( arguments["column_id"], type=arguments.get("type"), formula=arguments.get("formula"), + label=arguments.get("label"), ) elif name == "delete_column": result = await _delete_column( diff --git a/src/grist_mcp/tools/schema.py b/src/grist_mcp/tools/schema.py index 12e2a91..d11c3b9 100644 --- a/src/grist_mcp/tools/schema.py +++ b/src/grist_mcp/tools/schema.py @@ -31,6 +31,7 @@ async def add_column( column_id: str, column_type: str, formula: str | None = None, + label: str | None = None, client: GristClient | None = None, ) -> dict: """Add a column to a table.""" @@ -40,7 +41,9 @@ async def add_column( doc = auth.get_document(document) client = GristClient(doc) - created_id = await client.add_column(table, column_id, column_type, formula=formula) + created_id = await client.add_column( + table, column_id, column_type, formula=formula, label=label + ) return {"column_id": created_id} @@ -52,16 +55,17 @@ async def modify_column( column_id: str, type: str | None = None, formula: str | None = None, + label: str | None = None, client: GristClient | None = None, ) -> dict: - """Modify a column's type or formula.""" + """Modify a column's type, formula, or label.""" auth.authorize(agent, document, Permission.SCHEMA) if client is None: doc = auth.get_document(document) client = GristClient(doc) - await client.modify_column(table, column_id, type=type, formula=formula) + await client.modify_column(table, column_id, type=type, formula=formula, label=label) return {"modified": True} diff --git a/tests/unit/test_grist_client.py b/tests/unit/test_grist_client.py index 962d197..0b4671f 100644 --- a/tests/unit/test_grist_client.py +++ b/tests/unit/test_grist_client.py @@ -155,6 +155,27 @@ async def test_add_column(client, httpx_mock: HTTPXMock): col_id = await client.add_column("Table1", "NewCol", "Text", formula=None) assert col_id == "NewCol" + request = httpx_mock.get_request() + import json + payload = json.loads(request.content) + assert payload == {"columns": [{"id": "NewCol", "fields": {"type": "Text"}}]} + + +@pytest.mark.asyncio +async def test_add_column_with_label(client, httpx_mock: HTTPXMock): + httpx_mock.add_response( + url="https://grist.example.com/api/docs/abc123/tables/Table1/columns", + method="POST", + json={"columns": [{"id": "first_name"}]}, + ) + + col_id = await client.add_column("Table1", "first_name", "Text", label="First Name") + + assert col_id == "first_name" + request = httpx_mock.get_request() + import json + payload = json.loads(request.content) + assert payload == {"columns": [{"id": "first_name", "fields": {"type": "Text", "label": "First Name"}}]} @pytest.mark.asyncio @@ -169,6 +190,22 @@ async def test_modify_column(client, httpx_mock: HTTPXMock): await client.modify_column("Table1", "Amount", type="Int", formula="$Price * $Qty") +@pytest.mark.asyncio +async def test_modify_column_with_label(client, httpx_mock: HTTPXMock): + httpx_mock.add_response( + url="https://grist.example.com/api/docs/abc123/tables/Table1/columns", + method="PATCH", + json={}, + ) + + await client.modify_column("Table1", "Col1", label="Column One") + + request = httpx_mock.get_request() + import json + payload = json.loads(request.content) + assert payload == {"columns": [{"id": "Col1", "fields": {"label": "Column One"}}]} + + @pytest.mark.asyncio async def test_delete_column(client, httpx_mock: HTTPXMock): httpx_mock.add_response( diff --git a/tests/unit/test_tools_schema.py b/tests/unit/test_tools_schema.py index cb92b18..862f6bc 100644 --- a/tests/unit/test_tools_schema.py +++ b/tests/unit/test_tools_schema.py @@ -81,6 +81,25 @@ async def test_add_column(auth, mock_client): ) assert result == {"column_id": "NewCol"} + mock_client.add_column.assert_called_once_with( + "Table1", "NewCol", "Text", formula=None, label=None + ) + + +@pytest.mark.asyncio +async def test_add_column_with_label(auth, mock_client): + agent = auth.authenticate("schema-token") + + result = await add_column( + agent, auth, "budget", "Table1", "first_name", "Text", + label="First Name", + client=mock_client, + ) + + assert result == {"column_id": "NewCol"} + mock_client.add_column.assert_called_once_with( + "Table1", "first_name", "Text", formula=None, label="First Name" + ) @pytest.mark.asyncio @@ -95,6 +114,25 @@ async def test_modify_column(auth, mock_client): ) assert result == {"modified": True} + mock_client.modify_column.assert_called_once_with( + "Table1", "Col1", type="Int", formula="$A + $B", label=None + ) + + +@pytest.mark.asyncio +async def test_modify_column_with_label(auth, mock_client): + agent = auth.authenticate("schema-token") + + result = await modify_column( + agent, auth, "budget", "Table1", "Col1", + label="Column One", + client=mock_client, + ) + + assert result == {"modified": True} + mock_client.modify_column.assert_called_once_with( + "Table1", "Col1", type=None, formula=None, label="Column One" + ) @pytest.mark.asyncio