| name | tool-ui |
| description | Add custom UI components for Mastra tools that render in chat streaming
and history. Uses ToolUIRegistry pattern with type-safe component mapping.
Use when: creating tool UI cards, adding dynamic tool rendering, or
debugging why a tool component isn't showing in chat.
|
| user-invocable | true |
Tool UI Skill
Status: Production Ready
Last Updated: 2026-01-21
Dependencies: @/components/chat/renderers
Quick Start
To add a custom UI for a new tool:
/add-tool-ui my-tool-name
Or follow the manual steps below.
Step 1: Create the Component
Create a new card component in src/components/ai-elements/:
'use client';
import type { ComponentProps } from 'react';
import { cn } from '@/lib/utils';
export interface MyToolData {
result: string;
value: number;
}
interface MyToolCardProps extends ComponentProps<'div'> {
data: MyToolData;
}
export function MyToolCard({ data, className, ...props }: MyToolCardProps) {
return (
<div className={cn('not-prose mb-4 w-full rounded-md border p-4', className)} {...props}>
<span className="font-medium">{data.result}</span>
<span className="text-muted-foreground">{data.value}</span>
</div>
);
}
export function isMyToolData(data: unknown): data is MyToolData {
if (!data || typeof data !== 'object') return false;
const d = data as Record<string, unknown>;
return typeof d.result === 'string' && typeof d.value === 'number';
}
Critical: The type guard function is required for the registry to validate output data.
Step 2: Register in the Registry
Add registration to src/components/chat/renderers/index.ts:
import { MyToolCard, isMyToolData } from '@/components/ai-elements/my-tool-card';
toolUIRegistry.register({
toolIds: ['my-tool-id'],
Component: MyToolCard,
isValidOutput: isMyToolData,
});
Note: toolIds is an array - some tools have different IDs in different contexts
(e.g., ['get-weather', 'weatherTool']).
Step 3: Done
The component now automatically renders in:
- ✅ Streaming via
network-renderer.tsx
- ✅ History via
dynamic-tool-renderer.tsx
Registry API
interface ToolUIRegistration<TOutput = unknown> {
toolIds: string[];
Component: FC<{ data: TOutput }>;
isValidOutput: (output: unknown) => output is TOutput;
}
toolUIRegistry.register(registration);
toolUIRegistry.hasCustomUI('tool-id');
toolUIRegistry.getComponent('tool-id', output);
How It Works
flowchart TD
subgraph Registry["ToolUIRegistry"]
Map["Map<toolId, Registration>"]
end
subgraph Streaming["Streaming Context"]
NR["network-renderer.tsx"]
Extract["extractToolUIFromNetwork()"]
NR --> Extract --> Map
end
subgraph History["History Context"]
DTR["dynamic-tool-renderer.tsx"]
Lookup["getComponent()"]
DTR --> Lookup --> Map
end
Map --> Component["Your Component"]
Troubleshooting
Component not rendering during streaming
- Check the tool ID matches exactly what Mastra uses
- Verify the tool is in an agent that uses
networkRoute()
- Check
toolResults structure in network steps
Component not rendering in history
- Check
toolName in childMessages matches registered ID
- Verify
isValidOutput returns true for the data
- Check console for validation errors
Finding the correct tool ID
Look at your Mastra tool definition:
export const myTool = createTool({
id: 'my-tool-id',
});
Example: WeatherCard
Reference implementation at:
- Component:
src/components/ai-elements/weather-card.tsx
- Tool:
src/mastra/tools/weather-tool.ts
- Registration:
src/components/chat/renderers/index.ts