@monoharada/wcf-mcp 0.2.0 → 0.5.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 +51 -1
- package/core.mjs +332 -41
- package/package.json +1 -1
- package/validator.mjs +149 -7
package/README.md
CHANGED
|
@@ -100,7 +100,24 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
|
|
|
100
100
|
|
|
101
101
|
| ツール | 説明 |
|
|
102
102
|
|--------|------|
|
|
103
|
-
| `validate_markup` | HTML
|
|
103
|
+
| `validate_markup` | HTML スニペットを検証し、セマンティック検証(下表)で `suggestion` 付き診断を返す |
|
|
104
|
+
|
|
105
|
+
#### validate_markup 検出コード一覧
|
|
106
|
+
|
|
107
|
+
| Code | Severity | 説明 | 例 |
|
|
108
|
+
|------|----------|------|----|
|
|
109
|
+
| `unknownElement` | error | CEM に未登録のカスタム要素。prefix 補完提案あり | `<input-text>` → `dads-input-text` |
|
|
110
|
+
| `unknownAttribute` | warning | CEM に未登録の属性 | `<dads-button foo="x">` |
|
|
111
|
+
| `invalidEnumValue` | error | enum 属性に不正な値 | `type="banana"` |
|
|
112
|
+
| `invalidSlotName` | error | CEM に未登録のスロット名 | `slot="nonexistent"` |
|
|
113
|
+
| `missingRequiredAttribute` | error | フォーム要素の必須属性欠落 | `<dads-input-text>` without `label` |
|
|
114
|
+
| `orphanedChildComponent` | warning | 親要素なしの子コンポーネント | `<dads-breadcrumb-item>` without `<dads-breadcrumb>` |
|
|
115
|
+
| `emptyInteractiveElement` | warning | accessible name が空の操作要素 | `<dads-button></dads-button>` |
|
|
116
|
+
| `canonicalLowercaseRecommendation` | warning | 大文字を含む属性名(lowercase が canonical) | `Variant="solid"` → `variant` |
|
|
117
|
+
| `tokenMisuse` | warning | inline style でのトークン誤用 | `color: #000` → `var(--color-*)` |
|
|
118
|
+
| `ariaLiveNotRecommended` | warning | `aria-live` の使用(DADS 非推奨) | `aria-live="polite"` |
|
|
119
|
+
| `roleAlertNotRecommended` | warning | `role="alert"` の使用(DADS 非推奨) | `role="alert"` |
|
|
120
|
+
| `forbiddenAttribute` | warning | 禁止属性 | `placeholder` |
|
|
104
121
|
|
|
105
122
|
### UI パターン
|
|
106
123
|
|
|
@@ -355,6 +372,39 @@ Claude Desktop 設定例:
|
|
|
355
372
|
prefix: "myui" → dads-button → myui-button
|
|
356
373
|
```
|
|
357
374
|
|
|
375
|
+
## v0.3.0 新機能 — ランタイムセットアップ情報
|
|
376
|
+
|
|
377
|
+
v0.3.0 では、AI エージェントが CDN 非対応の vendor-local 配信モデルを正しく理解できるよう、3つのツールにランタイムセットアップ情報を追加しました。
|
|
378
|
+
|
|
379
|
+
### `get_design_system_overview` — setupInfo 新フィールド
|
|
380
|
+
|
|
381
|
+
| フィールド | 型 | 説明 |
|
|
382
|
+
|-----------|-----|------|
|
|
383
|
+
| `noCDN` | `true` | CDN 配信が利用不可であることを示すフラグ |
|
|
384
|
+
| `deliveryModel` | `"vendor-local"` | 配信モデルの種別(将来拡張可能) |
|
|
385
|
+
| `importMapHint` | `string` | import map のパターン説明 |
|
|
386
|
+
| `bootScript` | `string` | boot.js の役割説明 |
|
|
387
|
+
| `vendorSetup` | `object` | `init`/`add`/`workflow` の2段階セットアップガイド |
|
|
388
|
+
| `htmlSetup` | `string` | import map + boot.js を含む完全な HTML head テンプレート |
|
|
389
|
+
|
|
390
|
+
> 既存の `htmlBoilerplate` は変更なし(後方互換性)
|
|
391
|
+
|
|
392
|
+
### `get_install_recipe` — 新フィールド
|
|
393
|
+
|
|
394
|
+
| フィールド | 型 | 説明 |
|
|
395
|
+
|-----------|-----|------|
|
|
396
|
+
| `usageContext` | `"body-only"` | `usageSnippet` が body 用 HTML であることを明示 |
|
|
397
|
+
| `vendorHint` | `object` | `install`(CLI コマンド)、`importMap`(テンプレート)、`boot`(ブートスクリプト説明)。`importmap`(小文字 m)は非推奨エイリアス — v1.0 で削除予定 |
|
|
398
|
+
|
|
399
|
+
### `get_pattern_recipe` — 新フィールド
|
|
400
|
+
|
|
401
|
+
| フィールド | 型 | 説明 |
|
|
402
|
+
|-----------|-----|------|
|
|
403
|
+
| `entryHints` | `string[]` | パターンのエントリポイント(`["boot"]` など) |
|
|
404
|
+
| `scaffoldHint` | `object` | `doctype`、`importMap`、`bootScript`、`noscript`、`serveOverHttp` を含むページ雛形情報 |
|
|
405
|
+
|
|
406
|
+
> `scaffoldHint.serveOverHttp` は `file://` プロトコルでの実行を防止するガイダンスです。
|
|
407
|
+
|
|
358
408
|
## v0.2.0 マイグレーション
|
|
359
409
|
|
|
360
410
|
### `list_components` のデフォルトページネーション変更
|
package/core.mjs
CHANGED
|
@@ -145,8 +145,97 @@ const SYNONYM_TABLE = new Map([
|
|
|
145
145
|
['skip-navigation', ['skip-link', 'landmark', 'skip nav']],
|
|
146
146
|
['heading', ['heading hierarchy', 'h1', 'heading level']],
|
|
147
147
|
['form', ['input', 'validation', 'required', 'label']],
|
|
148
|
+
['layout', ['grid', 'flexbox', 'layout-shell', 'responsive', 'breakpoint']],
|
|
149
|
+
['responsive', ['media query', 'breakpoint', 'viewport', 'mobile']],
|
|
150
|
+
['error', ['validation', 'aria-invalid', 'aria-describedby', 'error text']],
|
|
151
|
+
['focus', ['focus-visible', 'focus ring', 'outline', 'tabindex', 'keyboard']],
|
|
152
|
+
['token', ['design token', 'css variable', 'custom property', 'spacing token']],
|
|
148
153
|
]);
|
|
149
154
|
|
|
155
|
+
// Interaction examples for form components (P-04 / #206)
|
|
156
|
+
const INTERACTION_EXAMPLES_MAP = Object.freeze({
|
|
157
|
+
'dads-input-text': [
|
|
158
|
+
{ scenario: 'Set value programmatically', trigger: 'property', code: 'el.value = "hello";' },
|
|
159
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
160
|
+
{ scenario: 'Clear validation error', trigger: 'attribute', code: 'el.error = false; el.errorText = "";' },
|
|
161
|
+
{ scenario: 'Listen to value change', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
|
|
162
|
+
],
|
|
163
|
+
'dads-textarea': [
|
|
164
|
+
{ scenario: 'Set value programmatically', trigger: 'property', code: 'el.value = "long text...";' },
|
|
165
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "入力できる文字数を超えています";' },
|
|
166
|
+
{ scenario: 'Listen to input event', trigger: 'event', code: "el.addEventListener('input', (e) => { console.log(e.target.value); });" },
|
|
167
|
+
],
|
|
168
|
+
'dads-select': [
|
|
169
|
+
{ scenario: 'Set selected value', trigger: 'property', code: 'el.value = "option1";' },
|
|
170
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
171
|
+
{ scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
|
|
172
|
+
],
|
|
173
|
+
'dads-checkbox': [
|
|
174
|
+
{ scenario: 'Set checked state', trigger: 'property', code: 'el.checked = true;' },
|
|
175
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
176
|
+
{ scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.checked); });" },
|
|
177
|
+
],
|
|
178
|
+
'dads-radio': [
|
|
179
|
+
{ scenario: 'Set checked state', trigger: 'property', code: 'el.checked = true;' },
|
|
180
|
+
{ scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
|
|
181
|
+
],
|
|
182
|
+
'dads-combobox': [
|
|
183
|
+
{ scenario: 'Set value programmatically', trigger: 'property', code: 'el.value = "selected-option";' },
|
|
184
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
185
|
+
{ scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
|
|
186
|
+
],
|
|
187
|
+
'dads-date-picker': [
|
|
188
|
+
{ scenario: 'Set date value', trigger: 'property', code: 'el.value = "2024-01-15";' },
|
|
189
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
190
|
+
{ scenario: 'Listen to change event', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.value); });" },
|
|
191
|
+
],
|
|
192
|
+
'dads-file-upload': [
|
|
193
|
+
{ scenario: 'Listen to file selection', trigger: 'event', code: "el.addEventListener('change', (e) => { console.log(e.target.files); });" },
|
|
194
|
+
{ scenario: 'Show validation error', trigger: 'attribute', code: 'el.error = true; el.errorText = "この項目は入力が必須です";' },
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Layout behavior metadata for layout/display components (P-05 / #207)
|
|
199
|
+
const LAYOUT_BEHAVIOR_MAP = Object.freeze({
|
|
200
|
+
'dads-layout-shell': {
|
|
201
|
+
responsive: {
|
|
202
|
+
breakpoints: { desktop: '80rem', tablet: '48rem' },
|
|
203
|
+
modes: ['auto', 'desktop', 'tablet', 'mobile'],
|
|
204
|
+
defaultMode: 'auto',
|
|
205
|
+
description: 'Automatically switches between desktop/tablet/mobile layouts based on viewport width when mode="auto".',
|
|
206
|
+
},
|
|
207
|
+
overflow: {
|
|
208
|
+
strategy: 'slot-driven',
|
|
209
|
+
description: 'Slots (header, sidebar, aside, footer) are auto-hidden when empty. Sidebar collapses to rail on tablet.',
|
|
210
|
+
},
|
|
211
|
+
constraints: {
|
|
212
|
+
patterns: ['website', 'app-shell', 'master-detail', 'left-header-pane', 'three-pane', 'three-pane-shell'],
|
|
213
|
+
defaultPattern: 'app-shell',
|
|
214
|
+
mobileSidebarOptions: ['hidden', 'top', 'bottom'],
|
|
215
|
+
description: 'Choose a pattern attribute to control layout structure. Pair with mode and mobile-sidebar for full control.',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
'dads-layout-sidebar': {
|
|
219
|
+
responsive: {
|
|
220
|
+
description: 'Designed to be placed inside dads-layout-shell sidebar slot. Width is controlled by the parent shell.',
|
|
221
|
+
},
|
|
222
|
+
constraints: {
|
|
223
|
+
description: 'Simple container for sidebar content. Use inside dads-layout-shell for responsive behavior.',
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
'dads-device-mock': {
|
|
227
|
+
responsive: {
|
|
228
|
+
devices: ['desktop', 'tablet', 'mobile'],
|
|
229
|
+
defaultDevice: 'mobile',
|
|
230
|
+
description: 'Renders a device frame (desktop/tablet/mobile) around slotted content. Set device attribute to switch.',
|
|
231
|
+
},
|
|
232
|
+
constraints: {
|
|
233
|
+
visibleHeight: 'Use visible-height attribute to clip the mock to a specific height (e.g. "220px").',
|
|
234
|
+
description: 'Display-only component for previewing content in device frames. Not a layout container.',
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
150
239
|
export function expandQueryWithSynonyms(query) {
|
|
151
240
|
const q = String(query ?? '').toLowerCase().trim();
|
|
152
241
|
if (!q) return [q];
|
|
@@ -752,10 +841,16 @@ export function levenshteinDistance(left, right) {
|
|
|
752
841
|
return prev[b.length];
|
|
753
842
|
}
|
|
754
843
|
|
|
755
|
-
export function suggestUnknownElementTagName(tagName, cemIndex) {
|
|
844
|
+
export function suggestUnknownElementTagName(tagName, cemIndex, prefix) {
|
|
756
845
|
const target = String(tagName ?? '').trim().toLowerCase();
|
|
757
846
|
if (!target || !target.includes('-')) return undefined;
|
|
758
847
|
|
|
848
|
+
// Try prefix-prepend before Levenshtein (e.g. input-text → dads-input-text)
|
|
849
|
+
if (prefix && cemIndex instanceof Map) {
|
|
850
|
+
const prefixed = `${String(prefix).toLowerCase()}-${target}`;
|
|
851
|
+
if (cemIndex.has(prefixed)) return prefixed;
|
|
852
|
+
}
|
|
853
|
+
|
|
759
854
|
let bestTag;
|
|
760
855
|
let bestDistance = Number.POSITIVE_INFINITY;
|
|
761
856
|
const candidateSource = cemIndex instanceof Map ? cemIndex.keys() : [];
|
|
@@ -775,15 +870,19 @@ export function suggestUnknownElementTagName(tagName, cemIndex) {
|
|
|
775
870
|
return bestTag;
|
|
776
871
|
}
|
|
777
872
|
|
|
778
|
-
export function buildDiagnosticSuggestion({ diagnostic, cemIndex }) {
|
|
873
|
+
export function buildDiagnosticSuggestion({ diagnostic, cemIndex, prefix }) {
|
|
779
874
|
const code = String(diagnostic?.code ?? '');
|
|
780
875
|
if (!code) return undefined;
|
|
781
876
|
|
|
782
877
|
if (code === 'unknownElement') {
|
|
783
|
-
const tagName = suggestUnknownElementTagName(diagnostic?.tagName, cemIndex);
|
|
878
|
+
const tagName = suggestUnknownElementTagName(diagnostic?.tagName, cemIndex, prefix);
|
|
784
879
|
return tagName ? `Did you mean "${tagName}"?` : undefined;
|
|
785
880
|
}
|
|
786
881
|
|
|
882
|
+
if (code === 'canonicalLowercaseRecommendation') {
|
|
883
|
+
return diagnostic?.hint ?? undefined;
|
|
884
|
+
}
|
|
885
|
+
|
|
787
886
|
if (code === 'forbiddenAttribute' && String(diagnostic?.attrName ?? '').toLowerCase() === 'placeholder') {
|
|
788
887
|
return 'Use aria-label or aria-describedby support text instead of placeholder.';
|
|
789
888
|
}
|
|
@@ -1621,6 +1720,9 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
1621
1720
|
detectMissingRequiredAttributes = () => [],
|
|
1622
1721
|
detectOrphanedChildComponents = () => [],
|
|
1623
1722
|
detectEmptyInteractiveElement = () => [],
|
|
1723
|
+
detectNonLowercaseAttributes = () => [],
|
|
1724
|
+
detectCdnReferences = () => [],
|
|
1725
|
+
detectMissingRuntimeScaffold = () => [],
|
|
1624
1726
|
} = await loadValidator();
|
|
1625
1727
|
const canonicalCemIndex = collectCemCustomElements(manifest);
|
|
1626
1728
|
const canonicalEnumMap = buildEnumAttributeMap(manifest);
|
|
@@ -1653,9 +1755,12 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
1653
1755
|
|
|
1654
1756
|
const tokenSuggestionMap = buildTokenSuggestionMap(designTokensData);
|
|
1655
1757
|
|
|
1758
|
+
const VENDOR_DIR = 'vendor-runtime';
|
|
1759
|
+
const PREFIX_STRIP_RE = /^[^-]+-/;
|
|
1760
|
+
|
|
1656
1761
|
const server = new McpServer({
|
|
1657
1762
|
name: 'web-components-factory-design-system',
|
|
1658
|
-
version: '0.
|
|
1763
|
+
version: '0.5.0',
|
|
1659
1764
|
});
|
|
1660
1765
|
|
|
1661
1766
|
server.registerPrompt(
|
|
@@ -1805,7 +1910,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
1805
1910
|
|
|
1806
1911
|
const overview = {
|
|
1807
1912
|
name: 'DADS Web Components (wcf)',
|
|
1808
|
-
version: '0.
|
|
1913
|
+
version: '0.5.0',
|
|
1809
1914
|
prefix: CANONICAL_PREFIX,
|
|
1810
1915
|
totalComponents: indexes.decls.length,
|
|
1811
1916
|
componentsByCategory: categoryCount,
|
|
@@ -1814,9 +1919,37 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
1814
1919
|
setupInfo: {
|
|
1815
1920
|
npmPackage: 'web-components-factory',
|
|
1816
1921
|
installCommand: 'npm install web-components-factory',
|
|
1817
|
-
vendorRuntimePath:
|
|
1922
|
+
vendorRuntimePath: `${VENDOR_DIR}/`,
|
|
1818
1923
|
htmlBoilerplate: '<script type="module" src="vendor-runtime/src/autoload.js"></script>',
|
|
1819
1924
|
noscriptGuidance: 'WCF components require JavaScript. Provide <noscript> fallback with static HTML equivalents for critical content.',
|
|
1925
|
+
noCDN: true,
|
|
1926
|
+
deliveryModel: 'vendor-local',
|
|
1927
|
+
distribution: {
|
|
1928
|
+
selfHosted: true,
|
|
1929
|
+
cdn: false,
|
|
1930
|
+
strategy: 'vendor-importmap',
|
|
1931
|
+
quickStart: 'npx web-components-factory init --prefix <prefix> --dir <dir>',
|
|
1932
|
+
description:
|
|
1933
|
+
'Components are installed locally via the wcf CLI. No CDN is available. All assets are served from the project directory using import maps and a boot script.',
|
|
1934
|
+
},
|
|
1935
|
+
importMapHint: 'WCF uses <script type="importmap"> for module resolution. Each component tag name maps to a local JS file: { "<prefix>-<component>": "./<dir>/components/<component>.js" }. The wcf CLI generates importmap.snippet.json automatically via `wcf init`.',
|
|
1936
|
+
bootScript: '<dir>/boot.js — sets the component prefix via setConfig(), then loads wc-autoloader.js which scans the DOM for custom element tags and dynamically imports them via the import map.',
|
|
1937
|
+
vendorSetup: {
|
|
1938
|
+
init: 'wcf init --prefix <prefix> --dir <dir>',
|
|
1939
|
+
add: 'wcf add <componentId> --prefix <prefix> --out <dir>',
|
|
1940
|
+
workflow: '1. wcf init で初期化(boot.js, importmap.snippet.json, autoloader を生成) → 2. wcf add で各コンポーネントを追加 → import map と boot.js が自動生成される',
|
|
1941
|
+
},
|
|
1942
|
+
htmlSetup: [
|
|
1943
|
+
'<script type="importmap">',
|
|
1944
|
+
'{',
|
|
1945
|
+
' "imports": {',
|
|
1946
|
+
' "<prefix>-button": "./<dir>/components/button.js",',
|
|
1947
|
+
' "<prefix>-card": "./<dir>/components/card.js"',
|
|
1948
|
+
' }',
|
|
1949
|
+
'}',
|
|
1950
|
+
'</script>',
|
|
1951
|
+
'<script type="module" src="./<dir>/boot.js"></script>',
|
|
1952
|
+
].join('\n'),
|
|
1820
1953
|
},
|
|
1821
1954
|
ideSetupTemplates: IDE_SETUP_TEMPLATES,
|
|
1822
1955
|
availablePrompts: [
|
|
@@ -2015,6 +2148,14 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2015
2148
|
if (accessibilityChecklist) {
|
|
2016
2149
|
api.accessibilityChecklist = accessibilityChecklist;
|
|
2017
2150
|
}
|
|
2151
|
+
const interactionExamples = canonicalTag ? INTERACTION_EXAMPLES_MAP[canonicalTag] : undefined;
|
|
2152
|
+
if (interactionExamples) {
|
|
2153
|
+
api.interactionExamples = interactionExamples;
|
|
2154
|
+
}
|
|
2155
|
+
const layoutBehavior = canonicalTag ? LAYOUT_BEHAVIOR_MAP[canonicalTag] : undefined;
|
|
2156
|
+
if (layoutBehavior) {
|
|
2157
|
+
api.layoutBehavior = layoutBehavior;
|
|
2158
|
+
}
|
|
2018
2159
|
|
|
2019
2160
|
return buildJsonToolResponse(api);
|
|
2020
2161
|
},
|
|
@@ -2133,7 +2274,19 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2133
2274
|
defineHint,
|
|
2134
2275
|
source: install.source,
|
|
2135
2276
|
usageSnippet,
|
|
2277
|
+
usageContext: 'body-only',
|
|
2136
2278
|
installHint: componentId ? `wcf add ${componentId}` : undefined,
|
|
2279
|
+
vendorHint: (() => {
|
|
2280
|
+
const im = tagNames.length > 0
|
|
2281
|
+
? JSON.stringify({ imports: Object.fromEntries(tagNames.map((t) => [t, `./<dir>/components/${t.replace(/^[^-]+-/, '')}.js`])) })
|
|
2282
|
+
: undefined;
|
|
2283
|
+
return {
|
|
2284
|
+
install: componentId ? `wcf add ${componentId} --prefix <prefix> --out <dir>` : undefined,
|
|
2285
|
+
importMap: im,
|
|
2286
|
+
importmap: im, // @deprecated — use importMap; will be removed in v1.0
|
|
2287
|
+
boot: '<dir>/boot.js -- loads autoloader that registers components via import map',
|
|
2288
|
+
};
|
|
2289
|
+
})(),
|
|
2137
2290
|
},
|
|
2138
2291
|
null,
|
|
2139
2292
|
2,
|
|
@@ -2226,8 +2379,40 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2226
2379
|
severity: 'warning',
|
|
2227
2380
|
});
|
|
2228
2381
|
|
|
2229
|
-
const
|
|
2230
|
-
|
|
2382
|
+
const lowercaseDiagnostics = detectNonLowercaseAttributes({
|
|
2383
|
+
filePath: '<markup>',
|
|
2384
|
+
text: html,
|
|
2385
|
+
cem: cemIndex,
|
|
2386
|
+
severity: 'warning',
|
|
2387
|
+
});
|
|
2388
|
+
|
|
2389
|
+
const cdnDiagnostics = detectCdnReferences({
|
|
2390
|
+
filePath: '<markup>',
|
|
2391
|
+
text: html,
|
|
2392
|
+
severity: 'warning',
|
|
2393
|
+
});
|
|
2394
|
+
|
|
2395
|
+
const scaffoldDiagnostics = detectMissingRuntimeScaffold({
|
|
2396
|
+
filePath: '<markup>',
|
|
2397
|
+
text: html,
|
|
2398
|
+
severity: 'warning',
|
|
2399
|
+
});
|
|
2400
|
+
|
|
2401
|
+
const allRawDiagnostics = [
|
|
2402
|
+
...cemDiagnostics,
|
|
2403
|
+
...enumDiagnostics,
|
|
2404
|
+
...slotDiagnostics,
|
|
2405
|
+
...requiredAttrDiagnostics,
|
|
2406
|
+
...orphanDiagnostics,
|
|
2407
|
+
...emptyInteractiveDiagnostics,
|
|
2408
|
+
...lowercaseDiagnostics,
|
|
2409
|
+
...tokenMisuseDiagnostics,
|
|
2410
|
+
...accessibilityDiagnostics,
|
|
2411
|
+
...cdnDiagnostics,
|
|
2412
|
+
...scaffoldDiagnostics,
|
|
2413
|
+
];
|
|
2414
|
+
const diagnostics = allRawDiagnostics.map((d) => {
|
|
2415
|
+
const suggestion = buildDiagnosticSuggestion({ diagnostic: d, cemIndex, prefix: p });
|
|
2231
2416
|
return {
|
|
2232
2417
|
file: d.file,
|
|
2233
2418
|
range: d.range,
|
|
@@ -2271,6 +2456,70 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2271
2456
|
},
|
|
2272
2457
|
);
|
|
2273
2458
|
|
|
2459
|
+
// -----------------------------------------------------------------------
|
|
2460
|
+
// Helper: buildFullPageHtml
|
|
2461
|
+
// -----------------------------------------------------------------------
|
|
2462
|
+
function escapeHtmlTitle(s) {
|
|
2463
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
/**
|
|
2467
|
+
* Build import map entries from a component closure.
|
|
2468
|
+
* @param {string[]} closure - Component IDs
|
|
2469
|
+
* @param {Object} components - Component metadata (from install registry)
|
|
2470
|
+
* @param {string} prefix - Tag name prefix
|
|
2471
|
+
* @param {string} dir - Directory placeholder or concrete path
|
|
2472
|
+
* @returns {Object} Import map entries { prefixedTag: path }
|
|
2473
|
+
*/
|
|
2474
|
+
function buildImportMapEntries(closure, components, prefix, dir) {
|
|
2475
|
+
return Object.fromEntries(
|
|
2476
|
+
closure.flatMap((cid) => {
|
|
2477
|
+
const meta = components[cid];
|
|
2478
|
+
const tags = Array.isArray(meta?.tags) ? meta.tags : [cid];
|
|
2479
|
+
return tags.map((t) => {
|
|
2480
|
+
const lower = String(t).toLowerCase();
|
|
2481
|
+
const suffix = lower.replace(PREFIX_STRIP_RE, '');
|
|
2482
|
+
return [withPrefix(lower, prefix), `./${dir}/components/${suffix}.js`];
|
|
2483
|
+
});
|
|
2484
|
+
}),
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
/**
|
|
2489
|
+
* Build a complete HTML5 page from pattern data, resolving scaffoldHint
|
|
2490
|
+
* placeholders into concrete paths.
|
|
2491
|
+
* @param {Object} opts
|
|
2492
|
+
* @param {string} opts.html - Pattern HTML body
|
|
2493
|
+
* @param {string} opts.title - Page title
|
|
2494
|
+
* @param {Object} opts.importMapEntries - Import map entries { tag: path }
|
|
2495
|
+
* @param {string} [opts.dir] - Directory for JS assets
|
|
2496
|
+
* @param {string} [opts.lang='ja'] - HTML lang attribute
|
|
2497
|
+
* @returns {string} Complete HTML5 document
|
|
2498
|
+
*/
|
|
2499
|
+
function buildFullPageHtml({ html, title, importMapEntries, dir = VENDOR_DIR, lang = 'ja' }) {
|
|
2500
|
+
const importMapJson = JSON.stringify({ imports: importMapEntries }, null, 2);
|
|
2501
|
+
return [
|
|
2502
|
+
'<!DOCTYPE html>',
|
|
2503
|
+
`<html lang="${lang}">`,
|
|
2504
|
+
'<head>',
|
|
2505
|
+
' <meta charset="UTF-8">',
|
|
2506
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1.0">',
|
|
2507
|
+
` <title>${escapeHtmlTitle(title)}</title>`,
|
|
2508
|
+
` <!-- distribution: selfHosted=true, strategy=vendor-importmap -->`,
|
|
2509
|
+
` <!-- Do NOT replace these local paths with CDN URLs. This design system is self-hosted. -->`,
|
|
2510
|
+
` <script type="importmap">`,
|
|
2511
|
+
`${importMapJson}`,
|
|
2512
|
+
` </script>`,
|
|
2513
|
+
` <script type="module" src="./${dir}/boot.js"></script>`,
|
|
2514
|
+
'</head>',
|
|
2515
|
+
'<body>',
|
|
2516
|
+
` <noscript>このページの機能にはJavaScriptが必要です。</noscript>`,
|
|
2517
|
+
` ${html}`,
|
|
2518
|
+
'</body>',
|
|
2519
|
+
'</html>',
|
|
2520
|
+
].join('\n');
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2274
2523
|
// -----------------------------------------------------------------------
|
|
2275
2524
|
// Tool: get_pattern_recipe
|
|
2276
2525
|
// -----------------------------------------------------------------------
|
|
@@ -2278,13 +2527,14 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2278
2527
|
'get_pattern_recipe',
|
|
2279
2528
|
{
|
|
2280
2529
|
description:
|
|
2281
|
-
'Get a complete pattern recipe with component dependencies and HTML. When: building a page layout from a pattern. Returns: dependency tree, install commands, and resolved HTML. After: use validate_markup to verify the generated HTML.',
|
|
2530
|
+
'Get a complete pattern recipe with component dependencies and HTML. When: building a page layout from a pattern. Returns: dependency tree, install commands, and resolved HTML. After: use validate_markup to verify the generated HTML. Use include: ["fullPage"] to get a complete HTML5 page ready for browser rendering.',
|
|
2282
2531
|
inputSchema: {
|
|
2283
2532
|
patternId: z.string(),
|
|
2284
2533
|
prefix: z.string().optional(),
|
|
2534
|
+
include: z.array(z.enum(['fullPage'])).optional(),
|
|
2285
2535
|
},
|
|
2286
2536
|
},
|
|
2287
|
-
async ({ patternId, prefix }) => {
|
|
2537
|
+
async ({ patternId, prefix, include }) => {
|
|
2288
2538
|
const id = String(patternId ?? '').trim();
|
|
2289
2539
|
const p = normalizePrefix(prefix);
|
|
2290
2540
|
const pat = patterns[id];
|
|
@@ -2315,28 +2565,59 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2315
2565
|
const canonicalHtml = String(pat.html ?? '');
|
|
2316
2566
|
const html = applyPrefixToHtml(canonicalHtml, p);
|
|
2317
2567
|
|
|
2568
|
+
const entryHints = Array.isArray(pat.entryHints) ? [...pat.entryHints] : ['boot'];
|
|
2569
|
+
|
|
2570
|
+
const importMapEntries = buildImportMapEntries(closure, components, p, '<dir>');
|
|
2571
|
+
|
|
2572
|
+
const scaffoldHint = {
|
|
2573
|
+
doctype: '<!DOCTYPE html>',
|
|
2574
|
+
importMap: `<script type="importmap">\n${JSON.stringify({ imports: importMapEntries }, null, 2)}\n</script>`,
|
|
2575
|
+
bootScript: '<script type="module" src="./<dir>/boot.js"></script>',
|
|
2576
|
+
noscript: '<noscript>このページの機能にはJavaScriptが必要です。</noscript>',
|
|
2577
|
+
serveOverHttp: 'Import maps require HTTP/HTTPS. Use a local dev server (e.g. npx serve .) instead of opening the HTML file directly via file:// protocol.',
|
|
2578
|
+
};
|
|
2579
|
+
|
|
2580
|
+
// Build fullPageHtml when requested via include: ['fullPage']
|
|
2581
|
+
const includeArr = Array.isArray(include) ? include : [];
|
|
2582
|
+
let fullPageHtml;
|
|
2583
|
+
if (includeArr.includes('fullPage')) {
|
|
2584
|
+
const resolvedImportMap = buildImportMapEntries(closure, components, p, VENDOR_DIR);
|
|
2585
|
+
fullPageHtml = buildFullPageHtml({
|
|
2586
|
+
html,
|
|
2587
|
+
title: pat.title ?? pat.id,
|
|
2588
|
+
importMapEntries: resolvedImportMap,
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
const result = {
|
|
2593
|
+
pattern: {
|
|
2594
|
+
id: pat.id,
|
|
2595
|
+
title: pat.title,
|
|
2596
|
+
description: pat.description,
|
|
2597
|
+
},
|
|
2598
|
+
prefix: p,
|
|
2599
|
+
requires,
|
|
2600
|
+
components: closure,
|
|
2601
|
+
install,
|
|
2602
|
+
html,
|
|
2603
|
+
canonicalHtml,
|
|
2604
|
+
installHint: closure.length > 0 ? `wcf add ${closure.join(' ')}` : undefined,
|
|
2605
|
+
entryHints,
|
|
2606
|
+
scaffoldHint,
|
|
2607
|
+
};
|
|
2608
|
+
|
|
2609
|
+
if (fullPageHtml !== undefined) {
|
|
2610
|
+
result.fullPageHtml = fullPageHtml;
|
|
2611
|
+
result.vendorSetup = {
|
|
2612
|
+
command: `npx web-components-factory init --prefix ${p} --dir ${VENDOR_DIR} && npx web-components-factory add ${closure.join(' ')} --prefix ${p} --out ${VENDOR_DIR}`,
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2318
2616
|
return {
|
|
2319
2617
|
content: [
|
|
2320
2618
|
{
|
|
2321
2619
|
type: 'text',
|
|
2322
|
-
text: JSON.stringify(
|
|
2323
|
-
{
|
|
2324
|
-
pattern: {
|
|
2325
|
-
id: pat.id,
|
|
2326
|
-
title: pat.title,
|
|
2327
|
-
description: pat.description,
|
|
2328
|
-
},
|
|
2329
|
-
prefix: p,
|
|
2330
|
-
requires,
|
|
2331
|
-
components: closure,
|
|
2332
|
-
install,
|
|
2333
|
-
html,
|
|
2334
|
-
canonicalHtml,
|
|
2335
|
-
installHint: closure.length > 0 ? `wcf add ${closure.join(' ')}` : undefined,
|
|
2336
|
-
},
|
|
2337
|
-
null,
|
|
2338
|
-
2,
|
|
2339
|
-
),
|
|
2620
|
+
text: JSON.stringify(result, null, 2),
|
|
2340
2621
|
},
|
|
2341
2622
|
],
|
|
2342
2623
|
};
|
|
@@ -2562,25 +2843,35 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2562
2843
|
// Snippet match: weight 1
|
|
2563
2844
|
if (snippet.includes(q)) score += 1;
|
|
2564
2845
|
|
|
2565
|
-
// Body text match: weight 1
|
|
2566
|
-
if (body && body.includes(q))
|
|
2846
|
+
// Body text match: weight 1, plus boost for multiple occurrences
|
|
2847
|
+
if (body && body.includes(q)) {
|
|
2848
|
+
score += 1;
|
|
2849
|
+
// Count additional occurrences in body for boost (cap at +2)
|
|
2850
|
+
let idx = body.indexOf(q);
|
|
2851
|
+
let occurrences = 0;
|
|
2852
|
+
while (idx !== -1 && occurrences < 3) {
|
|
2853
|
+
occurrences++;
|
|
2854
|
+
idx = body.indexOf(q, idx + q.length);
|
|
2855
|
+
}
|
|
2856
|
+
if (occurrences > 1) score += Math.min(occurrences - 1, 2);
|
|
2857
|
+
}
|
|
2567
2858
|
|
|
2568
|
-
// Synonym expansion match:
|
|
2569
|
-
if (
|
|
2570
|
-
|
|
2859
|
+
// Synonym expansion match: check all expanded terms, cap total synonym contribution at +2
|
|
2860
|
+
if (expandedTerms.length > 1) {
|
|
2861
|
+
let synScore = 0;
|
|
2862
|
+
const lowerKeywords = keywords.map((kw) => String(kw).toLowerCase());
|
|
2863
|
+
for (let i = 1; i < expandedTerms.length && synScore < 2; i++) {
|
|
2571
2864
|
const syn = expandedTerms[i];
|
|
2572
|
-
if (heading.includes(syn)
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
if (String(kw).toLowerCase().includes(syn)) {
|
|
2578
|
-
score += 1;
|
|
2865
|
+
if (heading.includes(syn)) { synScore += 1; continue; }
|
|
2866
|
+
if (snippet.includes(syn) || body.includes(syn)) { synScore += 1; continue; }
|
|
2867
|
+
for (const kw of lowerKeywords) {
|
|
2868
|
+
if (kw.includes(syn)) {
|
|
2869
|
+
synScore += 1;
|
|
2579
2870
|
break;
|
|
2580
2871
|
}
|
|
2581
2872
|
}
|
|
2582
|
-
if (score > 0) break;
|
|
2583
2873
|
}
|
|
2874
|
+
score += synScore;
|
|
2584
2875
|
}
|
|
2585
2876
|
|
|
2586
2877
|
if (score > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoharada/wcf-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MCP server for the web-components-factory design system. Provides component discovery, validation, and pattern-based UI composition without cloning the repository.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/validator.mjs
CHANGED
|
@@ -797,15 +797,17 @@ export function detectEmptyInteractiveElement({
|
|
|
797
797
|
}
|
|
798
798
|
|
|
799
799
|
/**
|
|
800
|
-
*
|
|
801
|
-
*
|
|
800
|
+
* Required attributes per form component (DIG-08).
|
|
801
|
+
* Both `label` and `name` are required for components that declare them in CEM.
|
|
802
|
+
* Note: dads-date-picker and dads-file-upload use slots for labels, not attributes.
|
|
802
803
|
*/
|
|
803
804
|
const REQUIRED_ATTRIBUTES = new Map([
|
|
804
|
-
['dads-input-text', ['label']],
|
|
805
|
-
['dads-textarea', ['label']],
|
|
806
|
-
['dads-select', ['label']],
|
|
807
|
-
['dads-checkbox', ['label']],
|
|
808
|
-
['dads-radio', ['label']],
|
|
805
|
+
['dads-input-text', ['label', 'name']],
|
|
806
|
+
['dads-textarea', ['label', 'name']],
|
|
807
|
+
['dads-select', ['label', 'name']],
|
|
808
|
+
['dads-checkbox', ['label', 'name']],
|
|
809
|
+
['dads-radio', ['label', 'name']],
|
|
810
|
+
['dads-combobox', ['label', 'name']],
|
|
809
811
|
]);
|
|
810
812
|
|
|
811
813
|
/**
|
|
@@ -866,3 +868,143 @@ export function detectMissingRequiredAttributes({
|
|
|
866
868
|
|
|
867
869
|
return diagnostics;
|
|
868
870
|
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Detect attributes written in non-lowercase on known custom elements.
|
|
874
|
+
* HTML attributes are case-insensitive, but WCF uses lowercase canonically.
|
|
875
|
+
* @param {{
|
|
876
|
+
* filePath?: string;
|
|
877
|
+
* text: string;
|
|
878
|
+
* cem: Map<string, { attributes: Set<string> }>;
|
|
879
|
+
* severity?: string;
|
|
880
|
+
* }} params
|
|
881
|
+
*/
|
|
882
|
+
export function detectNonLowercaseAttributes({
|
|
883
|
+
filePath = '<input>',
|
|
884
|
+
text,
|
|
885
|
+
cem,
|
|
886
|
+
severity = 'warning',
|
|
887
|
+
}) {
|
|
888
|
+
const diagnostics = [];
|
|
889
|
+
if (!(cem instanceof Map) || cem.size === 0) return diagnostics;
|
|
890
|
+
|
|
891
|
+
const lineStarts = computeLineIndex(text);
|
|
892
|
+
const tagRe = /<([a-z][a-z0-9-]*)\b([^<>]*?)>/gi;
|
|
893
|
+
let m;
|
|
894
|
+
|
|
895
|
+
while ((m = tagRe.exec(text))) {
|
|
896
|
+
const tag = String(m[1] ?? '').toLowerCase();
|
|
897
|
+
if (!tag.includes('-')) continue;
|
|
898
|
+
|
|
899
|
+
const meta = cem.get(tag);
|
|
900
|
+
if (!meta) continue;
|
|
901
|
+
|
|
902
|
+
const attrChunk = String(m[2] ?? '');
|
|
903
|
+
const rawAttrsStart = m.index + 1 + tag.length;
|
|
904
|
+
const attrs = parseAttributes(attrChunk);
|
|
905
|
+
|
|
906
|
+
for (const { name, offset } of attrs) {
|
|
907
|
+
// Check if attribute has non-lowercase characters BEFORE normalizing
|
|
908
|
+
if (name === name.toLowerCase()) continue;
|
|
909
|
+
|
|
910
|
+
const lower = name.toLowerCase();
|
|
911
|
+
|
|
912
|
+
// Skip global attributes and event handlers
|
|
913
|
+
if (shouldSkipAttr(lower)) continue;
|
|
914
|
+
|
|
915
|
+
// Only flag if the lowercase form is a known CEM attribute
|
|
916
|
+
if (!meta.attributes.has(lower)) continue;
|
|
917
|
+
|
|
918
|
+
const startIndex = rawAttrsStart + offset;
|
|
919
|
+
const endIndex = startIndex + name.length;
|
|
920
|
+
const range = makeRange(lineStarts, startIndex, endIndex);
|
|
921
|
+
diagnostics.push({
|
|
922
|
+
file: filePath,
|
|
923
|
+
range,
|
|
924
|
+
severity,
|
|
925
|
+
code: 'canonicalLowercaseRecommendation',
|
|
926
|
+
message: `Attribute "${name}" should be lowercase: "${lower}".`,
|
|
927
|
+
tagName: tag,
|
|
928
|
+
attrName: name,
|
|
929
|
+
hint: `Use "${lower}" instead of "${name}".`,
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return diagnostics;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Detect CDN URLs in markup that should use local vendor paths instead.
|
|
939
|
+
*/
|
|
940
|
+
export function detectCdnReferences({
|
|
941
|
+
filePath = '<input>',
|
|
942
|
+
text,
|
|
943
|
+
severity = 'warning',
|
|
944
|
+
}) {
|
|
945
|
+
const diagnostics = [];
|
|
946
|
+
const lineStarts = computeLineIndex(text);
|
|
947
|
+
const cdnRe = /https?:\/\/(?:cdn\.jsdelivr\.net|unpkg\.com|cdnjs\.cloudflare\.com|esm\.sh)/g;
|
|
948
|
+
let m;
|
|
949
|
+
while ((m = cdnRe.exec(text))) {
|
|
950
|
+
const range = makeRange(lineStarts, m.index, m.index + m[0].length);
|
|
951
|
+
diagnostics.push({
|
|
952
|
+
file: filePath,
|
|
953
|
+
range,
|
|
954
|
+
severity,
|
|
955
|
+
code: 'cdnReference',
|
|
956
|
+
message: `CDN URL detected: "${m[0]}". This design system is self-hosted. Use local vendor paths instead.`,
|
|
957
|
+
tagName: '',
|
|
958
|
+
hint: 'Replace CDN URLs with local paths (e.g., ./vendor-runtime/components/...). Run `wcf init` to set up local assets.',
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return diagnostics;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Detect missing importmap or boot.js in a full HTML page.
|
|
967
|
+
* Only triggers when the markup contains a full page structure (<!DOCTYPE html>).
|
|
968
|
+
*/
|
|
969
|
+
export function detectMissingRuntimeScaffold({
|
|
970
|
+
filePath = '<input>',
|
|
971
|
+
text,
|
|
972
|
+
severity = 'warning',
|
|
973
|
+
}) {
|
|
974
|
+
const diagnostics = [];
|
|
975
|
+
|
|
976
|
+
// Only check full HTML pages
|
|
977
|
+
if (!text.includes('<!DOCTYPE html>') && !text.includes('<!doctype html>')) {
|
|
978
|
+
return diagnostics;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const lineStarts = computeLineIndex(text);
|
|
982
|
+
|
|
983
|
+
if (!/<script\b[^>]*\btype\s*=\s*['"]importmap['"][^>]*>/i.test(text)) {
|
|
984
|
+
const range = makeRange(lineStarts, 0, Math.min(text.length, 15));
|
|
985
|
+
diagnostics.push({
|
|
986
|
+
file: filePath,
|
|
987
|
+
range,
|
|
988
|
+
severity,
|
|
989
|
+
code: 'missingImportmap',
|
|
990
|
+
message: 'Full HTML page is missing <script type="importmap">. WCF components require an import map for module resolution.',
|
|
991
|
+
tagName: '',
|
|
992
|
+
hint: 'Add <script type="importmap">{"imports":{...}}</script> in <head>. Run `wcf init` to generate one.',
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (!text.includes('boot.js')) {
|
|
997
|
+
const range = makeRange(lineStarts, 0, Math.min(text.length, 15));
|
|
998
|
+
diagnostics.push({
|
|
999
|
+
file: filePath,
|
|
1000
|
+
range,
|
|
1001
|
+
severity,
|
|
1002
|
+
code: 'missingBootScript',
|
|
1003
|
+
message: 'Full HTML page is missing boot.js script. WCF components require the boot script to initialize.',
|
|
1004
|
+
tagName: '',
|
|
1005
|
+
hint: 'Add <script type="module" src="./vendor-runtime/boot.js"></script> in <head>.',
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
return diagnostics;
|
|
1010
|
+
}
|