@webmcp-auto-ui/ui 2.5.32 → 2.5.33

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 (80) hide show
  1. package/package.json +15 -2
  2. package/src/agent/DiagnosticModal.svelte +126 -50
  3. package/src/agent/EphemeralBubble.svelte +13 -3
  4. package/src/agent/MCPserversList.svelte +147 -0
  5. package/src/agent/McpConnector.svelte +10 -1
  6. package/src/agent/RecipeBrowser.svelte +384 -0
  7. package/src/agent/RemoteMCPserversDemo.svelte +5 -121
  8. package/src/agent/ToolBrowser.svelte +133 -0
  9. package/src/agent/WebMCPserversList.svelte +2 -0
  10. package/src/agent/useAgentLoop.svelte.ts +396 -0
  11. package/src/base/chat-inline.svelte +64 -0
  12. package/src/base/dialog-content.svelte +3 -1
  13. package/src/components/HeaderControls.svelte +78 -0
  14. package/src/index.ts +13 -35
  15. package/src/stores/canvas.svelte.ts +0 -6
  16. package/src/widgets/SafeImage.svelte +67 -0
  17. package/src/widgets/WidgetRenderer.svelte +153 -78
  18. package/src/widgets/notebook/executors/index.ts +0 -1
  19. package/src/widgets/notebook/executors/sql.ts +32 -182
  20. package/src/widgets/notebook/import-modal-api.ts +237 -0
  21. package/src/widgets/notebook/import-modal.svelte +738 -0
  22. package/src/widgets/notebook/left-pane.ts +1 -1
  23. package/src/widgets/notebook/notebook.svelte +75 -0
  24. package/src/widgets/notebook/notebook.ts +38 -73
  25. package/src/widgets/notebook/prose.ts +6 -3
  26. package/src/widgets/notebook/shared.ts +68 -49
  27. package/src/widgets/rich/cards.svelte +74 -0
  28. package/src/widgets/rich/carousel.svelte +126 -0
  29. package/src/widgets/rich/chart-rich.svelte +221 -0
  30. package/src/widgets/rich/chat-input.svelte +52 -0
  31. package/src/widgets/rich/data-table.svelte +132 -0
  32. package/src/widgets/rich/gallery.svelte +115 -0
  33. package/src/widgets/rich/grid-data.svelte +85 -0
  34. package/src/widgets/rich/hemicycle.svelte +95 -0
  35. package/src/widgets/rich/js-sandbox.svelte +67 -0
  36. package/src/widgets/rich/json-viewer.svelte +82 -0
  37. package/src/widgets/rich/log.svelte +62 -0
  38. package/src/widgets/rich/profile.svelte +91 -0
  39. package/src/widgets/rich/sankey.svelte +73 -0
  40. package/src/widgets/rich/stat-card.svelte +60 -0
  41. package/src/widgets/rich/timeline.svelte +95 -0
  42. package/src/widgets/rich/trombinoscope.svelte +87 -0
  43. package/src/widgets/simple/actions.svelte +36 -0
  44. package/src/widgets/simple/alert.svelte +52 -0
  45. package/src/widgets/simple/chart.svelte +38 -0
  46. package/src/widgets/simple/code.svelte +30 -0
  47. package/src/widgets/simple/kv.svelte +31 -0
  48. package/src/widgets/simple/list.svelte +35 -0
  49. package/src/widgets/simple/stat.svelte +36 -0
  50. package/src/widgets/simple/tags.svelte +34 -0
  51. package/src/widgets/simple/text.svelte +130 -0
  52. package/src/widgets/helpers/safe-image.ts +0 -78
  53. package/src/widgets/notebook/import-modals.ts +0 -560
  54. package/src/widgets/notebook/recipe-browser.ts +0 -350
  55. package/src/widgets/rich/cards.ts +0 -181
  56. package/src/widgets/rich/carousel.ts +0 -319
  57. package/src/widgets/rich/chart-rich.ts +0 -386
  58. package/src/widgets/rich/d3.ts +0 -503
  59. package/src/widgets/rich/data-table.ts +0 -342
  60. package/src/widgets/rich/gallery.ts +0 -350
  61. package/src/widgets/rich/grid-data.ts +0 -173
  62. package/src/widgets/rich/hemicycle.ts +0 -313
  63. package/src/widgets/rich/js-sandbox.ts +0 -122
  64. package/src/widgets/rich/json-viewer.ts +0 -202
  65. package/src/widgets/rich/log.ts +0 -143
  66. package/src/widgets/rich/map.ts +0 -218
  67. package/src/widgets/rich/profile.ts +0 -256
  68. package/src/widgets/rich/sankey.ts +0 -257
  69. package/src/widgets/rich/stat-card.ts +0 -125
  70. package/src/widgets/rich/timeline.ts +0 -179
  71. package/src/widgets/rich/trombinoscope.ts +0 -246
  72. package/src/widgets/simple/actions.ts +0 -89
  73. package/src/widgets/simple/alert.ts +0 -100
  74. package/src/widgets/simple/chart.ts +0 -189
  75. package/src/widgets/simple/code.ts +0 -79
  76. package/src/widgets/simple/kv.ts +0 -68
  77. package/src/widgets/simple/list.ts +0 -89
  78. package/src/widgets/simple/stat.ts +0 -58
  79. package/src/widgets/simple/tags.ts +0 -125
  80. package/src/widgets/simple/text.ts +0 -198
