Skip to Content
TensorPass
Quickstart

Next.js

Add AI Connect to a Next.js App Router site, with a client provider, the server render state and a chat panel that loads on first open.

This page follows Taamtaam, a Next.js site whose "AI mode" button lets visitors search its restaurant catalog with their own AI. The provider lives in a client component in the root layout, and the chat panel loads the first time a visitor opens it.

You need React 19 or later, and Chrome with AI Wallet installed and one account connected to try it.

Install the packages

npm install @aiconnect/core @aiconnect/react

Create the client provider

AiConnectProvider uses React state and the browser, so it goes in a client component. Create the client with a lazy useState initializer so each page load builds one.

src/features/ai-mode/components/AiModeProvider.tsx
'use client';

import { useState } from 'react';
import type { ReactNode } from 'react';
import { AiConnect } from '@aiconnect/core';
import { AiConnectProvider } from '@aiconnect/react';
import '@aiconnect/react/styles.css';

function createClient() {
  return new AiConnect();
}

export default function AiModeProvider({ children }: { children: ReactNode }) {
  const [client] = useState(createClient);
  return <AiConnectProvider client={client}>{children}</AiConnectProvider>;
}

Render it in the root layout, around the parts of the page that use the AI:

src/app/layout.tsx
import type { ReactNode } from 'react';
import AiModeProvider from '@/features/ai-mode/components/AiModeProvider';

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <AiModeProvider>{children}</AiModeProvider>
      </body>
    </html>
  );
}

The server render state

The server cannot see the visitor's wallet. The SDK handles this in two places:

  • new AiConnect() checks for window. Built during the server render, it sends no request and stays in the checking state.
  • AiConnectProvider reads the client through useSyncExternalStore with a server snapshot of { status: 'checking' }. The server HTML and the hydration both render checking, and the provider switches to the browser's state right after.

Render your AI button in every state, so the layout does not move when the state arrives, and treat checking as a state that is not ready yet. A visitor whose wallet already approved your site goes from checking to connected without a click.

Open the panel once the site is connected

The button below opens the chat panel for a connected visitor and the connect window for the others. When the connection the visitor asked for arrives, the panel opens on its own. next/dynamic with ssr: false keeps the panel, the chat and the MCP client out of the first page load and off the server.

src/features/ai-mode/components/AiModeButton.tsx
'use client';

import { useCallback, useEffect, useRef, useState } from 'react';
import dynamic from 'next/dynamic';
import { useAiConnect } from '@aiconnect/react';

// The panel, the chat and the MCP client load on the first open only.
const ChatPanel = dynamic(() => import('./ChatPanel'), { ssr: false });

export default function AiModeButton() {
  const { client, openPicker } = useAiConnect();
  const [isPanelOpen, setPanelOpen] = useState(false);
  const panelAwaitsConnection = useRef(false);
  const closePanel = useCallback(() => setPanelOpen(false), []);

  const openAiMode = useCallback(() => {
    if (client.state.status === 'connected') {
      setPanelOpen(true);
      return;
    }
    panelAwaitsConnection.current = true;
    openPicker();
  }, [client, openPicker]);

  // The panel opens once the awaited connection arrives, and closes when the visitor disconnects.
  useEffect(() => client.subscribe((state) => {
    if (state.status === 'disconnected') {
      setPanelOpen(false);
    }
    if (state.status === 'connected' && panelAwaitsConnection.current) {
      panelAwaitsConnection.current = false;
      setPanelOpen(true);
    }
  }), [client]);

  return (
    <>
      <button type="button" onClick={openAiMode}>AI mode</button>
      {isPanelOpen ? <ChatPanel onClose={closePanel} /> : null}
    </>
  );
}

Build the chat panel

Create the McpToolSource inside the panel with useMemo. A relative URL such as /api/mcp resolves against the page's address, which exists in the browser only, so the panel must not render on the server. The ssr: false import above takes care of that.

src/features/ai-mode/components/ChatPanel.tsx
'use client';

import { useMemo, useState } from 'react';
import type { FormEvent } from 'react';
import { McpToolSource } from '@aiconnect/core';
import { useAiChat, useAiConnect } from '@aiconnect/react';

const SYSTEM_PROMPT = [
  'You are the restaurant guide of this site. Recommend only restaurants returned by the tools.',
  'Call list_filters once to learn the accepted values, then search_restaurants with the filters that match',
  'the visitor\'s request. When a search returns nothing, relax one filter and search again.',
].join(' ');

export default function ChatPanel({ onClose }: { onClose: () => void }) {
  const { client, state } = useAiConnect();
  const tools = useMemo(() => new McpToolSource('/api/mcp', 'MySite'), []);
  const { turns, send, busy, error } = useAiChat({ system: SYSTEM_PROMPT, tools });
  const [question, setQuestion] = useState('');

  const submit = (event: FormEvent) => {
    event.preventDefault();
    send(question);
    setQuestion('');
  };

  return (
    <section role="dialog" aria-label="AI mode">
      <button type="button" aria-label="Close AI mode" onClick={onClose}>×</button>
      {state.status === 'connected' ? (
        <p>
          Answered by your {state.providerName} account.{' '}
          <button type="button" onClick={() => client.disconnect()}>Disconnect</button>
        </p>
      ) : null}
      <ol>
        {turns.map((turn, index) => (
          <li key={index}>
            <p>{turn.question}</p>
            <p>{turn.answer}</p>
          </li>
        ))}
      </ol>
      {error ? <p role="alert">{error}</p> : null}
      <form onSubmit={submit}>
        <input value={question} onChange={(event) => setQuestion(event.target.value)} />
        <button type="submit" disabled={busy || !question.trim()}>Ask</button>
      </form>
    </section>
  );
}

Serve your MCP server on your origin

The visitor's browser calls your MCP server, so the server has to answer the page's origin. The simplest setup is a Route Handler at /api/mcp that relays POST requests to your MCP server, which is what Taamtaam does. The MCP guide has the relay and the server.

Theme the connect window

Import a stylesheet that sets the --aic-* tokens after @aiconnect/react/styles.css, in the same client component:

import '@aiconnect/react/styles.css';
import './aiConnectTheme.css';

The theming guide lists every token.

Move an existing AI route into the page

A Next.js site often calls its model in a Route Handler, such as app/api/chat/route.ts, because its API key has to stay on the server. With AI Connect the visitor's wallet answers and no key exists, so the call moves into a client component and the route goes away. The site's data stays on the server behind its MCP server.

'use client';

import { useMemo } from 'react';
import { createOpenAI } from '@ai-sdk/openai';
import { streamText } from 'ai';
import { useAiConnect } from '@aiconnect/react';

function useOpenAi() {
  const { client } = useAiConnect();
  return useMemo(() => createOpenAI({ apiKey: client.sdkOptions.apiKey, fetch: client.fetch }), [client]);
}

// In the chat panel:
const openai = useOpenAi();
const result = streamText({ model: openai('gpt-6-luna'), system: SYSTEM, prompt: question, tools });

The AI SDK guide shows the route before and after, and the change for the other SDKs.

Run it

Start next dev, open the site on localhost in Chrome with AI Wallet installed, click AI mode, then Connect AI Wallet, and approve the site in the wallet's window. The panel opens once the wallet connects the site.

On this page