@mcp-html-bridge/ui-engine 0.5.1 → 0.6.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.
Files changed (52) hide show
  1. package/dist/bridge.d.ts +3 -0
  2. package/dist/bridge.d.ts.map +1 -0
  3. package/{src/bridge.ts → dist/bridge.js} +6 -3
  4. package/dist/bridge.js.map +1 -0
  5. package/dist/engine.d.ts +26 -0
  6. package/dist/engine.d.ts.map +1 -0
  7. package/dist/engine.js +92 -0
  8. package/dist/engine.js.map +1 -0
  9. package/dist/html-builder.d.ts +16 -0
  10. package/dist/html-builder.d.ts.map +1 -0
  11. package/dist/html-builder.js +54 -0
  12. package/dist/html-builder.js.map +1 -0
  13. package/{src/index.ts → dist/index.d.ts} +3 -26
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +36 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/llm-renderer.d.ts +36 -0
  18. package/dist/llm-renderer.d.ts.map +1 -0
  19. package/dist/llm-renderer.js +103 -0
  20. package/dist/llm-renderer.js.map +1 -0
  21. package/dist/playground.d.ts +4 -0
  22. package/dist/playground.d.ts.map +1 -0
  23. package/{src/playground.ts → dist/playground.js} +12 -11
  24. package/dist/playground.js.map +1 -0
  25. package/dist/renderer.d.ts +7 -0
  26. package/dist/renderer.d.ts.map +1 -0
  27. package/dist/renderer.js +345 -0
  28. package/dist/renderer.js.map +1 -0
  29. package/dist/renderers/form.d.ts +4 -0
  30. package/dist/renderers/form.d.ts.map +1 -0
  31. package/dist/renderers/form.js +181 -0
  32. package/dist/renderers/form.js.map +1 -0
  33. package/dist/theme.d.ts +2 -0
  34. package/dist/theme.d.ts.map +1 -0
  35. package/{src/theme.ts → dist/theme.js} +6 -3
  36. package/dist/theme.js.map +1 -0
  37. package/dist/types.d.ts +50 -0
  38. package/dist/types.d.ts.map +1 -0
  39. package/dist/types.js +3 -0
  40. package/dist/types.js.map +1 -0
  41. package/dist/utilities.d.ts +19 -0
  42. package/dist/utilities.d.ts.map +1 -0
  43. package/{src/utilities.ts → dist/utilities.js} +6 -3
  44. package/dist/utilities.js.map +1 -0
  45. package/package.json +4 -1
  46. package/src/engine.ts +0 -118
  47. package/src/html-builder.ts +0 -61
  48. package/src/llm-renderer.ts +0 -129
  49. package/src/renderer.ts +0 -380
  50. package/src/renderers/form.ts +0 -200
  51. package/src/types.ts +0 -60
  52. package/tsconfig.json +0 -8
