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.
This commit is contained in:
86
tools/mcp/README.md
Normal file
86
tools/mcp/README.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user