Skip to Content
TensorPass
Quickstart

Svelte

Connect the visitor's AI and run your tools in a Svelte 5 app, with the AiConnect client of @aiconnect/core read through runes.

This page builds a small Svelte 5 app, served by Vite, with a connect button and a chat that searches a bookshop catalog through its tools. Svelte uses @aiconnect/core directly: createSubscriber turns the client's changes into Svelte reactivity, and a class with $state fields holds the conversation.

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

You need Svelte 5.7 or later for createSubscriber, 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 .svelte.ts module. In a browser it looks for AI Wallet right away and asks whether your site is already approved.

src/lib/aiConnect.svelte.ts
import { createSubscriber } from 'svelte/reactivity';
import { AiConnect } from '@aiconnect/core';
import type { ConnectionState } from '@aiconnect/core';

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

// Svelte listens to the client while a component or an effect reads the state.
const track = createSubscriber((update) => client.subscribe(update));

export const connection = {
  get state(): ConnectionState {
    track();
    return client.state;
  },
};

connection.state returns the client's current state. Read inside a component or an effect, it also subscribes to the client, and Svelte reruns what depends on it after each change. client.subscribe returns its own unsubscribe function, which createSubscriber calls once nothing reads the state anymore.

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/lib/ConnectButton.svelte
<script lang="ts">
  import { client, connection } from './aiConnect.svelte';

  const current = $derived(connection.state);
</script>

{#if current.status === 'checking'}
  <p role="status">Looking for AI Wallet</p>
{:else if current.status === 'noWallet'}
  <p 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>
{:else if current.status === 'connecting'}
  <p role="status">Approve this site in the AI Wallet window.</p>
{:else if current.status === 'connected'}
  <p role="status">
    Connected with {current.providerName}
    <button type="button" onclick={() => client.disconnect()}>Disconnect</button>
  </p>
{:else}
  <p role="status">
    {current.status === 'error' ? current.message : 'Use your own AI on this site.'}
    <button type="button" onclick={() => client.connect()}>Connect your AI</button>
  </p>
{/if}

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 Chat class keeps the transcript and passes it back on the next question. Its $state fields update the page as the answer streams.

src/lib/chat.svelte.ts
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 class Chat {
  answer = $state('');
  toolCalls = $state<ToolCall[]>([]);
  busy = $state(false);
  error = $state('');
  #tools: ToolSource;
  #transcript: ChatMessage[];

  constructor(system: string, tools: ToolSource) {
    this.#tools = tools;
    this.#transcript = [{ role: 'system', content: system }];
  }

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

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/lib/BookChat.svelte
<script lang="ts">
  import { McpToolSource } from '@aiconnect/core';
  import { connection } from './aiConnect.svelte';
  import { Chat } from './chat.svelte';

  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 chat = new Chat(SYSTEM, new McpToolSource('/api/mcp', 'Bookshop'));
  const current = $derived(connection.state);
  let question = $state('');

  function submit(event: SubmitEvent) {
    event.preventDefault();
    const text = question.trim();
    if (current.status !== 'connected' || !text) {
      return;
    }
    question = '';
    chat.ask(current.session, text);
  }
</script>

{#if current.status === 'connected'}
  <form onsubmit={submit}>
    <label for="question">Your question</label>
    <input id="question" bind:value={question} autocomplete="off" placeholder="A calm book for tonight" />
    <button type="submit" disabled={chat.busy}>Ask</button>
    {#each chat.toolCalls as call (call.id)}
      <p class="tool">{call.name} {JSON.stringify(call.arguments)}</p>
    {/each}
    <p class="answer">{chat.answer}</p>
    {#if chat.error}
      <p role="alert">{chat.error}</p>
    {/if}
  </form>
{:else}
  <p>Connect your AI to ask for a book that fits your mood.</p>
{/if}

Place both components in your app:

src/App.svelte
<script lang="ts">
  import BookChat from './lib/BookChat.svelte';
  import ConnectButton from './lib/ConnectButton.svelte';
</script>

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

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 new Chat 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