Skip to Content
TensorPass
Quickstart

JavaScript

Connect the visitor's AI and run your tools with the AiConnect client of @aiconnect/core, without React.

@aiconnect/core works without any framework. You build the connect button yourself from the client's state, and you run the conversation with runConversation. The connect window of @aiconnect/react is not part of this setup.

@aiconnect/core is an ES module that imports @modelcontextprotocol/sdk, so load it through your bundler, such as Vite, esbuild or webpack.

Install the package

npm install @aiconnect/core

The page

index.html
<button id="connect" type="button" hidden>Connect your AI</button>
<p id="status" role="status"></p>
<form id="ask" hidden>
  <label for="question">Your question</label>
  <input id="question" name="question" autocomplete="off" />
  <button type="submit">Ask</button>
</form>
<p id="answer"></p>
<script type="module" src="/src/aiMode.js"></script>

Follow the connection

Create one client for the page. In a browser it looks for AI Wallet right away and asks whether your site is already approved. Render its current state once, then subscribe to the changes.

src/aiMode.js
import { AiConnect, McpToolSource, finalAnswer, runConversation } from '@aiconnect/core';

const client = new AiConnect();

const connectButton = document.querySelector('#connect');
const statusLine = document.querySelector('#status');
const form = document.querySelector('#ask');
const answer = document.querySelector('#answer');

const STATUS_TEXT = {
  checking: 'Looking for AI Wallet',
  noWallet: 'Add AI Wallet to your browser, then reload this page.',
  disconnected: 'Use your own AI on this site.',
  connecting: 'Approve this site in the AI Wallet window.',
};

function render(state) {
  connectButton.hidden = state.status !== 'disconnected' && state.status !== 'error';
  form.hidden = state.status !== 'connected';
  if (state.status === 'connected') {
    statusLine.textContent = `Answered by your ${state.providerName} account.`;
  } else if (state.status === 'error') {
    statusLine.textContent = state.message;
  } else {
    statusLine.textContent = STATUS_TEXT[state.status];
  }
}

render(client.state);
client.subscribe(render);
connectButton.addEventListener('click', () => client.connect());

subscribe calls your listener on each change only, which is why the code renders client.state once first.

The states, in the order a first visit goes through them:

StatusMeaning
checkingThe client is looking for the wallet, or the page is rendering on a server.
noWalletNo wallet answered within 300 milliseconds. Show how to install AI Wallet.
disconnectedThe wallet is there and your site is not connected. A wallet that is locked when the page loads reports this until the visitor unlocks it.
connectingconnect() is waiting for the visitor in the wallet's window.
connectedYour site may chat. The state carries providerName, model and session.
errorThe connection failed. message holds the wallet's reason, such as "The connection was declined".

Run a conversation with your tools

runConversation sends the conversation to the visitor's model, runs the tools it asks for, and returns the messages it added. Keep the transcript yourself and pass it back on the next question.

src/aiMode.js
const SYSTEM = 'You recommend books from this site\'s catalog. Always search the catalog before recommending.';
const tools = new McpToolSource('/api/mcp', 'Bookshop');
let transcript = [{ role: 'system', content: SYSTEM }];

async function ask(question) {
  const { state } = client;
  if (state.status !== 'connected') {
    return;
  }
  const asked = { role: 'user', content: question };
  answer.textContent = '';
  const added = await runConversation({
    session: state.session,
    tools,
    messages: [...transcript, asked],
    onText: (delta) => {
      answer.textContent += delta;
    },
    onToolCall: (call) => {
      statusLine.textContent = `Running ${call.name}`;
    },
  });
  transcript = [...transcript, asked, ...added];
  // The streamed text covers every step of the loop; the final answer is the model's last message.
  answer.textContent = finalAnswer(added)?.content ?? answer.textContent;
  statusLine.textContent = `Answered by your ${state.providerName} account.`;
}

form.addEventListener('submit', (event) => {
  event.preventDefault();
  const question = String(new FormData(form).get('question') ?? '').trim();
  form.reset();
  if (!question) {
    return;
  }
  ask(question).catch((error) => {
    statusLine.textContent = error.message;
  });
});

added holds the assistant turns and the tool results of this question, so later questions see what the tools returned earlier. A chat request rejects with the wallet's message when it fails, for example when the visitor stops the answer from the wallet or when the provider refuses the call.

Ask without tools

For a single answer with no tools, call the session directly:

const { state } = client;
if (state.status === 'connected') {
  const { message } = await state.session.chat({
    messages: [{ role: 'user', content: 'Summarise this page in two sentences.' }],
    onText: (delta) => {
      answer.textContent += delta;
    },
  });
  console.log(message.content);
}

Disconnect

await client.disconnect();

This removes your site from the visitor's approved sites in the wallet, and the state becomes disconnected. The next connect() asks the visitor again.

Already using an AI SDK

A page that already calls a model through the OpenAI SDK, the Anthropic SDK, Google Gen AI, the Vercel AI SDK, LangChain or LlamaIndex keeps its code and passes the client's fetch to the SDK:

import OpenAI from 'openai';

const openai = new OpenAI(client.sdkOptions);

The AI SDK guide shows the change for each SDK.

The core reference lists every export with its options.

On this page