Build a Shopify checkout MCP App
Query a live Shopify Storefront API catalog, keep a local widget cart, create one Shopify cart at checkout, and hand the buyer to Shopify securely.
This guide builds a Noodle Seed MCP App that lets a buyer browse live Shopify products, choose variants, keep a non-secret cart in the widget, and continue to Shopify's hosted checkout. Products come directly from the Storefront GraphQL API. There is no product database or synchronization job.
The checkout design is intentionally simple: the widget does not create a Shopify cart while the buyer is
browsing. When the buyer selects Continue to checkout, one app-only tool calls Shopify's cartCreate
mutation with the current merchandise IDs and quantities. Shopify returns the final checkoutUrl, and the
host opens that exact allowlisted URL.
What this guide supports
Live product search, variants, manual GraphQL cursor pagination, a widget-local cart, an optional cart note,
one cartCreate, Shopify-authoritative totals and availability, and hosted checkout handoff.
Architecture
This boundary matters. The widget never receives the Storefront access token, never calls Shopify directly, and never receives Shopify's cart ID. Noodle resolves the token server-side through the credential broker.
Before you start
You need:
- A Shopify store where you can install sales channels and manage Storefront API permissions.
- At least one active product with a purchasable variant published to the Headless sales channel.
- Node.js 24 LTS or newer.
- The Noodle Seed CLI and a Noodle account for hosted deployment. Local development does not require an account.
- The store's canonical
https://<shop>.myshopify.comorigin. Do not use the admin URL.
The finished implementation is the Shopify Storefront example. Use it as the source of truth while following this guide.
1. Create a Shopify Headless storefront
In Shopify Admin:
- Open Apps and sales channels and install the Headless sales channel.
- Open Headless, then select Add storefront or Create storefront.
- Give the storefront a name that identifies this MCP App and environment, such as
MCP checkout - dev. - Open Storefront API permissions and grant only
unauthenticated_read_product_listings,unauthenticated_read_checkouts, andunauthenticated_write_checkoutsfor this example. - Publish the products you want buyers to see to the Headless sales channel.
Shopify creates public and private Storefront access tokens for the storefront. Copy the public Storefront access token for this guide.
Use the public token for this example
The example sends X-Shopify-Storefront-Access-Token, which is Shopify's public-token header. Do not paste a
private token into this configuration. Shopify private server-side access uses a different header and, for
buyer-initiated traffic, requires the buyer IP header. This example does not forward buyer IP.
Shopify classifies the public token as safe for client-side use. The example still stores it as a Noodle managed secret so it is not committed to source, copied into widget payloads, or mixed with app configuration. Because public-access capacity normally scales by buyer IP, load-test this server-side MVP against expected traffic before opening a high-volume storefront.
Official Shopify setup references:
- Getting started with the Storefront API
- Storefront API authentication
- Shopify API access scopes
- Querying products and collections
2. Start the Noodle project
Create a widget project and install its dependencies:
noodle init shopify-checkout --template widget
cd shopify-checkout
npm installCopy the files from the complete example into the matching project paths:
src/
helpers.ts
server.ts
shopify-config.ts
shopify-responses.ts
views/
storefront-checkout.css
storefront-checkout.tsx
test/
server.test.ts
storefront-checkout.test.tsxThe example uses idiomatic TypeScript authoring. Do not copy or hand-edit compiled manifest or connector artifact files.
3. Point the example at your store
Edit src/shopify-config.ts:
export const SHOPIFY_STORE_ORIGIN = 'https://your-shop.myshopify.com';
export const SHOPIFY_STOREFRONT_API_VERSION = '2026-07';Use the exact HTTPS origin with no path and no trailing slash. The same constant drives:
- The connector
baseUrl. - The connector SSRF
allowedOriginslist. - The app's checkout
handoff.allowedDomainslist. - The widget's final checkout URL validation.
The example pins Shopify Storefront API 2026-07 so a deploy does not change behavior unexpectedly. Shopify
releases stable API versions quarterly. Review the Storefront changelog and update the pinned version and
tests deliberately rather than switching to an unversioned URL.
If your product images come from another origin, add only that exact origin to the widget's
csp.resourceDomains. The default example allows https://cdn.shopify.com. Its
csp.connectDomains remains empty because the widget makes no direct network requests.
4. Understand the Shopify connector
The server declares one HTTP connector with brokered API-key authentication:
const shopifyStorefront = connector('shopify_storefront')
.version('1.0.0')
.http({
baseUrl: SHOPIFY_STORE_ORIGIN,
allowedOrigins: [SHOPIFY_STORE_ORIGIN],
auth: {
kind: 'apiKey',
header: 'X-Shopify-Storefront-Access-Token',
secret: secret('SHOPIFY_STOREFRONT_ACCESS_TOKEN'),
},
operations: {
search_products: { /* POST a products GraphQL query */ },
create_cart: { /* POST one cartCreate mutation */ },
},
});Both operations POST to /api/2026-07/graphql.json. They return the raw GraphQL envelope to a sandboxed
compute connector, which emits only the typed fields the tools need.
Product query
search_products sends a bounded Shopify products query. It returns at most 20 products and at most 20
variants per product. Each variant includes the merchandise ID required by cartCreate, availability,
selected options, and price.
The cursor belongs inside the GraphQL variables object:
variables: {
query: '${args.query}',
first: '${args.first}',
after: '${args.after}',
}Do not enable automatic HTTP query-string pagination for this operation. Shopify's endCursor must be
passed back as the GraphQL after variable in the next request body.
Checkout mutation
create_cart sends only the local cart lines and optional note:
mutation CreateCheckout($input: CartInput!) {
cartCreate(input: $input) {
cart {
checkoutUrl
cost {
subtotalAmount { amount currencyCode }
totalAmount { amount currencyCode }
}
}
userErrors { code field message }
warnings { code message target }
}
}Notice that the selection does not request cart.id. A Shopify cart ID contains a key that must be treated
as a secret. This app does not need it because it creates the cart once and immediately hands the buyer to
Shopify checkout.
Shopify's cartCreate reference
defines the input, checkoutUrl, userErrors, and warnings used here.
5. Understand the tool surface
The server exposes one model-visible entry tool and two widget-only helpers:
| Tool | Visibility | Responsibility |
|---|---|---|
open_storefront | Model and app | Query the first live product page and render the storefront widget. |
search_products | App only | Run a new product search or fetch the next GraphQL cursor page. |
create_checkout | App only | Create one Shopify cart from local lines and return the checkout URL. |
The app-only tools use visibility: ['app']. They remain callable from the widget through the same runtime
authentication, policy, and connector boundary, but hosts do not ask the model to orchestrate low-level cart
steps.
Every list is bounded, every tool has a human-readable title and annotations, and tool output is normalized
instead of forwarding raw Shopify payloads. See Designing tools for agents for
the rules enforced by noodle check.
6. Understand the local cart
The React widget stores the cart with useViewState:
type CartLine = {
merchandiseId: string;
productTitle: string;
variantTitle: string;
price: { amount: string; currencyCode: string };
quantity: number;
};
const [cart, setCart] = useViewState<readonly CartLine[]>('shopify_local_cart', []);This state is a buyer-interface convenience, not a Shopify session. It may contain only non-secret display snapshots and the merchandise IDs already returned by product search. Do not put access tokens, checkout URLs, Shopify cart IDs, customer credentials, or payment data in view state.
The widget's estimated subtotal helps the buyer review selections. It is not authoritative. When checkout starts, Shopify revalidates merchandise, quantity, stock, contextual price, discounts, tax, and shipping. The widget displays Shopify's mutation errors or warnings and opens the checkout URL only after a successful normalized result.
7. Configure the access token
For local development, put the token in a project-root .env file that is excluded from version control:
SHOPIFY_STOREFRONT_ACCESS_TOKEN=replace-with-your-public-storefront-tokenConfirm .env and .env.noodle are ignored before saving the value. Never put the token in server.ts, a
test fixture, a widget property, a tool result, or a command-line argument.
For the hosted target, link the project and enter the token through the managed-secret prompt:
noodle login
noodle link --org <org> --app shopify-checkout --env prod
noodle secrets set SHOPIFY_STOREFRONT_ACCESS_TOKEN --scope envNoodle resolves secret('SHOPIFY_STOREFRONT_ACCESS_TOKEN') for the linked organization, app, and environment
at call time. The compiled artifact contains only the reference name.
8. Test locally
Run the deterministic tests and static checks first:
npm test
noodle validate
noodle check --min-severity warn
noodle check --target chatgptThen start the local runtime and Devtools:
noodle devOpen the Devtools URL printed by the command. Call open_storefront with a Shopify search string:
{
"query": "boots",
"first": 12
}Verify all of the following before deploying:
- Products and variants match the live Shopify catalog.
- An unpublished product does not appear.
- Searching replaces the result set and Load more uses the next GraphQL cursor.
- Unavailable variants cannot be added.
- Adding, removing, and changing quantity do not make a Shopify cart request.
- Continue to checkout makes exactly one
create_checkouttool call. - Shopify errors prevent handoff and remain understandable.
- A successful call opens an HTTPS URL on the exact configured Shopify origin.
- Tool results, logs, view state, and widget payloads contain no Shopify cart ID or token.
9. Deploy safely
Start with owner-only access while you run the live-store smoke test:
noodle deploy --access owner-only
noodle openRepeat the local verification against the deployed endpoint. Confirm the hosted target has the correct environment-scoped secret and that Shopify attributes the checkout to the intended Headless channel.
Before changing the deployment to public consumer access, review expected traffic, Storefront API rate behavior, abuse controls, product visibility, fulfillment rules, returns, privacy disclosures, and the host's app-directory requirements. Checkout still happens on Shopify, so the MCP App must never collect card or payment details.
Error handling you must keep
Shopify GraphQL can return HTTP 200 with a top-level errors array. A successful HTTP status is not enough.
The example normalizer also checks:
- Missing or malformed product data.
cartCreate.userErrorssuch as an invalid or unavailable merchandise line.cartCreate.warningssuch as insufficient stock or a reduced quantity.- A missing or invalid
checkoutUrl. - A checkout URL whose origin differs from the configured store.
Keep these checks if you customize the output. Do not render identifier-dependent actions from a pending, errored, or malformed initial tool result.
Troubleshooting
| Symptom | Check |
|---|---|
| Shopify returns 401 or an authentication error | Confirm you copied the public Storefront token and use X-Shopify-Storefront-Access-Token. Re-enter the managed secret instead of logging it. |
| Product list is empty | Publish the products and variants to the Headless sales channel and confirm they are active. |
| GraphQL says access is denied | Review the storefront's Storefront API permissions and make sure the query has not been expanded beyond them. |
| Search works but images are blocked | Add the image CDN's exact HTTPS origin to csp.resourceDomains; do not add a wildcard. |
| Checkout returns a user error | Show the normalized code/message, keep the buyer in the local cart, and let them change or remove the affected variant. |
| Checkout URL is rejected | Confirm SHOPIFY_STORE_ORIGIN is the canonical myshopify.com origin returned by checkout. |
| Totals differ from the widget estimate | Use Shopify's mutation/checkout total. The local total is only a display estimate. |
| Buyers lose the cart after reopening the app | Expected for this one-shot design. The cart is widget-local and no Shopify cart session is resumed. |
Migrating from a synchronized storefront app
When replacing an older app that synchronized products into its own database, remove the synchronization assumptions instead of rebuilding them in Noodle:
| Older pattern | This guide |
|---|---|
| Product records copied into an app database | Live products query on every browse/search request. |
| Database product ID | Shopify product GID for display identity. |
| Database variant/SKU record | Shopify variant GID as merchandiseId. |
| Search index cursor | Shopify GraphQL endCursor passed as after. |
| Server-side session cart created while browsing | Non-secret widget-local cart. |
| Incremental cart mutations | One cartCreate when checkout begins. |
| Stored cart ID used to resume | No cart ID requested, returned, or persisted. |
Reconcile old filters and search behavior with Shopify's product query syntax. A database-backed semantic search feature does not automatically carry over to a live Storefront API query.
When you need cart resume later
Do not move Shopify's cart ID through the model, widget, view state, ordinary Noodle state handles, or tool results. The ID includes a secret key. A resumable design needs an encrypted server-side cart vault keyed by an opaque, non-secret handle, plus tenant/user ownership checks, expiry, redaction, and lifecycle cleanup.
Until that dedicated boundary exists, keep this app on the local-cart plus one-cartCreate design.
Next steps
Shopify example
Read and copy the complete tested implementation.
Connectors
Learn request mappings, credentials, allowlists, and compute normalization.
Apps & widgets
Learn widget result handling, app-only tools, view state, and handoff.
Deploy & operate
Manage environments, secrets, deployment access, logs, and rollback.