@monoharada/wcf-mcp 0.8.0 → 0.9.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,7 +78,7 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
78
78
  }
79
79
  ```
80
80
 
81
- ## 提供機能(19 tools + 1 prompt + 4 resources)
81
+ ## 提供機能(16 tools + 1 prompt + 5 resources)
82
82
 
83
83
  ### ガードレール
84
84
 
@@ -145,17 +145,6 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
145
145
  | `get_accessibility_docs` | component/topic/wcagLevel で A11y チェックリストとガイドライン要点を検索(`topic=all` では両ソースを混在返却) |
146
146
  | `search_guidelines` | ガイドライン(topic/query)をスコア付きで検索 |
147
147
 
148
- ### スキル管理(リポジトリ内専用)
149
-
150
- | ツール | 説明 |
151
- |--------|------|
152
- | `list_skills` | 登録済みデザインシステムスキル一覧を取得 |
153
- | `get_skill_manifest` | スキルの SKILL.md マニフェストを取得 |
154
- | `check_drift` | スキルレジストリとリポジトリの整合性を検証 |
155
-
156
- > **注意**: これらのツールはリポジトリ内でのみ有効です。npx 実行時は `SKILLS_REGISTRY_UNAVAILABLE` エラーを返します。
157
- > 有効化には `wcf-mcp.config.json` で `design-system-skills` プラグインを設定してください(下記参照)。
158
-
159
148
  #### `get_design_tokens` の使用例
160
149
 
161
150
  **リクエスト:**
@@ -253,6 +242,7 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
253
242
  | `wcf://tokens` | トークン summary(type/category/themes/sample) |
254
243
  | `wcf://guidelines/{topic}` | topic 別ガイドライン要約(`accessibility`,`css`,`patterns`,`all`) |
255
244
  | `wcf://llms-full` | `llms-full.txt` の全文 |
245
+ | `wcf://skills` | 登録済み Claude Code / Cursor / Codex スキルカタログ(skills-registry.json ベース) |
256
246
 
257
247
  ### Figma MCP との併用
258
248
 
@@ -339,7 +329,7 @@ npx @monoharada/wcf-mcp --transport=http --port=3100
339
329
  ※ `./plugins/custom-validation-plugin.mjs` は利用側プロジェクトに配置してください。
340
330
  このリポジトリには参照用として `packages/mcp-server/examples/plugins/custom-validation-plugin.mjs` を同梱しています。
341
331
 
342
- ### plugin 契約(v1.1
332
+ ### plugin 契約(v1)
343
333
 
344
334
  詳細仕様: [docs/plugin-contract-v1.md](../../docs/plugin-contract-v1.md)
345
335
 
@@ -392,21 +382,6 @@ Claude Desktop 設定例:
392
382
  prefix: "myui" → dads-button → myui-button
