Integrate Toucan into your own AI chat
Target Audience: Developers building a custom AI chat experience. The Explorer MCP is available with the Grow plan.
Goal
Extend an AI chat you already own with Toucan's data-aware assistant. Your host LLM stays in charge of the conversation; when the user asks a data question, it calls a single Toucan tool and renders the response inline.
This is the alternative to Embed an AI Chat: instead of dropping a Toucan-branded chat into your product, you keep your own chat UI and brand and add Toucan as a capability behind it. This is the Explorer MCP. To build with Toucan from your own agent instead, see the Maker MCP, or which MCP do I need.
How it works
Toucan exposes an MCP endpoint at
https://toucanai.cloud/api/mcp. Your backend connects to it like any
other MCP server, attaches the discovered tools to your LLM call, and the
model decides when to invoke them.
┌──────────────┐ user msg ┌─────────────────┐
│ Your chat │ ──────────► │ Your backend │
│ (UI) │ ◄────────── │ + your LLM │
└──────────────┘ envelope └────────┬────────┘
│ MCP
▼
┌─────────────────┐
│ Toucan MCP │
│ (orchestrator, │
│ query, charts) │
└─────────────────┘Tool results are self-contained JSON envelopes with an answer field
(markdown the LLM relays verbatim) and a visualizations array (chart
payloads your UI renders with the <tc-result-renderer> web component). No
Toucan auth token is required on the browser side — visualizations carry
their own data.
Prerequisites
- At least one connected and active database.
- A valid API key for token generation.
- (Recommended): Metadata confirmed after AI analysis review to improve the AI assistant's accuracy.
- (Recommended): Row-Level Security (RLS) configured for multi-tenant data isolation.
- An LLM SDK on your backend that speaks MCP. The examples below use the
Vercel AI SDK (
@ai-sdk/mcp), but any MCP-capable client works.
Steps
1. Generate a Toucan embed token on your backend
The MCP endpoint is authenticated with the same embed tokens you use for dashboard or chat embedding. Mint a token server-side, scoped to the end user's attributes so RLS applies to every query the assistant runs. How you cache it (per request, per session, per browser tab) is up to your auth model — just keep it server-side.
See Generate a token via API for the full token-generation flow.
The token expires after one hour, and it is the only credential this
endpoint accepts. There is no sign-in flow (OAuth) and no long-lived key on
/api/mcp, so it cannot be added in one click as a remote server in a generic
client that stores one static credential (Dust, Claude Desktop, ChatGPT
connectors, …): the connection works until the token expires, then stops.
Your backend has to mint a fresh token per session, which is also what scopes
RLS to the end user rather than to whoever configured the integration. We are
working on making this easier to use from generic clients.
2. Connect to the Toucan MCP endpoint
From your chat backend (typically the route that proxies to your LLM), open
an MCP session, then pull the tools list and the tool-usage prompt
that ships the system-prompt addendum your LLM needs to relay Toucan's
answer verbatim:
import { createMCPClient } from "@ai-sdk/mcp";
const client = await createMCPClient({
transport: {
type: "http",
url: "https://toucanai.cloud/api/mcp",
headers: { Authorization: `Bearer ${embedToken}` },
},
});
const [tools, toolUsage] = await Promise.all([
client.tools(),
client.experimental_getPrompt({ name: "tool-usage" }),
]);
const systemAddendum = toolUsage.messages
.map((m) => (m.content.type === "text" ? m.content.text : ""))
.join(" ");
// Remember to call client.close() when the request finishes.Both the tool list and the prompt are discovered at runtime via MCP, so any new capabilities or guidance Toucan ships will surface to your LLM automatically without code changes on your side.
3. Attach the tool to your LLM call
Pass the tools to your existing chat completion and append systemAddendum
to your own system prompt. The addendum tells the model to forward Toucan's
natural-language answer instead of paraphrasing it; without it the model
tends to summarize.
import { streamText } from "ai";
const result = streamText({
model: yourModel,
system: `${YOUR_SYSTEM_PROMPT} ${systemAddendum}`,
messages,
tools,
onFinish: () => client.close(),
onError: () => client.close(),
});4. Render visualizations in your UI
Load the Toucan embed script once in your app's root layout so the
<tc-result-renderer> custom element registers itself globally:
<script type="module" src="https://toucanai.cloud/embed/embed.js"></script>That single script tag is the only browser-side dependency.
Then, in your message-rendering loop, find the Toucan tool result, parse its
text content as JSON, and assign each visualization to a
<tc-result-renderer>. The renderer takes its input via the payload DOM
property (not an attribute), so you assign it programmatically once the
element is in the DOM.
Add a small helper to recognise a Toucan envelope by its schema field, so any
other tools you've attached pass through untouched. The current wire-format tag
is "toucan-mcp-v1"; new major versions will publish a new tag and this page
will be updated.
function parseToucanEnvelope(text) {
try {
const parsed = JSON.parse(text);
if (parsed?.schema === "toucan-mcp-v1") return parsed;
} catch {}
return null;
}Vanilla JS / any framework
function renderToucanResult(toolResult, container) {
const envelope = parseToucanEnvelope(toolResult.content?.[0]?.text);
if (!envelope) return; // not a Toucan tool result: ignore
for (const viz of envelope.visualizations) {
const el = document.createElement("tc-result-renderer");
el.style.cssText = "display: block; height: 360px";
el.payload = viz;
container.appendChild(el);
}
}React example
// In your message-part loop, alongside your text branch:
if (part.type.startsWith("tool-") || part.type === "dynamic-tool") {
const envelope = parseToucanEnvelope(part.output?.[0]?.text);
if (!envelope) return null; // not a Toucan tool result: ignore
return envelope.visualizations.map((viz, i) => (
<tc-result-renderer
key={i}
ref={(el) => { if (el) el.payload = viz; }}
style={{ display: "block", height: 360 }}
/>
));
}Each visualization payload includes both the chart configuration and its data rows, so no further Toucan API calls or auth tokens are needed in the browser.
5. Test
Send a non-data message ("hi") and verify your existing chat behaves as before. Then ask a data question ("Top 5 customers by revenue this quarter") and verify the answer streams in along with one or more rendered charts underneath.
Response envelope reference
Every Toucan tool result is a single text content block whose text is JSON
of the following shape. You don't need to import this type — it's reproduced
here for reference; just JSON.parse the text and read the fields.
type ToucanMCPEnvelope = {
schema: "toucan-mcp-v1";
answer: string; // markdown — surface verbatim
visualizations: Visualization[]; // empty when no chart was produced
threadId: string; // pass back on follow-up questions to retain context
};
type Visualization =
| { type: "chart"; title: string; narrative?: string; config: ...; data: Row[] }
| { type: "error"; title?: string; message: string }
| { type: "text"; text: string };The schema field is the version tag; renderers should branch on it and
ignore unknown type values rather than throwing, so the contract stays
forward-compatible.
Troubleshooting
| Status | code | What it means |
|---|---|---|
| 401 | UNAUTHENTICATED | No Authorization: Bearer header, or the embed token is expired or invalid. Mint a new one. |
| 403 | NO_ACTIVE_SUBSCRIPTION | The token is valid, but the organization's trial or subscription has ended. |
Current scope and limits
- Conversational memory via
threadId. Every envelope includes athreadId. Pass it back in the nextask_toucan_assistantcall to continue the same conversation thread — the assistant will remember previous queries and charts. Omit it (or start fresh) to begin a new thread. The host LLM decides when to thread vs. start fresh; thetool-usagesystem-prompt addendum (step 2) already instructs it how. Threads are ephemeral: they expire after a period of inactivity. - Browser does not need a Toucan token. Only your backend authenticates; visualizations are self-contained.
- One-hour tokens, minted per session. See the warning in step 1: this endpoint takes embed tokens only, so it is meant to be called from a backend you own, not registered in an off-the-shelf MCP client.
- RLS still applies. Every query the assistant runs is scoped to the attributes you put on the embed token in step 1.
- Each question uses AI credits, like a question asked in the Toucan assistant. See Credits and usage.
- Charts need a renderer. Visualizations come as data for
<tc-result-renderer>. A client that does not render them, such as a generic chat app, shows only the textanswer. - English and UTC. The assistant answers in English and reads relative dates ("this quarter") in UTC. It does not take the user's language or time zone yet.
- One tool. The server exposes
ask_toucan_assistantonly: the host cannot list what data is available, it asks the question and Toucan decides. See the tools reference. - in-progress notifications. Our tools pushes notifications as MCP
notifications/progressmessage. Make sure your library handle them, and wire them into your existing status indicator, to see Toucan's real progress.
Conclusion
Your chat now answers data questions through Toucan without leaving the host UI. Users keep your branding and conversational experience while gaining self-service analytics over your governed data layer.