npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lantos1618/better-ui

v0.9.1

Published

A minimal, type-safe AI-first UI framework for building tools

Readme

Better UI

Define once. Render in UI. Serve over MCP. Type-safe AI tools with views.

npm version npm downloads CI License TypeScript

Guide · API Reference · Examples

The Problem

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.

The Solution

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.

import { tool } from '@lantos1618/better-ui';
import { z } from 'zod';

const weather = tool({
  name: 'weather',
  description: 'Get weather for a city',
  input: z.object({ city: z.string() }),
  output: z.object({ temp: z.number(), condition: z.string() }),
});

weather.server(async ({ city }) => {
  const data = await weatherAPI.get(city);
  return { temp: data.temp, condition: data.condition };
});

weather.view((data) => (
  <div className="weather-card">
    <span>{data.temp}°</span>
    <span>{data.condition}</span>
  </div>
));

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.

Install

npm install @lantos1618/better-ui zod

What You Get

| Feature | What | |---------|------| | View integration | Tools render their own results — no other framework does this | | MCP server | Expose any tool registry to Claude Desktop, Cursor, VS Code | | AG-UI protocol | Compatible with CopilotKit, LangChain, Google ADK frontends | | Multi-provider | OpenAI, Anthropic, Google Gemini, OpenRouter | | Streaming views | Progressive partial data rendering | | Drop-in chat | <Chat /> component with automatic tool view rendering | | HITL confirmation | Tools can require human approval before executing | | Auth helpers | JWT, session cookies, BetterAuth integration | | Security | Context stripping, input validation, output sanitization, rate limiting |

Quick Start

1. Define a Tool

import { tool } from '@lantos1618/better-ui';
import { z } from 'zod';

export const search = tool({
  name: 'search',
  description: 'Search the web',
  input: z.object({ query: z.string().max(1000) }),
  output: z.object({
    results: z.array(z.object({
      title: z.string(),
      url: z.string(),
    })),
  }),
});

search.server(async ({ query }) => {
  const results = await searchAPI.search(query);
  return { results };
});

search.view((data) => (
  <ul>
    {data.results.map((r, i) => (
      <li key={i}><a href={r.url}>{r.title}</a></li>
    ))}
  </ul>
));

2. Use It in Chat

import { Chat } from '@lantos1618/better-ui/components';

function App() {
  return (
    <Chat
      endpoint="/api/chat"
      tools={{ weather, search }}
      className="h-[600px]"
    />
  );
}

Tool results render automatically using the tool's .view() component.

3. Wire Up the API Route

// app/api/chat/route.ts (Next.js)
import { streamText, convertToModelMessages } from 'ai';
import { createProvider } from '@lantos1618/better-ui';

const provider = createProvider({ provider: 'openai', model: 'gpt-4o' });

export async function POST(req: Request) {
  const { messages } = await req.json();
  const result = streamText({
    model: provider.model(),
    messages: convertToModelMessages(messages),
    tools: {
      weather: weatherTool.toAITool(),
      search: searchTool.toAITool(),
    },
  });
  return result.toUIMessageStreamResponse();
}

4. Or Expose as an MCP Server

import { createMCPServer } from '@lantos1618/better-ui/mcp';

const server = createMCPServer({
  name: 'my-tools',
  version: '1.0.0',
  tools: { weather, search },
});

server.start(); // stdio transport — works with Claude Desktop

Add to Claude Desktop config (~/.claude/claude_desktop_config.json):

{
  "mcpServers": {
    "my-tools": {
      "command": "npx",
      "args": ["tsx", "path/to/mcp-server.ts"]
    }
  }
}

Or use the HTTP handler for web-based MCP clients:

// app/api/mcp/route.ts
export const POST = server.httpHandler();

Tool API

Object Config

const myTool = tool({
  name: 'myTool',
  description: 'What this tool does',
  input: z.object({ query: z.string() }),
  output: z.object({ results: z.array(z.string()) }),
  tags: ['search'],
  cache: { ttl: 60000 },
  confirm: true,                    // require HITL confirmation
  hints: { destructive: true },     // behavioral metadata
  autoRespond: true,                // auto-send state back to AI after user action
  groupKey: (input) => input.query, // collapse related calls in thread
});

Fluent Builder

const search = tool('search')
  .description('Search the database')
  .input(z.object({ query: z.string() }))
  .output(z.object({ results: z.array(z.string()) }))
  .server(async ({ query }) => ({ results: await db.search(query) }))
  .view((data) => <ResultsList items={data.results} />)
  .build();

Handlers

// Server — runs in API routes, never on client
myTool.server(async (input, ctx) => {
  // ctx.env, ctx.headers, ctx.cookies, ctx.user, ctx.session
  return await db.query(input.query);
});

// Client — runs in browser. Auto-fetches to /api/tools/execute if not defined
myTool.client(async (input, ctx) => {
  return ctx.fetch('/api/search', { method: 'POST', body: JSON.stringify(input) });
});

