@sebgroup/green-core 2.33.0 → 2.34.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.
@@ -0,0 +1,356 @@
1
+ import "../../chunks/chunk.QU3DSPNU.js";
2
+ import { capitalize } from "../../utils/helpers/casing.js";
3
+ import { DOC_TYPES, PATHS, SEARCH_CONFIG, URI_SCHEME } from "./constants.js";
4
+ import { NotFoundError } from "./errors.js";
5
+ import { parseSearchQuery, performSearch } from "./search.js";
6
+ import {
7
+ buildResourceUri,
8
+ findComponent,
9
+ findIcon,
10
+ loadComponentsIndex,
11
+ loadGlobalIndex,
12
+ loadIconsIndex,
13
+ parseResourceUri,
14
+ readMcpFile
15
+ } from "./utils.js";
16
+ import {
17
+ validateGetComponentDocsInput,
18
+ validateGetGuideInput,
19
+ validateListGuidesInput,
20
+ validateSearchComponentsInput
21
+ } from "./validation.js";
22
+ async function handleSearchComponents(input) {
23
+ const validatedInput = validateSearchComponentsInput(input);
24
+ const {
25
+ query,
26
+ category = "all",
27
+ splitTerms = true,
28
+ matchAll = false,
29
+ useRegex = false,
30
+ maxResults = SEARCH_CONFIG.DEFAULT_MAX_RESULTS
31
+ } = validatedInput;
32
+ const loadComponents = category === "component" || category === "all";
33
+ const loadIcons = category === "icon" || category === "all";
34
+ const [componentsIndex, iconsIndex] = await Promise.all([
35
+ loadComponents ? loadComponentsIndex() : Promise.resolve(null),
36
+ loadIcons ? loadIconsIndex() : Promise.resolve(null)
37
+ ]);
38
+ const components = componentsIndex?.components || [];
39
+ const icons = iconsIndex?.icons || [];
40
+ const { searchTerms, regexPattern } = parseSearchQuery(
41
+ query,
42
+ splitTerms,
43
+ useRegex
44
+ );
45
+ const buildUris = (item, cat) => {
46
+ const shortName = item.tagName.replace(/^gds-/, "");
47
+ const resourceCategory = cat === "component" ? "components" : "icons";
48
+ const uris = {};
49
+ for (const docType of item.files) {
50
+ uris[docType] = buildResourceUri(resourceCategory, shortName, docType);
51
+ }
52
+ return uris;
53
+ };
54
+ const results = performSearch(
55
+ components,
56
+ icons,
57
+ query,
58
+ searchTerms,
59
+ regexPattern,
60
+ matchAll,
61
+ splitTerms,
62
+ maxResults,
63
+ buildUris
64
+ );
65
+ return {
66
+ content: [
67
+ {
68
+ type: "text",
69
+ text: JSON.stringify(
70
+ {
71
+ query,
72
+ resultCount: results.length,
73
+ results
74
+ },
75
+ null,
76
+ 2
77
+ )
78
+ }
79
+ ]
80
+ };
81
+ }
82
+ async function handleGetComponentDocs(input) {
83
+ const validatedInput = validateGetComponentDocsInput(input);
84
+ const {
85
+ componentName,
86
+ framework,
87
+ includeGuidelines = true,
88
+ includeInstructions = true
89
+ } = validatedInput;
90
+ const [componentsIndex, iconsIndex] = await Promise.all([
91
+ loadComponentsIndex(),
92
+ loadIconsIndex()
93
+ ]);
94
+ if (!componentsIndex || !iconsIndex) {
95
+ throw new NotFoundError(
96
+ "Failed to load component indexes",
97
+ "index",
98
+ "components/icons"
99
+ );
100
+ }
101
+ const component = findComponent(componentName, componentsIndex.components);
102
+ const icon = component ? null : findIcon(componentName, iconsIndex.icons);
103
+ const found = component || icon;
104
+ if (!found) {
105
+ throw new NotFoundError(
106
+ `Component not found: ${componentName}. Try using the search_components tool to find available components.`,
107
+ "component",
108
+ componentName
109
+ );
110
+ }
111
+ const shortName = found.tagName.replace(/^gds-/, "");
112
+ const sections = [];
113
+ let primaryDoc;
114
+ if (framework === "angular") {
115
+ primaryDoc = DOC_TYPES.ANGULAR;
116
+ } else if (framework === "react") {
117
+ primaryDoc = DOC_TYPES.REACT;
118
+ } else {
119
+ primaryDoc = DOC_TYPES.API;
120
+ }
121
+ sections.push(`# ${found.tagName} - ${capitalize(framework)}`);
122
+ sections.push("");
123
+ if (framework === "angular" || framework === "react") {
124
+ sections.push(`\u26A0\uFE0F **${capitalize(framework)}-Specific Documentation**`);
125
+ sections.push(
126
+ `The import paths and syntax below are for ${capitalize(framework)} applications.`
127
+ );
128
+ sections.push("");
129
+ }
130
+ if (found.files.includes(primaryDoc)) {
131
+ const content = await readMcpFile(`${shortName}/${primaryDoc}.md`);
132
+ if (content) {
133
+ const contentWithoutTitle = content.replace(/^#\s+.*?\n/, "");
134
+ sections.push(contentWithoutTitle);
135
+ sections.push("");
136
+ }
137
+ }
138
+ if ((framework === "angular" || framework === "react") && found.files.includes(DOC_TYPES.API)) {
139
+ const apiContent = await readMcpFile(`${shortName}/${DOC_TYPES.API}.md`);
140
+ if (apiContent) {
141
+ sections.push("---");
142
+ sections.push("");
143
+ sections.push("## Component API Reference");
144
+ sections.push("");
145
+ sections.push(
146
+ "The following properties, events, slots, and methods are available:"
147
+ );
148
+ sections.push("");
149
+ const apiWithoutHeader = apiContent.replace(/^#\s+.*?\n/, "").replace(/\*\*Class\*\*:.*?\n/, "").replace(/\*\*Tag\*\*:.*?\n/, "").trim();
150
+ sections.push(apiWithoutHeader);
151
+ sections.push("");
152
+ }
153
+ }
154
+ if (includeGuidelines && found.files.includes(DOC_TYPES.GUIDELINES)) {
155
+ const guidelines = await readMcpFile(
156
+ `${shortName}/${DOC_TYPES.GUIDELINES}.md`
157
+ );
158
+ if (guidelines) {
159
+ sections.push("---");
160
+ sections.push("");
161
+ sections.push("## Design Guidelines");
162
+ sections.push("");
163
+ sections.push(guidelines);
164
+ sections.push("");
165
+ }
166
+ }
167
+ if (includeInstructions && found.files.includes(DOC_TYPES.INSTRUCTIONS)) {
168
+ const instructions = await readMcpFile(
169
+ `${shortName}/${DOC_TYPES.INSTRUCTIONS}.md`
170
+ );
171
+ if (instructions) {
172
+ sections.push("---");
173
+ sections.push("");
174
+ sections.push("## Usage Instructions");
175
+ sections.push("");
176
+ sections.push(instructions);
177
+ sections.push("");
178
+ }
179
+ }
180
+ sections.push("---");
181
+ sections.push("");
182
+ sections.push("\u{1F4A1} **Using a different framework?**");
183
+ sections.push("Call this tool again with:");
184
+ if (framework !== "angular")
185
+ sections.push('- `framework: "angular"` for Angular documentation');
186
+ if (framework !== "react")
187
+ sections.push('- `framework: "react"` for React documentation');
188
+ if (framework !== "web-component")
189
+ sections.push('- `framework: "web-component"` for vanilla JS usage');
190
+ return {
191
+ content: [
192
+ {
193
+ type: "text",
194
+ text: sections.join("\n")
195
+ }
196
+ ]
197
+ };
198
+ }
199
+ async function handleListGuides(input) {
200
+ const validatedInput = validateListGuidesInput(input);
201
+ const { category = "all", framework } = validatedInput;
202
+ const globalIndex = await loadGlobalIndex();
203
+ if (!globalIndex) {
204
+ throw new NotFoundError("Failed to load global index", "index", "global");
205
+ }
206
+ let guides = globalIndex.guides;
207
+ if (category !== "all") {
208
+ guides = guides.filter((g) => g.category === category);
209
+ }
210
+ if (framework && framework !== "all") {
211
+ guides = guides.filter((g) => g.tags.includes(framework));
212
+ }
213
+ const guidesWithUris = guides.map((guide) => {
214
+ const name = guide.path.replace(/^(guides|concepts)\//, "").replace(/\.md$/, "");
215
+ const guideCategory = guide.path.startsWith("guides/") ? "guides" : "concepts";
216
+ const uri = buildResourceUri(guideCategory, name);
217
+ return {
218
+ title: guide.title,
219
+ category: guide.category,
220
+ description: guide.description,
221
+ tags: guide.tags,
222
+ resourceUri: uri
223
+ };
224
+ });
225
+ return {
226
+ content: [
227
+ {
228
+ type: "text",
229
+ text: JSON.stringify(
230
+ {
231
+ guideCount: guidesWithUris.length,
232
+ guides: guidesWithUris
233
+ },
234
+ null,
235
+ 2
236
+ )
237
+ }
238
+ ]
239
+ };
240
+ }
241
+ async function handleGetGuide(input) {
242
+ const validatedInput = validateGetGuideInput(input);
243
+ const { name } = validatedInput;
244
+ const globalIndex = await loadGlobalIndex();
245
+ if (!globalIndex) {
246
+ throw new NotFoundError("Failed to load global index", "index", "global");
247
+ }
248
+ const guide = globalIndex.guides.find((g) => {
249
+ const guideName = g.path.replace(/^(guides|concepts)\//, "").replace(/\.md$/, "");
250
+ return guideName === name;
251
+ });
252
+ if (!guide) {
253
+ throw new NotFoundError(
254
+ `Guide not found: ${name}. Use list_guides to see available guides.`,
255
+ "guide",
256
+ name
257
+ );
258
+ }
259
+ const content = await readMcpFile(guide.path);
260
+ if (!content) {
261
+ throw new NotFoundError(
262
+ `Guide file not found: ${guide.path}`,
263
+ "file",
264
+ guide.path
265
+ );
266
+ }
267
+ return {
268
+ content: [
269
+ {
270
+ type: "text",
271
+ text: `# ${guide.title}
272
+
273
+ ${content}`
274
+ }
275
+ ]
276
+ };
277
+ }
278
+ async function handleGetInstructions() {
279
+ const globalIndex = await loadGlobalIndex();
280
+ if (!globalIndex) {
281
+ throw new NotFoundError("Failed to load global index", "index", "global");
282
+ }
283
+ if (!globalIndex.instructions) {
284
+ throw new NotFoundError(
285
+ "Instructions not available. The MCP may not have been generated with instructions support.",
286
+ "file",
287
+ "INSTRUCTIONS.md"
288
+ );
289
+ }
290
+ const content = await readMcpFile("INSTRUCTIONS.md");
291
+ if (!content) {
292
+ throw new NotFoundError(
293
+ "Instructions file not found",
294
+ "file",
295
+ "INSTRUCTIONS.md"
296
+ );
297
+ }
298
+ return {
299
+ content: [
300
+ {
301
+ type: "text",
302
+ text: content
303
+ }
304
+ ]
305
+ };
306
+ }
307
+ async function handleResolveUri(uri) {
308
+ if (uri === URI_SCHEME.INSTRUCTIONS) {
309
+ const content2 = await readMcpFile(PATHS.INSTRUCTIONS_FILE);
310
+ if (!content2) {
311
+ throw new NotFoundError(
312
+ "Instructions file not found",
313
+ "file",
314
+ PATHS.INSTRUCTIONS_FILE
315
+ );
316
+ }
317
+ return {
318
+ content: [{ type: "text", text: content2 }]
319
+ };
320
+ }
321
+ const parsed = parseResourceUri(uri);
322
+ if (!parsed) {
323
+ throw new NotFoundError(`Invalid resource URI format: ${uri}`, "uri", uri);
324
+ }
325
+ const { category, name, docType } = parsed;
326
+ let filePath;
327
+ if (category === "components" || category === "icons") {
328
+ if (!docType) {
329
+ throw new NotFoundError(
330
+ `Document type required for ${category} URIs (e.g. green://${category}/${name}/api)`,
331
+ "docType",
332
+ uri
333
+ );
334
+ }
335
+ filePath = `${name}/${docType}.md`;
336
+ } else if (category === "guides" || category === "concepts") {
337
+ filePath = `${category}/${name}.md`;
338
+ } else {
339
+ throw new NotFoundError(`Unknown category: ${category}`, "category", uri);
340
+ }
341
+ const content = await readMcpFile(filePath);
342
+ if (!content) {
343
+ throw new NotFoundError(`Resource not found: ${uri}`, "file", filePath);
344
+ }
345
+ return {
346
+ content: [{ type: "text", text: content }]
347
+ };
348
+ }
349
+ export {
350
+ handleGetComponentDocs,
351
+ handleGetGuide,
352
+ handleGetInstructions,
353
+ handleListGuides,
354
+ handleResolveUri,
355
+ handleSearchComponents
356
+ };
@@ -9,6 +9,12 @@ function compileRegexPattern(pattern) {
9
9
  { maxLength: SEARCH_CONFIG.MAX_REGEX_LENGTH }
10
10
  );
11
11
  }
12
+ if (/([+*?]|\{\d+,?\d*\})\)*([+*?]|\{\d+,?\d*\})/.test(pattern)) {
13
+ throw new RegexError(
14
+ "Regex pattern contains nested quantifiers which could cause catastrophic backtracking",
15
+ pattern
16
+ );
17
+ }
12
18
  try {
13
19
  return new RegExp(pattern, "i");
14
20
  } catch (error) {
@@ -75,7 +81,8 @@ function checkMultiTermMatch(item, searchTerms, matchAll) {
75
81
  }
76
82
  function parseSearchQuery(query, splitTerms, useRegex) {
77
83
  if (useRegex) {
78
- const regexPattern = compileRegexPattern(query);
84
+ const pattern = query.startsWith("/") && query.endsWith("/") ? query.slice(1, -1) : query;
85
+ const regexPattern = compileRegexPattern(pattern);
79
86
  return { searchTerms: [query], regexPattern };
80
87
  }
81
88
  if (splitTerms) {
@@ -1,5 +1,17 @@
1
+ /**
2
+ * MCP tool registration for the Green Design System MCP Server.
3
+ *
4
+ * This module wires MCP protocol schemas to shared handler functions.
5
+ * The actual business logic lives in ./handlers.ts so it can be reused
6
+ * by other consumers (e.g. the context CLI).
7
+ *
8
+ * @module tools
9
+ */
1
10
  import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
11
  /**
3
- * Register tool handlers on the MCP server
12
+ * Register tool handlers on the MCP server.
13
+ *
14
+ * Defines the MCP tool schemas (names, descriptions, input schemas) and
15
+ * delegates each tool call to the corresponding shared handler function.
4
16
  */
5
17
  export declare function setupToolHandlers(server: Server): void;