Compare commits

9 Commits

Author SHA1 Message Date
9945b70fa7 chore(deps): update docker/setup-buildx-action action to v4 2026-03-28 22:03:10 +00:00
540e57ec81 Merge pull request 'chore(deps): update actions/checkout digest to de0fac2' (#4) from renovate/actions-checkout-digest into master
Reviewed-on: #4
2026-03-28 16:26:53 -04:00
d1e1043896 Merge pull request 'chore(deps): pin dependencies' (#2) from renovate/pin-dependencies into master
Reviewed-on: #2
2026-03-28 16:25:58 -04:00
6521078b6a chore(deps): pin dependencies 2026-03-26 22:12:00 +00:00
2f0a24aceb chore(deps): update actions/checkout digest to de0fac2 2026-02-04 16:03:19 +00:00
77bf95817d chore: update uv.lock
All checks were successful
Build and Push Docker Image / build (push) Successful in 8s
2026-01-26 15:23:21 -05:00
29a72ab005 docs: update changelog for v1.5.0 column label support 2026-01-26 15:19:20 -05:00
33bb464102 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.
2026-01-26 15:18:11 -05:00
d4e793224b chore: update uv.lock 2026-01-14 18:04:00 -05:00
11 changed files with 124 additions and 16 deletions

View File

@@ -18,10 +18,10 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Log in to Container Registry - name: Log in to Container Registry
uses: docker/login-action@v3 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -29,7 +29,7 @@ jobs:
- name: Extract metadata for Docker - name: Extract metadata for Docker
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: | tags: |
@@ -38,10 +38,10 @@ jobs:
type=raw,value=latest,enable=${{ !contains(github.ref, '-alpha') && !contains(github.ref, '-beta') && !contains(github.ref, '-rc') }} type=raw,value=latest,enable=${{ !contains(github.ref, '-alpha') && !contains(github.ref, '-beta') && !contains(github.ref, '-rc') }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v6 uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with: with:
context: . context: .
push: true push: true

View File

@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.5.0] - 2026-01-26
### Added
#### Column Label Support
- **`add_column`**: New optional `label` parameter for setting display name
- **`modify_column`**: New optional `label` parameter for updating display name
Labels are human-readable names shown in Grist column headers, separate from the `column_id` used in formulas and API calls. If not provided, Grist defaults the label to the column ID.
#### Usage
```python
# Create column with display label
add_column(document="crm", table="Contacts", column_id="first_name", column_type="Text", label="First Name")
# Update existing column's label
modify_column(document="crm", table="Contacts", column_id="first_name", label="Given Name")
```
## [1.4.1] - 2026-01-14 ## [1.4.1] - 2026-01-14
### Added ### Added

View File

@@ -1,8 +1,8 @@
# Stage 1: Builder # Stage 1: Builder
FROM python:3.14-slim AS builder FROM python:3.14-slim@sha256:fb83750094b46fd6b8adaa80f66e2302ecbe45d513f6cece637a841e1025b4ca AS builder
# Install uv # Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv COPY --from=ghcr.io/astral-sh/uv:latest@sha256:c4f5de312ee66d46810635ffc5df34a1973ba753e7241ce3a08ef979ddd7bea5 /uv /usr/local/bin/uv
WORKDIR /app WORKDIR /app
@@ -20,7 +20,7 @@ RUN uv sync --frozen --no-dev
# Stage 2: Runtime # Stage 2: Runtime
FROM python:3.14-slim FROM python:3.14-slim@sha256:fb83750094b46fd6b8adaa80f66e2302ecbe45d513f6cece637a841e1025b4ca
# Create non-root user # Create non-root user
RUN useradd --create-home --shell /bin/bash appuser RUN useradd --create-home --shell /bin/bash appuser

View File

@@ -1,7 +1,7 @@
# Production environment # Production environment
services: services:
grist-mcp: grist-mcp:
image: ghcr.io/xe138/grist-mcp-server:latest image: ghcr.io/xe138/grist-mcp-server:latest@sha256:2ef22bfac6cfbcbbfc513f61eaea3414b3a531d79e9d1d39bf6757cc9e27ea9a
ports: ports:
- "${PORT:-3000}:3000" - "${PORT:-3000}:3000"
volumes: volumes:

View File

@@ -203,11 +203,14 @@ class GristClient:
column_id: str, column_id: str,
column_type: str, column_type: str,
formula: str | None = None, formula: str | None = None,
label: str | None = None,
) -> str: ) -> str:
"""Add a column to a table. Returns column ID.""" """Add a column to a table. Returns column ID."""
fields = {"type": column_type} fields = {"type": column_type}
if formula: if formula:
fields["formula"] = formula fields["formula"] = formula
if label:
fields["label"] = label
payload = {"columns": [{"id": column_id, "fields": fields}]} payload = {"columns": [{"id": column_id, "fields": fields}]}
data = await self._request("POST", f"/tables/{table}/columns", json=payload) data = await self._request("POST", f"/tables/{table}/columns", json=payload)
@@ -219,13 +222,16 @@ class GristClient:
column_id: str, column_id: str,
type: str | None = None, type: str | None = None,
formula: str | None = None, formula: str | None = None,
label: str | None = None,
) -> None: ) -> None:
"""Modify a column's type or formula.""" """Modify a column's type, formula, or label."""
fields = {} fields = {}
if type is not None: if type is not None:
fields["type"] = type fields["type"] = type
if formula is not None: if formula is not None:
fields["formula"] = formula fields["formula"] = formula
if label is not None:
fields["label"] = label
payload = {"columns": [{"id": column_id, "fields": fields}]} payload = {"columns": [{"id": column_id, "fields": fields}]}
await self._request("PATCH", f"/tables/{table}/columns", json=payload) await self._request("PATCH", f"/tables/{table}/columns", json=payload)

