docs: add Docker deployment implementation plan

This commit is contained in:
2025-12-29 19:37:59 -05:00
parent efe3ddf27b
commit 6782861cff

View File

@@ -0,0 +1,444 @@
# Docker Deployment Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Make grist-mcp deployable via Docker Compose with automated CI builds on version tag pushes.
**Architecture:** Replace stdio transport with SSE (HTTP-based) for remote operation. Multi-stage Dockerfile for small images. Single adaptive CI workflow that detects Gitea vs GitHub and pushes to the appropriate registry.
**Tech Stack:** Python 3.14, Starlette (ASGI), Uvicorn, Docker, GitHub Actions (compatible with Gitea Actions)
---
### Task 1: Add SSE Transport Dependencies
**Files:**
- Modify: `pyproject.toml`
**Step 1: Add dependencies to pyproject.toml**
Edit `pyproject.toml` to add the SSE transport dependencies:
```python
dependencies = [
"mcp>=1.0.0",
"httpx>=0.27.0",
"pyyaml>=6.0",
"starlette>=0.41.0",
"uvicorn>=0.32.0",
"sse-starlette>=2.1.0",
]
```
**Step 2: Sync dependencies**
Run: `uv sync`
Expected: Dependencies install successfully
**Step 3: Commit**
```bash
git add pyproject.toml uv.lock
git commit -m "feat: add SSE transport dependencies"
```
---
### Task 2: Implement SSE Transport in main.py
**Files:**
- Modify: `src/grist_mcp/main.py`
**Step 1: Replace main.py with SSE implementation**
Replace the entire contents of `src/grist_mcp/main.py` with:
```python
"""Main entry point for the MCP server with SSE transport."""
import os
import sys
import uvicorn
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.routing import Route
from grist_mcp.server import create_server
from grist_mcp.auth import AuthError
def create_app() -> Starlette:
"""Create the Starlette ASGI application."""
config_path = os.environ.get("CONFIG_PATH", "/app/config.yaml")
if not os.path.exists(config_path):
print(f"Error: Config file not found at {config_path}", file=sys.stderr)
sys.exit(1)
try:
server = create_server(config_path)
except AuthError as e:
print(f"Authentication error: {e}", file=sys.stderr)
sys.exit(1)
sse = SseServerTransport("/messages")
async def handle_sse(request):
async with sse.connect_sse(
request.scope, request.receive, request._send
) as streams:
await server.run(
streams[0], streams[1], server.create_initialization_options()
)
async def handle_messages(request):
await sse.handle_post_message(request.scope, request.receive, request._send)
return Starlette(
routes=[
Route("/sse", endpoint=handle_sse),
Route("/messages", endpoint=handle_messages, methods=["POST"]),
]
)
def main():
"""Run the SSE server."""
port = int(os.environ.get("PORT", "3000"))
app = create_app()
print(f"Starting grist-mcp SSE server on port {port}")
print(f" SSE endpoint: http://0.0.0.0:{port}/sse")
print(f" Messages endpoint: http://0.0.0.0:{port}/messages")
uvicorn.run(app, host="0.0.0.0", port=port)
if __name__ == "__main__":
main()
```
**Step 2: Test that the server starts**
Run: `CONFIG_PATH=./config.yaml.example GRIST_MCP_TOKEN=test uv run python -m grist_mcp.main &`
Expected: Server starts, prints port info (will fail auth but that's OK for this test)
Kill: `pkill -f "python -m grist_mcp.main"`
**Step 3: Commit**
```bash
git add src/grist_mcp/main.py
git commit -m "feat: replace stdio with SSE transport"
```
---
### Task 3: Create Dockerfile
**Files:**
- Create: `Dockerfile`
**Step 1: Create multi-stage Dockerfile**
Create `Dockerfile` with:
```dockerfile
# Stage 1: Builder
FROM python:3.14-slim AS builder
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
# Copy dependency files
COPY pyproject.toml uv.lock ./
# Install dependencies
RUN uv sync --frozen --no-dev --no-install-project
# Copy source code
COPY src ./src
# Install the project
RUN uv sync --frozen --no-dev
# Stage 2: Runtime
FROM python:3.14-slim
# Create non-root user
RUN useradd --create-home --shell /bin/bash appuser
WORKDIR /app
# Copy virtual environment from builder
COPY --from=builder /app/.venv /app/.venv
# Copy source code
COPY --from=builder /app/src ./src
# Set environment
ENV PATH="/app/.venv/bin:$PATH"
ENV PORT=3000
# Switch to non-root user
USER appuser
EXPOSE 3000
CMD ["python", "-m", "grist_mcp.main"]
```
**Step 2: Verify Dockerfile syntax**
Run: `docker build --check .` (if available) or just proceed to next step
**Step 3: Commit**
```bash
git add Dockerfile
git commit -m "feat: add multi-stage Dockerfile"
```
---
### Task 4: Create docker-compose.yaml
**Files:**
- Create: `docker-compose.yaml`
**Step 1: Create docker-compose.yaml**
Create `docker-compose.yaml` with:
```yaml
services:
grist-mcp:
build: .
ports:
- "${PORT:-3000}:3000"
volumes:
- ./config.yaml:/app/config.yaml:ro
env_file:
- .env
restart: unless-stopped
```
**Step 2: Commit**
```bash
git add docker-compose.yaml
git commit -m "feat: add docker-compose.yaml"
```
---
### Task 5: Create .env.example
**Files:**
- Create: `.env.example`
**Step 1: Create .env.example**
Create `.env.example` with:
```bash
# grist-mcp environment configuration
# Server port (default: 3000)
PORT=3000
# Agent authentication token (required)
# Generate with: python -c "import secrets; print(secrets.token_urlsafe(32))"
GRIST_MCP_TOKEN=your-agent-token-here
# Grist API keys (referenced in config.yaml)
GRIST_WORK_API_KEY=your-work-api-key
GRIST_PERSONAL_API_KEY=your-personal-api-key
# Optional: Override config path (default: /app/config.yaml)
# CONFIG_PATH=/app/config.yaml
```
**Step 2: Commit**
```bash
git add .env.example
git commit -m "feat: add .env.example template"
```
---
### Task 6: Create CI Workflow
**Files:**
- Create: `.github/workflows/build.yaml`
**Step 1: Create workflow directory**
Run: `mkdir -p .github/workflows`
**Step 2: Create adaptive CI workflow**
Create `.github/workflows/build.yaml` with:
```yaml
name: Build and Push Docker Image
on:
push:
tags:
- 'v*.*.*'
env:
IMAGE_NAME: grist-mcp
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Determine registry
id: registry
run: |
if [ "${{ vars.GITEA_ACTIONS }}" = "true" ]; then
# Gitea: use server URL as registry
REGISTRY="${{ github.server_url }}"
REGISTRY="${REGISTRY#https://}"
REGISTRY="${REGISTRY#http://}"
echo "registry=${REGISTRY}" >> $GITHUB_OUTPUT
echo "is_gitea=true" >> $GITHUB_OUTPUT
else
# GitHub: use GHCR
echo "registry=ghcr.io" >> $GITHUB_OUTPUT
echo "is_gitea=false" >> $GITHUB_OUTPUT
fi
- name: Log in to GitHub Container Registry
if: steps.registry.outputs.is_gitea == 'false'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Gitea Container Registry
if: steps.registry.outputs.is_gitea == 'true'
uses: docker/login-action@v3
with:
registry: ${{ steps.registry.outputs.registry }}
username: ${{ github.actor }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ steps.registry.outputs.registry }}/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
```
**Step 3: Commit**
```bash
git add .github/workflows/build.yaml
git commit -m "feat: add CI workflow for Docker builds"
```
---
### Task 7: Update README with Docker Instructions
**Files:**
- Modify: `README.md`
**Step 1: Read current README**
Read `README.md` to understand current structure.
**Step 2: Add Docker section after existing content**
Add a "Docker Deployment" section with:
- Quick start with docker-compose
- Environment variable reference
- Configuration instructions
**Step 3: Commit**
```bash
git add README.md
git commit -m "docs: add Docker deployment instructions"
```
---
### Task 8: Test Docker Build Locally
**Files:**
- None (verification only)
**Step 1: Build the Docker image**
Run: `docker build -t grist-mcp:test .`
Expected: Build completes successfully
**Step 2: Create test config and .env**
Run:
```bash
cp config.yaml.example config.yaml
cp .env.example .env
```
Edit `.env` to set a test token matching one in `config.yaml`.
**Step 3: Test with docker-compose**
Run: `docker compose up -d`
Expected: Container starts
**Step 4: Verify server is running**
Run: `curl -I http://localhost:3000/sse`
Expected: HTTP response (connection may hang waiting for SSE, that's OK)
**Step 5: Clean up**
Run: `docker compose down`
**Step 6: Final commit if any fixes needed**
If any fixes were required, commit them.
---
## Post-Implementation
After all tasks complete:
1. **Tag a version:** `git tag v0.2.0 && git push --tags`
2. **Configure Gitea secret:** Add `REGISTRY_TOKEN` in Gitea repo settings
3. **Verify CI:** Check that workflow runs and pushes images