@mcp-html-bridge/ui-engine 0.3.0 → 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.
- package/package.json +1 -1
- package/src/engine.ts +50 -104
- package/src/index.ts +9 -13
- package/src/llm-renderer.ts +129 -0
- package/src/renderer.ts +380 -0
- package/src/types.ts +0 -23
- package/src/utilities.ts +319 -0
- package/src/data-sniffer.ts +0 -177
- package/src/renderers/composite.ts +0 -73
- package/src/renderers/data-grid.ts +0 -159
- package/src/renderers/json-tree.ts +0 -141
- package/src/renderers/metrics-card.ts +0 -108
- package/src/renderers/reading-block.ts +0 -114
package/src/renderer.ts
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
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/types.ts
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
// ── Render Intent ──
|
|
2
|
-
export type RenderIntent =
|
|
3
|
-
| 'form'
|
|
4
|
-
| 'data-grid'
|
|
5
|
-
| 'json-tree'
|
|
6
|
-
| 'reading-block'
|
|
7
|
-
| 'metrics-card'
|
|
8
|
-
| 'composite';
|
|
9
|
-
|
|
10
|
-
// ── Sniffer Result ──
|
|
11
|
-
export interface SniffResult {
|
|
12
|
-
intent: RenderIntent;
|
|
13
|
-
confidence: number;
|
|
14
|
-
metadata: Record<string, unknown>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
1
|
// ── Render Options ──
|
|
18
2
|
export interface RenderOptions {
|
|
19
3
|
darkMode?: boolean;
|
|
@@ -61,13 +45,6 @@ export interface JSONSchema {
|
|
|
61
45
|
additionalProperties?: boolean | JSONSchema;
|
|
62
46
|
}
|
|
63
47
|
|
|
64
|
-
// ── Renderer function signature ──
|
|
65
|
-
export type Renderer = (
|
|
66
|
-
data: unknown,
|
|
67
|
-
metadata: Record<string, unknown>,
|
|
68
|
-
themeCSS: string
|
|
69
|
-
) => string;
|
|
70
|
-
|
|
71
48
|
// ── MCP Tool definition (from protocol) ──
|
|
72
49
|
export interface MCPToolDefinition {
|
|
73
50
|
name: string;
|