Skip to content

Commit 5a5b117

Browse files
committed
feat(tool-widgets): componentForTool maps an MCP tool name to its component, every component decodes a compact result, both wrappers carry the helpers, and the committed spec and field labels refresh from the live API
1 parent b3058d8 commit 5a5b117

39 files changed

Lines changed: 5087 additions & 980 deletions

.github/workflows/release.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ jobs:
181181
# dist/ is gitignored: npm publish (above) already shipped it via the
182182
# package.json `files` allowlist. Commit only source, version bumps,
183183
# and the committed codegen (registry, manifest, docs tables).
184-
git add packages/ registry/ apps/docs/manifest.js README.md AGENTS.md
184+
# specs/ rides along so the committed spec and MCP tool list are the ones this codegen was built from; leaving them behind made the next hermetic CI run compare new bindings against an old spec.
185+
git add packages/ registry/ apps/docs/manifest.js README.md AGENTS.md specs/
185186
git commit -m "release: v$VERSION"
186187
# These two lines are INERT and the tag you see on the remote is not from here.
187188
# `git tag` makes a LIGHTWEIGHT tag and `git push --follow-tags` pushes only

AGENTS.md

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -441,17 +441,49 @@ In React, the same props are typed: `<RoxyNatalChart endpoint="astrology/natal-c
441441

442442
### Pattern 5: MCP tool-call response
443443

444-
A remote MCP server at `roxyapi.com/mcp/{domain}` exposes each RoxyAPI endpoint as an MCP tool. The JSON returned by the tool call has the same shape as the SDK response. Pass it straight into the matching component.
444+
A Remote MCP server at `roxyapi.com/mcp/{domain}` exposes each RoxyAPI endpoint as a tool, named for its method and path: `post_astrology_natal_chart`, `post_tarot_spreads_three_card`, `get_tarot_cards_id`. When your model calls one, the result is a single text content block holding the JSON string, and that JSON is the same shape the SDK returns. So the whole render is: parse it, look up the component, set `data`.
445+
446+
`componentForTool(name)` is the lookup, exported from `@roxyapi/ui`, `@roxyapi/ui-react` and `@roxyapi/ui-vue`. It returns `{ tag, pascal, attrs?, operationId, toolName }` for a tool the library draws, and `undefined` for one it does not. `operationId` names the OpenAPI operation the tool calls, for your logs or for finding the endpoint in the API reference. `attrs` values are always strings: set them as attributes in the DOM, spread them as props in React and Vue.
445447

446448
```ts
447-
// Pseudocode for any MCP-aware agent
448-
const result = await mcp.call('roxyapi.astrology.generate_natal_chart', {
449-
date: '1990-01-15', time: '14:30:00', latitude: 19.07, longitude: 72.88, timezone: 5.5,
450-
});
451-
document.querySelector('roxy-natal-chart').data = result;
449+
import { componentForTool } from '@roxyapi/ui';
450+
451+
// `toolName` and `result` are what your model handed back for one tool call.
452+
const found = componentForTool(toolName);
453+
if (found) {
454+
const el = document.createElement(found.tag);
455+
for (const [name, value] of Object.entries(found.attrs ?? {})) el.setAttribute(name, value);
456+
el.data = JSON.parse(result.content[0].text);
457+
container.append(el);
458+
}
452459
```
453460

454-
No field renames. No glue code. Use the decision tree above to pick the component for any tool.
461+
In React, `pascal` is the export name, so a namespace import renders it directly:
462+
463+
```tsx
464+
import * as RoxyUI from '@roxyapi/ui-react';
465+
import { componentForTool } from '@roxyapi/ui-react';
466+
467+
export function ToolWidget({ toolName, output }: { toolName: string; output: string }) {
468+
const found = componentForTool(toolName);
469+
if (!found) return null;
470+
const Component = RoxyUI[found.pascal as keyof typeof RoxyUI] as React.ComponentType<{ data: unknown }>;
471+
return <Component data={JSON.parse(output)} {...found.attrs} />;
472+
}
473+
```
474+
475+
Three things the lookup already handles, so you do not have to:
476+
477+
- **A compact result.** Ask a tool for the compact shape and its same-shaped arrays arrive columnar, as `{ "__cols": [names], "__rows": [[values]] }`. Every component decodes that on the way in and renders the same card either way. `expandCompact(value)` is exported too, for the paths that read the JSON before an element does.
478+
- **A server-prefixed name.** Some hosts prefix the tool name with the server it came from and a colon (`roxy_tarot:post_tarot_daily`). The lookup strips the prefix.
479+
- **Which component leads.** Three responses are rendered by two components each, and the lookup returns the one that leads with the drawing:
480+
- the natal chart response is drawn by both `<roxy-natal-chart>` and `<roxy-western-planets-table>`, the lookup returns `<roxy-natal-chart>`
481+
- the transit aspects response is drawn by both `<roxy-transit-wheel>` and `<roxy-aspects-table>`, the lookup returns `<roxy-transit-wheel>`
482+
- the Vedic birth chart response is drawn by both `<roxy-vedic-kundli>` and `<roxy-vedic-planets-table>`, the lookup returns `<roxy-vedic-kundli>`
483+
484+
Full recipe, including the vendor connectors and the Vercel AI SDK: <https://roxyapi.com/docs/tutorials/ai-chat-widgets>.
485+
486+
No field renames. No glue code. Use the decision tree above to pick the component for any tool the lookup does not cover.
455487

456488
### Pattern 6: Next.js RSC streaming
457489

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,40 @@ Always call `/location/search` first. Every chart endpoint expects latitude, lon
357357

358358
> **Timezone format.** RoxyAPI accepts both forms: a decimal-hour offset (`5.5` for IST, `-5` for EST) or an IANA name (`'Asia/Kolkata'`, `'America/New_York'`). Pick one and stay consistent. The decimal form is shorter and what `/location/search` returns; examples on this page use it. The IANA form is correct over DST boundaries when historical accuracy matters.
359359
360+
## Render an AI tool result
361+
362+
It ships inside `@roxyapi/ui`, `@roxyapi/ui-react` and `@roxyapi/ui-vue`, so whichever one you already installed has it and there is nothing else to add.
363+
364+
Your model calls a Remote MCP tool at `roxyapi.com/mcp/{domain}` and hands you back a tool name and a JSON string. `componentForTool(name)` turns that name into the component that draws it, so a chat answer shows a real tarot spread or a real chart instead of a wall of fields. It works in any chat UI that lets you render your own markup for a tool result.
365+
366+
```ts
367+
import { componentForTool } from '@roxyapi/ui';
368+
369+
const found = componentForTool(toolName);
370+
if (found) {
371+
const el = document.createElement(found.tag);
372+
for (const [name, value] of Object.entries(found.attrs ?? {})) el.setAttribute(name, value);
373+
el.data = JSON.parse(result.content[0].text);
374+
container.append(el);
375+
}
376+
```
377+
378+
In React, `pascal` is the export name, so a namespace import renders it directly:
379+
380+
```tsx
381+
import * as RoxyUI from '@roxyapi/ui-react';
382+
import { componentForTool } from '@roxyapi/ui-react';
383+
384+
export function ToolWidget({ toolName, output }: { toolName: string; output: string }) {
385+
const found = componentForTool(toolName);
386+
if (!found) return null;
387+
const Component = RoxyUI[found.pascal as keyof typeof RoxyUI] as React.ComponentType<{ data: unknown }>;
388+
return <Component data={JSON.parse(output)} {...found.attrs} />;
389+
}
390+
```
391+
392+
A compact tool result is decoded for you, and a name a host prefixed with its server (`roxy_tarot:post_tarot_daily`) resolves the same as a bare one. Full recipe, with the vendor connectors and the Vercel AI SDK: <https://roxyapi.com/docs/tutorials/ai-chat-widgets>. Runnable page: [examples/vanilla/tool-result.html](examples/vanilla/tool-result.html).
393+
360394
## Server-rendered, no JavaScript wiring
361395

362396
Server-rendered and cached pages (WordPress, JSX SSR, static HTML) cannot always run JavaScript to set the `data` property per element. Render the response into a child `<script type="application/json" class="roxy-data">` on the server instead. The component reads it on load. No per-element script, no API key in the browser.
@@ -992,7 +1026,7 @@ Persist the choice in `localStorage` from your own code; the components do not o
9921026
<details>
9931027
<summary><strong>How big is each component? What is the bundle cost?</strong></summary>
9941028

995-
Per-component bundles run 13-26 KB gzipped, capped at 30 KB by CI. The full bundle (every component, helpers, base styles, and the inlined design tokens) stays well under the 150 KB CI cap, around 108 KB gzipped today. The React and Vue packages load the runtime on mount, so a route that renders one chart pays for one component, not the whole catalog. Pin a concrete version in production for byte-stable cache hits.
1029+
Every component bundle is under 30 KB gzipped and the full bundle (every component, helpers, base styles, and the inlined design tokens) is under 150 KB gzipped. Both ceilings are enforced in CI on every build, measured on the compressed bytes a browser actually downloads, so a release cannot quietly grow past them. A route that renders one chart pays for one component, not the whole catalog. Pin a concrete version in production for byte-stable cache hits.
9961030
</details>
9971031

9981032
<details>

0 commit comments

Comments
 (0)