@fragments-sdk/cli 0.7.15 → 0.7.17

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 (33) hide show
  1. package/dist/bin.js +2 -2
  2. package/dist/init-DIZ6UNBL.js +806 -0
  3. package/dist/init-DIZ6UNBL.js.map +1 -0
  4. package/dist/{viewer-7I4WGVU3.js → viewer-QKIAPTPG.js} +67 -4
  5. package/dist/viewer-QKIAPTPG.js.map +1 -0
  6. package/package.json +3 -2
  7. package/src/commands/init-framework.ts +414 -0
  8. package/src/commands/init.ts +41 -1
  9. package/src/theme/__tests__/component-contrast.test.ts +210 -157
  10. package/src/viewer/components/App.tsx +5 -0
  11. package/src/viewer/components/HealthDashboard.tsx +1 -1
  12. package/src/viewer/components/PropsTable.tsx +2 -2
  13. package/src/viewer/components/RuntimeToolsRegistrar.tsx +17 -0
  14. package/src/viewer/components/ViewerStateSync.tsx +52 -0
  15. package/src/viewer/components/WebMCPDevTools.tsx +509 -0
  16. package/src/viewer/components/WebMCPIntegration.tsx +47 -0
  17. package/src/viewer/components/WebMCPStatusIndicator.tsx +60 -0
  18. package/src/viewer/entry.tsx +6 -3
  19. package/src/viewer/hooks/useA11yService.ts +1 -135
  20. package/src/viewer/hooks/useCompiledFragments.ts +42 -0
  21. package/src/viewer/server.ts +58 -3
  22. package/src/viewer/vite-plugin.ts +18 -0
  23. package/src/viewer/webmcp/__tests__/analytics.test.ts +108 -0
  24. package/src/viewer/webmcp/analytics.ts +165 -0
  25. package/src/viewer/webmcp/index.ts +3 -0
  26. package/src/viewer/webmcp/posthog-bridge.ts +39 -0
  27. package/src/viewer/webmcp/runtime-tools.ts +152 -0
  28. package/src/viewer/webmcp/scan-utils.ts +135 -0
  29. package/src/viewer/webmcp/use-tool-analytics.ts +69 -0
  30. package/src/viewer/webmcp/viewer-state.ts +45 -0
  31. package/dist/init-V42FFMUJ.js +0 -498
  32. package/dist/init-V42FFMUJ.js.map +0 -1
  33. package/dist/viewer-7I4WGVU3.js.map +0 -1
