I asserted that the MySQL migration job "was a comment, not a job" and that
nothing had ever run a fresh install on MySQL. Both wrong. The job existed, hit
this org's ARC runners, and was deliberately moved to internal CI in ffe9fa1cc
because Kubernetes runners without docker-in-docker cannot host service
containers - a fact written at the bottom of the very file I was editing. The
job I added would have failed at container setup on its first push.
Removed it. The leftover was the HEADER, which listed a job that had moved, and
that is now corrected in place with a note not to re-add the entry without
re-adding a job to a runner that can host it. Coverage stays on internal CI.
The api-contract job was green by accident. gen_openapi.py refuses to run while
a bundled plugin is disabled, instance/plugins.json is gitignored, and the step
only worked because the earlier pytest step writes that file at import time and
leaves it. Reordering the steps would have produced a failure about disabled
plugins that had nothing to do with the spec. It now seeds its own registry.
Confirmed against a clean archive of HEAD, where the generator does refuse.
shopdb_whoami claimed to report "what the token is scoped to" and reported the
ACCOUNT's permissions. A read-scoped token on an admin account printed
roles=[admin] and the full permission count, so a model would try a write, take
a 403, and have no tool that explained it. The field is now account_permissions
and carries a note saying a scoped token may allow fewer. Fixing this properly
means /api/auth/me publishing the patscopes claim, which is a separate change.
The MCP client caught only urllib's two error types, so an ordinary mistake
escaped as a raw exception with no cause reaching the caller: a space in a path
raises InvalidURL, a non-ASCII path raises UnicodeEncodeError, a slow read
raises TimeoutError. Paths are now percent-encoded and those escapes are
contained; both cases return the API's own 404 envelope instead.
The operation index collapsed optional authentication. Fourteen routes work
signed out and return more when authenticated, and the index printed them as
requiring a bearer token. They now read bearerAuth,optional.
Also: the generator's own hint told the reader to run "flask plugins enable";
the CLI group is "plugin".
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.
DELETEagainst 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
mcpvendored; 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
(
perpageis silently capped at 100 by the API).