393
383
  ```
394
384
 
395
- ## v0.8.0 新機能
396
-
397
- ### 新ツール(スキル管理)
398
- - **`list_skills`** — 登録済みデザインシステムスキルの一覧を取得
399
- - **`get_skill_manifest`** — スキルの SKILL.md マニフェスト内容を取得
400
- - **`check_drift`** — スキルレジストリとリポジトリの整合性を検証(ドリフト検出)
401
-
402
- ### 改善
403
- - **Plugin 契約 v1.1** — module plugin の `dataSources` パス解決をモジュールディレクトリ基準に統一
404
- - **Skills Registry plugin** — `plugins/design-system-skills/` として同梱。`wcf-mcp.config.json` で有効化
405
-
406
- > スキル管理ツールはリポジトリ内でのみ動作します。npx 実行時は graceful に `SKILLS_REGISTRY_UNAVAILABLE` を返します。
407
-
408
- ---
409
-
410
385
  ## v0.4.0 新機能
411
386
 
412
387
  ### 新ツール
@@ -668,8 +643,6 @@ packages/mcp-server/
668
643
  ├── server.mjs # MCP サーバー本体
669
644
  ├── validator.mjs # HTML バリデーター
670
645
  ├── package.json # npm パッケージ定義
671
- ├── plugins/ # 同梱プラグイン
672
- │ └── design-system-skills/ # スキル管理ツール (list_skills, get_skill_manifest, check_drift)
673
646
  └── data/ # バンドルデータ (npm run mcp:build で生成)
674
647
  ├── custom-elements.json
675
648
  ├── install-registry.json
package/core.mjs CHANGED
@@ -152,8 +152,24 @@ export const WCF_RESOURCE_URIS = Object.freeze({
152
152
  tokens: 'wcf://tokens',
153
153
  guidelinesTemplate: 'wcf://guidelines/{topic}',
154
154
  llmsFull: 'wcf://llms-full',
155
+ skills: 'wcf://skills',
155
156
  });
156
157
 
158
+ /** Normalize a skill entry to summary fields (omit compat/manifest for wcf://skills). */
159
+ function normalizeSkillSummary(s) {
160
+ return {
161
+ name: s.name,
162
+ description: s.description ?? '',
163
+ status: s.status ?? 'active',
164
+ path: s.path ?? '',
165
+ entry: s.entry ?? 'SKILL.md',
166
+ clients: Array.isArray(s.clients) ? s.clients : [],
167
+ tags: Array.isArray(s.tags) ? s.tags : [],
168
+ version: typeof s.version === 'string' ? s.version : '0.0.0',
169
+ dependencies: Array.isArray(s.dependencies) ? s.dependencies : [],
170
+ };
171
+ }
172
+
157
173
  // Unidirectional synonym table: key → expands to include these terms (DIG-09)
158
174
  // Searching "keyboard" also matches "focus", "tab" etc. but NOT reverse.
159
175
  const SYNONYM_TABLE = new Map([
@@ -2154,6 +2170,33 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
2154
2170
  },
2155
2171
  );
2156
2172
 
2173
+ // -----------------------------------------------------------------------
2174
+ // Resource: wcf://skills
2175
+ // -----------------------------------------------------------------------
2176
+ server.registerResource(
2177
+ 'wcf_skills',
2178
+ WCF_RESOURCE_URIS.skills,
2179
+ {
2180
+ title: 'WCF Skills Catalog',
2181
+ description: 'Registered Claude Code / Cursor / Codex skills from skills-registry.json.',
2182
+ mimeType: 'application/json',
2183
+ },
2184
+ async () => {
2185
+ const registry = await loadJsonData('skills-registry.json');
2186
+ if (!registry || !Array.isArray(registry.skills)) {
2187
+ throw new Error('SKILLS_REGISTRY_UNAVAILABLE: skills-registry.json is not available.');
2188
+ }
2189
+ const skills = registry.skills.map(normalizeSkillSummary);
2190
+ return {
2191
+ contents: [{
2192
+ uri: WCF_RESOURCE_URIS.skills,
2193
+ mimeType: 'application/json',
2194
+ text: JSON.stringify({ schemaVersion: registry.schemaVersion ?? 2, total: skills.length, skills }, null, 2),
2195
+ }],
2196
+ };
2197
+ },
2198
+ );
2199
+
2157
2200
  // -----------------------------------------------------------------------
2158
2201
  // Tool: get_design_system_overview
2159
2202
  // -----------------------------------------------------------------------
@@ -2249,6 +2292,10 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
2249
2292
  uri: WCF_RESOURCE_URIS.llmsFull,
2250
2293
  purpose: 'Full LLM reference text for WCF',
2251
2294
  },
2295
+ {
2296
+ uri: WCF_RESOURCE_URIS.skills,
2297
+ purpose: 'Skills catalog snapshot',
2298
+ },
2252
2299
  ],
2253
2300
  availableTools: [
2254
2301
  { name: 'get_design_system_overview', purpose: 'This overview (start here)' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoharada/wcf-mcp",
3
- "version": "0.8.0",
3
+ "version": "0.9.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": {
@@ -13,7 +13,6 @@
13
13
  "validator.mjs",
14
14
  "data/",
15
15
  "examples/",
16
- "plugins/",
17
16
  "wcf-mcp.config.example.json"
18
17
  ],
19
18
  "keywords": [
package/server.mjs CHANGED
@@ -25,6 +25,7 @@ const REPO_FILE_MAP = {
25
25
  'design-tokens.json': 'design-tokens.json',
26
26
  'guidelines-index.json': 'guidelines-index.json',
27
27
  'llms-full.txt': 'llms-full.txt',
28
+ 'skills-registry.json': 'registry/skills-registry.json',
28
29
  };
29
30
  export const DEFAULT_WCF_MCP_CONFIG = 'wcf-mcp.config.json';
30
31
 
@@ -3,9 +3,6 @@
3
3
  "guidelines-index.json": "./packages/mcp-server/data/guidelines-index.json"
4
4
  },
5
5
  "plugins": [
6
- {
7
- "module": "./packages/mcp-server/plugins/design-system-skills/index.mjs"
8
- },
9
6
  {
10
7
  "module": "./packages/mcp-server/examples/plugins/custom-validation-plugin.mjs"
11
8
  },
@@ -1,685 +0,0 @@
1
- /**
2
- * check_drift plugin tool for wcf-mcp.
3
- * Checks consistency across 4 data sources (CEM, install-registry,
4
- * skills-registry, pattern-registry) and detects drift (divergence).
5
- * Phase 1: JSON comparison only (no SKILL.md content analysis).
6
- * Plugin Contract v1.0+
7
- */
8
-
9
- import { access } from 'node:fs/promises';
10
- import { resolve } from 'node:path';
11
- import { REPO_ROOT, loadRegistry } from './shared.mjs';
12
-
13
- // ---------------------------------------------------------------------------
14
- // Helper utilities
15
- // ---------------------------------------------------------------------------
16
-
17
- /**
18
- * Simple string hash for generating drift IDs.
19
- * @param {string} str
20
- * @returns {string} 8-char hex string
21
- */
22
- function hashId(str) {
23
- let hash = 0;
24
- for (let i = 0; i < str.length; i++) {
25
- const char = str.charCodeAt(i);
26
- hash = ((hash << 5) - hash) + char;
27
- hash |= 0;
28
- }
29
- return Math.abs(hash).toString(16).padStart(8, '0').slice(0, 8);
30
- }
31
-
32
- /**
33
- * Generate a unique drift ID from rule + detail string.
34
- * @param {string} ruleId
35
- * @param {string} detail
36
- * @returns {string}
37
- */
38
- function driftId(ruleId, detail) {
39
- return `DRIFT-${ruleId}-${hashId(detail)}`;
40
- }
41
-
42
- /**
43
- * Create a drift report entry.
44
- * @param {string} ruleId
45
- * @param {string} severity HIGH | MEDIUM | LOW
46
- * @param {string} source Data source name
47
- * @param {string} target Target data source name
48
- * @param {string} message Human-readable description
49
- * @param {object} [details] Additional context
50
- * @returns {object}
51
- */
52
- function createDrift(ruleId, severity, source, target, message, details = {}) {
53
- return {
54
- id: driftId(ruleId, message),
55
- ruleId,
56
- severity,
57
- source,
58
- target,
59
- message,
60
- details,
61
- };
62
- }
63
-
64
- /**
65
- * Create a suggestion entry tied to a drift.
66
- * @param {string} id The drift ID this suggestion relates to
67
- * @param {string} action add | remove | update | document | investigate
68
- * @param {string} description Human-readable suggestion
69
- * @param {string} target File or data source to act on
70
- * @param {string} [priority] recommended | optional
71
- * @returns {object}
72
- */
73
- function createSuggestion(id, action, description, target, priority = 'recommended') {
74
- return { driftId: id, action, description, target, priority };
75
- }
76
-
77
- // ---------------------------------------------------------------------------
78
- // Data extraction helpers
79
- // ---------------------------------------------------------------------------
80
-
81
- /**
82
- * Extract custom-element-definition tags from CEM.
83
- * @param {object|null} cem
84
- * @returns {Map<string, string>} tagName -> modulePath
85
- */
86
- function extractCemTags(cem) {
87
- const tags = new Map();
88
- if (!cem?.modules) return tags;
89
- for (const mod of cem.modules) {
90
- if (!Array.isArray(mod.exports)) continue;
91
- for (const exp of mod.exports) {
92
- if (exp.kind === 'custom-element-definition' && exp.name) {
93
- tags.set(exp.name, mod.path);
94
- }
95
- }
96
- }
97
- return tags;
98
- }
99
-
100
- /**
101
- * Extract dads-* tag names from an HTML string.
102
- * @param {string} html
103
- * @returns {string[]} Unique sorted tag names
104
- */
105
- function extractDadsTags(html) {
106
- const matches = [...String(html).matchAll(/<(dads-[a-z][a-z0-9-]*)/g)];
107
- return [...new Set(matches.map((m) => m[1]))].sort();
108
- }
109
-
110
- // ---------------------------------------------------------------------------
111
- // Rule implementations
112
- // Each returns { drifts: DriftReport[], suggestions: DriftSuggestion[] }
113
- // ---------------------------------------------------------------------------
114
-
115
- /**
116
- * CIR01 - CEM component missing from install-registry.
117
- * For each CEM dads-* tag, verify it exists in install-registry tags.
118
- */
119
- function ruleCIR01(cemTags, irTags) {
120
- const drifts = [];
121
- const suggestions = [];
122
- for (const [tag] of cemTags) {
123
- if (!tag.startsWith('dads-')) continue;
124
- if (!(tag in irTags)) {
125
- const d = createDrift(
126
- 'CIR01',
127
- 'HIGH',
128
- 'custom-elements.json',
129
- 'install-registry.json',
130
- `CEM tag "${tag}" is missing from install-registry tags`,
131
- { tag },
132
- );
133
- drifts.push(d);
134
- suggestions.push(
135
- createSuggestion(d.id, 'add', `Add "${tag}" to install-registry tags section`, 'registry/install-registry.json'),
136
- );
137
- }
138
- }
139
- return { drifts, suggestions };
140
- }
141
-
142
- /**
143
- * CIR02 - Non-standard tags in CEM.
144
- * CEM exports with kind=custom-element-definition where name does NOT match dads-*.
145
- */
146
- function ruleCIR02(cemTags) {
147
- const drifts = [];
148
- const suggestions = [];
149
- for (const [tag, modulePath] of cemTags) {
150
- if (!tag.startsWith('dads-')) {
151
- const d = createDrift(
152
- 'CIR02',
153
- 'LOW',
154
- 'custom-elements.json',
155
- 'custom-elements.json',
156
- `CEM tag "${tag}" does not follow the dads-* naming convention`,
157
- { tag, modulePath },
158
- );
159
- drifts.push(d);
160
- suggestions.push(
161
- createSuggestion(d.id, 'investigate', `Verify if "${tag}" should follow the dads-* naming convention`, 'custom-elements.json', 'optional'),
162
- );
163
- }
164
- }
165
- return { drifts, suggestions };
166
- }
167
-
168
- /**
169
- * CIT01 - install-registry tag missing from CEM.
170
- * For each install-registry tags key, verify it exists in CEM exports.
171
- */
172
- function ruleCIT01(cemTags, irTags) {
173
- const drifts = [];
174
- const suggestions = [];
175
- for (const tag of Object.keys(irTags)) {
176
- if (!cemTags.has(tag)) {
177
- const d = createDrift(
178
- 'CIT01',
179
- 'HIGH',
180
- 'install-registry.json',
181
- 'custom-elements.json',
182
- `install-registry tag "${tag}" is not defined in CEM`,
183
- { tag, componentId: irTags[tag] },
184
- );
185
- drifts.push(d);
186
- suggestions.push(
187
- createSuggestion(d.id, 'remove', `Remove "${tag}" from install-registry or add its CEM definition`, 'registry/install-registry.json'),
188
- );
189
- }
190
- }
191
- return { drifts, suggestions };
192
- }
193
-
194
- /**
195
- * CIT02 - install-registry component tags mismatch with CEM.
196
- * For each install-registry component, compare its tags array with CEM-derived
197
- * tags for that component (by checking which CEM tags map to install-registry component IDs).
198
- */
199
- function ruleCIT02(cemTags, irTags, irComponents) {
200
- const drifts = [];
201
- const suggestions = [];
202
-
203
- // Build reverse map: componentId -> Set<tag> from irTags
204
- const tagsByComponent = {};
205
- for (const [tag, compId] of Object.entries(irTags)) {
206
- if (!tagsByComponent[compId]) tagsByComponent[compId] = new Set();
207
- tagsByComponent[compId].add(tag);
208
- }
209
-
210
- for (const [compId, comp] of Object.entries(irComponents)) {
211
- if (!Array.isArray(comp.tags)) continue;
212
- const declaredTags = new Set(comp.tags);
213
- const registeredTags = tagsByComponent[compId] ?? new Set();
214
-
215
- // Check tags in component.tags that are not in the tags section
216
- for (const tag of declaredTags) {
217
- if (!registeredTags.has(tag)) {
218
- const d = createDrift(
219
- 'CIT02',
220
- 'MEDIUM',
221
- 'install-registry.json',
222
- 'install-registry.json',
223
- `Component "${compId}" declares tag "${tag}" but it is not in the tags section`,
224
- { componentId: compId, tag },
225
- );
226
- drifts.push(d);
227
- suggestions.push(
228
- createSuggestion(d.id, 'update', `Add "${tag}" to install-registry tags section mapping to "${compId}"`, 'registry/install-registry.json'),
229
- );
230
- }
231
- }
232
-
233
- // Check tags in tags section that are not in component.tags
234
- for (const tag of registeredTags) {
235
- if (!declaredTags.has(tag)) {
236
- const d = createDrift(
237
- 'CIT02',
238
- 'MEDIUM',
239
- 'install-registry.json',
240
- 'install-registry.json',
241
- `Tag "${tag}" maps to "${compId}" in tags section but is not in component.tags`,
242
- { componentId: compId, tag },
243
- );
244
- drifts.push(d);
245
- suggestions.push(
246
- createSuggestion(d.id, 'update', `Add "${tag}" to component "${compId}" tags array`, 'registry/install-registry.json'),
247
- );
248
- }
249
- }
250
- }
251
- return { drifts, suggestions };
252
- }
253
-
254
- /**
255
- * IRD01 - Broken internal dependency in install-registry.
256
- * For each component's deps[], verify dep ID exists in components.
257
- */
258
- function ruleIRD01(irComponents) {
259
- const drifts = [];
260
- const suggestions = [];
261
- const componentIds = new Set(Object.keys(irComponents));
262
-
263
- for (const [compId, comp] of Object.entries(irComponents)) {
264
- if (!Array.isArray(comp.deps)) continue;
265
- for (const dep of comp.deps) {
266
- if (!componentIds.has(dep)) {
267
- const d = createDrift(
268
- 'IRD01',
269
- 'HIGH',
270
- 'install-registry.json',
271
- 'install-registry.json',
272
- `Component "${compId}" depends on "${dep}" which does not exist in components`,
273
- { componentId: compId, dependency: dep },
274
- );
275
- drifts.push(d);
276
- suggestions.push(
277
- createSuggestion(d.id, 'update', `Fix dependency "${dep}" in component "${compId}" or add the missing component`, 'registry/install-registry.json'),
278
- );
279
- }
280
- }
281
- }
282
- return { drifts, suggestions };
283
- }
284
-
285
- /**
286
- * IRT01 - install-registry tags/components inconsistency.
287
- * tags keys must map to valid component IDs, and component tags must be in tags section.
288
- */
289
- function ruleIRT01(irTags, irComponents) {
290
- const drifts = [];
291
- const suggestions = [];
292
- const componentIds = new Set(Object.keys(irComponents));
293
-
294
- // Check tags section: each value must be a valid component ID
295
- for (const [tag, compId] of Object.entries(irTags)) {
296
- if (!componentIds.has(compId)) {
297
- const d = createDrift(
298
- 'IRT01',
299
- 'HIGH',
300
- 'install-registry.json',
301
- 'install-registry.json',
302
- `Tag "${tag}" maps to component "${compId}" which does not exist`,
303
- { tag, componentId: compId },
304
- );
305
- drifts.push(d);
306
- suggestions.push(
307
- createSuggestion(d.id, 'update', `Fix tag "${tag}" mapping or add component "${compId}"`, 'registry/install-registry.json'),
308
- );
309
- }
310
- }
311
-
312
- // Check components section: each tag must be in tags section
313
- for (const [compId, comp] of Object.entries(irComponents)) {
314
- if (!Array.isArray(comp.tags)) continue;
315
- for (const tag of comp.tags) {
316
- if (!(tag in irTags)) {
317
- const d = createDrift(
318
- 'IRT01',
319
- 'HIGH',
320
- 'install-registry.json',
321
- 'install-registry.json',
322
- `Component "${compId}" tag "${tag}" is missing from tags section`,
323
- { componentId: compId, tag },
324
- );
325
- drifts.push(d);
326
- suggestions.push(
327
- createSuggestion(d.id, 'add', `Add "${tag}": "${compId}" to install-registry tags section`, 'registry/install-registry.json'),
328
- );
329
- }
330
- }
331
- }
332
- return { drifts, suggestions };
333
- }
334
-
335
- /**
336
- * CPR01 - pattern requires references missing component.
337
- * pattern.requires[] components must exist in install-registry components.
338
- */
339
- function ruleCPR01(patterns, irComponents) {
340
- const drifts = [];
341
- const suggestions = [];
342
- const componentIds = new Set(Object.keys(irComponents));
343
-
344
- for (const [patId, pat] of Object.entries(patterns)) {
345
- if (!Array.isArray(pat.requires)) continue;
346
- for (const req of pat.requires) {
347
- if (!componentIds.has(req)) {
348
- const d = createDrift(
349
- 'CPR01',
350
- 'HIGH',
351
- 'pattern-registry.json',
352
- 'install-registry.json',
353
- `Pattern "${patId}" requires component "${req}" which is not in install-registry`,
354
- { patternId: patId, requirement: req },
355
- );
356
- drifts.push(d);
357
- suggestions.push(
358
- createSuggestion(d.id, 'add', `Add component "${req}" to install-registry or remove it from pattern "${patId}" requires`, 'registry/install-registry.json'),
359
- );
360
- }
361
- }
362
- }
363
- return { drifts, suggestions };
364
- }
365
-
366
- /**
367
- * CPT01 - pattern HTML contains unknown CEM tags.
368
- * Extract dads-* tags from pattern HTML, verify each exists in CEM.
369
- */
370
- function ruleCPT01(patterns, cemTags) {
371
- const drifts = [];
372
- const suggestions = [];
373
-
374
- for (const [patId, pat] of Object.entries(patterns)) {
375
- if (!pat.html) continue;
376
- const tags = extractDadsTags(pat.html);
377
- for (const tag of tags) {
378
- if (!cemTags.has(tag)) {
379
- const d = createDrift(
380
- 'CPT01',
381
- 'HIGH',
382
- 'pattern-registry.json',
383
- 'custom-elements.json',
384
- `Pattern "${patId}" HTML uses tag "${tag}" which is not defined in CEM`,
385
- { patternId: patId, tag },
386
- );
387
- drifts.push(d);
388
- suggestions.push(
389
- createSuggestion(d.id, 'investigate', `Verify tag "${tag}" in pattern "${patId}" HTML or add its CEM definition`, 'registry/pattern-registry.json'),
390
- );
391
- }
392
- }
393
- }
394
- return { drifts, suggestions };
395
- }
396
-
397
- /**
398
- * CPT02 - pattern HTML uses tags not declared in requires.
399
- * dads-* tags in pattern HTML -> reverse lookup to component ID -> must be in pattern.requires.
400
- */
401
- function ruleCPT02(patterns, irTags) {
402
- const drifts = [];
403
- const suggestions = [];
404
-
405
- for (const [patId, pat] of Object.entries(patterns)) {
406
- if (!pat.html || !Array.isArray(pat.requires)) continue;
407
- const tags = extractDadsTags(pat.html);
408
- const requiresSet = new Set(pat.requires);
409
-
410
- for (const tag of tags) {
411
- const compId = irTags[tag];
412
- if (compId && !requiresSet.has(compId)) {
413
- const d = createDrift(
414
- 'CPT02',
415
- 'MEDIUM',
416
- 'pattern-registry.json',
417
- 'pattern-registry.json',
418
- `Pattern "${patId}" HTML uses tag "${tag}" (component "${compId}") but "${compId}" is not in requires`,
419
- { patternId: patId, tag, componentId: compId },
420
- );
421
- drifts.push(d);
422
- suggestions.push(
423
- createSuggestion(d.id, 'add', `Add "${compId}" to pattern "${patId}" requires array`, 'registry/pattern-registry.json'),
424
- );
425
- }
426
- }
427
- }
428
- return { drifts, suggestions };
429
- }
430
-
431
- /**
432
- * CPC01 - Pattern coverage of components.
433
- * Count how many install-registry components are referenced by at least one pattern's requires.
434
- */
435
- function ruleCPC01(patterns, irComponents) {
436
- const drifts = [];
437
- const suggestions = [];
438
- const componentIds = Object.keys(irComponents);
439
- if (componentIds.length === 0) return { drifts, suggestions };
440
-
441
- // Collect all component IDs referenced by any pattern
442
- const coveredComponents = new Set();
443
- for (const pat of Object.values(patterns)) {
444
- if (!Array.isArray(pat.requires)) continue;
445
- for (const req of pat.requires) {
446
- coveredComponents.add(req);
447
- }
448
- }
449
-
450
- const totalComponents = componentIds.length;
451
- const uncoveredIds = [];
452
- for (const id of componentIds) {
453
- if (!coveredComponents.has(id)) uncoveredIds.push(id);
454
- }
455
- const coveredCount = totalComponents - uncoveredIds.length;
456
- const coveragePercent = Math.round((coveredCount / totalComponents) * 100);
457
- const severity = coveragePercent < 50 ? 'MEDIUM' : 'LOW';
458
-
459
- if (uncoveredIds.length > 0) {
460
- const d = createDrift(
461
- 'CPC01',
462
- severity,
463
- 'pattern-registry.json',
464
- 'install-registry.json',
465
- `Pattern coverage: ${coveredCount}/${totalComponents} components (${coveragePercent}%). ${uncoveredIds.length} components not used in any pattern.`,
466
- {
467
- coverage: coveragePercent,
468
- coveredCount,
469
- totalComponents,
470
- uncoveredComponents: uncoveredIds,
471
- },
472
- );
473
- drifts.push(d);
474
- suggestions.push(
475
- createSuggestion(d.id, 'document', `Consider adding patterns for uncovered components: ${uncoveredIds.slice(0, 5).join(', ')}${uncoveredIds.length > 5 ? '...' : ''}`, 'registry/pattern-registry.json', 'optional'),
476
- );
477
- }
478
- return { drifts, suggestions };
479
- }
480
-
481
- /**
482
- * SIR01 - skills-registry path existence.
483
- * For each skill, verify skill.path + '/' + skill.entry file exists on disk.
484
- */
485
- async function ruleSIR01(skillsRegistry) {
486
- const drifts = [];
487
- const suggestions = [];
488
- if (!skillsRegistry?.skills) return { drifts, suggestions };
489
-
490
- for (const skill of skillsRegistry.skills) {
491
- const entry = skill.entry ?? 'SKILL.md';
492
- const fullPath = resolve(REPO_ROOT, skill.path, entry);
493
- try {
494
- await access(fullPath);
495
- } catch {
496
- const d = createDrift(
497
- 'SIR01',
498
- 'HIGH',
499
- 'skills-registry.json',
500
- 'filesystem',
501
- `Skill "${skill.name}" entry file not found at "${skill.path}/${entry}"`,
502
- { skillName: skill.name, path: skill.path, entry, expected: fullPath },
503
- );
504
- drifts.push(d);
505
- suggestions.push(
506
- createSuggestion(d.id, 'add', `Create "${entry}" at "${skill.path}/" or update the skill registry path`, 'registry/skills-registry.json'),
507
- );
508
- }
509
- }
510
- return { drifts, suggestions };
511
- }
512
-
513
- /**
514
- * SID01 - Skills dependency existence (v2 only).
515
- * Each skill.dependencies[] name must exist as another skill.name in the registry.
516
- */
517
- function ruleSID01(skillsRegistry) {
518
- const drifts = [];
519
- const suggestions = [];
520
- if (!skillsRegistry?.skills) return { drifts, suggestions };
521
-
522
- const skillNames = new Set(skillsRegistry.skills.map((s) => s.name));
523
-
524
- for (const skill of skillsRegistry.skills) {
525
- if (!Array.isArray(skill.dependencies)) continue;
526
- for (const dep of skill.dependencies) {
527
- const depName = typeof dep === 'string' ? dep : dep?.name;
528
- if (depName && !skillNames.has(depName)) {
529
- const d = createDrift(
530
- 'SID01',
531
- 'HIGH',
532
- 'skills-registry.json',
533
- 'skills-registry.json',
534
- `Skill "${skill.name}" depends on "${depName}" which does not exist in the registry`,
535
- { skillName: skill.name, dependency: depName },
536
- );
537
- drifts.push(d);
538
- suggestions.push(
539
- createSuggestion(d.id, 'add', `Add skill "${depName}" to the registry or remove the dependency from "${skill.name}"`, 'registry/skills-registry.json'),
540
- );
541
- }
542
- }
543
- }
544
- return { drifts, suggestions };
545
- }
546
-
547
- // ---------------------------------------------------------------------------
548
- // Scope mapping
549
- // ---------------------------------------------------------------------------
550
-
551
- const SCOPE_RULES = {
552
- all: ['CIR01', 'CIR02', 'CIT01', 'CIT02', 'IRD01', 'IRT01', 'CPR01', 'CPT01', 'CPT02', 'CPC01', 'SIR01', 'SID01'],
553
- cem: ['CIR01', 'CIR02', 'CIT01', 'CIT02', 'IRD01', 'IRT01'],
554
- skills: ['SIR01', 'SID01'],
555
- tokens: [], // Reserved for future Phase 2
556
- patterns: ['CPR01', 'CPT01', 'CPT02', 'CPC01'],
557
- };
558
-
559
- // ---------------------------------------------------------------------------
560
- // Main handler
561
- // ---------------------------------------------------------------------------
562
-
563
- /**
564
- * @param {object} args
565
- * @param {{ helpers: { loadJsonData: Function, buildJsonToolResponse: Function } }} ctx
566
- */
567
- async function checkDriftHandler(args, { helpers }) {
568
- const scope = args?.scope ?? 'all';
569
- const activeRules = new Set(SCOPE_RULES[scope] ?? SCOPE_RULES.all);
570
- const drifts = [];
571
- const suggestions = [];
572
- const rulesExecuted = [];
573
-
574
- // -----------------------------------------------------------------------
575
- // Load data sources
576
- // -----------------------------------------------------------------------
577
- const [cem, ir, pr, sr] = await Promise.all([
578
- helpers.loadJsonData('custom-elements.json'),
579
- helpers.loadJsonData('install-registry.json'),
580
- helpers.loadJsonData('pattern-registry.json'),
581
- loadRegistry(),
582
- ]);
583
-
584
- // -----------------------------------------------------------------------
585
- // Extract commonly used data
586
- // -----------------------------------------------------------------------
587
- const cemTags = extractCemTags(cem);
588
- const irTags = ir?.tags ?? {};
589
- const irComponents = ir?.components ?? {};
590
- const patterns = pr?.patterns ?? {};
591
-
592
- // -----------------------------------------------------------------------
593
- // Execute rules based on scope
594
- // -----------------------------------------------------------------------
595
-
596
- /** Collect results from a synchronous rule function */
597
- function runSync(ruleId, fn) {
598
- if (!activeRules.has(ruleId)) return;
599
- rulesExecuted.push(ruleId);
600
- const result = fn();
601
- drifts.push(...result.drifts);
602
- suggestions.push(...result.suggestions);
603
- }
604
-
605
- /** Collect results from an async rule function */
606
- async function runAsync(ruleId, fn) {
607
- if (!activeRules.has(ruleId)) return;
608
- rulesExecuted.push(ruleId);
609
- const result = await fn();
610
- drifts.push(...result.drifts);
611
- suggestions.push(...result.suggestions);
612
- }
613
-
614
- // CEM <-> install-registry rules
615
- runSync('CIR01', () => ruleCIR01(cemTags, irTags));
616
- runSync('CIR02', () => ruleCIR02(cemTags));
617
- runSync('CIT01', () => ruleCIT01(cemTags, irTags));
618
- runSync('CIT02', () => ruleCIT02(cemTags, irTags, irComponents));
619
- runSync('IRD01', () => ruleIRD01(irComponents));
620
- runSync('IRT01', () => ruleIRT01(irTags, irComponents));
621
-
622
- // Pattern rules
623
- runSync('CPR01', () => ruleCPR01(patterns, irComponents));
624
- runSync('CPT01', () => ruleCPT01(patterns, cemTags));
625
- runSync('CPT02', () => ruleCPT02(patterns, irTags));
626
- runSync('CPC01', () => ruleCPC01(patterns, irComponents));
627
-
628
- // Skills rules — require skills-registry for scopes that include skill rules
629
- const hasSkillRules = ['SIR01', 'SID01'].some((id) => activeRules.has(id));
630
- if (hasSkillRules && !sr) {
631
- const d = createDrift(
632
- 'SIR01',
633
- 'HIGH',
634
- 'skills-registry.json',
635
- 'filesystem',
636
- 'skills-registry.json is missing or corrupted — all skill rules skipped',
637
- {},
638
- );
639
- drifts.push(d);
640
- suggestions.push(
641
- createSuggestion(d.id, 'add', 'Create or restore registry/skills-registry.json', 'registry/skills-registry.json'),
642
- );
643
- rulesExecuted.push(...['SIR01', 'SID01'].filter((id) => activeRules.has(id)));
644
- } else {
645
- await runAsync('SIR01', () => ruleSIR01(sr));
646
- runSync('SID01', () => ruleSID01(sr));
647
- }
648
-
649
- // -----------------------------------------------------------------------
650
- // Build output
651
- // -----------------------------------------------------------------------
652
- const summary = { total: drifts.length, high: 0, medium: 0, low: 0, ignored: 0 };
653
- for (const d of drifts) {
654
- if (d.severity === 'HIGH') summary.high++;
655
- else if (d.severity === 'MEDIUM') summary.medium++;
656
- else if (d.severity === 'LOW') summary.low++;
657
- }
658
-
659
- const meta = {
660
- phase: 1,
661
- scope,
662
- rulesExecuted,
663
- timestamp: new Date().toISOString(),
664
- };
665
-
666
- return helpers.buildJsonToolResponse({ drifts, suggestions, summary, meta });
667
- }
668
-
669
- // ---------------------------------------------------------------------------
670
- // Plugin export
671
- // ---------------------------------------------------------------------------
672
-
673
- export default {
674
- name: 'design-system-skills-drift',
675
- version: '1.0.0',
676
- tools: [
677
- {
678
- name: 'check_drift',
679
- description:
680
- 'Check consistency across CEM, install-registry, skills-registry, and pattern-registry. Detects drift (divergence) between data sources. When: before PR, after registry updates, periodic audits. Returns: {drifts[], suggestions[], summary{total,high,medium,low,ignored}, meta{phase,scope,rulesExecuted,timestamp}}. Args: scope? (all|cem|skills|tokens|patterns, default: all). Phase 1: 12 JSON comparison rules.',
681
- inputSchema: {},
682
- handler: checkDriftHandler,
683
- },
684
- ],
685
- };
@@ -1,193 +0,0 @@
1
- /**
2
- * get_skill_manifest plugin tool for wcf-mcp.
3
- * Returns skill metadata (manifest) from skills-registry v2.
4
- * Supports metadata-only, full content, and section extraction modes.
5
- * Plugin Contract v1.0+ (content modes require v1.1+)
6
- */
7
-
8
- import { readFile } from 'node:fs/promises';
9
- import { resolve } from 'node:path';
10
- import {
11
- REPO_ROOT,
12
- loadRegistry,
13
- normalizeSkillEntry,
14
- buildErrorResponse,
15
- } from './shared.mjs';
16
-
17
- /** Maximum safe response size in bytes (95KB) */
18
- const MAX_SAFE_RESPONSE = 95 * 1024;
19
-
20
- /**
21
- * Normalize a heading text to a slug for section matching.
22
- * @param {string} text
23
- * @returns {string}
24
- */
25
- function normalizeHeading(text) {
26
- return text
27
- .trim()
28
- .toLowerCase()
29
- .replace(/[^a-z0-9\s_]/g, '')
30
- .replace(/\s+/g, '_');
31
- }
32
-
33
- /**
34
- * Detect sections (H1/H2 headings) from markdown content.
35
- * @param {string} content
36
- * @returns {{ sections: Array<{level: number, text: string, normalized: string, line: number}>, lines: string[] }}
37
- */
38
- function parseMarkdownSections(content) {
39
- const lines = content.split('\n');
40
- const sections = [];
41
- for (let i = 0; i < lines.length; i++) {
42
- const match = lines[i].match(/^(#{1,2})\s+(.+)/);
43
- if (match) {
44
- sections.push({
45
- level: match[1].length,
46
- text: match[2].trim(),
47
- normalized: normalizeHeading(match[2]),
48
- line: i,
49
- });
50
- }
51
- }
52
- return { sections, lines };
53
- }
54
-
55
- /**
56
- * Extract a specific section from parsed markdown by normalized heading name.
57
- * @param {{ sections: Array<{level: number, normalized: string, line: number}>, lines: string[] }} parsed
58
- * @param {string} sectionName
59
- * @returns {string|null}
60
- */
61
- function extractSection(parsed, sectionName) {
62
- const idx = parsed.sections.findIndex((s) =>
63
- s.normalized === sectionName,
64
- );
65
- if (idx === -1) return null;
66
-
67
- const startLine = parsed.sections[idx].line;
68
- const startLevel = parsed.sections[idx].level;
69
- const nextSection = parsed.sections
70
- .slice(idx + 1)
71
- .find((s) => s.level <= startLevel);
72
- const endLine = nextSection ? nextSection.line : parsed.lines.length;
73
-
74
- return parsed.lines.slice(startLine, endLine).join('\n').trim();
75
- }
76
-
77
- export default {
78
- name: 'design-system-skills-manifest',
79
- version: '1.0.0',
80
- tools: [
81
- {
82
- name: 'get_skill_manifest',
83
- description:
84
- 'Return skill metadata (manifest) from skills-registry v2. When: retrieving full skill details, reading SKILL.md content, extracting specific sections. Returns: {manifest, content?, section_content?, content_size?, available_sections?, truncated?}. After: use skill content to guide implementation. Args: skill_id (required, e.g. "css-writing-rules"), include_content? (boolean, requires Contract v1.1+), section? (overview|workflow|do_dont|references|quick_reference|token_architecture|error_contract|procedure).',
85
- inputSchema: {},
86
- async handler(args, { helpers }) {
87
- // 1. Load registry
88
- const registry = await loadRegistry();
89
- if (!registry || !Array.isArray(registry.skills)) {
90
- return buildErrorResponse(
91
- 'SKILLS_REGISTRY_UNAVAILABLE',
92
- 'Skills registry data not available.',
93
- );
94
- }
95
-
96
- // 2. Find skill by name
97
- const { skill_id, include_content, section } = args;
98
- const skill = registry.skills.find((s) => s.name === skill_id);
99
- if (!skill) {
100
- return buildErrorResponse(
101
- 'SKILL_NOT_FOUND',
102
- `Skill '${skill_id}' not found in registry.`,
103
- );
104
- }
105
-
106
- // 3. Build manifest with v2 defaults
107
- const manifest = normalizeSkillEntry(skill);
108
-
109
- // 4. Mode 1: Metadata Only
110
- const wantsContent = include_content === true;
111
- const wantsSection = typeof section === 'string' && section.length > 0;
112
-
113
- if (!wantsContent && !wantsSection) {
114
- return helpers.buildJsonToolResponse({ manifest });
115
- }
116
-
117
- // 5. Modes 2 & 3 require loadTextData (Contract v1.1+)
118
- if (typeof helpers.loadTextData !== 'function') {
119
- return buildErrorResponse(
120
- 'CONTRACT_VERSION_ERROR',
121
- 'Content retrieval requires Contract v1.1+. loadTextData helper is not available.',
122
- );
123
- }
124
-
125
- // 6. Read SKILL.md from disk
126
- const skillFilePath = resolve(REPO_ROOT, skill.path ?? '', skill.entry ?? 'SKILL.md');
127
- let fileContent;
128
- try {
129
- fileContent = await readFile(skillFilePath, 'utf-8');
130
- } catch {
131
- const relPath = `${skill.path ?? ''}/${skill.entry ?? 'SKILL.md'}`;
132
- return buildErrorResponse(
133
- 'SKILL_CONTENT_NOT_FOUND',
134
- `SKILL.md not found at ${relPath}`,
135
- );
136
- }
137
-
138
- // 7. Parse sections once (reused for detection and extraction)
139
- const parsed = parseMarkdownSections(fileContent);
140
- const available_sections = parsed.sections.map((s) => s.normalized);
141
-
142
- // 8. Build response payload
143
- const payload = { manifest };
144
- let totalContentSize = 0;
145
-
146
- // Mode 2: Full content
147
- if (wantsContent) {
148
- const contentBytes = Buffer.byteLength(fileContent, 'utf8');
149
- totalContentSize += contentBytes;
150
- payload.content = fileContent;
151
- payload.content_size = contentBytes;
152
- }
153
-
154
- // Mode 3: Section extraction
155
- if (wantsSection) {
156
- const sectionContent = extractSection(parsed, section);
157
- if (sectionContent === null) {
158
- return buildErrorResponse(
159
- 'SECTION_NOT_FOUND',
160
- `Section '${section}' not found in ${skill_id} SKILL.md. Available: [${available_sections.join(', ')}]`,
161
- );
162
- }
163
- const sectionBytes = Buffer.byteLength(sectionContent, 'utf8');
164
- totalContentSize += sectionBytes;
165
- payload.section_content = sectionContent;
166
- payload.content_size = payload.content_size ?? sectionBytes;
167
- }
168
-
169
- payload.available_sections = available_sections;
170
-
171
- // 9. Truncation check
172
- const responseJson = JSON.stringify(payload);
173
- const responseBytes = Buffer.byteLength(responseJson, 'utf8');
174
- if (responseBytes > MAX_SAFE_RESPONSE) {
175
- if (payload.content) {
176
- const overhead = responseBytes - totalContentSize;
177
- const availableBytes = MAX_SAFE_RESPONSE - overhead - 200;
178
- // Estimate char limit from byte ratio
179
- const ratio = Math.max(0, availableBytes) / Buffer.byteLength(payload.content, 'utf8');
180
- const charLimit = Math.floor(payload.content.length * Math.min(1, ratio));
181
- payload.content = payload.content.slice(0, charLimit);
182
- payload.content_size = Buffer.byteLength(payload.content, 'utf8');
183
- }
184
- payload.truncated = true;
185
- } else {
186
- payload.truncated = false;
187
- }
188
-
189
- return helpers.buildJsonToolResponse(payload);
190
- },
191
- },
192
- ],
193
- };
@@ -1,20 +0,0 @@
1
- /**
2
- * Design System Skills plugin bundle for wcf-mcp.
3
- * Aggregates list_skills, get_skill_manifest, and check_drift tools.
4
- */
5
-
6
- import listSkillsPlugin from './list-skills.mjs';
7
- import getSkillManifestPlugin from './get-skill-manifest.mjs';
8
- import checkDriftPlugin from './check-drift.mjs';
9
-
10
- const allTools = [
11
- ...listSkillsPlugin.tools,
12
- ...getSkillManifestPlugin.tools,
13
- ...checkDriftPlugin.tools,
14
- ];
15
-
16
- export default {
17
- name: 'design-system-skills',
18
- version: '1.0.0',
19
- tools: allTools,
20
- };
@@ -1,78 +0,0 @@
1
- /**
2
- * list_skills plugin tool for wcf-mcp.
3
- * Lists registered skills from skills-registry.json with filtering support.
4
- * Plugin Contract v1.0+
5
- */
6
-
7
- import { loadRegistry, normalizeSkillEntry, buildErrorResponse } from './shared.mjs';
8
-
9
- /**
10
- * Project a full skill entry to a lightweight summary (excludes compat/manifest).
11
- */
12
- function toSummary(skill) {
13
- const { compat, manifest, ...summary } = normalizeSkillEntry(skill);
14
- return summary;
15
- }
16
-
17
- export default {
18
- name: 'design-system-skills-list',
19
- version: '1.0.0',
20
- tools: [
21
- {
22
- name: 'list_skills',
23
- description:
24
- 'List registered Claude Code / Cursor / Codex skills from skills-registry.json. When: discovering available skills, checking skill metadata, filtering skills by client/tags. Returns: {total, skills[]} where each skill has name, description, status, clients, tags, version, dependencies, path, entry. After: use get_skill_manifest for full content. Args: client? (claude_code|cursor|codex), tags? (spec|token|audit|drift|workflow, AND logic), status? (active|deprecated|experimental), query? (free-text substring search).',
25
- inputSchema: {},
26
- async handler(args = {}, { helpers }) {
27
- const registry = await loadRegistry();
28
- if (!registry || !Array.isArray(registry.skills)) {
29
- return buildErrorResponse(
30
- 'SKILLS_REGISTRY_UNAVAILABLE',
31
- 'Skills registry data not available.',
32
- );
33
- }
34
-
35
- const { client, tags, status, query } = args;
36
- let matched = registry.skills;
37
-
38
- // Filter by status
39
- if (status) {
40
- matched = matched.filter(
41
- (s) => String(s.status ?? 'active') === status,
42
- );
43
- }
44
-
45
- // Filter by client
46
- if (client) {
47
- matched = matched.filter(
48
- (s) => Array.isArray(s.clients) && s.clients.includes(client),
49
- );
50
- }
51
-
52
- // Filter by tags (AND logic)
53
- if (Array.isArray(tags) && tags.length > 0) {
54
- matched = matched.filter((s) => {
55
- const skillTags = Array.isArray(s.tags) ? s.tags : [];
56
- return tags.every((t) => skillTags.includes(t));
57
- });
58
- }
59
-
60
- // Filter by free-text query
61
- if (typeof query === 'string' && query.trim() !== '') {
62
- const q = query.toLowerCase();
63
- matched = matched.filter((s) => {
64
- const name = String(s.name ?? '').toLowerCase();
65
- const desc = String(s.description ?? '').toLowerCase();
66
- return name.includes(q) || desc.includes(q);
67
- });
68
- }
69
-
70
- const skills = matched.map(toSummary);
71
- return helpers.buildJsonToolResponse({
72
- total: skills.length,
73
- skills,
74
- });
75
- },
76
- },
77
- ],
78
- };
@@ -1,75 +0,0 @@
1
- /**
2
- * Shared utilities for design-system-skills plugin.
3
- * Single source of truth for registry loading, defaults, and error responses.
4
- */
5
-
6
- import { readFile } from 'node:fs/promises';
7
- import { dirname, resolve } from 'node:path';
8
- import { fileURLToPath } from 'node:url';
9
-
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = dirname(__filename);
12
-
13
- export const REPO_ROOT = resolve(__dirname, '..', '..', '..', '..');
14
- export const REGISTRY_PATH = resolve(REPO_ROOT, 'registry', 'skills-registry.json');
15
-
16
- /** v2 defaults for v1 skill entries */
17
- export const V2_DEFAULTS = {
18
- tags: [],
19
- version: '0.0.0',
20
- dependencies: [],
21
- compat: {},
22
- manifest: {},
23
- };
24
-
25
- /**
26
- * Load skills registry from disk.
27
- * @returns {Promise<object|null>}
28
- */
29
- export async function loadRegistry() {
30
- try {
31
- const raw = await readFile(REGISTRY_PATH, 'utf-8');
32
- return JSON.parse(raw);
33
- } catch {
34
- return null;
35
- }
36
- }
37
-
38
- /**
39
- * Normalize a raw skill entry from the registry, applying v2 defaults.
40
- * @param {object} skill - Raw skill entry
41
- * @returns {object} Normalized skill fields
42
- */
43
- export function normalizeSkillEntry(skill) {
44
- return {
45
- name: skill.name,
46
- description: skill.description ?? '',
47
- status: skill.status ?? 'active',
48
- path: skill.path ?? '',
49
- entry: skill.entry ?? 'SKILL.md',
50
- clients: Array.isArray(skill.clients) ? skill.clients : [],
51
- tags: Array.isArray(skill.tags) ? skill.tags : V2_DEFAULTS.tags,
52
- version: typeof skill.version === 'string' ? skill.version : V2_DEFAULTS.version,
53
- dependencies: Array.isArray(skill.dependencies) ? skill.dependencies : V2_DEFAULTS.dependencies,
54
- compat: skill.compat ?? V2_DEFAULTS.compat,
55
- manifest: skill.manifest ?? V2_DEFAULTS.manifest,
56
- };
57
- }
58
-
59
- /**
60
- * Build an MCP error response.
61
- * @param {string} code
62
- * @param {string} message
63
- * @returns {object}
64
- */
65
- export function buildErrorResponse(code, message) {
66
- return {
67
- content: [
68
- {
69
- type: 'text',
70
- text: JSON.stringify({ error: { code, message } }, null, 2),
71
- },
72
- ],
73
- isError: true,
74
- };
75
- }