Skip to main content
Glama
README.md
# CodedByKay.ArtShow.Mcp

MCP server that does the whole `CodedByKay.ArtShow.CLI` workflow — add, edit,
view, and delete artwork; manage classifiers; publish `artwork.json`; check
catalog/R2 sync — from an MCP client (Claude Code, Claude Desktop, etc)
instead of the CLI's interactive TUI. Batch-capable throughout (add/update/
delete many artworks in one call) and can classify new artwork itself when
you don't hand it category/medium/tags/groups (see "Classification" below).

This is a second writer of `artwork.db` alongside `CodedByKay.ArtShow.CLI`
itself, and — new as of this rebuild — a second writer of R2 objects and of
the site's `data/artwork.json` too. See
`ArtShow.Workspace/working-on/mcp-rebuild-plan.md` for the full rationale and
`ArtShow.Workspace/AGENTS.md` for how this fits the rest of the workspace.
Both this server and the CLI talk to the same SQLite file in WAL mode, which
supports that safely (busy_timeout + a SAVEPOINT per batch item here).

Uses Node's built-in `node:sqlite` (no native DB dependency to build), plus
`sharp` for imaging and `@aws-sdk/client-s3` for R2 — both installed from
prebuilt binaries, no native build step. Requires Node.js 22.5+.

## Setup

```
npm install
npm run build
```

R2 credentials aren't configured separately — this server reads the exact
same sources the CLI does (see "Configuration" below), so if `dotnet run` in
`CodedByKay.ArtShow.CLI` already works, this does too.

## Tools

### Read / inspect
- `list_artworks` — filter (ids, type, category, medium, tag, group, text
  search, date range, missing-R2-key) + sort + paginate.
