Skip to content

Tutorial: assistant-enable a storefront (Next.js)

Northwind is a complete Next.js 15 storefront — catalog, cart, checkout, orders, on a real anonymous → authenticated session model. In this tutorial you start from that working shop and add a DioscHub assistant that browses the catalog, manages the visitor’s cart, and places orders as the visitor, with checkout gated behind explicit approval.

Everything here runs on the free tier: one assistant, one MCP server, two roles, one model. Nothing in this tutorial needs Pro.

You’ll build the integration yourself, guided by the explanation at each step — not by pasting files blind. Two branches and one directory support that:

  • main — the starting point. The bare shop, no AI anywhere. Clone this.
  • production — the finished result. git diff main..production is exactly what this tutorial adds, and nothing else — use it to check your work.
  • tutorial/ (on main) — the reference implementation, one directory per step. When a file is long and mechanical (a 24-tool catalog, HTTP plumbing), the tutorial explains its shape and you copy the finished file from here rather than transcribe it. When a file is the lesson (the transport, the auth broker), you write it yourself and check it against this directory.

The repository is intrigsoft/northwind-store. The whole integration touches four things — one provider component, one route handler, one mcp/ package, and hub configuration. The shop’s own code never changes.

  • A running DioscHub (free tier is fine) and its admin portal.
  • Node 22+.

Replace https://your-hub.example.com throughout with your DioscHub URL.

Clone the starting point and bring it up:

Terminal window
git clone https://github.com/intrigsoft/northwind-store
cd northwind-store
npm install
npm run dev # http://localhost:3010

Browse, add something to the cart, sign in (any credentials — it’s demo auth), and notice the cart carries over. That carry-over is the storefront’s own anonymous → authenticated session model: an httpOnly nw_sid cookie keys a server-side session, and signing in attaches a user to the same session. Every later step builds on that cookie — it’s the identity the assistant will borrow.

What you’re doing: mounting the chat widget and pointing it at your hub. This is pure frontend — the assistant can talk, but it has no identity and no tools yet.

The kit is a web component loaded from your hub. A small provider component wraps it, reads its config from the environment, and renders the <diosc-chat> element. It’s the same shape for any React app; the finished version — including a client-side navigate tool and page-context streaming — is in tutorial/step-2-embed/.

Copy the provider and install the client SDK from npm:

Terminal window
mkdir -p components/assistant
cp tutorial/step-2-embed/AssistantProvider.tsx components/assistant/
npm install @dioschub/client

Mount the provider once, in the root layout, so it survives navigation. The key detail is where its config comes from — read at request time (a server component) so one built image is configurable per deployment:

app/layout.tsx
import { AssistantProvider } from '@/components/assistant/AssistantProvider';
export const dynamic = 'force-dynamic'; // read env per request, not per build
export default function RootLayout({ children }: { children: React.ReactNode }) {
const assistant = {
backendUrl: process.env.DIOSC_PUBLIC_BACKEND_URL || process.env.NEXT_PUBLIC_DIOSC_BACKEND_URL || '',
apiKey: process.env.DIOSC_EMBED_API_KEY || process.env.NEXT_PUBLIC_DIOSC_API_KEY || '',
assistantId: process.env.DIOSC_PUBLIC_ASSISTANT_ID || process.env.NEXT_PUBLIC_DIOSC_ASSISTANT_ID || '',
};
return (
<StoreProvider>
{/* …existing shell… */}
<AssistantProvider {...assistant} />
</StoreProvider>
);
}

Now create the Assistant in the hub. In the admin portal: Assistants → New, name it Northwind Shopper, and generate its embed key — a public, browser-safe credential that identifies which Assistant the widget loads. Point the shop at it:

.env.local
NEXT_PUBLIC_DIOSC_BACKEND_URL=https://your-hub.example.com
NEXT_PUBLIC_DIOSC_API_KEY=<embed key>
NEXT_PUBLIC_DIOSC_ASSISTANT_ID=<assistant id>

Verify: restart npm run dev — the chat FAB appears bottom-right and holds a conversation as an anonymous visitor. On the free tier the panel carries a non-removable “Powered by DioscHub” line; that’s expected. The assistant knows nothing about the shop yet.

What you’re doing: giving the assistant hands. The MCP server is a thin, session-authorized adapter over the shop’s REST API — it adds no privileges of its own. (How the visitor’s session reaches it is step 4; here you just build the surface.)

Scaffold the package:

Terminal window
mkdir -p mcp/src && cd mcp
npm init -y
npm install @modelcontextprotocol/sdk express cors jose
npm install -D typescript @types/express @types/cors tsx

