@fragments-sdk/webmcp 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fragments/index.d.ts +88 -0
- package/dist/fragments/index.js +579 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +25 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +15 -0
- package/package.json +9 -16
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { CompiledFragmentsFile } from '@fragments-sdk/context/types';
|
|
2
|
+
import { WebMCPTool } from '@webmcp-sdk/core';
|
|
3
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
|
+
import { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
interface CreateFragmentsWebMCPToolsOptions {
|
|
7
|
+
/** Tool name prefix. Default: 'fragments'. */
|
|
8
|
+
prefix?: string;
|
|
9
|
+
/** Subset of tools to register. If omitted, all tools are registered. */
|
|
10
|
+
tools?: Array<'discover' | 'inspect' | 'blocks' | 'tokens' | 'implement' | 'graph'>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Build WebMCPTool[] from compiled fragments data.
|
|
14
|
+
*
|
|
15
|
+
* Reuses `buildMcpTools()` from @fragments-sdk/context for schema conversion
|
|
16
|
+
* (single source of truth — no schema drift). Execute handlers search/filter
|
|
17
|
+
* CompiledFragmentsFile data in-browser.
|
|
18
|
+
*/
|
|
19
|
+
declare function createFragmentsWebMCPTools(data: CompiledFragmentsFile, options?: CreateFragmentsWebMCPToolsOptions): WebMCPTool[];
|
|
20
|
+
|
|
21
|
+
interface SearchIndex {
|
|
22
|
+
/** component name → lowercased searchable string (name + description + tags + category) */
|
|
23
|
+
components: Map<string, string>;
|
|
24
|
+
/** block name → lowercased searchable string */
|
|
25
|
+
blocks: Map<string, string>;
|
|
26
|
+
}
|
|
27
|
+
declare function buildSearchIndex(data: CompiledFragmentsFile): SearchIndex;
|
|
28
|
+
type FragmentHandler = (data: CompiledFragmentsFile, args: Record<string, unknown>, index: SearchIndex) => unknown;
|
|
29
|
+
declare const HANDLERS: Record<string, FragmentHandler>;
|
|
30
|
+
|
|
31
|
+
interface UseFragmentToolsOptions extends CreateFragmentsWebMCPToolsOptions {
|
|
32
|
+
/** Disable registration. Default: true. */
|
|
33
|
+
enabled?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Auto-register all Fragments design system tools via WebMCP.
|
|
37
|
+
*
|
|
38
|
+
* Internally creates tools with `createFragmentsWebMCPTools()` and registers
|
|
39
|
+
* them using `useWebMCPTools()`. Memoized by data reference + prefix.
|
|
40
|
+
*/
|
|
41
|
+
declare function useFragmentTools(data: CompiledFragmentsFile | null, options?: UseFragmentToolsOptions): {
|
|
42
|
+
tools: WebMCPTool[];
|
|
43
|
+
count: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
interface UseCompiledFragmentsFromUrlResult {
|
|
47
|
+
data: CompiledFragmentsFile | null;
|
|
48
|
+
loading: boolean;
|
|
49
|
+
error: string | null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Fetch compiled fragments data from a URL.
|
|
53
|
+
* Useful for user apps that want to integrate WebMCP tools.
|
|
54
|
+
*/
|
|
55
|
+
declare function useCompiledFragmentsFromUrl(url: string): UseCompiledFragmentsFromUrlResult;
|
|
56
|
+
|
|
57
|
+
interface FragmentsWebMCPBaseProps {
|
|
58
|
+
/** Tool name prefix. Default: 'fragments'. */
|
|
59
|
+
prefix?: string;
|
|
60
|
+
/** Subset of tools to register. */
|
|
61
|
+
tools?: Array<'discover' | 'inspect' | 'blocks' | 'tokens' | 'implement' | 'graph'>;
|
|
62
|
+
children: ReactNode;
|
|
63
|
+
}
|
|
64
|
+
interface FragmentsWebMCPDataProps extends FragmentsWebMCPBaseProps {
|
|
65
|
+
/** Pre-loaded compiled fragments data. */
|
|
66
|
+
data: CompiledFragmentsFile;
|
|
67
|
+
url?: never;
|
|
68
|
+
}
|
|
69
|
+
interface FragmentsWebMCPUrlProps extends FragmentsWebMCPBaseProps {
|
|
70
|
+
/** URL to fetch compiled fragments data from. */
|
|
71
|
+
url: string;
|
|
72
|
+
data?: never;
|
|
73
|
+
}
|
|
74
|
+
type FragmentsWebMCPProps = FragmentsWebMCPDataProps | FragmentsWebMCPUrlProps;
|
|
75
|
+
/**
|
|
76
|
+
* Batteries-included WebMCP integration for Fragments design system.
|
|
77
|
+
*
|
|
78
|
+
* Makes any app AI-native in 3 lines:
|
|
79
|
+
* ```tsx
|
|
80
|
+
* import compiledData from './fragments.compiled.json';
|
|
81
|
+
* import { FragmentsWebMCP } from '@fragments-sdk/webmcp/fragments';
|
|
82
|
+
*
|
|
83
|
+
* <FragmentsWebMCP data={compiledData}><App /></FragmentsWebMCP>
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
declare function FragmentsWebMCP(props: FragmentsWebMCPProps): react_jsx_runtime.JSX.Element;
|
|
87
|
+
|
|
88
|
+
export { type CreateFragmentsWebMCPToolsOptions, type FragmentHandler, FragmentsWebMCP, type FragmentsWebMCPProps, HANDLERS, type SearchIndex, type UseFragmentToolsOptions, buildSearchIndex, createFragmentsWebMCPTools, useCompiledFragmentsFromUrl, useFragmentTools };
|
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
// src/fragments/create-tools.ts
|
|
2
|
+
import {
|
|
3
|
+
buildMcpTools,
|
|
4
|
+
buildToolNames
|
|
5
|
+
} from "@fragments-sdk/context/mcp-tools";
|
|
6
|
+
|
|
7
|
+
// src/fragments/handlers.ts
|
|
8
|
+
import { ComponentGraphEngine, deserializeGraph } from "@fragments-sdk/context/graph";
|
|
9
|
+
function buildSearchIndex(data) {
|
|
10
|
+
const components = /* @__PURE__ */ new Map();
|
|
11
|
+
for (const [key, frag] of Object.entries(data.fragments)) {
|
|
12
|
+
const searchable = [
|
|
13
|
+
frag.meta.name,
|
|
14
|
+
frag.meta.description,
|
|
15
|
+
frag.meta.category,
|
|
16
|
+
...frag.meta.tags ?? []
|
|
17
|
+
].join(" ").toLowerCase();
|
|
18
|
+
components.set(key, searchable);
|
|
19
|
+
}
|
|
20
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
21
|
+
if (data.blocks) {
|
|
22
|
+
for (const [key, block] of Object.entries(data.blocks)) {
|
|
23
|
+
const searchable = [
|
|
24
|
+
block.name,
|
|
25
|
+
block.description,
|
|
26
|
+
block.category,
|
|
27
|
+
...block.tags ?? [],
|
|
28
|
+
...block.components
|
|
29
|
+
].join(" ").toLowerCase();
|
|
30
|
+
blocks.set(key, searchable);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { components, blocks };
|
|
34
|
+
}
|
|
35
|
+
function clampLimit(value, defaultVal, max = 50) {
|
|
36
|
+
if (typeof value !== "number") return defaultVal;
|
|
37
|
+
return Math.max(1, Math.min(max, Math.floor(value)));
|
|
38
|
+
}
|
|
39
|
+
function truncStr(value, maxLen = 200) {
|
|
40
|
+
if (typeof value !== "string") return "";
|
|
41
|
+
return value.slice(0, maxLen);
|
|
42
|
+
}
|
|
43
|
+
function validateEnum(value, allowed) {
|
|
44
|
+
if (typeof value !== "string") return void 0;
|
|
45
|
+
return allowed.includes(value) ? value : void 0;
|
|
46
|
+
}
|
|
47
|
+
function handleDiscover(data, args, index) {
|
|
48
|
+
const limit = clampLimit(args.limit, 10);
|
|
49
|
+
const search = truncStr(args.search);
|
|
50
|
+
const category = truncStr(args.category);
|
|
51
|
+
const status = validateEnum(args.status, [
|
|
52
|
+
"stable",
|
|
53
|
+
"beta",
|
|
54
|
+
"deprecated",
|
|
55
|
+
"experimental"
|
|
56
|
+
]);
|
|
57
|
+
const component = truncStr(args.component);
|
|
58
|
+
const compact = args.compact === true;
|
|
59
|
+
const verbosity = validateEnum(args.verbosity, [
|
|
60
|
+
"compact",
|
|
61
|
+
"standard",
|
|
62
|
+
"full"
|
|
63
|
+
]);
|
|
64
|
+
let results = Object.values(data.fragments);
|
|
65
|
+
if (category) {
|
|
66
|
+
const lower = category.toLowerCase();
|
|
67
|
+
results = results.filter(
|
|
68
|
+
(f) => f.meta.category.toLowerCase() === lower
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (status) {
|
|
72
|
+
results = results.filter((f) => f.meta.status === status);
|
|
73
|
+
}
|
|
74
|
+
if (component) {
|
|
75
|
+
const lower = component.toLowerCase();
|
|
76
|
+
const target = results.find(
|
|
77
|
+
(f) => f.meta.name.toLowerCase() === lower
|
|
78
|
+
);
|
|
79
|
+
if (target) {
|
|
80
|
+
results = results.filter(
|
|
81
|
+
(f) => f.meta.category === target.meta.category && f.meta.name !== target.meta.name
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (search) {
|
|
86
|
+
const lower = search.toLowerCase();
|
|
87
|
+
results = results.filter((f) => {
|
|
88
|
+
const searchable = index.components.get(f.meta.name) ?? "";
|
|
89
|
+
return searchable.includes(lower);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
results = results.slice(0, limit);
|
|
93
|
+
if (compact || verbosity === "compact") {
|
|
94
|
+
return {
|
|
95
|
+
components: results.map((f) => ({
|
|
96
|
+
name: f.meta.name,
|
|
97
|
+
category: f.meta.category
|
|
98
|
+
})),
|
|
99
|
+
count: results.length
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (verbosity === "full") {
|
|
103
|
+
return { components: results, count: results.length };
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
components: results.map((f) => ({
|
|
107
|
+
name: f.meta.name,
|
|
108
|
+
description: f.meta.description,
|
|
109
|
+
category: f.meta.category,
|
|
110
|
+
tags: f.meta.tags,
|
|
111
|
+
status: f.meta.status,
|
|
112
|
+
usage: f.usage
|
|
113
|
+
})),
|
|
114
|
+
count: results.length
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function handleInspect(data, args, _index) {
|
|
118
|
+
const componentName = truncStr(args.component);
|
|
119
|
+
if (!componentName) {
|
|
120
|
+
return { error: true, message: "component parameter is required" };
|
|
121
|
+
}
|
|
122
|
+
const lower = componentName.toLowerCase();
|
|
123
|
+
const fragment = Object.values(data.fragments).find(
|
|
124
|
+
(f) => f.meta.name.toLowerCase() === lower
|
|
125
|
+
);
|
|
126
|
+
if (!fragment) {
|
|
127
|
+
return { error: true, message: `Component "${componentName}" not found` };
|
|
128
|
+
}
|
|
129
|
+
const fields = Array.isArray(args.fields) ? args.fields.map((f) => truncStr(f)) : null;
|
|
130
|
+
if (!fields) {
|
|
131
|
+
const variant = truncStr(args.variant);
|
|
132
|
+
if (variant) {
|
|
133
|
+
const lower2 = variant.toLowerCase();
|
|
134
|
+
return {
|
|
135
|
+
...fragment,
|
|
136
|
+
variants: fragment.variants.filter(
|
|
137
|
+
(v) => v.name.toLowerCase() === lower2
|
|
138
|
+
)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return fragment;
|
|
142
|
+
}
|
|
143
|
+
const result = {};
|
|
144
|
+
for (const field of fields) {
|
|
145
|
+
const normalized = field === "usage" ? "usage" : field;
|
|
146
|
+
const parts = normalized.split(".");
|
|
147
|
+
let value = fragment;
|
|
148
|
+
for (const part of parts) {
|
|
149
|
+
if (value && typeof value === "object") {
|
|
150
|
+
value = value[part];
|
|
151
|
+
} else {
|
|
152
|
+
value = void 0;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (value !== void 0) {
|
|
157
|
+
result[field] = value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
function handleBlocks(data, args, index) {
|
|
163
|
+
if (!data.blocks) {
|
|
164
|
+
return { blocks: [], count: 0 };
|
|
165
|
+
}
|
|
166
|
+
const limit = clampLimit(args.limit, 50);
|
|
167
|
+
const search = truncStr(args.search);
|
|
168
|
+
const category = truncStr(args.category);
|
|
169
|
+
const component = truncStr(args.component);
|
|
170
|
+
const name = truncStr(args.name);
|
|
171
|
+
const verbosity = validateEnum(args.verbosity, [
|
|
172
|
+
"compact",
|
|
173
|
+
"standard",
|
|
174
|
+
"full"
|
|
175
|
+
]);
|
|
176
|
+
let results = Object.values(data.blocks);
|
|
177
|
+
if (name) {
|
|
178
|
+
const lower = name.toLowerCase();
|
|
179
|
+
results = results.filter((b) => b.name.toLowerCase() === lower);
|
|
180
|
+
return { blocks: results, count: results.length };
|
|
181
|
+
}
|
|
182
|
+
if (category) {
|
|
183
|
+
const lower = category.toLowerCase();
|
|
184
|
+
results = results.filter((b) => b.category.toLowerCase() === lower);
|
|
185
|
+
}
|
|
186
|
+
if (component) {
|
|
187
|
+
const lower = component.toLowerCase();
|
|
188
|
+
results = results.filter(
|
|
189
|
+
(b) => b.components.some((c) => c.toLowerCase() === lower)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
if (search) {
|
|
193
|
+
const lower = search.toLowerCase();
|
|
194
|
+
results = results.filter((b) => {
|
|
195
|
+
const searchable = index.blocks.get(b.name) ?? "";
|
|
196
|
+
return searchable.includes(lower);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
results = results.slice(0, limit);
|
|
200
|
+
if (verbosity === "compact") {
|
|
201
|
+
return {
|
|
202
|
+
blocks: results.map((b) => ({
|
|
203
|
+
name: b.name,
|
|
204
|
+
description: b.description,
|
|
205
|
+
category: b.category,
|
|
206
|
+
components: b.components
|
|
207
|
+
})),
|
|
208
|
+
count: results.length
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return { blocks: results, count: results.length };
|
|
212
|
+
}
|
|
213
|
+
function handleTokens(data, args, _index) {
|
|
214
|
+
if (!data.tokens) {
|
|
215
|
+
return { tokens: [], category: void 0 };
|
|
216
|
+
}
|
|
217
|
+
const limit = clampLimit(args.limit, 25);
|
|
218
|
+
const category = truncStr(args.category);
|
|
219
|
+
const search = truncStr(args.search);
|
|
220
|
+
if (category) {
|
|
221
|
+
const lower = category.toLowerCase();
|
|
222
|
+
const matchingCategory = Object.keys(data.tokens.categories).find(
|
|
223
|
+
(c) => c.toLowerCase() === lower
|
|
224
|
+
);
|
|
225
|
+
if (!matchingCategory) {
|
|
226
|
+
return { tokens: [], category };
|
|
227
|
+
}
|
|
228
|
+
let tokens = data.tokens.categories[matchingCategory];
|
|
229
|
+
if (search) {
|
|
230
|
+
const searchLower = search.toLowerCase();
|
|
231
|
+
tokens = tokens.filter((t) => t.name.toLowerCase().includes(searchLower));
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
tokens: tokens.slice(0, limit),
|
|
235
|
+
category: matchingCategory
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (search) {
|
|
239
|
+
const lower = search.toLowerCase();
|
|
240
|
+
const tokens = [];
|
|
241
|
+
for (const entries of Object.values(data.tokens.categories)) {
|
|
242
|
+
for (const token of entries) {
|
|
243
|
+
if (token.name.toLowerCase().includes(lower)) {
|
|
244
|
+
tokens.push(token);
|
|
245
|
+
if (tokens.length >= limit) break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (tokens.length >= limit) break;
|
|
249
|
+
}
|
|
250
|
+
return { tokens };
|
|
251
|
+
}
|
|
252
|
+
const summary = {};
|
|
253
|
+
for (const [cat, entries] of Object.entries(data.tokens.categories)) {
|
|
254
|
+
summary[cat] = entries.length;
|
|
255
|
+
}
|
|
256
|
+
return { prefix: data.tokens.prefix, total: data.tokens.total, categories: summary };
|
|
257
|
+
}
|
|
258
|
+
function handleImplement(data, args, index) {
|
|
259
|
+
const useCase = truncStr(args.useCase);
|
|
260
|
+
const limit = clampLimit(args.limit, 5);
|
|
261
|
+
const verbosity = validateEnum(args.verbosity, [
|
|
262
|
+
"compact",
|
|
263
|
+
"standard",
|
|
264
|
+
"full"
|
|
265
|
+
]);
|
|
266
|
+
if (!useCase) {
|
|
267
|
+
return { error: true, message: "useCase parameter is required" };
|
|
268
|
+
}
|
|
269
|
+
const discoverResult = handleDiscover(
|
|
270
|
+
data,
|
|
271
|
+
{ search: useCase, limit, verbosity: "full" },
|
|
272
|
+
index
|
|
273
|
+
);
|
|
274
|
+
const components = discoverResult.count > 0 ? discoverResult.components : Object.values(data.fragments).slice(0, limit);
|
|
275
|
+
const componentNames = new Set(components.map((c) => c.meta.name));
|
|
276
|
+
let relevantBlocks = [];
|
|
277
|
+
if (data.blocks) {
|
|
278
|
+
relevantBlocks = Object.values(data.blocks).filter(
|
|
279
|
+
(b) => b.components.some((c) => componentNames.has(c))
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (relevantBlocks.length === 0 && data.blocks) {
|
|
283
|
+
const blocksResult = handleBlocks(
|
|
284
|
+
data,
|
|
285
|
+
{ search: useCase, limit: 3 },
|
|
286
|
+
index
|
|
287
|
+
);
|
|
288
|
+
relevantBlocks = blocksResult.blocks;
|
|
289
|
+
}
|
|
290
|
+
let relevantTokens = [];
|
|
291
|
+
if (data.tokens) {
|
|
292
|
+
const words = useCase.toLowerCase().split(/\s+/);
|
|
293
|
+
for (const word of words) {
|
|
294
|
+
if (word.length < 3) continue;
|
|
295
|
+
const result = handleTokens(
|
|
296
|
+
data,
|
|
297
|
+
{ search: word, limit: 5 },
|
|
298
|
+
index
|
|
299
|
+
);
|
|
300
|
+
if (result.tokens) {
|
|
301
|
+
relevantTokens.push(...result.tokens);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const seen = /* @__PURE__ */ new Set();
|
|
305
|
+
relevantTokens = relevantTokens.filter((t) => {
|
|
306
|
+
if (seen.has(t.name)) return false;
|
|
307
|
+
seen.add(t.name);
|
|
308
|
+
return true;
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if (verbosity === "compact") {
|
|
312
|
+
return {
|
|
313
|
+
components: components.map((c) => ({
|
|
314
|
+
name: c.meta.name,
|
|
315
|
+
category: c.meta.category
|
|
316
|
+
})),
|
|
317
|
+
blocks: relevantBlocks.map((b) => ({
|
|
318
|
+
name: b.name,
|
|
319
|
+
category: b.category
|
|
320
|
+
})),
|
|
321
|
+
tokens: relevantTokens.map((t) => t.name)
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
components,
|
|
326
|
+
blocks: relevantBlocks,
|
|
327
|
+
tokens: relevantTokens
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
var cachedEngine = null;
|
|
331
|
+
function getEngine(data) {
|
|
332
|
+
const key = data.generatedAt;
|
|
333
|
+
if (cachedEngine && cachedEngine.key === key) return cachedEngine.engine;
|
|
334
|
+
const graph = deserializeGraph(data.graph);
|
|
335
|
+
const engine = new ComponentGraphEngine(graph, data.blocks);
|
|
336
|
+
cachedEngine = { engine, key };
|
|
337
|
+
return engine;
|
|
338
|
+
}
|
|
339
|
+
function handleGraph(data, args, _index) {
|
|
340
|
+
if (!data.graph) {
|
|
341
|
+
return { error: true, message: "No graph data available" };
|
|
342
|
+
}
|
|
343
|
+
const mode = validateEnum(args.mode, [
|
|
344
|
+
"dependencies",
|
|
345
|
+
"dependents",
|
|
346
|
+
"alternatives",
|
|
347
|
+
"health",
|
|
348
|
+
"impact",
|
|
349
|
+
"path",
|
|
350
|
+
"composition",
|
|
351
|
+
"islands"
|
|
352
|
+
]);
|
|
353
|
+
if (!mode) {
|
|
354
|
+
return { error: true, message: "mode parameter is required" };
|
|
355
|
+
}
|
|
356
|
+
const component = truncStr(args.component);
|
|
357
|
+
const graph = data.graph;
|
|
358
|
+
switch (mode) {
|
|
359
|
+
case "health": {
|
|
360
|
+
const engine = getEngine(data);
|
|
361
|
+
return engine.getHealth();
|
|
362
|
+
}
|
|
363
|
+
case "dependencies": {
|
|
364
|
+
if (!component) {
|
|
365
|
+
return { error: true, message: "component parameter is required for dependencies mode" };
|
|
366
|
+
}
|
|
367
|
+
const edges = graph.edges.filter(
|
|
368
|
+
(e) => e.s.toLowerCase() === component.toLowerCase()
|
|
369
|
+
);
|
|
370
|
+
return {
|
|
371
|
+
component,
|
|
372
|
+
dependencies: edges.map((e) => ({
|
|
373
|
+
target: e.t,
|
|
374
|
+
type: e.ty
|
|
375
|
+
}))
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
case "dependents": {
|
|
379
|
+
if (!component) {
|
|
380
|
+
return { error: true, message: "component parameter is required for dependents mode" };
|
|
381
|
+
}
|
|
382
|
+
const edges = graph.edges.filter(
|
|
383
|
+
(e) => e.t.toLowerCase() === component.toLowerCase()
|
|
384
|
+
);
|
|
385
|
+
return {
|
|
386
|
+
component,
|
|
387
|
+
dependents: edges.map((e) => ({
|
|
388
|
+
source: e.s,
|
|
389
|
+
type: e.ty
|
|
390
|
+
}))
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
case "alternatives": {
|
|
394
|
+
if (!component) {
|
|
395
|
+
return { error: true, message: "component parameter is required for alternatives mode" };
|
|
396
|
+
}
|
|
397
|
+
const altEdges = graph.edges.filter(
|
|
398
|
+
(e) => e.ty === "alternative-to" && (e.s.toLowerCase() === component.toLowerCase() || e.t.toLowerCase() === component.toLowerCase())
|
|
399
|
+
);
|
|
400
|
+
const alternatives = altEdges.map(
|
|
401
|
+
(e) => e.s.toLowerCase() === component.toLowerCase() ? e.t : e.s
|
|
402
|
+
);
|
|
403
|
+
return { component, alternatives };
|
|
404
|
+
}
|
|
405
|
+
case "impact": {
|
|
406
|
+
if (!component) {
|
|
407
|
+
return { error: true, message: "component parameter is required for impact mode" };
|
|
408
|
+
}
|
|
409
|
+
const engine = getEngine(data);
|
|
410
|
+
const maxDepth = typeof args.maxDepth === "number" ? Math.min(args.maxDepth, 10) : 3;
|
|
411
|
+
return engine.impact(component, maxDepth);
|
|
412
|
+
}
|
|
413
|
+
case "path": {
|
|
414
|
+
if (!component) {
|
|
415
|
+
return { error: true, message: "component parameter is required for path mode" };
|
|
416
|
+
}
|
|
417
|
+
const target = truncStr(args.target);
|
|
418
|
+
if (!target) {
|
|
419
|
+
return { error: true, message: "target parameter is required for path mode" };
|
|
420
|
+
}
|
|
421
|
+
const engine = getEngine(data);
|
|
422
|
+
return engine.path(component, target);
|
|
423
|
+
}
|
|
424
|
+
case "composition": {
|
|
425
|
+
if (!component) {
|
|
426
|
+
return { error: true, message: "component parameter is required for composition mode" };
|
|
427
|
+
}
|
|
428
|
+
const engine = getEngine(data);
|
|
429
|
+
return engine.composition(component);
|
|
430
|
+
}
|
|
431
|
+
case "islands": {
|
|
432
|
+
const engine = getEngine(data);
|
|
433
|
+
return { islands: engine.islands() };
|
|
434
|
+
}
|
|
435
|
+
default:
|
|
436
|
+
return { error: true, message: `Unknown mode: ${mode}` };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
var HANDLERS = {
|
|
440
|
+
discover: handleDiscover,
|
|
441
|
+
inspect: handleInspect,
|
|
442
|
+
blocks: handleBlocks,
|
|
443
|
+
tokens: handleTokens,
|
|
444
|
+
implement: handleImplement,
|
|
445
|
+
graph: handleGraph
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// src/fragments/create-tools.ts
|
|
449
|
+
var WEBMCP_TOOL_KEYS = [
|
|
450
|
+
"discover",
|
|
451
|
+
"inspect",
|
|
452
|
+
"blocks",
|
|
453
|
+
"tokens",
|
|
454
|
+
"implement",
|
|
455
|
+
"graph"
|
|
456
|
+
];
|
|
457
|
+
function createFragmentsWebMCPTools(data, options) {
|
|
458
|
+
const prefix = options?.prefix ?? "fragments";
|
|
459
|
+
const toolNames = buildToolNames(prefix);
|
|
460
|
+
const mcpSchemas = buildMcpTools(prefix);
|
|
461
|
+
const searchIndex = buildSearchIndex(data);
|
|
462
|
+
const enabledKeys = options?.tools ? WEBMCP_TOOL_KEYS.filter((key) => options.tools.includes(key)) : WEBMCP_TOOL_KEYS;
|
|
463
|
+
return enabledKeys.filter((key) => key !== "graph" || data.graph != null).map((key) => {
|
|
464
|
+
const schema = mcpSchemas.find((s) => s.name === toolNames[key]);
|
|
465
|
+
const handler = HANDLERS[key];
|
|
466
|
+
return {
|
|
467
|
+
name: schema.name,
|
|
468
|
+
description: schema.description,
|
|
469
|
+
inputSchema: schema.inputSchema,
|
|
470
|
+
annotations: { readOnlyHint: true },
|
|
471
|
+
execute: async (input, _client) => {
|
|
472
|
+
try {
|
|
473
|
+
return handler(data, input, searchIndex);
|
|
474
|
+
} catch (err) {
|
|
475
|
+
return {
|
|
476
|
+
error: true,
|
|
477
|
+
message: err instanceof Error ? err.message : String(err)
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/fragments/use-fragment-tools.ts
|
|
486
|
+
import { useMemo } from "react";
|
|
487
|
+
import { useWebMCPTools } from "@webmcp-sdk/react";
|
|
488
|
+
function useFragmentTools(data, options) {
|
|
489
|
+
const prefix = options?.prefix ?? "fragments";
|
|
490
|
+
const enabled = options?.enabled ?? true;
|
|
491
|
+
const toolsFilter = options?.tools;
|
|
492
|
+
const toolsKey = toolsFilter ? JSON.stringify(toolsFilter) : "";
|
|
493
|
+
const tools = useMemo(() => {
|
|
494
|
+
if (!data) return [];
|
|
495
|
+
return createFragmentsWebMCPTools(data, { prefix, tools: toolsFilter });
|
|
496
|
+
}, [data, prefix, toolsKey]);
|
|
497
|
+
useWebMCPTools(tools, { enabled });
|
|
498
|
+
return { tools, count: tools.length };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/fragments/use-compiled.ts
|
|
502
|
+
import { useState, useEffect } from "react";
|
|
503
|
+
function useCompiledFragmentsFromUrl(url) {
|
|
504
|
+
const [data, setData] = useState(null);
|
|
505
|
+
const [loading, setLoading] = useState(true);
|
|
506
|
+
const [error, setError] = useState(null);
|
|
507
|
+
useEffect(() => {
|
|
508
|
+
let cancelled = false;
|
|
509
|
+
setLoading(true);
|
|
510
|
+
setError(null);
|
|
511
|
+
fetch(url).then((res) => {
|
|
512
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
513
|
+
return res.json();
|
|
514
|
+
}).then((json) => {
|
|
515
|
+
if (!cancelled) {
|
|
516
|
+
setData(json);
|
|
517
|
+
setLoading(false);
|
|
518
|
+
}
|
|
519
|
+
}).catch((err) => {
|
|
520
|
+
if (!cancelled) {
|
|
521
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
522
|
+
setLoading(false);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
return () => {
|
|
526
|
+
cancelled = true;
|
|
527
|
+
};
|
|
528
|
+
}, [url]);
|
|
529
|
+
return { data, loading, error };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/fragments/FragmentsWebMCP.tsx
|
|
533
|
+
import { WebMCPProvider } from "@webmcp-sdk/react";
|
|
534
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
535
|
+
function FragmentsToolsRegistrar({
|
|
536
|
+
data,
|
|
537
|
+
prefix,
|
|
538
|
+
tools
|
|
539
|
+
}) {
|
|
540
|
+
useFragmentTools(data, { prefix, tools });
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
function FragmentsWebMCPWithUrl({
|
|
544
|
+
url,
|
|
545
|
+
prefix,
|
|
546
|
+
tools,
|
|
547
|
+
children
|
|
548
|
+
}) {
|
|
549
|
+
const { data } = useCompiledFragmentsFromUrl(url);
|
|
550
|
+
return /* @__PURE__ */ jsxs(WebMCPProvider, { devShim: true, children: [
|
|
551
|
+
/* @__PURE__ */ jsx(FragmentsToolsRegistrar, { data, prefix, tools }),
|
|
552
|
+
children
|
|
553
|
+
] });
|
|
554
|
+
}
|
|
555
|
+
function FragmentsWebMCPWithData({
|
|
556
|
+
data,
|
|
557
|
+
prefix,
|
|
558
|
+
tools,
|
|
559
|
+
children
|
|
560
|
+
}) {
|
|
561
|
+
return /* @__PURE__ */ jsxs(WebMCPProvider, { devShim: true, children: [
|
|
562
|
+
/* @__PURE__ */ jsx(FragmentsToolsRegistrar, { data, prefix, tools }),
|
|
563
|
+
children
|
|
564
|
+
] });
|
|
565
|
+
}
|
|
566
|
+
function FragmentsWebMCP(props) {
|
|
567
|
+
if ("url" in props && props.url) {
|
|
568
|
+
return /* @__PURE__ */ jsx(FragmentsWebMCPWithUrl, { ...props });
|
|
569
|
+
}
|
|
570
|
+
return /* @__PURE__ */ jsx(FragmentsWebMCPWithData, { ...props });
|
|
571
|
+
}
|
|
572
|
+
export {
|
|
573
|
+
FragmentsWebMCP,
|
|
574
|
+
HANDLERS,
|
|
575
|
+
buildSearchIndex,
|
|
576
|
+
createFragmentsWebMCPTools,
|
|
577
|
+
useCompiledFragmentsFromUrl,
|
|
578
|
+
useFragmentTools
|
|
579
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { DefineToolConfig, InferInput, InputSchema, JSONSchemaInput, ModelContextAPI, ModelContextClient, PropertyDef, SPEC_URL, SPEC_VERSION, ShimRegistry, ToolAnnotations, ToolCallEvent, ToolExecuteCallback, WebMCPTool, defineTool, getExperimentalModelContext, getModelContext, installWebMCPShim, isShimInstalled, isWebMCPSupported, requireModelContext, uninstallWebMCPShim } from '@webmcp-sdk/core';
|
|
2
|
+
import '@webmcp-sdk/react';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
SPEC_VERSION,
|
|
4
|
+
SPEC_URL,
|
|
5
|
+
isWebMCPSupported,
|
|
6
|
+
getModelContext,
|
|
7
|
+
getExperimentalModelContext,
|
|
8
|
+
requireModelContext,
|
|
9
|
+
installWebMCPShim,
|
|
10
|
+
uninstallWebMCPShim,
|
|
11
|
+
isShimInstalled,
|
|
12
|
+
defineTool
|
|
13
|
+
} from "@webmcp-sdk/core";
|
|
14
|
+
export {
|
|
15
|
+
SPEC_URL,
|
|
16
|
+
SPEC_VERSION,
|
|
17
|
+
defineTool,
|
|
18
|
+
getExperimentalModelContext,
|
|
19
|
+
getModelContext,
|
|
20
|
+
installWebMCPShim,
|
|
21
|
+
isShimInstalled,
|
|
22
|
+
isWebMCPSupported,
|
|
23
|
+
requireModelContext,
|
|
24
|
+
uninstallWebMCPShim
|
|
25
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UseWebMCPToolOptions, UseWebMCPToolsOptions, WebMCPContextValue, WebMCPProvider, WebMCPProviderProps, useWebMCPContext, useWebMCPStatus, useWebMCPTool, useWebMCPTools } from '@webmcp-sdk/react';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/react/index.ts
|
|
2
|
+
import {
|
|
3
|
+
WebMCPProvider,
|
|
4
|
+
useWebMCPContext,
|
|
5
|
+
useWebMCPTool,
|
|
6
|
+
useWebMCPTools,
|
|
7
|
+
useWebMCPStatus
|
|
8
|
+
} from "@webmcp-sdk/react";
|
|
9
|
+
export {
|
|
10
|
+
WebMCPProvider,
|
|
11
|
+
useWebMCPContext,
|
|
12
|
+
useWebMCPStatus,
|
|
13
|
+
useWebMCPTool,
|
|
14
|
+
useWebMCPTools
|
|
15
|
+
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fragments-sdk/webmcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"license": "FSL-1.1-MIT",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Fragments design system WebMCP integration — exposes component intelligence to AI agents via @webmcp-sdk",
|
|
6
6
|
"author": "Conan McNicholl",
|
|
7
7
|
"homepage": "https://usefragments.com",
|
|
8
8
|
"repository": {
|
|
@@ -39,36 +39,29 @@
|
|
|
39
39
|
"files": [
|
|
40
40
|
"dist"
|
|
41
41
|
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@webmcp-sdk/core": "^0.1.0",
|
|
44
|
+
"@webmcp-sdk/react": "^0.1.0"
|
|
45
|
+
},
|
|
42
46
|
"peerDependencies": {
|
|
43
47
|
"react": ">=18",
|
|
44
|
-
"@fragments-sdk/context": ">=0.
|
|
45
|
-
},
|
|
46
|
-
"peerDependenciesMeta": {
|
|
47
|
-
"react": {
|
|
48
|
-
"optional": true
|
|
49
|
-
},
|
|
50
|
-
"@fragments-sdk/context": {
|
|
51
|
-
"optional": true
|
|
52
|
-
}
|
|
48
|
+
"@fragments-sdk/context": ">=0.4.0"
|
|
53
49
|
},
|
|
54
50
|
"devDependencies": {
|
|
55
51
|
"@testing-library/react": "^14.0.0",
|
|
56
52
|
"@types/react": "^18.3.0",
|
|
57
53
|
"react": "^18.3.0",
|
|
58
54
|
"react-dom": "^18.3.0",
|
|
59
|
-
"tsx": "^4.21.0",
|
|
60
55
|
"tsup": "^8.3.5",
|
|
61
56
|
"typescript": "^5.7.2",
|
|
62
57
|
"vitest": "^2.1.8",
|
|
63
|
-
"@fragments-sdk/context": "0.
|
|
58
|
+
"@fragments-sdk/context": "0.4.0"
|
|
64
59
|
},
|
|
65
60
|
"scripts": {
|
|
66
61
|
"build": "tsup",
|
|
67
62
|
"dev": "tsup --watch",
|
|
68
63
|
"test": "vitest run",
|
|
69
64
|
"typecheck": "tsc --noEmit",
|
|
70
|
-
"clean": "rm -rf dist"
|
|
71
|
-
"spec:check": "tsx scripts/check-spec-drift.ts",
|
|
72
|
-
"spec:update": "tsx scripts/check-spec-drift.ts --update"
|
|
65
|
+
"clean": "rm -rf dist"
|
|
73
66
|
}
|
|
74
67
|
}
|