datastore-mcp
README.md
# datastore-mcp
A remote MCP server that gives an AI assistant a durable, named key/value store —
backed by S3-compatible object storage, with each entry stored as an
[Open Knowledge Format](https://cloud.google.com/blog/products/data-analytics/how-the-open-knowledge-format-can-improve-data-sharing)
(OKF) document, fronted by an OAuth 2.1 authorization server.
See `datastore-mcp-brief-s3-okf.md` (the design brief this implements) for the full
rationale. This README covers what's needed to run it.
## Tools
`state_create`, `state_get`, `state_update`, `state_delete`, `state_list` — see the
brief's §4 for exact argument/behavior tables.
## Decisions made while implementing (brief §9)
The brief left six items open. This implementation resolves them as follows:
1. **S3 provider / conditional writes** — provider-agnostic; `state_create`'s
conflict check uses the universally-supported read-then-write pattern (`GET` the
metadata key first), not a conditional PUT. Works on any S3-compatible provider,
at the cost of a narrow single-user race window (brief §8).
2. **`type` field** — caller-supplied optional argument on `state_create`, defaulting
to `"mcp-state"` when omitted.
3. **Binary response size threshold** — 256 KiB (`BINARY_INLINE_THRESHOLD_BYTES` in
`src/store.ts`). Below it, `state_get` inlines binary content as base64; at or
above it, a presigned URL (1 hour TTL) is returned instead.
4. **Orphaned binary payload cleanup** — left indefinitely, matching the soft-delete
philosophy. No sweep/lifecycle job is implemented.
5. **Bucket versioning** — not enabled by this codebase; left as a bucket-level
choice for whoever provisions it. If you do enable it, pair it with a lifecycle
rule expiring noncurrent versions (brief §0) — this app's access pattern is
frequent overwrite, so unversioned storage grows unboundedly otherwise.
6. **`var_name` validation** — `^[A-Za-z0-9_-]{1,200}$` (see `assertValidVarName` in
`src/store.ts`). No separators are allowed at all, which incidentally rules out
`..`/leading-slash/empty-segment path traversal by construction rather than by
explicit denylist.
## Local development
```bash
cp .env.example .env # fill in S3 + OAuth credentials
npm install
npm run dev
```
`npm run dev` runs `src/index.ts` directly via `tsx watch`. `npm run build && npm start`
runs the compiled `dist/` output, matching what the Docker image runs.
## Configuration
See `.env.example` and `src/config.ts` for the full list of environment variables
and validation rules (brief §7). All required variables are validated eagerly at
boot; missing or malformed values fail fast with a specific error message.
## Deployment
Two-stage Docker build (`Dockerfile`), single Fly.io Machine pinned to `ams`
(`fly.toml`) — see the file for why (in-memory OAuth state, brief §5/§6).
```bash
fly launch --no-deploy # first time only, review fly.toml
fly secrets set S3_ENDPOINT=... S3_REGION=... S3_BUCKET=... \
S3_ACCESS_KEY_ID=... S3_SECRET_ACCESS_KEY=... \
OAUTH_USERNAME=... OAUTH_PASSWORD=... PUBLIC_URL=https://<app>.fly.dev
fly deploy
```
`/health` performs a live `HeadBucket` check against the configured bucket — point
Fly's health check at it (already configured in `fly.toml`).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues