Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 14 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# shop

`shop` lets agents search millions of stores, build carts, check out and pay with the shopper's Shop account, and track orders. When a store has no agent checkout, the shopper can approve a purchase on Shop and the agent gets a card to pay at the merchant's own checkout.
`shop` lets agents search millions of stores, build carts, check out and pay with the shopper's Shop account, and track orders. When a store has no agent checkout, the shopper can approve a purchase on Shop and the agent gets a one-time credential to pay at the site's own checkout.

## Contents

Expand All @@ -19,9 +19,10 @@

```sh
npm install --global @shopify/shop@latest
shop skill --install
```

This installs the `shop` command and its agent skill. Then ask your agent:
Installs the `shop` command, then teaches your agent when and how to use it. Then ask your agent:

> Find three rain jackets under US$100 that ship to the US.

Expand Down Expand Up @@ -284,15 +285,15 @@ Some stores have no agent checkout API (UCP); the agent has to enter a payment c

1. The agent describes the purchase with `shop spend-request create`.
2. The shopper reviews and approves it on Shop.
3. The agent gets a card from `shop spend-request get`, good for up to the `approval_limit` the shopper saw and approved.
4. The agent pays at the merchant's checkout and reports the outcome with `shop spend-request complete`.
3. `shop spend-request get` returns a one-time credential that fills the card fields of the site's checkout form, good for up to the `approval_limit` the shopper saw and approved.
4. The agent pays at the site's checkout and reports the outcome with `shop spend-request complete`.

`--instrument-id` is one of the cards from `shop account`. Nested values such as line items take JSON, a `@file`, or `-` for stdin:
`--instrument-id` names one of the `selectable_instruments` from `shop account`; omit it to use the shopper's preferred card. Nested values such as line items take JSON, a `@file`, or `-` for stdin:

```sh
shop spend-request create \
--business-name "Example Bookshop" --business-url https://example-bookshop.com --business-country US \
--currency USD --approval-limit 1495 --instrument-id 3f1e2d4c-5b6a-4789-8abc-def012345678 \
--currency USD --approval-limit 1495 \
--line-item @items.json \
--total '{"type":"total","amount":1495}'
```
Expand Down Expand Up @@ -320,7 +321,7 @@ shop spend-request create \

</details>

Give the shopper the `continue_url`, then wait for their approval. `--output-file` keeps the card out of the transcript: the issued instrument is written to a new file only you can read, and stdout carries the status and terms:
Give the shopper the `continue_url`, then wait for their approval. `--output-file` keeps the credential out of the transcript: the issued instrument is written to a new file only you can read, and stdout carries the status and terms:

```sh
shop spend-request get 0198e63b-1234-7abc-8def-123456789abc --wait 90 --output-file ./card.json
Expand Down Expand Up @@ -355,9 +356,9 @@ shop spend-request get 0198e63b-1234-7abc-8def-123456789abc --wait 90 --output-f

</details>

The card arrives when the status is `consumed`. Hand the file to the program that fills the checkout form; without `--output-file`, the instrument is returned inline instead.
The credential arrives when the status is `consumed`. Hand the file to the program that fills the checkout form; without `--output-file`, the instrument is returned inline instead.

After paying at the merchant's checkout, the agent reports the outcome:
After paying at the site's checkout, the agent reports the outcome:

```sh
shop spend-request complete 0198e63b-1234-7abc-8def-123456789abc \
Expand Down Expand Up @@ -399,22 +400,20 @@ To use the server without the CLI:
- Connect an MCP host to `https://mcp.shop.com/` to access the same tools. Sign-in goes through the host's OAuth flow, against the same Shop account, and the tools behave as they do in the CLI.
- A host that supports the skills extension ([SEP-2640](https://modelcontextprotocol.io/seps/2640-skills-extension)) finds the server's skill with `skills/list` on the same connection and reads its files with `resources/read`, so there is nothing to install. This package bundles the same skill.

To install the skill from this repository:
Install the skill from an installed `shop` command:

```sh
npx skills add Shopify/shop
shop skill --install
```

The npm package's install hook runs the same `skills add` on its bundled copy.

## Reference

```sh
shop --help # the command tree
shop search --help # a command's flags and examples
shop search --schema # its complete input schema
shop tools # every command with its MCP tool, usage and schema, as JSON — for agents
shop skill # the bundled agent guidance, as prose — what `npx skills add` installs
shop skill # the bundled agent guidance, as prose
```

Every command accepts `--arguments <JSON|@FILE|->` with the complete argument object from `--schema`.
Expand All @@ -433,4 +432,4 @@ Errors use the same JSON envelope, and their `next` steps say what to do:
{"ok":false,"v":1,"error":{"code":"auth_required","message":"Not signed in on this device","retryable":false,"next":["Run shop account login --wait 0 and show the shopper the verification URL and code","Run shop account login --wait 90 to finish once they approve","Retry this command after signing in"]}}
```

To install the bundled skill again without reinstalling the package, run `npx skills add "$(npm root -g)/@shopify/shop/skills/shop" -g -y`.
Run `shop skill --install` again after upgrading `shop` to replace the installed guidance.
Binary file modified lib/shop.wasm
Binary file not shown.
50 changes: 50 additions & 0 deletions lib/skill-file.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

/** Replace one bundled-skill file inside a fixed harness root or the working directory. */
export function writeSkillFile(request, { home = os.homedir(), cwd = process.cwd() } = {}) {
if (request === null || typeof request !== "object" || Array.isArray(request) ||
Object.keys(request).sort().join(",") !== "content,path" ||
typeof request.path !== "string" || !request.path || request.path.includes("\0") ||
typeof request.content !== "string") {
throw new Error("invalid skill-file request");
}
const resolved = path.resolve(cwd, request.path);
const roots = [path.join(home, ".agents", "skills"), path.join(home, ".claude", "skills"), cwd];
// The whitelist is the seam: the module may name a file, the host decides whether it is inside a
// permitted root. Both sides are compared through the filesystem (realpath of the deepest existing
// ancestor), not lexically, so a symlinked `shop/` directory under a root cannot carry the write
// elsewhere; the final component is then refused if it is itself a link.
const permitted = roots.some(root => inside(realExisting(path.resolve(root)), realExisting(resolved)));
if (!permitted) throw new Error("skill path outside permitted directories");
fs.mkdirSync(path.dirname(resolved), { recursive: true });
try {
if (fs.lstatSync(resolved).isSymbolicLink()) throw new Error("skill path is a symlink");
} catch (error) {
if (error.code !== "ENOENT") throw error;
}
fs.writeFileSync(resolved, request.content, "utf8");
return resolved;
}

function inside(root, target) {
const relative = path.relative(root, target);
return relative === "" || (!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`));
}