// Stream — progressive partial updates
myTool.stream(async (input, { stream }) => {
  stream({ status: 'searching...' });
  const results = await search(input.query);
  stream({ results, status: 'done' });
  return { results, status: 'done', count: results.length };
});

// View — render results (the differentiator)
myTool.view((data, { loading, error, streaming, onAction }) => {
  if (loading) return <Spinner />;
  if (error) return <ErrorCard message={error.message} />;
  if (streaming) return <PartialResults data={data} />;
  return <Results items={data.results} />;
});

Execution

// Server-side
const result = await myTool.run(input, { isServer: true });

// Client-side (auto-fetches if no .client() defined)
const result = await myTool.run(input, { isServer: false });

// Streaming
for await (const { partial, done } of myTool.runStream(input)) {
  console.log(partial); // progressive updates
  if (done) break;
}

// AI SDK integration
const aiTool = myTool.toAITool(); // { description, inputSchema, execute }

React Hooks

import { useTool, useTools, useToolStream } from '@lantos1618/better-ui/react';

useTool

const { data, loading, error, execute, reset, executed } = useTool(myTool, initialInput, {
  auto: false,
  onSuccess: (data) => {},
  onError: (error) => {},
});

useToolStream

const { data, finalData, streaming, loading, error, execute, reset } = useToolStream(myTool);

useTools

const tools = useTools({ weather, search });

await tools.weather.execute({ city: 'London' });
tools.weather.data;    // result
tools.search.loading;  // loading state

Chat Components

import { Chat, ChatProvider, Thread, Composer, Message, ToolResult } from '@lantos1618/better-ui/components';

Drop-in

<Chat endpoint="/api/chat" tools={{ weather, search }} className="h-[600px]" />

Composable

<ChatProvider endpoint="/api/chat" tools={tools}>
  <div className="flex flex-col h-screen">
    <Thread className="flex-1 overflow-y-auto" />
    <Composer placeholder="Type a message..." />
  </div>
</ChatProvider>

All Components

| Component | Description | |-----------|-------------| | Chat | All-in-one (ChatProvider + Thread + Composer) | | ChatProvider | Context provider wrapping AI SDK's useChat | | Thread | Message list with auto-scroll | | Message | Single message with tool view rendering | | Composer | Input form with send button | | ToolResult | Renders a tool's .view() in chat context | | Panel / ChatPanel | Sidebar panel for thread management | | Markdown | Markdown renderer with syntax highlighting | | ThemeProvider | Theme CSS variable provider |

View Building Blocks

Pre-made view components for common patterns:

import {
  QuestionView,    // Multiple choice / free-text questions
  FormView,        // Dynamic forms
  DataTableView,   // Sortable data tables
  ProgressView,    // Step-by-step progress
  MediaDisplayView,// Image/video display
  CodeBlockView,   // Syntax-highlighted code
  FileUploadView,  // File upload UI
} from '@lantos1618/better-ui/components';

MCP Server

Turn any tool registry into an MCP server. Zero dependencies beyond Better UI itself.

import { createMCPServer } from '@lantos1618/better-ui/mcp';

const server = createMCPServer({
  name: 'my-app',
  version: '1.0.0',
  tools: { weather, search, calculator },
  context: { env: process.env },  // passed to every tool execution
});

Transports

// stdio — for Claude Desktop, Cursor, VS Code extensions
server.start();

// HTTP — for Next.js, Express, Cloudflare Workers, Deno
const handler = server.httpHandler();
// Use as: export const POST = handler;

Programmatic Use

// List tools with JSON schemas
const tools = server.listTools();

// Call a tool directly
const result = await server.callTool('weather', { city: 'Tokyo' });
// → { content: [{ type: 'text', text: '{"temp":21,"city":"Tokyo","condition":"sunny"}' }] }

// Handle raw JSON-RPC messages
const response = await server.handleMessage({
  jsonrpc: '2.0',
  id: 1,
  method: 'tools/call',
  params: { name: 'weather', arguments: { city: 'Tokyo' } },
});

Schema Conversion

The built-in zodToJsonSchema converter handles common Zod types without extra dependencies:

import { zodToJsonSchema } from '@lantos1618/better-ui/mcp';

zodToJsonSchema(z.object({
  name: z.string().min(1).max(100),
  age: z.number().int().min(0),
  role: z.enum(['admin', 'user']),
}));
// → { type: 'object', properties: { name: { type: 'string', ... }, ... }, required: [...] }

AG-UI Protocol

Expose your tools via the AG-UI (Agent-User Interaction Protocol) — compatible with CopilotKit, LangChain, Google ADK, and any AG-UI frontend.

import { createAGUIServer } from '@lantos1618/better-ui/agui';

const server = createAGUIServer({
  name: 'my-tools',
  tools: { weather, search },
});

// Next.js route handler — returns SSE event stream
export const POST = server.handler();

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.


Providers

import { createProvider } from '@lantos1618/better-ui';

