Files
shopdb-flask/tools/mcp/README.md
cproudlock 762d1b7210 Make the API usable by an agent, and stop the contract going stale
Someone writing a script against ShopDB had two options: read a 705 KB spec, or
guess. The spec is the reference, not an index, and handing an agent the whole
thing means either truncating it or spending a context window on 479 operations
nobody asked about.

Three additions, each feeding the next.

/api/docs/operations.txt is the index: every operation on one line with its
method, path, tag, auth and summary, at around 70 KB for all 486. A caller can
hold the entire API at once, find the four operations it needs, and only then
fetch their schemas.

/api/docs/openapi.json?tag=... narrows the spec to one of the 41 tag groups.
Components are kept intact deliberately: pruning the schema graph to match would
leave references resolving to nothing, and a generator that fails confusingly is
worse than a file that is larger than strictly necessary.

tools/mcp/ is an MCP server built on both. Five tools rather than 483, because a
tool per operation is unusable - clients degrade past a few dozen and models
start picking the wrong one. It reads everything from the live instance at call
time, so enabling a plugin adds operations it can already reach with no change
here. The token is the security boundary and the README says so plainly: every
operation the credential permits is reachable, so exploratory work wants a
read-scoped token.

The rest of this is about the contract not drifting, which is what makes any of
the above trustworthy.

Two CI jobs:

- api-contract runs the parity gates and then regenerates the spec and fails if
  anything changed. Those gates already existed and already worked - they caught
  both routes added here, unprompted - but nothing ran them except by hand, and
  the committed spec had silently drifted by three operations, including two
  setup endpoints added the day before.
- migrations-mysql is the job the header comment has been claiming for some
  time while not existing. It described "the REAL multi-site deploy path" and
  "what catches an Alembic regression on MySQL before it ships", and there was
  no such job. That is worse than no comment: it is the false confidence that
  let a migration ship which could not build a schema on a fresh MySQL database.
  The suite runs on SQLite, and SQLite accepts a foreign key onto a table that
  does not exist.

CONTRIBUTING.md now says to run gen_openapi.py --refresh after changing a route,
and that a route the sweep cannot classify belongs in PUBLIC_ENDPOINTS with a
reason rather than hand-edited into the inventory, which the next refresh would
overwrite. The new index route is declared there alongside the spec it indexes.
2026-09-20 12:59:49 -04:00

87 lines
3.5 KiB
Markdown

# ShopDB MCP server
Exposes a ShopDB instance to an LLM client (Claude Code, Claude Desktop, or any
other MCP client) so it can discover and call the API directly, instead of
someone generating a client by hand from the spec.
## The token is the security boundary
**Read this before pointing it at production.**
Every operation the credential is allowed to perform is reachable through this
server, including writes and deletes. There is no separate allow-list here: the
scope of the personal access token decides what can happen, and nothing else
does.
That is a deliberate choice, and it puts the decision where it can be enforced
server-side rather than in a client that a model is talking to. It also means:
- For anything exploratory, create a **read-scoped token**. A read-scoped token
cannot be talked into a write, whatever the model decides to call.
- Give it the narrowest scopes that cover the task, not your admin session.
- `DELETE` against an asset database is irreversible. The tool descriptions say
so, which makes a model markedly more careful, but a token that permits it is
the only thing standing in the way.
Create a token under Settings > API Tokens. `shopdb_whoami` reports what the
token can do; call it first.
## Install
pip install -e tools/mcp
Then register it with your client. For Claude Code:
claude mcp add shopdb \
--env SHOPDB_URL=https://shopdb.example.net/shopdb \
--env SHOPDB_TOKEN=<token> \
-- shopdb-mcp
`SHOPDB_URL` must include the mount path if the site is served under one
(`/shopdb`), because that is what the instance's own spec declares.
| Variable | Required | Meaning |
|---|---|---|
| `SHOPDB_URL` | yes | Base URL including any mount path |
| `SHOPDB_TOKEN` | yes | Personal access token |
| `SHOPDB_VERIFY_TLS` | no | `0` to accept a self-signed certificate. Defaults to verifying |
## Tools
| Tool | Purpose |
|---|---|
| `shopdb_whoami` | Instance, identity, scopes, operation count. Call first |
| `shopdb_search_operations` | Find operations by keyword or tag. Start here |
| `shopdb_describe_operation` | Parameters, request body and responses for one operation |
| `shopdb_call` | Call any operation |
| `shopdb_llms_guide` | The instance's own orientation guide |
## Why five tools and not 483
ShopDB publishes 483 operations. One tool each would be unusable: MCP clients
degrade badly past a few dozen tools and models start choosing wrong ones. So
the surface is small and generic, and the instance's own published index is what
makes it navigable - `search` reads `/api/docs/operations.txt` (about 70 KB for
every operation) and `describe` fetches only the tag group it needs rather than
the 700 KB spec.
Nothing is generated ahead of time. Enabling a plugin adds operations this
server can already reach, with no change here and no reinstall.
## Staying current
The server reads everything from the live instance, so it describes whatever
that instance actually serves. The staleness risk is upstream: `docs/openapi.json`
is generated and committed, and if it drifts, the instance serves a stale spec
and this server faithfully reports stale information.
That is guarded by the `api-contract` CI job and by
`python scripts/gen_openapi.py --refresh`. See CONTRIBUTING.md.
## Limitations
- Air-gapped sites need `mcp` vendored; it is not bundled here.
- No streaming or subscriptions - every tool is a request and a response.
- Responses over 100 KB are truncated with a note. Paginate instead
(`perpage` is silently capped at 100 by the API).