- `get_artwork` — fetch one or more artworks by id.
- `get_status` — no network: catalog counts, classifier breakdown, the
  pending-publish flag, and the resolved config (paths, R2 bucket, and where
  credentials were found or that they're missing). Good first call.
- `check_sync` — cross-references the catalog against the R2 bucket: keys
  referenced but missing, bucket objects orphaned (unreferenced), total
  bytes. `verifyUrls: true` also HEAD-checks every public URL.
- `verify_catalog` — DB-only consistency sweep the CLI has no equivalent of:
  missing R2 keys/video ids, invalid dates, empty titles, duplicate R2 keys,
  classifier values used by an artwork but not in the `Classifiers` table.

### Write — artworks
- `add_artworks` — **the centerpiece.** Add one or more artworks (images
  and/or YouTube videos, mixed freely) in a single call. Per image: EXIF-
  orients, encodes a full-res WebP (q90) + 600w WebP thumbnail (q75),
  allocates collision-free R2 keys, uploads both, inserts the row, marks the
  catalog dirty, HEAD-verifies both public URLs, then deletes the local
  source file (`deleteSourceAfterUpload`, default **true** — only deletes
  after both URLs verify live). Every item succeeds or fails independently.
  `dryRun: true` validates/encodes/resolves classifiers without uploading or
  writing anything. See "Classification" below for `onMissingClassifiers`
  and `classifierMode`.
- `update_artworks` — batch partial update by id; nullable fields
  (`description`/`createdDate`/`category`/`medium`/`r2Key`/`thumbR2Key`/
  `videoId`) explicitly settable to `null` to clear them. Doesn't touch R2 or
  re-encode images.
- `delete_artworks` — batch delete, **including R2 objects** (the CLI's real
  "Delete artwork" behavior) unless `keepFiles: true`. Requires
  `confirm: true`.
- `replace_artwork_image` — point an existing image row at a new local file:
  uploads new R2 objects, updates the row, verifies, then removes the old
  R2 objects. The one workflow the CLI has no direct answer for short of
  delete-and-re-add.
- `relocate_artwork_media` — batch-move an image row's R2 objects to their
  canonical `originals/<year>/<slug>.webp` / `thumbs/<year>/<slug>-600w.webp`
  path (year from `CreatedDate`, falling back to `AddedDate`) via a
  server-side R2 copy — no download, no re-encoding, bytes untouched. For
  when `CreatedDate` was corrected after upload and left the object under
  the wrong year folder. A row already at its canonical keys is left alone
  and reported `unchanged`; a manual copy done ahead of time (new key
  already present, old key already gone) is tolerated and still updates the
  row. Every id succeeds or fails independently.

### Classifiers
- `list_classifiers` — the vocabulary (Category/Medium/Tag/Group), each with
  a usage count and example titles. Pass `match: string[]` to fuzzy-match
  candidate values against it (surfaces "watercolour" → "watercolor" instead
  of letting a near-duplicate spelling become a second classifier).
- `create_classifiers` — batch add known values without attaching them to
  any artwork.
- `rename_classifier` — cascades into every artwork using it. `merge: true`
  folds a rename into an already-existing classifier of the same name
  instead of erroring — the real fix for a near-duplicate.
- `delete_classifier` — reports usage count; `force: true` to unlink from
  artworks and delete anyway.

### Publish
- `publish` — writes `data/artwork.json` **byte-identically** to what the
  CLI's `System.Text.Json` writer produces (see "Publish parity" below),
  clears the pending-publish flag, and returns a diff (ids added/removed,
  and which fields changed on modified ids). Metadata only — does **not**
  commit or push; review the diff, then commit + push the site repo
  yourself (Netlify auto-deploys on push). `dryRun: true` computes the diff
  without writing.
  Also writes `data/collections.json` (collection blurb/cover/order, see
  `set_collection_details`).
- `set_publish_state` / `get_publish_state` — read/manually set the
  pending-publish flag.

### Maintenance
- `prune_orphaned_r2_objects` — deletes bucket objects no catalog row
  references (originals, thumbs **and display** files all count as
  referenced). Dry-run by default; `confirm: true` to execute.
- `backfill_image_metadata` — one-off catch-up for rows added before the
  2026-10 data-contract change: records each image's pixel size and uploads the
  2000px `display/` WebP where the original is larger. Dry-run by default;
  `confirm: true` processes up to `limit` rows per call. Idempotent.
- `set_collection_details` (under Classifiers) — a Group's blurb, cover
  artwork and display order, published to `data/collections.json`.

Also: an MCP **resource** `artshow://classifiers` (the same vocabulary
`list_classifiers` returns, without a tool call) and an MCP **prompt**
`add-new-drawings` that walks through classifying-and-adding every image in
a staging folder end to end.

## Classification

This server can't see an image — classification is the calling agent's job.
Three things make that consistent instead of ad hoc:

1. **`list_classifiers` is the vocabulary**, with usage counts and example
   titles, so an agent picks an established value over inventing a new one.
2. **`onMissingClassifiers` on `add_artworks`** (default **`"error"`**): an
   item with no category/medium/tags/groups at all is rejected, and the
   error carries the *entire* vocabulary — so an agent that hits it can look
   at the image and retry immediately instead of a second round trip.
   `"warn"` inserts anyway with a flag on the result; `"allow"` is silent
   (the pre-rebuild behavior).
3. **`classifierMode`**: `"lenient"` (default, matches the CLI's own
   auto-add-on-first-use behavior) accepts a new value and creates it,
   reporting it in the result as `classifiersCreated`. `"strict"` rejects a
   value not already in the vocabulary — if something close exists
   ("watercolour" vs "watercolor") the error suggests it by name; if
   nothing does, it says so and points at `list_classifiers`.

## Publish parity

`CodedByKay.ArtShow.CLI` writes `data/artwork.json` via
`JsonSerializer.Serialize(items, new JsonSerializerOptions { WriteIndented = true })`.
For this server's `publish` to be a safe drop-in replacement (rather than
having every alternate publish between the CLI and this server rewrite the
whole file), `src/publishing/dotnet-json-encoder.ts` reimplements that
writer's exact output byte-for-byte: CRLF line endings, 2-space indent, no
trailing newline, comma-before-newline placement, empty arrays/objects
collapsed to `[]`/`{}`, and `JavaScriptEncoder.Default`'s escaping (`"` as
`\u0022` — **not** `\"` —, `&`, `'`, `<`, `>`, and everything above `0x7E`
escaped as uppercase `\uXXXX`). The `"` rule was wrong until 2026-10 and was
found by diffing against the CLI's real output; `ArtShow.CLI --publish` makes
that comparison scriptable.
Verified with a byte-for-byte round-trip test against the live file — see
`src/test/dotnet-json-encoder.test.ts`.

## Configuration

