::: note
**TL;DR**
- A language model should never draw a chart. It should call a tool and hand the JSON to a component that already knows how to draw it. That is generative UI, also called tool UI, done with web components.
- Four lines do it on any surface: read the tool name and the result text, parse the text, map the name to a component, set `data`.
- Every RoxyAPI tool result is the same JSON the typed SDKs return, and every component in the open source UI library is a stateless `data` consumer, so nothing needs reshaping in between.
- Works in the Vercel AI SDK, assistant-ui, CopilotKit and anything hand rolled, and works the same whether your process calls the Remote MCP server or a model vendor calls it for you.
:::

Your astrology chatbot already has the chart. It called a tool, a full natal chart came back with every planet, house cusp and aspect in it, and then the chatbot did the one thing that throws all of that away: it passed the JSON to a language model and asked for a paragraph. What the user sees is a screen of prose. The wheel that the calculation exists to produce never reaches the screen at all.

There is a better shape, and it is not a better prompt. The model writes the sentence. A component draws the chart. This post is about the join between those two, the render step, which is one lookup from the tool name the model used to the component that already knows that payload. The setup half of this, choosing a model and connecting the servers, is covered in [why we ship no bundled AI chat endpoint](/blogs/ai-astrology-chat-endpoint-bring-your-own-llm "bring your own LLM, own your system prompt, model choice and memory"), and the components themselves are covered in the [UI component library post](/blogs/roxy-ui-astrology-vedic-tarot-web-components "open source astrology, kundli and tarot components for React, Vue and plain HTML"). Neither of them covers this join.

## Why a chatbot should never describe a birth chart in prose

A chart is a spatial object. Planets sit at degrees, in houses, in aspect to one another, and the meaning lives in those relationships. Prose is a lossy encoding of that geometry, so asking a model to narrate a full chart costs you twice: once in attention, once in accuracy.

The attention cost is the obvious one. A complete natal chart response carries planetary positions, house cusps, the aspect grid and interpretation text for all of it. Narrated, that is several screens. Readers do not read several screens. They scroll to the end, decide the answer was generic, and leave, which is exactly the outcome the calculation was supposed to prevent.

The accuracy cost is quieter and worse. Every number the model restates is a number it can restate wrong. Ascendant degrees, aspect orbs and dasha dates are values you already hold as exact figures in a response that was verified against NASA JPL Horizons. Passing them through a language model as prose turns a checked value into a generated one for no gain. See [how the calculations are verified](/methodology "ephemeris verification methodology and gold standard testing") for what that verification actually covers.

The same argument holds beyond Western astrology. A kundli has a fixed square or diamond layout that readers recognise on sight. A human design bodygraph is a fixed arrangement of centres and the channels that connect them. A tarot spread is positional, so the card in the past slot means something different from the same card in the outcome slot. In every one of those cases the shape carries meaning that a sentence cannot.

So split the job. Let the model do what it is good at, which is reading the payload, choosing what matters for this user and writing two paragraphs about it in the voice your product uses. Let a component do the geometry.

Ready to build this? The [Astrology API](/products/astrology-api "production ready astrology API with natal charts, transits and synastry") gives you the verified chart data and the components that render it, on one key. [See pricing](/pricing "RoxyAPI pricing tiers and request quotas").

## The four lines that render any RoxyAPI tool result

Find the tool name and the result text in whatever your framework hands you, `JSON.parse` the text, call `componentForTool(name)` for the component, and set `data` on it. That is the whole recipe, and it does not change between frameworks or model vendors.

It stays the same because two things hold everywhere. First, a RoxyAPI tool result is one text content block holding the same JSON the typed SDKs return, so there is exactly one place to look and no per tool parsing. Second, every component in the library is stateless: it fetches nothing, holds no client, needs no key, and renders whatever object you assign to `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` appears when one component covers several tools and needs an attribute to tell them apart. A tool that nothing renders returns `undefined`, which matters more than it sounds: a bot that also calls a reference lookup or a city search simply keeps answering in prose instead of throwing. You never maintain a list of which tools have a widget.

The tool names are the ones the server publishes, so you can read them yourself. `tools/list` needs no key, and the names that come back are literal strings such as `post_astrology_natal_chart`, `post_tarot_spreads_three_card` and `post_tarot_daily`.

