> Documentation index: https://roxyapi.com/llms.txt. Building an integration? Read https://roxyapi.com/AGENTS.md first.

# AI chat widgets that render tool results as charts

> Your chatbot already gets structured JSON back from every RoxyAPI tool call. Hand that JSON to the matching component and the answer arrives as a real natal chart wheel, kundli, or tarot spread instead of a wall of text. Time to ship: about 15 minutes.

An LLM should never draw a chart. It should call a tool and hand the result to something that already knows how to draw it. That is generative UI, also called tool UI: the model picks the tool, your interface picks the component. Every component in [`@roxyapi/ui`](/docs/ui "open source astrology UI components: natal chart, kundli, tarot spread, human design bodygraph") is a stateless `data` consumer, and a tool result is the same JSON the SDK returns, so the one piece your app supplies is the map from the tool NAME the model used to the component that renders it. `componentForTool()` is that map.

## What you get

- A chat reply that renders the wheel, the spread, or the bodygraph inline, next to the prose the model wrote.
- One component in your app, roughly 20 lines, that covers every tool your agent can call. No per-tool wiring, no list to maintain as you add domains.
- The same four lines whether the model is Claude, GPT, or Gemini, and whether you run the tool call yourself or let the vendor call our Remote MCP server for you.

## Prerequisites