View File

@@ -186,13 +186,14 @@ def create_server(
"column_id": {"type": "string"}, "column_id": {"type": "string"},
"column_type": {"type": "string"}, "column_type": {"type": "string"},
"formula": {"type": "string"}, "formula": {"type": "string"},
"label": {"type": "string", "description": "Display label for the column"},
}, },
"required": ["document", "table", "column_id", "column_type"], "required": ["document", "table", "column_id", "column_type"],
}, },
), ),
Tool( Tool(
name="modify_column", name="modify_column",
description="Modify a column's type or formula", description="Modify a column's type, formula, or label",
inputSchema={ inputSchema={
"type": "object", "type": "object",
"properties": { "properties": {
@@ -201,6 +202,7 @@ def create_server(
"column_id": {"type": "string"}, "column_id": {"type": "string"},
"type": {"type": "string"}, "type": {"type": "string"},
"formula": {"type": "string"}, "formula": {"type": "string"},
"label": {"type": "string", "description": "Display label for the column"},
}, },
"required": ["document", "table", "column_id"], "required": ["document", "table", "column_id"],
}, },
@@ -311,6 +313,7 @@ def create_server(
_current_agent, auth, arguments["document"], arguments["table"], _current_agent, auth, arguments["document"], arguments["table"],
arguments["column_id"], arguments["column_type"], arguments["column_id"], arguments["column_type"],
formula=arguments.get("formula"), formula=arguments.get("formula"),
label=arguments.get("label"),
) )
elif name == "modify_column": elif name == "modify_column":
result = await _modify_column( result = await _modify_column(
@@ -318,6 +321,7 @@ def create_server(
arguments["column_id"], arguments["column_id"],
type=arguments.get("type"), type=arguments.get("type"),
formula=arguments.get("formula"), formula=arguments.get("formula"),
label=arguments.get("label"),
) )
elif name == "delete_column": elif name == "delete_column":
result = await _delete_column( result = await _delete_column(

View File

@@ -31,6 +31,7 @@ async def add_column(
column_id: str, column_id: str,
column_type: str, column_type: str,
formula: str | None = None, formula: str | None = None,
label: str | None = None,
client: GristClient | None = None, client: GristClient | None = None,
) -> dict: ) -> dict:
"""Add a column to a table.""" """Add a column to a table."""
@@ -40,7 +41,9 @@ async def add_column(
doc = auth.get_document(document) doc = auth.get_document(document)
client = GristClient(doc) 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} return {"column_id": created_id}
@@ -52,16 +55,17 @@ async def modify_column(
column_id: str, column_id: str,
type: str | None = None, type: str | None = None,
formula: str | None = None, formula: str | None = None,
label: str | None = None,
client: GristClient | None = None, client: GristClient | None = None,
) -> dict: ) -> dict:
"""Modify a column's type or formula.""" """Modify a column's type, formula, or label."""
auth.authorize(agent, document, Permission.SCHEMA) auth.authorize(agent, document, Permission.SCHEMA)
if client is None: if client is None:
doc = auth.get_document(document) doc = auth.get_document(document)
client = GristClient(doc) 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} return {"modified": True}

View File

@@ -1,4 +1,4 @@
FROM python:3.14-slim FROM python:3.14-slim@sha256:fb83750094b46fd6b8adaa80f66e2302ecbe45d513f6cece637a841e1025b4ca
WORKDIR /app WORKDIR /app

View File

@@ -155,6 +155,27 @@ async def test_add_column(client, httpx_mock: HTTPXMock):
col_id = await client.add_column("Table1", "NewCol", "Text", formula=None) col_id = await client.add_column("Table1", "NewCol", "Text", formula=None)
assert col_id == "NewCol" 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 @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") 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 @pytest.mark.asyncio
async def test_delete_column(client, httpx_mock: HTTPXMock): async def test_delete_column(client, httpx_mock: HTTPXMock):
httpx_mock.add_response( httpx_mock.add_response(

View File

@@ -81,6 +81,25 @@ async def test_add_column(auth, mock_client):
) )
assert result == {"column_id": "NewCol"} 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 @pytest.mark.asyncio
@@ -95,6 +114,25 @@ async def test_modify_column(auth, mock_client):
) )
assert result == {"modified": True} 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 @pytest.mark.asyncio

2
uv.lock generated
View File

@@ -153,7 +153,7 @@ wheels = [
[[package]] [[package]]
name = "grist-mcp" name = "grist-mcp"
version = "1.3.0" version = "1.4.1"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "httpx" }, { name = "httpx" },