Files
shopdb-flask/tools/mcp
cproudlock a97346ed81 Correct the CI claims and the agent tooling's honesty about itself
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".
2026-09-20 14:49:30 -04:00
..

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