@lantos1618/better-ui 0.8.0 → 0.9.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.
@@ -1,5 +1,6 @@
1
1
  import { T as Tool, g as ToolContext } from '../tool-yZTixiN2.mjs';
2
- import { z } from 'zod';
2
+ export { z as zodToJsonSchema } from '../schema-DdZf6066.mjs';
3
+ import 'zod';
3
4
  import 'react';
4
5
 
5
6
  /**
@@ -127,33 +128,4 @@ declare class MCPServer {
127
128
  */
128
129
  declare function createMCPServer(config: MCPServerConfig): MCPServer;
129
130
 
130
- /**
131
- * Lightweight Zod-to-JSON-Schema converter.
132
- * Handles common Zod types without requiring zod-to-json-schema dependency.
133
- */
134
-
135
- interface JsonSchema {
136
- type?: string;
137
- properties?: Record<string, JsonSchema>;
138
- required?: string[];
139
- items?: JsonSchema;
140
- enum?: unknown[];
141
- description?: string;
142
- default?: unknown;
143
- minimum?: number;
144
- maximum?: number;
145
- minLength?: number;
146
- maxLength?: number;
147
- pattern?: string;
148
- format?: string;
149
- anyOf?: JsonSchema[];
150
- oneOf?: JsonSchema[];
151
- nullable?: boolean;
152
- additionalProperties?: boolean | JsonSchema;
153
- [key: string]: unknown;
154
- }
155
- declare function zodToJsonSchema(schema: z.ZodType | {
156
- [key: string]: any;
157
- }): JsonSchema;
158
-
159
- export { MCPServer, type MCPServerConfig, createMCPServer, zodToJsonSchema };
131
+ export { MCPServer, type MCPServerConfig, createMCPServer };
@@ -1,5 +1,6 @@
1
1
  import { T as Tool, g as ToolContext } from '../tool-yZTixiN2.js';
2
- import { z } from 'zod';
2
+ export { z as zodToJsonSchema } from '../schema-DdZf6066.js';
3
+ import 'zod';
3
4
  import 'react';
4
5
 
5
6
  /**
@@ -127,33 +128,4 @@ declare class MCPServer {
127
128
  */
128
129
  declare function createMCPServer(config: MCPServerConfig): MCPServer;
129
130
 