Reads the **same sources `CodedByKay.ArtShow.CLI` does** (mirroring
`Program.cs`'s `ConfigurationBuilder` precedence — env vars win, then
dotnet user-secrets, then `appsettings.json`), so no separate credential
setup is needed if the CLI already works:

| Source | What it provides |
|---|---|
| `<CLI root>/appsettings.json` | `Catalog:DbPath`, `Publish:SiteRepoPath`, `R2:Bucket`/`AccountId`/`PublicBaseUrl` (and `R2:AccessKeyId`/`SecretAccessKey`, normally left empty here on purpose) |
| `%APPDATA%\Microsoft\UserSecrets\codedbykay-artshow-cli\secrets.json` | `R2:AccessKeyId` / `R2:SecretAccessKey` — read BOM-safe (dotnet writes this file with a UTF-8 BOM) |
| Environment variables | Override anything above — see below |

Env var overrides (all optional):

| Variable | Overrides |
|---|---|
| `ARTSHOW_CLI_ROOT` | Path to `CodedByKay.ArtShow.CLI` (default `C:\Users\kaywi\dev\CodedByKay.ArtShow.CLI`) — where `appsettings.json` is read from |
| `ARTSHOW_DB_PATH` | Full path to `artwork.db` (default: `<CLI root>` + `Catalog:DbPath`) |
| `ARTSHOW_SITE_REPO_PATH` | Site repo root (default `C:\Users\kaywi\dev\CodedByKay.ArtShow`) — `publish` writes to `<this>/data/artwork.json` |
| `ARTSHOW_STAGING_FOLDER` | Default `folder` for the `add-new-drawings` prompt |
| `ARTSHOW_R2_BUCKET`, `ARTSHOW_R2_ACCOUNT_ID`, `ARTSHOW_R2_PUBLIC_BASE_URL` | R2 bucket config |
| `ARTSHOW_R2_ACCESS_KEY_ID`, `ARTSHOW_R2_SECRET_ACCESS_KEY` | R2 credentials |

Call `get_status` to see exactly what was resolved and where each value
came from (including whether credentials were found and in which file).

## Safety

- Every destructive tool (`delete_artworks`, `prune_orphaned_r2_objects`)
  requires `confirm: true`; both plus `add_artworks`/`replace_artwork_image`
  support `dryRun`.
- A local source file is **never** deleted before both its R2 public URLs
  HEAD 200.
- If a catalog insert fails after an R2 upload already succeeded, the
  server best-effort deletes the just-uploaded objects rather than leaving
  them orphaned.
- This server never runs EF Core migrations and never creates tables — a
  schema mismatch at startup fails loudly and tells you to run the CLI once
  instead of silently improvising.
- Each batch item (in `add_artworks`/`update_artworks`) runs in its own
  SQLite `SAVEPOINT`, so one bad item can't corrupt the catalog or block the
  rest of the batch.

## Registering with a client

```json
{
  "mcpServers": {
    "artshow-catalog": {
      "command": "node",
      "args": ["C:\\Users\\kaywi\\dev\\CodedByKay.ArtShow.Mcp\\dist\\index.js"]
    }
  }
}
```

Or with the Claude Code CLI:

```
claude mcp add artshow-catalog -- node C:\Users\kaywi\dev\CodedByKay.ArtShow.Mcp\dist\index.js
```

## Development

```
npm run dev      # tsc --watch
npm test         # build, then run src/test/*.test.ts under node --test
```

Several tests read the **live** `artwork.db` and site `artwork.json`
read-only (never mutate) to check real-world parity — they skip gracefully
if those paths don't exist on the machine running them.

## Data contract (2026-10)

`artwork.json` items carry `width`, `height`, `displayUrl`, `datePrecision` and
`media[]` after the legacy fields, and `data/collections.json` is written
alongside it. Both writers (this one and the CLI's) produce byte-identical
output for both files — `src/publishing/derive.ts` holds the derivations that
must match `CodedByKay.ArtShow.CLI/Publishing/MediaDerivation.cs`. New image
adds record size and upload a `display/<year>/<slug>-2000w.webp` when the
original's long edge is over 2000px (`media/imaging.ts` `createDisplay`).
`src/schema-guard.ts` now also requires the CLI's new columns, so run the CLI
once (or `dotnet ef database update`) before starting a build of this server
against an older DB.