@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 CHANGED
@@ -1,29 +1,18 @@
1
1
  # Better UI
2
2
 
3
- > A minimal, type-safe AI-first UI framework for building tools
3
+ > Define once. Render in UI. Serve over MCP. Type-safe AI tools with views.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@lantos1618/better-ui.svg)](https://www.npmjs.com/package/@lantos1618/better-ui)
6
6
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue)](https://www.typescriptlang.org/)
8
8
 
9
- ## What is Better UI?
9
+ ## The Problem
10
10
 
11
- Better UI provides a clean, fluent API for creating tools that AI assistants can execute. Define input/output schemas with Zod, implement server and client logic separately, and render results with React components.
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
- **Key differentiators**:
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
- ## Installation
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}</span>
35
+ <span>{data.temp}°</span>
49
36
  <span>{data.condition}</span>
50
37
  </div>
51
38
  ));
52
39
  ```
53
40
 
54
- ## Drop-in Chat UI
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
- ```tsx
57
- import { Chat } from '@lantos1618/better-ui/components';
43
+ ## Install
58
44
 
59
- function App() {
60
- return (
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
- Or compose your own layout:
72
-
73
- ```tsx
74
- import { ChatProvider, Thread, Composer } from '@lantos1618/better-ui/components';
49
+ ## What You Get
75
50
 
76
- function App() {
77
- return (
78
- <ChatProvider endpoint="/api/chat" tools={tools}>
79
- <div className="flex flex-col h-screen">
80
- <Thread className="flex-1 overflow-y-auto" />
81
- <Composer placeholder="Type a message..." />
82
- </div>
83
- </ChatProvider>
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
- Tool results in chat automatically render using the tool's `.view()` component.
62
+ ## Quick Start
89
63
 
90
- ## Multi-Provider Support
64
+ ### 1. Define a Tool
91
65
 
92
66
  ```typescript
93
- import { createProvider } from '@lantos1618/better-ui';
67
+ import { tool } from '@lantos1618/better-ui';
68
+ import { z } from 'zod';
94
69
 
95
- // OpenAI
96
- const provider = createProvider({ provider: 'openai', model: 'gpt-4o' });
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
- // Anthropic
99
- const provider = createProvider({ provider: 'anthropic', model: 'claude-4-sonnet' });
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
- // Google Gemini
102
- const provider = createProvider({ provider: 'google', model: 'gemini-2.5-pro' });
96
+ ### 2. Use It in Chat
103
97
 
104
- // OpenRouter (access any model)
105
- const provider = createProvider({
106
- provider: 'openrouter',
107
- model: 'anthropic/claude-4-sonnet',
108
- apiKey: process.env.OPENROUTER_API_KEY,
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
- Use with the chat handler:
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
- ## Streaming Tool Views
134
-
135
- Tools can stream partial results progressively:
137
+ ### 4. Or Expose as an MCP Server
136
138
 
137
139
  ```typescript
138
- import { useToolStream } from '@lantos1618/better-ui';
140
+ import { createMCPServer } from '@lantos1618/better-ui/mcp';
139
141
 
140
- // Define a streaming tool
141
- const analysis = tool({
142
- name: 'analysis',
143
- input: z.object({ query: z.string() }),
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
- analysis.stream(async ({ query }, { stream }) => {
148
- stream({ summary: 'Analyzing...' }); // Partial update
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
- // In a component:
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
- return (
159
- <div>
160
- <button onClick={() => execute({ query })}>Analyze</button>
161
- {loading && <p>Starting...</p>}
162
- {streaming && <p>Streaming: {data?.summary}</p>}
163
- {data?.score && <p>Score: {data.score}</p>}
164
- </div>
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
- ## Core Concepts
164
+ Or use the HTTP handler for web-based MCP clients:
170
165
 
171
- ### 1. Tool Definition
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({ /* input schema */ }),
178
- output: z.object({ /* output schema */ }),
179
- tags: ['category', 'type'],
180
- cache: { ttl: 60000 }, // optional caching
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
- ### 2. Server Implementation
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
- myTool.server(async ({ query }, ctx) => {
190
- // Direct access to databases, secrets, file system
191
- const results = await db.search(query);
192
- return { results };
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
- ### 3. Client Implementation (Optional)
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
- myTool.client(async ({ query }, ctx) => {
202
- const cached = ctx.cache.get(query);
203
- if (cached) return cached;
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
- ### 4. View (Our Differentiator)
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
- The `.view()` method defines how to render the tool's results:
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
- ```typescript
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 <Error message={error.message} />;
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
- ### 5. Streaming
226
-
227
- The `.stream()` method enables progressive partial updates:
235
+ ### Execution
228
236
 
229
237
  ```typescript
230
- myTool.stream(async ({ query }, { stream }) => {
231
- stream({ status: 'searching...' });
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
- ## Fluent Builder Alternative
241
+ // Client-side (auto-fetches if no .client() defined)
242
+ const result = await myTool.run(input, { isServer: false });
239
243
 
240
- ```typescript
241
- const search = tool('search')
242
- .description('Search the database')
243
- .input(z.object({ query: z.string() }))
244
- .output(z.object({ results: z.array(z.string()) }))
245
- .server(async ({ query }) => ({ results: await db.search(query) }))
246
- .stream(async ({ query }, { stream }) => {
247
- stream({ results: [] });
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
- ### `useTool(tool, input?, options?)`
258
+ ```typescript
259
+ import { useTool, useTools, useToolStream } from '@lantos1618/better-ui/react';
260
+ ```
261
+
262
+ ### `useTool`
257
263
 
258
264
  ```typescript
259
- import { useTool } from '@lantos1618/better-ui';
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(tool, options?)`
272
+ ### `useToolStream`
276
273
 
277
274
  ```typescript
278
- import { useToolStream } from '@lantos1618/better-ui';
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(tools, options?)`
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
- await tools.search.execute({ query: 'restaurants' });
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
- Import from `@lantos1618/better-ui/components`:
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 chat component (ChatProvider + Thread + Composer) |
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 automatic tool view rendering |
320
+ | `Message` | Single message with tool view rendering |
315
321
  | `Composer` | Input form with send button |
316
- | `ToolResult` | Renders a tool's `.View` in chat context |
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
- All components accept `className` for styling customization.
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
- | Provider | Package Required | Example Model |
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) | `anthropic/claude-4-sonnet` |
423
+ | OpenRouter | `@ai-sdk/openai` (included) | any model via `provider/model` |
328
424
 
329
- ```bash
330
- # Optional providers
331
- npm install @ai-sdk/anthropic # For Anthropic
332
- npm install @ai-sdk/google # For Google Gemini
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 # Core tool() API with streaming
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 # React hooks (useTool, useTools)
342
- useToolStream.ts # Streaming hook
502
+ useTool.ts useTool, useTools hooks
503
+ useToolStream.ts useToolStream hook
343
504
  components/
344
- Chat.tsx # All-in-one chat component
345
- ChatProvider.tsx # Chat context provider
346
- Thread.tsx # Message list
347
- Message.tsx # Single message
348
- Composer.tsx # Input form
349
- ToolResult.tsx # Tool view renderer
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 # OpenAI adapter
352
- anthropic.ts # Anthropic adapter
353
- google.ts # Google Gemini adapter
354
- openrouter.ts # OpenRouter adapter
355
- adapters/
356
- nextjs.ts # Next.js route handlers
357
- express.ts # Express middleware
358
- index.ts # Main exports
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 dev # Run dev server
366
- npm run build:lib # Build library
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