Skip to main content
Glama
ulrich-johannes

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`).