130
- /**
131
- * Lightweight Zod-to-JSON-Schema converter.
132
- * Handles common Zod types without requiring zod-to-json-schema dependency.
133
- */
134
-
135
- interface JsonSchema {
136
- type?: string;
137
- properties?: Record<string, JsonSchema>;
138
- required?: string[];
139
- items?: JsonSchema;
140
- enum?: unknown[];
141
- description?: string;
142
- default?: unknown;
143
- minimum?: number;
144
- maximum?: number;
145
- minLength?: number;
146
- maxLength?: number;
147
- pattern?: string;
148
- format?: string;
149
- anyOf?: JsonSchema[];
150
- oneOf?: JsonSchema[];
151
- nullable?: boolean;
152
- additionalProperties?: boolean | JsonSchema;
153
- [key: string]: unknown;
154
- }
155
- declare function zodToJsonSchema(schema: z.ZodType | {
156
- [key: string]: any;
157
- }): JsonSchema;
158
-
159
- export { MCPServer, type MCPServerConfig, createMCPServer, zodToJsonSchema };
131
+ export { MCPServer, type MCPServerConfig, createMCPServer };
@@ -0,0 +1,122 @@
1
+ import { T as Tool, g as ToolContext } from '../tool-yZTixiN2.mjs';
2
+ import { J as JsonSchema } from '../schema-DdZf6066.mjs';
3
+ import 'zod';
4
+ import 'react';
5
+
6
+ interface OpenAPISpecConfig {
7
+ /** API title */
8
+ title: string;
9
+ /** API version */
10
+ version: string;
11
+ /** Tool registry */
12
+ tools: Record<string, Tool>;
13
+ /** API description */
14
+ description?: string;
15
+ /** Base URL for servers (default: '/') */
16
+ serverUrl?: string;
17
+ /** Base path prefix for tool endpoints (default: '/api/tools') */
18
+ basePath?: string;
19
+ /** Tags to group endpoints */
20
+ tags?: Array<{
21
+ name: string;
22
+ description?: string;
23
+ }>;
24
+ }
25
+ interface OpenAPISpec {
26
+ openapi: '3.1.0';
27
+ info: {
28
+ title: string;
29
+ version: string;
30
+ description?: string;
31
+ };
32
+ servers: Array<{
33
+ url: string;
34
+ }>;
35
+ paths: Record<string, PathItem>;
36
+ components: {
37
+ schemas: Record<string, JsonSchema>;
38
+ };
39
+ tags?: Array<{
40
+ name: string;
41
+ description?: string;
42
+ }>;
43
+ }
44
+ interface PathItem {
45
+ post: {
46
+ operationId: string;
47
+ summary: string;
48
+ description?: string;
49
+ tags?: string[];
50
+ requestBody: {
51
+ required: true;
52
+ content: {
53
+ 'application/json': {
54
+ schema: {
55
+ $ref: string;
56
+ };
57
+ };
58
+ };
59
+ };
60
+ responses: Record<string, {
61
+ description: string;
62
+ content?: {
63
+ 'application/json': {
64
+ schema: {
65
+ $ref: string;
66
+ } | JsonSchema;
67
+ };
68
+ };
69
+ }>;
70
+ };
71
+ }
72
+ /**
73
+ * Generate an OpenAPI 3.1 spec from a Better UI tool registry.
74
+ */
75
+ declare function generateOpenAPISpec(config: OpenAPISpecConfig): OpenAPISpec;
76
+ /**
77
+ * Create a request handler that serves the OpenAPI spec as JSON.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * // Next.js route: app/api/openapi/route.ts
82
+ * export const GET = openAPIHandler({
83
+ * title: 'My Tools',
84
+ * version: '1.0.0',
85
+ * tools,
86
+ * });
87
+ * ```
88
+ */
89
+ declare function openAPIHandler(config: OpenAPISpecConfig): (req: Request) => Response;
90
+ interface ToolRouterConfig {
91
+ /** Tool registry */
92
+ tools: Record<string, Tool>;
93
+ /** Base path prefix (default: '/api/tools') */
94
+ basePath?: string;
95
+ /** Optional context passed to every tool execution */
96
+ context?: Partial<ToolContext>;
97
+ /** Called before tool execution — throw to reject */
98
+ onBeforeExecute?: (toolName: string, input: unknown, req: Request) => Promise<void> | void;
99
+ }
100
+ /**
101
+ * Create a request handler that routes to individual tool endpoints.
102
+ * Each tool is callable at `POST {basePath}/{toolName}`.
103
+ *
104
+ * Also serves the OpenAPI spec at `GET {basePath}` and a Swagger UI at `GET {basePath}/docs`.
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * // Next.js catch-all: app/api/tools/[...path]/route.ts
109
+ * import { toolRouter } from '@lantos1618/better-ui/openapi';
110
+ * const router = toolRouter({ tools });
111
+ * export const GET = router;
112
+ * export const POST = router;
113
+ *
114
+ * // Now callable:
115
+ * // POST /api/tools/weather { "city": "Tokyo" } -> { "result": { "temp": 22, ... } }
116
+ * // GET /api/tools -> OpenAPI JSON spec
117
+ * // GET /api/tools/docs -> Swagger UI
118
+ * ```
119
+ */
120
+ declare function toolRouter(config: ToolRouterConfig): (req: Request) => Promise<Response>;
121
+
122
+ export { type OpenAPISpec, type OpenAPISpecConfig, type ToolRouterConfig, generateOpenAPISpec, openAPIHandler, toolRouter };
@@ -0,0 +1,122 @@
1
+ import { T as Tool, g as ToolContext } from '../tool-yZTixiN2.js';
2
+ import { J as JsonSchema } from '../schema-DdZf6066.js';
3
+ import 'zod';
4
+ import 'react';
5
+
6
+ interface OpenAPISpecConfig {
7
+ /** API title */
8
+ title: string;
9
+ /** API version */
10
+ version: string;
11
+ /** Tool registry */
12
+ tools: Record<string, Tool>;
13
+ /** API description */
14
+ description?: string;
15
+ /** Base URL for servers (default: '/') */
16
+ serverUrl?: string;
17
+ /** Base path prefix for tool endpoints (default: '/api/tools') */
18
+ basePath?: string;
19
+ /** Tags to group endpoints */
20
+ tags?: Array<{
21
+ name: string;
22
+ description?: string;
23
+ }>;
24
+ }
25
+ interface OpenAPISpec {
26
+ openapi: '3.1.0';
27
+ info: {
28
+ title: string;
29
+ version: string;
30
+ description?: string;
31
+ };
32
+ servers: Array<{
33
+ url: string;
34
+ }>;
35
+ paths: Record<string, PathItem>;
36
+ components: {
37
+ schemas: Record<string, JsonSchema>;
38
+ };
39
+ tags?: Array<{
40
+ name: string;
41
+ description?: string;
42
+ }>;
43
+ }
44
+ interface PathItem {
45
+ post: {
46
+ operationId: string;
47
+ summary: string;
48
+ description?: string;
49
+ tags?: string[];
50
+ requestBody: {
51
+ required: true;
52
+ content: {
53
+ 'application/json': {
54
+ schema: {
55
+ $ref: string;
56
+ };
57
+ };
58
+ };
59
+ };
60
+ responses: Record<string, {
61
+ description: string;
62
+ content?: {
63
+ 'application/json': {
64
+ schema: {
65
+ $ref: string;
66
+ } | JsonSchema;
67
+ };
68
+ };
69
+ }>;
70
+ };
71
+ }
72
+ /**
73
+ * Generate an OpenAPI 3.1 spec from a Better UI tool registry.
74
+ */
75
+ declare function generateOpenAPISpec(config: OpenAPISpecConfig): OpenAPISpec;
76
+ /**
77
+ * Create a request handler that serves the OpenAPI spec as JSON.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * // Next.js route: app/api/openapi/route.ts
82
+ * export const GET = openAPIHandler({
83
+ * title: 'My Tools',
84
+ * version: '1.0.0',
85
+ * tools,
86
+ * });
87
+ * ```
88
+ */
89
+ declare function openAPIHandler(config: OpenAPISpecConfig): (req: Request) => Response;
90
+ interface ToolRouterConfig {
91
+ /** Tool registry */
92
+ tools: Record<string, Tool>;
93
+ /** Base path prefix (default: '/api/tools') */
94
+ basePath?: string;
95
+ /** Optional context passed to every tool execution */
96
+ context?: Partial<ToolContext>;
97
+ /** Called before tool execution — throw to reject */
98
+ onBeforeExecute?: (toolName: string, input: unknown, req: Request) => Promise<void> | void;
99
+ }
100
+ /**
101
+ * Create a request handler that routes to individual tool endpoints.
102
+ * Each tool is callable at `POST {basePath}/{toolName}`.
103
+ *
104
+ * Also serves the OpenAPI spec at `GET {basePath}` and a Swagger UI at `GET {basePath}/docs`.
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * // Next.js catch-all: app/api/tools/[...path]/route.ts
109
+ * import { toolRouter } from '@lantos1618/better-ui/openapi';
110
+ * const router = toolRouter({ tools });
111
+ * export const GET = router;
112
+ * export const POST = router;
113
+ *
114
+ * // Now callable:
115
+ * // POST /api/tools/weather { "city": "Tokyo" } -> { "result": { "temp": 22, ... } }
116
+ * // GET /api/tools -> OpenAPI JSON spec
117
+ * // GET /api/tools/docs -> Swagger UI
118
+ * ```
119
+ */
120
+ declare function toolRouter(config: ToolRouterConfig): (req: Request) => Promise<Response>;
121
+
122
+ export { type OpenAPISpec, type OpenAPISpecConfig, type ToolRouterConfig, generateOpenAPISpec, openAPIHandler, toolRouter };
@@ -0,0 +1,339 @@
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/openapi/index.ts
21
+ var openapi_exports = {};
22
+ __export(openapi_exports, {
23
+ generateOpenAPISpec: () => generateOpenAPISpec,
24
+ openAPIHandler: () => openAPIHandler,
25
+ toolRouter: () => toolRouter
26
+ });
27
+ module.exports = __toCommonJS(openapi_exports);
28
+
29
+ // src/mcp/schema.ts
30
+ function zodToJsonSchema(schema) {
31
+ return convert(schema);
32
+ }
33
+ function convert(schema) {
34
+ const def = schema._def;
35
+ const typeName = def?.typeName;
36
+ switch (typeName) {
37
+ case "ZodString":
38
+ return convertString(def);
39
+ case "ZodNumber":
40
+ return convertNumber(def);
41
+ case "ZodBoolean":
42
+ return { type: "boolean" };
43
+ case "ZodNull":
44
+ return { type: "null" };
45
+ case "ZodLiteral":
46
+ return { enum: [def.value] };
47
+ case "ZodEnum":
48
+ return { type: "string", enum: def.values };
49
+ case "ZodNativeEnum":
50
+ return { enum: Object.values(def.values) };
51
+ case "ZodObject":
52
+ return convertObject(def);
53
+ case "ZodArray":
54
+ return convertArray(def);
55
+ case "ZodOptional":
56
+ return convert(def.innerType);
57
+ case "ZodNullable": {
58
+ const inner = convert(def.innerType);
59
+ return { anyOf: [inner, { type: "null" }] };
60
+ }
61
+ case "ZodDefault":
62
+ return { ...convert(def.innerType), default: def.defaultValue() };
63
+ case "ZodUnion":
64
+ return { anyOf: def.options.map((o) => convert(o)) };
65
+ case "ZodDiscriminatedUnion":
66
+ return { oneOf: [...def.options.values()].map((o) => convert(o)) };
67
+ case "ZodRecord":
68
+ return {
69
+ type: "object",
70
+ additionalProperties: convert(def.valueType)
71
+ };
72
+ case "ZodTuple": {
73
+ const items = def.items.map((item) => convert(item));
74
+ return { type: "array", items: items.length === 1 ? items[0] : void 0, prefixItems: items };
75
+ }
76
+ case "ZodEffects":
77
+ return convert(def.schema);
78
+ case "ZodPipeline":
79
+ return convert(def.in);
80
+ case "ZodLazy":
81
+ return convert(def.getter());
82
+ case "ZodAny":
83
+ return {};
84
+ case "ZodUnknown":
85
+ return {};
86
+ default:
87
+ return {};
88
+ }
89
+ }
90
+ function convertString(def) {
91
+ const schema = { type: "string" };
92
+ if (def.checks) {
93
+ for (const check of def.checks) {
94
+ switch (check.kind) {
95
+ case "min":
96
+ schema.minLength = check.value;
97
+ break;
98
+ case "max":
99
+ schema.maxLength = check.value;
100
+ break;
101
+ case "email":
102
+ schema.format = "email";
103
+ break;
104
+ case "url":
105
+ schema.format = "uri";
106
+ break;
107
+ case "uuid":
108
+ schema.format = "uuid";
109
+ break;
110
+ case "regex":
111
+ schema.pattern = check.regex.source;
112
+ break;
113
+ }
114
+ }
115
+ }
116
+ if (def.description) schema.description = def.description;
117
+ return schema;
118
+ }
119
+ function convertNumber(def) {
120
+ const schema = { type: "number" };
121
+ if (def.checks) {
122
+ for (const check of def.checks) {
123
+ switch (check.kind) {
124
+ case "min":
125
+ schema.minimum = check.value;
126
+ if (check.inclusive === false) schema.exclusiveMinimum = check.value;
127
+ break;
128
+ case "max":
129
+ schema.maximum = check.value;
130
+ if (check.inclusive === false) schema.exclusiveMaximum = check.value;
131
+ break;
132
+ case "int":
133
+ schema.type = "integer";
134
+ break;
135
+ }
136
+ }
137
+ }
138
+ if (def.description) schema.description = def.description;
139
+ return schema;
140
+ }
141
+ function convertObject(def) {
142
+ const shape = def.shape();
143
+ const properties = {};
144
+ const required = [];
145
+ for (const [key, value] of Object.entries(shape)) {
146
+ properties[key] = convert(value);
147
+ const fieldDef = value._def;
148
+ const isOptional = fieldDef?.typeName === "ZodOptional" || fieldDef?.typeName === "ZodDefault";
149
+ if (!isOptional) {
150
+ required.push(key);
151
+ }
152
+ }
153
+ const schema = { type: "object", properties };
154
+ if (required.length > 0) schema.required = required;
155
+ if (def.description) schema.description = def.description;
156
+ return schema;
157
+ }
158
+ function convertArray(def) {
159
+ const schema = {
160
+ type: "array",
161
+ items: convert(def.type)
162
+ };
163
+ if (def.minLength) schema.minItems = def.minLength.value;
164
+ if (def.maxLength) schema.maxItems = def.maxLength.value;
165
+ if (def.description) schema.description = def.description;
166
+ return schema;
167
+ }
168
+
169
+ // src/openapi/index.ts
170
+ function generateOpenAPISpec(config) {
171
+ const basePath = config.basePath ?? "/api/tools";
172
+ const paths = {};
173
+ const schemas = {};
174
+ const autoTags = /* @__PURE__ */ new Set();
175
+ for (const [key, tool] of Object.entries(config.tools)) {
176
+ const name = tool.name || key;
177
+ const inputSchemaName = `${name}Input`;
178
+ const outputSchemaName = `${name}Output`;
179
+ schemas[inputSchemaName] = zodToJsonSchema(tool.inputSchema);
180
+ if (tool.outputSchema) {
181
+ schemas[outputSchemaName] = zodToJsonSchema(tool.outputSchema);
182
+ }
183
+ const toolTags = tool.tags.length > 0 ? tool.tags : [name];
184
+ for (const t of toolTags) autoTags.add(t);
185
+ const responseSchema = tool.outputSchema ? { $ref: `#/components/schemas/${outputSchemaName}` } : { type: "object" };
186
+ const hints = [];
187
+ if (tool.hints.destructive) hints.push("destructive");
188
+ if (tool.hints.readOnly) hints.push("read-only");
189
+ if (tool.hints.idempotent) hints.push("idempotent");
190
+ if (tool.requiresConfirmation) hints.push("requires confirmation");
191
+ const hintsStr = hints.length > 0 ? ` [${hints.join(", ")}]` : "";
192
+ paths[`${basePath}/${name}`] = {
193
+ post: {
194
+ operationId: name,
195
+ summary: tool.description || name,
196
+ description: (tool.description || "") + hintsStr,
197
+ tags: tool.tags.length > 0 ? tool.tags : void 0,
198
+ requestBody: {
199
+ required: true,
200
+ content: {
201
+ "application/json": {
202
+ schema: { $ref: `#/components/schemas/${inputSchemaName}` }
203
+ }
204
+ }
205
+ },
206
+ responses: {
207
+ "200": {
208
+ description: "Successful tool execution",
209
+ content: {
210
+ "application/json": {
211
+ schema: {
212
+ type: "object",
213
+ properties: {
214
+ result: responseSchema
215
+ }
216
+ }
217
+ }
218
+ }
219
+ },
220
+ "400": {
221
+ description: "Invalid input (Zod validation error)"
222
+ },
223
+ "404": {
224
+ description: "Tool not found"
225
+ },
226
+ "500": {
227
+ description: "Tool execution failed"
228
+ }
229
+ }
230
+ }
231
+ };
232
+ }
233
+ const tags = config.tags ?? [...autoTags].map((t) => ({ name: t }));
234
+ return {
235
+ openapi: "3.1.0",
236
+ info: {
237
+ title: config.title,
238
+ version: config.version,
239
+ ...config.description ? { description: config.description } : {}
240
+ },
241
+ servers: [{ url: config.serverUrl ?? "/" }],
242
+ paths,
243
+ components: { schemas },
244
+ ...tags.length > 0 ? { tags } : {}
245
+ };
246
+ }
247
+ function openAPIHandler(config) {
248
+ const spec = generateOpenAPISpec(config);
249
+ const json = JSON.stringify(spec, null, 2);
250
+ return () => new Response(json, {
251
+ headers: {
252
+ "Content-Type": "application/json",
253
+ "Access-Control-Allow-Origin": "*"
254
+ }
255
+ });
256
+ }
257
+ function toolRouter(config) {
258
+ const basePath = config.basePath ?? "/api/tools";
259
+ return async (req) => {
260
+ const url = new URL(req.url);
261
+ const path = url.pathname;
262
+ if (req.method === "OPTIONS") {
263
+ return new Response(null, {
264
+ headers: {
265
+ "Access-Control-Allow-Origin": "*",
266
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
267
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
268
+ }
269
+ });
270
+ }
271
+ if (req.method === "GET" && (path === basePath || path === basePath + "/")) {
272
+ const spec = generateOpenAPISpec({
273
+ title: "Tool API",
274
+ version: "1.0.0",
275
+ tools: config.tools,
276
+ basePath,
277
+ serverUrl: url.origin
278
+ });
279
+ return new Response(JSON.stringify(spec, null, 2), {
280
+ headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
281
+ });
282
+ }
283
+ if (req.method === "GET" && (path === basePath + "/docs" || path === basePath + "/docs/")) {
284
+ const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>API Docs</title>
285
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
286
+ </head><body><div id="swagger-ui"></div>
287
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
288
+ <script>SwaggerUIBundle({url:'${basePath}',dom_id:'#swagger-ui',deepLinking:true});</script>
289
+ </body></html>`;
290
+ return new Response(html, {
291
+ headers: { "Content-Type": "text/html", "Access-Control-Allow-Origin": "*" }
292
+ });
293
+ }
294
+ if (req.method === "POST" && path.startsWith(basePath + "/")) {
295
+ const toolName = path.slice(basePath.length + 1).replace(/\/$/, "");
296
+ if (!toolName) {
297
+ return Response.json({ error: "Missing tool name" }, { status: 400 });
298
+ }
299
+ if (!Object.prototype.hasOwnProperty.call(config.tools, toolName)) {
300
+ return Response.json({ error: "Tool not found" }, { status: 404 });
301
+ }
302
+ const tool = config.tools[toolName];
303
+ let input;
304
+ try {
305
+ input = await req.json();
306
+ } catch {
307
+ return Response.json({ error: "Invalid JSON body" }, { status: 400 });
308
+ }
309
+ try {
310
+ if (config.onBeforeExecute) {
311
+ await config.onBeforeExecute(toolName, input, req);
312
+ }
313
+ const result = await tool.run(input, {
314
+ isServer: true,
315
+ headers: req.headers,
316
+ ...config.context
317
+ });
318
+ return Response.json({ result }, {
319
+ headers: { "Access-Control-Allow-Origin": "*" }
320
+ });
321
+ } catch (error) {
322
+ if (error instanceof Error && error.name === "ZodError") {
323
+ return Response.json({ error: error.message }, { status: 400 });
324
+ }
325
+ return Response.json(
326
+ { error: error instanceof Error ? error.message : "Tool execution failed" },
327
+ { status: 500 }
328
+ );
329
+ }
330
+ }
331
+ return Response.json({ error: "Not found" }, { status: 404 });
332
+ };
333
+ }
334
+ // Annotate the CommonJS export names for ESM import in node:
335
+ 0 && (module.exports = {
336
+ generateOpenAPISpec,
337
+ openAPIHandler,
338
+ toolRouter
339
+ });
@@ -0,0 +1,175 @@
1
+ import {
2
+ zodToJsonSchema
3
+ } from "../chunk-OH73K7I5.mjs";
4
+ import "../chunk-Y6FXYEAI.mjs";
5
+
6
+ // src/openapi/index.ts
7
+ function generateOpenAPISpec(config) {
8
+ const basePath = config.basePath ?? "/api/tools";
9
+ const paths = {};
10
+ const schemas = {};
11
+ const autoTags = /* @__PURE__ */ new Set();
12
+ for (const [key, tool] of Object.entries(config.tools)) {
13
+ const name = tool.name || key;
14
+ const inputSchemaName = `${name}Input`;
15
+ const outputSchemaName = `${name}Output`;
16
+ schemas[inputSchemaName] = zodToJsonSchema(tool.inputSchema);
17
+ if (tool.outputSchema) {
18
+ schemas[outputSchemaName] = zodToJsonSchema(tool.outputSchema);
19
+ }
20
+ const toolTags = tool.tags.length > 0 ? tool.tags : [name];
21
+ for (const t of toolTags) autoTags.add(t);
22
+ const responseSchema = tool.outputSchema ? { $ref: `#/components/schemas/${outputSchemaName}` } : { type: "object" };
23
+ const hints = [];
24
+ if (tool.hints.destructive) hints.push("destructive");
25
+ if (tool.hints.readOnly) hints.push("read-only");
26
+ if (tool.hints.idempotent) hints.push("idempotent");
27
+ if (tool.requiresConfirmation) hints.push("requires confirmation");
28
+ const hintsStr = hints.length > 0 ? ` [${hints.join(", ")}]` : "";
29
+ paths[`${basePath}/${name}`] = {
30
+ post: {
31
+ operationId: name,
32
+ summary: tool.description || name,
33
+ description: (tool.description || "") + hintsStr,
34
+ tags: tool.tags.length > 0 ? tool.tags : void 0,
35
+ requestBody: {
36
+ required: true,
37
+ content: {
38
+ "application/json": {
39
+ schema: { $ref: `#/components/schemas/${inputSchemaName}` }
40
+ }
41
+ }
42
+ },
43
+ responses: {
44
+ "200": {
45
+ description: "Successful tool execution",
46
+ content: {
47
+ "application/json": {
48
+ schema: {
49
+ type: "object",
50
+ properties: {
51
+ result: responseSchema
52
+ }
53
+ }
54
+ }
55
+ }
56
+ },
57
+ "400": {
58
+ description: "Invalid input (Zod validation error)"
59
+ },
60
+ "404": {
61
+ description: "Tool not found"
62
+ },
63
+ "500": {
64
+ description: "Tool execution failed"
65
+ }
66
+ }
67
+ }
68
+ };
69
+ }
70
+ const tags = config.tags ?? [...autoTags].map((t) => ({ name: t }));
71
+ return {
72
+ openapi: "3.1.0",
73
+ info: {
74
+ title: config.title,
75
+ version: config.version,
76
+ ...config.description ? { description: config.description } : {}
77
+ },
78
+ servers: [{ url: config.serverUrl ?? "/" }],
79
+ paths,
80
+ components: { schemas },
81
+ ...tags.length > 0 ? { tags } : {}
82
+ };
83
+ }
84
+ function openAPIHandler(config) {
85
+ const spec = generateOpenAPISpec(config);
86
+ const json = JSON.stringify(spec, null, 2);
87
+ return () => new Response(json, {
88
+ headers: {
89
+ "Content-Type": "application/json",
90
+ "Access-Control-Allow-Origin": "*"
91
+ }
92
+ });
93
+ }
94
+ function toolRouter(config) {
95
+ const basePath = config.basePath ?? "/api/tools";
96
+ return async (req) => {
97
+ const url = new URL(req.url);
98
+ const path = url.pathname;
99
+ if (req.method === "OPTIONS") {
100
+ return new Response(null, {
101
+ headers: {
102
+ "Access-Control-Allow-Origin": "*",
103
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
104
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
105
+ }
106
+ });
107
+ }
108
+ if (req.method === "GET" && (path === basePath || path === basePath + "/")) {
109
+ const spec = generateOpenAPISpec({
110
+ title: "Tool API",
111
+ version: "1.0.0",
112
+ tools: config.tools,
113
+ basePath,
114
+ serverUrl: url.origin
115
+ });
116
+ return new Response(JSON.stringify(spec, null, 2), {
117
+ headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
118
+ });
119
+ }
120
+ if (req.method === "GET" && (path === basePath + "/docs" || path === basePath + "/docs/")) {
121
+ const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>API Docs</title>
122
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
123
+ </head><body><div id="swagger-ui"></div>
124
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
125
+ <script>SwaggerUIBundle({url:'${basePath}',dom_id:'#swagger-ui',deepLinking:true});</script>
126
+ </body></html>`;
127
+ return new Response(html, {
128
+ headers: { "Content-Type": "text/html", "Access-Control-Allow-Origin": "*" }
129
+ });
130
+ }
131
+ if (req.method === "POST" && path.startsWith(basePath + "/")) {
132
+ const toolName = path.slice(basePath.length + 1).replace(/\/$/, "");
133
+ if (!toolName) {
134
+ return Response.json({ error: "Missing tool name" }, { status: 400 });
135
+ }
136
+ if (!Object.prototype.hasOwnProperty.call(config.tools, toolName)) {
137
+ return Response.json({ error: "Tool not found" }, { status: 404 });
138
+ }
139
+ const tool = config.tools[toolName];
140
+ let input;
141
+ try {
142
+ input = await req.json();
143
+ } catch {
144
+ return Response.json({ error: "Invalid JSON body" }, { status: 400 });
145
+ }
146
+ try {
147
+ if (config.onBeforeExecute) {
148
+ await config.onBeforeExecute(toolName, input, req);
149
+ }
150
+ const result = await tool.run(input, {
151
+ isServer: true,
152
+ headers: req.headers,
153
+ ...config.context
154
+ });
155
+ return Response.json({ result }, {
156
+ headers: { "Access-Control-Allow-Origin": "*" }
157
+ });
158
+ } catch (error) {
159
+ if (error instanceof Error && error.name === "ZodError") {
160
+ return Response.json({ error: error.message }, { status: 400 });
161
+ }
162
+ return Response.json(
163
+ { error: error instanceof Error ? error.message : "Tool execution failed" },
164
+ { status: 500 }
165
+ );
166
+ }
167
+ }
168
+ return Response.json({ error: "Not found" }, { status: 404 });
169
+ };
170
+ }
171
+ export {
172
+ generateOpenAPISpec,
173
+ openAPIHandler,
174
+ toolRouter
175
+ };
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Lightweight Zod-to-JSON-Schema converter.
5
+ * Handles common Zod types without requiring zod-to-json-schema dependency.
6
+ */
7
+
8
+ interface JsonSchema {
9
+ type?: string;
10
+ properties?: Record<string, JsonSchema>;
11
+ required?: string[];
12
+ items?: JsonSchema;
13
+ enum?: unknown[];
14
+ description?: string;
15
+ default?: unknown;
16
+ minimum?: number;
17
+ maximum?: number;
18
+ minLength?: number;
19
+ maxLength?: number;
20
+ pattern?: string;
21
+ format?: string;
22
+ anyOf?: JsonSchema[];
23
+ oneOf?: JsonSchema[];
24
+ nullable?: boolean;
25
+ additionalProperties?: boolean | JsonSchema;
26
+ [key: string]: unknown;
27
+ }
28
+ declare function zodToJsonSchema(schema: z.ZodType | {
29
+ [key: string]: any;
30
+ }): JsonSchema;
31
+
32
+ export { type JsonSchema as J, zodToJsonSchema as z };
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Lightweight Zod-to-JSON-Schema converter.
5
+ * Handles common Zod types without requiring zod-to-json-schema dependency.
6
+ */
7
+
8
+ interface JsonSchema {
9
+ type?: string;
10
+ properties?: Record<string, JsonSchema>;
11
+ required?: string[];
12
+ items?: JsonSchema;
13
+ enum?: unknown[];
14
+ description?: string;
15
+ default?: unknown;
16
+ minimum?: number;
17
+ maximum?: number;
18
+ minLength?: number;
19
+ maxLength?: number;
20
+ pattern?: string;
21
+ format?: string;
22
+ anyOf?: JsonSchema[];
23
+ oneOf?: JsonSchema[];
24
+ nullable?: boolean;
25
+ additionalProperties?: boolean | JsonSchema;
26
+ [key: string]: unknown;
27
+ }
28
+ declare function zodToJsonSchema(schema: z.ZodType | {
29
+ [key: string]: any;
30
+ }): JsonSchema;
31
+
32
+ export { type JsonSchema as J, zodToJsonSchema as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lantos1618/better-ui",
3
- "version": "0.8.0",
3
+ "version": "0.9.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",
@@ -41,6 +41,11 @@
41
41
  "import": "./dist/agui/index.mjs",
42
42
  "require": "./dist/agui/index.js"
43
43
  },
44
+ "./openapi": {
45
+ "types": "./dist/openapi/index.d.ts",
46
+ "import": "./dist/openapi/index.mjs",
47
+ "require": "./dist/openapi/index.js"
48
+ },
44
49
  "./theme.css": "./src/theme.css"
45
50
  },
46
51
  "files": [
@@ -69,7 +74,7 @@
69
74
  },
70
75
  "scripts": {
71
76
  "build": "npm run build:lib",
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",
77
+ "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 src/openapi/index.ts --format cjs,esm --dts --clean --tsconfig tsconfig.lib.json",
73
78
  "test": "jest",
74
79
  "type-check": "tsc --noEmit",
75
80
  "prepublishOnly": "npm run build:lib"