The tool catalog is data, not lesson. Northwind exposes 24 tools — catalog reads, cart mutations, orders, returns, checkout — each a declarative entry mapping a tool name to one REST call. You don’t learn anything by transcribing 24 of them, so copy the catalog and the REST plumbing wholesale; the interesting part is the annotations they carry:

Terminal window
cp ../tutorial/step-3-mcp/tools.ts src/ # the 24-tool catalog
cp ../tutorial/step-3-mcp/tool-handlers.ts src/ # each tool → a storefront call
cp ../tutorial/step-3-mcp/api-client.ts src/ # the REST client

Open src/tools.ts and look at one read and one write:

{ name: 'search_products', annotations: { readOnlyHint: true }, … }
{ name: 'place_order', annotations: { destructiveHint: true }, … }

Reads carry readOnlyHint; mutations carry destructiveHint. These hints don’t enforce anything — they’re advisory metadata the hub reads when you set the approval policy in step 5. That’s the whole reason to annotate honestly.

The transport and auth are the framework — you register tools. src/server.ts builds on @dioschub/mcp-server, which owns the stateless Streamable HTTP transport and the OAuth-style token broker (step 4). You hand it the catalog; it hands each tool the visitor’s session as ctx.auth.

import { createMcpServer } from '@dioschub/mcp-server';
import { northwindTools } from './tools.js';
import { ToolHandlers } from './tool-handlers.js';
import { StorefrontApi } from './api-client.js';
const server = createMcpServer<{ cookie: string }>({
name: 'northwind-storefront',
adminKey: process.env.MCP_BIND_SECRET!, // authenticates the shop's /bind call
jwtSecrets: process.env.MCP_JWT_SECRET!, // signs the framework's handle-JWT
hub: { url: process.env.DIOSC_HUB_URL!, apiKey: process.env.DIOSC_HUB_API_KEY! },
});
for (const t of northwindTools) {
server.tool({
name: t.name,
description: t.description,
input: toZod(t.inputSchema), // tiny jsonSchema→zod helper
// ctx.auth is the visitor's session — resolved by the framework (step 4).
handler: (args, ctx) =>
new ToolHandlers(new StorefrontApi({ cookie: ctx.auth.cookie })).handle(t.name, args),
});
}
server.listen(Number(process.env.MCP_PORT) || 3011);

No hand-written transport, no per-request server, no gate to write — the framework does all of it. ctx.auth is typed as whatever you bind (here { cookie }); how it gets there is step 4. The finished server.ts is in tutorial/step-3-mcp/.

Verify with no hub involved yet — list the tools directly:

Terminal window
npm run dev # add: "dev": "tsx src/server.ts"
curl -s localhost:3011/api/tools | jq '.tools[].name'

Step 4 — Identity and auth (the framework’s token broker)

Section titled “Step 4 — Identity and auth (the framework’s token broker)”

What you’re doing: connecting the visitor’s real session to those tools — correctly. This is the security heart of the integration, so it’s worth understanding even though you don’t write it.

The MCP authorization spec (2025-11-25) casts an MCP server as an OAuth-style resource server: it must accept only tokens issued for itself, and must never pass a client-presented token through to an upstream API. @dioschub/mcp-server implements exactly this — so you don’t write a broker, you configure one. The flow it runs for you:

  1. At bind time, the storefront POSTs the visitor’s identity + nw_sid cookie to the framework’s POST /bind — a server-to-server call inside your own trust domain, guarded by the admin key.
  2. The framework caches the cookie under a jti (it never leaves the process), mints a short-lived handle-JWT whose audience is the server name, and registers that with the hub as the session’s auth artifact.
  3. Every tool call arrives with Authorization: Bearer <handle>. The framework verifies it, resolves the cached cookie, and hands it to your tool as ctx.auth (step 3). The handle never goes upstream; the cookie never touches the hub.

The result: the hub is credential-blind twice over — it holds a token that references the credentials, never the credentials themselves. And an invalid/expired handle becomes a 401 + WWW-Authenticate the hub reads as a re-auth signal — the kit re-binds and the turn resumes.

Write the bind route on the storefront — app/api/diosc/bind/route.ts. It resolves the signed-in user from the shop’s own session and POSTs { connectionId: wsId, identity, artifacts: { cookie } } to the framework’s /bind, with the admin key as the x-admin-key header. The finished file is tutorial/step-4-auth/bind-route.ts. Three properties to notice:

  • The kit calls it same-origin (bindEndpoint: '/api/diosc/bind' in AssistantProvider), so the shop’s cookies flow automatically.
  • Guests bind too, with identity: null — the hub keeps them on the anonymous role; signing in re-binds the same connection and the conversation survives the promotion.
  • The storefront holds no hub credentials — its only secret is the shared bind key for its own MCP server.

