9.0 KiB
9.0 KiB
Grist MCP Server Design
Overview
A dockerized MCP server that allows AI agents to interact with Grist documents. Each agent authenticates with a token that grants access to specific documents with defined permission levels.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ grist-mcp Container │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ MCP Server │ │ Grist Client │ │
│ │ (SSE/HTTP) │─────────▶│ Layer │ │
│ │ :8080 │ │ │ │
│ └──────────┬───────────┘ └──────────────────────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ config.yaml │ ← Mounted volume (read-only) │
│ └───────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────────┐
│ AI Agents │ │ Grist Servers │
│ (with tokens)│ │ │
└─────────────┘ └─────────────────┘
Key characteristics:
- Single container, single port (8080)
- All configuration from one YAML file
- No database, no admin API
- Restart container to apply config changes
Configuration
config.yaml
# ============================================================
# Token Generation:
# python -c "import secrets; print(secrets.token_urlsafe(32))"
# openssl rand -base64 32
# ============================================================
# Document definitions (each is self-contained)
documents:
budget-2024:
url: https://work.getgrist.com
doc_id: mK7xB2pQ9mN4v
api_key: ${GRIST_WORK_API_KEY}
expenses:
url: https://work.getgrist.com
doc_id: nL8yC3qR0oO5w
api_key: ${GRIST_WORK_API_KEY}
personal-tracker:
url: https://docs.getgrist.com
doc_id: pN0zE5sT2qP7x
api_key: ${GRIST_PERSONAL_API_KEY}
# Agent tokens with access scopes
tokens:
- token: dG9rZW4tZmluYW5jZS1hZ2VudC0xMjM0NTY3ODkw
name: finance-agent
scope:
- document: budget-2024
permissions: [read, write]
- document: expenses
permissions: [read]
- token: K7xB2pQ9mN4vR8wY1zA3cE6fH0jL5oI2sU7tD4gM
name: analytics-agent
scope:
- document: personal-tracker
permissions: [read, write, schema]
Permission Levels
| Permission | Description |
|---|---|
read |
Query tables, get records, list structure |
write |
Create, update, delete records |
schema |
Create/modify tables, columns, formulas |
Environment Variables
API keys are referenced via ${VAR} syntax in config.yaml and resolved at startup. Store actual keys in .env file:
GRIST_WORK_API_KEY=actual-api-key-here
GRIST_PERSONAL_API_KEY=another-api-key
MCP Tools
Discovery
| Tool | Permission | Description |
|---|---|---|
list_documents |
(always available) | List documents this token can access with their permissions |
Read Operations (requires read)
| Tool | Description |
|---|---|
list_tables |
List all tables in a document |
describe_table |
Get column names, types, and formulas for a table |
get_records |
Fetch records with optional filters and sorting |
sql_query |
Run read-only SQL against the document |
Write Operations (requires write)
| Tool | Description |
|---|---|
add_records |
Insert one or more records into a table |
update_records |
Update existing records by ID |
delete_records |
Delete records by ID |
Schema Operations (requires schema)
| Tool | Description |
|---|---|
create_table |
Create a new table with column definitions |
add_column |
Add a column to an existing table |
modify_column |
Change column type or formula |
delete_column |
Remove a column from a table |
Authentication Flow
- Agent connects to MCP server at
http://host:8080/mcp - Agent provides token via
Authorization: Bearer <token>header - Server looks up token in config, retrieves associated scope
- All subsequent tool calls are validated against that scope
Validation on Each Tool Call
Agent calls: get_records(document="budget-2024", table="Transactions")
│
▼
┌───────────────────────────────┐
│ 1. Is token valid? │──No──▶ 401 Unauthorized
└───────────────┬───────────────┘
│ Yes
▼
┌───────────────────────────────┐
│ 2. Does token have access to │──No──▶ 403 Forbidden
│ document "budget-2024"? │
└───────────────┬───────────────┘
│ Yes
▼
┌───────────────────────────────┐
│ 3. Does token have "read" │──No──▶ 403 Forbidden
│ permission on this doc? │
└───────────────┬───────────────┘
│ Yes
▼
┌───────────────────────────────┐
│ 4. Execute against Grist API │
│ using doc's api_key │
└───────────────────────────────┘
Error Responses
- Invalid/missing token →
401 Unauthorized(no details) - Valid token, wrong document →
403 Forbidden: Document not in scope - Valid token, wrong permission →
403 Forbidden: Permission denied
Project Structure
grist-mcp/
├── Dockerfile
├── docker-compose.yaml
├── config.yaml.example # Template with dummy values
├── pyproject.toml
├── uv.lock
├── src/
│ └── grist_mcp/
│ ├── __init__.py
│ ├── main.py # Entry point, loads config, starts server
│ ├── config.py # Config parsing, env var substitution
│ ├── auth.py # Token validation, permission checking
│ ├── grist_client.py # Grist API wrapper
│ └── tools/
│ ├── __init__.py
│ ├── discovery.py # list_documents
│ ├── read.py # list_tables, describe_table, get_records, sql_query
│ ├── write.py # add_records, update_records, delete_records
│ └── schema.py # create_table, add_column, modify_column, delete_column
└── tests/
└── ...
Docker Setup
docker-compose.yaml
services:
grist-mcp:
build: .
ports:
- "8080:8080"
volumes:
- ./config.yaml:/app/config.yaml:ro
env_file:
- .env
Dockerfile
FROM ghcr.io/astral-sh/uv:python3.14-bookworm-slim
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
COPY src/ ./src/
CMD ["uv", "run", "python", "-m", "grist_mcp.main"]
pyproject.toml
[project]
name = "grist-mcp"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = [
"mcp",
"httpx",
"pyyaml",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Technology Stack
| Component | Choice |
|---|---|
| Language | Python 3.14 |
| Package Manager | uv |
| MCP Framework | mcp (official SDK) |
| HTTP Client | httpx |
| Config Parsing | pyyaml |
| Deployment | Docker |