@fragments-sdk/cli 0.7.14 → 0.7.16

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 (67) hide show
  1. package/dist/bin.js +7 -7
  2. package/dist/{chunk-CRTN6BIW.js → chunk-QLTLLQBI.js} +2 -2
  3. package/dist/{chunk-TQOGBAOZ.js → chunk-WLXFE6XW.js} +91 -2
  4. package/dist/chunk-WLXFE6XW.js.map +1 -0
  5. package/dist/core/index.d.ts +44 -3
  6. package/dist/core/index.js +11 -3
  7. package/dist/{defineFragment-C6PFzZyo.d.ts → defineFragment-BI9KoPrs.d.ts} +1 -1
  8. package/dist/{generate-ZPERYZLF.js → generate-ICIPKCKV.js} +2 -2
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +2 -2
  11. package/dist/init-DIZ6UNBL.js +806 -0
  12. package/dist/init-DIZ6UNBL.js.map +1 -0
  13. package/dist/mcp-bin.js +2 -2
  14. package/dist/{scan-BSMLGBX4.js → scan-X3DI2X5G.js} +2 -2
  15. package/dist/{service-QACVPR37.js → service-JEWWTSKI.js} +2 -2
  16. package/dist/{static-viewer-2RQD5QLR.js → static-viewer-JIWCYKVK.js} +2 -2
  17. package/dist/{tokens-A3BZIQPB.js → tokens-K2AGUUOJ.js} +2 -2
  18. package/dist/{viewer-CNLZQUFO.js → viewer-QKIAPTPG.js} +126 -15
  19. package/dist/viewer-QKIAPTPG.js.map +1 -0
  20. package/package.json +3 -2
  21. package/src/commands/init-framework.ts +414 -0
  22. package/src/commands/init.ts +41 -1
  23. package/src/core/__tests__/preview-runtime.test.tsx +111 -0
  24. package/src/core/index.ts +13 -0
  25. package/src/core/preview-runtime.tsx +144 -0
  26. package/src/viewer/components/App.tsx +8 -3
  27. package/src/viewer/components/FragmentRenderer.tsx +61 -0
  28. package/src/viewer/components/HealthDashboard.tsx +1 -1
  29. package/src/viewer/components/IsolatedPreviewFrame.tsx +10 -8
  30. package/src/viewer/components/PreviewFrameHost.tsx +27 -60
  31. package/src/viewer/components/PropsTable.tsx +2 -2
  32. package/src/viewer/components/RuntimeToolsRegistrar.tsx +17 -0
  33. package/src/viewer/components/SkeletonLoader.tsx +114 -125
  34. package/src/viewer/components/VariantMatrix.tsx +3 -3
  35. package/src/viewer/components/ViewerStateSync.tsx +52 -0
  36. package/src/viewer/components/WebMCPDevTools.tsx +509 -0
  37. package/src/viewer/components/WebMCPIntegration.tsx +47 -0
  38. package/src/viewer/components/WebMCPStatusIndicator.tsx +60 -0
  39. package/src/viewer/entry.tsx +32 -5
  40. package/src/viewer/hooks/useA11yService.ts +1 -135
  41. package/src/viewer/hooks/useCompiledFragments.ts +42 -0
  42. package/src/viewer/index.html +1 -1
  43. package/src/viewer/public/favicon.ico +0 -0
  44. package/src/viewer/server.ts +59 -3
  45. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +19 -0
  46. package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +1 -1
  47. package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +69 -104
  48. package/src/viewer/vite-plugin.ts +76 -1
  49. package/src/viewer/webmcp/__tests__/analytics.test.ts +108 -0
  50. package/src/viewer/webmcp/analytics.ts +165 -0
  51. package/src/viewer/webmcp/index.ts +3 -0
  52. package/src/viewer/webmcp/posthog-bridge.ts +39 -0
  53. package/src/viewer/webmcp/runtime-tools.ts +152 -0
  54. package/src/viewer/webmcp/scan-utils.ts +135 -0
  55. package/src/viewer/webmcp/use-tool-analytics.ts +69 -0
  56. package/src/viewer/webmcp/viewer-state.ts +45 -0
  57. package/dist/chunk-TQOGBAOZ.js.map +0 -1
  58. package/dist/init-GID2DXB3.js +0 -498
  59. package/dist/init-GID2DXB3.js.map +0 -1
  60. package/dist/viewer-CNLZQUFO.js.map +0 -1
  61. package/src/viewer/components/StoryRenderer.tsx +0 -121
  62. /package/dist/{chunk-CRTN6BIW.js.map → chunk-QLTLLQBI.js.map} +0 -0
  63. /package/dist/{generate-ZPERYZLF.js.map → generate-ICIPKCKV.js.map} +0 -0
  64. /package/dist/{scan-BSMLGBX4.js.map → scan-X3DI2X5G.js.map} +0 -0
  65. /package/dist/{service-QACVPR37.js.map → service-JEWWTSKI.js.map} +0 -0
  66. /package/dist/{static-viewer-2RQD5QLR.js.map → static-viewer-JIWCYKVK.js.map} +0 -0
  67. /package/dist/{tokens-A3BZIQPB.js.map → tokens-K2AGUUOJ.js.map} +0 -0