@@ -0,0 +1,384 @@
1
+ <svelte:options customElement={{ tag: 'auto-recipe-browser', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ import { fade, fly } from 'svelte/transition';
5
+ import { filterRecipes, sortRecipes, recipeToMarkdown, recipeToDownloadBlob } from '@webmcp-auto-ui/agent';
6
+ import { encode } from '@webmcp-auto-ui/sdk';
7
+ import { extractCellsFromRecipe } from '@webmcp-auto-ui/ui';
8
+ import type { McpMultiClient } from '@webmcp-auto-ui/core';
9
+ import type { RecipeData } from '@webmcp-auto-ui/sdk';
10
+
11
+ interface RecipeItem {
12
+ name: string;
13
+ description?: string;
14
+ body?: string;
15
+ when?: string;
16
+ components_used?: string[];
17
+ servers?: string[];
18
+ layout?: { type: string; columns?: number; arrangement?: string };
19
+ originalName?: string;
20
+ serverUrl?: string;
21
+ server?: string;
22
+ serverName?: string;
23
+ id?: string;
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ interface Props {
28
+ open: boolean;
29
+ mcpRecipes: RecipeItem[];
30
+ webmcpRecipes: RecipeItem[];
31
+ initialFilter?: string;
32
+ /** Layout toggle: list (default) or grid */
33
+ layout?: 'list' | 'grid';
34
+ onOpenInNotebook?: (type: string, data: Record<string, unknown>) => void;
35
+ /** Called when user clicks a recipe to view its detail. Host shows the recipe modal. */
36
+ onOpenRecipe?: (recipe: RecipeItem) => void;
37
+ }
38
+
39
+ let {
40
+ open = $bindable(false),
41
+ mcpRecipes = [],
42
+ webmcpRecipes = [],
43
+ initialFilter = '',
44
+ layout: initialLayout = 'list',
45
+ onOpenInNotebook,
46
+ onOpenRecipe,
47
+ }: Props = $props();
48
+
49
+ function getMultiClient(): McpMultiClient | undefined {
50
+ return (globalThis as unknown as { __multiMcp?: { multiClient: McpMultiClient } }).__multiMcp?.multiClient;
51
+ }
52
+
53
+ let query = $state('');
54
+ /** Kind filter: 'all' | 'mcp' | 'webmcp' — ported from vanilla recipe-browser */
55
+ let kind = $state<'all' | 'mcp' | 'webmcp'>('all');
56
+ /** Layout toggle — ported from vanilla recipe-browser */
57
+ let layout = $state<'list' | 'grid'>(initialLayout);
58
+ let selected = $state<RecipeItem | null>(null);
59
+ let mcpCollapsed = $state(false);
60
+ let webmcpCollapsed = $state(false);
61
+ let copyState = $state<'idle' | 'copied'>('idle');
62
+ let copyTimer: ReturnType<typeof setTimeout> | undefined;
63
+
64
+ // Sync initialFilter into search when modal opens
65
+ $effect(() => {
66
+ if (open) {
67
+ query = initialFilter ?? '';
68
+ kind = 'all';
69
+ selected = null;
70
+ }
71
+ });
72
+
73
+ function applyKindFilter(recipes: RecipeItem[], filterKind: 'all' | 'mcp' | 'webmcp'): RecipeItem[] {
74
+ if (filterKind === 'all') return recipes;
75
+ return recipes.filter((_r) => {
76
+ // webmcpRecipes are already separated — use the array membership to determine kind
77
+ return filterKind === 'webmcp' ? webmcpRecipes.includes(_r) : mcpRecipes.includes(_r);
78
+ });
79
+ }
80
+
81
+ const filteredMcp = $derived(
82
+ kind === 'webmcp' ? [] : sortRecipes(filterRecipes(mcpRecipes, query))
83
+ );
84
+ const filteredWebmcp = $derived(
85
+ kind === 'mcp' ? [] : sortRecipes(filterRecipes(webmcpRecipes, query))
86
+ );
87
+ const totalResults = $derived(filteredMcp.length + filteredWebmcp.length);
88
+
89
+ function close() { open = false; selected = null; }
90
+
91
+ function onKeydown(e: KeyboardEvent) {
92
+ if (e.key === 'Escape') close();
93
+ }
94
+
95
+ async function openRecipe(recipe: RecipeItem) {
96
+ await ensureBody(recipe);
97
+ selected = recipe;
98
+ onOpenRecipe?.(recipe);
99
+ }
100
+
101
+ async function downloadRecipe(recipe: RecipeItem) {
102
+ const { blob, filename } = recipeToDownloadBlob(recipe as Record<string, unknown>);
103
+ const url = URL.createObjectURL(blob);
104
+ const a = document.createElement('a');
105
+ a.href = url;
106
+ a.download = filename;
107
+ document.body.appendChild(a);
108
+ a.click();
109
+ document.body.removeChild(a);
110
+ URL.revokeObjectURL(url);
111
+ }
112
+
113
+ async function ensureBody(recipe: RecipeItem) {
114
+ const multiClient = getMultiClient();
115
+ if (recipe.body || !recipe.serverUrl || !multiClient) return;
116
+ try {
117
+ const identifier = recipe.originalName ?? recipe.name;
118
+ const res = await multiClient.callToolOn(recipe.serverUrl, 'get_recipe', {
119
+ name: identifier,
120
+ id: (recipe as any).id ?? identifier,
121
+ });
122
+ const text = res.content?.find((c: { type: string }) => c.type === 'text') as { text?: string } | undefined;
123
+ if (text?.text) {
124
+ let body = text.text;
125
+ try {
126
+ const parsed = JSON.parse(body);
127
+ if (parsed && typeof parsed === 'object' && typeof parsed.content === 'string') {
128
+ body = parsed.content;
129
+ }
130
+ } catch { /* not JSON — keep raw text */ }
131
+ recipe.body = body;
132
+ }
133
+ } catch (err) {
134
+ console.warn('[RecipeBrowser] get_recipe failed:', err);
135
+ }
136
+ }
137
+
138
+ async function openInNotebook(recipe: RecipeItem) {
139
+ await ensureBody(recipe);
140
+ const body = recipe.body ?? '';
141
+
142
+ const cells = extractCellsFromRecipe(body, {
143
+ title: recipe.name,
144
+ description: recipe.description,
145
+ });
146
+
147
+ const connected = getMultiClient()?.listServers() ?? [];
148
+ const serverNames = Array.isArray(recipe.servers) ? recipe.servers : [];
149
+ const servers = serverNames
150
+ .map((name) => {
151
+ const hit = connected.find((s) => s.name === name);
152
+ const url = hit?.url ?? (typeof recipe.serverUrl === 'string' ? recipe.serverUrl : undefined);
153
+ return url ? { name, url, kind: 'data' as const } : null;
154
+ })
155
+ .filter((s): s is { name: string; url: string; kind: 'data' } => s !== null);
156
+
157
+ const data: Record<string, unknown> = {
158
+ title: recipe.name,
159
+ cells,
160
+ mode: 'edit',
161
+ servers,
162
+ };
163
+
164
+ onOpenInNotebook?.('notebook', data);
165
+ close();
166
+ }
167
+
168
+ async function copyHyperSkillUrl(recipe: RecipeItem) {
169
+ try {
170
+ const md = recipeToMarkdown(recipe as Record<string, unknown>);
171
+ const hsUrl = await encode(window.location.origin, md);
172
+ await navigator.clipboard.writeText(hsUrl);
173
+ copyState = 'copied';
174
+ if (copyTimer) clearTimeout(copyTimer);
175
+ copyTimer = setTimeout(() => { copyState = 'idle'; }, 2000);
176
+ } catch (err) {
177
+ console.error('Failed to copy HyperSkill URL:', err);
178
+ }
179
+ }
180
+ </script>
181
+
182
+ <svelte:window onkeydown={onKeydown} />
183
+
184
+ {#if open}
185
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
186
+ <div
187
+ class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-6"
188
+ transition:fade={{ duration: 180 }}
189
+ onclick={(e) => { if (e.target === e.currentTarget) close(); }}
190
+ >
191
+ <div
192
+ class="w-full max-w-2xl max-h-[85vh] bg-surface border border-border2 rounded-2xl flex flex-col shadow-2xl overflow-hidden"
193
+ transition:fly={{ y: 24, duration: 240 }}
194
+ >
195
+ <!-- Header -->
196
+ <div class="flex items-center gap-4 px-6 py-4 border-b border-border flex-shrink-0">
197
+ <span class="font-mono text-sm font-bold text-text1 flex-1">Recipe Browser</span>
198
+ <button class="text-text2 hover:text-text1 font-mono text-base leading-none transition-colors"
199
+ onclick={close}>x</button>
200
+ </div>
201
+
202
+ <!-- Toolbar: search + kind filter + layout toggle -->
203
+ <div class="px-6 pt-4 pb-2 flex-shrink-0 flex items-center gap-2">
204
+ <input
205
+ type="text"
206
+ bind:value={query}
207
+ placeholder="Search recipes..."
208
+ class="font-mono text-xs h-8 px-3 rounded-lg border border-border2 bg-surface2 text-text1 flex-1 placeholder:text-text2/40 focus:outline-none focus:border-accent/50 transition-colors"
209
+ />
210
+
211
+ <!-- Kind filter — ported from vanilla recipe-browser -->
212
+ <select
213
+ bind:value={kind}
214
+ class="font-mono text-xs h-8 px-2 rounded-lg border border-border2 bg-surface2 text-text1 focus:outline-none focus:border-accent/50 transition-colors"
215
+ >
216
+ <option value="all">All kinds</option>
217
+ <option value="webmcp">WebMCP</option>
218
+ <option value="mcp">MCP</option>
219
+ </select>
220
+
221
+ <!-- Layout toggle — ported from vanilla recipe-browser -->
222
+ <div class="flex rounded-lg border border-border2 overflow-hidden">
223
+ <button
224
+ type="button"
225
+ onclick={() => layout = 'list'}
226
+ class="font-mono text-xs px-2 h-8 transition-colors {layout === 'list' ? 'bg-accent text-white' : 'bg-surface2 text-text2 hover:text-text1'}"
227
+ title="List view"
228
+ >list</button>
229
+ <button
230
+ type="button"
231
+ onclick={() => layout = 'grid'}
232
+ class="font-mono text-xs px-2 h-8 transition-colors {layout === 'grid' ? 'bg-accent text-white' : 'bg-surface2 text-text2 hover:text-text1'}"
233
+ title="Grid view"
234
+ >grid</button>
235
+ </div>
236
+ </div>
237
+
238
+ <!-- Recipe lists -->
239
+ <div class="flex-1 overflow-y-auto px-6 pb-5 flex flex-col gap-3">
240
+
241
+ {#if totalResults === 0}
242
+ <div class="flex items-center justify-center py-12">
243
+ <span class="font-mono text-xs text-text2">No recipes found</span>
244
+ </div>
245
+ {:else}
246
+
247
+ <!-- Grid layout merges both lists -->
248
+ {#if layout === 'grid'}
249
+ <div class="grid grid-cols-2 gap-2 mt-2">
250
+ {#each [...filteredMcp, ...filteredWebmcp] as recipe, i (`grid:${recipe.name}:${i}`)}
251
+ {@const isWebmcp = webmcpRecipes.includes(recipe)}
252
+ <div class="group flex flex-col gap-1 p-3 bg-surface2/50 rounded-lg hover:bg-surface2 transition-colors cursor-pointer border border-border2/50"
253
+ onclick={() => openRecipe(recipe)}>
254
+ <div class="font-mono text-[11px] text-text1 font-medium truncate">{recipe.name}</div>
255
+ {#if recipe.description}
256
+ <div class="font-mono text-[9px] text-text2 line-clamp-2">{recipe.description}</div>
257
+ {/if}
258
+ <div class="flex items-center gap-1 mt-auto pt-1">
259
+ <span class="font-mono text-[9px] px-1.5 py-0.5 rounded-full border {isWebmcp ? 'border-teal/40 text-teal' : 'border-accent/40 text-accent'}">
260
+ {isWebmcp ? 'webmcp' : 'mcp'}
261
+ </span>
262
+ </div>
263
+ <div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
264
+ <button
265
+ title="Download .md"
266
+ class="font-mono text-[10px] h-5 px-1.5 rounded border border-border2 text-text2 hover:text-text1 transition-colors"
267
+ onclick={(e) => { e.stopPropagation(); downloadRecipe(recipe); }}
268
+ >.md</button>
269
+ <button
270
+ title="Open in notebook"
271
+ class="font-mono text-[10px] h-5 px-1.5 rounded border border-border2 text-text2 hover:text-text1 transition-colors"
272
+ onclick={(e) => { e.stopPropagation(); openInNotebook(recipe); }}
273
+ >nb</button>
274
+ <button
275
+ title="Copy HyperSkill URL"
276
+ class="font-mono text-[10px] h-5 px-1.5 rounded border transition-colors {copyState === 'copied' ? 'border-teal/40 text-teal' : 'border-accent/40 text-accent hover:bg-accent/10'}"
277
+ onclick={(e) => { e.stopPropagation(); copyHyperSkillUrl(recipe); }}
278
+ >{copyState === 'copied' ? '✓' : 'hs'}</button>
279
+ </div>
280
+ </div>
281
+ {/each}
282
+ </div>
283
+
284
+ {:else}
285
+ <!-- List layout (default) -->
286
+
287
+ <!-- MCP Recipes -->
288
+ {#if filteredMcp.length > 0}
289
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
290
+ <div
291
+ class="flex items-center gap-1 cursor-pointer select-none mt-2"
292
+ onclick={() => mcpCollapsed = !mcpCollapsed}
293
+ >
294
+ <span class="text-[10px] font-mono text-text2 uppercase tracking-wider">MCP Recipes ({filteredMcp.length})</span>
295
+ <span class="text-[10px] text-text2 ml-auto transition-transform {mcpCollapsed ? '' : 'rotate-90'}">{@html '&#x25B6;'}</span>
296
+ </div>
297
+ {#if !mcpCollapsed}
298
+ <div class="flex flex-col gap-1">
299
+ {#each filteredMcp as recipe, i (`mcp:${recipe.name}:${i}`)}
300
+ <div class="group flex items-center gap-2 px-3 py-2 bg-surface2/50 rounded-lg hover:bg-surface2 transition-colors">
301
+ <button
302
+ class="flex-1 min-w-0 text-left cursor-pointer"
303
+ onclick={() => openRecipe(recipe)}
304
+ >
305
+ <div class="font-mono text-[11px] text-text1 font-medium truncate">{recipe.name}</div>
306
+ {#if recipe.description}
307
+ <div class="font-mono text-[9px] text-text2 line-clamp-1 mt-0.5">{recipe.description}</div>
308
+ {/if}
309
+ </button>
310
+ <div class="flex items-center gap-1 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
311
+ <button
312
+ title="Download .md"
313
+ class="font-mono text-[10px] h-6 px-2 rounded border border-border2 text-text2 hover:text-text1 transition-colors"
314
+ onclick={(e) => { e.stopPropagation(); downloadRecipe(recipe); }}
315
+ >.md</button>
316
+ <button
317
+ title="Open in notebook"
318
+ class="font-mono text-[10px] h-6 px-2 rounded border border-border2 text-text2 hover:text-text1 transition-colors"
319
+ onclick={(e) => { e.stopPropagation(); openInNotebook(recipe); }}
320
+ >nb</button>
321
+ <button
322
+ title="Copy HyperSkill URL"
323
+ class="font-mono text-[10px] h-6 px-2 rounded border transition-colors {copyState === 'copied' ? 'border-teal/40 text-teal' : 'border-accent/40 text-accent hover:bg-accent/10'}"
324
+ onclick={(e) => { e.stopPropagation(); copyHyperSkillUrl(recipe); }}
325
+ >{copyState === 'copied' ? '✓' : 'hs'}</button>
326
+ </div>
327
+ </div>
328
+ {/each}
329
+ </div>
330
+ {/if}
331
+ {/if}
332
+
333
+ <!-- WebMCP Recipes -->
334
+ {#if filteredWebmcp.length > 0}
335
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
336
+ <div
337
+ class="flex items-center gap-1 cursor-pointer select-none mt-2"
338
+ onclick={() => webmcpCollapsed = !webmcpCollapsed}
339
+ >
340
+ <span class="text-[10px] font-mono text-text2 uppercase tracking-wider">WebMCP Recipes ({filteredWebmcp.length})</span>
341
+ <span class="text-[10px] text-text2 ml-auto transition-transform {webmcpCollapsed ? '' : 'rotate-90'}">{@html '&#x25B6;'}</span>
342
+ </div>
343
+ {#if !webmcpCollapsed}
344
+ <div class="flex flex-col gap-1">
345
+ {#each filteredWebmcp as recipe, i (`webmcp:${recipe.name}:${i}`)}
346
+ <div class="group flex items-center gap-2 px-3 py-2 bg-surface2/50 rounded-lg hover:bg-surface2 transition-colors">
347
+ <button
348
+ class="flex-1 min-w-0 text-left cursor-pointer"
349
+ onclick={() => openRecipe(recipe)}
350
+ >
351
+ <div class="font-mono text-[11px] text-text1 font-medium truncate">{recipe.name}</div>
352
+ {#if recipe.description}
353
+ <div class="font-mono text-[9px] text-text2 line-clamp-1 mt-0.5">{recipe.description}</div>
354
+ {/if}
355
+ </button>
356
+ <div class="flex items-center gap-1 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
357
+ <button
358
+ title="Download .md"
359
+ class="font-mono text-[10px] h-6 px-2 rounded border border-border2 text-text2 hover:text-text1 transition-colors"
360
+ onclick={(e) => { e.stopPropagation(); downloadRecipe(recipe); }}
361
+ >.md</button>
362
+ <button
363
+ title="Open in notebook"
364
+ class="font-mono text-[10px] h-6 px-2 rounded border border-border2 text-text2 hover:text-text1 transition-colors"
365
+ onclick={(e) => { e.stopPropagation(); openInNotebook(recipe); }}
366
+ >nb</button>
367
+ <button
368
+ title="Copy HyperSkill URL"
369
+ class="font-mono text-[10px] h-6 px-2 rounded border transition-colors {copyState === 'copied' ? 'border-teal/40 text-teal' : 'border-accent/40 text-accent hover:bg-accent/10'}"
370
+ onclick={(e) => { e.stopPropagation(); copyHyperSkillUrl(recipe); }}
371
+ >{copyState === 'copied' ? '✓' : 'hs'}</button>
372
+ </div>
373
+ </div>
374
+ {/each}
375
+ </div>
376
+ {/if}
377
+ {/if}
378
+ {/if}
379
+
380
+ {/if}
381
+ </div>
382
+ </div>
383
+ </div>
384
+ {/if}
@@ -1,4 +1,7 @@
1
+ <!-- Backward-compat shim for Track F rename. Remove after index.ts is updated to MCPserversList. -->
1
2
  <script lang="ts">
3
+ import MCPserversList from './MCPserversList.svelte';
4
+
2
5
  interface Server {
3
6
  id: string;
4
7
  name: string;
@@ -20,126 +23,7 @@
20
23
  ontoolclick?: (url: string) => void;
21
24
  }
22
25
 
23
- let {
24
- servers,
25
- connectedUrls = [],
26
- loading = [],
27
- onconnect,
28
- onconnectall,
29
- ondisconnect,
30
- recipeCountByServer,
31
- onrecipeclick,
32
- toolCountByServer,
33
- ontoolclick,
34
- }: Props = $props();
35
-
36
- const allConnected = $derived(
37
- servers.length > 0 && servers.every(s => connectedUrls.includes(s.url))
38
- );
39
- const anyConnected = $derived(
40
- servers.some(s => connectedUrls.includes(s.url))
41
- );
42
-
43
- function isConnected(url: string) {
44
- return connectedUrls.includes(url);
45
- }
46
- function isLoading(url: string) {
47
- return loading.includes(url);
48
- }
26
+ let props: Props = $props();
49
27
  </script>
50
28
 
51
- <div class="flex flex-col gap-2">
52
- <span class="text-[9px] font-mono uppercase tracking-wider text-text2">
53
- Available MCP servers
54
- </span>
55
-
56
- <div class="flex flex-col gap-1">
57
- {#each servers as server (server.id)}
58
- {@const connected = isConnected(server.url)}
59
- {@const busy = isLoading(server.url)}
60
- <div
61
- class="group flex items-center gap-2 px-2 py-1.5 rounded border border-border2 bg-surface2 hover:border-accent/30 transition-colors"
62
- >
63
- <!-- status dot -->
64
- <div
65
- class="w-1.5 h-1.5 rounded-full flex-shrink-0 {busy
66
- ? 'bg-amber animate-pulse'
67
- : connected
68
- ? 'bg-teal'
69
- : 'bg-text2/30'}"
70
- ></div>
71
-
72
- <!-- info -->
73
- <div class="flex-1 min-w-0 flex flex-col">
74
- <span class="font-mono text-xs font-medium text-text1">{server.name}</span>
75
- <span class="text-[10px] text-text2 truncate">{server.description}</span>
76
- {#if connected && (recipeCountByServer?.[server.url] || toolCountByServer?.[server.url])}
77
- <span class="flex items-center gap-1.5 mt-0.5">
78
- {#if recipeCountByServer?.[server.url]}
79
- <button class="text-[10px] font-mono text-accent hover:underline"
80
- onclick={(e) => { e.stopPropagation(); onrecipeclick?.(server.url); }}>
81
- {recipeCountByServer[server.url]} recipes
82
- </button>
83
- {/if}
84
- {#if recipeCountByServer?.[server.url] && toolCountByServer?.[server.url]}
85
- <span class="text-[10px] text-text2">·</span>
86
- {/if}
87
- {#if toolCountByServer?.[server.url]}
88
- <button class="text-[10px] font-mono text-accent hover:underline"
89
- onclick={(e) => { e.stopPropagation(); ontoolclick?.(server.url); }}>
90
- {toolCountByServer[server.url]} tools
91
- </button>
92
- {/if}
93
- </span>
94
- {/if}
95
- </div>
96
-
97
- <!-- action -->
98
- <div class="flex-shrink-0">
99
- {#if busy}
100
- <div class="w-4 h-4 border border-accent/50 border-t-accent rounded-full animate-spin"></div>
101
- {:else if connected}
102
- <button
103
- onclick={() => ondisconnect?.(server.url)}
104
- class="text-xs font-mono px-1.5 h-6 rounded text-teal group-hover:text-accent2 transition-colors"
105
- title="Disconnect"
106
- >
107
- <span class="group-hover:hidden">&#10003;</span>
108
- <span class="hidden group-hover:inline text-accent2">&#215;</span>
109
- </button>
110
- {:else}
111
- <button
112
- onclick={() => onconnect?.(server.url)}
113
- class="text-[10px] font-mono px-1.5 h-6 rounded border border-border2 bg-surface2 hover:border-accent/50 hover:text-accent text-text2 transition-colors"
114
- >
115
- connect
116
- </button>
117
- {/if}
118
- </div>
119
- </div>
120
- {/each}
121
- </div>
122
-
123
- <!-- bottom actions -->
124
- <div class="flex items-center gap-2 mt-1">
125
- <button
126
- onclick={onconnectall}
127
- disabled={allConnected}
128
- class="text-xs font-mono px-2 h-7 rounded border border-accent/40 bg-accent/10 text-accent hover:bg-accent/20 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
129
- >
130
- Load all
131
- </button>
132
- {#if anyConnected}
133
- <button
134
- onclick={() => {
135
- for (const s of servers) {
136
- if (isConnected(s.url)) ondisconnect?.(s.url);
137
- }
138
- }}
139
- class="text-xs font-mono px-2 h-7 rounded border border-border2 bg-surface2 hover:border-accent2/50 hover:text-accent2 text-text2 transition-colors"
140
- >
141
- Disconnect all
142
- </button>
143
- {/if}
144
- </div>
145
- </div>
29
+ <MCPserversList {...props} />
@@ -0,0 +1,133 @@
1
+ <svelte:options customElement={{ tag: 'auto-tool-browser', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ import { filterRecipes, sortRecipes, groupToolsByServer, formatToolSchema } from '@webmcp-auto-ui/agent';
5
+ import type { BrowsableTool } from '@webmcp-auto-ui/agent';
6
+ import { MarkdownView, CodeView } from '@webmcp-auto-ui/ui';
7
+
8
+ interface Props {
9
+ open: boolean;
10
+ tools: BrowsableTool[];
11
+ initialFilter?: string;
12
+ }
13
+
14
+ let { open = $bindable(false), tools = [], initialFilter = '' }: Props = $props();
15
+
16
+ let query = $state('');
17
+ let selected = $state<BrowsableTool | null>(null);
18
+
19
+ $effect(() => {
20
+ if (open) {
21
+ query = initialFilter || '';
22
+ selected = null;
23
+ }
24
+ });
25
+
26
+ const filtered = $derived(sortRecipes(filterRecipes(tools, query)));
27
+ const grouped = $derived(groupToolsByServer(filtered));
28
+
29
+ function close() {
30
+ open = false;
31
+ selected = null;
32
+ }
33
+
34
+ function handleKeydown(e: KeyboardEvent) {
35
+ if (e.key === 'Escape') {
36
+ if (selected) selected = null;
37
+ else close();
38
+ }
39
+ }
40
+ </script>
41
+
42
+ <svelte:window onkeydown={open ? handleKeydown : undefined} />
43
+
44
+ {#if open}
45
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
46
+ <div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4"
47
+ onclick={close}>
48
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
49
+ <div class="bg-surface border border-border2 rounded-2xl shadow-2xl w-full max-w-2xl max-h-[85vh] flex flex-col overflow-hidden"
50
+ onclick={(e) => e.stopPropagation()}>
51
+
52
+ <!-- Header -->
53
+ <div class="flex items-center gap-3 px-6 py-4 border-b border-border flex-shrink-0">
54
+ {#if selected}
55
+ <button class="text-text2 hover:text-text1 transition-colors" onclick={() => selected = null}>
56
+ &larr;
57
+ </button>
58
+ <span class="font-mono text-sm font-bold text-text1 truncate flex-1">{selected.name}</span>
59
+ {:else}
60
+ <span class="font-mono text-sm font-bold text-text1 flex-1">Tool Browser</span>
61
+ <span class="text-[10px] font-mono text-text2">{filtered.length} tools</span>
62
+ {/if}
63
+ <button class="text-text2 hover:text-text1 text-lg leading-none transition-colors"
64
+ onclick={close}>x</button>
65
+ </div>
66
+
67
+ {#if selected}
68
+ <!-- Detail view -->
69
+ <div class="flex-1 overflow-y-auto p-6 flex flex-col gap-4">
70
+ {#if selected.description}
71
+ <MarkdownView source={selected.description} class="text-text2" />
72
+ {:else}
73
+ <p class="text-sm text-text2 italic">No description available</p>
74
+ {/if}
75
+
76
+ {#if selected.server}
77
+ <div class="flex flex-col gap-1">
78
+ <span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Server</span>
79
+ <span class="font-mono text-xs text-text1 bg-surface2 rounded px-3 py-2 border border-border2">{selected.server}</span>
80
+ </div>
81
+ {/if}
82
+
83
+ {#if selected.inputSchema}
84
+ <div class="flex flex-col gap-1">
85
+ <span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Input schema</span>
86
+ <div class="max-h-[400px] overflow-y-auto">
87
+ <CodeView lang="json" source={formatToolSchema(selected) ?? ''} />
88
+ </div>
89
+ </div>
90
+ {/if}
91
+ </div>
92
+
93
+ {:else}
94
+ <!-- Search -->
95
+ <div class="px-4 py-3 border-b border-border flex-shrink-0">
96
+ <input
97
+ type="text"
98
+ bind:value={query}
99
+ placeholder="Search tools..."
100
+ class="w-full bg-surface2 border border-border2 rounded-lg px-3 h-8 text-xs font-mono text-text1
101
+ outline-none placeholder:text-text2/40 focus:border-accent/50 transition-colors"
102
+ />
103
+ </div>
104
+
105
+ <!-- List -->
106
+ <div class="flex-1 overflow-y-auto min-h-0">
107
+ {#each [...grouped.entries()] as [serverName, serverTools] (serverName)}
108
+ <div class="px-4 pt-3 pb-1">
109
+ <span class="text-[9px] font-mono uppercase tracking-wider text-text2">
110
+ {serverName} ({serverTools.length})
111
+ </span>
112
+ </div>
113
+ {#each serverTools as tool, i (`${serverName}:${tool.name}:${i}`)}
114
+ <button
115
+ class="w-full text-left px-4 py-2 border-b border-border/30 hover:bg-surface2/80 transition-colors"
116
+ onclick={() => selected = tool}
117
+ >
118
+ <div class="font-mono text-xs font-medium text-text1">{tool.name}</div>
119
+ {#if tool.description}
120
+ <div class="text-[10px] text-text2 truncate mt-0.5">{tool.description}</div>
121
+ {/if}
122
+ </button>
123
+ {/each}
124
+ {/each}
125
+
126
+ {#if filtered.length === 0}
127
+ <div class="p-8 text-center text-text2 text-xs font-mono">No tools found</div>
128
+ {/if}
129
+ </div>
130
+ {/if}
131
+ </div>
132
+ </div>
133
+ {/if}
@@ -1,3 +1,5 @@
1
+ <svelte:options customElement={{ tag: 'auto-webmcp-servers-list', shadow: 'none' }} />
2
+
1
3
  <script lang="ts">
2
4
  interface Server {
3
5
  id: string;