@lantos1618/better-ui 0.4.1 → 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.
@@ -0,0 +1,386 @@
1
+ import "../chunk-Y6FXYEAI.mjs";
2
+
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
+ // src/mcp/server.ts
144
+ var PARSE_ERROR = -32700;
145
+ var INVALID_REQUEST = -32600;
146
+ var METHOD_NOT_FOUND = -32601;
147
+ var INVALID_PARAMS = -32602;
148
+ var INTERNAL_ERROR = -32603;
149
+ var MCPServer = class {
150
+ constructor(config) {
151
+ this.initialized = false;
152
+ this.running = false;
153
+ this.config = config;
154
+ }
155
+ /** Start listening on stdin for JSON-RPC messages */
156
+ start() {
157
+ if (this.running) return;
158
+ this.running = true;
159
+ const stdin = process.stdin;
160
+ const stdout = process.stdout;
161
+ stdin.setEncoding("utf-8");
162
+ let buffer = "";
163
+ stdin.on("data", (chunk) => {
164
+ buffer += chunk;
165
+ let newlineIdx;
166
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
167
+ const line = buffer.slice(0, newlineIdx).trim();
168
+ buffer = buffer.slice(newlineIdx + 1);
169
+ if (!line) continue;
170
+ this.handleLine(line).then((response) => {
171
+ if (response) {
172
+ stdout.write(JSON.stringify(response) + "\n");
173
+ }
174
+ }).catch((err) => {
175
+ const errorResponse = {
176
+ jsonrpc: "2.0",
177
+ id: null,
178
+ error: { code: INTERNAL_ERROR, message: err.message }
179
+ };
180
+ stdout.write(JSON.stringify(errorResponse) + "\n");
181
+ this.config.onError?.(err instanceof Error ? err : new Error(String(err)));
182
+ });
183
+ }
184
+ });
185
+ stdin.on("end", () => {
186
+ this.running = false;
187
+ });
188
+ this.config.onStart?.();
189
+ }
190
+ /** Stop the server */
191
+ stop() {
192
+ this.running = false;
193
+ }
194
+ /** Handle a single JSON-RPC message. Returns a response or null for notifications. */
195
+ async handleMessage(message) {
196
+ if (message.id === void 0 || message.id === null) {
197
+ if (message.method === "notifications/initialized") {
198
+ }
199
+ return null;
200
+ }
201
+ switch (message.method) {
202
+ case "initialize":
203
+ return this.handleInitialize(message);
204
+ case "tools/list":
205
+ return this.handleToolsList(message);
206
+ case "tools/call":
207
+ return this.handleToolsCall(message);
208
+ case "ping":
209
+ return { jsonrpc: "2.0", id: message.id, result: {} };
210
+ default:
211
+ return {
212
+ jsonrpc: "2.0",
213
+ id: message.id,
214
+ error: { code: METHOD_NOT_FOUND, message: `Method not found: ${message.method}` }
215
+ };
216
+ }
217
+ }
218
+ /** Get all tools as MCP tool schemas */
219
+ listTools() {
220
+ return Object.values(this.config.tools).map((tool) => ({
221
+ name: tool.name,
222
+ description: tool.description || tool.name,
223
+ inputSchema: zodToJsonSchema(tool.inputSchema)
224
+ }));
225
+ }
226
+ /** Execute a tool by name */
227
+ async callTool(name, args) {
228
+ if (!Object.prototype.hasOwnProperty.call(this.config.tools, name)) {
229
+ throw new McpError(INVALID_PARAMS, `Unknown tool: ${name}`);
230
+ }
231
+ const tool = this.config.tools[name];
232
+ try {
233
+ const result = await tool.run(args, {
234
+ isServer: true,
235
+ ...this.config.context
236
+ });
237
+ const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
238
+ return { content: [{ type: "text", text }] };
239
+ } catch (error) {
240
+ if (error instanceof Error && error.name === "ZodError") {
241
+ throw new McpError(INVALID_PARAMS, `Invalid input: ${error.message}`);
242
+ }
243
+ throw error;
244
+ }
245
+ }
246
+ // ─── Private ─────────────────────────────────────────────────────────────
247
+ async handleLine(line) {
248
+ let message;
249
+ try {
250
+ message = JSON.parse(line);
251
+ } catch {
252
+ return {
253
+ jsonrpc: "2.0",
254
+ id: null,
255
+ error: { code: PARSE_ERROR, message: "Parse error" }
256
+ };
257
+ }
258
+ if (!message.jsonrpc || message.jsonrpc !== "2.0") {
259
+ return {
260
+ jsonrpc: "2.0",
261
+ id: message.id ?? null,
262
+ error: { code: INVALID_REQUEST, message: "Invalid JSON-RPC version" }
263
+ };
264
+ }
265
+ return this.handleMessage(message);
266
+ }
267
+ handleInitialize(message) {
268
+ this.initialized = true;
269
+ return {
270
+ jsonrpc: "2.0",
271
+ id: message.id,
272
+ result: {
273
+ protocolVersion: "2024-11-05",
274
+ capabilities: {
275
+ tools: {}
276
+ },
277
+ serverInfo: {
278
+ name: this.config.name,
279
+ version: this.config.version
280
+ }
281
+ }
282
+ };
283
+ }
284
+ handleToolsList(message) {
285
+ return {
286
+ jsonrpc: "2.0",
287
+ id: message.id,
288
+ result: {
289
+ tools: this.listTools()
290
+ }
291
+ };
292
+ }
293
+ async handleToolsCall(message) {
294
+ const params = message.params;
295
+ const toolName = params?.name;
296
+ const args = params?.arguments ?? {};
297
+ if (!toolName) {
298
+ return {
299
+ jsonrpc: "2.0",
300
+ id: message.id,
301
+ error: { code: INVALID_PARAMS, message: "Missing tool name" }
302
+ };
303
+ }
304
+ try {
305
+ const result = await this.callTool(toolName, args);
306
+ return {
307
+ jsonrpc: "2.0",
308
+ id: message.id,
309
+ result
310
+ };
311
+ } catch (error) {
312
+ if (error instanceof McpError) {
313
+ return {
314
+ jsonrpc: "2.0",
315
+ id: message.id,
316
+ error: { code: error.code, message: error.message }
317
+ };
318
+ }
319
+ return {
320
+ jsonrpc: "2.0",
321
+ id: message.id,
322
+ result: {
323
+ content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }],
324
+ isError: true
325
+ }
326
+ };
327
+ }
328
+ }
329
+ /**
330
+ * Create a Web Request handler for HTTP-based MCP transport.
331
+ * Compatible with Next.js route handlers, Deno, Bun, Cloudflare Workers, etc.
332
+ *
333
+ * @example
334
+ * ```typescript
335
+ * // Next.js route: app/api/mcp/route.ts
336
+ * import { server } from '@/lib/mcp';
337
+ * export const POST = server.httpHandler();
338
+ * ```
339
+ */
340
+ httpHandler() {
341
+ return async (req) => {
342
+ const contentType = req.headers.get("content-type") || "";
343
+ if (!contentType.includes("application/json")) {
344
+ return Response.json(
345
+ { jsonrpc: "2.0", id: null, error: { code: PARSE_ERROR, message: "Content-Type must be application/json" } },
346
+ { status: 400 }
347
+ );
348
+ }
349
+ let message;
350
+ try {
351
+ message = await req.json();
352
+ } catch {
353
+ return Response.json(
354
+ { jsonrpc: "2.0", id: null, error: { code: PARSE_ERROR, message: "Parse error" } },
355
+ { status: 400 }
356
+ );
357
+ }
358
+ if (!message.jsonrpc || message.jsonrpc !== "2.0") {
359
+ return Response.json(
360
+ { jsonrpc: "2.0", id: message.id ?? null, error: { code: INVALID_REQUEST, message: "Invalid JSON-RPC version" } },
361
+ { status: 400 }
362
+ );
363
+ }
364
+ const response = await this.handleMessage(message);
365
+ if (!response) {
366
+ return new Response(null, { status: 204 });
367
+ }
368
+ return Response.json(response);
369
+ };
370
+ }
371
+ };
372
+ var McpError = class extends Error {
373
+ constructor(code, message) {
374
+ super(message);
375
+ this.code = code;
376
+ this.name = "McpError";
377
+ }
378
+ };
379
+ function createMCPServer(config) {
380
+ return new MCPServer(config);
381
+ }
382
+ export {
383
+ MCPServer,
384
+ createMCPServer,
385
+ zodToJsonSchema
386
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lantos1618/better-ui",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
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",
@@ -31,6 +31,11 @@
31
31
  "import": "./dist/persistence/index.mjs",
32
32
  "require": "./dist/persistence/index.js"
33
33
  },
34
+ "./mcp": {
35
+ "types": "./dist/mcp/index.d.ts",
36
+ "import": "./dist/mcp/index.mjs",
37
+ "require": "./dist/mcp/index.js"
38
+ },
34
39
  "./theme.css": "./src/theme.css"
35
40
  },
36
41
  "files": [
@@ -55,7 +60,7 @@
55
60
  },
56
61
  "scripts": {
57
62
  "build": "npm run build:lib",
58
- "build:lib": "tsup src/index.ts src/react/index.ts src/components/index.ts src/auth/index.ts src/persistence/index.ts --format cjs,esm --dts --clean --tsconfig tsconfig.lib.json",
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",
59
64
  "test": "jest",
60
65
  "type-check": "tsc --noEmit",
61
66
  "prepublishOnly": "npm run build:lib"