@@ -0,0 +1,509 @@
1
+ import { useState, useEffect, useCallback, useRef } from "react";
2
+ import {
3
+ Tabs,
4
+ Select,
5
+ Textarea,
6
+ Button,
7
+ CodeBlock,
8
+ Badge,
9
+ Card,
10
+ Stack,
11
+ Box,
12
+ Text,
13
+ } from "@fragments-sdk/ui";
14
+ import { useWebMCPContext } from "@fragments-sdk/webmcp/react";
15
+ import type {
16
+ WebMCPTool,
17
+ JSONSchemaInput,
18
+ ToolCallEvent,
19
+ } from "@fragments-sdk/webmcp";
20
+ import { SPEC_VERSION } from "@fragments-sdk/webmcp";
21
+ import { useToolAnalytics } from "../webmcp/use-tool-analytics.js";
22
+
23
+ function generateInputSkeleton(tool: WebMCPTool): string {
24
+ const schema = tool.inputSchema as JSONSchemaInput | undefined;
25
+ if (!schema?.properties) return "{}";
26
+ const obj: Record<string, unknown> = {};
27
+ for (const [key, prop] of Object.entries(schema.properties)) {
28
+ if (prop.default !== undefined) obj[key] = prop.default;
29
+ else if (prop.enum?.length) obj[key] = prop.enum[0];
30
+ else if (prop.type === "string") obj[key] = "";
31
+ else if (prop.type === "number" || prop.type === "integer") obj[key] = 0;
32
+ else if (prop.type === "boolean") obj[key] = false;
33
+ else if (prop.type === "array") obj[key] = [];
34
+ else obj[key] = null;
35
+ }
36
+ return JSON.stringify(obj, null, 2);
37
+ }
38
+
39
+ function formatTimestamp(ts: number): string {
40
+ return new Date(ts).toLocaleTimeString();
41
+ }
42
+
43
+ function formatDuration(ms: number): string {
44
+ if (ms < 1000) return `${ms}ms`;
45
+ if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
46
+ return `${(ms / 60_000).toFixed(1)}m`;
47
+ }
48
+
49
+ const MAX_ACTIVITY_ENTRIES = 50;
50
+
51
+ export function WebMCPDevTools() {
52
+ const { shimRegistry, isShim } = useWebMCPContext();
53
+ const { summary, refreshSummary } = useToolAnalytics();
54
+ const [tools, setTools] = useState<WebMCPTool[]>([]);
55
+ const [selectedTool, setSelectedTool] = useState<string>("");
56
+ const [inputJson, setInputJson] = useState("{}");
57
+ const [executing, setExecuting] = useState(false);
58
+ const [result, setResult] = useState<{ type: "success" | "error"; data: string } | null>(null);
59
+ const [activity, setActivity] = useState<ToolCallEvent[]>([]);
60
+ const [expandedEntry, setExpandedEntry] = useState<number | null>(null);
61
+ const activityRef = useRef(activity);
62
+ activityRef.current = activity;
63
+
64
+ // Sync tools from registry
65
+ useEffect(() => {
66
+ if (!shimRegistry) return;
67
+
68
+ const update = () => {
69
+ setTools(Array.from(shimRegistry.tools.values()));
70
+ };
71
+ update();
72
+
73
+ shimRegistry.addEventListener("toolchange", update);
74
+ return () => shimRegistry.removeEventListener("toolchange", update);
75
+ }, [shimRegistry]);
76
+
77
+ // Subscribe to toolcall events
78
+ useEffect(() => {
79
+ if (!shimRegistry) return;
80
+
81
+ const onToolCall = (event: ToolCallEvent) => {
82
+ setActivity((prev) => [event, ...prev].slice(0, MAX_ACTIVITY_ENTRIES));
83
+ };
84
+
85
+ shimRegistry.addEventListener("toolcall", onToolCall);
86
+ return () => shimRegistry.removeEventListener("toolcall", onToolCall);
87
+ }, [shimRegistry]);
88
+
89
+ // Update input skeleton when tool selection changes
90
+ useEffect(() => {
91
+ if (!selectedTool) return;
92
+ const tool = tools.find((t) => t.name === selectedTool);
93
+ if (tool) {
94
+ setInputJson(generateInputSkeleton(tool));
95
+ setResult(null);
96
+ }
97
+ }, [selectedTool, tools]);
98
+
99
+ // Auto-select first tool
100
+ useEffect(() => {
101
+ if (tools.length > 0 && !selectedTool) {
102
+ setSelectedTool(tools[0].name);
103
+ }
104
+ }, [tools, selectedTool]);
105
+
106
+ const handleExecute = useCallback(async () => {
107
+ const tool = tools.find((t) => t.name === selectedTool);
108
+ if (!tool) return;
109
+
110
+ setExecuting(true);
111
+ setResult(null);
112
+
113
+ try {
114
+ const input = JSON.parse(inputJson);
115
+ const output = await tool.execute(input, {} as any);
116
+ setResult({
117
+ type: "success",
118
+ data: JSON.stringify(output, null, 2),
119
+ });
120
+ } catch (err) {
121
+ setResult({
122
+ type: "error",
123
+ data: err instanceof Error ? err.message : String(err),
124
+ });
125
+ } finally {
126
+ setExecuting(false);
127
+ }
128
+ }, [tools, selectedTool, inputJson]);
129
+
130
+ const currentTool = tools.find((t) => t.name === selectedTool);
131
+ const schema = currentTool?.inputSchema as JSONSchemaInput | undefined;
132
+
133
+ return (
134
+ <Stack direction="column" gap="md" style={{ height: "100%" }}>
135
+ <Tabs defaultValue="tools">
136
+ <Tabs.List variant="underline">
137
+ <Tabs.Tab value="tools">Tools</Tabs.Tab>
138
+ <Tabs.Tab value="execute">Execute</Tabs.Tab>
139
+ <Tabs.Tab value="activity">
140
+ Activity{activity.length > 0 ? ` (${activity.length})` : ""}
141
+ </Tabs.Tab>
142
+ <Tabs.Tab value="analytics">Analytics</Tabs.Tab>
143
+ </Tabs.List>
144
+
145
+ {/* Tab 1: Tool Catalog */}
146
+ <Tabs.Panel value="tools">
147
+ <Stack direction="column" gap="md" style={{ paddingTop: "12px" }}>
148
+ {tools.length === 0 ? (
149
+ <Text color="secondary">No tools registered yet.</Text>
150
+ ) : (
151
+ <>
152
+ <Select
153
+ value={selectedTool}
154
+ onValueChange={setSelectedTool}
155
+ placeholder="Select a tool"
156
+ >
157
+ <Select.Trigger />
158
+ <Select.Content>
159
+ {tools.map((tool) => (
160
+ <Select.Item key={tool.name} value={tool.name}>
161
+ {tool.name}
162
+ </Select.Item>
163
+ ))}
164
+ </Select.Content>
165
+ </Select>
166
+
167
+ {currentTool && (
168
+ <Card variant="outlined">
169
+ <Card.Body>
170
+ <Stack direction="column" gap="sm">
171
+ <Text weight="semibold">{currentTool.name}</Text>
172
+ <Text color="secondary" size="sm">
173
+ {currentTool.description}
174
+ </Text>
175
+ {currentTool.annotations?.readOnlyHint && (
176
+ <div>
177
+ <Badge variant="info" size="sm">
178
+ readOnly
179
+ </Badge>
180
+ </div>
181
+ )}
182
+
183
+ {schema?.properties && (
184
+ <Box style={{ marginTop: "8px" }}>
185
+ <Text size="xs" weight="semibold" color="secondary" style={{ marginBottom: "4px" }}>
186
+ Parameters
187
+ </Text>
188
+ <Stack direction="column" gap="xs">
189
+ {Object.entries(schema.properties).map(
190
+ ([name, prop]) => (
191
+ <Box
192
+ key={name}
193
+ style={{
194
+ padding: "6px 8px",
195
+ borderRadius: "var(--radius-sm)",
196
+ backgroundColor: "var(--bg-secondary)",
197
+ }}
198
+ >
199
+ <Stack direction="row" gap="xs" align="center">
200
+ <Text
201
+ size="xs"
202
+ weight="medium"
203
+ style={{ fontFamily: "monospace" }}
204
+ >
205
+ {name}
206
+ </Text>
207
+ <Text size="2xs" color="tertiary">
208
+ {prop.type}
209
+ </Text>
210
+ {schema.required?.includes(name) && (
211
+ <Badge variant="warning" size="sm">
212
+ required
213
+ </Badge>
214
+ )}
215
+ </Stack>
216
+ {prop.description && (
217
+ <Text size="2xs" color="secondary" style={{ marginTop: "2px" }}>
218
+ {prop.description}
219
+ </Text>
220
+ )}
221
+ {prop.enum && (
222
+ <Text size="2xs" color="tertiary" style={{ marginTop: "2px", fontFamily: "monospace" }}>
223
+ enum: {prop.enum.join(" | ")}
224
+ </Text>
225
+ )}
226
+ {prop.default !== undefined && (
227
+ <Text size="2xs" color="tertiary" style={{ marginTop: "2px" }}>
228
+ default: {JSON.stringify(prop.default)}
229
+ </Text>
230
+ )}
231
+ </Box>
232
+ )
233
+ )}
234
+ </Stack>
235
+ </Box>
236
+ )}
237
+ </Stack>
238
+ </Card.Body>
239
+ </Card>
240
+ )}
241
+ </>
242
+ )}
243
+ </Stack>
244
+ </Tabs.Panel>
245
+
246
+ {/* Tab 2: Manual Execution */}
247
+ <Tabs.Panel value="execute">
248
+ <Stack direction="column" gap="md" style={{ paddingTop: "12px" }}>
249
+ <Select
250
+ value={selectedTool}
251
+ onValueChange={setSelectedTool}
252
+ placeholder="Select a tool"
253
+ >
254
+ <Select.Trigger />
255
+ <Select.Content>
256
+ {tools.map((tool) => (
257
+ <Select.Item key={tool.name} value={tool.name}>
258
+ {tool.name}
259
+ </Select.Item>
260
+ ))}
261
+ </Select.Content>
262
+ </Select>
263
+
264
+ <Textarea
265
+ label="Input (JSON)"
266
+ rows={8}
267
+ value={inputJson}
268
+ onChange={setInputJson}
269
+ resize="vertical"
270
+ style={{ fontFamily: "monospace", fontSize: "12px" }}
271
+ />
272
+
273
+ <Button
274
+ variant="primary"
275
+ onClick={handleExecute}
276
+ disabled={executing || !selectedTool}
277
+ data-testid="webmcp-execute-btn"
278
+ >
279
+ {executing ? "Executing..." : "Execute"}
280
+ </Button>
281
+
282
+ {result && (
283
+ <Box style={{ marginTop: "4px" }}>
284
+ {result.type === "success" ? (
285
+ <CodeBlock
286
+ code={result.data}
287
+ language="json"
288
+ maxHeight={300}
289
+ wordWrap
290
+ showCopy
291
+ compact
292
+ />
293
+ ) : (
294
+ <Box
295
+ style={{
296
+ padding: "12px",
297
+ borderRadius: "var(--radius-md)",
298
+ backgroundColor: "var(--bg-danger, #fef2f2)",
299
+ }}
300
+ >
301
+ <Text size="sm" style={{ color: "var(--color-danger, #dc2626)" }}>
302
+ {result.data}
303
+ </Text>
304
+ </Box>
305
+ )}
306
+ </Box>
307
+ )}
308
+ </Stack>
309
+ </Tabs.Panel>
310
+
311
+ {/* Tab 3: Activity Log */}
312
+ <Tabs.Panel value="activity">
313
+ <Stack direction="column" gap="xs" style={{ paddingTop: "12px" }}>
314
+ {activity.length === 0 ? (
315
+ <Text color="secondary" size="sm">
316
+ No tool calls yet. Execute a tool or wait for an agent to call
317
+ one.
318
+ </Text>
319
+ ) : (
320
+ activity.map((entry, i) => (
321
+ <Card
322
+ key={`${entry.timestamp}-${i}`}
323
+ variant="outlined"
324
+ style={{ padding: "8px 12px", cursor: "pointer" }}
325
+ onClick={() => setExpandedEntry(expandedEntry === i ? null : i)}
326
+ >
327
+ <Stack direction="row" gap="xs" align="center">
328
+ <Badge
329
+ size="sm"
330
+ variant={entry.error ? "error" : "success"}
331
+ >
332
+ {entry.toolName}
333
+ </Badge>
334
+ <Text size="2xs" color="tertiary">
335
+ {formatTimestamp(entry.timestamp)}
336
+ </Text>
337
+ <Text size="2xs" color="secondary">
338
+ {entry.durationMs}ms
339
+ </Text>
340
+ </Stack>
341
+ {expandedEntry === i && (
342
+ <Stack direction="column" gap="xs" style={{ marginTop: "8px" }}>
343
+ <Text size="2xs" weight="semibold" color="secondary">
344
+ Input
345
+ </Text>
346
+ <CodeBlock
347
+ code={JSON.stringify(entry.input, null, 2)}
348
+ language="json"
349
+ compact
350
+ maxHeight={150}
351
+ />
352
+ <Text size="2xs" weight="semibold" color="secondary">
353
+ {entry.error ? "Error" : "Output"}
354
+ </Text>
355
+ <CodeBlock
356
+ code={
357
+ entry.error
358
+ ? entry.error
359
+ : JSON.stringify(entry.output, null, 2)
360
+ }
361
+ language={entry.error ? "plaintext" : "json"}
362
+ compact
363
+ maxHeight={150}
364
+ />
365
+ </Stack>
366
+ )}
367
+ </Card>
368
+ ))
369
+ )}
370
+ </Stack>
371
+ </Tabs.Panel>
372
+
373
+ {/* Tab 4: Analytics */}
374
+ <Tabs.Panel value="analytics">
375
+ <Stack direction="column" gap="md" style={{ paddingTop: "12px" }}>
376
+ <Stack direction="row" gap="sm" align="center">
377
+ <Text weight="semibold" size="sm">Session Analytics</Text>
378
+ <Button variant="ghost" size="sm" onClick={refreshSummary}>
379
+ Refresh
380
+ </Button>
381
+ </Stack>
382
+
383
+ {!summary || summary.totalCalls === 0 ? (
384
+ <Text color="secondary" size="sm">
385
+ No tool calls recorded yet. Analytics will appear after tools are used.
386
+ </Text>
387
+ ) : (
388
+ <Stack direction="column" gap="md">
389
+ {/* Summary Cards */}
390
+ <Stack direction="row" gap="sm" style={{ flexWrap: 'wrap' }}>
391
+ <Card variant="outlined" style={{ flex: '1 1 120px', padding: '12px' }}>
392
+ <Card.Body>
393
+ <Text size="2xs" color="tertiary">Total Calls</Text>
394
+ <Text size="lg" weight="bold">{summary.totalCalls}</Text>
395
+ </Card.Body>
396
+ </Card>
397
+ <Card variant="outlined" style={{ flex: '1 1 120px', padding: '12px' }}>
398
+ <Card.Body>
399
+ <Text size="2xs" color="tertiary">Unique Tools</Text>
400
+ <Text size="lg" weight="bold">{summary.uniqueTools}</Text>
401
+ </Card.Body>
402
+ </Card>
403
+ <Card variant="outlined" style={{ flex: '1 1 120px', padding: '12px' }}>
404
+ <Card.Body>
405
+ <Text size="2xs" color="tertiary">Error Rate</Text>
406
+ <Text size="lg" weight="bold">{(summary.errorRate * 100).toFixed(0)}%</Text>
407
+ </Card.Body>
408
+ </Card>
409
+ <Card variant="outlined" style={{ flex: '1 1 120px', padding: '12px' }}>
410
+ <Card.Body>
411
+ <Text size="2xs" color="tertiary">Duration</Text>
412
+ <Text size="lg" weight="bold">{formatDuration(summary.sessionDurationMs)}</Text>
413
+ </Card.Body>
414
+ </Card>
415
+ </Stack>
416
+
417
+ {/* Tool Breakdown */}
418
+ <Box>
419
+ <Text size="xs" weight="semibold" color="secondary" style={{ marginBottom: "8px" }}>
420
+ Tool Usage
421
+ </Text>
422
+ <Stack direction="column" gap="xs">
423
+ {Object.entries(summary.toolStats)
424
+ .sort(([, a], [, b]) => b.callCount - a.callCount)
425
+ .map(([name, stat]) => (
426
+ <Box
427
+ key={name}
428
+ style={{
429
+ padding: "8px 10px",
430
+ borderRadius: "var(--radius-sm)",
431
+ backgroundColor: "var(--bg-secondary)",
432
+ }}
433
+ >
434
+ <Stack direction="row" gap="sm" align="center" style={{ justifyContent: "space-between" }}>
435
+ <Text size="xs" weight="medium" style={{ fontFamily: "monospace" }}>
436
+ {name}
437
+ </Text>
438
+ <Stack direction="row" gap="xs">
439
+ <Badge size="sm" variant="info">{stat.callCount}x</Badge>
440
+ <Text size="2xs" color="tertiary">avg {stat.avgDuration}ms</Text>
441
+ {stat.errorCount > 0 && (
442
+ <Badge size="sm" variant="error">{stat.errorCount} err</Badge>
443
+ )}
444
+ </Stack>
445
+ </Stack>
446
+ <Text size="2xs" color="tertiary" style={{ marginTop: "2px" }}>
447
+ p50: {stat.p50}ms | p95: {stat.p95}ms
448
+ </Text>
449
+ </Box>
450
+ ))}
451
+ </Stack>
452
+ </Box>
453
+
454
+ {/* Common Flows */}
455
+ {summary.commonFlows.length > 0 && (
456
+ <Box>
457
+ <Text size="xs" weight="semibold" color="secondary" style={{ marginBottom: "8px" }}>
458
+ Common Flows
459
+ </Text>
460
+ <Stack direction="column" gap="xs">
461
+ {summary.commonFlows.map((flow, i) => (
462
+ <Box
463
+ key={i}
464
+ style={{
465
+ padding: "6px 8px",
466
+ borderRadius: "var(--radius-sm)",
467
+ backgroundColor: "var(--bg-secondary)",
468
+ }}
469
+ >
470
+ <Stack direction="row" gap="xs" align="center">
471
+ <Text size="2xs" style={{ fontFamily: "monospace" }}>
472
+ {flow.sequence.join(' → ')}
473
+ </Text>
474
+ <Badge size="sm" variant="info">{flow.count}x</Badge>
475
+ </Stack>
476
+ </Box>
477
+ ))}
478
+ </Stack>
479
+ </Box>
480
+ )}
481
+ </Stack>
482
+ )}
483
+ </Stack>
484
+ </Tabs.Panel>
485
+ </Tabs>
486
+
487
+ {/* Protocol Status Footer */}
488
+ <Box
489
+ style={{
490
+ marginTop: "auto",
491
+ paddingTop: "12px",
492
+ borderTop: "1px solid var(--border-subtle)",
493
+ }}
494
+ >
495
+ <Stack direction="row" gap="sm" align="center">
496
+ <Badge variant="success" size="sm" dot>
497
+ {isShim ? "Connected" : "Native"}
498
+ </Badge>
499
+ <Text size="2xs" color="tertiary">
500
+ W3C Draft
501
+ </Text>
502
+ <Text size="2xs" color="tertiary">
503
+ {tools.length} tool{tools.length !== 1 ? "s" : ""}
504
+ </Text>
505
+ </Stack>
506
+ </Box>
507
+ </Stack>
508
+ );
509
+ }
@@ -0,0 +1,47 @@
1
+ import { useEffect, type ReactNode } from "react";
2
+ import { WebMCPProvider, useWebMCPContext } from "@fragments-sdk/webmcp/react";
3
+ import { useFragmentTools } from "@fragments-sdk/webmcp/fragments";
4
+ import { useCompiledFragments } from "../hooks/useCompiledFragments.js";
5
+ import { useToolAnalytics } from "../webmcp/use-tool-analytics.js";
6
+ import { RuntimeToolsRegistrar } from "./RuntimeToolsRegistrar.js";
7
+
8
+ function FragmentToolsRegistrar() {
9
+ const { data } = useCompiledFragments();
10
+ const { shimRegistry } = useWebMCPContext();
11
+ const { count } = useFragmentTools(data);
12
+
13
+ // Expose registry on window for E2E testing (dev-only)
14
+ useEffect(() => {
15
+ if (!shimRegistry || typeof window === "undefined") return;
16
+ (window as any).__WEBMCP_REGISTRY__ = shimRegistry;
17
+ (window as any).__WEBMCP_TOOL_COUNT__ = shimRegistry.tools.size;
18
+
19
+ const onToolChange = () => {
20
+ (window as any).__WEBMCP_TOOL_COUNT__ = shimRegistry.tools.size;
21
+ };
22
+ shimRegistry.addEventListener("toolchange", onToolChange);
23
+ return () => shimRegistry.removeEventListener("toolchange", onToolChange);
24
+ }, [shimRegistry]);
25
+
26
+ return null;
27
+ }
28
+
29
+ function AnalyticsWiring() {
30
+ useToolAnalytics();
31
+ return null;
32
+ }
33
+
34
+ interface WebMCPIntegrationProps {
35
+ children: ReactNode;
36
+ }
37
+
38
+ export function WebMCPIntegration({ children }: WebMCPIntegrationProps) {
39
+ return (
40
+ <WebMCPProvider devShim>
41
+ <FragmentToolsRegistrar />
42
+ <RuntimeToolsRegistrar />
43
+ <AnalyticsWiring />
44
+ {children}
45
+ </WebMCPProvider>
46
+ );
47
+ }
@@ -0,0 +1,60 @@
1
+ import { useState, useEffect } from "react";
2
+ import { useWebMCPContext } from "@fragments-sdk/webmcp/react";
3
+ import { Badge, Drawer } from "@fragments-sdk/ui";
4
+ import { WebMCPDevTools } from "./WebMCPDevTools.js";
5
+
6
+ export function WebMCPStatusIndicator() {
7
+ const { shimRegistry, isSupported, isShim } = useWebMCPContext();
8
+ const [toolCount, setToolCount] = useState(0);
9
+ const [drawerOpen, setDrawerOpen] = useState(false);
10
+
11
+ useEffect(() => {
12
+ if (!shimRegistry) return;
13
+
14
+ const update = () => setToolCount(shimRegistry.tools.size);
15
+ update();
16
+
17
+ shimRegistry.addEventListener("toolchange", update);
18
+ return () => shimRegistry.removeEventListener("toolchange", update);
19
+ }, [shimRegistry]);
20
+
21
+ if (!isSupported || toolCount === 0) return null;
22
+
23
+ return (
24
+ <>
25
+ <button
26
+ onClick={() => setDrawerOpen(true)}
27
+ title={`WebMCP: ${toolCount} tool${toolCount !== 1 ? "s" : ""} registered`}
28
+ data-testid="webmcp-status"
29
+ style={{
30
+ display: "inline-flex",
31
+ alignItems: "center",
32
+ background: "none",
33
+ border: "none",
34
+ padding: 0,
35
+ cursor: "pointer",
36
+ }}
37
+ >
38
+ <Badge variant="success" size="sm" dot>
39
+ WebMCP {toolCount}
40
+ </Badge>
41
+ </button>
42
+
43
+ <Drawer open={drawerOpen} onOpenChange={setDrawerOpen} modal={false}>
44
+ <Drawer.Content side="right" size="lg">
45
+ <Drawer.Close />
46
+ <Drawer.Header>
47
+ <Drawer.Title>WebMCP DevTools</Drawer.Title>
48
+ <Drawer.Description>
49
+ {toolCount} tool{toolCount !== 1 ? "s" : ""} registered via{" "}
50
+ {isShim ? "dev shim" : "native API"}
51
+ </Drawer.Description>
52
+ </Drawer.Header>
53
+ <Drawer.Body>
54
+ <WebMCPDevTools />
55
+ </Drawer.Body>
56
+ </Drawer.Content>
57
+ </Drawer>
58
+ </>
59
+ );
60
+ }
@@ -4,6 +4,7 @@ import { App } from "./components/App.js";
4
4
  import { ThemeProvider } from "./components/ThemeProvider.js";
5
5
  import { ToastProvider } from "./components/Toast.js";
6
6
  import { AppSkeleton } from "./components/SkeletonLoader.js";
7
+ import { WebMCPIntegration } from "./components/WebMCPIntegration.js";
7
8
  // Fragments UI globals - base resets (box-sizing, body, scrollbars) + --fui-* CSS variables
8
9
  import "@fragments-sdk/ui";
9
10
  // Viewer-specific token aliases and utility classes
@@ -249,9 +250,11 @@ if (rootElement) {
249
250
  appRoot?.render(
250
251
  <ThemeProvider>
251
252
  <ToastProvider position="bottom-right" duration={3000}>
252
- <AppErrorBoundary>
253
- <App fragments={currentFragments} />
254
- </AppErrorBoundary>
253
+ <WebMCPIntegration>
254
+ <AppErrorBoundary>
255
+ <App fragments={currentFragments} />
256
+ </AppErrorBoundary>
257
+ </WebMCPIntegration>
255
258
  </ToastProvider>
256
259
  </ThemeProvider>
257
260
  );