package/src/renderer.ts DELETED
@@ -1,380 +0,0 @@
1
- /**
2
- * Universal JSON → HTML renderer.
3
- *
4
- * No hardcoded business logic. No status badges, no price formatting,
5
- * no regex-based field type guessing. Just clean structural rendering
6
- * that handles any JSON shape gracefully:
7
- *
8
- * Array<Object> → sortable <table> (union of all keys)
9
- * Object → <dl> key-value pairs (recursive for nesting)
10
- * Array<mixed> → <ul> list with recursive items
11
- * string → text (auto-links URLs)
12
- * number → number
13
- * boolean → true/false
14
- * null → placeholder
15
- * empty → explicit empty indicator
16
- *
17
- * All formatting decisions are the caller's responsibility (LLM or user).
18
- */
19
- import { escapeHtml } from './html-builder.js';
20
-
21
- // ── Constants ──
22
-
23
- const MAX_DEPTH = 30;
24
- const MAX_TABLE_ROWS = 500;
25
- const MAX_LIST_ITEMS = 200;
26
- const URL_REGEX = /^https?:\/\/[^\s<>"{}|\\^`[\]]+$/;
27
- const IMAGE_EXT_REGEX = /\.(png|jpe?g|gif|webp|svg|ico|bmp)(\?[^\s]*)?$/i;
28
-
29
- // ── Structural detection ──
30
-
31
- function isArrayOfObjects(data: unknown): data is Record<string, unknown>[] {
32
- if (!Array.isArray(data) || data.length === 0) return false;
33
- // At least 80% must be non-null objects (tolerates a few nulls in the array)
34
- let objCount = 0;
35
- for (const item of data) {
36
- if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
37
- objCount++;
38
- }
39
- }
40
- return objCount / data.length >= 0.8 && objCount >= 1;
41
- }
42
-
43
- function isFlatObject(data: unknown): data is Record<string, unknown> {
44
- if (data === null || typeof data !== 'object' || Array.isArray(data)) return false;
45
- const values = Object.values(data as Record<string, unknown>);
46
- if (values.length === 0) return true;
47
- return values.every(v => v === null || typeof v !== 'object');
48
- }
49
-
50
- function humanizeKey(key: string): string {
51
- return key
52
- .replace(/([A-Z])/g, ' $1')
53
- .replace(/[_-]/g, ' ')
54
- .replace(/^\w/, c => c.toUpperCase())
55
- .trim();
56
- }
57
-
58
- function isUrl(value: string): boolean {
59
- return URL_REGEX.test(value);
60
- }
61
-
62
- function isImageUrl(value: string): boolean {
63
- return IMAGE_EXT_REGEX.test(value);
64
- }
65
-
66
- // ── Render primitives ──
67
-
68
- function renderValue(value: unknown, depth: number): string {
69
- if (value === null || value === undefined) {
70
- return '<span class="mcp-null">\u2014</span>';
71
- }
72
- if (typeof value === 'boolean') {
73
- return `<span class="mcp-bool mcp-bool-${value}">${value}</span>`;
74
- }
75
- if (typeof value === 'number') {
76
- return `<span class="mcp-num">${value}</span>`;
77
- }
78
- if (typeof value === 'string') {
79
- if (value.length === 0) {
80
- return '<span class="mcp-null">(empty)</span>';
81
- }
82
- // Auto-link URLs
83
- if (isUrl(value)) {
84
- const escaped = escapeHtml(value);
85
- if (isImageUrl(value)) {
86
- return `<a href="${escaped}" target="_blank" rel="noopener"><img class="mcp-img" src="${escaped}" alt="" loading="lazy"></a>`;
87
- }
88
- return `<a class="mcp-link" href="${escaped}" target="_blank" rel="noopener">${escaped}</a>`;
89
- }
90
- if (value.length > 300) {
91
- return `<div class="mcp-text">${escapeHtml(value)}</div>`;
92
- }
93
- return escapeHtml(value);
94
- }
95
- // Recurse into objects/arrays
96
- return renderAny(value, depth + 1);
97
- }
98
-
99
- // ── Core recursive renderer ──
100
-
101
- /** Collect union of all keys across an array of objects, preserving order */
102
- function collectColumns(rows: Record<string, unknown>[]): string[] {
103
- const seen = new Set<string>();
104
- const columns: string[] = [];
105
- for (const row of rows) {
106
- for (const key of Object.keys(row)) {
107
- if (!seen.has(key)) {
108
- seen.add(key);
109
- columns.push(key);
110
- }
111
- }
112
- }
113
- return columns;
114
- }
115
-
116
- function renderTable(rows: Record<string, unknown>[], tableId: string): string {
117
- const columns = collectColumns(rows);
118
- const truncated = rows.length > MAX_TABLE_ROWS;
119
- const displayRows = truncated ? rows.slice(0, MAX_TABLE_ROWS) : rows;
120
-
121
- const headerCells = columns
122
- .map((col, i) =>
123
- `<th onclick="__mcpSort('${escapeHtml(tableId)}',${i})" class="mcp-sortable">${escapeHtml(humanizeKey(col))}<span class="mcp-sort-icon">\u21C5</span></th>`)
124
- .join('');
125
-
126
- const bodyRows = displayRows
127
- .map(row => {
128
- const cells = columns.map(col => {
129
- const val = Object.prototype.hasOwnProperty.call(row, col) ? row[col] : undefined;
130
- return `<td>${renderValue(val, 1)}</td>`;
131
- }).join('');
132
- return `<tr>${cells}</tr>`;
133
- })
134
- .join('\n');
135
-
136
- const meta = truncated
137
- ? `${rows.length} rows \u00D7 ${columns.length} columns (showing first ${MAX_TABLE_ROWS})`
138
- : `${rows.length} rows \u00D7 ${columns.length} columns`;
139
-
140
- return `<div class="mcp-table-wrap">
141
- <div class="mcp-table-meta">${meta}</div>
142
- <div class="mcp-table-scroll">
143
- <table class="mcp-table" id="${escapeHtml(tableId)}">
144
- <thead><tr>${headerCells}</tr></thead>
145
- <tbody>${bodyRows}</tbody>
146
- </table>
147
- </div>
148
- </div>`;
149
- }
150
-
151
- function renderKeyValue(obj: Record<string, unknown>, depth: number): string {
152
- const entries = Object.entries(obj);
153
- if (entries.length === 0) {
154
- return '<div class="mcp-empty">(empty object)</div>';
155
- }
156
- const items = entries.map(([key, val]) => {
157
- const rendered = renderValue(val, depth);
158
- return `<div class="mcp-kv">
159
- <dt class="mcp-key">${escapeHtml(humanizeKey(key))}</dt>
160
- <dd class="mcp-val">${rendered}</dd>
161
- </div>`;
162
- }).join('\n');
163
-
164
- return `<dl class="mcp-dl">${items}</dl>`;
165
- }
166
-
167
- function renderList(arr: unknown[], depth: number): string {
168
- if (arr.length === 0) {
169
- return '<div class="mcp-empty">(empty array)</div>';
170
- }
171
- const truncated = arr.length > MAX_LIST_ITEMS;
172
- const displayItems = truncated ? arr.slice(0, MAX_LIST_ITEMS) : arr;
173
-
174
- const items = displayItems.map(item =>
175
- `<li>${renderValue(item, depth)}</li>`
176
- ).join('\n');
177
-
178
- const suffix = truncated
179
- ? `<li class="mcp-truncated">\u2026 and ${arr.length - MAX_LIST_ITEMS} more items</li>`
180
- : '';
181
-
182
- return `<ul class="mcp-list">${items}\n${suffix}</ul>`;
183
- }
184
-
185
- function renderCollapsible(label: string, content: string, open = true): string {
186
- return `<details class="mcp-details" ${open ? 'open' : ''}>
187
- <summary class="mcp-summary">${escapeHtml(label)}</summary>
188
- <div class="mcp-details-body">${content}</div>
189
- </details>`;
190
- }
191
-
192
- // Global table counter for unique IDs
193
- let tableCounter = 0;
194
-
195
- function renderAny(data: unknown, depth: number): string {
196
- // Depth guard
197
- if (depth > MAX_DEPTH) {
198
- return '<span class="mcp-null">(max depth reached)</span>';
199
- }
200
-
201
- // Primitive
202
- if (data === null || data === undefined || typeof data !== 'object') {
203
- return renderValue(data, depth);
204
- }
205
-
206
- // Array of objects → table
207
- if (isArrayOfObjects(data)) {
208
- // Filter to only objects for the table, skip non-objects
209
- const objectRows = (data as unknown[]).filter(
210
- (item): item is Record<string, unknown> =>
211
- item !== null && typeof item === 'object' && !Array.isArray(item)
212
- );
213
- const tableId = `mcp-grid-${tableCounter++}`;
214
- const content = renderTable(objectRows, tableId);
215
- return depth > 0 ? renderCollapsible(`Array (${data.length} items)`, content) : content;
216
- }
217
-
218
- // Generic array → list or collapsible
219
- if (Array.isArray(data)) {
220
- const content = renderList(data, depth);
221
- return depth > 0 ? renderCollapsible(`Array (${data.length})`, content) : content;
222
- }
223
-
224
- // Flat object → key-value pairs
225
- const obj = data as Record<string, unknown>;
226
- if (isFlatObject(obj)) {
227
- return renderKeyValue(obj, depth);
228
- }
229
-
230
- // Nested object → grouped sections
231
- const entries = Object.entries(obj);
232
- if (entries.length === 0) {
233
- return '<div class="mcp-empty">(empty object)</div>';
234
- }
235
-
236
- const sections = entries.map(([key, val]) => {
237
- if (val !== null && typeof val === 'object') {
238
- return renderCollapsible(humanizeKey(key), renderAny(val, depth + 1), depth < 2);
239
- }
240
- return `<div class="mcp-kv">
241
- <dt class="mcp-key">${escapeHtml(humanizeKey(key))}</dt>
242
- <dd class="mcp-val">${renderValue(val, depth)}</dd>
243
- </div>`;
244
- });
245
-
246
- // If all entries are primitives at this level, use dl
247
- const allPrimitive = entries.every(([, v]) => v === null || typeof v !== 'object');
248
- if (allPrimitive) {
249
- return renderKeyValue(obj, depth);
250
- }
251
-
252
- return `<div class="mcp-section">${sections.join('\n')}</div>`;
253
- }
254
-
255
- // ── Public API ──
256
-
257
- /** Render any JSON data as an HTML fragment. No business logic, pure structure. */
258
- export function renderJSON(data: unknown): string {
259
- tableCounter = 0; // Reset per render call
260
- return `<div class="mcp-root">${renderAny(data, 0)}</div>`;
261
- }
262
-
263
- /** Get the CSS for the universal renderer */
264
- export function getRendererCSS(): string {
265
- return `
266
- .mcp-root { max-width: 960px; margin: 0 auto; }
267
-
268
- /* Table */
269
- .mcp-table-wrap { overflow: hidden; }
270
- .mcp-table-meta {
271
- font-size: var(--text-xs); color: var(--text-tertiary);
272
- padding-bottom: var(--sp-2); margin-bottom: var(--sp-2);
273
- border-bottom: 1px solid var(--border);
274
- }
275
- .mcp-table-scroll { overflow-x: auto; }
276
- .mcp-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
277
- .mcp-table th {
278
- text-align: left; padding: var(--sp-2) var(--sp-3);
279
- font-weight: 600; color: var(--text-secondary); font-size: var(--text-xs);
280
- text-transform: uppercase; letter-spacing: 0.05em;
281
- border-bottom: 2px solid var(--border);
282
- white-space: nowrap; user-select: none;
283
- position: sticky; top: 0; background: var(--bg-primary); z-index: 1;
284
- }
285
- .mcp-sortable { cursor: pointer; }
286
- .mcp-sortable:hover { color: var(--accent); }
287
- .mcp-sort-icon { margin-left: 4px; opacity: 0.3; font-size: 10px; }
288
- .mcp-table td {
289
- padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border);
290
- vertical-align: top; max-width: 400px;
291
- }
292
- .mcp-table tbody tr:hover { background: var(--accent-subtle); }
293
-
294
- /* Key-Value */
295
- .mcp-dl { display: grid; grid-template-columns: 1fr; gap: 0; }
296
- .mcp-kv {
297
- display: grid; grid-template-columns: minmax(120px, auto) 1fr;
298
- gap: var(--sp-3); padding: var(--sp-2) 0;
299
- border-bottom: 1px solid var(--border);
300
- }
301
- .mcp-kv:last-child { border-bottom: none; }
302
- .mcp-key {
303
- font-weight: 600; font-size: var(--text-sm); color: var(--text-secondary);
304
- word-break: break-word;
305
- }
306
- .mcp-val { font-size: var(--text-sm); word-break: break-word; }
307
-
308
- /* List */
309
- .mcp-list {
310
- list-style: none; padding: 0; margin: 0;
311
- display: flex; flex-direction: column; gap: var(--sp-1);
312
- }
313
- .mcp-list li {
314
- padding: var(--sp-1) var(--sp-2); font-size: var(--text-sm);
315
- border-left: 2px solid var(--border); margin-left: var(--sp-2);
316
- }
317
-
318
- /* Collapsible sections */
319
- .mcp-details {
320
- border: 1px solid var(--border); border-radius: var(--radius-sm);
321
- margin-bottom: var(--sp-2);
322
- }
323
- .mcp-summary {
324
- padding: var(--sp-2) var(--sp-3); font-weight: 600; font-size: var(--text-sm);
325
- cursor: pointer; user-select: none;
326
- }
327
- .mcp-summary:hover { color: var(--accent); }
328
- .mcp-details-body { padding: var(--sp-3); border-top: 1px solid var(--border); }
329
-
330
- /* Sections */
331
- .mcp-section { display: flex; flex-direction: column; gap: var(--sp-3); }
332
-
333
- /* Primitives */
334
- .mcp-null { color: var(--text-tertiary); font-style: italic; }
335
- .mcp-bool { font-weight: 600; }
336
- .mcp-bool-true { color: var(--success); }
337
- .mcp-bool-false { color: var(--danger); }
338
- .mcp-num { font-variant-numeric: tabular-nums; }
339
- .mcp-text { white-space: pre-wrap; line-height: 1.6; }
340
- .mcp-empty { color: var(--text-tertiary); font-style: italic; padding: var(--sp-2) 0; }
341
- .mcp-truncated { color: var(--text-tertiary); font-style: italic; }
342
-
343
- /* Links & images */
344
- .mcp-link { color: var(--accent); text-decoration: none; word-break: break-all; }
345
- .mcp-link:hover { text-decoration: underline; }
346
- .mcp-img { max-width: 200px; max-height: 150px; border-radius: var(--radius-sm); border: 1px solid var(--border); }
347
-
348
- /* Nested table styling */
349
- .mcp-details .mcp-table-wrap { margin: 0; }
350
- .mcp-details .mcp-table th { position: static; }
351
- `;
352
- }
353
-
354
- /** Get the JS for table sorting (supports multiple tables) */
355
- export function getRendererJS(): string {
356
- return `
357
- function __mcpSort(tableId, colIdx) {
358
- var table = document.getElementById(tableId);
359
- if (!table) return;
360
- var tbody = table.tBodies[0];
361
- var rows = Array.from(tbody.rows);
362
- var key = tableId + '_' + colIdx;
363
- var dir = table.dataset.sortKey === key && table.dataset.sortDir === 'asc' ? 'desc' : 'asc';
364
- table.dataset.sortDir = dir;
365
- table.dataset.sortKey = key;
366
-
367
- rows.sort(function(a, b) {
368
- var ac = a.cells[colIdx], bc = b.cells[colIdx];
369
- if (!ac || !bc) return 0;
370
- var av = ac.textContent.trim();
371
- var bv = bc.textContent.trim();
372
- var an = parseFloat(av.replace(/[^\\d.-]/g, ''));
373
- var bn = parseFloat(bv.replace(/[^\\d.-]/g, ''));
374
- if (!isNaN(an) && !isNaN(bn)) return dir === 'asc' ? an - bn : bn - an;
375
- return dir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);
376
- });
377
-
378
- rows.forEach(function(row) { tbody.appendChild(row); });
379
- }`;
380
- }
@@ -1,200 +0,0 @@
1
- // ── Form Renderer: JSON Schema → interactive form ──
2
- import type { JSONSchema } from '../types.js';
3
- import { escapeHtml } from '../html-builder.js';
4
-
5
- function renderField(name: string, schema: JSONSchema, required: boolean): string {
6
- const label = schema.title ?? name;
7
- const desc = schema.description ? `<div class="field-desc">${escapeHtml(schema.description)}</div>` : '';
8
- const requiredAttr = required ? 'required' : '';
9
- const requiredMark = required ? '<span class="required">*</span>' : '';
10
-
11
- // Enum → capsule select
12
- if (schema.enum && schema.enum.length > 0) {
13
- const capsules = schema.enum
14
- .map((v) => {
15
- const val = escapeHtml(String(v));
16
- return `<label class="capsule"><input type="radio" name="${escapeHtml(name)}" value="${val}"><span>${val}</span></label>`;
17
- })
18
- .join('\n');
19
- return `<div class="field animate-in">
20
- <label class="field-label">${escapeHtml(label)}${requiredMark}</label>
21
- ${desc}
22
- <div class="capsule-group">${capsules}</div>
23
- </div>`;
24
- }
25
-
26
- // Boolean → toggle
27
- if (schema.type === 'boolean') {
28
- const checked = schema.default === true ? 'checked' : '';
29
- return `<div class="field animate-in">
30
- <label class="toggle-label">
31
- <input type="checkbox" name="${escapeHtml(name)}" ${checked}>
32
- <span class="toggle-switch"></span>
33
- <span>${escapeHtml(label)}${requiredMark}</span>
34
- </label>
35
- ${desc}
36
- </div>`;
37
- }
38
-
39
- // Nested object → collapsible section
40
- if (schema.type === 'object' && schema.properties) {
41
- const inner = renderProperties(schema.properties, schema.required ?? []);
42
- return `<details class="nested-section animate-in" open>
43
- <summary class="section-title">${escapeHtml(label)}${requiredMark}</summary>
44
- ${desc}
45
- <div class="nested-fields">${inner}</div>
46
- </details>`;
47
- }
48
-
49
- // Array → textarea hint
50
- if (schema.type === 'array') {
51
- return `<div class="field animate-in">
52
- <label class="field-label" for="f-${escapeHtml(name)}">${escapeHtml(label)}${requiredMark}</label>
53
- ${desc}
54
- <textarea id="f-${escapeHtml(name)}" name="${escapeHtml(name)}" class="input textarea" placeholder="JSON array..." ${requiredAttr}>${escapeHtml(String(schema.default ?? ''))}</textarea>
55
- </div>`;
56
- }
57
-
58
- // Number/integer
59
- if (schema.type === 'number' || schema.type === 'integer') {
60
- const min = schema.minimum !== undefined ? `min="${schema.minimum}"` : '';
61
- const max = schema.maximum !== undefined ? `max="${schema.maximum}"` : '';
62
- const step = schema.type === 'integer' ? 'step="1"' : '';
63
- const def = schema.default !== undefined ? `value="${escapeHtml(String(schema.default))}"` : '';
64
- return `<div class="field animate-in">
65
- <label class="field-label" for="f-${escapeHtml(name)}">${escapeHtml(label)}${requiredMark}</label>
66
- ${desc}
67
- <input type="number" id="f-${escapeHtml(name)}" name="${escapeHtml(name)}" class="input" ${min} ${max} ${step} ${def} ${requiredAttr}>
68
- </div>`;
69
- }
70
-
71
- // String (default)
72
- const inputType = schema.format === 'email' ? 'email'
73
- : schema.format === 'uri' ? 'url'
74
- : schema.format === 'date' ? 'date'
75
- : schema.format === 'date-time' ? 'datetime-local'
76
- : schema.format === 'password' ? 'password'
77
- : 'text';
78
-
79
- const isLong = (schema.maxLength && schema.maxLength > 200) || schema.format === 'textarea';
80
- const def = schema.default !== undefined ? escapeHtml(String(schema.default)) : '';
81
-
82
- if (isLong) {
83
- return `<div class="field animate-in">
84
- <label class="field-label" for="f-${escapeHtml(name)}">${escapeHtml(label)}${requiredMark}</label>
85
- ${desc}
86
- <textarea id="f-${escapeHtml(name)}" name="${escapeHtml(name)}" class="input textarea" ${requiredAttr}>${def}</textarea>
87
- </div>`;
88
- }
89
-
90
- return `<div class="field animate-in">
91
- <label class="field-label" for="f-${escapeHtml(name)}">${escapeHtml(label)}${requiredMark}</label>
92
- ${desc}
93
- <input type="${inputType}" id="f-${escapeHtml(name)}" name="${escapeHtml(name)}" class="input" value="${def}" ${requiredAttr}>
94
- </div>`;
95
- }
96
-
97
- function renderProperties(
98
- properties: Record<string, JSONSchema>,
99
- required: string[]
100
- ): string {
101
- const reqSet = new Set(required);
102
- return Object.entries(properties)
103
- .map(([name, schema]) => renderField(name, schema, reqSet.has(name)))
104
- .join('\n');
105
- }
106
-
107
- export function renderForm(
108
- schema: JSONSchema,
109
- metadata: Record<string, unknown>
110
- ): string {
111
- const toolName = metadata['toolName'] as string | undefined;
112
- const toolDesc = metadata['toolDescription'] as string | undefined;
113
-
114
- const header = toolName
115
- ? `<div class="form-header"><h2>${escapeHtml(toolName)}</h2>${toolDesc ? `<p class="form-desc">${escapeHtml(toolDesc)}</p>` : ''}</div>`
116
- : '';
117
-
118
- const fields = schema.properties
119
- ? renderProperties(schema.properties, schema.required ?? [])
120
- : '<p class="text-secondary">No input parameters required.</p>';
121
-
122
- return `${header}
123
- <form id="mcp-form" class="mcp-form card" onsubmit="return __mcpSubmit(event)">
124
- ${fields}
125
- <div class="form-actions">
126
- <button type="submit" class="btn btn-primary">Execute Tool</button>
127
- <button type="reset" class="btn btn-ghost">Reset</button>
128
- </div>
129
- </form>`;
130
- }
131
-
132
- export function getFormCSS(): string {
133
- return `
134
- .mcp-form { display: flex; flex-direction: column; gap: var(--sp-5); max-width: 640px; }
135
- .form-header h2 { font-size: var(--text-2xl); font-weight: 700; }
136
- .form-desc { color: var(--text-secondary); margin-top: var(--sp-1); }
137
-
138
- .field { display: flex; flex-direction: column; gap: var(--sp-1); }
139
- .field-label { font-weight: 600; font-size: var(--text-sm); color: var(--text-primary); }
140
- .field-desc { font-size: var(--text-xs); color: var(--text-tertiary); }
141
- .required { color: var(--danger); margin-left: 2px; }
142
-
143
- .input {
144
- padding: var(--sp-2) var(--sp-3);
145
- border: 1px solid var(--border);
146
- border-radius: var(--radius-sm);
147
- font-size: var(--text-sm);
148
- font-family: var(--font-sans);
149
- background: var(--bg-primary);
150
- color: var(--text-primary);
151
- transition: border-color var(--duration-fast) var(--ease-out),
152
- box-shadow var(--duration-fast) var(--ease-out);
153
- }
154
- .input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-subtle); }
155
- .textarea { min-height: 80px; resize: vertical; }
156
-
157
- .capsule-group { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
158
- .capsule input { display: none; }
159
- .capsule span {
160
- display: inline-block;
161
- padding: var(--sp-1) var(--sp-3);
162
- border: 1px solid var(--border);
163
- border-radius: var(--radius-full);
164
- font-size: var(--text-sm);
165
- cursor: pointer;
166
- transition: all var(--duration-fast) var(--ease-out);
167
- }
168
- .capsule input:checked + span { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }
169
- .capsule span:hover { border-color: var(--accent); }
170
-
171
- .toggle-label { display: flex; align-items: center; gap: var(--sp-2); cursor: pointer; font-weight: 600; font-size: var(--text-sm); }
172
- .toggle-label input { display: none; }
173
- .toggle-switch {
174
- width: 36px; height: 20px; background: var(--border-strong); border-radius: 10px;
175
- position: relative; transition: background var(--duration-fast) var(--ease-out);
176
- }
177
- .toggle-switch::after {
178
- content: ''; position: absolute; top: 2px; left: 2px;
179
- width: 16px; height: 16px; background: white; border-radius: 50%;
180
- transition: transform var(--duration-fast) var(--ease-out);
181
- }
182
- .toggle-label input:checked + .toggle-switch { background: var(--accent); }
183
- .toggle-label input:checked + .toggle-switch::after { transform: translateX(16px); }
184
-
185
- .nested-section { border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--sp-4); }
186
- .nested-section summary { cursor: pointer; user-select: none; }
187
- .nested-fields { margin-top: var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-4); }
188
-
189
- .form-actions { display: flex; gap: var(--sp-3); margin-top: var(--sp-2); }
190
- .btn {
191
- padding: var(--sp-2) var(--sp-5); border-radius: var(--radius-sm);
192
- font-weight: 600; font-size: var(--text-sm); cursor: pointer; border: none;
193
- transition: all var(--duration-fast) var(--ease-out);
194
- }
195
- .btn-primary { background: var(--accent); color: var(--accent-text); }
196
- .btn-primary:hover { background: var(--accent-hover); }
197
- .btn-ghost { background: transparent; color: var(--text-secondary); border: 1px solid var(--border); }
198
- .btn-ghost:hover { background: var(--bg-tertiary); }
199
- `;
200
- }
package/src/types.ts DELETED
@@ -1,60 +0,0 @@
1
- // ── Render Options ──
2
- export interface RenderOptions {
3
- darkMode?: boolean;
4
- debug?: boolean;
5
- title?: string;
6
- }
7
-
8
- // ── Engine Input (discriminated union) ──
9
- export interface SchemaInput {
10
- mode: 'schema';
11
- schema: JSONSchema;
12
- toolName?: string;
13
- toolDescription?: string;
14
- }
15
-
16
- export interface DataInput {
17
- mode: 'data';
18
- data: unknown;
19
- toolName?: string;
20
- toolDescription?: string;
21
- }
22
-
23
- export type EngineInput = SchemaInput | DataInput;
24
-
25
- // ── JSON Schema subset (sufficient for MCP tool schemas) ──
26
- export interface JSONSchema {
27
- type?: string | string[];
28
- properties?: Record<string, JSONSchema>;
29
- required?: string[];
30
- items?: JSONSchema;
31
- enum?: unknown[];
32
- description?: string;
33
- default?: unknown;
34
- title?: string;
35
- format?: string;
36
- minimum?: number;
37
- maximum?: number;
38
- minLength?: number;
39
- maxLength?: number;
40
- pattern?: string;
41
- oneOf?: JSONSchema[];
42
- anyOf?: JSONSchema[];
43
- allOf?: JSONSchema[];
44
- $ref?: string;
45
- additionalProperties?: boolean | JSONSchema;
46
- }
47
-
48
- // ── MCP Tool definition (from protocol) ──
49
- export interface MCPToolDefinition {
50
- name: string;
51
- description?: string;
52
- inputSchema: JSONSchema;
53
- }
54
-
55
- // ── MCP Client types ──
56
- export interface MCPServerInfo {
57
- name: string;
58
- version: string;
59
- tools: MCPToolDefinition[];
60
- }
package/tsconfig.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src"
6
- },
7
- "include": ["src/**/*"]
8
- }