Skip to Content
TensorPass
Quickstart

Vue

Connect the visitor's AI and run your tools in a Vue 3 app, with the AiConnect client of @aiconnect/core held in a composable.

This page builds a small Vue 3 app, served by Vite, with a connect button and a chat that searches a bookshop catalog through its tools. Vue uses @aiconnect/core directly: a composable keeps the client's connection state in a Vue ref, and your components render from it.

The connect window of @aiconnect/react is made of React components, so a Vue app shows its own connect button, which calls client.connect().

You need Vue 3, and Chrome with AI Wallet installed and one account connected to try it.

Install the package

npm install @aiconnect/core

Follow the connection

Create one client for the page in a module of its own. In a browser it looks for AI Wallet right away and asks whether your site is already approved. useConnection gives each component the state as a ref.

src/aiConnect.ts
import { onScopeDispose, shallowRef } from 'vue';
import { AiConnect } from '@aiconnect/core';

// One client for the page. In a browser it looks for AI Wallet right away.
export const client = new AiConnect();

// The connection state as a Vue ref. It follows the client until the component unmounts.
export function useConnection() {
  const state = shallowRef(client.state);
  onScopeDispose(client.subscribe((next) => {
    state.value = next;
  }));
  return state;
}

subscribe calls your listener on each change only, so the ref starts from client.state. A shallowRef keeps the state object as the client built it, session included, and Vue updates the components each time the client replaces it. onScopeDispose removes the listener when the component unmounts.

client.installUrl is the AI Wallet page, https://tensorpass.app/wallet, where a visitor without the wallet gets it.

Add the connect button

The button renders one line per state. The JavaScript quickstart explains each of the six states.

src/components/ConnectButton.vue
<script setup lang="ts">
import { client, useConnection } from '../aiConnect';

const state = useConnection();
</script>

<template>
  <p v-if="state.status === 'checking'" role="status">Looking for AI Wallet</p>
  <p v-else-if="state.status === 'noWallet'" role="status">
    Add AI Wallet to your browser, then reload this page.
    <a :href="client.installUrl" target="_blank" rel="noreferrer">Install AI Wallet</a>
  </p>
  <p v-else-if="state.status === 'connecting'" role="status">Approve this site in the AI Wallet window.</p>
  <p v-else-if="state.status === 'connected'" role="status">
    Connected with {{ state.providerName }}
    <button type="button" @click="client.disconnect()">Disconnect</button>
  </p>
  <p v-else role="status">
    {{ state.status === 'error' ? state.message : 'Use your own AI on this site.' }}
    <button type="button" @click="client.connect()">Connect your AI</button>
  </p>
</template>

A visitor who declines in the wallet's window lands in the error state with the wallet's reason, "The connection was declined", and the button asks the wallet again. A visitor who approved your site on an earlier visit is connected a moment after the page loads, with no click.

client.disconnect() removes your site from the visitor's approved sites in the wallet. The next connect() asks the visitor again.

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. The composable keeps the transcript and passes it back on the next question.

src/useChat.ts
import { ref } from 'vue';
import { finalAnswer, runConversation } from '@aiconnect/core';
import type { ChatMessage, ProviderSession, ToolCall, ToolSource } from '@aiconnect/core';

// A conversation on the visitor's AI. It keeps the transcript and shows the last answer with its tool calls.
export function useChat(system: string, tools: ToolSource) {
  const answer = ref('');
  const toolCalls = ref<ToolCall[]>([]);
  const busy = ref(false);
  const error = ref('');
  let transcript: ChatMessage[] = [{ role: 'system', content: system }];

  async function ask(session: ProviderSession, question: string) {
    const asked: ChatMessage = { role: 'user', content: question };
    answer.value = '';
    toolCalls.value = [];
    error.value = '';
    busy.value = true;
    try {
      const added = await runConversation({
        session,
        tools,
        messages: [...transcript, asked],
        onText: (delta) => {
          answer.value += delta;
        },
        onToolCall: (call) => {
          toolCalls.value.push(call);
        },
      });
      transcript = [...transcript, asked, ...added];
      // The streamed text covers every step of the loop; the final answer is the model's last message.
      answer.value = finalAnswer(added)?.content ?? answer.value;
    } catch (failure) {
      error.value = failure instanceof Error ? failure.message : String(failure);
    } finally {
      busy.value = false;
    }
  }

  return { answer, toolCalls, busy, error, ask };
}

added holds the assistant turns and the tool results of the 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 from a component

McpToolSource reads the tools from your MCP server; the MCP guide shows how to serve one. The chat reads the session from the connection state when the visitor asks.

src/components/BookChat.vue
<script setup lang="ts">
import { ref } from 'vue';
import { McpToolSource } from '@aiconnect/core';
import { useConnection } from '../aiConnect';
import { useChat } from '../useChat';

const SYSTEM = 'You recommend books from this site\'s catalog. Always search the catalog before recommending.';

// Built once: the source keeps its MCP connection and the tool list it read.
const tools = new McpToolSource('/api/mcp', 'Bookshop');

const state = useConnection();
const { answer, toolCalls, busy, error, ask } = useChat(SYSTEM, tools);
const question = ref('');

function submit() {
  const text = question.value.trim();
  if (state.value.status !== 'connected' || !text) {
    return;
  }
  question.value = '';
  ask(state.value.session, text);
}
</script>

<template>
  <form v-if="state.status === 'connected'" @submit.prevent="submit">
    <label for="question">Your question</label>
    <input id="question" v-model="question" autocomplete="off" placeholder="A calm book for tonight" />
    <button type="submit" :disabled="busy">Ask</button>
    <p v-for="call in toolCalls" :key="call.id" class="tool">{{ call.name }} {{ JSON.stringify(call.arguments) }}</p>
    <p class="answer">{{ answer }}</p>
    <p v-if="error" role="alert">{{ error }}</p>
  </form>
  <p v-else>Connect your AI to ask for a book that fits your mood.</p>
</template>

Place both components in your app:

src/App.vue
<script setup lang="ts">
import BookChat from './components/BookChat.vue';
import ConnectButton from './components/ConnectButton.vue';
</script>

<template>
  <main>
    <h1>Bookshop</h1>
    <ConnectButton />
    <BookChat />
  </main>
</template>

The answer grows in the page as the model streams it. ask does not queue questions, which is why the button stays disabled while busy is true.

Try it without a server

Any object with listTools and callTool is a tool source. Pass the demoTools catalog of the React quickstart to useChat in place of the McpToolSource to try the chat before your MCP server exists.

Run it

  1. Start Vite with npm run dev. AI Wallet serves pages on HTTPS and on http://localhost or http://127.0.0.1.
  2. Open the page in Chrome with AI Wallet installed and an account connected in the wallet.
  3. Click Connect your AI and approve your site in the window the wallet opens.
  4. Ask a question. The tool calls show as they run, and the answer streams from the account the wallet chose for your site.

The core reference lists every export with its options.

On this page