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.
How this tutorial works
Section titled “How this tutorial works”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..productionis exactly what this tutorial adds, and nothing else — use it to check your work.tutorial/(onmain) — 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.
Prerequisites
Section titled “Prerequisites”- A running DioscHub (free tier is fine) and its admin portal.
- Node 22+.
Replace https://your-hub.example.com throughout with your DioscHub URL.
Step 1 — Run the bare shop
Section titled “Step 1 — Run the bare shop”Clone the starting point and bring it up:
git clone https://github.com/intrigsoft/northwind-storecd northwind-storenpm installnpm run dev # http://localhost:3010Browse, 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.
Step 2 — Embed the assistant
Section titled “Step 2 — Embed the assistant”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:
mkdir -p components/assistantcp tutorial/step-2-embed/AssistantProvider.tsx components/assistant/npm install @dioschub/clientMount 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:
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:
NEXT_PUBLIC_DIOSC_BACKEND_URL=https://your-hub.example.comNEXT_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.
Step 3 — The MCP server
Section titled “Step 3 — The MCP server”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:
mkdir -p mcp/src && cd mcpnpm init -ynpm install @modelcontextprotocol/sdk express cors josenpm install -D typescript @types/express @types/cors tsxThe 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:
cp ../tutorial/step-3-mcp/tools.ts src/ # the 24-tool catalogcp ../tutorial/step-3-mcp/tool-handlers.ts src/ # each tool → a storefront callcp ../tutorial/step-3-mcp/api-client.ts src/ # the REST clientOpen 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:
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:
- At bind time, the storefront POSTs the visitor’s identity +
nw_sidcookie to the framework’sPOST /bind— a server-to-server call inside your own trust domain, guarded by the admin key. - 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. - Every tool call arrives with
Authorization: Bearer <handle>. The framework verifies it, resolves the cached cookie, and hands it to your tool asctx.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'inAssistantProvider), 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:
# mcp/.env — the MCP server talks to the hub and signs tokensDIOSC_HUB_URL=https://your-hub.example.comDIOSC_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 serverNORTHWIND_MCP_URL=http://localhost:3011MCP_BIND_SECRET=<same value>DIOSC_HUB_ROLE_ID=role-shopperVerify: 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:
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 UnauthorizedStep 5 — Configure the hub
Section titled “Step 5 — Configure the hub”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.)
5.1 Register the MCP server
Section titled “5.1 Register the MCP server”MCP servers → Add:
| Field | Value |
|---|---|
| Name | northwind-storefront |
| Transport | Streamable HTTP |
| Server URL | http://localhost:3011/mcp (or your deployed MCP URL) |
| Auth config | leave 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.
5.2 Add a model
Section titled “5.2 Add a model”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.
5.3 Create the two roles
Section titled “5.3 Create the two roles”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-storefronttoolset — 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-miniconfig 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 grantplace_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.
5.4 Give the assistant its voice
Section titled “5.4 Give the assistant its voice”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.
5.5 (Optional) Navigation
Section titled “5.5 (Optional) Navigation”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.
Verify the full journey
Section titled “Verify the full journey”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.
Where you are
Section titled “Where you are”Check your work against the finished branch at any time:
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.