Because the framework performs the hub bind, the hub admin key lives on the MCP server, not the shop. Mint it in the portal: API keys → New capability key, scope auth:bind (this is a secret diosc_ak_… key — not the public embed key from step 2, which the hub would reject here with 403). Then configure both sides:

Terminal window
# mcp/.env — the MCP server talks to the hub and signs tokens
DIOSC_HUB_URL=https://your-hub.example.com
DIOSC_HUB_API_KEY=<diosc_ak_… admin key with auth:bind>
MCP_JWT_SECRET=<strong random value>
MCP_BIND_SECRET=<strong random value>
# .env.local — the storefront only knows its own MCP server
NORTHWIND_MCP_URL=http://localhost:3011
MCP_BIND_SECRET=<same value>
DIOSC_HUB_ROLE_ID=role-shopper

Verify: restart both processes, sign in, open the assistant. The hub’s Sessions view now shows the bound identity, and curl -s localhost:3011/health reports boundSessions ≥ 1. Confirm the gate rejects an unissued call:

Terminal window
curl -s -XPOST localhost:3011/mcp -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_cart"}}' -i | head -1
# HTTP/1.1 401 Unauthorized

What you’re doing: telling the hub about the MCP server, giving the assistant its tools and voice, and gating the one irreversible action. Every setting here is exact and every setting is free-tier — one assistant, one MCP server, two roles, one model. (DioscHub also has config export/import, but that rides on the admin API, which is a Pro feature — so this tutorial configures the hub by hand, which is what a free-tier deployment does anyway. The exact values also live in the repo at tutorial/step-5-hub/HUB-CONFIG.md.)

MCP servers → Add:

FieldValue
Namenorthwind-storefront
TransportStreamable HTTP
Server URLhttp://localhost:3011/mcp (or your deployed MCP URL)
Auth configleave empty

Leaving auth empty is deliberate: a credential-less MCP server is a BYOA conduit, so the hub forwards each session’s bound artifact — the broker JWT from step 4 — as the Authorization header on every call. After saving, use Tools → Refresh so the hub discovers the 24 tools.

Models → Add (free tier allows one): pick a provider, model gpt-5-mini (a good cost/latency fit for a shopping assistant), and paste your own provider API key. The key is stored on the hub and never reaches the browser or the assistant context.

Open the Northwind Shopper assistant → Roles. Free tier allows up to three roles per assistant; you need two:

Role shopper (authenticated visitors — this is the role-shopper id you set as DIOSC_HUB_ROLE_ID):

  • Tools: grant the full northwind-storefront toolset — all 24 tools.
  • Approval policy: require approval for the destructive tools — place_order, cancel_order, remove_cart_item. Leave every read and additive tool (search, add-to-cart, apply-promo, wishlist, …) to run without approval.
  • Model: the gpt-5-mini config from 5.2.

Role anonymous (guests, before sign-in):

  • Tools: grant the read-only subset — search_products, get_product, get_reviews, get_related, get_store_policies, get_flash_sale, list_vouchers, plus the cart tools (get_cart, add_to_cart, update_cart_item, remove_cart_item, apply_promo) so a guest can build a cart by chat. Do not grant place_order, orders, returns, or profile — a guest has no account to act on.
  • Approval policy: require approval for remove_cart_item (the one destructive tool a guest can reach).

When a guest signs in, the kit re-binds the same connection and the hub promotes the session from anonymous to shopper in place — the conversation and cart carry over.

Open the assistant → System prompt, and paste the contents of tutorial/step-5-hub/ASSISTANT-PROMPT.md from the repo. It tells the model how to shop on the visitor’s behalf, how to present products, and to stop and confirm before anything irreversible.

The provider from step 2 registers a client-side navigate tool. To let the assistant move the shopper between pages, add the storefront’s routes under the assistant’s Sitemap (/, /search, /p/:id, /cart, /checkout, /orders, /wishlist, /account). Free tier allows up to 10 sitemap entries — enough for the whole shop. The hub only emits navigation paths it validated against this list.

As a signed-in visitor, ask the assistant to find wireless headphones under $150, add the best-rated one to my cart, then check out. Search and add-to-cart execute directly — scoped to your cart, which you can confirm in the shop UI. Checkout surfaces an approval card (the standard consensus dialog — free tier); nothing is ordered until you approve it. Reject it, and the order never happens.

That is the whole integration: the shop’s code untouched, identity arriving via one bind route, capability via one adapter that can do nothing the visitor couldn’t, and the irreversible step held behind human approval — all on the free tier.

Check your work against the finished branch at any time:

Terminal window
git diff production -- ':!tutorial'

An empty diff means you built exactly what ships. The production branch also carries the Dockerfiles for both services if you want to deploy the result.