@monoharada/wcf-mcp 0.9.1 → 0.11.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
- ## 提供機能(16 tools + 1 prompt + 5 resources)
81
+ ## 提供機能(19 tools + 2 prompts + 5 resources)
82
82
 
83
83
  ### ガードレール
84
84
 
@@ -101,6 +101,11 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
101
101
  | ツール | 説明 |
102
102
  |--------|------|
103
103
  | `validate_markup` | HTML スニペットを検証し、セマンティック検証(下表)で `suggestion` 付き診断を返す |
104
+ | `validate_files` | 複数のマークアップファイルをまとめて検証し、ファイル別診断と集計を返す |
105
+ | `validate_project` | ディレクトリを走査し、include/exclude glob に一致する複数ファイルをまとめて検証する |
106
+
107
+ `validate_*` 系は common template syntax(`{{ }}`, `{% %}`, `<% %>`, `<? ?>`, script/style blocks)をそのまま HTML と誤認しないようにマスクして検証します。
108
+ `validate_project` の既定 include は `*.html`, `*.htm`, `*.njk`, `*.liquid`, `*.astro`, `*.twig`, `*.hbs` です。
104
109
 
105
110
  #### validate_markup 検出コード一覧
106
111
 
@@ -119,6 +124,7 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
119
124
  | `roleAlertNotRecommended` | warning | `role="alert"` の使用(DADS 非推奨) | `role="alert"` |
120
125
  | `emptyLabel` | error | 空の `label` 属性(アクセシビリティ違反) | `<dads-input-text label="">` |
121
126
  | `emptyAriaLabel` | error | 空の `aria-label` 属性(アクセシビリティ違反) | `<dads-button aria-label="">` |
127
+ | `duplicateId` | error | 同一ドキュメント内で `id` が重複している | `<div id="hero">...</div><section id="hero">...</section>` |
122
128
  | `forbiddenAttribute` | warning | 禁止属性 | `placeholder` |
123
129
 
124
130
  ### UI パターン
@@ -140,10 +146,11 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
140
146
 
141
147
  | ツール | 説明 |
142
148
  |--------|------|
143
- | `get_design_tokens` | デザイントークンを type/category/query/theme で検索(`theme=light` のみ。`dark/all` はエラー) |
149
+ | `get_design_tokens` | デザイントークンを type/category/query/theme で検索(現状データは `light` のみ。`all` は利用可能テーマ全体、`dark` は非サポート) |
144
150
  | `get_design_token_detail` | 単一トークンの詳細(references/referencedBy/relatedTokens/usageExamples)を取得 |
145
151
  | `get_accessibility_docs` | component/topic/wcagLevel で A11y チェックリストとガイドライン要点を検索(`topic=all` では両ソースを混在返却) |
146
152
  | `search_guidelines` | ガイドライン(topic/query)をスコア付きで検索 |
153
+ | `search_design_system_knowledge` | components/patterns/guidelines/tokens/skills を横断して検索し、exact/prefix/intent-aware ranking と source ごとの follow-up 導線を返す |
147
154
 
148
155
  #### `get_design_tokens` の使用例
149
156
 
@@ -226,12 +233,14 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
226
233
  }