::: tip
Leave `compact: true` on. The compact response shape sends each field name once for a repeated array instead of once per row, which is a real reduction in tokens per turn and therefore in the inference cost of running your own agent. Every component decodes that shape before rendering, so there is nothing to turn off and nothing to unpack first. If your own code wants the plain object, `expandCompact` is exported alongside `componentForTool`.
:::

## Rendering tool results in a Vercel AI SDK chatbot

When your own process holds the connection to the Remote MCP server, the tool result arrives directly in the message parts and there is no vendor envelope in the way. Connect the servers your agent needs, including the location server, so the model resolves a birthplace to coordinates itself rather than asking the user for latitude and longitude.

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

const astrology = await createMCPClient({
  transport: {
    type: 'http',
    url: 'https://roxyapi.com/mcp/astrology',
    headers: { Authorization: `Bearer ${process.env.ROXY_API_KEY}` },
  },
});

const location = await createMCPClient({
  transport: {
    type: 'http',
    url: 'https://roxyapi.com/mcp/location',
    headers: { Authorization: `Bearer ${process.env.ROXY_API_KEY}` },
  },
});

const tools = { ...(await astrology.tools()), ...(await location.tools()) };
```

Those tools reach `useChat` as `dynamic-tool` parts, each carrying `toolName`, `state` and `output`. One component renders every one of them, whatever domain it came from:

```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 that under the text parts of the assistant message and the reply arrives as a paragraph plus a real chart. Add one line to your system prompt while you are there: 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: one file maps the message parts to components, one file renders them above the reply, and every domain you enable renders itself, so cloning it gives you the widgets on day one. The [chat widgets tutorial](/docs/tutorials/ai-chat-widgets "render a RoxyAPI tool result as a chart in your own chat UI") walks the same pattern for other frameworks and for the vendor connectors. If you use [AI Elements](https://elements.ai-sdk.dev/components/tool), its output slot takes any node, so the same `<Widget>` goes there and you keep the collapsible tool header you already have.

One routing note while you are wiring this up. The domain servers above are for the deployed agent making real calls at run time. The coding assistant helping you write the integration connects the keyless docs server at `https://roxyapi.com/mcp/docs`, which returns the reference rather than live calculations.

## Where the tool name and the result live in each vendor connector

When a model vendor calls our Remote MCP server for you, the same two values come back inside the vendor response objects. The recipe does not change, only the two paths you read them from. The request configuration for each vendor is covered in the [bring your own LLM post](/blogs/ai-astrology-chat-endpoint-bring-your-own-llm "connect Claude, GPT or Gemini to a Remote MCP server"); what follows is only the read side.

::: tabs

### Anthropic

Messages API under the MCP connector beta `mcp-client-2025-11-20`, with a `mcp_toolset` entry in `tools` matching each server you declare. The name is on the `mcp_tool_use` block and the result is the first content block of `mcp_tool_result`, which 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. Every call becomes an `mcp_call` item in the output, and its `output` field is a string, not an object, so it is parsed the same way as the others.

```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. Two differences here, both handled below. The tool name arrives prefixed with the name you gave the server, and `componentForTool` strips that prefix for you, so pass the name through untouched. 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. This path was checked against a live call to our tarot server, which completed and returned the shape below.

```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).

:::

## Which chat UIs can host a component

Any chat UI that lets you return your own markup for a tool result can host these charts, because the render slot is ordinary JSX and the component is an ordinary element. The question to ask of a framework is narrow: does it hand me the tool result and let me decide what to draw with it.

- **Vercel AI SDK and AI Elements**: covered above. The tool parts are on the message, and the AI Elements output slot takes any node.
- **assistant-ui**: register the tool with `defineToolkit()` and mount it with `Tools({ toolkit })`. The `render` function receives the result, so it is the same four lines returning a widget.
- **CopilotKit**: `useRenderTool` for a named tool, or `useDefaultRenderTool` as the catch all, which is the one to reach for when the tools arrive from an MCP server and you do not want to name each one.
- **Plain React, Vue, Svelte, Angular, Solid or a hand rolled UI**: nothing to install beyond the component package, since you already control the markup.

One exception is 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 ChatKit can show RoxyAPI fields inside its own cards but cannot host these charts.

