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