227
234
  ```
228
235
 
229
- > **注意**: `theme` `"light"` のみサポート。`"dark"` `"all"` を指定すると `INVALID_THEME` エラーが返ります。
236
+ > **注意**: 現在の実データは `light` のみです。`theme="all"` は利用可能テーマ全体として `light` を返します。`theme="dark"` `INVALID_THEME` エラーです。
237
+ > `var(--token, #fff)` のような literal fallback は relationship graph には含めません。`var(--token-a, var(--token-b))` のような token fallback のみ参照関係として扱います。
230
238
 
231
239
  ### Prompt
232
240
 
233
241
  | 名前 | 説明 |
234
242
  |------|------|
243
+ | `build_page` | パターンID またはコンポーネントリストから no-build HTML ページを構築するガイド付きプロンプト |
235
244
  | `figma_to_wcf` | Figma URL を入力に、`overview → tokens → component api → snippet → validate` の実行順を返す |
236
245
 
237
246
  ### Resources (`wcf://`)
@@ -336,13 +345,20 @@ npx @monoharada/wcf-mcp --transport=http --port=3100
336
345
  詳細仕様: [docs/plugin-contract-v1.md](../../docs/plugin-contract-v1.md)
337
346
 
338
347
  - `plugins[].name` / `plugins[].version` は必須
348
+ - `plugins[].validators` を使うと `validate_markup` / `validate_files` / `validate_project` に独自診断を差し込めます
349
+ - `plugins[].prompts` / `plugins[].resources` を使うと MCP prompt / resource を追加できます
350
+ - `plugins[].resourceTemplates` を使うと MCP resource template も追加できます
339
351
  - tool 名は組み込みツール名と重複不可(例: `list_components` など)
352
+ - prompt の `argsSchema` は plain shape に加えて zod object shape も使えます
340
353
  - `dataSources` で差し替え可能な key は次のみ
341
354
  - `custom-elements.json`
342
355
  - `install-registry.json`
343
356
  - `pattern-registry.json`
357
+ - `component-selector-guide.json`
344
358
  - `design-tokens.json`
345
359
  - `guidelines-index.json`
360
+ - `skills-registry.json`
361
+ - `llms-full.txt`
346
362
 
347
363
  ## structuredContent rollback
348
364
 
@@ -636,8 +652,13 @@ CEM やレジストリを更新した後:
636
652
  ```bash
637
653
  npm run mcp:build # データファイルをパッケージにコピー
638
654
  npm run mcp:check # データが最新かチェック(CI用)
655
+ npm run mcp:summary # MCP inventory + quality summary(JSON)を生成
656
+ npm run mcp:summary:check # summary JSON の drift をチェック
639
657
  ```
640
658
 
659
+ - machine-readable summary: `packages/mcp-server/mcp-spec-test/summary/v3-final.json`
660
+ - `npm run mcp:check:response-size` は human-readable stdout、`node scripts/mcp/check-response-size.mjs --json` は JSON 出力に使えます
661
+
641
662
  ### パッケージ構成
642
663
 
643
664
  ```
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
  }
@@ -13,7 +13,7 @@ export const CANONICAL_PREFIX = 'dads';
13
13
  export const MAX_PREFIX_LENGTH = 64;
14
14
  export const STRUCTURED_CONTENT_DISABLE_FLAG = 'WCF_MCP_DISABLE_STRUCTURED_CONTENT';
15
15
  export const MAX_TOOL_RESULT_BYTES = 100 * 1024;
16
- export const PLUGIN_TOOL_NOTICE = 'Plugin tool (contract v1.1).';
16
+ export const PLUGIN_TOOL_NOTICE = 'Plugin tool (contract v1.4).';
17
17
  export const PACKAGE_VERSION = String(packageMeta?.version ?? '0.0.0');
18
18
 
19
19
  export const CATEGORY_MAP = {
@@ -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,53 @@ 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_pattern_recipe({ patternId: "<id>", include: ["fullPage"] })',
119
+ ' → Returns a complete <!DOCTYPE html> page in one call',
120
+ '2. validate_markup({ html: "<the full page HTML>" }) → Validate the full page (catches missing importmap / boot script)',
121
+ '3. Save the fullPageHtml result to a file and serve via HTTP',
122
+ '',
123
+ `## Available Pattern IDs (${patternIds.length})`,
124
+ patternIds.length > 0 ? patternIds.join(', ') : '(none)',
125
+ '',
126
+ `## Available Component IDs (${componentIds.length})`,
127
+ componentIds.length > 0 ? componentIds.join(', ') : '(none)',
128
+ '',
129
+ '## Custom Page Construction',
130
+ '1. generate_usage_snippet({ component: "<componentId>" }) → Get HTML for each component',
131
+ '2. generate_full_page_html({ html: "<combined fragments>" }) → Wrap into a complete page',
132
+ '3. validate_markup({ html: "<the full page HTML>" }) → Validate the full page (catches missing importmap / boot script)',
133
+ '',
134
+ '## Important Notes',
135
+ '- No CDN exists. All files are served from a local vendor directory.',
136
+ `- CLI setup: npm install web-components-factory → npx wcf init --prefix ${prefix}`,
137
+ '- file:// protocol does not work. An HTTP server is required.',
138
+ '',
139
+ '## Prompts',
140
+ '- build_page: Guided prompt for building a no-build HTML page',
141
+ '- figma_to_wcf: Convert Figma designs to WCF implementation',
142
+ ].join('\n');
143
+ }
144
+
97
145
  export const IDE_SETUP_TEMPLATES = Object.freeze([
98
146
  {
99
147
  ide: 'Claude Desktop',
package/core/plugins.mjs CHANGED
@@ -5,17 +5,20 @@
5
5
  import { z } from 'zod';
6
6
  import { PLUGIN_TOOL_NOTICE } from './constants.mjs';
7
7
 
8
- export const PLUGIN_CONTRACT_VERSION = '1.1.0';
8
+ export const PLUGIN_CONTRACT_VERSION = '1.4.0';
9
9
 
10
10
  // Single-module constants (DD-14)
11
11
  const PLUGIN_DATA_SOURCE_KEYS = Object.freeze(new Set([
12
12
  'custom-elements.json',
13
13
  'install-registry.json',
14
14
  'pattern-registry.json',
15
+ 'component-selector-guide.json',
15
16
  'design-tokens.json',
16
17
  'guidelines-index.json',
18
+ 'skills-registry.json',
19
+ 'llms-full.txt',
17
20
  ]));
18
- const BUILTIN_TOOL_NAMES = Object.freeze(new Set([
21
+ export const BUILTIN_TOOL_NAMES = Object.freeze(new Set([
19
22
  'get_design_system_overview',
20
23
  'list_components',
21
24
  'search_icons',
@@ -23,6 +26,8 @@ const BUILTIN_TOOL_NAMES = Object.freeze(new Set([
23
26
  'generate_usage_snippet',
24
27
  'get_install_recipe',
25
28
  'validate_markup',
29
+ 'validate_files',
30
+ 'validate_project',
26
31
  'list_patterns',
27
32
  'get_pattern_recipe',
28
33
  'generate_pattern_snippet',
@@ -30,9 +35,24 @@ const BUILTIN_TOOL_NAMES = Object.freeze(new Set([
30
35
  'get_design_token_detail',
31
36
  'get_accessibility_docs',
32
37
  'search_guidelines',
38
+ 'search_design_system_knowledge',
33
39
  'generate_full_page_html',
34
40
  'get_component_selector_guide',
35
41
  ]));
42
+ const BUILTIN_PROMPT_NAMES = Object.freeze(new Set([
43
+ 'figma_to_wcf',
44
+ 'build_page',
45
+ ]));
46
+ const BUILTIN_RESOURCE_URIS = Object.freeze(new Set([
47
+ 'wcf://components',
48
+ 'wcf://tokens',
49
+ 'wcf://guidelines/{topic}',
50
+ 'wcf://llms-full',
51
+ 'wcf://skills',
52
+ ]));
53
+ const BUILTIN_RESOURCE_TEMPLATE_URIS = Object.freeze(new Set([
54
+ 'wcf://guidelines/{topic}',
55
+ ]));
36
56
 
37
57
  function isPlainObject(value) {
38
58
  return value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -109,11 +129,151 @@ function normalizePluginTools(pluginName, tools) {
109
129
  return out;
110
130
  }
111
131
 
132
+ function normalizePluginValidators(pluginName, validators) {
133
+ if (!Array.isArray(validators)) return [];
134
+ const out = [];
135
+ const seen = new Set();
136
+ for (const rawValidator of validators) {
137
+ if (!isPlainObject(rawValidator)) {
138
+ throw new Error(toPluginErrorMessage(pluginName, 'validators entries must be objects'));
139
+ }
140
+ const name = String(rawValidator.name ?? '').trim();
141
+ if (!name) throw new Error(toPluginErrorMessage(pluginName, 'validator.name is required'));
142
+ if (seen.has(name)) {
143
+ throw new Error(toPluginErrorMessage(pluginName, `duplicate validator name: ${name}`));
144
+ }
145
+ seen.add(name);
146
+ if (typeof rawValidator.handler !== 'function') {
147
+ throw new Error(toPluginErrorMessage(pluginName, `validator "${name}" needs handler`));
148
+ }
149
+ out.push({
150
+ name,
151
+ description: String(rawValidator.description ?? '').trim() || `Validator hook provided by ${pluginName}.`,
152
+ handler: rawValidator.handler,
153
+ });
154
+ }
155
+ return out;
156
+ }
157
+
158
+ function normalizePluginPrompts(pluginName, prompts) {
159
+ if (!Array.isArray(prompts)) return [];
160
+ const out = [];
161
+ const seen = new Set();
162
+ for (const rawPrompt of prompts) {
163
+ if (!isPlainObject(rawPrompt)) {
164
+ throw new Error(toPluginErrorMessage(pluginName, 'prompts entries must be objects'));
165
+ }
166
+ const name = String(rawPrompt.name ?? '').trim();
167
+ if (!name) throw new Error(toPluginErrorMessage(pluginName, 'prompt.name is required'));
168
+ if (seen.has(name)) {
169
+ throw new Error(toPluginErrorMessage(pluginName, `duplicate prompt name: ${name}`));
170
+ }
171
+ seen.add(name);
172
+ const hasHandler = typeof rawPrompt.handler === 'function';
173
+ const hasStaticText = typeof rawPrompt.text === 'string';
174
+ if (!hasHandler && !hasStaticText) {
175
+ throw new Error(toPluginErrorMessage(pluginName, `prompt "${name}" needs handler or text`));
176
+ }
177
+ out.push({
178
+ name,
179
+ title: String(rawPrompt.title ?? name).trim(),
180
+ description: String(rawPrompt.description ?? '').trim() || `Prompt provided by ${pluginName}.`,
181
+ argsSchema: isPlainObject(rawPrompt.argsSchema) ? rawPrompt.argsSchema : {},
182
+ handler: hasHandler ? rawPrompt.handler : undefined,
183
+ text: hasStaticText ? rawPrompt.text : undefined,
184
+ });
185
+ }
186
+ return out;
187
+ }
188
+
189
+ function normalizePluginResources(pluginName, resources) {
190
+ if (!Array.isArray(resources)) return [];
191
+ const out = [];
192
+ const seenNames = new Set();
193
+ const seenUris = new Set();
194
+ for (const rawResource of resources) {
195
+ if (!isPlainObject(rawResource)) {
196
+ throw new Error(toPluginErrorMessage(pluginName, 'resources entries must be objects'));
197
+ }
198
+ const name = String(rawResource.name ?? '').trim();
199
+ const uri = String(rawResource.uri ?? '').trim();
200
+ if (!name) throw new Error(toPluginErrorMessage(pluginName, 'resource.name is required'));
201
+ if (!uri) throw new Error(toPluginErrorMessage(pluginName, `resource "${name}" needs uri`));
202
+ if (seenNames.has(name)) {
203
+ throw new Error(toPluginErrorMessage(pluginName, `duplicate resource name: ${name}`));
204
+ }
205
+ if (seenUris.has(uri)) {
206
+ throw new Error(toPluginErrorMessage(pluginName, `duplicate resource uri: ${uri}`));
207
+ }
208
+ seenNames.add(name);
209
+ seenUris.add(uri);
210
+ const hasHandler = typeof rawResource.handler === 'function';
211
+ const hasText = typeof rawResource.text === 'string';
212
+ const hasPayload = Object.prototype.hasOwnProperty.call(rawResource, 'payload');
213
+ if (!hasHandler && !hasText && !hasPayload) {
214
+ throw new Error(toPluginErrorMessage(pluginName, `resource "${name}" needs handler, text, or payload`));
215
+ }
216
+ out.push({
217
+ name,
218
+ uri,
219
+ title: String(rawResource.title ?? name).trim(),
220
+ description: String(rawResource.description ?? '').trim() || `Resource provided by ${pluginName}.`,
221
+ mimeType: String(rawResource.mimeType ?? '').trim() || undefined,
222
+ handler: hasHandler ? rawResource.handler : undefined,
223
+ text: hasText ? rawResource.text : undefined,
224
+ payload: hasPayload ? rawResource.payload : undefined,
225
+ });
226
+ }
227
+ return out;
228
+ }
229
+
230
+ function normalizePluginResourceTemplates(pluginName, resourceTemplates) {
231
+ if (!Array.isArray(resourceTemplates)) return [];
232
+ const out = [];
233
+ const seenNames = new Set();
234
+ const seenTemplates = new Set();
235
+ for (const rawTemplate of resourceTemplates) {
236
+ if (!isPlainObject(rawTemplate)) {
237
+ throw new Error(toPluginErrorMessage(pluginName, 'resourceTemplates entries must be objects'));
238
+ }
239
+ const name = String(rawTemplate.name ?? '').trim();
240
+ const uriTemplate = String(rawTemplate.uriTemplate ?? '').trim();
241
+ if (!name) throw new Error(toPluginErrorMessage(pluginName, 'resourceTemplate.name is required'));
242
+ if (!uriTemplate) throw new Error(toPluginErrorMessage(pluginName, `resourceTemplate "${name}" needs uriTemplate`));
243
+ if (seenNames.has(name)) throw new Error(toPluginErrorMessage(pluginName, `duplicate resourceTemplate name: ${name}`));
244
+ if (seenTemplates.has(uriTemplate)) throw new Error(toPluginErrorMessage(pluginName, `duplicate resourceTemplate uriTemplate: ${uriTemplate}`));
245
+ seenNames.add(name);
246
+ seenTemplates.add(uriTemplate);
247
+ const hasHandler = typeof rawTemplate.handler === 'function';
248
+ const hasText = typeof rawTemplate.text === 'string';
249
+ const hasPayload = Object.prototype.hasOwnProperty.call(rawTemplate, 'payload');
250
+ if (!hasHandler && !hasText && !hasPayload) {
251
+ throw new Error(toPluginErrorMessage(pluginName, `resourceTemplate "${name}" needs handler, text, or payload`));
252
+ }
253
+ out.push({
254
+ name,
255
+ uriTemplate,
256
+ title: String(rawTemplate.title ?? name).trim(),
257
+ description: String(rawTemplate.description ?? '').trim() || `Resource template provided by ${pluginName}.`,
258
+ mimeType: String(rawTemplate.mimeType ?? '').trim() || undefined,
259
+ handler: hasHandler ? rawTemplate.handler : undefined,
260
+ text: hasText ? rawTemplate.text : undefined,
261
+ payload: hasPayload ? rawTemplate.payload : undefined,
262
+ list: Array.isArray(rawTemplate.list) ? rawTemplate.list : undefined,
263
+ complete: isPlainObject(rawTemplate.complete) ? rawTemplate.complete : undefined,
264
+ });
265
+ }
266
+ return out;
267
+ }
268
+
112
269
  export function normalizePlugins(plugins = []) {
113
270
  if (!Array.isArray(plugins)) throw new Error('Invalid plugin configuration: plugins must be an array');
114
271
  const normalized = [];
115
272
  const seenPluginNames = new Set();
116
273
  const seenToolNames = new Set(BUILTIN_TOOL_NAMES);
274
+ const seenPromptNames = new Set(BUILTIN_PROMPT_NAMES);
275
+ const seenResourceUris = new Set(BUILTIN_RESOURCE_URIS);
276
+ const seenResourceTemplateUris = new Set(BUILTIN_RESOURCE_TEMPLATE_URIS);
117
277
 
118
278
  for (const rawPlugin of plugins) {
119
279
  if (!isPlainObject(rawPlugin)) throw new Error('Invalid plugin configuration: each plugin must be an object');
@@ -124,15 +284,37 @@ export function normalizePlugins(plugins = []) {
124
284
  seenPluginNames.add(name);
125
285
 
126
286
  const tools = normalizePluginTools(name, rawPlugin.tools);
287
+ const validators = normalizePluginValidators(name, rawPlugin.validators);
288
+ const prompts = normalizePluginPrompts(name, rawPlugin.prompts);
289
+ const resources = normalizePluginResources(name, rawPlugin.resources);
290
+ const resourceTemplates = normalizePluginResourceTemplates(name, rawPlugin.resourceTemplates);
127
291
  for (const tool of tools) {
128
292
  if (seenToolNames.has(tool.name)) {
129
293
  throw new Error(toPluginErrorMessage(name, `tool name collision: ${tool.name}`));
130
294
  }
131
295
  seenToolNames.add(tool.name);
132
296
  }
297
+ for (const prompt of prompts) {
298
+ if (seenPromptNames.has(prompt.name)) {
299
+ throw new Error(toPluginErrorMessage(name, `prompt name collision: ${prompt.name}`));
300
+ }
301
+ seenPromptNames.add(prompt.name);
302
+ }
303
+ for (const resource of resources) {
304
+ if (seenResourceUris.has(resource.uri)) {
305
+ throw new Error(toPluginErrorMessage(name, `resource uri collision: ${resource.uri}`));
306
+ }
307
+ seenResourceUris.add(resource.uri);
308
+ }
309
+ for (const resourceTemplate of resourceTemplates) {
310
+ if (seenResourceTemplateUris.has(resourceTemplate.uriTemplate)) {
311
+ throw new Error(toPluginErrorMessage(name, `resourceTemplate uri collision: ${resourceTemplate.uriTemplate}`));
312
+ }
313
+ seenResourceTemplateUris.add(resourceTemplate.uriTemplate);
314
+ }
133
315
 
134
316
  const dataSources = normalizePluginDataSources(name, rawPlugin.dataSources);
135
- normalized.push({ name, version, tools, dataSources });
317
+ normalized.push({ name, version, tools, validators, prompts, resources, resourceTemplates, dataSources });
136
318
  }
137
319
 
138
320
  return normalized;