Skip to content

Commit 1500c9f

Browse files
Add VitePress documentation website (#32)
Sets up a VitePress site (matching geo-agent style) under docs/ with GitHub Actions deployment to GitHub Pages. Covers quickstart, datasets, private data access, deployment, and architecture. Closes #31 Co-authored-by: Carl Boettiger <cboettig@berkeley.edu>
1 parent 76b8a17 commit 1500c9f

13 files changed

Lines changed: 2980 additions & 11 deletions

File tree

.github/workflows/gh-pages.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Deploy to GitHub Pages
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'docs/**'
8+
- 'package.json'
9+
- '.github/workflows/gh-pages.yml'
10+
11+
workflow_dispatch:
12+
13+
permissions:
14+
contents: read
15+
pages: write
16+
id-token: write
17+
18+
concurrency:
19+
group: pages
20+
cancel-in-progress: true
21+
22+
jobs:
23+
build:
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
- uses: actions/setup-node@v4
29+
with:
30+
node-version: 20
31+
cache: npm
32+
33+
- name: Install dependencies
34+
run: npm ci
35+
36+
- name: Build VitePress docs
37+
run: npm run docs:build
38+
39+
- name: Setup Pages
40+
uses: actions/configure-pages@v5
41+
42+
- name: Upload artifact
43+
uses: actions/upload-pages-artifact@v3
44+
with:
45+
path: docs/.vitepress/dist
46+
47+
deploy:
48+
needs: build
49+
environment:
50+
name: github-pages
51+
url: ${{ steps.deployment.outputs.page_url }}
52+
runs-on: ubuntu-latest
53+
steps:
54+
- name: Deploy to GitHub Pages
55+
id: deployment
56+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,6 @@ cython_debug/
180180
.pypirc
181181

182182
.venv/
183+
node_modules/
184+
docs/.vitepress/dist/
185+
docs/.vitepress/cache/

AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Agent Architecture
22

3+
> **The `.md` files in this repo are runtime prompt artifacts, not documentation.**
4+
> `server.py` loads them at startup and injects their content into MCP tool descriptions
5+
> and prompts. Editing them changes what the agent is instructed to do.
6+
> `README.md` is the only human-facing documentation.
7+
38
## Deployment
49

510
The MCP server runs on the NRP Nautilus Kubernetes cluster at **`https://duckdb-mcp.nrp-nautilus.io`**.

README.md

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,25 @@ We have a fully-hosted version
122122

123123
- **server.py** - Main MCP server with FastMCP framework
124124
- **stac.py** - STAC catalog integration for dynamic dataset discovery
125-
- **query-setup.md** - Required DuckDB configuration for all queries
126-
- **query-optimization.md** - Performance optimization guidelines
127-
- **h3-guide.md** - H3 geospatial operations reference
125+
126+
### Runtime Prompt Files
127+
128+
The `.md` files in this repo are **not documentation** — they are curated prompt content loaded by `server.py` at startup and injected directly into MCP tool descriptions and prompts at runtime. The agent (LLM) reads them as instructions, not humans.
129+
130+
| File | How it is used |
131+
|---|---|
132+
| `query-setup.md` | SQL parsed and executed in every fresh DuckDB connection before a query runs |
133+
| `query-optimization.md` | Injected verbatim into the `query` tool description |
134+
| `h3-guide.md` | Injected verbatim into the `query` tool description |
135+
| `assistant-role.md` | Served as the `geospatial-analyst` MCP prompt (role + response style) |
136+
137+
Editing these files changes what the agent is told to do. They must be written for a stateless LLM — short, concrete, and unambiguous. See `AGENTS.md` for editing rules.
128138

129139
### Key Design Patterns
130140

131-
1. **Prompt Engineering**: Injects strict SQL rules into tool descriptions to guide LLM behavior
132-
2. **Isolation Engine**: Each query gets a fresh DuckDB connection for security
133-
3. **Context Injection**: Documentation is embedded into MCP resources and tool descriptions
134-
4. **Partition Pruning**: Uses H3 resolution columns (`h0`) for efficient S3 reads
141+
1. **Isolation Engine**: Each query runs in a fresh `duckdb.connect(":memory:")` — no state or credentials survive between requests
142+
2. **Context Injection**: Prompt files are embedded into tool descriptions so even MCP clients that don't support `prompts/list` receive the guidance
143+
3. **Partition Pruning**: H3 resolution columns (`h0`) enable DuckDB to skip S3 partitions, giving 5–20× speedups on large datasets
135144

136145
## Kubernetes Deployment
137146

@@ -153,9 +162,9 @@ The deployment:
153162

154163
### Tools
155164

156-
- `list_datasets(catalog_url?, catalog_token?)` - List available datasets from the STAC catalog
157-
- `get_dataset(dataset_id, catalog_url?, catalog_token?)` - Get S3 paths and schema for a dataset
158-
- `query(sql_query, s3_key?, s3_secret?, s3_endpoint?)` - Execute DuckDB SQL against S3 parquet files
165+
- `browse_stac_catalog(catalog_url?, catalog_token?)` - List available datasets from the STAC catalog
166+
- `get_stac_details(dataset_id, catalog_url?, catalog_token?)` - Get S3 paths and schema for a dataset
167+
- `query(sql_query, s3_key?, s3_secret?, s3_endpoint?, s3_scope?)` - Execute DuckDB SQL against S3 parquet files
159168

160169
### Resources
161170

@@ -289,6 +298,6 @@ Contributions welcome! Key areas:
289298

290299
For issues and questions:
291300
- GitHub Issues: [boettiger-lab/mcp-data-server](https://github.com/boettiger-lab/mcp-data-server)
292-
- Dataset questions: Use the `list_datasets` tool or browse the [public STAC catalog](https://s3-west.nrp-nautilus.io/public-data/stac/catalog.json)
301+
- Dataset questions: Use the `browse_stac_catalog` tool or browse the [public STAC catalog](https://s3-west.nrp-nautilus.io/public-data/stac/catalog.json)
293302

294303

docs/.vitepress/config.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
export default {
2+
title: 'MCP Data Server',
3+
description: 'MCP server providing SQL access to large-scale geospatial datasets via DuckDB and S3.',
4+
base: '/mcp-data-server/',
5+
6+
themeConfig: {
7+
nav: [
8+
{ text: 'Guide', link: '/guide/quickstart' },
9+
{ text: 'Datasets', link: '/guide/datasets' },
10+
{ text: 'GitHub', link: 'https://github.com/boettiger-lab/mcp-data-server', target: '_blank' },
11+
],
12+
13+
sidebar: [
14+
{
15+
text: 'Getting Started',
16+
items: [
17+
{ text: 'Quick Start', link: '/guide/quickstart' },
18+
{ text: 'Available Datasets', link: '/guide/datasets' },
19+
{ text: 'Private Data Access', link: '/guide/private-data' },
20+
],
21+
},
22+
{
23+
text: 'Operations',
24+
items: [
25+
{ text: 'Deployment', link: '/guide/deployment' },
26+
{ text: 'Architecture', link: '/guide/architecture' },
27+
],
28+
},
29+
],
30+
31+
socialLinks: [
32+
{ icon: 'github', link: 'https://github.com/boettiger-lab/mcp-data-server' },
33+
],
34+
35+
footer: {
36+
message: 'Released under the MIT License.',
37+
},
38+
39+
search: {
40+
provider: 'local',
41+
},
42+
},
43+
}

docs/guide/architecture.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Architecture
2+
3+
## Runtime prompt files
4+
5+
The `.md` files in this repo are **not documentation** — they are curated prompt content loaded by `server.py` at startup and injected directly into MCP tool descriptions and prompts. The agent (LLM) reads them as instructions.
6+
7+
| File | How it is used |
8+
|---|---|
9+
| `query-setup.md` | SQL extracted and executed in every fresh DuckDB connection before a query runs |
10+
| `query-optimization.md` | Injected verbatim into the `query` tool description |
11+
| `h3-guide.md` | Injected verbatim into the `query` tool description |
12+
| `assistant-role.md` | Served as the `geospatial-analyst` MCP prompt (role and response style) |
13+
14+
Editing these files changes what the agent is told to do. They must be written for a stateless LLM — short, concrete, and unambiguous. See `AGENTS.md` for editing rules.
15+
16+
## Two-process design
17+
18+
The server is designed around two distinct agent processes. See `AGENTS.md` for the full details.
19+
20+
**Process 1 — Real-time MCP tool (small LLM)**
21+
22+
Handles user requests in real time. Has no memory between requests. Its only context is what is injected at call time from the prompt files above.
23+
24+
**Process 2 — Asynchronous evaluation (Claude)**
25+
26+
Reviews logs from real user sessions, identifies slow or incorrect queries, diagnoses root causes, and updates the small LLM's prompt files. This separation prevents misdiagnoses (e.g. blaming DuckDB internals when the real cause was a query violating a rule in `query-optimization.md`) from corrupting the prompt files.
27+
28+
## Isolation engine
29+
30+
Each query runs in a fresh `duckdb.connect(":memory:")`:
31+
32+
```python
33+
@contextmanager
34+
def get_isolated_db(...):
35+
conn = duckdb.connect(database=":memory:")
36+
try:
37+
# Run setup SQL from query-setup.md
38+
# Inject per-request S3 credentials if provided
39+
yield conn
40+
finally:
41+
conn.close()
42+
```
43+
44+
No state, credentials, or query results persist between requests.
45+
46+
## Context injection
47+
48+
Prompt content is embedded into tool descriptions so that MCP clients which don't support `prompts/list` (e.g. VS Code) still receive the guidance. The `query` tool description includes the full text of `query-optimization.md` and `h3-guide.md`.
49+
50+
## STAC catalog integration
51+
52+
Dataset discovery is handled by `stac.py`. The agent calls `browse_stac_catalog` to list available datasets, then `get_stac_details` to resolve S3 paths and column schemas. S3 paths are never hardcoded in the server or guessed by the agent.

docs/guide/datasets.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Available Datasets
2+
3+
Datasets are served from the public STAC catalog at:
4+
5+
```
6+
https://s3-west.nrp-nautilus.io/public-data/stac/catalog.json
7+
```
8+
9+
The agent discovers dataset paths and schemas dynamically — use the `browse_stac_catalog` and `get_stac_details` tools rather than hardcoding any S3 paths.
10+
11+
## Current datasets
12+
13+
| Dataset | Description |
14+
|---|---|
15+
| **GLWD** | Global Lakes and Wetlands Database |
16+
| **Vulnerable Carbon** | Conservation International carbon vulnerability data |
17+
| **NCP** | Nature Contributions to People biodiversity scores |
18+
| **Countries & Regions** | Global administrative boundaries (Overture Maps) |
19+
| **WDPA** | World Database on Protected Areas |
20+
| **Ramsar Sites** | Wetlands of International Importance |
21+
| **HydroBASINS** | Global watershed boundaries (levels 3–6) |
22+
| **iNaturalist** | Species occurrence range maps |
23+
| **Corruption Index 2024** | Transparency International data |
24+
25+
## H3 spatial indexing
26+
27+
All datasets are indexed using [Uber's H3 hexagonal grid system](https://h3geo.org).
28+
29+
| Column | Resolution | Cell area |
30+
|---|---|---|
31+
| `h8` | Resolution 8 | ~0.74 km² |
32+
| `h4` | Resolution 4 | ~1,771 km² |
33+
| `h0` | Resolution 0 | ~4,357,449 km² |
34+
35+
### Area calculations
36+
37+
```sql
38+
-- Area in km² using H3 hex counts
39+
SELECT APPROX_COUNT_DISTINCT(h8) * 0.737327598 AS area_km2
40+
FROM read_parquet('s3://...')
41+
WHERE ...
42+
```
43+
44+
### Cross-dataset joins
45+
46+
Always include `h0` in join conditions to enable partition pruning:
47+
48+
```sql
49+
SELECT a.h8, a.value, b.other_value
50+
FROM read_parquet('s3://dataset-a/**') a
51+
JOIN read_parquet('s3://dataset-b/**') b
52+
ON a.h8 = b.h8 AND a.h0 = b.h0 -- h0 required for pruning
53+
```
54+
55+
Omitting `h0` forces a full scan of both datasets and is 5–20× slower.

docs/guide/deployment.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Deployment
2+
3+
The server runs on the [NRP Nautilus](https://nrp.ai) Kubernetes cluster.
4+
5+
## Hosted endpoint
6+
7+
```
8+
https://duckdb-mcp.nrp-nautilus.io/mcp
9+
```
10+
11+
- 2 replicas, each cloning this repo at startup and running `server.py` via `uv`
12+
- 16 Gi RAM requested, up to 160 Gi / 16 CPU per pod
13+
- HAProxy ingress with CORS enabled, 10-minute query timeout, 1-hour SSE tunnel timeout
14+
15+
## Kubernetes manifests
16+
17+
```bash
18+
kubectl apply -f k8s/deployment.yaml
19+
kubectl apply -f k8s/service.yaml
20+
kubectl apply -f k8s/ingress.yaml
21+
```
22+
23+
## Redeploying after a push
24+
25+
After pushing changes to `main`, restart the pods so they re-clone the repo:
26+
27+
```bash
28+
kubectl rollout restart deployment/duckdb-mcp
29+
```
30+
31+
## Environment variables
32+
33+
| Variable | Default | Description |
34+
|---|---|---|
35+
| `STAC_CATALOG_URL` | NRP public catalog | URL of the STAC catalog to serve |
36+
| `THREADS` | 100 | DuckDB thread count (S3 workloads are I/O-bound) |
37+
| `PORT` | 8000 | HTTP server port |

docs/guide/private-data.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Private Data Access
2+
3+
The server supports private STAC catalogs and private S3 buckets. Credentials are supplied per-call and scoped to that request only — they are never logged, cached, or shared between clients.
4+
5+
## Private STAC catalog
6+
7+
Pass a bearer token alongside the catalog URL:
8+
9+
```json
10+
{
11+
"tool": "browse_stac_catalog",
12+
"arguments": {
13+
"catalog_url": "https://your-app.example.org/stac/catalog.json",
14+
"catalog_token": "YOUR_BEARER_TOKEN"
15+
}
16+
}
17+
```
18+
19+
Pass the same `catalog_url` and `catalog_token` to `get_stac_details` as well.
20+
21+
::: tip Serving a private catalog
22+
If you use oauth2-proxy for browser access, add a parallel nginx `auth_request` bypass for the `/stac/` path that accepts a static shared token via the `Authorization` header. This allows the MCP server to fetch catalog metadata without a browser OAuth session.
23+
:::
24+
25+
## Private S3 data
26+
27+
Pass S3 credentials directly to the `query` tool:
28+
29+
```json
30+
{
31+
"tool": "query",
32+
"arguments": {
33+
"sql_query": "SELECT * FROM read_parquet('s3://my-private-bucket/data/**') LIMIT 10",
34+
"s3_key": "YOUR_ACCESS_KEY_ID",
35+
"s3_secret": "YOUR_SECRET_ACCESS_KEY",
36+
"s3_endpoint": "minio.example.org"
37+
}
38+
}
39+
```
40+
41+
`s3_endpoint` defaults to `s3-west.nrp-nautilus.io` if omitted.
42+
43+
## Mixing private and public data
44+
45+
Use `s3_scope` when a query reads from both a private and the public S3 endpoint, so DuckDB routes each path to the correct endpoint:
46+
47+
```json
48+
{
49+
"tool": "query",
50+
"arguments": {
51+
"sql_query": "SELECT a.h8, b.value FROM read_parquet('s3://private-wyoming/...') a JOIN read_parquet('s3://public-data/...') b ON a.h8 = b.h8 AND a.h0 = b.h0",
52+
"s3_key": "YOUR_ACCESS_KEY_ID",
53+
"s3_secret": "YOUR_SECRET_ACCESS_KEY",
54+
"s3_endpoint": "minio.example.org",
55+
"s3_scope": "s3://private-wyoming"
56+
}
57+
}
58+
```
59+
60+
## Security properties
61+
62+
| Concern | How it is handled |
63+
|---|---|
64+
| Credential bleed between clients | Each request uses a separate `duckdb.connect(":memory:")` — secrets are connection-scoped and destroyed on close |
65+
| Credentials in server logs | `CREATE SECRET` statements are constructed internally and never written to stderr |
66+
| Credentials in transit | All traffic is TLS-terminated at the ingress |
67+
| Credential persistence | `stateless_http=True` — no session state survives between requests |
68+
69+
## Deploying private apps without a separate server
70+
71+
Private geo-agent apps can share the public MCP server endpoint and pass credentials per-call. This avoids maintaining a separate server deployment per app while ensuring all apps benefit from server improvements automatically.

0 commit comments

Comments
 (0)