@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.
- package/dist/bridge.d.ts +3 -0
- package/dist/bridge.d.ts.map +1 -0
- package/{src/bridge.ts → dist/bridge.js} +6 -3
- package/dist/bridge.js.map +1 -0
- package/dist/engine.d.ts +26 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +92 -0
- package/dist/engine.js.map +1 -0
- package/dist/html-builder.d.ts +16 -0
- package/dist/html-builder.d.ts.map +1 -0
- package/dist/html-builder.js +54 -0
- package/dist/html-builder.js.map +1 -0
- package/{src/index.ts → dist/index.d.ts} +3 -26
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/llm-renderer.d.ts +36 -0
- package/dist/llm-renderer.d.ts.map +1 -0
- package/dist/llm-renderer.js +103 -0
- package/dist/llm-renderer.js.map +1 -0
- package/dist/playground.d.ts +4 -0
- package/dist/playground.d.ts.map +1 -0
- package/{src/playground.ts → dist/playground.js} +12 -11
- package/dist/playground.js.map +1 -0
- package/dist/renderer.d.ts +7 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +345 -0
- package/dist/renderer.js.map +1 -0
- package/dist/renderers/form.d.ts +4 -0
- package/dist/renderers/form.d.ts.map +1 -0
- package/dist/renderers/form.js +181 -0
- package/dist/renderers/form.js.map +1 -0
- package/dist/theme.d.ts +2 -0
- package/dist/theme.d.ts.map +1 -0
- package/{src/theme.ts → dist/theme.js} +6 -3
- package/dist/theme.js.map +1 -0
- package/dist/types.d.ts +50 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utilities.d.ts +19 -0
- package/dist/utilities.d.ts.map +1 -0
- package/{src/utilities.ts → dist/utilities.js} +6 -3
- package/dist/utilities.js.map +1 -0
- package/package.json +4 -1
- package/src/engine.ts +0 -118
- package/src/html-builder.ts +0 -61
- package/src/llm-renderer.ts +0 -129
- package/src/renderer.ts +0 -380
- package/src/renderers/form.ts +0 -200
- package/src/types.ts +0 -60
- 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
|
-
}
|
package/src/renderers/form.ts
DELETED
|
@@ -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
|
-
}
|