@monoharada/wcf-mcp 0.10.0 → 0.12.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/README.md CHANGED
@@ -78,13 +78,13 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
78
78
  }
79
79
  ```
80
80
 
81
- ## 提供機能(19 tools + 1 prompt + 5 resources)
81
+ ## 提供機能(19 tools + 2 prompts + 5 resources)
82
82
 
83
83
  ### ガードレール
84
84
 
85
85
  | ツール | 説明 |
86
86
  |--------|------|
87
- | `get_design_system_overview` | 最初に呼ぶ前提情報(カテゴリ別コンポーネント数、利用可能パターン、推奨ワークフロー、IDE設定テンプレート)を返す |
87
+ | `get_design_system_overview` | 最初に呼ぶ前提情報(カテゴリ別コンポーネント数、利用可能パターン、`componentPatternMap`、推奨 preloading / workflow、IDE設定テンプレート)を返す |
88
88
 
89
89
  ### コンポーネント検索・API
90
90
 
@@ -100,7 +100,7 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
100
100
 
101
101
  | ツール | 説明 |
102
102
  |--------|------|
103
- | `validate_markup` | HTML スニペットを検証し、セマンティック検証(下表)で `suggestion` 付き診断を返す |
103
+ | `validate_markup` | HTML スニペットを検証し、セマンティック検証(下表)で `suggestion` 付き診断を返す。severity は `error` / `warning` / `info` |
104
104
  | `validate_files` | 複数のマークアップファイルをまとめて検証し、ファイル別診断と集計を返す |
105
105
  | `validate_project` | ディレクトリを走査し、include/exclude glob に一致する複数ファイルをまとめて検証する |
106
106
 
@@ -126,6 +126,11 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
126
126
  | `emptyAriaLabel` | error | 空の `aria-label` 属性(アクセシビリティ違反) | `<dads-button aria-label="">` |
127
127
  | `duplicateId` | error | 同一ドキュメント内で `id` が重複している | `<div id="hero">...</div><section id="hero">...</section>` |
128
128
  | `forbiddenAttribute` | warning | 禁止属性 | `placeholder` |
129
+ | `sortOnTh` / `sortWrongTarget` / `sortTypeOnWrongElement` | warning | `dads-table` のソート構造誤用 | `th[data-sort]`, `button[data-sort-type]` |
130
+ | `selectionControlWrongElement` | warning | `data-select-row` / `data-select-all` が checkbox 以外 | `<button data-select-row>` |
131
+ | `resourceListWholeLinkMissingInteraction` | warning | `dads-resource-list[href]` に `data-interaction="whole"` がない | `<dads-resource-list href="...">` |
132
+ | `nativePatternReplaceable` | warning / info | 既存 DADS コンポーネントに置換可能な独自パターン | `role="tablist"`, `role="dialog"`, `<dl>` |
133
+ | `customAnimationReplaceable` | warning | スピナー相当の独自 CSS アニメーション | `@keyframes spin` + `animation` |
129
134
 
130
135
  ### UI パターン
131
136
 
@@ -240,13 +245,14 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
240
245
 
241
246
  | 名前 | 説明 |
242
247
  |------|------|
248
+ | `build_page` | パターンID またはコンポーネントリストから no-build HTML ページを構築するガイド付きプロンプト |
243
249
  | `figma_to_wcf` | Figma URL を入力に、`overview → tokens → component api → snippet → validate` の実行順を返す |
244
250
 
245
251
  ### Resources (`wcf://`)
246
252
 
247
253
  | URI | 説明 |
248
254
  |-----|------|
249
- | `wcf://components` | コンポーネントカタログのスナップショット |
255
+ | `wcf://components` | コンポーネントカタログのスナップショット。プロトタイピング前の preload を推奨 |
250
256
  | `wcf://tokens` | トークン summary(type/category/themes/sample) |
251
257
  | `wcf://guidelines/{topic}` | topic 別ガイドライン要約(`accessibility`,`css`,`patterns`,`all`) |
252
258
  | `wcf://llms-full` | `llms-full.txt` の全文 |
package/core/cem.mjs CHANGED
@@ -899,7 +899,7 @@ export function resolveDeclByComponent(indexes, component, prefix) {
899
899
  return undefined;
900
900
  }
901
901
 