createProvider({ provider: 'openai', model: 'gpt-4o' });
createProvider({ provider: 'anthropic', model: 'claude-4-sonnet' });
createProvider({ provider: 'google', model: 'gemini-2.5-pro' });
createProvider({ provider: 'openrouter', model: 'anthropic/claude-4-sonnet', apiKey: '...' });

| Provider | Package | Example Models | |----------|---------|----------------| | OpenAI | @ai-sdk/openai (included) | gpt-4o, gpt-5.2 | | Anthropic | @ai-sdk/anthropic (optional) | claude-4-sonnet, claude-4-opus | | Google | @ai-sdk/google (optional) | gemini-2.5-pro | | OpenRouter | @ai-sdk/openai (included) | any model via provider/model |


Auth

import { jwtAuth, sessionAuth, betterAuth } from '@lantos1618/better-ui/auth';

// JWT Bearer tokens
const auth = jwtAuth({ secret: process.env.JWT_SECRET!, issuer: 'my-app' });

// Cookie-based sessions
const auth = sessionAuth({ cookieName: 'session', verify: async (token) => db.getSession(token) });

// BetterAuth integration
const auth = betterAuth(authInstance);

Persistence

import { createMemoryAdapter } from '@lantos1618/better-ui/persistence';

const adapter = createMemoryAdapter(); // in-memory, for dev/testing

await adapter.createThread('New Chat');
await adapter.saveMessages(threadId, messages);
await adapter.getMessages(threadId);

Implement the PersistenceAdapter interface for Drizzle, Prisma, or any database.


HITL (Human-in-the-Loop)

Tools can require confirmation before executing:

const sendEmail = tool({
  name: 'sendEmail',
  description: 'Send an email',
  input: z.object({ to: z.string().email(), subject: z.string(), body: z.string() }),
  confirm: true, // always require confirmation
  // or: confirm: (input) => input.to.endsWith('@company.com') // conditional
  // or: hints: { destructive: true } // auto-implies confirmation
});

When confirm is set, toAITool() omits the execute function, leaving the tool call at state: 'input-available' for client-side confirmation before execution.


Security

Better UI is designed with security boundaries between server and client:

  • Context strippingenv, headers, cookies, user, session are automatically removed when running on the client
  • Input validation — Zod schemas validate and strip unknown keys before execution
  • Output validation — Output schemas prevent accidental data leakage (extra fields are stripped)
  • Server isolation — Server handlers never run on the client; auto-fetch kicks in instead
  • Serialization safetytoJSON() excludes handlers, schemas, and internal config
  • Rate limiting — Pluggable rate limiter with in-memory and Redis backends
  • Audit logging — Structured JSON logging for every tool execution
  • Prototype pollution protection — Safe object merging in state context handling
  • MCP hardeninghasOwnProperty checks prevent prototype chain traversal on tool lookup

Project Structure

src/
  tool.tsx              Core tool() API — schema, handlers, view, streaming
  index.ts              Main exports (server-safe, no React)
  react/
    useTool.ts           useTool, useTools hooks
    useToolStream.ts     useToolStream hook
  components/
    Chat.tsx             All-in-one chat
    ChatProvider.tsx      Chat context provider
    Thread.tsx           Message list
    Message.tsx          Single message
    Composer.tsx         Input form
    ToolResult.tsx       Tool view renderer
    Panel.tsx            Sidebar panel
    Markdown.tsx         Markdown renderer
    Question.tsx         Question view block
    Form.tsx             Form view block
    DataTable.tsx        Data table view block
    Progress.tsx         Progress view block
    MediaDisplay.tsx     Media view block
    CodeBlock.tsx        Code block view block
    FileUpload.tsx       File upload view block
    Toast.tsx            Toast notifications
    ThemeProvider.tsx     Theme CSS variables
  providers/
    openai.ts            OpenAI adapter
    anthropic.ts         Anthropic adapter
    google.ts            Google Gemini adapter
    openrouter.ts        OpenRouter adapter
  auth/
    jwt.ts               JWT auth helper
    session.ts           Session cookie auth
    better-auth.ts       BetterAuth integration
  persistence/
    types.ts             PersistenceAdapter interface
    memory.ts            In-memory adapter
  mcp/
    server.ts            MCP server (stdio + HTTP + SSE)
    schema.ts            Zod → JSON Schema converter
  agui/
    server.ts            AG-UI protocol server (SSE)
examples/
  nextjs-demo/           Full Next.js demo app
  vite-demo/             Vite + Express demo app
  mcp-server/            Standalone MCP server example

Development

npm install
npm run build        # Build library
npm test             # Run 228 tests across 11 suites
npm run type-check   # TypeScript check

Deploy the Demo

The examples/nextjs-demo/ is a full-featured chat app ready to deploy:

cd examples/nextjs-demo
npm install
# Set OPENAI_API_KEY in .env.local
npm run dev

To deploy on Vercel, set the Root Directory to examples/nextjs-demo in your project settings.

Contributing

See CONTRIBUTING.md for guidelines.

License

MIT