/** `realpath` of the deepest existing ancestor, with the not-yet-existing tail appended lexically. */
function realExisting(absolute) {
let existing = absolute;
const tail = [];
for (;;) {
try { return path.join(fs.realpathSync(existing), ...tail.reverse()); }
catch (error) {
if (error.code !== "ENOENT") throw error;
const parent = path.dirname(existing);
if (parent === existing) throw error;
tail.push(path.basename(existing));
existing = parent;
}
}
}
8 changes: 8 additions & 0 deletions lib/worker.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from "node:fs";
import { writePrivateFile } from "./private-file.mjs";
import { writeSkillFile } from "./skill-file.mjs";
import { parentPort, workerData } from "node:worker_threads";
import { WASI } from "node:wasi";

Expand Down Expand Up @@ -96,6 +97,13 @@ const shopHost = {
return stage(enc.encode(writePrivateFile(request)));
});
},
skill_write(ptr, len) {
staged = null;
return guarded(() => {
const request = JSON.parse(dec.decode(view(ptr, len, FRAME_CAP)));
return stage(enc.encode(writeSkillFile(request)));
});
},
read_result(ptr, cap) {
return guarded(() => {
if (staged === null) return -1;
Expand Down
8 changes: 2 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@shopify/shop",
"version": "1.0.2",
"version": "1.0.3",
"description": "Shop for agents: product discovery, checkout, and Shop order tracking across millions of stores",
"license": "MIT",
"repository": {
Expand All @@ -25,11 +25,7 @@
"plugins",
".claude-plugin",
".agents",
"scripts",
"README.md",
"LICENSE"
],
"scripts": {
"postinstall": "node scripts/postinstall.mjs"
}
]
}
2 changes: 1 addition & 1 deletion plugins/shop/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "shop",
"version": "1.0.2",
"version": "1.0.3",
"description": "Shop across merchants through the Universal Commerce Protocol",
"author": {
"name": "Shopify"
Expand Down
2 changes: 1 addition & 1 deletion plugins/shop/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "shop",
"version": "1.0.2",
"version": "1.0.3",
"description": "Shop across merchants through the Universal Commerce Protocol",
"author": {
"name": "Shopify"
Expand Down
2 changes: 1 addition & 1 deletion plugins/shop/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "shop",
"version": "1.0.2",
"version": "1.0.3",
"description": "Shop across merchants through the Universal Commerce Protocol",
"author": {
"name": "Shopify"
Expand Down
2 changes: 1 addition & 1 deletion plugins/shop/skills/shop/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ This is notation, not JSON. `?` means optional. Replace `RESOURCE` with `cart` o
| `update_cart`, `update_checkout` | `{shop,id,RESOURCE:full_document}` |
| `complete_checkout` | `{shop,id,checkout:{payment:{instruments:[offered_instrument_with_selected_true]}}}`. |
| `get_account` | `{}` |
| `create_spend_request` | `{spend_request:{business:{name,url,country},currency,line_items?,totals,shipping_address?,instrument_id,approval_limit,context?:{intent?}},meta?}`; for payment credential entry using a browser. |
| `create_spend_request` | `{spend_request:{business:{name,url,country},currency,line_items?,totals,shipping_address?,instrument_id?,approval_limit,context?:{intent?}},meta?}`; for payment credential entry using a browser. |
| `get_spend_request`, `cancel_spend_request` | `{id}`; cancel may add `meta`. Get may issue/replay a credential and is not read-only. |
| `update_spend_request` | `{id,spend_request:{shipping_address?,line_items?,totals?,approval_limit?},meta?}`. |
| `complete_spend_request` | `{id,result:{status:"success",order?:{id?,permalink_url?,total?,currency?}}}` or `{id,result:{status:"error",failure_code,message?}}`; present optional fields are non-null. |
Expand Down
11 changes: 5 additions & 6 deletions plugins/shop/skills/shop/references/transactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,11 @@ The buyer approves the payee, purchase terms, saved card and spending limit in S
Use `update_spend_request` with `{id,spend_request:{…},meta?}` to change the shipping address, items, totals, or spending limit. Omitted fields stay unchanged. Supplied arrays replace their previous values, and Wallet deep-merges address fields, unlike full-document cart/checkout updates. Wallet decides whether the request's current state allows the update; do not assume that buyer approval makes it editable.

1. Observe the actual payee and purchase terms.
2. Call `get_account`. When cards are returned, use the card matching nonempty `preferred_instrument_id` unless the user chose another; if an older response has cards but no preference pointer, ask the user to choose by `display.funding_instrument.description` as is when present, otherwise brand and last digits. If no cards were returned, follow `messages` and `next[]` and do not create the spend request. The preference is not checkout `selected`, an eligibility guarantee, a credential, or purchase authorization; the user still approves the payee, terms, card, and ceiling in Shop.
3. Call `create_spend_request` once with `spend_request` containing the required payee `business{name,url,country}`, `currency`, total, selected `instrument_id`, and `approval_limit` in currency minor units. Include actual line items, shipping address, and `context.intent` when available. Never invent missing terms.
4. Share the returned `continue_url` unchanged. If absent, say the spend request is waiting in Shop; never construct a review link.
5. With the CLI, run `shop spend-request get '<spend-request-id>' --wait 90`; with another binding, call `get_spend_request` again only after a successful `pending` result. After buyer approval, get returns the payment credential. This is not a read-only operation. Do not run checks concurrently. When a credential is delivered, the response includes `consumed` terms and an `instrument`.
6. Enter the issued `instrument`'s credential only into the reviewed website's checkout form. Use it exactly as returned and only for the approved purchase. Its `display` describes that issued card. Never repeat its number, expiry, CVV, or name in chat, logs, diagnostics, plans, or error text. `display.funding_instrument` is the user's saved card that pays: name that card to the user, and if the business needs to find the payment, offer the issued card's `display.last_digits`. Present either card's `description`, when present, as is; otherwise use its brand and last digits.
7. After the purchase attempt, call `complete_spend_request` with the observed `success` or `error`. Report success only from transaction evidence and include order details only if known. For an error, send the observed `failure_code` and optional `message`, preserving any unknown charge outcome. Never include a payment credential. This records your report, not payment confirmation. Follow the recovery section if report acceptance is uncertain.
2. Call `create_spend_request` once with `spend_request` containing the required payee `business{name,url,country}`, `currency`, total, and `approval_limit` in currency minor units. Omit `instrument_id` to use the account's preferred card; name one only when the user chose a different saved card (ids from `get_account.selectable_instruments[]`, shown by `display.funding_instrument.description` when present, otherwise brand and last digits). Include actual line items, shipping address, and `context.intent` when available. Never invent missing terms. The preferred card is not checkout `selected`, an eligibility guarantee, a credential, or purchase authorization; the user still approves the payee, terms, card, and ceiling in Shop.
3. Share the returned `continue_url` unchanged. If absent, say the spend request is waiting in Shop; never construct a review link.
4. With the CLI, run `shop spend-request get '<spend-request-id>' --wait 90`; with another binding, call `get_spend_request` again only after a successful `pending` result. After buyer approval, get returns the payment credential. This is not a read-only operation. Do not run checks concurrently. When a credential is delivered, the response includes `consumed` terms and an `instrument`.
5. Enter the issued `instrument`'s credential only into the reviewed website's checkout form. Use it exactly as returned and only for the approved purchase. Its `display` describes that issued card. Never repeat its number, expiry, CVV, or name in chat, logs, diagnostics, plans, or error text. `display.funding_instrument` is the user's saved card that pays: name that card to the user, and if the business needs to find the payment, offer the issued card's `display.last_digits`. Present either card's `description`, when present, as is; otherwise use its brand and last digits.
6. After the purchase attempt, call `complete_spend_request` with the observed `success` or `error`. Report success only from transaction evidence and include order details only if known. For an error, send the observed `failure_code` and optional `message`, preserving any unknown charge outcome. Never include a payment credential. This records your report, not payment confirmation. Follow the recovery section if report acceptance is uncertain.

`expires_at` is the spend request's deadline for its next step (`pending`: approve by; `approved`: retrieve the credential by), not the card's expiry. `consumed` without an `instrument` does not establish credential delivery or payment. Never create a replacement spend request merely to recover an uncertain issuance.

Expand Down
73 changes: 0 additions & 73 deletions scripts/postinstall.mjs

This file was deleted.

Loading
Loading