@lantos1618/better-ui 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +410 -207
- package/dist/agui/index.d.mts +116 -0
- package/dist/agui/index.d.ts +116 -0
- package/dist/agui/index.js +328 -0
- package/dist/agui/index.mjs +156 -0
- package/dist/auth/index.mjs +1 -1
- package/dist/chunk-CIESM3BP.mjs +33 -0
- package/dist/chunk-XDWHMRBN.mjs +159 -0
- package/dist/components/index.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/mcp/index.d.mts +157 -0
- package/dist/mcp/index.d.ts +157 -0
- package/dist/mcp/index.js +491 -0
- package/dist/mcp/index.mjs +332 -0
- package/dist/persistence/index.mjs +1 -1
- package/dist/react/index.mjs +1 -1
- package/package.json +17 -3
- package/dist/chunk-Y6FXYEAI.mjs +0 -10
package/README.md
CHANGED
|
@@ -1,29 +1,18 @@
|
|
|
1
1
|
# Better UI
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Define once. Render in UI. Serve over MCP. Type-safe AI tools with views.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@lantos1618/better-ui)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## The Problem
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Every AI framework lets you define tools. None of them let the tool own its own UI. You end up with tool definitions in one place, rendering logic scattered somewhere else, and no way to expose those same tools to external AI clients.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
- **View integration** - tools render their own results in UI (no other framework does this)
|
|
15
|
-
- **Multi-provider support** - OpenAI, Anthropic, Google Gemini, OpenRouter
|
|
16
|
-
- **Streaming tool views** - progressive partial data rendering
|
|
17
|
-
- **Pre-made chat components** - drop-in `<Chat />` with automatic tool view rendering
|
|
18
|
-
- **Server infrastructure** - rate limiting, caching, Next.js/Express adapters
|
|
13
|
+
## The Solution
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npm install @lantos1618/better-ui zod
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Quick Start
|
|
15
|
+
Better UI tools are self-contained units: **schema + server logic + view + streaming**, all in one definition. Use them in chat, call them from React, or expose them as an MCP server — same tool, zero glue code.
|
|
27
16
|
|
|
28
17
|
```typescript
|
|
29
18
|
import { tool } from '@lantos1618/better-ui';
|
|
@@ -36,82 +25,97 @@ const weather = tool({
|
|
|
36
25
|
output: z.object({ temp: z.number(), condition: z.string() }),
|
|
37
26
|
});
|
|
38
27
|
|
|
39
|
-
// Server implementation (runs on server)
|
|
40
28
|
weather.server(async ({ city }) => {
|
|
41
29
|
const data = await weatherAPI.get(city);
|
|
42
30
|
return { temp: data.temp, condition: data.condition };
|
|
43
31
|
});
|
|
44
32
|
|
|
45
|
-
// View for rendering results (our differentiator!)
|
|
46
33
|
weather.view((data) => (
|
|
47
34
|
<div className="weather-card">
|
|
48
|
-
<span>{data.temp}
|
|
35
|
+
<span>{data.temp}°</span>
|
|
49
36
|
<span>{data.condition}</span>
|
|
50
37
|
</div>
|
|
51
38
|
));
|
|
52
39
|
```
|
|
53
40
|
|
|
54
|
-
|
|
41
|
+
That's it. The tool validates input/output with Zod, runs server logic securely, and renders its own results. Drop it into chat and it just works. Expose it over MCP and Claude Desktop can call it.
|
|
55
42
|
|
|
56
|
-
|
|
57
|
-
import { Chat } from '@lantos1618/better-ui/components';
|
|
43
|
+
## Install
|
|
58
44
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<Chat
|
|
62
|
-
endpoint="/api/chat"
|
|
63
|
-
tools={{ weather, search, counter }}
|
|
64
|
-
className="h-[600px]"
|
|
65
|
-
placeholder="Ask something..."
|
|
66
|
-
/>
|
|
67
|
-
);
|
|
68
|
-
}
|
|
45
|
+
```bash
|
|
46
|
+
npm install @lantos1618/better-ui zod
|
|
69
47
|
```
|
|
70
48
|
|
|
71
|
-
|
|
49
|
+
## What You Get
|
|
72
50
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
```
|
|
51
|
+
| Feature | What |
|
|
52
|
+
|---------|------|
|
|
53
|
+
| **View integration** | Tools render their own results — no other framework does this |
|
|
54
|
+
| **MCP server** | Expose any tool registry to Claude Desktop, Cursor, VS Code |
|
|
55
|
+
| **AG-UI protocol** | Compatible with CopilotKit, LangChain, Google ADK frontends |
|
|
56
|
+
| **Multi-provider** | OpenAI, Anthropic, Google Gemini, OpenRouter |
|
|
57
|
+
| **Streaming views** | Progressive partial data rendering |
|
|
58
|
+
| **Drop-in chat** | `<Chat />` component with automatic tool view rendering |
|
|
59
|
+
| **HITL confirmation** | Tools can require human approval before executing |
|
|
60
|
+
| **Auth helpers** | JWT, session cookies, BetterAuth integration |
|
|
61
|
+
| **Security** | Context stripping, input validation, output sanitization, rate limiting |
|
|
87
62
|
|
|
88
|
-
|
|
63
|
+
## Quick Start
|
|
89
64
|
|
|
90
|
-
|
|
65
|
+
### 1. Define a Tool
|
|
91
66
|
|
|
92
67
|
```typescript
|
|
93
|
-
import {
|
|
68
|
+
import { tool } from '@lantos1618/better-ui';
|
|
69
|
+
import { z } from 'zod';
|
|
94
70
|
|
|
95
|
-
|
|
96
|
-
|
|
71
|
+
export const search = tool({
|
|
72
|
+
name: 'search',
|
|
73
|
+
description: 'Search the web',
|
|
74
|
+
input: z.object({ query: z.string().max(1000) }),
|
|
75
|
+
output: z.object({
|
|
76
|
+
results: z.array(z.object({
|
|
77
|
+
title: z.string(),
|
|
78
|
+
url: z.string(),
|
|
79
|
+
})),
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
97
82
|
|
|
98
|
-
|
|
99
|
-
const
|
|
83
|
+
search.server(async ({ query }) => {
|
|
84
|
+
const results = await searchAPI.search(query);
|
|
85
|
+
return { results };
|
|
86
|
+
});
|
|
100
87
|
|
|
101
|
-
|
|
102
|
-
|
|
88
|
+
search.view((data) => (
|
|
89
|
+
<ul>
|
|
90
|
+
{data.results.map((r, i) => (
|
|
91
|
+
<li key={i}><a href={r.url}>{r.title}</a></li>
|
|
92
|
+
))}
|
|
93
|
+
</ul>
|
|
94
|
+
));
|
|
95
|
+
```
|
|
103
96
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
97
|
+
### 2. Use It in Chat
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { Chat } from '@lantos1618/better-ui/components';
|
|
101
|
+
|
|
102
|
+
function App() {
|
|
103
|
+
return (
|
|
104
|
+
<Chat
|
|
105
|
+
endpoint="/api/chat"
|
|
106
|
+
tools={{ weather, search }}
|
|
107
|
+
className="h-[600px]"
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
110
111
|
```
|
|
111
112
|
|
|
112
|
-
|
|
113
|
+
Tool results render automatically using the tool's `.view()` component.
|
|
114
|
+
|
|
115
|
+
### 3. Wire Up the API Route
|
|
113
116
|
|
|
114
117
|
```typescript
|
|
118
|
+
// app/api/chat/route.ts (Next.js)
|
|
115
119
|
import { streamText, convertToModelMessages } from 'ai';
|
|
116
120
|
import { createProvider } from '@lantos1618/better-ui';
|
|
117
121
|
|
|
@@ -124,249 +128,448 @@ export async function POST(req: Request) {
|
|
|
124
128
|
messages: convertToModelMessages(messages),
|
|
125
129
|
tools: {
|
|
126
130
|
weather: weatherTool.toAITool(),
|
|
131
|
+
search: searchTool.toAITool(),
|
|
127
132
|
},
|
|
128
133
|
});
|
|
129
134
|
return result.toUIMessageStreamResponse();
|
|
130
135
|
}
|
|
131
136
|
```
|
|
132
137
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
Tools can stream partial results progressively:
|
|
138
|
+
### 4. Or Expose as an MCP Server
|
|
136
139
|
|
|
137
140
|
```typescript
|
|
138
|
-
import {
|
|
141
|
+
import { createMCPServer } from '@lantos1618/better-ui/mcp';
|
|
139
142
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
output: z.object({ summary: z.string(), score: z.number() }),
|
|
143
|
+
const server = createMCPServer({
|
|
144
|
+
name: 'my-tools',
|
|
145
|
+
version: '1.0.0',
|
|
146
|
+
tools: { weather, search },
|
|
145
147
|
});
|
|
146
148
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const result = await analyzeData(query);
|
|
150
|
-
stream({ summary: result.summary }); // More data
|
|
151
|
-
return { summary: result.summary, score: result.score }; // Final
|
|
152
|
-
});
|
|
149
|
+
server.start(); // stdio transport — works with Claude Desktop
|
|
150
|
+
```
|
|
153
151
|
|
|
154
|
-
|
|
155
|
-
function AnalysisWidget({ query }) {
|
|
156
|
-
const { data, streaming, loading, execute } = useToolStream(analysis);
|
|
152
|
+
Add to Claude Desktop config (`~/.claude/claude_desktop_config.json`):
|
|
157
153
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"mcpServers": {
|
|
157
|
+
"my-tools": {
|
|
158
|
+
"command": "npx",
|
|
159
|
+
"args": ["tsx", "path/to/mcp-server.ts"]
|
|
160
|
+
}
|
|
161
|
+
}
|
|
166
162
|
}
|
|
167
163
|
```
|
|
168
164
|
|
|
169
|
-
|
|
165
|
+
Or use the HTTP handler for web-based MCP clients:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// app/api/mcp/route.ts
|
|
169
|
+
export const POST = server.httpHandler();
|
|
170
|
+
```
|
|
170
171
|
|
|
171
|
-
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Tool API
|
|
175
|
+
|
|
176
|
+
### Object Config
|
|
172
177
|
|
|
173
178
|
```typescript
|
|
174
179
|
const myTool = tool({
|
|
175
180
|
name: 'myTool',
|
|
176
181
|
description: 'What this tool does',
|
|
177
|
-
input: z.object({
|
|
178
|
-
output: z.object({
|
|
179
|
-
tags: ['
|
|
180
|
-
cache: { ttl: 60000 },
|
|
182
|
+
input: z.object({ query: z.string() }),
|
|
183
|
+
output: z.object({ results: z.array(z.string()) }),
|
|
184
|
+
tags: ['search'],
|
|
185
|
+
cache: { ttl: 60000 },
|
|
186
|
+
confirm: true, // require HITL confirmation
|
|
187
|
+
hints: { destructive: true }, // behavioral metadata
|
|
188
|
+
autoRespond: true, // auto-send state back to AI after user action
|
|
189
|
+
groupKey: (input) => input.query, // collapse related calls in thread
|
|
181
190
|
});
|
|
182
191
|
```
|
|
183
192
|
|
|
184
|
-
###
|
|
185
|
-
|
|
186
|
-
The `.server()` method defines logic that runs on the server (API routes, server components):
|
|
193
|
+
### Fluent Builder
|
|
187
194
|
|
|
188
195
|
```typescript
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
})
|
|
196
|
+
const search = tool('search')
|
|
197
|
+
.description('Search the database')
|
|
198
|
+
.input(z.object({ query: z.string() }))
|
|
199
|
+
.output(z.object({ results: z.array(z.string()) }))
|
|
200
|
+
.server(async ({ query }) => ({ results: await db.search(query) }))
|
|
201
|
+
.view((data) => <ResultsList items={data.results} />)
|
|
202
|
+
.build();
|
|
194
203
|
```
|
|
195
204
|
|
|
196
|
-
###
|
|
197
|
-
|
|
198
|
-
The `.client()` method defines what happens when called from the browser. If not specified, auto-fetches to `/api/tools/execute`.
|
|
205
|
+
### Handlers
|
|
199
206
|
|
|
200
207
|
```typescript
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return ctx.fetch('/api/search', {
|
|
206
|
-
method: 'POST',
|
|
207
|
-
body: JSON.stringify({ query })
|
|
208
|
-
});
|
|
208
|
+
// Server — runs in API routes, never on client
|
|
209
|
+
myTool.server(async (input, ctx) => {
|
|
210
|
+
// ctx.env, ctx.headers, ctx.cookies, ctx.user, ctx.session
|
|
211
|
+
return await db.query(input.query);
|
|
209
212
|
});
|
|
210
|
-
```
|
|
211
213
|
|
|
212
|
-
|
|
214
|
+
// Client — runs in browser. Auto-fetches to /api/tools/execute if not defined
|
|
215
|
+
myTool.client(async (input, ctx) => {
|
|
216
|
+
return ctx.fetch('/api/search', { method: 'POST', body: JSON.stringify(input) });
|
|
217
|
+
});
|
|
213
218
|
|
|
214
|
-
|
|
219
|
+
// Stream — progressive partial updates
|
|
220
|
+
myTool.stream(async (input, { stream }) => {
|
|
221
|
+
stream({ status: 'searching...' });
|
|
222
|
+
const results = await search(input.query);
|
|
223
|
+
stream({ results, status: 'done' });
|
|
224
|
+
return { results, status: 'done', count: results.length };
|
|
225
|
+
});
|
|
215
226
|
|
|
216
|
-
|
|
217
|
-
myTool.view((data, { loading, error, streaming }) => {
|
|
227
|
+
// View — render results (the differentiator)
|
|
228
|
+
myTool.view((data, { loading, error, streaming, onAction }) => {
|
|
218
229
|
if (loading) return <Spinner />;
|
|
219
|
-
if (error) return <
|
|
230
|
+
if (error) return <ErrorCard message={error.message} />;
|
|
220
231
|
if (streaming) return <PartialResults data={data} />;
|
|
221
232
|
return <Results items={data.results} />;
|
|
222
233
|
});
|
|
223
234
|
```
|
|
224
235
|
|
|
225
|
-
###
|
|
226
|
-
|
|
227
|
-
The `.stream()` method enables progressive partial updates:
|
|
236
|
+
### Execution
|
|
228
237
|
|
|
229
238
|
```typescript
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const results = await search(query);
|
|
233
|
-
stream({ results, status: 'done' });
|
|
234
|
-
return { results, status: 'done', count: results.length };
|
|
235
|
-
});
|
|
236
|
-
```
|
|
239
|
+
// Server-side
|
|
240
|
+
const result = await myTool.run(input, { isServer: true });
|
|
237
241
|
|
|
238
|
-
|
|
242
|
+
// Client-side (auto-fetches if no .client() defined)
|
|
243
|
+
const result = await myTool.run(input, { isServer: false });
|
|
239
244
|
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const results = await db.search(query);
|
|
249
|
-
return { results };
|
|
250
|
-
})
|
|
251
|
-
.view((data) => <ResultsList items={data.results} />);
|
|
245
|
+
// Streaming
|
|
246
|
+
for await (const { partial, done } of myTool.runStream(input)) {
|
|
247
|
+
console.log(partial); // progressive updates
|
|
248
|
+
if (done) break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// AI SDK integration
|
|
252
|
+
const aiTool = myTool.toAITool(); // { description, inputSchema, execute }
|
|
252
253
|
```
|
|
253
254
|
|
|
255
|
+
---
|
|
256
|
+
|
|
254
257
|
## React Hooks
|
|
255
258
|
|
|
256
|
-
|
|
259
|
+
```typescript
|
|
260
|
+
import { useTool, useTools, useToolStream } from '@lantos1618/better-ui/react';
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### `useTool`
|
|
257
264
|
|
|
258
265
|
```typescript
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const {
|
|
262
|
-
data, // Result data
|
|
263
|
-
loading, // Loading state
|
|
264
|
-
error, // Error if any
|
|
265
|
-
execute, // Execute function
|
|
266
|
-
reset, // Reset state
|
|
267
|
-
executed, // Has been executed
|
|
268
|
-
} = useTool(myTool, initialInput, {
|
|
266
|
+
const { data, loading, error, execute, reset, executed } = useTool(myTool, initialInput, {
|
|
269
267
|
auto: false,
|
|
270
268
|
onSuccess: (data) => {},
|
|
271
269
|
onError: (error) => {},
|
|
272
270
|
});
|
|
273
271
|
```
|
|
274
272
|
|
|
275
|
-
### `useToolStream
|
|
273
|
+
### `useToolStream`
|
|
276
274
|
|
|
277
275
|
```typescript
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const {
|
|
281
|
-
data, // Progressive partial data
|
|
282
|
-
finalData, // Complete validated data (when done)
|
|
283
|
-
streaming, // True while receiving partial updates
|
|
284
|
-
loading, // True before first chunk
|
|
285
|
-
error,
|
|
286
|
-
execute,
|
|
287
|
-
reset,
|
|
288
|
-
} = useToolStream(myTool);
|
|
276
|
+
const { data, finalData, streaming, loading, error, execute, reset } = useToolStream(myTool);
|
|
289
277
|
```
|
|
290
278
|
|
|
291
|
-
### `useTools
|
|
279
|
+
### `useTools`
|
|
292
280
|
|
|
293
281
|
```typescript
|
|
294
|
-
import { useTools } from '@lantos1618/better-ui';
|
|
295
|
-
|
|
296
282
|
const tools = useTools({ weather, search });
|
|
297
283
|
|
|
298
284
|
await tools.weather.execute({ city: 'London' });
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
tools.weather.data; // Weather result
|
|
302
|
-
tools.search.loading; // Search loading state
|
|
285
|
+
tools.weather.data; // result
|
|
286
|
+
tools.search.loading; // loading state
|
|
303
287
|
```
|
|
304
288
|
|
|
289
|
+
---
|
|
290
|
+
|
|
305
291
|
## Chat Components
|
|
306
292
|
|
|
307
|
-
|
|
293
|
+
```typescript
|
|
294
|
+
import { Chat, ChatProvider, Thread, Composer, Message, ToolResult } from '@lantos1618/better-ui/components';
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Drop-in
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
<Chat endpoint="/api/chat" tools={{ weather, search }} className="h-[600px]" />
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Composable
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
<ChatProvider endpoint="/api/chat" tools={tools}>
|
|
307
|
+
<div className="flex flex-col h-screen">
|
|
308
|
+
<Thread className="flex-1 overflow-y-auto" />
|
|
309
|
+
<Composer placeholder="Type a message..." />
|
|
310
|
+
</div>
|
|
311
|
+
</ChatProvider>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### All Components
|
|
308
315
|
|
|
309
316
|
| Component | Description |
|
|
310
317
|
|-----------|-------------|
|
|
311
|
-
| `Chat` | All-in-one
|
|
318
|
+
| `Chat` | All-in-one (ChatProvider + Thread + Composer) |
|
|
312
319
|
| `ChatProvider` | Context provider wrapping AI SDK's `useChat` |
|
|
313
320
|
| `Thread` | Message list with auto-scroll |
|
|
314
|
-
| `Message` | Single message with
|
|
321
|
+
| `Message` | Single message with tool view rendering |
|
|
315
322
|
| `Composer` | Input form with send button |
|
|
316
|
-
| `ToolResult` | Renders a tool's `.
|
|
323
|
+
| `ToolResult` | Renders a tool's `.view()` in chat context |
|
|
324
|
+
| `Panel` / `ChatPanel` | Sidebar panel for thread management |
|
|
325
|
+
| `Markdown` | Markdown renderer with syntax highlighting |
|
|
326
|
+
| `ThemeProvider` | Theme CSS variable provider |
|
|
327
|
+
|
|
328
|
+
### View Building Blocks
|
|
329
|
+
|
|
330
|
+
Pre-made view components for common patterns:
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import {
|
|
334
|
+
QuestionView, // Multiple choice / free-text questions
|
|
335
|
+
FormView, // Dynamic forms
|
|
336
|
+
DataTableView, // Sortable data tables
|
|
337
|
+
ProgressView, // Step-by-step progress
|
|
338
|
+
MediaDisplayView,// Image/video display
|
|
339
|
+
CodeBlockView, // Syntax-highlighted code
|
|
340
|
+
FileUploadView, // File upload UI
|
|
341
|
+
} from '@lantos1618/better-ui/components';
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## MCP Server
|
|
347
|
+
|
|
348
|
+
Turn any tool registry into an [MCP](https://modelcontextprotocol.io) server. Zero dependencies beyond Better UI itself.
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import { createMCPServer } from '@lantos1618/better-ui/mcp';
|
|
352
|
+
|
|
353
|
+
const server = createMCPServer({
|
|
354
|
+
name: 'my-app',
|
|
355
|
+
version: '1.0.0',
|
|
356
|
+
tools: { weather, search, calculator },
|
|
357
|
+
context: { env: process.env }, // passed to every tool execution
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Transports
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
// stdio — for Claude Desktop, Cursor, VS Code extensions
|
|
365
|
+
server.start();
|
|
366
|
+
|
|
367
|
+
// HTTP — for Next.js, Express, Cloudflare Workers, Deno
|
|
368
|
+
const handler = server.httpHandler();
|
|
369
|
+
// Use as: export const POST = handler;
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Programmatic Use
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
// List tools with JSON schemas
|
|
376
|
+
const tools = server.listTools();
|
|
377
|
+
|
|
378
|
+
// Call a tool directly
|
|
379
|
+
const result = await server.callTool('weather', { city: 'Tokyo' });
|
|
380
|
+
// → { content: [{ type: 'text', text: '{"temp":21,"city":"Tokyo","condition":"sunny"}' }] }
|
|
381
|
+
|
|
382
|
+
// Handle raw JSON-RPC messages
|
|
383
|
+
const response = await server.handleMessage({
|
|
384
|
+
jsonrpc: '2.0',
|
|
385
|
+
id: 1,
|
|
386
|
+
method: 'tools/call',
|
|
387
|
+
params: { name: 'weather', arguments: { city: 'Tokyo' } },
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Schema Conversion
|
|
392
|
+
|
|
393
|
+
The built-in `zodToJsonSchema` converter handles common Zod types without extra dependencies:
|
|
317
394
|
|
|
318
|
-
|
|
395
|
+
```typescript
|
|
396
|
+
import { zodToJsonSchema } from '@lantos1618/better-ui/mcp';
|
|
397
|
+
|
|
398
|
+
zodToJsonSchema(z.object({
|
|
399
|
+
name: z.string().min(1).max(100),
|
|
400
|
+
age: z.number().int().min(0),
|
|
401
|
+
role: z.enum(['admin', 'user']),
|
|
402
|
+
}));
|
|
403
|
+
// → { type: 'object', properties: { name: { type: 'string', ... }, ... }, required: [...] }
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## AG-UI Protocol
|
|
409
|
+
|
|
410
|
+
Expose your tools via the [AG-UI (Agent-User Interaction Protocol)](https://docs.ag-ui.com) — compatible with CopilotKit, LangChain, Google ADK, and any AG-UI frontend.
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
import { createAGUIServer } from '@lantos1618/better-ui/agui';
|
|
414
|
+
|
|
415
|
+
const server = createAGUIServer({
|
|
416
|
+
name: 'my-tools',
|
|
417
|
+
tools: { weather, search },
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Next.js route handler — returns SSE event stream
|
|
421
|
+
export const POST = server.handler();
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
The handler emits standard AG-UI events (`RUN_STARTED`, `TOOL_CALL_START`, `TOOL_CALL_ARGS`, `TOOL_CALL_RESULT`, `TOOL_CALL_END`, `RUN_FINISHED`) over Server-Sent Events.
|
|
425
|
+
|
|
426
|
+
---
|
|
319
427
|
|
|
320
428
|
## Providers
|
|
321
429
|
|
|
322
|
-
|
|
323
|
-
|
|
430
|
+
```typescript
|
|
431
|
+
import { createProvider } from '@lantos1618/better-ui';
|
|
432
|
+
|
|
433
|
+
createProvider({ provider: 'openai', model: 'gpt-4o' });
|
|
434
|
+
createProvider({ provider: 'anthropic', model: 'claude-4-sonnet' });
|
|
435
|
+
createProvider({ provider: 'google', model: 'gemini-2.5-pro' });
|
|
436
|
+
createProvider({ provider: 'openrouter', model: 'anthropic/claude-4-sonnet', apiKey: '...' });
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
| Provider | Package | Example Models |
|
|
440
|
+
|----------|---------|----------------|
|
|
324
441
|
| OpenAI | `@ai-sdk/openai` (included) | `gpt-4o`, `gpt-5.2` |
|
|
325
|
-
| Anthropic | `@ai-sdk/anthropic` (optional) | `claude-4-sonnet` |
|
|
442
|
+
| Anthropic | `@ai-sdk/anthropic` (optional) | `claude-4-sonnet`, `claude-4-opus` |
|
|
326
443
|
| Google | `@ai-sdk/google` (optional) | `gemini-2.5-pro` |
|
|
327
|
-
| OpenRouter | `@ai-sdk/openai` (included) | `
|
|
444
|
+
| OpenRouter | `@ai-sdk/openai` (included) | any model via `provider/model` |
|
|
328
445
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## Auth
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
import { jwtAuth, sessionAuth, betterAuth } from '@lantos1618/better-ui/auth';
|
|
452
|
+
|
|
453
|
+
// JWT Bearer tokens
|
|
454
|
+
const auth = jwtAuth({ secret: process.env.JWT_SECRET!, issuer: 'my-app' });
|
|
455
|
+
|
|
456
|
+
// Cookie-based sessions
|
|
457
|
+
const auth = sessionAuth({ cookieName: 'session', verify: async (token) => db.getSession(token) });
|
|
458
|
+
|
|
459
|
+
// BetterAuth integration
|
|
460
|
+
const auth = betterAuth(authInstance);
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Persistence
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
import { createMemoryAdapter } from '@lantos1618/better-ui/persistence';
|
|
469
|
+
|
|
470
|
+
const adapter = createMemoryAdapter(); // in-memory, for dev/testing
|
|
471
|
+
|
|
472
|
+
await adapter.createThread('New Chat');
|
|
473
|
+
await adapter.saveMessages(threadId, messages);
|
|
474
|
+
await adapter.getMessages(threadId);
|
|
333
475
|
```
|
|
334
476
|
|
|
477
|
+
Implement the `PersistenceAdapter` interface for Drizzle, Prisma, or any database.
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## HITL (Human-in-the-Loop)
|
|
482
|
+
|
|
483
|
+
Tools can require confirmation before executing:
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
const sendEmail = tool({
|
|
487
|
+
name: 'sendEmail',
|
|
488
|
+
description: 'Send an email',
|
|
489
|
+
input: z.object({ to: z.string().email(), subject: z.string(), body: z.string() }),
|
|
490
|
+
confirm: true, // always require confirmation
|
|
491
|
+
// or: confirm: (input) => input.to.endsWith('@company.com') // conditional
|
|
492
|
+
// or: hints: { destructive: true } // auto-implies confirmation
|
|
493
|
+
});
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
When `confirm` is set, `toAITool()` omits the `execute` function, leaving the tool call at `state: 'input-available'` for client-side confirmation before execution.
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## Security
|
|
501
|
+
|
|
502
|
+
Better UI is designed with security boundaries between server and client:
|
|
503
|
+
|
|
504
|
+
- **Context stripping** — `env`, `headers`, `cookies`, `user`, `session` are automatically removed when running on the client
|
|
505
|
+
- **Input validation** — Zod schemas validate and strip unknown keys before execution
|
|
506
|
+
- **Output validation** — Output schemas prevent accidental data leakage (extra fields are stripped)
|
|
507
|
+
- **Server isolation** — Server handlers never run on the client; auto-fetch kicks in instead
|
|
508
|
+
- **Serialization safety** — `toJSON()` excludes handlers, schemas, and internal config
|
|
509
|
+
- **Rate limiting** — Pluggable rate limiter with in-memory and Redis backends
|
|
510
|
+
- **Audit logging** — Structured JSON logging for every tool execution
|
|
511
|
+
- **Prototype pollution protection** — Safe object merging in state context handling
|
|
512
|
+
- **MCP hardening** — `hasOwnProperty` checks prevent prototype chain traversal on tool lookup
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
335
516
|
## Project Structure
|
|
336
517
|
|
|
337
518
|
```
|
|
338
519
|
src/
|
|
339
|
-
tool.tsx
|
|
520
|
+
tool.tsx Core tool() API — schema, handlers, view, streaming
|
|
521
|
+
index.ts Main exports (server-safe, no React)
|
|
340
522
|
react/
|
|
341
|
-
useTool.ts
|
|
342
|
-
useToolStream.ts
|
|
523
|
+
useTool.ts useTool, useTools hooks
|
|
524
|
+
useToolStream.ts useToolStream hook
|
|
343
525
|
components/
|
|
344
|
-
Chat.tsx
|
|
345
|
-
ChatProvider.tsx
|
|
346
|
-
Thread.tsx
|
|
347
|
-
Message.tsx
|
|
348
|
-
Composer.tsx
|
|
349
|
-
ToolResult.tsx
|
|
526
|
+
Chat.tsx All-in-one chat
|
|
527
|
+
ChatProvider.tsx Chat context provider
|
|
528
|
+
Thread.tsx Message list
|
|
529
|
+
Message.tsx Single message
|
|
530
|
+
Composer.tsx Input form
|
|
531
|
+
ToolResult.tsx Tool view renderer
|
|
532
|
+
Panel.tsx Sidebar panel
|
|
533
|
+
Markdown.tsx Markdown renderer
|
|
534
|
+
Question.tsx Question view block
|
|
535
|
+
Form.tsx Form view block
|
|
536
|
+
DataTable.tsx Data table view block
|
|
537
|
+
Progress.tsx Progress view block
|
|
538
|
+
MediaDisplay.tsx Media view block
|
|
539
|
+
CodeBlock.tsx Code block view block
|
|
540
|
+
FileUpload.tsx File upload view block
|
|
541
|
+
Toast.tsx Toast notifications
|
|
542
|
+
ThemeProvider.tsx Theme CSS variables
|
|
350
543
|
providers/
|
|
351
|
-
openai.ts
|
|
352
|
-
anthropic.ts
|
|
353
|
-
google.ts
|
|
354
|
-
openrouter.ts
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
544
|
+
openai.ts OpenAI adapter
|
|
545
|
+
anthropic.ts Anthropic adapter
|
|
546
|
+
google.ts Google Gemini adapter
|
|
547
|
+
openrouter.ts OpenRouter adapter
|
|
548
|
+
auth/
|
|
549
|
+
jwt.ts JWT auth helper
|
|
550
|
+
session.ts Session cookie auth
|
|
551
|
+
better-auth.ts BetterAuth integration
|
|
552
|
+
persistence/
|
|
553
|
+
types.ts PersistenceAdapter interface
|
|
554
|
+
memory.ts In-memory adapter
|
|
555
|
+
mcp/
|
|
556
|
+
server.ts MCP server (stdio + HTTP + SSE)
|
|
557
|
+
schema.ts Zod → JSON Schema converter
|
|
558
|
+
agui/
|
|
559
|
+
server.ts AG-UI protocol server (SSE)
|
|
560
|
+
examples/
|
|
561
|
+
nextjs-demo/ Full Next.js demo app
|
|
562
|
+
vite-demo/ Vite + Express demo app
|
|
563
|
+
mcp-server/ Standalone MCP server example
|
|
359
564
|
```
|
|
360
565
|
|
|
361
566
|
## Development
|
|
362
567
|
|
|
363
568
|
```bash
|
|
364
569
|
npm install
|
|
365
|
-
npm run
|
|
366
|
-
npm
|
|
367
|
-
npm run build # Build everything
|
|
570
|
+
npm run build # Build library
|
|
571
|
+
npm test # Run tests (226 tests)
|
|
368
572
|
npm run type-check # TypeScript check
|
|
369
|
-
npm test # Run tests
|
|
370
573
|
```
|
|
371
574
|
|
|
372
575
|
## License
|