From c1d29f1a335e51ee41a982299ab49ba41a43d91e Mon Sep 17 00:00:00 2001 From: Bill Date: Wed, 3 Dec 2025 14:01:10 -0500 Subject: [PATCH] Add grist-mcp design document --- docs/plans/2025-12-03-grist-mcp-design.md | 255 ++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 docs/plans/2025-12-03-grist-mcp-design.md diff --git a/docs/plans/2025-12-03-grist-mcp-design.md b/docs/plans/2025-12-03-grist-mcp-design.md new file mode 100644 index 0000000..7e8640a --- /dev/null +++ b/docs/plans/2025-12-03-grist-mcp-design.md @@ -0,0 +1,255 @@ +# 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 + +```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: + +```bash +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 + +1. Agent connects to MCP server at `http://host:8080/mcp` +2. Agent provides token via `Authorization: Bearer ` header +3. Server looks up token in config, retrieves associated scope +4. 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 + +```yaml +services: + grist-mcp: + build: . + ports: + - "8080:8080" + volumes: + - ./config.yaml:/app/config.yaml:ro + env_file: + - .env +``` + +### Dockerfile + +```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 + +```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 |