---
title: "AI agent chatbot"
description: "Build an AI agent that can read and edit Tiptap documents using the Server AI Toolkit API."
canonical_url: "https://tiptap.dev/docs/content-ai/capabilities/server-ai-toolkit/agents/ai-agent-chatbot"
---

# AI agent chatbot

Build an AI agent that can read and edit Tiptap documents using the Server AI Toolkit API.

- **1. Get access**

  Access the Server AI Toolkit by purchasing the paid Server AI Toolkit add-on.
  Contact our team.
- **2. Install the package**

  The Server AI Toolkit package is open source. Install it from the public npm registry using npm
  or your preferred package manager.
- **3. Authenticate to the service**

  Configure your Content AI credentials to access the Server AI Toolkit service.

Build a simple AI agent chatbot that can read and edit Tiptap documents.

> **Interactive demo:** [server ai agent chatbot](https://ai-toolkit-demos.vercel.app/server-ai-agent-chatbot)

See the [source code on GitHub](https://github.com/ueberdosis/ai-toolkit-demos).

## Tech stack

- [React](https://react.dev/) + [Next.js](https://nextjs.org/)
- [AI SDK by Vercel](https://ai-sdk.dev/) + [OpenAI](https://openai.com/) models
- Server AI Toolkit API

## Installation

Create a [Next.js](https://nextjs.org/) project:

```bash
npx create-next-app@latest server-ai-agent-chatbot
```

Install the core Tiptap packages, collaboration extensions, and the [Vercel AI SDK](https://ai-sdk.dev/) for OpenAI:

```bash
npm install @tiptap/react @tiptap/starter-kit @tiptap/extension-collaboration @tiptap-pro/provider ai @ai-sdk/react @ai-sdk/openai zod uuid yjs jsonwebtoken
```

Install the Server AI Toolkit package:

```bash
npm install @tiptap/server-ai-toolkit
```

Install TypeScript types for jsonwebtoken:

```bash
npm install --save-dev @types/jsonwebtoken
```

## API endpoint

Create an API endpoint that uses the [Vercel AI SDK](https://ai-sdk.dev/) to call the OpenAI model. Get tool definitions from the Server AI Toolkit API and execute tools via the API.

### Environment variables

> **Set up authorization first:**
>
> Before creating the helper functions, set up your environment variables and authentication helpers
> (`createJwtToken` and `getAuthHeaders`) by following the [authorization
> guide](https://tiptap.dev/docs/content-ai/capabilities/server-ai-toolkit/install.md#set-up-authorization).

You also need an OpenAI API key and, if using Tiptap Cloud collaboration, a `TIPTAP_CLOUD_SECRET` for the Document Server. Add these to your `.env` file alongside the variables from the [authorization
guide](https://tiptap.dev/docs/content-ai/capabilities/server-ai-toolkit/install.md#set-up-authorization):

```sh
# .env (in addition to the Server AI Toolkit variables)
TIPTAP_CLOUD_SECRET=your-tiptap-cloud-document-server-secret
OPENAI_API_KEY=your-openai-key # The AI SDK will pick this up automatically
```

### Helper functions

Create several helper functions to support your endpoint and interact with the Server AI Toolkit:

#### Get tool definitions

This function fetches the prompt and available tool definitions from the Server AI Toolkit API.

```ts
// lib/server-ai-toolkit/get-tool-definitions.ts
import type z from 'zod'
import { getAuthHeaders } from './get-auth-headers'

/**
 * Gets tool definitions from the Server AI Toolkit API
 */
export async function getToolDefinitions(editorContext: unknown): Promise<{
  prompt: string
  tools: {
    name: string
    description: string
    inputSchema: z.core.JSONSchema.JSONSchema
  }[]
}> {
  const apiBaseUrl = process.env.TIPTAP_CLOUD_AI_API_URL || 'https://api.tiptap.dev'

  const response = await fetch(`${apiBaseUrl}/v3/ai/toolkit/tools`, {
    method: 'POST',
    headers: getAuthHeaders(),
    body: JSON.stringify({
      editorContext,
    }),
  })

  if (!response.ok) {
    throw new Error(`Failed to fetch tools: ${response.statusText}`)
  }
  const responseData = await response.json()

  return responseData
}
```

> **Using custom nodes?:**
>
> If your editor includes custom nodes or marks, configure them with `addJsonSchemaAwareness`. See
> the [custom nodes guide](https://tiptap.dev/docs/content-ai/capabilities/server-ai-toolkit/advanced-guides/custom-nodes.md).

#### Execute tool

This function executes a tool via the Server AI Toolkit API. It sends the tool name, input parameters, document ID, and editor context data to the API. The server automatically fetches and saves the Tiptap Cloud document using the credentials in the JWT.

```ts
// lib/server-ai-toolkit/execute-tool.ts
import { getAuthHeaders } from './get-auth-headers'

/**
 * Executes a tool via the Server AI Toolkit API
 */
export async function executeTool(
  toolName: string,
  input: unknown,
  documentId: string,
  editorContext: unknown,
): Promise<{ output: unknown; toolResult: unknown; docChanged: boolean; document: object | null }> {
  const apiBaseUrl = process.env.TIPTAP_CLOUD_AI_API_URL || 'https://api.tiptap.dev'

  const response = await fetch(`${apiBaseUrl}/v3/ai/toolkit/execute-tool`, {
    method: 'POST',
    headers: getAuthHeaders(),
    body: JSON.stringify({
      toolName,
      input,
      experimental_documentOptions: { documentId },
      editorContext,
    }),
  })

  if (!response.ok) {
    throw new Error(`Tool execution failed: ${response.statusText}`)
  }

  return response.json()
}
```

Now create the main API route:

```ts
// app/api/server-ai-agent-chatbot/route.ts
import { openai } from '@ai-sdk/openai'
import { createAgentUIStreamResponse, ToolLoopAgent, tool } from 'ai'
import z from 'zod'
import { executeTool } from '@/lib/server-ai-toolkit/execute-tool'
import { getToolDefinitions } from '@/lib/server-ai-toolkit/get-tool-definitions'

export async function POST(req: Request) {
  const {
    messages,
    editorContext,
    documentId,
  }: {
    messages: unknown[]
    editorContext: unknown
    documentId: string
  } = await req.json()

  // Get prompt and tool definitions from the Server AI Toolkit API
  const { prompt, tools: toolDefinitions } = await getToolDefinitions(editorContext)

  // Convert API tool definitions to AI SDK tool format
  const tools = Object.fromEntries(
    toolDefinitions.map((toolDef) => [
      toolDef.name,
      tool({
        description: toolDef.description,
        inputSchema: z.fromJSONSchema(toolDef.inputSchema),
        execute: async (input) => {
          try {
            const result = await executeTool(toolDef.name, input, documentId, editorContext)
            return result.output
          } catch (error) {
            console.error(`Failed to execute tool ${toolDef.name}:`, error)
            return {
              error: error instanceof Error ? error.message : 'Unknown error',
            }
          }
        },
      }),
    ]),
  )

  const agent = new ToolLoopAgent({
    model: openai('gpt-5.4-mini'),
    instructions: `You are an assistant that can edit rich text documents.
In your responses, be concise and to the point. However, the content you generate in the document does not need to be concise and to the point: instead, it should follow the user's request as closely as possible.
Before calling any tools, summarize you're going to do (in a sentence or less), as a high-level view of the task, as a human writer would describe it.
Rule: In your responses, do not provide any details of the tool calls.
Rule: In your responses, do not provide any details of the HTML content of the document.

${prompt}`,
    tools,
  })

  return createAgentUIStreamResponse({
    agent,
    uiMessages: messages,
  })
}
```

## Client-side setup

Create a client-side React component that renders the Tiptap Editor with collaboration support and a simple chat UI. This component leverages the [useChat hook](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) from the Vercel AI SDK to call the API endpoint and manage the chat conversation.

First, create a server function to generate a JWT for Tiptap Cloud collaboration. This function creates a secure JWT that authorizes the client to access a specific document in Tiptap Cloud's collaboration service.

```ts
// app/actions.ts
'use server'

import jwt from 'jsonwebtoken'

const TIPTAP_CLOUD_SECRET = process.env.TIPTAP_CLOUD_SECRET
const TIPTAP_CLOUD_DOCUMENT_SERVER_ID = process.env.TIPTAP_CLOUD_DOCUMENT_SERVER_ID

export async function getCollabConfig(
  userId: string,
  documentName: string,
): Promise<{ token: string; appId: string }> {
  if (!TIPTAP_CLOUD_SECRET) {
    throw new Error('TIPTAP_CLOUD_SECRET environment variable is not set')
  }

  if (!TIPTAP_CLOUD_DOCUMENT_SERVER_ID) {
    throw new Error('TIPTAP_CLOUD_DOCUMENT_SERVER_ID environment variable is not set')
  }

  const payload = {
    sub: userId,
    allowedDocumentNames: [documentName],
  }

  const token = jwt.sign(payload, TIPTAP_CLOUD_SECRET, { expiresIn: '1h' })

  return { token, appId: TIPTAP_CLOUD_DOCUMENT_SERVER_ID }
}
```

Now create the main page component:

```tsx
// app/page.tsx
'use client'

import { useChat } from '@ai-sdk/react'
import { Collaboration } from '@tiptap/extension-collaboration'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { TiptapCollabProvider } from '@tiptap-pro/provider'
import { ServerAiToolkit, getEditorContext } from '@tiptap/server-ai-toolkit'
import { DefaultChatTransport } from 'ai'
import { useEffect, useRef, useState } from 'react'
import { v4 as uuid } from 'uuid'
import * as Y from 'yjs'
import { getCollabConfig } from './actions'

export default function Page() {
  const [doc] = useState(() => new Y.Doc())
  const [documentId] = useState(() => `server-ai-agent-chatbot/${uuid()}`)
  const providerRef = useRef<TiptapCollabProvider | null>(null)

  const editor = useEditor({
    immediatelyRender: false,
    extensions: [StarterKit, Collaboration.configure({ document: doc }), ServerAiToolkit],
  })

  // Get the JWT and appId from server function
  useEffect(() => {
    const setupProvider = async () => {
      try {
        const { token, appId } = await getCollabConfig('user-1', documentId)

        const collabProvider = new TiptapCollabProvider({
          appId,
          name: documentId,
          token,
          document: doc,
          user: 'user-1',
          onOpen() {
            console.log('WebSocket connection opened.')
          },
          onConnect() {
            editor?.commands.setContent('Hello, world!')
          },
        })

        providerRef.current = collabProvider
      } catch (error) {
        console.error('Failed to setup collaboration:', error)
      }
    }

    setupProvider()

    return () => {
      if (providerRef.current) {
        providerRef.current.destroy()
        providerRef.current = null
      }
    }
  }, [documentId, doc, editor])

  const editorContext = editor ? getEditorContext(editor) : null

  const { messages, sendMessage } = useChat({
    transport: new DefaultChatTransport({
      api: '/api/server-ai-agent-chatbot',
      body: { documentId },
    }),
  })

  const [input, setInput] = useState('Replace the last paragraph with a short story about Tiptap')

  if (!editor) return null

  return (
    <div>
      <EditorContent editor={editor} />
      {messages?.map((message) => (
        <div key={message.id} style={{ whiteSpace: 'pre-wrap' }}>
          <strong>{message.role}</strong>
          <br />
          <div className="mt-2 whitespace-pre-wrap">
            {message.parts
              .filter((p) => p.type === 'text')
              .map((p) => p.text)
              .join('\n') || 'Loading...'}
          </div>
        </div>
      ))}
      <form
        onSubmit={(e) => {
          e.preventDefault()
          if (input.trim()) {
            sendMessage({ text: input }, { body: { editorContext } })
            setInput('')
          }
        }}
      >
        <input value={input} onChange={(e) => setInput(e.target.value)} />
        <button type="submit">Send</button>
      </form>
    </div>
  )
}
```

> **Document state management:**
>
> This implementation passes a `documentId` via `experimental_documentOptions` so the Server AI
> Toolkit automatically fetches and saves the document from Tiptap Cloud. The client uses Tiptap
> Collaboration to sync changes in real-time.

## End result

With additional CSS styles, the result is a simple but polished AI chatbot application that uses the Server AI Toolkit to edit documents:

> **Interactive demo:** [server ai agent chatbot](https://ai-toolkit-demos.vercel.app/server-ai-agent-chatbot)

See the [source code on GitHub](https://github.com/ueberdosis/ai-toolkit-demos).

## Alternative: provide the document directly

Instead of using Tiptap Cloud documents, you can provide and manage the document yourself by
passing a `document` field (instead of `experimental_documentOptions`) in the execute-tool request
body.

With this approach, you need to fetch the document before each tool execution and save back the
updated document when `docChanged` is `true`.

```ts
import { loadDocument, saveDocument } from './db'

const document = await loadDocument()
const response = await fetch(`${apiBaseUrl}/v3/ai/toolkit/execute-tool`, {
  method: 'POST',
  headers: getAuthHeaders(),
  body: JSON.stringify({
    toolName,
    input,
    experimental_documentOptions: { documentId },
    editorContext,
  }),
})

if (!response.ok) {
  throw new Error(`Tool execution failed: ${response.statusText}`)
}

const body = await response.json()

// Store the document if the tool execution modified the document
if (body.docChanged && body.document) {
  await saveDocument(body.document)
}
```

> **The tiptapRead tool can modify the document:**
>
> The `tiptapRead` tool can make edits to the document to prepare it for subsequent read operations.
> Therefore, if the `docChanged` property is `true`, you should update the document after executing
> the `tiptapRead` tool.

The approach of providing the document inline in the `document` field has several limitations. First, [Tiptap comments](https://tiptap.dev/docs/comments/getting-started/overview.md) are not supported, because they are stored in the Tiptap Cloud document. Additionally, changes cannot be associated with a certain user, and there is no built-in integration with [version history](https://tiptap.dev/docs/collaboration/documents/snapshot.md).

See the [REST API
reference](https://tiptap.dev/docs/content-ai/capabilities/server-ai-toolkit/api-reference/rest-api.md) for more information about the `document` field.

## Next steps

- Allow your AI to generate custom elements with the [custom nodes guide](https://tiptap.dev/docs/content-ai/capabilities/server-ai-toolkit/advanced-guides/custom-nodes.md)
- Learn more about the available tools in the [Agents section](https://tiptap.dev/docs/content-ai/capabilities/server-ai-toolkit/agents.md)
- Explore the [REST API reference](https://tiptap.dev/docs/content-ai/capabilities/server-ai-toolkit/api-reference/rest-api.md) for complete endpoint documentation