@@ -24,6 +24,7 @@ import {
24
24
  findStorybookDir,
25
25
  findPreviewConfigPath,
26
26
  generatePreviewModule,
27
+ parseFragmentFile,
27
28
  } from "../core/node.js";
28
29
  import svgr from "vite-plugin-svgr";
29
30
  import {
@@ -927,6 +928,24 @@ export function fragmentsPlugin(options: FragmentsPluginOptions): Plugin[] {
927
928
  return;
928
929
  }
929
930
 
931
+ // Handle /fragments/compiled.json endpoint for WebMCP integration
932
+ if (req.url === "/fragments/compiled.json") {
933
+ try {
934
+ const { join } = await import("node:path");
935
+ const fragmentsJsonPath = join(projectRoot, BRAND.outFile);
936
+ const content = await readFile(fragmentsJsonPath, "utf-8");
937
+ res.writeHead(200, { "Content-Type": "application/json" });
938
+ res.end(content);
939
+ } catch (error) {
940
+ console.warn(`[${BRAND.name}] Failed to serve compiled.json:`, error);
941
+ res.writeHead(404, { "Content-Type": "application/json" });
942
+ res.end(JSON.stringify({
943
+ error: `${BRAND.outFile} not found. Run '${BRAND.cliCommand} build' first.`,
944
+ }));
945
+ }
946
+ return;
947
+ }
948
+
930
949
  // Handle /fragments/context endpoint for AI context generation
931
950
  if (req.url?.startsWith("/fragments/context")) {
932
951
  try {
@@ -1617,6 +1636,33 @@ async function generateFragmentsModule(
1617
1636
  config: FragmentsConfig,
1618
1637
  previewConfigPath: string | null
1619
1638
  ): Promise<string> {
1639
+ const authoredVariantCodeCache = new Map<string, Record<string, string>>();
1640
+
1641
+ async function loadAuthoredVariantCode(fragmentFilePath: string): Promise<Record<string, string>> {
1642
+ if (authoredVariantCodeCache.has(fragmentFilePath)) {
1643
+ return authoredVariantCodeCache.get(fragmentFilePath)!;
1644
+ }
1645
+
1646
+ try {
1647
+ const source = await readFile(fragmentFilePath, "utf-8");
1648
+ const parsed = parseFragmentFile(source, fragmentFilePath);
1649
+ const variantCodeByName: Record<string, string> = {};
1650
+
1651
+ for (const variant of parsed.variants) {
1652
+ if (typeof variant.code !== "string") continue;
1653
+ const normalized = variant.code.trim();
1654
+ if (normalized.length === 0) continue;
1655
+ variantCodeByName[variant.name] = normalized;
1656
+ }
1657
+
1658
+ authoredVariantCodeCache.set(fragmentFilePath, variantCodeByName);
1659
+ return variantCodeByName;
1660
+ } catch {
1661
+ authoredVariantCodeCache.set(fragmentFilePath, {});
1662
+ return {};
1663
+ }
1664
+ }
1665
+
1620
1666
  // Group files by base component path to identify pairs
1621
1667
  const filesByBasePath = new Map<string, {
1622
1668
  storyFile?: { absolutePath: string; relativePath: string };
@@ -1653,6 +1699,14 @@ async function generateFragmentsModule(
1653
1699
  ? files.fragmentFile.absolutePath
1654
1700
  : null;
1655
1701
 
1702
+ const codeSourceFile = files.fragmentFile && files.fragmentFile.absolutePath
1703
+ ? files.fragmentFile.absolutePath
1704
+ : (!isStory ? primaryFile.absolutePath : null);
1705
+
1706
+ const authoredVariantCode = codeSourceFile
1707
+ ? await loadAuthoredVariantCode(codeSourceFile)
1708
+ : {};
1709
+
1656
1710
  // Extract component name from file path
1657
1711
  const componentName = extractComponentName(primaryFile.relativePath);
1658
1712
 
@@ -1666,7 +1720,8 @@ async function generateFragmentsModule(
1666
1720
  componentName: ${JSON.stringify(componentName)},
1667
1721
  dependencies: ${JSON.stringify(dependencies)},
1668
1722
  loader: () => import("${primaryFile.absolutePath}"),
1669
- metadataLoader: ${metadataPath ? `() => import("${metadataPath}")` : 'null'}
1723
+ metadataLoader: ${metadataPath ? `() => import("${metadataPath}")` : 'null'},
1724
+ authoredVariantCode: ${JSON.stringify(authoredVariantCode)}
1670
1725
  }`;
1671
1726
  })
1672
1727
  );
@@ -1743,6 +1798,20 @@ function mergeMetadata(fragment, metadataModule) {
1743
1798
  return fragment;
1744
1799
  }
1745
1800
 
1801
+ function mergeAuthoredVariantCode(fragment, authoredVariantCode) {
1802
+ if (!fragment?.variants || !authoredVariantCode) return fragment;
1803
+
1804
+ for (const variant of fragment.variants) {
1805
+ if (!variant || variant.code) continue;
1806
+ const code = authoredVariantCode[variant.name];
1807
+ if (typeof code === "string" && code.trim().length > 0) {
1808
+ variant.code = code;
1809
+ }
1810
+ }
1811
+
1812
+ return fragment;
1813
+ }
1814
+
1746
1815
  // Load all fragments (for initial render)
1747
1816
  // Gracefully handles individual failures - one bad story won't break all fragments
1748
1817
  export async function loadAllFragments() {
@@ -1780,6 +1849,8 @@ export async function loadAllFragments() {
1780
1849
  }
1781
1850
  }
1782
1851
 
1852
+ fragment = mergeAuthoredVariantCode(fragment, loader.authoredVariantCode);
1853
+
1783
1854
  loadedFragments.set(loader.path, fragment);
1784
1855
  return { path: loader.path, fragment };
1785
1856
  } catch (error) {
@@ -1837,6 +1908,10 @@ export async function loadFragment(path) {
1837
1908
  }
1838
1909
  }
1839
1910
 
1911
+ if (fragment) {
1912
+ fragment = mergeAuthoredVariantCode(fragment, loader.authoredVariantCode);
1913
+ }
1914
+
1840
1915
  if (fragment) {
1841
1916
  loadedFragments.set(path, fragment);
1842
1917
  }
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ToolAnalyticsCollector } from '../analytics.js';
3
+ import type { ToolCallEvent } from '@fragments-sdk/webmcp';
4
+
5
+ function createEvent(overrides: Partial<ToolCallEvent> = {}): ToolCallEvent {
6
+ return {
7
+ toolName: 'fragments_discover',
8
+ input: {},
9
+ output: {},
10
+ error: null,
11
+ durationMs: 50,
12
+ timestamp: Date.now(),
13
+ ...overrides,
14
+ };
15
+ }
16
+
17
+ describe('ToolAnalyticsCollector', () => {
18
+ it('records events and computes summary', () => {
19
+ const collector = new ToolAnalyticsCollector();
20
+ collector.record(createEvent({ toolName: 'fragments_discover', durationMs: 10 }));
21
+ collector.record(createEvent({ toolName: 'fragments_discover', durationMs: 20 }));
22
+ collector.record(createEvent({ toolName: 'fragments_inspect', durationMs: 30 }));
23
+
24
+ const summary = collector.toSummary();
25
+ expect(summary.totalCalls).toBe(3);
26
+ expect(summary.uniqueTools).toBe(2);
27
+ expect(summary.toolStats.fragments_discover.callCount).toBe(2);
28
+ expect(summary.toolStats.fragments_discover.avgDuration).toBe(15);
29
+ expect(summary.toolStats.fragments_inspect.callCount).toBe(1);
30
+ });
31
+
32
+ it('tracks error rate', () => {
33
+ const collector = new ToolAnalyticsCollector();
34
+ collector.record(createEvent({ error: null }));
35
+ collector.record(createEvent({ error: 'fail' }));
36
+
37
+ const summary = collector.toSummary();
38
+ expect(summary.errorRate).toBe(0.5);
39
+ });
40
+
41
+ it('detects common flows', () => {
42
+ const collector = new ToolAnalyticsCollector();
43
+ const base = Date.now();
44
+
45
+ // Create a repeated flow: discover -> inspect -> discover -> inspect
46
+ collector.record(createEvent({ toolName: 'fragments_discover', timestamp: base }));
47
+ collector.record(createEvent({ toolName: 'fragments_inspect', timestamp: base + 100 }));
48
+ collector.record(createEvent({ toolName: 'fragments_discover', timestamp: base + 200 }));
49
+ collector.record(createEvent({ toolName: 'fragments_inspect', timestamp: base + 300 }));
50
+
51
+ const summary = collector.toSummary();
52
+ expect(summary.commonFlows.length).toBeGreaterThan(0);
53
+ expect(summary.commonFlows[0].sequence).toEqual(['fragments_discover', 'fragments_inspect']);
54
+ expect(summary.commonFlows[0].count).toBe(2);
55
+ });
56
+
57
+ it('splits sessions on 30s gap', () => {
58
+ const collector = new ToolAnalyticsCollector();
59
+ const base = Date.now();
60
+
61
+ collector.record(createEvent({ toolName: 'a', timestamp: base }));
62
+ collector.record(createEvent({ toolName: 'b', timestamp: base + 100 }));
63
+ // 31s gap
64
+ collector.record(createEvent({ toolName: 'a', timestamp: base + 31_100 }));
65
+ collector.record(createEvent({ toolName: 'b', timestamp: base + 31_200 }));
66
+
67
+ const summary = collector.toSummary();
68
+ // "a -> b" appears in both sessions
69
+ const abFlow = summary.commonFlows.find(f =>
70
+ f.sequence[0] === 'a' && f.sequence[1] === 'b'
71
+ );
72
+ expect(abFlow).toBeDefined();
73
+ expect(abFlow!.count).toBe(2);
74
+ });
75
+
76
+ it('computes percentiles', () => {
77
+ const collector = new ToolAnalyticsCollector();
78
+ for (let i = 1; i <= 100; i++) {
79
+ collector.record(createEvent({ durationMs: i }));
80
+ }
81
+
82
+ const summary = collector.toSummary();
83
+ const stat = summary.toolStats.fragments_discover;
84
+ expect(stat.p50).toBeGreaterThanOrEqual(49);
85
+ expect(stat.p50).toBeLessThanOrEqual(51);
86
+ expect(stat.p95).toBeGreaterThanOrEqual(94);
87
+ expect(stat.p95).toBeLessThanOrEqual(96);
88
+ });
89
+
90
+ it('produces PostHog properties', () => {
91
+ const collector = new ToolAnalyticsCollector();
92
+ collector.record(createEvent());
93
+
94
+ const props = collector.toPostHogProperties();
95
+ expect(props.webmcp_total_calls).toBe(1);
96
+ expect(props.webmcp_unique_tools).toBe(1);
97
+ expect(typeof props.webmcp_tool_breakdown).toBe('string');
98
+ });
99
+
100
+ it('resets state', () => {
101
+ const collector = new ToolAnalyticsCollector();
102
+ collector.record(createEvent());
103
+ collector.reset();
104
+
105
+ const summary = collector.toSummary();
106
+ expect(summary.totalCalls).toBe(0);
107
+ });
108
+ });
@@ -0,0 +1,165 @@
1
+ import type { ToolCallEvent } from '@fragments-sdk/webmcp';
2
+
3
+ interface ToolStats {
4
+ callCount: number;
5
+ totalDuration: number;
6
+ durations: number[];
7
+ errorCount: number;
8
+ }
9
+
10
+ interface ToolFlow {
11
+ sequence: string[];
12
+ count: number;
13
+ }
14
+
15
+ interface AnalyticsSummary {
16
+ totalCalls: number;
17
+ uniqueTools: number;
18
+ sessionDurationMs: number;
19
+ errorRate: number;
20
+ toolStats: Record<string, {
21
+ callCount: number;
22
+ avgDuration: number;
23
+ p50: number;
24
+ p95: number;
25
+ errorCount: number;
26
+ }>;
27
+ commonFlows: ToolFlow[];
28
+ }
29
+
30
+ const SESSION_GAP_MS = 30_000;
31
+
32
+ export class ToolAnalyticsCollector {
33
+ private stats = new Map<string, ToolStats>();
34
+ private events: ToolCallEvent[] = [];
35
+ private sessionStart: number | null = null;
36
+
37
+ record(event: ToolCallEvent): void {
38
+ this.events.push(event);
39
+
40
+ if (this.sessionStart === null) {
41
+ this.sessionStart = event.timestamp;
42
+ }
43
+
44
+ const existing = this.stats.get(event.toolName);
45
+ if (existing) {
46
+ existing.callCount++;
47
+ existing.totalDuration += event.durationMs;
48
+ existing.durations.push(event.durationMs);
49
+ if (event.error) existing.errorCount++;
50
+ } else {
51
+ this.stats.set(event.toolName, {
52
+ callCount: 1,
53
+ totalDuration: event.durationMs,
54
+ durations: [event.durationMs],
55
+ errorCount: event.error ? 1 : 0,
56
+ });
57
+ }
58
+ }
59
+
60
+ toSummary(): AnalyticsSummary {
61
+ let totalCalls = 0;
62
+ let totalErrors = 0;
63
+ const toolStats: AnalyticsSummary['toolStats'] = {};
64
+
65
+ for (const [name, stat] of this.stats) {
66
+ totalCalls += stat.callCount;
67
+ totalErrors += stat.errorCount;
68
+
69
+ const sorted = [...stat.durations].sort((a, b) => a - b);
70
+ toolStats[name] = {
71
+ callCount: stat.callCount,
72
+ avgDuration: Math.round(stat.totalDuration / stat.callCount),
73
+ p50: percentile(sorted, 50),
74
+ p95: percentile(sorted, 95),
75
+ errorCount: stat.errorCount,
76
+ };
77
+ }
78
+
79
+ const sessionDurationMs = this.sessionStart !== null && this.events.length > 0
80
+ ? this.events[this.events.length - 1].timestamp - this.sessionStart
81
+ : 0;
82
+
83
+ return {
84
+ totalCalls,
85
+ uniqueTools: this.stats.size,
86
+ sessionDurationMs,
87
+ errorRate: totalCalls > 0 ? Math.round((totalErrors / totalCalls) * 100) / 100 : 0,
88
+ toolStats,
89
+ commonFlows: this.detectFlows(),
90
+ };
91
+ }
92
+
93
+ toPostHogProperties(): Record<string, unknown> {
94
+ const summary = this.toSummary();
95
+ return {
96
+ webmcp_total_calls: summary.totalCalls,
97
+ webmcp_unique_tools: summary.uniqueTools,
98
+ webmcp_session_duration_ms: summary.sessionDurationMs,
99
+ webmcp_error_rate: summary.errorRate,
100
+ webmcp_tool_breakdown: JSON.stringify(summary.toolStats),
101
+ webmcp_common_flows: JSON.stringify(summary.commonFlows),
102
+ };
103
+ }
104
+
105
+ private detectFlows(): ToolFlow[] {
106
+ if (this.events.length < 2) return [];
107
+
108
+ // Split into sessions (30s gap)
109
+ const sessions: ToolCallEvent[][] = [];
110
+ let currentSession: ToolCallEvent[] = [this.events[0]];
111
+
112
+ for (let i = 1; i < this.events.length; i++) {
113
+ if (this.events[i].timestamp - this.events[i - 1].timestamp > SESSION_GAP_MS) {
114
+ sessions.push(currentSession);
115
+ currentSession = [];
116
+ }
117
+ currentSession.push(this.events[i]);
118
+ }
119
+ sessions.push(currentSession);
120
+
121
+ // Count 2-grams and 3-grams
122
+ const flowCounts = new Map<string, number>();
123
+
124
+ for (const session of sessions) {
125
+ const names = session.map(e => e.toolName);
126
+
127
+ // 2-grams
128
+ for (let i = 0; i < names.length - 1; i++) {
129
+ const key = `${names[i]} → ${names[i + 1]}`;
130
+ flowCounts.set(key, (flowCounts.get(key) ?? 0) + 1);
131
+ }
132
+
133
+ // 3-grams
134
+ for (let i = 0; i < names.length - 2; i++) {
135
+ const key = `${names[i]} → ${names[i + 1]} → ${names[i + 2]}`;
136
+ flowCounts.set(key, (flowCounts.get(key) ?? 0) + 1);
137
+ }
138
+ }
139
+
140
+ // Return top flows (count >= 2)
141
+ return [...flowCounts.entries()]
142
+ .filter(([, count]) => count >= 2)
143
+ .sort((a, b) => b[1] - a[1])
144
+ .slice(0, 10)
145
+ .map(([key, count]) => ({
146
+ sequence: key.split(' → '),
147
+ count,
148
+ }));
149
+ }
150
+
151
+ reset(): void {
152
+ this.stats.clear();
153
+ this.events = [];
154
+ this.sessionStart = null;
155
+ }
156
+ }
157
+
158
+ function percentile(sorted: number[], p: number): number {
159
+ if (sorted.length === 0) return 0;
160
+ const index = (p / 100) * (sorted.length - 1);
161
+ const lower = Math.floor(index);
162
+ const upper = Math.ceil(index);
163
+ if (lower === upper) return sorted[lower];
164
+ return Math.round(sorted[lower] + (sorted[upper] - sorted[lower]) * (index - lower));
165
+ }
@@ -0,0 +1,3 @@
1
+ export { runAxeScan, serializeViolation, type ScanResult } from './scan-utils.js';
2
+ export { createRuntimeWebMCPTools, type RuntimeToolsOptions } from './runtime-tools.js';
3
+ export { getViewerState, setViewerState, type ViewerState } from './viewer-state.js';
@@ -0,0 +1,39 @@
1
+ import type { ToolCallEvent } from '@fragments-sdk/webmcp';
2
+ import type { ToolAnalyticsCollector } from './analytics.js';
3
+
4
+ interface PostHogLike {
5
+ capture(event: string, properties?: Record<string, unknown>): void;
6
+ }
7
+
8
+ interface PostHogBridge {
9
+ onToolCall(event: ToolCallEvent): void;
10
+ onSessionEnd(): void;
11
+ flush(): void;
12
+ }
13
+
14
+ export function createPostHogBridge(
15
+ posthog: PostHogLike,
16
+ collector: ToolAnalyticsCollector
17
+ ): PostHogBridge {
18
+ return {
19
+ onToolCall(event: ToolCallEvent): void {
20
+ posthog.capture('webmcp_tool_call', {
21
+ tool_name: event.toolName,
22
+ duration_ms: event.durationMs,
23
+ success: event.error === null,
24
+ error: event.error,
25
+ });
26
+ },
27
+
28
+ onSessionEnd(): void {
29
+ const props = collector.toPostHogProperties();
30
+ if ((props.webmcp_total_calls as number) > 0) {
31
+ posthog.capture('webmcp_session_summary', props);
32
+ }
33
+ },
34
+
35
+ flush(): void {
36
+ this.onSessionEnd();
37
+ },
38
+ };
39
+ }
@@ -0,0 +1,152 @@
1
+ import type { WebMCPTool, InputSchema } from '@fragments-sdk/webmcp';
2
+ import type { CompiledFragmentsFile } from '@fragments-sdk/context/types';
3
+ import { runAxeScan } from './scan-utils.js';
4
+ import { getViewerState } from './viewer-state.js';
5
+
6
+ export interface RuntimeToolsOptions {
7
+ compiledData?: CompiledFragmentsFile | null;
8
+ }
9
+
10
+ export function createRuntimeWebMCPTools(options: RuntimeToolsOptions = {}): WebMCPTool[] {
11
+ const tools: WebMCPTool[] = [];
12
+
13
+ // --- fragments_a11y ---
14
+ tools.push({
15
+ name: 'fragments_a11y',
16
+ description: 'Run an accessibility audit on the currently previewed component. Returns axe-core violations, a WCAG compliance score, and severity counts. Runs directly on the live DOM for accurate results.',
17
+ inputSchema: {
18
+ type: 'object',
19
+ properties: {
20
+ component: { type: 'string', description: 'Component name to audit' },
21
+ variant: { type: 'string', description: 'Specific variant to audit' },
22
+ standard: { type: 'string', enum: ['AA', 'AAA'], default: 'AA', description: 'WCAG compliance level' },
23
+ },
24
+ required: ['component'],
25
+ } as InputSchema,
26
+ annotations: { readOnlyHint: true },
27
+ execute: async (input) => {
28
+ const component = input.component as string;
29
+ const standard = (input.standard as string) || 'AA';
30
+
31
+ const result = await runAxeScan('[data-preview-container="true"]');
32
+ if (!result) {
33
+ return { error: true, message: 'No preview container found. Navigate to a component first.' };
34
+ }
35
+
36
+ const total = result.passes + result.violations.length + result.incomplete;
37
+ const wcagScore = total > 0 ? Math.round((result.passes / total) * 100) : 100;
38
+
39
+ return {
40
+ component,
41
+ wcagScore,
42
+ standard,
43
+ violations: result.violations,
44
+ passCount: result.passes,
45
+ incompleteCount: result.incomplete,
46
+ counts: result.counts,
47
+ };
48
+ },
49
+ });
50
+
51
+ // --- fragments_theme ---
52
+ tools.push({
53
+ name: 'fragments_theme',
54
+ description: 'Inspect the current runtime theme state. Returns the resolved theme (light/dark), seed configuration values, and computed CSS token values from the live DOM.',
55
+ inputSchema: {
56
+ type: 'object',
57
+ properties: {
58
+ category: { type: 'string', description: 'Filter tokens by category (e.g., "colors", "spacing")' },
59
+ search: { type: 'string', description: 'Search token names' },
60
+ includeComputed: { type: 'boolean', default: true, description: 'Include computed CSS values' },
61
+ },
62
+ } as InputSchema,
63
+ annotations: { readOnlyHint: true },
64
+ execute: async (input) => {
65
+ const category = input.category as string | undefined;
66
+ const search = input.search as string | undefined;
67
+ const includeComputed = input.includeComputed !== false;
68
+
69
+ const root = document.documentElement;
70
+ const computedStyle = getComputedStyle(root);
71
+
72
+ // Detect theme
73
+ const resolvedTheme = root.classList.contains('dark') ? 'dark' : 'light';
74
+
75
+ // Read seed values
76
+ const seed = {
77
+ brand: computedStyle.getPropertyValue('--fui-seed-brand').trim() || null,
78
+ neutral: computedStyle.getPropertyValue('--fui-seed-neutral').trim() || null,
79
+ radius: computedStyle.getPropertyValue('--fui-seed-radius').trim() || null,
80
+ density: computedStyle.getPropertyValue('--fui-seed-density').trim() || null,
81
+ };
82
+
83
+ // Get tokens from compiled data
84
+ const compiledData = options.compiledData;
85
+ const tokenEntries: Array<{ name: string; value: string; computed?: string; category?: string }> = [];
86
+
87
+ if (compiledData?.tokens?.categories) {
88
+ for (const [cat, entries] of Object.entries(compiledData.tokens.categories)) {
89
+ if (category && cat.toLowerCase() !== category.toLowerCase()) continue;
90
+
91
+ for (const token of entries) {
92
+ if (search && !token.name.toLowerCase().includes(search.toLowerCase())) continue;
93
+
94
+ const entry: { name: string; value: string; computed?: string; category?: string } = {
95
+ name: token.name,
96
+ value: token.value,
97
+ category: cat,
98
+ };
99
+
100
+ if (includeComputed) {
101
+ entry.computed = computedStyle.getPropertyValue(token.name).trim() || token.value;
102
+ }
103
+
104
+ tokenEntries.push(entry);
105
+ }
106
+ }
107
+ }
108
+
109
+ return {
110
+ resolvedTheme,
111
+ seed,
112
+ tokens: tokenEntries,
113
+ tokenCount: tokenEntries.length,
114
+ };
115
+ },
116
+ });
117
+
118
+ // --- fragments_context ---
119
+ tools.push({
120
+ name: 'fragments_context',
121
+ description: 'Get information about what the developer is currently viewing in the Fragments dev viewer. Returns the active component, variant, viewport settings, theme, and panel state.',
122
+ inputSchema: {
123
+ type: 'object',
124
+ properties: {
125
+ fields: {
126
+ type: 'array',
127
+ items: { type: 'string' },
128
+ description: 'Specific fields to return (e.g., ["currentComponent", "theme"]). Returns all fields if omitted.',
129
+ },
130
+ },
131
+ } as InputSchema,
132
+ annotations: { readOnlyHint: true },
133
+ execute: async (input) => {
134
+ const state = getViewerState();
135
+ const fields = input.fields as string[] | undefined;
136
+
137
+ if (fields && fields.length > 0) {
138
+ const filtered: Record<string, unknown> = {};
139
+ for (const field of fields) {
140
+ if (field in state) {
141
+ filtered[field] = state[field as keyof typeof state];
142
+ }
143
+ }
144
+ return filtered;
145
+ }
146
+
147
+ return state;
148
+ },
149
+ });
150
+
151
+ return tools;
152
+ }