Compare commits
9 Commits
v1.0.0-alp
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 16691e1d21 | |||
| 204d00caf4 | |||
| ca03d22b97 | |||
| 107db82c52 | |||
| 4b89837b43 | |||
| 5aaa943010 | |||
| c8cea249bc | |||
| ae894ff52e | |||
| 7b7eea2f67 |
@@ -11,7 +11,7 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: linux_x64
|
runs-on: ubuntu-docker
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
268
README.md
268
README.md
@@ -15,50 +15,33 @@ grist-mcp is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/)
|
|||||||
- **Security**: Token-based authentication with per-document permission scopes (read, write, schema)
|
- **Security**: Token-based authentication with per-document permission scopes (read, write, schema)
|
||||||
- **Multi-tenant**: Support multiple Grist instances and documents
|
- **Multi-tenant**: Support multiple Grist instances and documents
|
||||||
|
|
||||||
## Requirements
|
## Quick Start (Docker)
|
||||||
|
|
||||||
- Python 3.14+
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker and Docker Compose
|
||||||
- Access to one or more Grist documents with API keys
|
- Access to one or more Grist documents with API keys
|
||||||
|
|
||||||
## Installation
|
### 1. Create configuration directory
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
mkdir grist-mcp && cd grist-mcp
|
||||||
git clone https://github.com/your-org/grist-mcp.git
|
|
||||||
cd grist-mcp
|
|
||||||
|
|
||||||
# Install with uv
|
|
||||||
uv sync --dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
### 2. Download configuration files
|
||||||
|
|
||||||
Create a `config.yaml` file based on the example:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 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
|
cp config.yaml.example config.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuration Structure
|
### 3. Generate tokens
|
||||||
|
|
||||||
```yaml
|
Generate a secure token for your agent:
|
||||||
# Document definitions
|
|
||||||
documents:
|
|
||||||
my-document:
|
|
||||||
url: https://docs.getgrist.com # Grist instance URL
|
|
||||||
doc_id: abcd1234 # Document ID from URL
|
|
||||||
api_key: ${GRIST_API_KEY} # API key (supports env vars)
|
|
||||||
|
|
||||||
# Agent tokens with access scopes
|
|
||||||
tokens:
|
|
||||||
- token: your-secret-token # Unique token for this agent
|
|
||||||
name: my-agent # Human-readable name
|
|
||||||
scope:
|
|
||||||
- document: my-document
|
|
||||||
permissions: [read, write] # Allowed: read, write, schema
|
|
||||||
```
|
|
||||||
|
|
||||||
### Generating Tokens
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -c "import secrets; print(secrets.token_urlsafe(32))"
|
python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||||
@@ -66,34 +49,53 @@ python -c "import secrets; print(secrets.token_urlsafe(32))"
|
|||||||
openssl rand -base64 32
|
openssl rand -base64 32
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
### 4. Configure config.yaml
|
||||||
|
|
||||||
- `CONFIG_PATH`: Path to config file (default: `/app/config.yaml`)
|
Edit `config.yaml` to define your Grist documents and agent tokens:
|
||||||
- `GRIST_MCP_TOKEN`: Agent token for authentication
|
|
||||||
- Config file supports `${VAR}` syntax for API keys
|
|
||||||
|
|
||||||
## Usage
|
```yaml
|
||||||
|
# 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)
|
||||||
|
|
||||||
### Running the Server
|
# Agent tokens with access scopes
|
||||||
|
tokens:
|
||||||
The server uses SSE (Server-Sent Events) transport over HTTP:
|
- token: your-generated-token-here # The token you generated in step 3
|
||||||
|
name: my-agent # Human-readable name
|
||||||
```bash
|
scope:
|
||||||
# Set your agent token
|
- document: my-document # Must match a document name above
|
||||||
export GRIST_MCP_TOKEN="your-agent-token"
|
permissions: [read, write] # Allowed: read, write, schema
|
||||||
|
|
||||||
# Run with custom config path (defaults to port 3000)
|
|
||||||
CONFIG_PATH=./config.yaml uv run python -m grist_mcp.main
|
|
||||||
|
|
||||||
# Or specify a custom port
|
|
||||||
PORT=8080 CONFIG_PATH=./config.yaml uv run python -m grist_mcp.main
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The server exposes two endpoints:
|
**Finding your Grist document ID**: Open your Grist document in a browser. The URL will look like:
|
||||||
- `http://localhost:3000/sse` - SSE connection endpoint
|
`https://docs.getgrist.com/abcd1234efgh5678/My-Document` - the document ID is `abcd1234efgh5678`.
|
||||||
- `http://localhost:3000/messages` - Message posting endpoint
|
|
||||||
|
|
||||||
### MCP Client Configuration
|
**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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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):
|
Add to your MCP client configuration (e.g., Claude Desktop):
|
||||||
|
|
||||||
@@ -101,24 +103,13 @@ Add to your MCP client configuration (e.g., Claude Desktop):
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"grist": {
|
"grist": {
|
||||||
|
"type": "sse",
|
||||||
"url": "http://localhost:3000/sse"
|
"url": "http://localhost:3000/sse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
For remote deployments, use the server's public URL:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"grist": {
|
|
||||||
"url": "https://your-server.example.com/sse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Available Tools
|
## Available Tools
|
||||||
|
|
||||||
### Discovery
|
### Discovery
|
||||||
@@ -149,6 +140,54 @@ For remote deployments, use the server's public URL:
|
|||||||
| `modify_column` | Change a column's type or formula |
|
| `modify_column` | Change a column's type or formula |
|
||||||
| `delete_column` | Remove a column from a table |
|
| `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` |
|
||||||
|
|
||||||
|
### config.yaml Structure
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 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 queries
|
||||||
|
- `write`: Add, update, delete records
|
||||||
|
- `schema`: Create tables, add/modify/delete columns
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
- **Token-based auth**: Each agent has a unique token with specific document access
|
- **Token-based auth**: Each agent has a unique token with specific document access
|
||||||
@@ -159,10 +198,30 @@ For remote deployments, use the server's public URL:
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Running Tests
|
### Requirements
|
||||||
|
|
||||||
|
- Python 3.14+
|
||||||
|
- uv package manager
|
||||||
|
|
||||||
|
### Local Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run pytest -v
|
# 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GRIST_MCP_TOKEN="your-agent-token"
|
||||||
|
CONFIG_PATH=./config.yaml uv run python -m grist_mcp.main
|
||||||
```
|
```
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
@@ -170,7 +229,6 @@ uv run pytest -v
|
|||||||
```
|
```
|
||||||
grist-mcp/
|
grist-mcp/
|
||||||
├── src/grist_mcp/
|
├── src/grist_mcp/
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── main.py # Entry point
|
│ ├── main.py # Entry point
|
||||||
│ ├── server.py # MCP server setup and tool registration
|
│ ├── server.py # MCP server setup and tool registration
|
||||||
│ ├── config.py # Configuration loading
|
│ ├── config.py # Configuration loading
|
||||||
@@ -182,73 +240,13 @@ grist-mcp/
|
|||||||
│ ├── write.py # Write operations
|
│ ├── write.py # Write operations
|
||||||
│ └── schema.py # Schema operations
|
│ └── schema.py # Schema operations
|
||||||
├── tests/
|
├── tests/
|
||||||
├── config.yaml.example
|
│ ├── unit/ # Unit tests
|
||||||
└── pyproject.toml
|
│ └── integration/ # Integration tests
|
||||||
```
|
├── deploy/
|
||||||
|
│ ├── dev/ # Development docker-compose
|
||||||
## Docker Deployment
|
│ ├── test/ # Test docker-compose
|
||||||
|
│ └── prod/ # Production docker-compose
|
||||||
### Prerequisites
|
└── config.yaml.example
|
||||||
|
|
||||||
- Docker and Docker Compose
|
|
||||||
|
|
||||||
### Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Copy example files
|
|
||||||
cp .env.example .env
|
|
||||||
cp config.yaml.example config.yaml
|
|
||||||
|
|
||||||
# 2. Edit .env with your tokens and API keys
|
|
||||||
# - Set GRIST_MCP_TOKEN to a secure agent token
|
|
||||||
# - Set your Grist API keys
|
|
||||||
|
|
||||||
# 3. Edit config.yaml with your document settings
|
|
||||||
# - Configure your Grist documents
|
|
||||||
# - Set up token scopes and permissions
|
|
||||||
|
|
||||||
# 4. Start the server
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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` |
|
|
||||||
| `GRIST_*_API_KEY` | Grist API keys referenced in config.yaml | - |
|
|
||||||
|
|
||||||
### Using Prebuilt Images
|
|
||||||
|
|
||||||
To use a prebuilt image from a container registry:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# docker-compose.yaml
|
|
||||||
services:
|
|
||||||
grist-mcp:
|
|
||||||
image: your-registry/grist-mcp:latest
|
|
||||||
ports:
|
|
||||||
- "${PORT:-3000}:3000"
|
|
||||||
volumes:
|
|
||||||
- ./config.yaml:/app/config.yaml:ro
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
restart: unless-stopped
|
|
||||||
```
|
|
||||||
|
|
||||||
### Building Locally
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build the image
|
|
||||||
docker build -t grist-mcp .
|
|
||||||
|
|
||||||
# Run directly
|
|
||||||
docker run -p 3000:3000 \
|
|
||||||
-v $(pwd)/config.yaml:/app/config.yaml:ro \
|
|
||||||
--env-file .env \
|
|
||||||
grist-mcp
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ documents:
|
|||||||
doc_id: pN0zE5sT2qP7x
|
doc_id: pN0zE5sT2qP7x
|
||||||
api_key: ${GRIST_PERSONAL_API_KEY}
|
api_key: ${GRIST_PERSONAL_API_KEY}
|
||||||
|
|
||||||
|
# Docker networking example: connect via internal hostname,
|
||||||
|
# but send the external domain in the Host header
|
||||||
|
docker-grist:
|
||||||
|
url: http://grist:8080
|
||||||
|
doc_id: abc123
|
||||||
|
api_key: ${GRIST_API_KEY}
|
||||||
|
host_header: grist.example.com # Required when Grist validates Host header
|
||||||
|
|
||||||
# Agent tokens with access scopes
|
# Agent tokens with access scopes
|
||||||
tokens:
|
tokens:
|
||||||
- token: REPLACE_WITH_GENERATED_TOKEN
|
- token: REPLACE_WITH_GENERATED_TOKEN
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
# Production environment - resource limits, logging, restart policy
|
# Production environment
|
||||||
services:
|
services:
|
||||||
grist-mcp:
|
grist-mcp:
|
||||||
build:
|
image: ghcr.io/xe138/grist-mcp-server:latest
|
||||||
context: ../..
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
ports:
|
ports:
|
||||||
- "${PORT:-3000}:3000"
|
- "${PORT:-3000}:3000"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -12,18 +10,6 @@ services:
|
|||||||
- CONFIG_PATH=/app/config.yaml
|
- CONFIG_PATH=/app/config.yaml
|
||||||
- EXTERNAL_PORT=${PORT:-3000}
|
- EXTERNAL_PORT=${PORT:-3000}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 512M
|
|
||||||
cpus: "1"
|
|
||||||
reservations:
|
|
||||||
memory: 128M
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "50m"
|
|
||||||
max-file: "5"
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3000/health')"]
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3000/health')"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|||||||
3
renovate.json
Normal file
3
renovate.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ class Document:
|
|||||||
url: str
|
url: str
|
||||||
doc_id: str
|
doc_id: str
|
||||||
api_key: str
|
api_key: str
|
||||||
|
host_header: str | None = None # Override Host header for Docker networking
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -78,6 +79,7 @@ def load_config(config_path: str) -> Config:
|
|||||||
url=doc_data["url"],
|
url=doc_data["url"],
|
||||||
doc_id=doc_data["doc_id"],
|
doc_id=doc_data["doc_id"],
|
||||||
api_key=doc_data["api_key"],
|
api_key=doc_data["api_key"],
|
||||||
|
host_header=doc_data.get("host_header"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Parse tokens
|
# Parse tokens
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ class GristClient:
|
|||||||
self._doc = document
|
self._doc = document
|
||||||
self._base_url = f"{document.url.rstrip('/')}/api/docs/{document.doc_id}"
|
self._base_url = f"{document.url.rstrip('/')}/api/docs/{document.doc_id}"
|
||||||
self._headers = {"Authorization": f"Bearer {document.api_key}"}
|
self._headers = {"Authorization": f"Bearer {document.api_key}"}
|
||||||
|
if document.host_header:
|
||||||
|
self._headers["Host"] = document.host_header
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
|
|
||||||
async def _request(self, method: str, path: str, **kwargs) -> dict:
|
async def _request(self, method: str, path: str, **kwargs) -> dict:
|
||||||
|
|||||||
@@ -74,19 +74,34 @@ def _ensure_config(config_path: str) -> bool:
|
|||||||
|
|
||||||
# Check if path is a directory (Docker creates this when mounting missing file)
|
# Check if path is a directory (Docker creates this when mounting missing file)
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
os.rmdir(path)
|
print(f"ERROR: Config path is a directory: {path}")
|
||||||
|
print()
|
||||||
|
print("This usually means the config file doesn't exist on the host.")
|
||||||
|
print("Please create the config file before starting the container:")
|
||||||
|
print()
|
||||||
|
print(f" mkdir -p $(dirname {config_path})")
|
||||||
|
print(f" cat > {config_path} << 'EOF'")
|
||||||
|
print(CONFIG_TEMPLATE)
|
||||||
|
print("EOF")
|
||||||
|
print()
|
||||||
|
return False
|
||||||
|
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Create template config
|
# Create template config
|
||||||
with open(path, "w") as f:
|
try:
|
||||||
f.write(CONFIG_TEMPLATE)
|
with open(path, "w") as f:
|
||||||
|
f.write(CONFIG_TEMPLATE)
|
||||||
print(f"Created template configuration at: {path}")
|
print(f"Created template configuration at: {path}")
|
||||||
print()
|
print()
|
||||||
print("Please edit this file to configure your Grist documents and agent tokens,")
|
print("Please edit this file to configure your Grist documents and agent tokens,")
|
||||||
print("then restart the server.")
|
print("then restart the server.")
|
||||||
|
except PermissionError:
|
||||||
|
print(f"ERROR: Cannot create config file at: {path}")
|
||||||
|
print()
|
||||||
|
print("Please create the config file manually before starting the container.")
|
||||||
|
print()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user