v0.7.0 — Fulfillment tools, API version validation, ShopifyQL throttling
What's New
Fulfillment is now built in — 3 tools, 59 → 62
get_fulfillment_orders, create_fulfillment and update_fulfillment_tracking shipped only as
custom-tools/*.yaml examples. They resolved just for consumers who copied that YAML into their own
custom_paths — everyone else got "tool not found", and there was no built-in way to mark an order
fulfilled or attach tracking. They are now real tools in src/shopify/tools/orders/.
get_fulfillment_orders— fulfillment orders for an order, with assigned location and line itemscreate_fulfillment— fulfil an order, optionally with tracking and customer notificationupdate_fulfillment_tracking— attach or replace tracking info on an existing fulfillment
API version validation at startup
Shopify serves an unrecognised api_version with its oldest supported schema and an HTTP 200 —
never an error. A typo or an aged-out version therefore survived for months and then broke with no
config change on your side.
api_version is now checked against the known quarterly support windows at boot. Malformed or
aged-out fails startup with a message naming the cause; nearing end of support, or newer than this
build's table, boots with a warning. COB_SHOPIFY_ALLOW_UNSUPPORTED_API_VERSION=true overrides the
hard failure.
ShopifyQL rate limiting is now understood and handled
Analytics requests are billed against extensions.shopifyqlCost — a second allowance, separate from
the familiar extensions.cost leaky bucket, with no restore rate, that resets in full at a
window boundary (observed: one minute). It was not tracked at all, and the resulting throttle
arrives as a THROTTLED GraphQL error inside an HTTP 200, raised outside the retry wrapper — so
it could never be retried and surfaced as a bare "Rate limited. Please retry later."
The client now waits exactly until the window resets (default up to 10s,
rateLimit.maxShopifyQLWaitMs), and past that fails with a message naming the reset time and why an
immediate retry cannot help. It also warns when the allowance drops below 10%. Cost scales steeply
with the range scanned — one wide grouped query measured 259 points of 1000.
Observability
loadYamlToolsDetailed() reports per-path load counts and skipped paths. The startup log line now
carries builtInTools, customTools, resolved customToolPaths with per-path counts,
customToolPathsSkipped with reasons, and apiVersion.
Security
All 27 open dependency advisories resolved — pnpm audit clean at every severity, prod and dev.
| Package | From | To |
|---|---|---|
| hono | 4.12.22 | 4.13.4 |
| @hono/node-server | 1.19.13 | 1.19.17 |
| fast-uri | 3.1.2 | 3.1.6 |
| ip-address | 10.2.0 | 10.5.0 |
| postcss | 8.5.15 | 8.5.26 |
| vite | 8.0.8 | 8.2.2 |
| body-parser | 2.2.2 | 2.3.0 |
| esbuild | 0.27.4 | 0.28.2 |
| nanoid | 3.3.12 | 3.3.18 |
Why these kept coming back. The pnpm.overrides floors were not actually pinning anything. Each
had been written as >=<the version current when it was added>, and the already-locked version kept
satisfying that range — so pnpm install never re-resolved. Every package sat at exactly its
floor (hono@4.12.22 under >=4.12.22), and the override looked like a patched pin while silently
absorbing each new advisory against the same package. That is why hono, fast-uri, ip-address
and postcss were re-flagged after both 0.6.5 and 0.6.6.
The floors are now caret ranges. Carets invalidate the stale lock entry and cap the major — a
plain >= would now pull fast-uri 4.x and @hono/node-server 2.x in underneath the MCP SDK.
Bug Fixes
cob-shopify --versionreported0.6.0at package version0.6.6.VERSIONwas a
hand-maintained constant, and the test guarding it asserted that same literal — restating the
constant instead of checking it, so the two drifted apart across six releases. It now compares
againstpackage.json.pnpm lintfailed on Windows for files you never touched. Sources are stored LF, but
core.autocrlf=true(the Git for Windows default) rewrote them to CRLF on checkout, and Biome's
formatter requires LF. Added.gitattributes(* text=auto eol=lf). Lint also no longer
format-checks git-ignored paths.- Concurrent JSON storage writes dropped tokens. Writes were read-modify-write with no
serialization, so overlapping token refreshes — ordinary for parallel tool calls — lost all but
the last. The "atomic write" also used a fixed<file>.tmp, so two writers raced the same path
and failed with ENOENT (or EPERM on Windows). Writes are now serialized in-process with unique
temp names, and transient Windows rename errors are retried. refund_rate_summaryworks on both sides of Shopify's "sales reversals" rename.pnpm test:integrationnow runs serially. It fanned out across ~20 workers, each with its own
rate limiter and no shared view of the per-store ShopifyQL allowance, so the suite throttled
itself and reported budget exhaustion as tool failures.ToolDefinition.cliActionoverrides the derived CLI action name. Mechanical domain-word
stripping turnedget_fulfillment_ordersintoorders get-fulfillment, losing the subject and
colliding visually withget-fulfillment-status. It is noworders get-fulfillment-orders.
Numbers
- 62 built-in tools across 5 domains — Products (15), Orders (15), Customers (9),
Inventory (7), Analytics (16) - 663 tests passing
- 0 open dependency advisories
Full Changelog: v0.6.6...v0.7.0