Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ SITE_NAME="Next.js Commerce"
SHOPIFY_REVALIDATION_SECRET=""
SHOPIFY_STOREFRONT_ACCESS_TOKEN=""
SHOPIFY_STORE_DOMAIN="[your-shopify-store-subdomain].myshopify.com"
# Optional. A Chrome origin-trial token for WebMCP, bound to the exact origin you deploy to.
# Leave empty to keep WebMCP available only in browsers with the testing flag enabled.
WEBMCP_ORIGIN_TRIAL_TOKEN=""
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,60 @@ Your app should now be running on [localhost:3000](http://localhost:3000/).
1. Run `pnpm dev` to ensure everything is working correctly.
</details>

## WebMCP tools for AI agents

[WebMCP](https://github.com/webmachinelearning/webmcp) is a proposed browser API that lets a page hand AI agents a set of typed tools, so an agent can call `shop.search_products` instead of guessing at the DOM. This storefront registers four:

| Tool | What it does |
| -------------------------- | ---------------------------------------- |
| `shop.search_products` | Search the catalog |
| `shop.get_product_options` | List a product's option names and values |
| `shop.get_cart` | Read what is in the cart |
| `shop.add_to_cart` | Add one variant to the cart |

Two files: `components/webmcp-tools.tsx` registers the tools with [`use-webmcp-tool`](https://github.com/GoogleChromeLabs/use-webmcp-tool), Chrome's React hook for `document.modelContext`, and `lib/webmcp/actions.ts` implements them as server actions on top of the existing `lib/shopify` functions. Browsers without WebMCP are unaffected — the hook feature-detects and does nothing.

### Try it locally

1. Open `chrome://flags/#enable-webmcp-testing`, enable **WebMCP for testing**, and relaunch Chrome.
2. Install the [Model Context Tool Inspector](https://github.com/beaufortfrancois/model-context-tool-inspector) extension.
3. Run `pnpm dev`, open the storefront, and use the inspector's side panel to list the tools and call them by hand.

The inspector is a development tool, not a security boundary. Only use it on pages you trust.

### Add your own tool

Write a server action that returns a plain object, then register it. That is the whole pattern:

```tsx
// lib/webmcp/actions.ts
export async function getShippingPolicy() {
return { policy: "Free shipping over $50, delivered in 3-5 business days." };
}

// components/webmcp-tools.tsx
useWebMCP({
name: "shop.get_shipping_policy",
description: "Explain this store's shipping cost and delivery time.",
annotations: { readOnlyHint: true, untrustedContentHint: true },
execute: getShippingPolicy,
formatOutput: reportErrors,
});
```

Two habits worth copying. Validate arguments inside the action — the `inputSchema` tells the agent what to send, but it cannot stop a confused or hostile one sending something else, which is why `shop.add_to_cart` re-derives the variant from the product instead of accepting a variant id. And return failures as `{ error: "..." }` so `reportErrors` can mark them as real MCP errors; otherwise the agent reads a failure as success.

### Enabling it on a deployment

The flag above only affects your own browser. For real visitors, WebMCP runs as a Chrome origin trial **through Chrome 156**, and the deployed origin needs its own token:

1. Register the exact origin at [developer.chrome.com/origintrials](https://developer.chrome.com/origintrials).
2. Set the token as `WEBMCP_ORIGIN_TRIAL_TOKEN`.

`app/layout.tsx` emits `<meta http-equiv="origin-trial">` only when that variable is set, so leaving it unset is a clean no-op. Tokens are origin-bound and expire, so a token for one deployment does nothing on preview URLs or forks.

> **Not yet enrolled.** No origin-trial token is registered for `demo.vercel.store`, so these tools currently register only in a browser with the testing flag on.

## Vercel, Next.js Commerce, and Shopify Integration Guide

You can use this comprehensive [integration guide](https://vercel.com/docs/integrations/ecommerce/shopify) with step-by-step instructions on how to configure Shopify as a headless CMS using Next.js Commerce as your headless Shopify storefront on Vercel.
11 changes: 11 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CartProvider } from "components/cart/cart-context";
import { Navbar } from "components/layout/navbar";
import { WebMCPTools } from "components/webmcp-tools";
import { WelcomeToast } from "components/welcome-toast";
import { GeistSans } from "geist/font/sans";
import { getCart } from "lib/shopify";
Expand All @@ -10,6 +11,10 @@ import { baseUrl } from "lib/utils";

const { SITE_NAME } = process.env;

// TODO(gaojude): no origin-trial token is registered for demo.vercel.store yet, so
// WebMCP only works in a browser with chrome://flags/#enable-webmcp-testing enabled.
const webmcpOriginTrialToken = process.env.WEBMCP_ORIGIN_TRIAL_TOKEN;

export const metadata = {
metadataBase: new URL(baseUrl),
title: {
Expand All @@ -32,6 +37,11 @@ export default async function RootLayout({

return (
<html lang="en" className={GeistSans.variable}>
<head>
{webmcpOriginTrialToken ? (
<meta httpEquiv="origin-trial" content={webmcpOriginTrialToken} />
) : null}
</head>
<body className="bg-neutral-50 text-black selection:bg-teal-300 dark:bg-neutral-900 dark:text-white dark:selection:bg-pink-500 dark:selection:text-white">
<CartProvider cartPromise={cart}>
<Navbar />
Expand All @@ -40,6 +50,7 @@ export default async function RootLayout({
<Toaster closeButton />
<WelcomeToast />
</main>
<WebMCPTools />
</CartProvider>
</body>
</html>
Expand Down
132 changes: 132 additions & 0 deletions components/webmcp-tools.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"use client";

import {
addProductToCart,
getCartSummary,
getProductOptions,
searchProducts,
} from "lib/webmcp/actions";
import { useWebMCP } from "use-webmcp-tool";

// WebMCP lets this page hand an AI agent a set of typed tools instead of making
// it read the DOM. Each useWebMCP call registers one tool while this component
// is mounted, and unregisters it on unmount. Browsers without WebMCP get a no-op.
//
// To add your own tool, copy any block below: give it a name, a description the
// agent will read, a JSON Schema for its arguments, and an execute function.

const READ_ONLY = { readOnlyHint: true, untrustedContentHint: true };
const WRITES = { readOnlyHint: false, untrustedContentHint: false };

// Our actions report a problem as { error: "..." }. Without this the hook would
// serialize that object as a *successful* result and the agent could not tell a
// failed lookup from a good one, so turn it into a real MCP error instead.
function reportErrors(result: unknown) {
if (result && typeof result === "object" && "error" in result) {
return {
content: [{ type: "text", text: String(result.error) }],
isError: true,
};
}
return result;
}

const searchProductsSchema = {
type: "object",
properties: {
query: {
type: "string",
description: "Words to search for in the catalog.",
},
limit: {
type: "integer",
description: "How many products to return. Defaults to 5, maximum 10.",
},
},
required: ["query"],
additionalProperties: false,
};

const getProductOptionsSchema = {
type: "object",
properties: {
handle: {
type: "string",
description: "A product handle returned by shop.search_products.",
},
},
required: ["handle"],
additionalProperties: false,
};

const addToCartSchema = {
type: "object",
properties: {
handle: {
type: "string",
description: "A product handle returned by shop.search_products.",
},
selectedOptions: {
type: "array",
description:
"One entry per product option, using names and values from shop.get_product_options.",
items: {
type: "object",
properties: {
name: { type: "string" },
value: { type: "string" },
},
required: ["name", "value"],
additionalProperties: false,
},
},
quantity: {
type: "integer",
description: "How many units to add. Defaults to 1.",
},
},
required: ["handle", "selectedOptions"],
additionalProperties: false,
};

export function WebMCPTools() {
useWebMCP({
name: "shop.search_products",
description:
"Search this store's catalog. Pass a returned handle to shop.get_product_options.",
inputSchema: searchProductsSchema,
annotations: READ_ONLY,
execute: searchProducts,
formatOutput: reportErrors,
});

useWebMCP({
name: "shop.get_product_options",
description:
"List the option names and values for one product, such as size or color.",
inputSchema: getProductOptionsSchema,
annotations: READ_ONLY,
execute: getProductOptions,
formatOutput: reportErrors,
});

useWebMCP({
name: "shop.get_cart",
description: "Read what is currently in the shopper's cart.",
annotations: READ_ONLY,
execute: getCartSummary,
formatOutput: reportErrors,
});

useWebMCP({
name: "shop.add_to_cart",
description:
"Add one product variant to the cart. Choose every option first with shop.get_product_options.",
inputSchema: addToCartSchema,
annotations: WRITES,
execute: addProductToCart,
formatOutput: reportErrors,
});

return null;
}
Loading