Add support for uploading file attachments to Grist documents: - GristClient.upload_attachment() method using multipart/form-data - upload_attachment tool function with base64 decoding and MIME detection - Tool registration in server.py - Comprehensive unit tests (7 new tests) Returns attachment ID for linking to records via update_records. Bumps version to 1.3.0.
grist-mcp
MCP server for AI agents to interact with Grist documents.
Overview
grist-mcp is a Model Context Protocol (MCP) server that enables AI agents to read, write, and modify Grist spreadsheets. It provides secure, token-based access control with granular permissions per document.
Features
- Discovery: List accessible documents with permissions
- Read Operations: List tables, describe columns, fetch records, run SQL queries
- Write Operations: Add, update, and delete records
- Schema Operations: Create tables, add/modify/delete columns
- Security: Token-based authentication with per-document permission scopes (read, write, schema)
- Multi-tenant: Support multiple Grist instances and documents
Quick Start (Docker)
Prerequisites
- Docker and Docker Compose
- Access to one or more Grist documents with API keys
1. Create configuration directory
mkdir grist-mcp && cd grist-mcp
2. Download configuration files
# Download docker-compose.yml
curl -O https://raw.githubusercontent.com/Xe138/grist-mcp-server/master/deploy/prod/docker-compose.yml
# Download example config
curl -O https://raw.githubusercontent.com/Xe138/grist-mcp-server/master/config.yaml.example
cp config.yaml.example config.yaml
3. Generate tokens
Generate a secure token for your agent:
python -c "import secrets; print(secrets.token_urlsafe(32))"
# or
openssl rand -base64 32
4. Configure config.yaml
Edit config.yaml to define your Grist documents and agent tokens:
# Document definitions
documents:
my-document: # Friendly name (used in token scopes)
url: https://docs.getgrist.com # Your Grist instance URL
doc_id: abcd1234efgh5678 # Document ID from the URL
api_key: your-grist-api-key # Grist API key (or use ${ENV_VAR} syntax)
# Agent tokens with access scopes
tokens:
- token: your-generated-token-here # The token you generated in step 3
name: my-agent # Human-readable name
scope:
- document: my-document # Must match a document name above
permissions: [read, write] # Allowed: read, write, schema
Finding your Grist document ID: Open your Grist document in a browser. The URL will look like:
https://docs.getgrist.com/abcd1234efgh5678/My-Document - the document ID is abcd1234efgh5678.
Getting a Grist API key: In Grist, go to Profile Settings → API → Create API Key.
5. Create .env file
Create a .env file with your agent token:
# .env
GRIST_MCP_TOKEN=your-generated-token-here
PORT=3000
The GRIST_MCP_TOKEN must match one of the tokens defined in config.yaml.
6. Start the server
docker compose up -d
The server will be available at http://localhost:3000.
7. Configure your MCP client
Add to your MCP client configuration (e.g., Claude Desktop):
{
"mcpServers": {
"grist": {
"type": "sse",
"url": "http://localhost:3000/sse"
}
}
}
Available Tools
Discovery
| Tool | Description |
|---|---|
list_documents |
List documents accessible to this agent with their permissions |
Read Operations (requires read permission)
| Tool | Description |
|---|---|
list_tables |
List all tables in a document |
describe_table |
Get column information (id, type, formula) for a table |
get_records |
Fetch records with optional filter, sort, and limit |
sql_query |
Run a read-only SELECT query against a document |
Write Operations (requires write permission)
| Tool | Description |
|---|---|
add_records |
Add new records to a table |
update_records |
Update existing records by ID |
delete_records |
Delete records by ID |
Schema Operations (requires schema permission)
| Tool | Description |
|---|---|
create_table |
Create a new table with specified columns |
add_column |
Add a column to an existing table |
modify_column |
Change a column's type or formula |
delete_column |
Remove a column from a table |
Configuration Reference
Environment Variables
| Variable | Description | Default |
|---|---|---|
PORT |
Server port | 3000 |
GRIST_MCP_TOKEN |
Agent authentication token (required) | - |
CONFIG_PATH |
Path to config file inside container | /app/config.yaml |
LOG_LEVEL |
Logging verbosity (DEBUG, INFO, WARNING, ERROR) |
INFO |
GRIST_MCP_URL |
Public URL of this server (for session proxy tokens) | - |
config.yaml Structure
# Document definitions (each is self-contained)
documents:
budget-2024:
url: https://work.getgrist.com
doc_id: mK7xB2pQ9mN4v
api_key: ${GRIST_WORK_API_KEY} # Supports environment variable substitution
personal-tracker:
url: https://docs.getgrist.com
doc_id: pN0zE5sT2qP7x
api_key: ${GRIST_PERSONAL_API_KEY}
# Agent tokens with access scopes
tokens:
- token: your-secure-token-here
name: finance-agent
scope:
- document: budget-2024
permissions: [read, write] # Can read and write
- token: another-token-here
name: readonly-agent
scope:
- document: budget-2024
permissions: [read] # Read only
- document: personal-tracker
permissions: [read, write, schema] # Full access
Permission Levels
read: Query tables and records, run SQL querieswrite: Add, update, delete recordsschema: Create tables, add/modify/delete columns
Logging
Configuration
Set the LOG_LEVEL environment variable to control logging verbosity:
| Level | Description |
|---|---|
DEBUG |
Show all logs including HTTP requests and tool arguments |
INFO |
Show tool calls with stats (default) |
WARNING |
Show only auth errors and warnings |
ERROR |
Show only errors |
# In .env or docker-compose.yml
LOG_LEVEL=INFO
Log Format
At INFO level, each tool call produces a single log line:
2026-01-02 10:15:23 | agent-name (abc...xyz) | get_records | sales | 42 records | success | 125ms
| Field | Description |
|---|---|
| Timestamp | YYYY-MM-DD HH:MM:SS |
| Agent | Agent name with truncated token |
| Tool | MCP tool name |
| Document | Document name (or - for list_documents) |
| Stats | Operation result (e.g., 42 records, 3 tables) |
| Status | success, auth_error, or error |
| Duration | Execution time in milliseconds |
Errors include details on a second indented line:
2026-01-02 10:15:23 | agent-name (abc...xyz) | add_records | sales | - | error | 89ms
Grist API error: Invalid column 'foo'
Production Recommendations
- Use
LOG_LEVEL=INFOfor normal operation (default) - Use
LOG_LEVEL=DEBUGfor troubleshooting (shows HTTP traffic) - Use
LOG_LEVEL=WARNINGfor minimal logging
Security
- Token-based auth: Each agent has a unique token with specific document access
- Permission scopes: Granular control with
read,write, andschemapermissions - SQL validation: Only SELECT queries allowed, no multi-statement queries
- API key isolation: Each document can use a different Grist API key
- No token exposure: Tokens are validated at startup, not stored in responses
Development
Requirements
- Python 3.14+
- uv package manager
Local Setup
# Clone the repository
git clone https://github.com/Xe138/grist-mcp-server.git
cd grist-mcp-server
# Install dependencies
uv sync --dev
# Run tests
make test-unit
Running Locally
export GRIST_MCP_TOKEN="your-agent-token"
CONFIG_PATH=./config.yaml uv run python -m grist_mcp.main
Project Structure
grist-mcp/
├── src/grist_mcp/
│ ├── main.py # Entry point
│ ├── server.py # MCP server setup and tool registration
│ ├── config.py # Configuration loading
│ ├── auth.py # Authentication and authorization
│ ├── grist_client.py # Grist API client
│ └── tools/
│ ├── discovery.py # list_documents
│ ├── read.py # Read operations
│ ├── write.py # Write operations
│ └── schema.py # Schema operations
├── tests/
│ ├── unit/ # Unit tests
│ └── integration/ # Integration tests
├── deploy/
│ ├── dev/ # Development docker-compose
│ ├── test/ # Test docker-compose
│ └── prod/ # Production docker-compose
└── config.yaml.example
License
MIT