902
- export function buildComponentNotFoundError(component, indexes, prefix) {
902
+ export function buildComponentNotFoundError(component, indexes, prefix, installRegistry) {
903
903
  const comp = typeof component === 'string' ? component.trim() : '';
904
904
  const p = normalizePrefix(prefix);
905
905
  const suggestions = [];
@@ -916,8 +916,19 @@ export function buildComponentNotFoundError(component, indexes, prefix) {
916
916
  suggestions.push(suggested);
917
917
  }
918
918
 
919
- const msg = suggestions.length > 0
920
- ? `Component not found: ${comp}. Did you mean: ${suggestions.join(', ')}?`
921
- : `Component not found: ${comp}`;
922
- return { content: [{ type: 'text', text: msg }], isError: true };
919
+ const parts = [];
920
+ if (suggestions.length > 0) {
921
+ parts.push(`Component not found: ${comp}. Did you mean: ${suggestions.join(', ')}?`);
922
+ } else {
923
+ parts.push(`Component not found: ${comp}`);
924
+ }
925
+
926
+ const validIds = installRegistry?.components && typeof installRegistry.components === 'object'
927
+ ? Object.keys(installRegistry.components).sort()
928
+ : [];
929
+ if (validIds.length > 0) {
930
+ parts.push(`\nAvailable component IDs (${validIds.length}): ${validIds.join(', ')}`);
931
+ }
932
+
933
+ return { content: [{ type: 'text', text: parts.join('') }], isError: true };
923
934
  }
@@ -81,6 +81,7 @@ export const CATEGORY_MAP = {
81
81
  };
82
82
 
83
83
  export const FIGMA_TO_WCF_PROMPT = 'figma_to_wcf';
84
+ export const BUILD_PAGE_PROMPT = 'build_page';
84
85
  export const WCF_RESOURCE_URIS = Object.freeze({
85
86
  components: 'wcf://components',
86
87
  tokens: 'wcf://tokens',
@@ -94,6 +95,59 @@ const NPX_TEMPLATE = Object.freeze({
94
95
  args: ['@monoharada/wcf-mcp'],
95
96
  });
96
97
 
98
+ /**
99
+ * Build the server-level instructions string sent to MCP clients on initialization.
100
+ * This enables AI agents to immediately understand what the server offers and how to
101
+ * build pages without any prior knowledge.
102
+ */
103
+ export function buildServerInstructions(prefix, installRegistry, patterns) {
104
+ const components = installRegistry?.components && typeof installRegistry.components === 'object'
105
+ ? installRegistry.components : {};
106
+ const componentIds = Object.keys(components).sort();
107
+ const patternIds = patterns && typeof patterns === 'object'
108
+ ? Object.keys(patterns).sort() : [];
109
+
110
+ return [
111
+ '# wcf-mcp: DADS Web Components Design System MCP Server',
112
+ '',
113
+ '## Capabilities',
114
+ 'This server provides a no-build / no-CDN Web Components design system.',
115
+ 'You can generate complete HTML pages using MCP tools alone — no CLI installation required.',
116
+ '',
117
+ '## Quickest Workflow (build a page)',
118
+ '1. get_design_system_overview()',
119
+ ' → Inspect componentPatternMap before writing custom HTML/CSS',
120
+ '2. Read resource wcf://components',
121
+ ' → Preload the component catalog to avoid re-implementing existing components',
122
+ '3. get_pattern_recipe({ patternId: "<id>", include: ["fullPage"] })',
123
+ ' → Returns a complete <!DOCTYPE html> page in one call',
124
+ '4. validate_markup({ html: "<the full page HTML>" }) → Validate the full page (catches missing importmap / boot script)',
125
+ '5. Save the fullPageHtml result to a file and serve via HTTP',
126
+ '',
127
+ `## Available Pattern IDs (${patternIds.length})`,
128
+ patternIds.length > 0 ? patternIds.join(', ') : '(none)',
129
+ '',
130
+ `## Available Component IDs (${componentIds.length})`,
131
+ componentIds.length > 0 ? componentIds.join(', ') : '(none)',
132
+ '',
133
+ '## Custom Page Construction',
134
+ '1. get_design_system_overview() → review componentPatternMap',
135
+ '2. Read resource wcf://components → preload component catalog context',
136
+ '3. generate_usage_snippet({ component: "<componentId>" }) → Get HTML for each component',
137
+ '4. generate_full_page_html({ html: "<combined fragments>" }) → Wrap into a complete page',
138
+ '5. validate_markup({ html: "<the full page HTML>" }) → Validate the full page (catches missing importmap / boot script)',
139
+ '',
140
+ '## Important Notes',
141
+ '- No CDN exists. All files are served from a local vendor directory.',
142
+ `- CLI setup: npm install web-components-factory → npx wcf init --prefix ${prefix}`,
143
+ '- file:// protocol does not work. An HTTP server is required.',
144
+ '',
145
+ '## Prompts',
146
+ '- build_page: Guided prompt for building a no-build HTML page',
147
+ '- figma_to_wcf: Convert Figma designs to WCF implementation',
148
+ ].join('\n');
149
+ }
150
+
97
151
  export const IDE_SETUP_TEMPLATES = Object.freeze([
98
152
  {
99
153
  ide: 'Claude Desktop',
package/core/plugins.mjs CHANGED
@@ -41,6 +41,7 @@ export const BUILTIN_TOOL_NAMES = Object.freeze(new Set([
41
41
  ]));
42
42
  const BUILTIN_PROMPT_NAMES = Object.freeze(new Set([
43
43
  'figma_to_wcf',
44
+ 'build_page',
44
45
  ]));
45
46
  const BUILTIN_RESOURCE_URIS = Object.freeze(new Set([
46
47
  'wcf://components',
package/core/prefix.mjs CHANGED
@@ -111,7 +111,7 @@ export function buildDiagnosticSuggestion({ diagnostic, cemIndex, prefix }) {
111
111
  }
112
112
 
113
113
  if (code === 'forbiddenAttribute' && String(diagnostic?.attrName ?? '').toLowerCase() === 'placeholder') {
114
- return 'Use aria-label or aria-describedby support text instead of placeholder.';
114
+ return 'Use support-text (attribute or slot), a visible label, and aria-describedby/aria-label where needed instead of placeholder. See: https://design.digital.go.jp/dads/components/input-text/accessibility/';
115
115
  }
116
116
 
117
117
  if (code === 'ariaLiveNotRecommended') {
@@ -130,6 +130,18 @@ export function buildDiagnosticSuggestion({ diagnostic, cemIndex, prefix }) {
130
130
  return diagnostic?.hint ?? 'Provide a meaningful aria-label value or use a visible <label> element.';
131
131
  }
132
132
 
133
+ if (
134
+ code === 'sortOnTh' ||
135
+ code === 'sortWrongTarget' ||
136
+ code === 'sortTypeOnWrongElement' ||
137
+ code === 'selectionControlWrongElement' ||
138
+ code === 'resourceListWholeLinkMissingInteraction' ||
139
+ code === 'nativePatternReplaceable' ||
140
+ code === 'customAnimationReplaceable'
141
+ ) {
142
+ return diagnostic?.hint ?? undefined;
143
+ }
144
+
133
145
  return undefined;
134
146
  }
135
147
 
package/core/register.mjs CHANGED
@@ -6,7 +6,7 @@ import fs from 'node:fs/promises';
6
6
  import path from 'node:path';
7
7
  import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
8
8
  import { z } from 'zod';
9
- import { CANONICAL_PREFIX, PACKAGE_VERSION, PLUGIN_TOOL_NOTICE, FIGMA_TO_WCF_PROMPT, WCF_RESOURCE_URIS, IDE_SETUP_TEMPLATES } from './constants.mjs';
9
+ import { CANONICAL_PREFIX, PACKAGE_VERSION, PLUGIN_TOOL_NOTICE, FIGMA_TO_WCF_PROMPT, BUILD_PAGE_PROMPT, WCF_RESOURCE_URIS, IDE_SETUP_TEMPLATES } from './constants.mjs';
10
10
  import { normalizePrefix, withPrefix, toCanonicalTagName, getCategory, buildDiagnosticSuggestion, applyPrefixToHtml, applyPrefixToTagMap, mergeWithPrefixed } from './prefix.mjs';
11
11
  import { buildJsonToolResponse, buildJsonToolErrorResponse, expandQueryWithSynonyms, finalizeToolResult } from './response.mjs';
12
12
  import { normalizePlugins, buildPluginDataSourceMap, toPassthroughSchema } from './plugins.mjs';
@@ -130,6 +130,77 @@ function buildFigmaToWcfPromptText({ figmaUrl, userIntent }) {
130
130
  ].join('\n');
131
131
  }
132
132
 
133
+ function buildBuildPagePromptText({ patternId, components: componentsCsv, userIntent, detectedPrefix, installRegistry, patterns }) {
134
+ const p = normalizePrefix(detectedPrefix);
135
+ const intent = String(userIntent ?? '').trim();
136
+ const registryComponents = installRegistry?.components && typeof installRegistry.components === 'object'
137
+ ? installRegistry.components : {};
138
+ const componentIds = Object.keys(registryComponents).sort();
139
+ const patternIds = patterns && typeof patterns === 'object'
140
+ ? Object.keys(patterns).sort() : [];
141
+
142
+ const lines = [
143
+ intent ? `Page goal: ${intent}` : 'Page goal: (not specified)',
144
+ '',
145
+ ];
146
+
147
+ // patternId takes priority over components when both are specified
148
+ if (patternId) {
149
+ lines.push(
150
+ '## Using a Pattern',
151
+ `1. get_pattern_recipe({ patternId: "${patternId}", include: ["fullPage"] })`,
152
+ '2. validate_markup({ html: "<the full page HTML>" }) — pass the entire page to catch missing importmap / boot script',
153
+ '3. Save the fullPageHtml to a .html file',
154
+ '',
155
+ );
156
+ if (componentsCsv) {
157
+ lines.push(
158
+ '> Note: patternId was specified, so the components argument is ignored. Remove patternId to use individual components instead.',
159
+ '',
160
+ );
161
+ }
162
+ } else if (componentsCsv) {
163
+ const ids = componentsCsv.split(',').map((s) => s.trim()).filter(Boolean);
164
+ lines.push(
165
+ '## Using Specific Components',
166
+ ...ids.map((id) => `- generate_usage_snippet({ component: "${id}" })`),
167
+ '- Combine the HTML fragments',
168
+ '- generate_full_page_html({ html: "<combined fragments>" }) → returns fullHtml',
169
+ '- validate_markup({ html: "<the full page HTML>" }) — pass the entire page to catch missing importmap / boot script',
170
+ '',
171
+ );
172
+ } else {
173
+ lines.push(
174
+ '## Workflow Options',
175
+ '',
176
+ '### Option A: Use a pattern (recommended)',
177
+ '1. get_pattern_recipe({ patternId: "<id>", include: ["fullPage"] })',
178
+ '2. validate_markup({ html: "<the full page HTML>" }) — pass the entire page to catch missing importmap / boot script',
179
+ '3. Save the fullPageHtml to a .html file',
180
+ '',
181
+ '### Option B: Build from individual components',
182
+ '1. generate_usage_snippet({ component: "<componentId>" }) for each component',
183
+ '2. Combine the HTML fragments',
184
+ '3. generate_full_page_html({ html: "<combined fragments>" }) → returns fullHtml',
185
+ '4. validate_markup({ html: "<the full page HTML>" }) — pass the entire page to catch missing importmap / boot script',
186
+ '',
187
+ );
188
+ }
189
+
190
+ lines.push(
191
+ `## Available Pattern IDs (${patternIds.length})`,
192
+ patternIds.length > 0 ? patternIds.join(', ') : '(none)',
193
+ '',
194
+ `## Available Component IDs (${componentIds.length})`,
195
+ componentIds.length > 0 ? componentIds.join(', ') : '(none)',
196
+ '',
197
+ '## CLI Vendor Setup (alternative)',
198
+ `npx web-components-factory init --prefix ${p} --dir .` + (patternId ? ` --pattern ${patternId}` : ''),
199
+ );
200
+
201
+ return lines.join('\n');
202
+ }
203
+
133
204
  function escapeHtmlTitle(s) {
134
205
  return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
135
206
  }
@@ -178,6 +249,71 @@ function scoreSearchFields(query, terms, fields) {
178
249
  return score;
179
250
  }
180
251
 
252
+ function normalizeSearchText(value) {
253
+ return String(value ?? '').trim().toLowerCase();
254
+ }
255
+
256
+ function normalizeSearchTokens(value) {
257
+ return normalizeSearchText(value)
258
+ .split(/[^a-z0-9\u3040-\u30ff\u3400-\u9fff-]+/u)
259
+ .filter(Boolean);
260
+ }
261
+
262
+ function includesWholeToken(value, query) {
263
+ return normalizeSearchTokens(value).includes(normalizeSearchText(query));
264
+ }
265
+
266
+ function buildSelectorGuideComponentLookup(selectorGuideData) {
267
+ const lookup = new Map();
268
+ const categories = Array.isArray(selectorGuideData?.categories) ? selectorGuideData.categories : [];
269
+ for (const category of categories) {
270
+ const categoryKey = String(category?.key ?? '');
271
+ const components = Array.isArray(category?.components) ? category.components : [];
272
+ for (const component of components) {
273
+ const id = String(component?.id ?? '').trim();
274
+ const tagName = String(component?.tagName ?? '').trim().toLowerCase();
275
+ const useCase = String(component?.useCase ?? '');
276
+ const keywords = Array.isArray(component?.keywords) ? component.keywords.map((keyword) => String(keyword)) : [];
277
+ const entry = { id, tagName, useCase, keywords, categoryKey };
278
+ if (id) lookup.set(`id:${id}`, entry);
279
+ if (tagName) lookup.set(`tag:${tagName}`, entry);
280
+ }
281
+ }
282
+ return lookup;
283
+ }
284
+
285
+ function scoreSelectorGuideComponent(component, query) {
286
+ const q = normalizeSearchText(query);
287
+ if (!q) return 0;
288
+
289
+ const id = String(component?.id ?? '');
290
+ const tagName = String(component?.tagName ?? '').toLowerCase();
291
+ const useCase = String(component?.useCase ?? '');
292
+ const keywords = Array.isArray(component?.keywords) ? component.keywords : [];
293
+
294
+ if (id.toLowerCase() === q) return 120;
295
+ if (tagName === q || tagName === `dads-${q}`) return 115;
296
+ if (keywords.some((keyword) => normalizeSearchText(keyword) === q)) return 110;
297
+ if (includesWholeToken(useCase, q)) return 90;
298
+ if (keywords.some((keyword) => includesWholeToken(keyword, q))) return 85;
299
+ if (tagName.startsWith(q) || id.toLowerCase().startsWith(q)) return 70;
300
+ if (useCase.toLowerCase().includes(q)) return 40;
301
+ if (keywords.some((keyword) => normalizeSearchText(keyword).includes(q))) return 35;
302
+ if (tagName.includes(q) || id.toLowerCase().includes(q)) return 20;
303
+ return 0;
304
+ }
305
+
306
+ function buildOverviewPatternMap(selectorGuideData) {
307
+ const entries = Array.isArray(selectorGuideData?.componentPatternMap) ? selectorGuideData.componentPatternMap : [];
308
+ return entries.map((entry) => ({
309
+ pattern: String(entry?.pattern ?? ''),
310
+ componentIds: Array.isArray(entry?.componentIds) ? entry.componentIds.map((item) => String(item)) : [],
311
+ usage: typeof entry?.usage === 'string' ? entry.usage : undefined,
312
+ note: String(entry?.note ?? ''),
313
+ keywords: Array.isArray(entry?.keywords) ? entry.keywords.map((item) => String(item)) : [],
314
+ })).filter((entry) => entry.pattern && entry.componentIds.length > 0);
315
+ }
316
+
181
317
  function detectKnowledgeIntentSources(query, terms) {
182
318
  const raw = `${query} ${terms.join(' ')}`.toLowerCase();
183
319
  const intents = new Set();
@@ -361,10 +497,12 @@ async function walkProjectFiles(rootDir) {
361
497
  function summarizeDiagnostics(diagnostics) {
362
498
  const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === 'error').length;
363
499
  const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === 'warning').length;
500
+ const infoCount = diagnostics.filter((diagnostic) => diagnostic.severity === 'info').length;
364
501
  return {
365
502
  total: diagnostics.length,
366
503
  errorCount,
367
504
  warningCount,
505
+ infoCount,
368
506
  };
369
507
  }
370
508
 
@@ -549,6 +687,7 @@ export function registerAll(context) {
549
687
  llmsFullText,
550
688
  tokenSuggestionMap,
551
689
  componentTokenRefMap,
690
+ selectorGuideData,
552
691
  plugins,
553
692
  loadJsonData,
554
693
  loadJson,
@@ -586,6 +725,10 @@ export function registerAll(context) {
586
725
  detectNonLowercaseAttributes,
587
726
  detectCdnReferences,
588
727
  detectMissingRuntimeScaffold,
728
+ detectTableAuthoringMisuse = () => [],
729
+ detectResourceListAuthoringMisuse = () => [],
730
+ detectReplaceableNativePatterns = () => [],
731
+ detectReplaceableAnimationPatterns = () => [],
589
732
  } = await loadValidator();
590
733
 
591
734
  const p = normalizePrefix(prefix);
@@ -686,6 +829,32 @@ export function registerAll(context) {
686
829
  severity: 'warning',
687
830
  });
688
831
 
832
+ const tableAuthoringDiagnostics = detectTableAuthoringMisuse({
833
+ filePath,
834
+ text,
835
+ prefix: p,
836
+ severity: 'warning',
837
+ });
838
+
839
+ const resourceListAuthoringDiagnostics = detectResourceListAuthoringMisuse({
840
+ filePath,
841
+ text,
842
+ prefix: p,
843
+ severity: 'warning',
844
+ });
845
+
846
+ const replaceableNativeDiagnostics = detectReplaceableNativePatterns({
847
+ filePath,
848
+ text,
849
+ prefix: p,
850
+ });
851
+
852
+ const replaceableAnimationDiagnostics = detectReplaceableAnimationPatterns({
853
+ filePath,
854
+ text,
855
+ prefix: p,
856
+ });
857
+
689
858
  const allRawDiagnostics = [
690
859
  ...cemDiagnostics,
691
860
  ...enumDiagnostics,
@@ -699,6 +868,10 @@ export function registerAll(context) {
699
868
  ...accessibilityDiagnostics,
700
869
  ...cdnDiagnostics,
701
870
  ...scaffoldDiagnostics,
871
+ ...tableAuthoringDiagnostics,
872
+ ...resourceListAuthoringDiagnostics,
873
+ ...replaceableNativeDiagnostics,
874
+ ...replaceableAnimationDiagnostics,
702
875
  ];
703
876
 
704
877
  for (const plugin of plugins) {
@@ -770,6 +943,32 @@ export function registerAll(context) {
770
943
  }),
771
944
  );
772
945
 
946
+ // -----------------------------------------------------------------------
947
+ // Prompt: build_page
948
+ // -----------------------------------------------------------------------
949
+ server.registerPrompt(
950
+ BUILD_PAGE_PROMPT,
951
+ {
952
+ title: 'Build Page',
953
+ description:
954
+ 'Guided prompt for building a no-build HTML page from a pattern or component list.',
955
+ argsSchema: {
956
+ patternId: z.string().optional().describe('Pattern ID (e.g., "search-results", "card-grid"). Use list_patterns to see all.'),
957
+ components: z.string().optional().describe('Comma-separated component IDs if not using a pattern'),
958
+ userIntent: z.string().optional().describe('What the page should accomplish'),
959
+ },
960
+ },
961
+ async ({ patternId, components: componentsCsv, userIntent }) => ({
962
+ messages: [{
963
+ role: 'user',
964
+ content: {
965
+ type: 'text',
966
+ text: buildBuildPagePromptText({ patternId, components: componentsCsv, userIntent, detectedPrefix, installRegistry, patterns }),
967
+ },
968
+ }],
969
+ }),
970
+ );
971
+
773
972
  // -----------------------------------------------------------------------
774
973
  // Resource: wcf://components
775
974
  // -----------------------------------------------------------------------
@@ -926,6 +1125,7 @@ export function registerAll(context) {
926
1125
  const cat = getCategory(tagName);
927
1126
  categoryCount[cat] = (categoryCount[cat] ?? 0) + 1;
928
1127
  }
1128
+ const componentPatternMap = buildOverviewPatternMap(selectorGuideData);
929
1129
 
930
1130
  const patternList = Object.values(patterns).map((p) => ({
931
1131
  id: p?.id,
@@ -940,6 +1140,7 @@ export function registerAll(context) {
940
1140
  componentsByCategory: categoryCount,
941
1141
  totalPatterns: patternList.length,
942
1142
  patterns: patternList,
1143
+ componentPatternMap,
943
1144
  setupInfo: {
944
1145
  npmPackage: 'web-components-factory',
945
1146
  installCommand: 'npm install web-components-factory',
@@ -987,6 +1188,10 @@ export function registerAll(context) {
987
1188
  name: FIGMA_TO_WCF_PROMPT,
988
1189
  purpose: 'Figma-to-WCF conversion workflow prompt',
989
1190
  },
1191
+ {
1192
+ name: BUILD_PAGE_PROMPT,
1193
+ purpose: 'Build a no-build HTML page from a pattern or component list',
1194
+ },
990
1195
  ],
991
1196
  availableResources: [
992
1197
  { uri: WCF_RESOURCE_URIS.components, purpose: 'Component catalog snapshot' },
@@ -1017,10 +1222,10 @@ export function registerAll(context) {
1017
1222
  { name: 'get_component_selector_guide', purpose: 'Component selection guide by category and use case' },
1018
1223
  ],
1019
1224
  recommendedWorkflow: [
1020
- '1. get_design_system_overview → understand components, patterns, tokens, and IDE setup templates',
1021
- '2. figma_to_wcf (optional)bootstrap the Figma-to-WCF tool sequence',
1022
- '3. search_design_system_knowledgedo a broad first-pass search across components, patterns, tokens, guidelines, and skills',
1023
- '4. wcf://components and wcf://tokens resources preload catalog/token context',
1225
+ '1. get_design_system_overview → start here and inspect componentPatternMap before creating custom HTML/CSS',
1226
+ '2. wcf://components and wcf://tokens resources preload the component/token catalog',
1227
+ '3. figma_to_wcf (optional) bootstrap the Figma-to-WCF tool sequence',
1228
+ '4. search_design_system_knowledge → do a broad first-pass search across components, patterns, tokens, guidelines, and skills',
1024
1229
  '5. search_guidelines → find relevant guidelines',
1025
1230
  '6. get_design_tokens → get correct token values',
1026
1231
  '7. get_design_token_detail → inspect one token with references/referencedBy and usage examples',
@@ -1033,6 +1238,18 @@ export function registerAll(context) {
1033
1238
  '14. generate_full_page_html → wrap fragment into a complete preview-ready page',
1034
1239
  '15. get_install_recipe → get import/install instructions',
1035
1240
  ],
1241
+ recommendedPreloadResources: [
1242
+ {
1243
+ uri: WCF_RESOURCE_URIS.components,
1244
+ reason: 'Read this before prototyping to avoid re-implementing existing WCF components.',
1245
+ when: 'Before generating page markup',
1246
+ },
1247
+ {
1248
+ uri: WCF_RESOURCE_URIS.tokens,
1249
+ reason: 'Read this before styling to avoid hard-coded token values.',
1250
+ when: 'Before writing CSS or inline styles',
1251
+ },
1252
+ ],
1036
1253
  experimental: {
1037
1254
  plugins: {
1038
1255
  enabled: plugins.length > 0,
@@ -1160,6 +1377,12 @@ export function registerAll(context) {
1160
1377
  if (related.length > 0) api.relatedComponents = related;
1161
1378
  const a11y = extractAccessibilityChecklist(d, { prefix });
1162
1379
  if (a11y) api.accessibilityChecklist = a11y;
1380
+ const interactionExamples = cTag ? INTERACTION_EXAMPLES_MAP[cTag] : undefined;
1381
+ if (interactionExamples) api.interactionExamples = interactionExamples;
1382
+ const layoutBehavior = cTag ? LAYOUT_BEHAVIOR_MAP[cTag] : undefined;
1383
+ if (layoutBehavior) api.layoutBehavior = layoutBehavior;
1384
+ const authoringGuidance = d?.custom?.authoringGuidance;
1385
+ if (authoringGuidance) api.authoringGuidance = authoringGuidance;
1163
1386
  results.push(api);
1164
1387
  }
1165
1388
  return buildJsonToolResponse(results);
@@ -1179,7 +1402,7 @@ export function registerAll(context) {
1179
1402
 
1180
1403
  if (!decl) {
1181
1404
  const identifier = component || tagName || className || '';
1182
- return buildComponentNotFoundError(identifier, indexes, p);
1405
+ return buildComponentNotFoundError(identifier, indexes, p, installRegistry);
1183
1406
  }
1184
1407
 
1185
1408
  const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
@@ -1200,6 +1423,10 @@ export function registerAll(context) {
1200
1423
  if (accessibilityChecklist) {
1201
1424
  api.accessibilityChecklist = accessibilityChecklist;
1202
1425
  }
1426
+ const authoringGuidance = decl?.custom?.authoringGuidance;
1427
+ if (authoringGuidance) {
1428
+ api.authoringGuidance = authoringGuidance;
1429
+ }
1203
1430
  const interactionExamples = canonicalTag ? INTERACTION_EXAMPLES_MAP[canonicalTag] : undefined;
1204
1431
  if (interactionExamples) {
1205
1432
  api.interactionExamples = interactionExamples;
@@ -1232,7 +1459,7 @@ export function registerAll(context) {
1232
1459
  const decl = resolved?.decl;
1233
1460
 
1234
1461
  if (!decl) {
1235
- return buildComponentNotFoundError(component, indexes, p);
1462
+ return buildComponentNotFoundError(component, indexes, p, installRegistry);
1236
1463
  }
1237
1464
 
1238
1465
  const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
@@ -1265,10 +1492,7 @@ export function registerAll(context) {
1265
1492
  const decl = resolved?.decl;
1266
1493
 
1267
1494
  if (!decl) {
1268
- return {
1269
- content: [{ type: 'text', text: `Component not found: ${component}` }],
1270
- isError: true,
1271
- };
1495
+ return buildComponentNotFoundError(component, indexes, p, installRegistry);
1272
1496
  }
1273
1497
 
1274
1498
  const canonicalTag = typeof decl.tagName === 'string' ? decl.tagName.toLowerCase() : undefined;
@@ -1588,13 +1812,13 @@ export function registerAll(context) {
1588
1812
  },
1589
1813
  },
1590
1814
  async ({ category, useCase }) => {
1591
- if (!context.selectorGuideData || !Array.isArray(context.selectorGuideData.categories)) {
1815
+ if (!selectorGuideData || !Array.isArray(selectorGuideData.categories)) {
1592
1816
  return buildJsonToolErrorResponse({
1593
1817
  error: 'Component selector guide not available.',
1594
1818
  });
1595
1819
  }
1596
1820
 
1597
- let categories = context.selectorGuideData.categories;
1821
+ let categories = selectorGuideData.categories;
1598
1822
 
1599
1823
  if (typeof category === 'string' && category.trim()) {
1600
1824
  const cat = category.trim().toLowerCase();
@@ -1602,15 +1826,35 @@ export function registerAll(context) {
1602
1826
  }
1603
1827
 
1604
1828
  if (typeof useCase === 'string' && useCase.trim()) {
1605
- const kw = useCase.trim().toLowerCase();
1606
- categories = categories.map((c) => ({
1607
- ...c,
1608
- components: c.components.filter((comp) =>
1609
- comp.useCase.toLowerCase().includes(kw) ||
1610
- comp.id.toLowerCase().includes(kw) ||
1611
- comp.tagName.toLowerCase().includes(kw)
1612
- ),
1613
- })).filter((c) => c.components.length > 0);
1829
+ const scored = [];
1830
+ for (const categoryEntry of categories) {
1831
+ const components = Array.isArray(categoryEntry?.components) ? categoryEntry.components : [];
1832
+ for (const component of components) {
1833
+ const score = scoreSelectorGuideComponent(component, useCase);
1834
+ if (score <= 0) continue;
1835
+ scored.push({
1836
+ categoryKey: categoryEntry.key,
1837
+ component: { ...component, _score: score },
1838
+ });
1839
+ }
1840
+ }
1841
+
1842
+ const grouped = new Map();
1843
+ for (const hit of scored.sort((left, right) => right.component._score - left.component._score)) {
1844
+ const list = grouped.get(hit.categoryKey) ?? [];
1845
+ list.push(hit.component);
1846
+ grouped.set(hit.categoryKey, list);
1847
+ }
1848
+
1849
+ categories = categories
1850
+ .map((categoryEntry) => ({
1851
+ ...categoryEntry,
1852
+ components: (grouped.get(categoryEntry.key) ?? []).map((component) => {
1853
+ const { _score, ...rest } = component;
1854
+ return rest;
1855
+ }),
1856
+ }))
1857
+ .filter((categoryEntry) => categoryEntry.components.length > 0);
1614
1858
  }
1615
1859
 
1616
1860
  return buildJsonToolResponse({
@@ -2051,19 +2295,25 @@ export function registerAll(context) {
2051
2295
  const terms = expandQueryWithSynonyms(q).filter(Boolean);
2052
2296
  const limit = Number.isInteger(maxResults) ? maxResults : 10;
2053
2297
  const results = [];
2298
+ const selectorGuideLookup = buildSelectorGuideComponentLookup(selectorGuideData);
2054
2299
 
2055
2300
  if (requestedSources.has('components')) {
2056
2301
  const page = buildComponentSummaries(indexes, {
2057
- query: q,
2058
2302
  limit: 200,
2059
2303
  prefix: p,
2060
2304
  });
2061
2305
  for (const item of page.items) {
2306
+ const canonicalTagName = toCanonicalTagName(item.tagName, p) ?? item.tagName;
2307
+ const guideEntry =
2308
+ selectorGuideLookup.get(`tag:${String(canonicalTagName).toLowerCase()}`) ??
2309
+ selectorGuideLookup.get(`id:${installRegistry?.tags?.[String(canonicalTagName).toLowerCase()] ?? ''}`);
2062
2310
  const score = scoreSearchFields(q, terms, [
2063
2311
  { text: item.tagName, weight: 5 },
2064
2312
  { text: item.className, weight: 4 },
2065
2313
  { text: item.description, weight: 2 },
2066
2314
  { text: item.category, weight: 1 },
2315
+ { text: guideEntry?.useCase, weight: 4 },
2316
+ { text: Array.isArray(guideEntry?.keywords) ? guideEntry.keywords.join(' ') : '', weight: 5 },
2067
2317
  ]);
2068
2318
  if (score <= 0) continue;
2069
2319
  results.push({
@@ -2074,6 +2324,8 @@ export function registerAll(context) {
2074
2324
  metadata: {
2075
2325
  className: item.className,
2076
2326
  category: item.category,
2327
+ useCase: guideEntry?.useCase,
2328
+ keywords: guideEntry?.keywords ?? [],
2077
2329
  },
2078
2330
  score: score + getKnowledgeSourceBoost('components', q, terms),
2079
2331
  });