If you are not in a framework at all, the plain DOM version is the same four lines: create the element the binding names, copy any `attrs` across, assign the parsed object to `data`, and append it. The [UI components documentation](/docs/ui "astrology UI components for React, Vue and plain HTML with theming and localization") has that snippet alongside theming, section hiding and the language options.

## Where host rendered widgets are going

Everything above renders inside an app you control. The industry mechanism for rendering inside a chat client you do not own is the MCP Apps extension, which is Final at spec version 2026-01-26. A tool declares a UI resource, the host loads it in a sandboxed frame, pushes the tool result into it, and passes down its own theme so the widget matches the surrounding conversation.

Hosts that render server declared UI resources today include Claude, ChatGPT, VS Code and Cursor, with the [extension overview](https://modelcontextprotocol.io/extensions/apps/overview) and the [specification repository](https://github.com/modelcontextprotocol/ext-apps) carrying the current list and the message contract. That is a genuine shift in where a chart can appear: not only in the chat UI a developer builds, but in the general purpose assistant a user already has open.

The useful part for anyone building on this today is that the component layer does not change between those two worlds. A stateless element that takes a parsed tool result and renders a wheel is the same element whether your React app sets `data` on it or a host frame does. Build the render step once, against the tool name, and you are not rewriting it for the next surface.

## FAQ

**Can ChatGPT or Claude render these charts directly?**

Inside your own app, yes, today: your code receives the tool result and hands it to the component, and that works with Claude, GPT or Gemini as the model. Inside the ChatGPT or Claude interface itself, the industry mechanism is the MCP Apps extension, where the server declares a UI resource that the host loads in a sandboxed frame and fills with the tool result. Those hosts render server declared resources now, and the components described here are the same stateless components such a surface renders. A RoxyAPI connector inside those hosts answers in text today.

**Do I need React to render a tool result as a chart?**

No. The components are web components, so plain HTML works with a script tag and four lines of DOM code, and there are wrapper packages for React and Vue if you prefer typed props. React 19 assigns an object prop to a custom element as a property rather than an attribute, which is what lets a whole chart response through as `data` with no manual ref.

**How do I know which component renders a given tool?**

Call `componentForTool` with the tool name and read the binding it returns, which names the custom element and the React or Vue wrapper. A tool with no component returns `undefined`, so lookup tools and city searches fall through to prose without special casing. The full catalog with live previews is on the [components page](/ui "every RoxyAPI UI component with a live preview and copy paste code").

**Does the compact response shape break the widget?**

No, and you should keep it on. The compact shape sends each field name once for a repeated array instead of once per row, which lowers the tokens your agent spends per turn, and every component decodes it internally before rendering. Nothing in your code has to unpack it first.

**Where does the API key live when a chart renders in the browser?**

The secret key stays on the server. The model call and the MCP connection both belong in a server route or a server action, and only the parsed result crosses to the client, which is exactly what the component needs. If you want a widget that fetches for itself on a static page, use a publishable key with your origins allowlisted instead.

**Which MCP server should my coding assistant connect to?**

Point coding assistants such as GitHub Copilot, Claude Code and Cursor at the keyless docs server at `https://roxyapi.com/mcp/docs`, which returns the reference so the agent writes correct field names first try. The per domain servers are for the deployed agent making real calls at run time. The [MCP documentation](/docs/mcp "connect RoxyAPI Remote MCP to Claude Desktop, ChatGPT, Cursor and custom clients") covers both.

## Conclusion

The render step is the shortest part of an insight chatbot and the part most often skipped, which is why so many of them answer a birth chart question with a wall of text. Four lines fix it, and they stay four lines across every domain on one key, across the Remote MCP servers and the typed SDKs, and across the UI library and the copy paste widgets that share the same components.

Start from the [chat widgets tutorial](/docs/tutorials/ai-chat-widgets "render a RoxyAPI tool result as a chart in your own chat UI"), take the components from [the UI documentation](/docs/ui "astrology UI components for React, Vue and plain HTML"), wire the servers with the [MCP documentation](/docs/mcp "Remote MCP servers for astrology, Vedic, tarot and every other domain"), or clone one of the [open source templates](/starters "open source MIT astrology, tarot and Vedic app templates") and have the whole thing running against verified calculations this afternoon. [See pricing](/pricing "RoxyAPI pricing tiers and request quotas").