Skip to content
WebMCP Agents
Esc
↑↓navigate↵open⌘Jpreview
On this page

Field Store on Next.js

The store on the Next.js App Router with shadcn/ui, where Server Components render the pages, client components hold the tools, the chat is a shadcn block, and the API key provably stays on the server.

examples/nextjs is Field Store again, trimmed to home, product, cart and checkout, on Next.js 16 (App Router, Turbopack) with shadcn/ui (Tailwind v4). It exists to show three things the Vite examples can’t:

  • Server Components render the pages; only the tools that live in a component and the chat are client components (they need the DOM).
  • The chat is a shadcn block (agent-chat from the @webmcp-agents registry), built on useAgentChat and shadcn/ui’s AI patterns: Message Scroller, Tool (Trace), Subagent, Reasoning, Prompt Input, Model Selector.
  • The key never reaches the client: it lives in a server-only module and is tainted with React’s experimental_taintUniqueValue, so a Server Component can’t pass it to a client component by accident.

Field Store on Next.js: the store front, server-rendered, with the shadcn chat block

A product page: a Server Component around the client add_to_cart form

Run it

pnpm --filter @webmcp-agents/example-nextjs dev    # http://localhost:5176; builds the packages it consumes first
pnpm --filter @webmcp-agents/example-nextjs e2e    # Playwright, desktop + Pixel 7

Put a key in examples/nextjs/.env.local (see .env.example); never with a NEXT_PUBLIC_ prefix. In development the devtools panel is on Alt+Shift+A, and window.agent.invoke("open_product", { slug: "ocean-tee" }) works from the console.

Where things are

  • app/
    • layout.tsx (Server Component: fonts, the agent cursor’s stylesheet, <Providers>)
    • providers.tsx (“use client”: <WebAgentsProvider tools={tools}>, header, the chat block)
    • page.tsx (home, a Server Component; ?q= is what search_products navigates to)
    • product/[slug]/page.tsx (Server Component + the client add_to_cart form)
    • cart/, checkout/ (client views; checkout holds apply_promo and the confirm-gated checkout)
    • api/chat/route.ts (createVercelChatRoute({ app, agents, model }), the example’s guarded route)
  • webmcp-agents/
    • agents.ts (shopper with the checkout sub-agent: shared by the client and the route)
    • tools/ (index.ts: the catalog, plain modules, one file per tool)
    • hooks/ (the forms’ tools: useWebTool with shadcn’s Form and React Hook Form; the forms’ UI is in components/)
  • lib/env.server.ts (server-only + taint)
  • lib/catalog.ts, lib/cart-store.ts (the cart, no React), hooks/use-cart.ts (useCart()), lib/navigate.ts
  • components/web-agents/ (the installed registry blocks: agent-chat, agent-tool, agent-subagent, agent-reasoning, agent-model-selector)

Tools and agents

Tool Where On
search_products useWebTool in the header, shadcn Form everywhere
open_product, go_to_page, get_cart catalog (webmcp-agents/tools/index.ts, no React) everywhere
add_to_cart useWebTool, shadcn Form with a Select for the size and fill a product page
apply_promo useWebTool, shadcn Form checkout, no code applied
checkout useWebTool, shadcn Form with a Select for the country and fill, requireConfirmation checkout; the cursor types the details, fill sets the country, and it parks on Place order until the person clicks

shopper sees everything but apply_promo and checkout; it hands a purchase to the checkout sub-agent (delegate_to_checkout), which runs with those two tools plus get_cart and go_to_page (listed by reference for the catalog tools, by id for the two a component owns). In the chat the delegate call shows as a Subagent trace with the nested tool lines inside (Define an agent).

The chat route is the example’s guarded one, from the private @webmcp-agents/vercel-api package; in your own app it is createChatHandler (Add the chat).

Try it

Ask for a mug

Open the Assistant (bottom left in development) and send “Add a ceramic mug to my cart”. open_product and add_to_cart show as Trace lines while the cursor works the page.

Check out through the sub-agent

“Check out with promo WELCOME10 to [email protected], 1 Main St”. The shopper delegates; the Subagent trace nests get_cart, go_to_page, apply_promo and checkout, which parks: “Confirm on the page”. Click Place order.

See that the key stays put

View source: no key. The route answers GET /api/chat with { enabled, model, provider, agents } and nothing else. Pass env.anthropicKey from lib/env.server.ts as a prop to a client component: React throws, because the key is tainted.

Patterns worth copying

  • next.config.ts turns the taint APIs on (experimental: { taint: true }): React’s taint APIs exist only behind that flag.
  • server-only + taint for the one secret (lib/env.server.ts); the route handler reads process.env itself (Add the chat).
  • A locked-down route (app/api/chat/route.ts): chat sessions, the tool allowlist (allowedTools in webmcp-agents/tools/index.ts), an agent required, and Turnstile when TURNSTILE_SECRET_KEY and NEXT_PUBLIC_TURNSTILE_SITE_KEY are set (Secure the chat).
  • Consume the built packages (dist), not the @webmcp-agents/source condition: Turbopack has no custom export conditions, and this is the published shape anyway. predev builds the dependencies.
  • The cursor stylesheet is a plain import in app/layout.tsx (import "@webmcp-agents/core/cursor.css"), the same line as in a Vite app: it has no pseudo-class Turbopack’s CSS parser would refuse.
  • The same provider line as every other app, <WebAgentsProvider tools={tools}>: the library follows the URL itself, so there is no usePathname and no router hook (Tools across your app).
  • A shadcn Select is a custom widget. The cursor visits it but has nothing to type into, so the forms with one pass fill: (values) => form.reset(values) (Tools with input). form.tsx in examples/ui (the examples’ shared shadcn/ui components) is shadcn’s Form for React Hook Form in the base-nova style.
  • Catalog tools are plain modules with no React (defineWebTool from core, the cart store in lib/cart-store.ts): agents.ts lists them by reference, and the chat route imports agents.ts on the server. They navigate with the App Router’s router.push, handed over by <Providers> (lib/navigate.ts).
  • No “ready” gate. <Providers> renders the provider, the header and the page on the server; in a browser without WebMCP the provider loads the polyfill itself and the tools register once it is there. In next dev the provider installs the devtools too (they give window.agent to the e2e tests); <Providers> has no code for them.

Learn more

Install in a Next.js app is the same setup for your own project. Use your shadcn components covers the chat blocks, Define an agent the shopper and its checkout sub-agent, and useAgentChat the hook underneath.

Was this page helpful?