@lantos1618/better-ui 0.5.0 → 0.6.1

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
@@ -52,6 +52,7 @@ npm install @lantos1618/better-ui zod
52
52
  |---------|------|
53
53
  | **View integration** | Tools render their own results — no other framework does this |
54
54
  | **MCP server** | Expose any tool registry to Claude Desktop, Cursor, VS Code |
55
+ | **AG-UI protocol** | Compatible with CopilotKit, LangChain, Google ADK frontends |
55
56
  | **Multi-provider** | OpenAI, Anthropic, Google Gemini, OpenRouter |
56
57
  | **Streaming views** | Progressive partial data rendering |
57
58
  | **Drop-in chat** | `<Chat />` component with automatic tool view rendering |
@@ -404,6 +405,26 @@ zodToJsonSchema(z.object({
404
405
 
405
406
  ---
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
+ ---
427
+
407
428
  ## Providers
408
429
 
409
430
  ```typescript
@@ -532,8 +553,10 @@ src/
532
553
  types.ts PersistenceAdapter interface
533
554
  memory.ts In-memory adapter
534
555
  mcp/
535
- server.ts MCP server (stdio + HTTP)
556
+ server.ts MCP server (stdio + HTTP + SSE)
536
557
  schema.ts Zod → JSON Schema converter
558
+ agui/
559
+ server.ts AG-UI protocol server (SSE)
537
560
  examples/
538
561
  nextjs-demo/ Full Next.js demo app
539
562
  vite-demo/ Vite + Express demo app
@@ -545,7 +568,7 @@ examples/
545
568
  ```bash
546
569
  npm install
547
570
  npm run build # Build library
548
- npm test # Run tests (163 tests)
571
+ npm test # Run tests (226 tests)
549
572
  npm run type-check # TypeScript check
550
573
  ```
551
574
 
@@ -0,0 +1,117 @@
1
+ import { T as Tool, c as ToolContext } from '../tool-Ca2x-VNK.mjs';
2
+ import 'zod';
3
+ import 'react';
4
+
5
+ /**
6
+ * AG-UI (Agent-User Interaction Protocol) Server for Better UI
7
+ *
8
+ * Implements the AG-UI protocol, allowing Better UI tools to be used with
9
+ * CopilotKit, LangChain, and any AG-UI compatible frontend.
10
+ *
11
+ * Protocol: Server-Sent Events (SSE) over HTTP
12
+ *
13
+ * @see https://docs.ag-ui.com
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { createAGUIServer } from '@lantos1618/better-ui/agui';
18
+ *
19
+ * const server = createAGUIServer({
20
+ * name: 'my-tools',
21
+ * tools: { weather: weatherTool, search: searchTool },
22
+ * });
23
+ *
24
+ * // Next.js route handler
25
+ * export const POST = server.handler();
26
+ * ```
27
+ */
28
+
29
+ type AGUIEventType = 'RUN_STARTED' | 'RUN_FINISHED' | 'RUN_ERROR' | 'STEP_STARTED' | 'STEP_FINISHED' | 'TEXT_MESSAGE_START' | 'TEXT_MESSAGE_CONTENT' | 'TEXT_MESSAGE_END' | 'TOOL_CALL_START' | 'TOOL_CALL_ARGS' | 'TOOL_CALL_END' | 'TOOL_CALL_RESULT' | 'STATE_SNAPSHOT' | 'STATE_DELTA' | 'CUSTOM' | 'RAW';
30
+ interface AGUIEvent {
31
+ type: AGUIEventType;
32
+ timestamp?: number;
33
+ [key: string]: unknown;
34
+ }
35
+ interface RunAgentInput {
36
+ threadId: string;
37
+ runId: string;
38
+ /** Tool definitions from the client */
39
+ tools?: Array<{
40
+ name: string;
41
+ description?: string;
42
+ }>;
43
+ /** Messages context */
44
+ messages?: Array<{
45
+ role: string;
46
+ content: string;
47
+ }>;
48
+ /** Single tool call to execute */
49
+ toolCall?: {
50
+ id: string;
51
+ name: string;
52
+ args: Record<string, unknown>;
53
+ };
54
+ /** Multiple tool calls to execute in sequence */
55
+ toolCalls?: Array<{
56
+ id: string;
57
+ name: string;
58
+ args: Record<string, unknown>;
59
+ }>;
60
+ /** State context from the frontend */
61
+ state?: Record<string, unknown>;
62
+ }
63
+ interface AGUIServerConfig {
64
+ /** Server name */
65
+ name: string;
66
+ /** Tool registry — keys are tool names */
67
+ tools: Record<string, Tool>;
68
+ /** Optional context passed to every tool execution */
69
+ context?: Partial<ToolContext>;
70
+ /** Called on errors */
71
+ onError?: (error: Error) => void;
72
+ }
73
+ declare class AGUIServer {
74
+ private config;
75
+ constructor(config: AGUIServerConfig);
76
+ /** Get available tools in AG-UI format */
77
+ listTools(): Array<{
78
+ name: string;
79
+ description: string;
80
+ parameters: Record<string, unknown>;
81
+ }>;
82
+ /**
83
+ * Create an HTTP handler that implements the AG-UI protocol.
84
+ * Returns an SSE stream of AG-UI events.
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * // Next.js: app/api/agui/route.ts
89
+ * export const POST = server.handler();
90
+ *
91
+ * // Express:
92
+ * app.post('/api/agui', (req, res) => server.handler()(req));
93
+ * ```
94
+ */
95
+ handler(): (req: Request) => Promise<Response>;
96
+ /**
97
+ * Execute a tool call and emit AG-UI events.
98
+ */
99
+ private executeToolCall;
100
+ }
101
+ /**
102
+ * Create an AG-UI server from a Better UI tool registry.
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * const server = createAGUIServer({
107
+ * name: 'my-app',
108
+ * tools: { weather: weatherTool, search: searchTool },
109
+ * });
110
+ *
111
+ * // Use as Next.js route handler
112
+ * export const POST = server.handler();
113
+ * ```
114
+ */
115
+ declare function createAGUIServer(config: AGUIServerConfig): AGUIServer;
116
+
117
+ export { type AGUIEvent, type AGUIEventType, AGUIServer, type AGUIServerConfig, type RunAgentInput, createAGUIServer };
@@ -0,0 +1,117 @@
1
+ import { T as Tool, c as ToolContext } from '../tool-Ca2x-VNK.js';
2
+ import 'zod';
3
+ import 'react';
4
+
5
+ /**
6
+ * AG-UI (Agent-User Interaction Protocol) Server for Better UI
7
+ *
8
+ * Implements the AG-UI protocol, allowing Better UI tools to be used with
9
+ * CopilotKit, LangChain, and any AG-UI compatible frontend.
10
+ *
11
+ * Protocol: Server-Sent Events (SSE) over HTTP
12
+ *
13
+ * @see https://docs.ag-ui.com
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { createAGUIServer } from '@lantos1618/better-ui/agui';
18
+ *
19
+ * const server = createAGUIServer({
20
+ * name: 'my-tools',
21
+ * tools: { weather: weatherTool, search: searchTool },
22
+ * });
23
+ *
24
+ * // Next.js route handler
25
+ * export const POST = server.handler();
26
+ * ```
27
+ */
28
+
29
+ type AGUIEventType = 'RUN_STARTED' | 'RUN_FINISHED' | 'RUN_ERROR' | 'STEP_STARTED' | 'STEP_FINISHED' | 'TEXT_MESSAGE_START' | 'TEXT_MESSAGE_CONTENT' | 'TEXT_MESSAGE_END' | 'TOOL_CALL_START' | 'TOOL_CALL_ARGS' | 'TOOL_CALL_END' | 'TOOL_CALL_RESULT' | 'STATE_SNAPSHOT' | 'STATE_DELTA' | 'CUSTOM' | 'RAW';
30
+ interface AGUIEvent {
31
+ type: AGUIEventType;
32
+ timestamp?: number;
33
+ [key: string]: unknown;
34
+ }
35
+ interface RunAgentInput {
36
+ threadId: string;
37
+ runId: string;
38
+ /** Tool definitions from the client */
39
+ tools?: Array<{
40
+ name: string;
41
+ description?: string;
42
+ }>;
43
+ /** Messages context */
44
+ messages?: Array<{
45
+ role: string;
46
+ content: string;
47
+ }>;
48
+ /** Single tool call to execute */
49
+ toolCall?: {
50
+ id: string;
51
+ name: string;
52
+ args: Record<string, unknown>;
53
+ };
54
+ /** Multiple tool calls to execute in sequence */
55
+ toolCalls?: Array<{
56
+ id: string;
57
+ name: string;
58
+ args: Record<string, unknown>;
59
+ }>;
60
+ /** State context from the frontend */
61
+ state?: Record<string, unknown>;
62
+ }
63
+ interface AGUIServerConfig {
64
+ /** Server name */
65
+ name: string;
66
+ /** Tool registry — keys are tool names */
67
+ tools: Record<string, Tool>;
68
+ /** Optional context passed to every tool execution */
69
+ context?: Partial<ToolContext>;
70
+ /** Called on errors */
71
+ onError?: (error: Error) => void;
72
+ }
73
+ declare class AGUIServer {
74
+ private config;
75
+ constructor(config: AGUIServerConfig);
76
+ /** Get available tools in AG-UI format */
77
+ listTools(): Array<{
78
+ name: string;
79
+ description: string;
80
+ parameters: Record<string, unknown>;
81
+ }>;
82
+ /**
83
+ * Create an HTTP handler that implements the AG-UI protocol.
84
+ * Returns an SSE stream of AG-UI events.
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * // Next.js: app/api/agui/route.ts
89
+ * export const POST = server.handler();
90
+ *
91
+ * // Express:
92
+ * app.post('/api/agui', (req, res) => server.handler()(req));
93
+ * ```
94
+ */
95
+ handler(): (req: Request) => Promise<Response>;
96
+ /**
97
+ * Execute a tool call and emit AG-UI events.
98
+ */
99
+ private executeToolCall;
100
+ }
101
+ /**
102
+ * Create an AG-UI server from a Better UI tool registry.
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * const server = createAGUIServer({
107
+ * name: 'my-app',
108
+ * tools: { weather: weatherTool, search: searchTool },
109
+ * });
110
+ *
111
+ * // Use as Next.js route handler
112
+ * export const POST = server.handler();
113
+ * ```
114
+ */
115
+ declare function createAGUIServer(config: AGUIServerConfig): AGUIServer;
116
+
117
+ export { type AGUIEvent, type AGUIEventType, AGUIServer, type AGUIServerConfig, type RunAgentInput, createAGUIServer };
@@ -0,0 +1,307 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/agui/index.ts
21
+ var agui_exports = {};
22
+ __export(agui_exports, {
23
+ AGUIServer: () => AGUIServer,
24
+ createAGUIServer: () => createAGUIServer
25
+ });
26
+ module.exports = __toCommonJS(agui_exports);
27
+
28
+ // src/mcp/schema.ts
29
+ function zodToJsonSchema(schema) {
30
+ return convert(schema);
31
+ }
32
+ function convert(schema) {
33
+ const def = schema._def;
34
+ const typeName = def?.typeName;
35
+ switch (typeName) {
36
+ case "ZodString":
37
+ return convertString(def);
38
+ case "ZodNumber":
39
+ return convertNumber(def);
40
+ case "ZodBoolean":
41
+ return { type: "boolean" };
42
+ case "ZodNull":
43
+ return { type: "null" };
44
+ case "ZodLiteral":
45
+ return { enum: [def.value] };
46
+ case "ZodEnum":
47
+ return { type: "string", enum: def.values };
48
+ case "ZodNativeEnum":
49
+ return { enum: Object.values(def.values) };
50
+ case "ZodObject":
51
+ return convertObject(def);
52
+ case "ZodArray":
53
+ return convertArray(def);
54
+ case "ZodOptional":
55
+ return convert(def.innerType);
56
+ case "ZodNullable": {
57
+ const inner = convert(def.innerType);
58
+ return { anyOf: [inner, { type: "null" }] };
59
+ }
60
+ case "ZodDefault":
61
+ return { ...convert(def.innerType), default: def.defaultValue() };
62
+ case "ZodUnion":
63
+ return { anyOf: def.options.map((o) => convert(o)) };
64
+ case "ZodDiscriminatedUnion":
65
+ return { oneOf: [...def.options.values()].map((o) => convert(o)) };
66
+ case "ZodRecord":
67
+ return {
68
+ type: "object",
69
+ additionalProperties: convert(def.valueType)
70
+ };
71
+ case "ZodTuple": {
72
+ const items = def.items.map((item) => convert(item));
73
+ return { type: "array", items: items.length === 1 ? items[0] : void 0, prefixItems: items };
74
+ }
75
+ case "ZodEffects":
76
+ return convert(def.schema);
77
+ case "ZodPipeline":
78
+ return convert(def.in);
79
+ case "ZodLazy":
80
+ return convert(def.getter());
81
+ case "ZodAny":
82
+ return {};
83
+ case "ZodUnknown":
84
+ return {};
85
+ default:
86
+ return {};
87
+ }
88
+ }
89
+ function convertString(def) {
90
+ const schema = { type: "string" };
91
+ if (def.checks) {
92
+ for (const check of def.checks) {
93
+ switch (check.kind) {
94
+ case "min":
95
+ schema.minLength = check.value;
96
+ break;
97
+ case "max":
98
+ schema.maxLength = check.value;
99
+ break;
100
+ case "email":
101
+ schema.format = "email";
102
+ break;
103
+ case "url":
104
+ schema.format = "uri";
105
+ break;
106
+ case "uuid":
107
+ schema.format = "uuid";
108
+ break;
109
+ case "regex":
110
+ schema.pattern = check.regex.source;
111
+ break;
112
+ }
113
+ }
114
+ }
115
+ if (def.description) schema.description = def.description;
116
+ return schema;
117
+ }
118
+ function convertNumber(def) {
119
+ const schema = { type: "number" };
120
+ if (def.checks) {
121
+ for (const check of def.checks) {
122
+ switch (check.kind) {
123
+ case "min":
124
+ schema.minimum = check.value;
125
+ if (check.inclusive === false) schema.exclusiveMinimum = check.value;
126
+ break;
127
+ case "max":
128
+ schema.maximum = check.value;
129
+ if (check.inclusive === false) schema.exclusiveMaximum = check.value;
130
+ break;
131
+ case "int":
132
+ schema.type = "integer";
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ if (def.description) schema.description = def.description;
138
+ return schema;
139
+ }
140
+ function convertObject(def) {
141
+ const shape = def.shape();
142
+ const properties = {};
143
+ const required = [];
144
+ for (const [key, value] of Object.entries(shape)) {
145
+ properties[key] = convert(value);
146
+ const fieldDef = value._def;
147
+ const isOptional = fieldDef?.typeName === "ZodOptional" || fieldDef?.typeName === "ZodDefault";
148
+ if (!isOptional) {
149
+ required.push(key);
150
+ }
151
+ }
152
+ const schema = { type: "object", properties };
153
+ if (required.length > 0) schema.required = required;
154
+ if (def.description) schema.description = def.description;
155
+ return schema;
156
+ }
157
+ function convertArray(def) {
158
+ const schema = {
159
+ type: "array",
160
+ items: convert(def.type)
161
+ };
162
+ if (def.minLength) schema.minItems = def.minLength.value;
163
+ if (def.maxLength) schema.maxItems = def.maxLength.value;
164
+ if (def.description) schema.description = def.description;
165
+ return schema;
166
+ }
167
+
168
+ // src/agui/server.ts
169
+ var AGUIServer = class {
170
+ constructor(config) {
171
+ this.config = config;
172
+ }
173
+ /** Get available tools in AG-UI format */
174
+ listTools() {
175
+ return Object.values(this.config.tools).map((tool) => ({
176
+ name: tool.name,
177
+ description: tool.description || tool.name,
178
+ parameters: zodToJsonSchema(tool.inputSchema)
179
+ }));
180
+ }
181
+ /**
182
+ * Create an HTTP handler that implements the AG-UI protocol.
183
+ * Returns an SSE stream of AG-UI events.
184
+ *
185
+ * @example
186
+ * ```typescript
187
+ * // Next.js: app/api/agui/route.ts
188
+ * export const POST = server.handler();
189
+ *
190
+ * // Express:
191
+ * app.post('/api/agui', (req, res) => server.handler()(req));
192
+ * ```
193
+ */
194
+ handler() {
195
+ return async (req) => {
196
+ let input;
197
+ try {
198
+ input = await req.json();
199
+ } catch {
200
+ return new Response("Invalid JSON", { status: 400 });
201
+ }
202
+ const { threadId, runId, toolCall } = input;
203
+ if (!threadId || !runId) {
204
+ return new Response("Missing threadId or runId", { status: 400 });
205
+ }
206
+ const encoder = new TextEncoder();
207
+ const self = this;
208
+ const stream = new ReadableStream({
209
+ async start(controller) {
210
+ const emit = (event) => {
211
+ const data = JSON.stringify({ ...event, timestamp: event.timestamp ?? Date.now() });
212
+ controller.enqueue(encoder.encode(`data: ${data}
213
+
214
+ `));
215
+ };
216
+ try {
217
+ emit({ type: "RUN_STARTED", threadId, runId });
218
+ const calls = toolCall ? [toolCall] : input.toolCalls ?? [];
219
+ if (calls.length > 0) {
220
+ for (const call of calls) {
221
+ await self.executeToolCall(call, emit);
222
+ }
223
+ } else {
224
+ const tools = self.listTools();
225
+ const messageId = `msg_${runId}`;
226
+ emit({ type: "TEXT_MESSAGE_START", messageId, role: "assistant" });
227
+ emit({
228
+ type: "TEXT_MESSAGE_CONTENT",
229
+ messageId,
230
+ delta: `Available tools: ${tools.map((t) => t.name).join(", ")}`
231
+ });
232
+ emit({ type: "TEXT_MESSAGE_END", messageId });
233
+ }
234
+ emit({ type: "RUN_FINISHED", threadId, runId });
235
+ } catch (err) {
236
+ emit({
237
+ type: "RUN_ERROR",
238
+ threadId,
239
+ runId,
240
+ message: err instanceof Error ? err.message : "Unknown error"
241
+ });
242
+ self.config.onError?.(err instanceof Error ? err : new Error(String(err)));
243
+ }
244
+ controller.close();
245
+ }
246
+ });
247
+ return new Response(stream, {
248
+ headers: {
249
+ "Content-Type": "text/event-stream",
250
+ "Cache-Control": "no-cache",
251
+ "Connection": "keep-alive"
252
+ }
253
+ });
254
+ };
255
+ }
256
+ /**
257
+ * Execute a tool call and emit AG-UI events.
258
+ */
259
+ async executeToolCall(toolCall, emit) {
260
+ const { id, name, args } = toolCall;
261
+ if (!Object.prototype.hasOwnProperty.call(this.config.tools, name)) {
262
+ emit({
263
+ type: "TOOL_CALL_START",
264
+ toolCallId: id,
265
+ toolCallName: name
266
+ });
267
+ emit({
268
+ type: "TOOL_CALL_END",
269
+ toolCallId: id
270
+ });
271
+ throw new Error(`Unknown tool: ${name}`);
272
+ }
273
+ const tool = this.config.tools[name];
274
+ emit({
275
+ type: "TOOL_CALL_START",
276
+ toolCallId: id,
277
+ toolCallName: name
278
+ });
279
+ emit({
280
+ type: "TOOL_CALL_ARGS",
281
+ toolCallId: id,
282
+ delta: JSON.stringify(args)
283
+ });
284
+ const result = await tool.run(args, {
285
+ isServer: true,
286
+ ...this.config.context
287
+ });
288
+ const resultText = typeof result === "string" ? result : JSON.stringify(result);
289
+ emit({
290
+ type: "TOOL_CALL_RESULT",
291
+ toolCallId: id,
292
+ result: resultText
293
+ });
294
+ emit({
295
+ type: "TOOL_CALL_END",
296
+ toolCallId: id
297
+ });
298
+ }
299
+ };
300
+ function createAGUIServer(config) {
301
+ return new AGUIServer(config);
302
+ }
303
+ // Annotate the CommonJS export names for ESM import in node:
304
+ 0 && (module.exports = {
305
+ AGUIServer,
306
+ createAGUIServer
307
+ });
@@ -0,0 +1,144 @@
1
+ import {
2
+ zodToJsonSchema
3
+ } from "../chunk-OH73K7I5.mjs";
4
+ import "../chunk-Y6FXYEAI.mjs";
5
+
6
+ // src/agui/server.ts
7
+ var AGUIServer = class {
8
+ constructor(config) {
9
+ this.config = config;
10
+ }
11
+ /** Get available tools in AG-UI format */
12
+ listTools() {
13
+ return Object.values(this.config.tools).map((tool) => ({
14
+ name: tool.name,
15
+ description: tool.description || tool.name,
16
+ parameters: zodToJsonSchema(tool.inputSchema)
17
+ }));
18
+ }
19
+ /**
20
+ * Create an HTTP handler that implements the AG-UI protocol.
21
+ * Returns an SSE stream of AG-UI events.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * // Next.js: app/api/agui/route.ts
26
+ * export const POST = server.handler();
27
+ *
28
+ * // Express:
29
+ * app.post('/api/agui', (req, res) => server.handler()(req));
30
+ * ```
31
+ */
32
+ handler() {
33
+ return async (req) => {
34
+ let input;
35
+ try {
36
+ input = await req.json();
37
+ } catch {
38
+ return new Response("Invalid JSON", { status: 400 });
39
+ }
40
+ const { threadId, runId, toolCall } = input;
41
+ if (!threadId || !runId) {
42
+ return new Response("Missing threadId or runId", { status: 400 });
43
+ }
44
+ const encoder = new TextEncoder();
45
+ const self = this;
46
+ const stream = new ReadableStream({
47
+ async start(controller) {
48
+ const emit = (event) => {
49
+ const data = JSON.stringify({ ...event, timestamp: event.timestamp ?? Date.now() });
50
+ controller.enqueue(encoder.encode(`data: ${data}
51
+
52
+ `));
53
+ };
54
+ try {
55
+ emit({ type: "RUN_STARTED", threadId, runId });
56
+ const calls = toolCall ? [toolCall] : input.toolCalls ?? [];
57
+ if (calls.length > 0) {
58
+ for (const call of calls) {
59
+ await self.executeToolCall(call, emit);
60
+ }
61
+ } else {
62
+ const tools = self.listTools();
63
+ const messageId = `msg_${runId}`;
64
+ emit({ type: "TEXT_MESSAGE_START", messageId, role: "assistant" });
65
+ emit({
66
+ type: "TEXT_MESSAGE_CONTENT",
67
+ messageId,
68
+ delta: `Available tools: ${tools.map((t) => t.name).join(", ")}`
69
+ });
70
+ emit({ type: "TEXT_MESSAGE_END", messageId });
71
+ }
72
+ emit({ type: "RUN_FINISHED", threadId, runId });
73
+ } catch (err) {
74
+ emit({
75
+ type: "RUN_ERROR",
76
+ threadId,
77
+ runId,
78
+ message: err instanceof Error ? err.message : "Unknown error"
79
+ });
80
+ self.config.onError?.(err instanceof Error ? err : new Error(String(err)));
81
+ }
82
+ controller.close();
83
+ }
84
+ });
85
+ return new Response(stream, {
86
+ headers: {
87
+ "Content-Type": "text/event-stream",
88
+ "Cache-Control": "no-cache",
89
+ "Connection": "keep-alive"
90
+ }
91
+ });
92
+ };
93
+ }
94
+ /**
95
+ * Execute a tool call and emit AG-UI events.
96
+ */
97
+ async executeToolCall(toolCall, emit) {
98
+ const { id, name, args } = toolCall;
99
+ if (!Object.prototype.hasOwnProperty.call(this.config.tools, name)) {
100
+ emit({
101
+ type: "TOOL_CALL_START",
102
+ toolCallId: id,
103
+ toolCallName: name
104
+ });
105
+ emit({
106
+ type: "TOOL_CALL_END",
107
+ toolCallId: id
108
+ });
109
+ throw new Error(`Unknown tool: ${name}`);
110
+ }
111
+ const tool = this.config.tools[name];
112
+ emit({
113
+ type: "TOOL_CALL_START",
114
+ toolCallId: id,
115
+ toolCallName: name
116
+ });
117
+ emit({
118
+ type: "TOOL_CALL_ARGS",
119
+ toolCallId: id,
120
+ delta: JSON.stringify(args)
121
+ });
122
+ const result = await tool.run(args, {
123
+ isServer: true,
124
+ ...this.config.context
125
+ });
126
+ const resultText = typeof result === "string" ? result : JSON.stringify(result);
127
+ emit({
128
+ type: "TOOL_CALL_RESULT",
129
+ toolCallId: id,
130
+ result: resultText
131
+ });
132
+ emit({
133
+ type: "TOOL_CALL_END",
134
+ toolCallId: id
135
+ });
136
+ }
137
+ };
138
+ function createAGUIServer(config) {
139
+ return new AGUIServer(config);
140
+ }
141
+ export {
142
+ AGUIServer,
143
+ createAGUIServer
144
+ };
@@ -0,0 +1,143 @@
1
+ // src/mcp/schema.ts
2
+ function zodToJsonSchema(schema) {
3
+ return convert(schema);
4
+ }
5
+ function convert(schema) {
6
+ const def = schema._def;
7
+ const typeName = def?.typeName;
8
+ switch (typeName) {
9
+ case "ZodString":
10
+ return convertString(def);
11
+ case "ZodNumber":
12
+ return convertNumber(def);
13
+ case "ZodBoolean":
14
+ return { type: "boolean" };
15
+ case "ZodNull":
16
+ return { type: "null" };
17
+ case "ZodLiteral":
18
+ return { enum: [def.value] };
19
+ case "ZodEnum":
20
+ return { type: "string", enum: def.values };
21
+ case "ZodNativeEnum":
22
+ return { enum: Object.values(def.values) };
23
+ case "ZodObject":
24
+ return convertObject(def);
25
+ case "ZodArray":
26
+ return convertArray(def);
27
+ case "ZodOptional":
28
+ return convert(def.innerType);
29
+ case "ZodNullable": {
30
+ const inner = convert(def.innerType);
31
+ return { anyOf: [inner, { type: "null" }] };
32
+ }
33
+ case "ZodDefault":
34
+ return { ...convert(def.innerType), default: def.defaultValue() };
35
+ case "ZodUnion":
36
+ return { anyOf: def.options.map((o) => convert(o)) };
37
+ case "ZodDiscriminatedUnion":
38
+ return { oneOf: [...def.options.values()].map((o) => convert(o)) };
39
+ case "ZodRecord":
40
+ return {
41
+ type: "object",
42
+ additionalProperties: convert(def.valueType)
43
+ };
44
+ case "ZodTuple": {
45
+ const items = def.items.map((item) => convert(item));
46
+ return { type: "array", items: items.length === 1 ? items[0] : void 0, prefixItems: items };
47
+ }
48
+ case "ZodEffects":
49
+ return convert(def.schema);
50
+ case "ZodPipeline":
51
+ return convert(def.in);
52
+ case "ZodLazy":
53
+ return convert(def.getter());
54
+ case "ZodAny":
55
+ return {};
56
+ case "ZodUnknown":
57
+ return {};
58
+ default:
59
+ return {};
60
+ }
61
+ }
62
+ function convertString(def) {
63
+ const schema = { type: "string" };
64
+ if (def.checks) {
65
+ for (const check of def.checks) {
66
+ switch (check.kind) {
67
+ case "min":
68
+ schema.minLength = check.value;
69
+ break;
70
+ case "max":
71
+ schema.maxLength = check.value;
72
+ break;
73
+ case "email":
74
+ schema.format = "email";
75
+ break;
76
+ case "url":
77
+ schema.format = "uri";
78
+ break;
79
+ case "uuid":
80
+ schema.format = "uuid";
81
+ break;
82
+ case "regex":
83
+ schema.pattern = check.regex.source;
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ if (def.description) schema.description = def.description;
89
+ return schema;
90
+ }
91
+ function convertNumber(def) {
92
+ const schema = { type: "number" };
93
+ if (def.checks) {
94
+ for (const check of def.checks) {
95
+ switch (check.kind) {
96
+ case "min":
97
+ schema.minimum = check.value;
98
+ if (check.inclusive === false) schema.exclusiveMinimum = check.value;
99
+ break;
100
+ case "max":
101
+ schema.maximum = check.value;
102
+ if (check.inclusive === false) schema.exclusiveMaximum = check.value;
103
+ break;
104
+ case "int":
105
+ schema.type = "integer";
106
+ break;
107
+ }
108
+ }
109
+ }
110
+ if (def.description) schema.description = def.description;
111
+ return schema;
112
+ }
113
+ function convertObject(def) {
114
+ const shape = def.shape();
115
+ const properties = {};
116
+ const required = [];
117
+ for (const [key, value] of Object.entries(shape)) {
118
+ properties[key] = convert(value);
119
+ const fieldDef = value._def;
120
+ const isOptional = fieldDef?.typeName === "ZodOptional" || fieldDef?.typeName === "ZodDefault";
121
+ if (!isOptional) {
122
+ required.push(key);
123
+ }
124
+ }
125
+ const schema = { type: "object", properties };
126
+ if (required.length > 0) schema.required = required;
127
+ if (def.description) schema.description = def.description;
128
+ return schema;
129
+ }
130
+ function convertArray(def) {
131
+ const schema = {
132
+ type: "array",
133
+ items: convert(def.type)
134
+ };
135
+ if (def.minLength) schema.minItems = def.minLength.value;
136
+ if (def.maxLength) schema.maxItems = def.maxLength.value;
137
+ if (def.description) schema.description = def.description;
138
+ return schema;
139
+ }
140
+
141
+ export {
142
+ zodToJsonSchema
143
+ };
@@ -96,6 +96,19 @@ declare class MCPServer {
96
96
  * ```
97
97
  */
98
98
  httpHandler(): (req: Request) => Promise<Response>;
99
+ /**
100
+ * Create a Streamable HTTP handler (MCP spec 2025-03-26).
101
+ * Supports both single JSON-RPC requests and SSE streaming for long-running operations.
102
+ * Compatible with Next.js route handlers, Deno, Bun, Cloudflare Workers.
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * // Next.js route: app/api/mcp/route.ts
107
+ * import { server } from '@/lib/mcp';
108
+ * export const POST = server.streamableHttpHandler();
109
+ * ```
110
+ */
111
+ streamableHttpHandler(): (req: Request) => Promise<Response>;
99
112
  }
100
113
  /**
101
114
  * Create an MCP server from a Better UI tool registry.
@@ -96,6 +96,19 @@ declare class MCPServer {
96
96
  * ```
97
97
  */
98
98
  httpHandler(): (req: Request) => Promise<Response>;
99
+ /**
100
+ * Create a Streamable HTTP handler (MCP spec 2025-03-26).
101
+ * Supports both single JSON-RPC requests and SSE streaming for long-running operations.
102
+ * Compatible with Next.js route handlers, Deno, Bun, Cloudflare Workers.
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * // Next.js route: app/api/mcp/route.ts
107
+ * import { server } from '@/lib/mcp';
108
+ * export const POST = server.streamableHttpHandler();
109
+ * ```
110
+ */
111
+ streamableHttpHandler(): (req: Request) => Promise<Response>;
99
112
  }
100
113
  /**
101
114
  * Create an MCP server from a Better UI tool registry.
package/dist/mcp/index.js CHANGED
@@ -394,6 +394,84 @@ var MCPServer = class {
394
394
  return Response.json(response);
395
395
  };
396
396
  }
397
+ /**
398
+ * Create a Streamable HTTP handler (MCP spec 2025-03-26).
399
+ * Supports both single JSON-RPC requests and SSE streaming for long-running operations.
400
+ * Compatible with Next.js route handlers, Deno, Bun, Cloudflare Workers.
401
+ *
402
+ * @example
403
+ * ```typescript
404
+ * // Next.js route: app/api/mcp/route.ts
405
+ * import { server } from '@/lib/mcp';
406
+ * export const POST = server.streamableHttpHandler();
407
+ * ```
408
+ */
409
+ streamableHttpHandler() {
410
+ return async (req) => {
411
+ const accept = req.headers.get("accept") || "";
412
+ const contentType = req.headers.get("content-type") || "";
413
+ if (!contentType.includes("application/json")) {
414
+ return Response.json(
415
+ { jsonrpc: "2.0", id: null, error: { code: PARSE_ERROR, message: "Content-Type must be application/json" } },
416
+ { status: 400 }
417
+ );
418
+ }
419
+ let message;
420
+ try {
421
+ message = await req.json();
422
+ } catch {
423
+ return Response.json(
424
+ { jsonrpc: "2.0", id: null, error: { code: PARSE_ERROR, message: "Parse error" } },
425
+ { status: 400 }
426
+ );
427
+ }
428
+ if (!message.jsonrpc || message.jsonrpc !== "2.0") {
429
+ return Response.json(
430
+ { jsonrpc: "2.0", id: message.id ?? null, error: { code: INVALID_REQUEST, message: "Invalid JSON-RPC version" } },
431
+ { status: 400 }
432
+ );
433
+ }
434
+ if (accept.includes("text/event-stream")) {
435
+ const encoder = new TextEncoder();
436
+ const self = this;
437
+ const sseStream = new ReadableStream({
438
+ async start(controller) {
439
+ try {
440
+ const response2 = await self.handleMessage(message);
441
+ if (response2) {
442
+ controller.enqueue(encoder.encode(`event: message
443
+ data: ${JSON.stringify(response2)}
444
+
445
+ `));
446
+ }
447
+ } catch (err) {
448
+ const errorResponse = {
449
+ jsonrpc: "2.0",
450
+ id: message.id ?? null,
451
+ error: { code: INTERNAL_ERROR, message: err instanceof Error ? err.message : "Internal error" }
452
+ };
453
+ controller.enqueue(encoder.encode(`event: message
454
+ data: ${JSON.stringify(errorResponse)}
455
+
456
+ `));
457
+ }
458
+ controller.close();
459
+ }
460
+ });
461
+ return new Response(sseStream, {
462
+ headers: {
463
+ "Content-Type": "text/event-stream",
464
+ "Cache-Control": "no-cache"
465
+ }
466
+ });
467
+ }
468
+ const response = await this.handleMessage(message);
469
+ if (!response) {
470
+ return new Response(null, { status: 204 });
471
+ }
472
+ return Response.json(response);
473
+ };
474
+ }
397
475
  };
398
476
  var McpError = class extends Error {
399
477
  constructor(code, message) {
@@ -1,145 +1,8 @@
1
+ import {
2
+ zodToJsonSchema
3
+ } from "../chunk-OH73K7I5.mjs";
1
4
  import "../chunk-Y6FXYEAI.mjs";
2
5
 
3
- // src/mcp/schema.ts
4
- function zodToJsonSchema(schema) {
5
- return convert(schema);
6
- }
7
- function convert(schema) {
8
- const def = schema._def;
9
- const typeName = def?.typeName;
10
- switch (typeName) {
11
- case "ZodString":
12
- return convertString(def);
13
- case "ZodNumber":
14
- return convertNumber(def);
15
- case "ZodBoolean":
16
- return { type: "boolean" };
17
- case "ZodNull":
18
- return { type: "null" };
19
- case "ZodLiteral":
20
- return { enum: [def.value] };
21
- case "ZodEnum":
22
- return { type: "string", enum: def.values };
23
- case "ZodNativeEnum":
24
- return { enum: Object.values(def.values) };
25
- case "ZodObject":
26
- return convertObject(def);
27
- case "ZodArray":
28
- return convertArray(def);
29
- case "ZodOptional":
30
- return convert(def.innerType);
31
- case "ZodNullable": {
32
- const inner = convert(def.innerType);
33
- return { anyOf: [inner, { type: "null" }] };
34
- }
35
- case "ZodDefault":
36
- return { ...convert(def.innerType), default: def.defaultValue() };
37
- case "ZodUnion":
38
- return { anyOf: def.options.map((o) => convert(o)) };
39
- case "ZodDiscriminatedUnion":
40
- return { oneOf: [...def.options.values()].map((o) => convert(o)) };
41
- case "ZodRecord":
42
- return {
43
- type: "object",
44
- additionalProperties: convert(def.valueType)
45
- };
46
- case "ZodTuple": {
47
- const items = def.items.map((item) => convert(item));
48
- return { type: "array", items: items.length === 1 ? items[0] : void 0, prefixItems: items };
49
- }
50
- case "ZodEffects":
51
- return convert(def.schema);
52
- case "ZodPipeline":
53
- return convert(def.in);
54
- case "ZodLazy":
55
- return convert(def.getter());
56
- case "ZodAny":
57
- return {};
58
- case "ZodUnknown":
59
- return {};
60
- default:
61
- return {};
62
- }
63
- }
64
- function convertString(def) {
65
- const schema = { type: "string" };
66
- if (def.checks) {
67
- for (const check of def.checks) {
68
- switch (check.kind) {
69
- case "min":
70
- schema.minLength = check.value;
71
- break;
72
- case "max":
73
- schema.maxLength = check.value;
74
- break;
75
- case "email":
76
- schema.format = "email";
77
- break;
78
- case "url":
79
- schema.format = "uri";
80
- break;
81
- case "uuid":
82
- schema.format = "uuid";
83
- break;
84
- case "regex":
85
- schema.pattern = check.regex.source;
86
- break;
87
- }
88
- }
89
- }
90
- if (def.description) schema.description = def.description;
91
- return schema;
92
- }
93
- function convertNumber(def) {
94
- const schema = { type: "number" };
95
- if (def.checks) {
96
- for (const check of def.checks) {
97
- switch (check.kind) {
98
- case "min":
99
- schema.minimum = check.value;
100
- if (check.inclusive === false) schema.exclusiveMinimum = check.value;
101
- break;
102
- case "max":
103
- schema.maximum = check.value;
104
- if (check.inclusive === false) schema.exclusiveMaximum = check.value;
105
- break;
106
- case "int":
107
- schema.type = "integer";
108
- break;
109
- }
110
- }
111
- }
112
- if (def.description) schema.description = def.description;
113
- return schema;
114
- }
115
- function convertObject(def) {
116
- const shape = def.shape();
117
- const properties = {};
118
- const required = [];
119
- for (const [key, value] of Object.entries(shape)) {
120
- properties[key] = convert(value);
121
- const fieldDef = value._def;
122
- const isOptional = fieldDef?.typeName === "ZodOptional" || fieldDef?.typeName === "ZodDefault";
123
- if (!isOptional) {
124
- required.push(key);
125
- }
126
- }
127
- const schema = { type: "object", properties };
128
- if (required.length > 0) schema.required = required;
129
- if (def.description) schema.description = def.description;
130
- return schema;
131
- }
132
- function convertArray(def) {
133
- const schema = {
134
- type: "array",
135
- items: convert(def.type)
136
- };
137
- if (def.minLength) schema.minItems = def.minLength.value;
138
- if (def.maxLength) schema.maxItems = def.maxLength.value;
139
- if (def.description) schema.description = def.description;
140
- return schema;
141
- }
142
-
143
6
  // src/mcp/server.ts
144
7
  var PARSE_ERROR = -32700;
145
8
  var INVALID_REQUEST = -32600;
@@ -368,6 +231,84 @@ var MCPServer = class {
368
231
  return Response.json(response);
369
232
  };
370
233
  }
234
+ /**
235
+ * Create a Streamable HTTP handler (MCP spec 2025-03-26).
236
+ * Supports both single JSON-RPC requests and SSE streaming for long-running operations.
237
+ * Compatible with Next.js route handlers, Deno, Bun, Cloudflare Workers.
238
+ *
239
+ * @example
240
+ * ```typescript
241
+ * // Next.js route: app/api/mcp/route.ts
242
+ * import { server } from '@/lib/mcp';
243
+ * export const POST = server.streamableHttpHandler();
244
+ * ```
245
+ */
246
+ streamableHttpHandler() {
247
+ return async (req) => {
248
+ const accept = req.headers.get("accept") || "";
249
+ const contentType = req.headers.get("content-type") || "";
250
+ if (!contentType.includes("application/json")) {
251
+ return Response.json(
252
+ { jsonrpc: "2.0", id: null, error: { code: PARSE_ERROR, message: "Content-Type must be application/json" } },
253
+ { status: 400 }
254
+ );
255
+ }
256
+ let message;
257
+ try {
258
+ message = await req.json();
259
+ } catch {
260
+ return Response.json(
261
+ { jsonrpc: "2.0", id: null, error: { code: PARSE_ERROR, message: "Parse error" } },
262
+ { status: 400 }
263
+ );
264
+ }
265
+ if (!message.jsonrpc || message.jsonrpc !== "2.0") {
266
+ return Response.json(
267
+ { jsonrpc: "2.0", id: message.id ?? null, error: { code: INVALID_REQUEST, message: "Invalid JSON-RPC version" } },
268
+ { status: 400 }
269
+ );
270
+ }
271
+ if (accept.includes("text/event-stream")) {
272
+ const encoder = new TextEncoder();
273
+ const self = this;
274
+ const sseStream = new ReadableStream({
275
+ async start(controller) {
276
+ try {
277
+ const response2 = await self.handleMessage(message);
278
+ if (response2) {
279
+ controller.enqueue(encoder.encode(`event: message
280
+ data: ${JSON.stringify(response2)}
281
+
282
+ `));
283
+ }
284
+ } catch (err) {
285
+ const errorResponse = {
286
+ jsonrpc: "2.0",
287
+ id: message.id ?? null,
288
+ error: { code: INTERNAL_ERROR, message: err instanceof Error ? err.message : "Internal error" }
289
+ };
290
+ controller.enqueue(encoder.encode(`event: message
291
+ data: ${JSON.stringify(errorResponse)}
292
+
293
+ `));
294
+ }
295
+ controller.close();
296
+ }
297
+ });
298
+ return new Response(sseStream, {
299
+ headers: {
300
+ "Content-Type": "text/event-stream",
301
+ "Cache-Control": "no-cache"
302
+ }
303
+ });
304
+ }
305
+ const response = await this.handleMessage(message);
306
+ if (!response) {
307
+ return new Response(null, { status: 204 });
308
+ }
309
+ return Response.json(response);
310
+ };
311
+ }
371
312
  };
372
313
  var McpError = class extends Error {
373
314
  constructor(code, message) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lantos1618/better-ui",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "A minimal, type-safe AI-first UI framework for building tools",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -36,6 +36,11 @@
36
36
  "import": "./dist/mcp/index.mjs",
37
37
  "require": "./dist/mcp/index.js"
38
38
  },
39
+ "./agui": {
40
+ "types": "./dist/agui/index.d.ts",
41
+ "import": "./dist/agui/index.mjs",
42
+ "require": "./dist/agui/index.js"
43
+ },
39
44
  "./theme.css": "./src/theme.css"
40
45
  },
41
46
  "files": [
@@ -50,7 +55,11 @@
50
55
  "ai",
51
56
  "react",
52
57
  "tools",
53
- "typescript"
58
+ "typescript",
59
+ "mcp",
60
+ "ag-ui",
61
+ "model-context-protocol",
62
+ "ai-tools"
54
63
  ],
55
64
  "author": "Lyndon Leong",
56
65
  "license": "MIT",
@@ -60,7 +69,7 @@
60
69
  },
61
70
  "scripts": {
62
71
  "build": "npm run build:lib",
63
- "build:lib": "tsup src/index.ts src/react/index.ts src/components/index.ts src/auth/index.ts src/persistence/index.ts src/mcp/index.ts --format cjs,esm --dts --clean --tsconfig tsconfig.lib.json",
72
+ "build:lib": "tsup src/index.ts src/react/index.ts src/components/index.ts src/auth/index.ts src/persistence/index.ts src/mcp/index.ts src/agui/index.ts --format cjs,esm --dts --clean --tsconfig tsconfig.lib.json",
64
73
  "test": "jest",
65
74
  "type-check": "tsc --noEmit",
66
75
  "prepublishOnly": "npm run build:lib"