1. A RoxyAPI key. [Get your API key](/account?tab=keys).
2. A chat app that already calls RoxyAPI tools. If you do not have one yet, the [AI chatbot tutorial](/docs/tutorials/ai-chatbot "build an AI astrology chatbot with Remote MCP and tool calling") builds it, and the [open source chatbot template](https://github.com/RoxyAPI/astrology-ai-chatbot) is the same thing ready to clone.
3. The component package for your stack: `npm install @roxyapi/ui-react`, `npm install @roxyapi/ui-vue`, or the CDN bundle for plain HTML.
4. React 19 if you are on React. It assigns an object prop to a custom element as a property, which is what lets a whole chart response through as `data`.

## The recipe

Four lines, and they are the same on every surface below:

1. Find the tool **name** and the result **text** in whatever your framework or model API hands you.
2. `JSON.parse` the text.
3. `componentForTool(name)` for the component.
4. Set `data`.

```ts
import { componentForTool } from '@roxyapi/ui';

const binding = componentForTool('post_astrology_natal_chart');
// { tag: 'roxy-natal-chart', pascal: 'RoxyNatalChart', operationId: 'generateNatalChart', toolName: 'post_astrology_natal_chart' }
```

`tag` is the custom element for plain DOM, `pascal` is the wrapper name in the React and Vue packages, and `attrs` is present when one component covers several tools and needs an attribute to tell them apart. A tool nothing renders returns `undefined`, so a bot that calls a reference lookup simply keeps answering in prose instead of breaking.

**Tip: Leave the compact response shape on. Every component decodes it, so you keep the lower inference cost per turn and still pass the result straight through. If you want the plain object for your own code, `expandCompact` is exported from all three packages.**

## Vercel AI SDK

Your process holds the connection, so you see the tool result directly. Connect the servers your agent needs:

```ts
import { createMCPClient } from '@ai-sdk/mcp';

const tarot = await createMCPClient({
  transport: {
    type: 'http',
    url: 'https://roxyapi.com/mcp/tarot',
    headers: { 'X-API-Key': process.env.ROXY_API_KEY! },
  },
});

const tools = await tarot.tools();
```

Those tools arrive in `useChat` as `dynamic-tool` parts, each carrying `toolName`, `state`, and `output`. One component renders every one of them:

```tsx
'use client';

import * as RoxyUI from '@roxyapi/ui-react';
import type { UIMessage } from 'ai';
import type { ElementType } from 'react';

export function ToolWidget({ message }: { message: UIMessage }) {
  return message.parts.map((part, i) => {
    if (part.type !== 'dynamic-tool' || part.state !== 'output-available') return null;

    const binding = RoxyUI.componentForTool(part.toolName);
    if (!binding) return null;

    const Widget = RoxyUI[binding.pascal as keyof typeof RoxyUI] as ElementType;
    const data = JSON.parse(part.output.content[0].text);

    return <Widget key={i} data={data} {...binding.attrs} />;
  });
}
```

Render it under the text parts of the assistant message and you are done. One line in your system prompt pays for itself here: tell the model that the app draws every chart, spread and table it receives beside the reply, so it refers to the drawing and interprets it instead of reprinting the positions the reader can already see. The [open source chatbot template](https://github.com/RoxyAPI/astrology-ai-chatbot) ships exactly this pattern: `src/lib/tool-widgets.ts` maps the message parts and `src/components/chat/ToolWidget.tsx` renders them above the reply, so cloning it gives you the widgets on day one, and both files are the reference for your own app.

Using [AI Elements](https://elements.ai-sdk.dev/components/tool)? Its `ToolOutput` slot takes any node, so pass the same `<Widget>` as the output and keep the collapsible tool header you already have.

**Which server does what:**
The per-domain servers above are for the deployed agent making real calls at run time. The coding agent helping you write this connects the keyless docs server at `https://roxyapi.com/mcp/docs` instead, which returns the reference rather than live calculations.

## Vendor-hosted connectors

When the model vendor calls our Remote MCP server for you, the tool name and the result come back inside their own response objects. The recipe does not change, only where you read the two values from.


### Anthropic

Messages API, MCP connector beta. Send `mcp_servers` plus a matching `mcp_toolset` entry in `tools`, then read the name off the `mcp_tool_use` block and the result off the `mcp_tool_result` block, whose first content block holds the JSON string.

```ts
const use = message.content.find((b) => b.type === 'mcp_tool_use');
const result = message.content.find((b) => b.type === 'mcp_tool_result');

const binding = componentForTool(use.name);          // post_tarot_spreads_three_card
const data = JSON.parse(result.content[0].text);
```

Reference: [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector).

### OpenAI

Responses API with `tools: [{ type: 'mcp', server_label, server_url, authorization }]`. Every call becomes an `mcp_call` item, and its `output` is the JSON string.

```ts
const call = response.output.find((item) => item.type === 'mcp_call');

const binding = componentForTool(call.name);         // post_astrology_natal_chart
const data = JSON.parse(call.output);
```

Reference: [remote MCP tool](https://developers.openai.com/api/docs/guides/tools-connectors-mcp).

### Gemini

Interactions API with `tools: [{ type: 'mcp_server', name, url, headers }]`, where the server name is snake_case. Two differences worth knowing, both handled below. The tool name arrives prefixed with your server name, and `componentForTool` accepts it as is. The result step carries the whole tool result as a JSON string, so you parse twice: once for the tool result, once for the text block inside it.

```ts
const call = steps.find((s) => s.type === 'mcp_server_tool_call');
const step = steps.find((s) => s.type === 'mcp_server_tool_result');

const binding = componentForTool(call.name);         // roxy_tarot:post_tarot_daily
const result = JSON.parse(step.result.call_tool_result_json);
const data = JSON.parse(result.content[0].text);
```

Reference: [remote MCP in the Interactions API](https://ai.google.dev/gemini-api/docs/function-calling).


## Other chat frameworks

Any chat UI that lets you return your own markup for a tool result can host these components, because the render slot is ordinary JSX and the component is an ordinary element.

- **assistant-ui**: register the tool in `defineToolkit()` and mount it with `Tools({ toolkit })`. The `render` function receives the result, so it is the four lines above returning `<Widget data={...} />`.
- **CopilotKit**: `useRenderTool` for a named tool, or `useDefaultRenderTool` as the catch-all, which is the one you want when the tools come from an MCP server and you do not want to name each.
- **AI Elements**: covered above, the output slot takes any node.
- **Plain React, Svelte, Angular, Solid, or a hand-rolled UI**: nothing to install beyond the component package. You already control the markup.

One exception worth knowing before you plan around it: OpenAI ChatKit renders widgets from a closed declarative schema of cards, lists, and a fixed set of nodes, with no slot for a custom component, so it can show RoxyAPI fields in its own cards but cannot host these charts.

## Vanilla and Vue


### Plain HTML

Load the bundle once, then create the element the binding names. No build step, no framework.

```html
<script src="https://cdn.jsdelivr.net/npm/@roxyapi/ui@latest/dist/cdn/roxy-ui.js" defer></script>
<div id="widgets"></div>

<script>
  function renderToolResult(toolName, resultText) {
    const binding = RoxyUI.componentForTool(toolName);
    if (!binding) return;

    const el = document.createElement(binding.tag);
    for (const [name, value] of Object.entries(binding.attrs ?? {})) el.setAttribute(name, value);
    el.data = JSON.parse(resultText);
    document.getElementById('widgets').append(el);
  }
</script>
```

Call `renderToolResult` from wherever your chat client receives a completed tool call.

### Vue

```vue
<script setup lang="ts">
import * as RoxyUI from '@roxyapi/ui-vue';

const props = defineProps<{ toolName: string; resultText: string }>();

const binding = RoxyUI.componentForTool(props.toolName);
const Widget = binding ? RoxyUI[binding.pascal as keyof typeof RoxyUI] : null;
const data = binding ? JSON.parse(props.resultText) : null;
</script>

<template>
  <component :is="Widget" v-if="Widget" :data="data" v-bind="binding?.attrs" />
</template>
```

In Nuxt, render this in a client context (`<ClientOnly>` or a `.client.vue` file), the same constraint as `'use client'` in the Next.js App Router.


## Gotchas


### Nothing renders and there is no error
`componentForTool` returned `undefined`, which is the correct answer for a tool no component covers. Log the name you passed and compare it with the [component catalog](/ui "every RoxyAPI UI component with a live preview and copy-paste code"). Reference and lookup tools deliberately have no chart.

### The chart is empty but the tool call succeeded
You passed the envelope instead of the result. A tool result is one text block holding a JSON string, so the value you want is `content[0].text` parsed, not the object that wraps it. In the Vercel AI SDK that is `part.output.content[0].text`, not `part.output`.

### The tool name has a prefix in front of it
Gemini prefixes the tool name with the name you gave the server, as in `roxy_tarot:post_tarot_daily`. `componentForTool` strips it, so pass the name through untouched rather than splitting it yourself.

### Do I have to turn the compact shape off
No, and you should not. Every component decodes it, so keep it on and keep the lower inference cost. `expandCompact` gives you the plain object if your own code needs to read the fields.

### `'use client'` errors in the Next.js App Router
The wrappers use the DOM. Add `'use client'` to any file that imports from `@roxyapi/ui-react`, and keep the fetch or the model call in the server half.

### The key ends up in the browser
Keep the secret key on the server. The model call and the MCP connection both belong in a server route or server action; only the parsed result crosses to the client. For a browser-side call, use a publishable key with your origins allowlisted, as described on [Copy-paste widgets](/docs/widgets#get-your-publishable-key).

## What to build next

- The [UI components page](/docs/ui "astrology UI components for React, Vue, and plain HTML with theming and localization") covers theming, hiding blocks, localization, and every component you can render.
- A coding agent wiring this up reads the UI agent guide, which maps every tool name to its component: https://raw.githubusercontent.com/RoxyAPI/ui/main/AGENTS.md, raw markdown to fetch whole with `curl -s`.
- The [MCP setup docs](/docs/mcp "connect RoxyAPI Remote MCP to Claude Desktop, ChatGPT, Cursor, and custom clients") cover connecting the servers from every client.
- The [AI chatbot tutorial](/docs/tutorials/ai-chatbot "build a multi-domain AI astrology chatbot with Remote MCP") builds the chat half this page renders.
- The [templates](/templates "open source MIT astrology and tarot app templates") are clone-and-ship apps you can drop this component straight into.
