@ncds/ui-admin-mcp 1.0.0-alpha.10

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.
Files changed (94) hide show
  1. package/README.md +113 -0
  2. package/bin/components.bundle.js +1 -0
  3. package/bin/definitions/compliance-rules.json +64 -0
  4. package/bin/definitions/instructions.md +164 -0
  5. package/bin/definitions/js-api.json +165 -0
  6. package/bin/definitions/rules.json +59 -0
  7. package/bin/definitions/token-descriptions.json +27 -0
  8. package/bin/definitions/tool-definitions.json +58 -0
  9. package/bin/server.d.ts +1 -0
  10. package/bin/server.js +217 -0
  11. package/bin/server.mjs +8 -0
  12. package/bin/tools/getComponentProps.d.ts +4 -0
  13. package/bin/tools/getComponentProps.js +20 -0
  14. package/bin/tools/getDesignTokens.d.ts +13 -0
  15. package/bin/tools/getDesignTokens.js +20 -0
  16. package/bin/tools/listComponents.d.ts +16 -0
  17. package/bin/tools/listComponents.js +24 -0
  18. package/bin/tools/listIcons.d.ts +22 -0
  19. package/bin/tools/listIcons.js +23 -0
  20. package/bin/tools/ping.d.ts +17 -0
  21. package/bin/tools/ping.js +20 -0
  22. package/bin/tools/renderToHtml.d.ts +23 -0
  23. package/bin/tools/renderToHtml.js +267 -0
  24. package/bin/tools/searchComponent.d.ts +4 -0
  25. package/bin/tools/searchComponent.js +33 -0
  26. package/bin/tools/searchIcon.d.ts +7 -0
  27. package/bin/tools/searchIcon.js +19 -0
  28. package/bin/tools/validateHtml.d.ts +18 -0
  29. package/bin/tools/validateHtml.js +85 -0
  30. package/bin/types.d.ts +123 -0
  31. package/bin/types.js +5 -0
  32. package/bin/utils/bemValidator.d.ts +36 -0
  33. package/bin/utils/bemValidator.js +198 -0
  34. package/bin/utils/compliance.d.ts +52 -0
  35. package/bin/utils/compliance.js +199 -0
  36. package/bin/utils/dataLoader.d.ts +35 -0
  37. package/bin/utils/dataLoader.js +192 -0
  38. package/bin/utils/domEnvironment.d.ts +9 -0
  39. package/bin/utils/domEnvironment.js +75 -0
  40. package/bin/utils/fuzzyMatch.d.ts +21 -0
  41. package/bin/utils/fuzzyMatch.js +110 -0
  42. package/bin/utils/logger.d.ts +18 -0
  43. package/bin/utils/logger.js +27 -0
  44. package/bin/utils/response.d.ts +28 -0
  45. package/bin/utils/response.js +39 -0
  46. package/bin/utils/tokenValidator.d.ts +24 -0
  47. package/bin/utils/tokenValidator.js +162 -0
  48. package/bin/version.d.ts +4 -0
  49. package/bin/version.js +7 -0
  50. package/data/_icons.json +12401 -0
  51. package/data/_meta.json +12 -0
  52. package/data/_tokens.json +661 -0
  53. package/data/badge-group.json +295 -0
  54. package/data/badge.json +246 -0
  55. package/data/bread-crumb.json +87 -0
  56. package/data/button-group.json +94 -0
  57. package/data/button.json +259 -0
  58. package/data/carousel-arrow.json +87 -0
  59. package/data/carousel-number-group.json +87 -0
  60. package/data/checkbox.json +99 -0
  61. package/data/combo-box.json +173 -0
  62. package/data/date-picker.json +123 -0
  63. package/data/divider.json +91 -0
  64. package/data/dot.json +103 -0
  65. package/data/dropdown.json +258 -0
  66. package/data/empty-state.json +227 -0
  67. package/data/featured-icon.json +139 -0
  68. package/data/file-input.json +315 -0
  69. package/data/horizontal-tab.json +329 -0
  70. package/data/image-file-input.json +339 -0
  71. package/data/input-base.json +299 -0
  72. package/data/modal.json +143 -0
  73. package/data/notification.json +194 -0
  74. package/data/number-input.json +295 -0
  75. package/data/pagination.json +101 -0
  76. package/data/password-input.json +263 -0
  77. package/data/progress-bar.json +109 -0
  78. package/data/progress-circle.json +96 -0
  79. package/data/radio.json +86 -0
  80. package/data/range-date-picker-with-buttons.json +273 -0
  81. package/data/range-date-picker.json +259 -0
  82. package/data/select-box.json +193 -0
  83. package/data/select.json +132 -0
  84. package/data/slider.json +100 -0
  85. package/data/spinner.json +93 -0
  86. package/data/switch.json +156 -0
  87. package/data/tag.json +159 -0
  88. package/data/textarea.json +96 -0
  89. package/data/toggle.json +102 -0
  90. package/data/tooltip.json +185 -0
  91. package/data/vertical-tab.json +314 -0
  92. package/package.json +61 -0
  93. package/templates/.mcp.json.example +8 -0
  94. package/templates/README.md +30 -0
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderToHtml = void 0;
4
+ const response_js_1 = require("../utils/response.js");
5
+ // ── 상수 ──────────────────────────────────────────────────────────
6
+ /** React 특수 props 차단 — injection 방지 */
7
+ const BLOCKED_PROPS = new Set(['dangerouslySetInnerHTML', 'ref', '__self', '__source']);
8
+ /** 아이콘 관련 props — React 변환 시 import 생성 대상 */
9
+ const ICON_PROP_NAMES = new Set(['leadingIcon', 'trailingIcon', 'icon', 'groupIcon']);
10
+ // ── 타입 가드 ──────────────────────────────────────────────────────
11
+ /** { type: 'icon', icon: string } 형태인지 판별 */
12
+ const isIconSlot = (value) => typeof value === 'object' &&
13
+ value !== null &&
14
+ value.type === 'icon' &&
15
+ typeof value.icon === 'string';
16
+ // ── Props 변환 ──────────────────────────────────────────────────────
17
+ /** BLOCKED_PROPS를 제거 — props spec 유무와 무관하게 항상 적용 */
18
+ const removeBlockedProps = (props) => Object.fromEntries(Object.entries(props).filter(([key]) => !BLOCKED_PROPS.has(key)));
19
+ /** componentData.props에 정의된 키 + children만 허용하는 화이트리스트 필터 */
20
+ const sanitizeProps = (userProps, propsSpec) => {
21
+ const allowedKeys = new Set([...Object.keys(propsSpec), 'children']);
22
+ return Object.fromEntries(Object.entries(userProps).filter(([key]) => allowedKeys.has(key)));
23
+ };
24
+ /** 아이콘 prop의 icon 문자열을 실제 React 컴포넌트로 resolve */
25
+ const resolveIconProps = (props, iconBundle, propsSpec) => {
26
+ const resolved = { ...props };
27
+ for (const key of ICON_PROP_NAMES) {
28
+ const value = resolved[key];
29
+ const spec = propsSpec?.[key];
30
+ // bare function prop (FeaturedIcon 등) — 문자열 이름을 컴포넌트 함수로 직접 resolve
31
+ if (spec?.type === 'function' && typeof value === 'string') {
32
+ const iconComponent = iconBundle[value];
33
+ if (typeof iconComponent === 'function') {
34
+ resolved[key] = iconComponent;
35
+ }
36
+ continue;
37
+ }
38
+ // IconSlotType 래퍼 (Button leadingIcon 등) — 래퍼 내부 icon을 컴포넌트로 교체
39
+ if (!isIconSlot(value))
40
+ continue;
41
+ const iconComponent = iconBundle[value.icon];
42
+ if (typeof iconComponent === 'function') {
43
+ resolved[key] = { ...value, icon: iconComponent };
44
+ }
45
+ }
46
+ return resolved;
47
+ };
48
+ // ── JSX 변환 ──────────────────────────────────────────────────────
49
+ /** JSX attribute 값에서 " 를 이스케이프 */
50
+ const escapeJsxAttrValue = (value) => value.replace(/"/g, '"');
51
+ /** 단일 prop을 JSX attribute 문자열로 변환 */
52
+ const toJsxAttr = (key, value, spec) => {
53
+ if (typeof value === 'boolean' && value)
54
+ return key;
55
+ // bare function icon prop (FeaturedIcon 등) — icon={SearchLg} 형태
56
+ if (ICON_PROP_NAMES.has(key) && spec?.type === 'function' && typeof value === 'string')
57
+ return `${key}={${value}}`;
58
+ if (typeof value === 'string')
59
+ return `${key}="${escapeJsxAttrValue(value)}"`;
60
+ if (typeof value === 'number')
61
+ return `${key}={${value}}`;
62
+ if (ICON_PROP_NAMES.has(key) && isIconSlot(value))
63
+ return `${key}={{ type: 'icon', icon: ${value.icon} }}`;
64
+ if (spec?.type === 'enum' || spec?.type === 'string')
65
+ return `${key}="${String(value)}"`;
66
+ return `${key}={${JSON.stringify(value)}}`;
67
+ };
68
+ /** props 객체를 JSX attribute 문자열로 변환 */
69
+ const propsToJsxAttrs = (props, propsSpec) => Object.entries(props)
70
+ .filter(([key]) => key !== 'children')
71
+ .map(([key, value]) => toJsxAttr(key, value, propsSpec[key]))
72
+ .join(' ');
73
+ // ── 아이콘 추출 ──────────────────────────────────────────────────
74
+ /** icon prop 값에서 아이콘 이름을 추출 */
75
+ const extractIconName = (value) => {
76
+ if (isIconSlot(value))
77
+ return value.icon;
78
+ if (typeof value === 'string' && value.length > 0)
79
+ return value;
80
+ return null;
81
+ };
82
+ /** appliedProps에서 아이콘 이름을 추출 (PascalCase) */
83
+ const extractIconNames = (props) => Object.entries(props)
84
+ .filter(([key]) => ICON_PROP_NAMES.has(key))
85
+ .map(([, value]) => extractIconName(value))
86
+ .filter((name) => name !== null);
87
+ // ── 검증 ────────────────────────────────────────────────────────
88
+ /** 잘못된 enum 값을 경고 문자열로 변환 — 교정 결과도 포함 */
89
+ const formatInvalidEnum = (key, value, spec) => {
90
+ const allowed = spec.values ?? [];
91
+ const correction = spec.default !== undefined ? ` Corrected to default '${spec.default}'.` : ' Removed (no default).';
92
+ return `Invalid enum value '${value}' for prop '${key}'.${correction} Allowed: ${allowed.join(', ')}.`;
93
+ };
94
+ /** props 검증 — enum 불일치, 필수 prop 누락을 warnings로 반환 */
95
+ const validateProps = (userProps, propsSpec) => {
96
+ const missingRequired = Object.entries(propsSpec)
97
+ .filter(([key, spec]) => spec.required && !(key in userProps))
98
+ .map(([key]) => `Required prop '${key}' is missing.`);
99
+ const invalidEnums = Object.entries(userProps)
100
+ .filter(([key, value]) => {
101
+ const spec = propsSpec[key];
102
+ return spec?.type === 'enum' && spec.values && typeof value === 'string' && !spec.values.includes(value);
103
+ })
104
+ .map(([key, value]) => formatInvalidEnum(key, value, propsSpec[key]));
105
+ const missingArrays = Object.entries(propsSpec)
106
+ .filter(([key, spec]) => spec.rawType?.includes('[]') && !(key in userProps))
107
+ .map(([key]) => `Array prop '${key}' is missing. Filled with empty array []. Pass data for meaningful rendering.`);
108
+ return [...missingRequired, ...invalidEnums, ...missingArrays];
109
+ };
110
+ /** invalid enum 값을 default 또는 제거로 교정 — 할루시네이션 HTML 방지 */
111
+ const correctInvalidEnums = (userProps, propsSpec) => {
112
+ const corrected = { ...userProps };
113
+ for (const [key, value] of Object.entries(corrected)) {
114
+ const spec = propsSpec[key];
115
+ if (spec?.type !== 'enum' || !spec.values || typeof value !== 'string')
116
+ continue;
117
+ if (spec.values.includes(value))
118
+ continue;
119
+ if (spec.default !== undefined) {
120
+ corrected[key] = spec.default;
121
+ }
122
+ else {
123
+ delete corrected[key];
124
+ }
125
+ }
126
+ return corrected;
127
+ };
128
+ /** required prop 누락 시 type별 safe default 주입 — React crash 방지 (fillMissingArrayProps 통합) */
129
+ const correctMissingRequired = (userProps, propsSpec) => {
130
+ const corrected = { ...userProps };
131
+ for (const [key, spec] of Object.entries(propsSpec)) {
132
+ // array prop — required 여부와 무관하게 [] 주입 (.map() crash 방지)
133
+ if (spec.rawType?.includes('[]') && !(key in corrected && Array.isArray(corrected[key]))) {
134
+ corrected[key] = [];
135
+ continue;
136
+ }
137
+ // required가 아니거나 이미 값이 있으면 스킵
138
+ if (!spec.required || key in corrected)
139
+ continue;
140
+ // default가 있으면 사용
141
+ if (spec.default !== undefined) {
142
+ corrected[key] = spec.default;
143
+ continue;
144
+ }
145
+ // icon function prop (FeaturedIcon 등) — 빈 span 반환 스텁 주입하여 crash 방지
146
+ if (spec.type === 'function' && ICON_PROP_NAMES.has(key)) {
147
+ corrected[key] = () => null;
148
+ continue;
149
+ }
150
+ // 일반 function/ReactNode는 HTML에 영향 없으므로 주입하지 않음
151
+ if (spec.type === 'function' || spec.type === 'ReactNode')
152
+ continue;
153
+ // type별 safe default
154
+ const safeDefaults = {
155
+ string: '',
156
+ object: {},
157
+ boolean: false,
158
+ number: 0,
159
+ enum: spec.values?.[0] ?? '',
160
+ };
161
+ if (spec.type in safeDefaults) {
162
+ corrected[key] = safeDefaults[spec.type];
163
+ }
164
+ }
165
+ return corrected;
166
+ };
167
+ // ── 응답 조립 ──────────────────────────────────────────────────────
168
+ /** componentMap의 props 스펙에서 defaultsUsed를 자동 계산 */
169
+ const calcDefaultsUsed = (propsSpec, userProps) => Object.fromEntries(Object.entries(propsSpec)
170
+ .filter(([key, spec]) => spec.default !== undefined && !(key in userProps))
171
+ .map(([key, spec]) => [key, spec.default]));
172
+ /** React 변환 매핑 정보를 생성 */
173
+ const buildReactOutput = (componentData, appliedProps, iconMeta) => {
174
+ const { exportName, importPath, props: propsSpec } = componentData;
175
+ const imports = [`import { ${exportName} } from '${importPath}';`];
176
+ const dependencies = [importPath];
177
+ const iconNames = extractIconNames(appliedProps);
178
+ if (iconNames.length > 0 && iconMeta) {
179
+ imports.push(`import { ${iconNames.join(', ')} } from '${iconMeta.packageName}';`);
180
+ if (!dependencies.includes(iconMeta.packageName)) {
181
+ dependencies.push(iconMeta.packageName);
182
+ }
183
+ }
184
+ const attrsStr = propsSpec ? propsToJsxAttrs(appliedProps, propsSpec) : '';
185
+ const attrsPrefix = attrsStr ? ' ' + attrsStr : '';
186
+ const children = appliedProps.children;
187
+ const jsx = children
188
+ ? `<${exportName}${attrsPrefix}>${String(children)}</${exportName}>`
189
+ : `<${exportName}${attrsPrefix} />`;
190
+ return { imports, jsx, cssImport: `import '${importPath}/style.css';`, dependencies };
191
+ };
192
+ /** dataVersion 객체를 생성 */
193
+ const buildDataVersion = (cdnMeta, iconMeta) => ({
194
+ ...(cdnMeta && { '@ncds/ui-admin': cdnMeta.version }),
195
+ ...(iconMeta && { [iconMeta.packageName]: iconMeta.version }),
196
+ });
197
+ /** jsRequired에 따라 js 필드를 생성 */
198
+ const buildJsField = (componentData, jsApiMap) => {
199
+ if (!componentData.jsRequired)
200
+ return { required: false };
201
+ const jsApi = jsApiMap.get(componentData.exportName);
202
+ return {
203
+ required: true,
204
+ description: '이 컴포넌트는 인터랙션을 위해 CDN JS가 필요합니다',
205
+ ...(jsApi && {
206
+ api: {
207
+ className: jsApi.className,
208
+ constructor: jsApi.constructor,
209
+ constructorParams: jsApi.constructorParams,
210
+ methods: jsApi.methods,
211
+ example: jsApi.example,
212
+ },
213
+ }),
214
+ };
215
+ };
216
+ // ── 진입점 ────────────────────────────────────────────────────────
217
+ /** render_to_html tool — props로 React 컴포넌트를 렌더링하여 HTML + React 매핑 반환 */
218
+ const renderToHtml = (params) => {
219
+ const { componentMap, bundle, iconBundle, cdnMeta, iconMeta, reactRuntime, jsApiMap, name, props } = params;
220
+ const normalized = (0, response_js_1.normalizeName)(name);
221
+ const componentData = componentMap.get(normalized);
222
+ if (!componentData)
223
+ return (0, response_js_1.componentNotFoundResponse)(normalized);
224
+ const exportName = componentData.exportName;
225
+ if (!exportName) {
226
+ return (0, response_js_1.errorResponse)('EXPORT_NAME_MISSING', `'${normalized}' 컴포넌트에 exportName이 없습니다.`, '데이터를 재추출해주세요 (yarn extract).');
227
+ }
228
+ const Component = bundle[exportName] ?? null;
229
+ if (!Component) {
230
+ return (0, response_js_1.errorResponse)('COMPONENT_NOT_IN_BUNDLE', `'${normalized}' (${exportName}) 컴포넌트가 번들에 없습니다.`, '번들을 재빌드해주세요 (yarn build:bundle).');
231
+ }
232
+ try {
233
+ const safeProps = removeBlockedProps(props ?? {});
234
+ const sanitized = componentData.props ? sanitizeProps(safeProps, componentData.props) : safeProps;
235
+ const enumWarnings = componentData.props ? validateProps(sanitized, componentData.props) : [];
236
+ const enumCorrected = componentData.props ? correctInvalidEnums(sanitized, componentData.props) : sanitized;
237
+ const corrected = componentData.props ? correctMissingRequired(enumCorrected, componentData.props) : enumCorrected;
238
+ const postWarnings = componentData.props ? validateProps(corrected, componentData.props) : [];
239
+ const warnings = [...enumWarnings, ...postWarnings.filter((w) => !enumWarnings.includes(w))];
240
+ const resolvedProps = resolveIconProps(corrected, iconBundle, componentData.props);
241
+ const rawHtml = reactRuntime.renderToStaticMarkup(reactRuntime.createElement(Component, resolvedProps));
242
+ const html = `<!-- ncua:${normalized} start -->\n${rawHtml}\n<!-- ncua:${normalized} end -->`;
243
+ const hasDefaults = componentData.props ? calcDefaultsUsed(componentData.props, corrected) : {};
244
+ return (0, response_js_1.successResponse)({
245
+ html,
246
+ component: normalized,
247
+ exportName,
248
+ importPath: componentData.importPath,
249
+ appliedProps: corrected,
250
+ ...(Object.keys(hasDefaults).length > 0 && { defaultsUsed: hasDefaults }),
251
+ ...(warnings.length > 0 && { warnings }),
252
+ js: buildJsField(componentData, jsApiMap),
253
+ cdn: cdnMeta ?? undefined,
254
+ dataVersion: buildDataVersion(cdnMeta, iconMeta),
255
+ react: buildReactOutput(componentData, corrected, iconMeta),
256
+ });
257
+ }
258
+ catch (err) {
259
+ // catch에서 warnings 재계산 (let 금지 규칙 준수 — validateProps는 순수 함수이므로 동일 결과 보장)
260
+ const catchWarnings = componentData.props ? validateProps(props ?? {}, componentData.props) : [];
261
+ const suggestion = componentData.usageExamples
262
+ ? 'get_component_props로 usageExamples를 확인하고 해당 props로 재시도하세요.'
263
+ : '사용 가능한 속성을 확인하려면 get_component_props를 사용하세요.';
264
+ return (0, response_js_1.errorResponse)('RENDER_FAILED', `'${normalized}' 렌더링 실패: ${(0, response_js_1.toErrorMessage)(err)}`, suggestion, catchWarnings.length > 0 ? catchWarnings : undefined);
265
+ }
266
+ };
267
+ exports.renderToHtml = renderToHtml;
@@ -0,0 +1,4 @@
1
+ import type { ComponentData } from '../types.js';
2
+ import { type McpToolResponse } from '../utils/response.js';
3
+ /** search_component tool — name/description/aliases 퍼지 검색 후 점수순 반환 */
4
+ export declare const searchComponent: (componentMap: Map<string, ComponentData>, query: string) => McpToolResponse;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.searchComponent = void 0;
4
+ /**
5
+ * search_component tool — name/description/aliases 퍼지 키워드 검색 (순수 함수)
6
+ *
7
+ * Calculator: searchComponent — 입력(componentMap, query) → 출력(매칭 결과, 점수순 정렬)
8
+ */
9
+ const dataLoader_js_1 = require("../utils/dataLoader.js");
10
+ const response_js_1 = require("../utils/response.js");
11
+ const fuzzyMatch_js_1 = require("../utils/fuzzyMatch.js");
12
+ /** search_component tool — name/description/aliases 퍼지 검색 후 점수순 반환 */
13
+ const searchComponent = (componentMap, query) => {
14
+ if (!query.trim())
15
+ return (0, response_js_1.successResponse)([]);
16
+ const scored = (0, dataLoader_js_1.getAllComponents)(componentMap)
17
+ .map((c) => {
18
+ // name, description, aliases 모두에 대해 퍼지 매칭 → 최고 점수 채택
19
+ const targets = [c.name, c.description, ...c.aliases];
20
+ const match = (0, fuzzyMatch_js_1.bestFuzzyMatch)(query, targets);
21
+ return { component: c, score: match?.score ?? 0 };
22
+ })
23
+ .filter((item) => item.score > 0)
24
+ .sort((a, b) => b.score - a.score);
25
+ const results = scored.map(({ component: { name, category, description, aliases } }) => ({
26
+ name,
27
+ category,
28
+ description,
29
+ aliases,
30
+ }));
31
+ return (0, response_js_1.successResponse)(results);
32
+ };
33
+ exports.searchComponent = searchComponent;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * search_icon tool — 키워드로 아이콘 검색 (순수 함수)
3
+ */
4
+ import type { IconData } from '../types.js';
5
+ import { type McpToolResponse } from '../utils/response.js';
6
+ /** search_icon tool — 키워드로 아이콘 이름/kebab을 검색 */
7
+ export declare const searchIcon: (iconData: IconData, query: string) => McpToolResponse;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.searchIcon = void 0;
4
+ const response_js_1 = require("../utils/response.js");
5
+ /** search_icon tool — 키워드로 아이콘 이름/kebab을 검색 */
6
+ const searchIcon = (iconData, query) => {
7
+ const lower = query.trim().toLowerCase();
8
+ if (!lower)
9
+ return (0, response_js_1.successResponse)([]);
10
+ const results = iconData.icons
11
+ .filter((icon) => icon.name.toLowerCase().includes(lower) || icon.kebab.includes(lower))
12
+ .map((icon) => ({
13
+ name: icon.name,
14
+ kebab: icon.kebab,
15
+ fill: icon.fill,
16
+ }));
17
+ return (0, response_js_1.successResponse)(results);
18
+ };
19
+ exports.searchIcon = searchIcon;
@@ -0,0 +1,18 @@
1
+ import type { ComponentData, ComplianceRulesData, TokenData } from '../types.js';
2
+ import type { CdnMeta } from '../utils/dataLoader.js';
3
+ import { type McpToolResponse } from '../utils/response.js';
4
+ export type { ValidationError } from '../utils/bemValidator.js';
5
+ export { buildRootClassMap } from '../utils/bemValidator.js';
6
+ export { buildTokenValueMap } from '../utils/tokenValidator.js';
7
+ /** validate_html tool 파라미터 */
8
+ export interface ValidateHtmlParams {
9
+ componentMap: Map<string, ComponentData>;
10
+ rootClassMap: Map<string, string>;
11
+ html: string;
12
+ cdnMeta?: CdnMeta | null;
13
+ tokenData?: TokenData | null;
14
+ complianceRules?: ComplianceRulesData | null;
15
+ tokenValueMap?: Map<string, string[]> | null;
16
+ }
17
+ /** validate_html tool — HTML의 NCUA BEM 클래스 정합성 + 디자인 시스템 준수도 검증 */
18
+ export declare const validateHtml: (params: ValidateHtmlParams) => McpToolResponse;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateHtml = exports.buildTokenValueMap = exports.buildRootClassMap = void 0;
4
+ /**
5
+ * validate_html tool — NCUA HTML BEM 검증 + 디자인 시스템 준수도 검증 진입점
6
+ *
7
+ * BEM 검증: utils/bemValidator.ts
8
+ * Compliance 검증: utils/compliance.ts
9
+ * 이 파일은 오케스트레이션만 담당한다.
10
+ */
11
+ const node_html_parser_1 = require("node-html-parser");
12
+ const response_js_1 = require("../utils/response.js");
13
+ const bemValidator_js_1 = require("../utils/bemValidator.js");
14
+ const compliance_js_1 = require("../utils/compliance.js");
15
+ var bemValidator_js_2 = require("../utils/bemValidator.js");
16
+ Object.defineProperty(exports, "buildRootClassMap", { enumerable: true, get: function () { return bemValidator_js_2.buildRootClassMap; } });
17
+ var tokenValidator_js_1 = require("../utils/tokenValidator.js");
18
+ Object.defineProperty(exports, "buildTokenValueMap", { enumerable: true, get: function () { return tokenValidator_js_1.buildTokenValueMap; } });
19
+ /** validate_html tool — HTML의 NCUA BEM 클래스 정합성 + 디자인 시스템 준수도 검증 */
20
+ const validateHtml = (params) => {
21
+ const { componentMap, rootClassMap, html, cdnMeta, tokenData, complianceRules, tokenValueMap } = params;
22
+ const trimmed = html.trim();
23
+ if (trimmed.length === 0)
24
+ return (0, response_js_1.successResponse)({ valid: true });
25
+ const root = (0, node_html_parser_1.parse)(trimmed);
26
+ const elements = root.querySelectorAll('[class]');
27
+ const { errors, warnings, invalidClassesSet, hasNcuaClasses } = (0, bemValidator_js_1.collectBemErrors)({
28
+ elements,
29
+ rootClassMap,
30
+ componentMap,
31
+ });
32
+ if (!hasNcuaClasses)
33
+ return (0, response_js_1.successResponse)({ valid: true });
34
+ if (cdnMeta)
35
+ errors.push(...(0, bemValidator_js_1.checkCdnInclusion)(trimmed, cdnMeta));
36
+ const compliance = buildCompliance({
37
+ root,
38
+ componentMap,
39
+ rootClassMap,
40
+ warnings,
41
+ tokenData,
42
+ complianceRules,
43
+ tokenValueMap,
44
+ });
45
+ return buildResponse({ errors, warnings, invalidClassesSet, root, compliance });
46
+ };
47
+ exports.validateHtml = validateHtml;
48
+ /** compliance 검증 오케스트레이션 — tokenData 또는 complianceRules가 있을 때만 실행 */
49
+ const buildCompliance = (params) => {
50
+ const { root, componentMap, rootClassMap, warnings, tokenData, complianceRules, tokenValueMap } = params;
51
+ if (!complianceRules && !tokenData)
52
+ return undefined;
53
+ const ncuaResult = complianceRules
54
+ ? (0, compliance_js_1.detectNcuaOmission)({ root, complianceRules, componentMap })
55
+ : { errors: [], ncuaUsage: { used: 0, available: 0 } };
56
+ const tokenResult = tokenData && tokenValueMap
57
+ ? (0, compliance_js_1.detectTokenIssues)({ root, tokenData, tokenValueMap })
58
+ : { errors: [], tokenUsage: { correct: 0, missing: 0, invalid: 0 } };
59
+ const customResult = (0, compliance_js_1.detectCustomNotSeparated)({ root, rootClassMap, warnings });
60
+ return (0, compliance_js_1.buildComplianceSummary)({
61
+ ncuaErrors: ncuaResult.errors,
62
+ tokenErrors: tokenResult.errors,
63
+ customErrors: customResult.errors,
64
+ ncuaUsage: ncuaResult.ncuaUsage,
65
+ tokenUsage: tokenResult.tokenUsage,
66
+ customSeparation: customResult.customSeparation,
67
+ });
68
+ };
69
+ /** 최종 응답 빌드 */
70
+ const buildResponse = (params) => {
71
+ const { errors, warnings, invalidClassesSet, root, compliance } = params;
72
+ if (errors.length === 0 && warnings.length === 0 && !compliance)
73
+ return (0, response_js_1.successResponse)({ valid: true });
74
+ if (errors.length === 0 && warnings.length === 0 && compliance)
75
+ return (0, response_js_1.successResponse)({ valid: true, compliance });
76
+ if (errors.length === 0)
77
+ return (0, response_js_1.successResponse)({ valid: true, warnings, ...(compliance && { compliance }) });
78
+ return (0, response_js_1.successResponse)({
79
+ valid: false,
80
+ errors,
81
+ ...(warnings.length > 0 && { warnings }),
82
+ fixed_html: (0, bemValidator_js_1.buildFixedHtml)(root, invalidClassesSet),
83
+ ...(compliance && { compliance }),
84
+ });
85
+ };
package/bin/types.d.ts ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * MCP 서버 공통 타입 정의
3
+ */
4
+ export interface PropSpec {
5
+ type: 'string' | 'enum' | 'boolean' | 'number' | 'ReactNode' | 'function' | 'object';
6
+ required: boolean;
7
+ default?: unknown;
8
+ values?: string[];
9
+ rawType?: string;
10
+ properties?: Record<string, PropSpec>;
11
+ }
12
+ export interface ComponentUsage {
13
+ import: string;
14
+ react: Record<string, string>;
15
+ }
16
+ export interface ComponentData {
17
+ name: string;
18
+ exportName: string;
19
+ importPath: string;
20
+ jsRequired: boolean;
21
+ category: string;
22
+ description: string;
23
+ aliases: string[];
24
+ hasChildren: boolean;
25
+ whenToUse: string[];
26
+ forbiddenRules: string[];
27
+ seeAlso: string[];
28
+ /** Props가 확장하는 HTML 요소 — 에이전트가 표준 HTML 속성 사용 가능 여부를 판단 */
29
+ htmlElement?: string;
30
+ /** render_to_html에 전달할 실제 사용 예시 props — meta.ts에서 추출 */
31
+ usageExamples?: Record<string, Record<string, unknown>>;
32
+ props: Record<string, PropSpec>;
33
+ html?: Record<string, string>;
34
+ bemClasses: string[];
35
+ usage: ComponentUsage;
36
+ }
37
+ export interface IconEntry {
38
+ name: string;
39
+ kebab: string;
40
+ fill: boolean;
41
+ }
42
+ export interface IconData {
43
+ totalCount: number;
44
+ fillCount: number;
45
+ icons: IconEntry[];
46
+ }
47
+ /** server.ts에서 주입하는 React 런타임 의존성 */
48
+ export interface ReactRuntime {
49
+ createElement: (type: unknown, props?: Record<string, unknown>) => unknown;
50
+ renderToStaticMarkup: (element: unknown) => string;
51
+ }
52
+ export interface Capability {
53
+ tool: string;
54
+ description: string;
55
+ available: boolean;
56
+ }
57
+ export type McpErrorCode = 'COMPONENT_NOT_FOUND' | 'EXPORT_NAME_MISSING' | 'COMPONENT_NOT_IN_BUNDLE' | 'RENDER_FAILED' | 'INVALID_TOKEN_CATEGORY' | 'ICON_NOT_FOUND' | 'INVALID_PARAMS' | 'INTERNAL_ERROR';
58
+ export interface TokenEntry {
59
+ name: string;
60
+ value: string;
61
+ description: string;
62
+ }
63
+ export interface TokenGroup {
64
+ name: string;
65
+ category: string;
66
+ description: string;
67
+ tokens: TokenEntry[];
68
+ }
69
+ export interface TokenData {
70
+ totalCount: number;
71
+ categories: string[];
72
+ groups: TokenGroup[];
73
+ }
74
+ export interface JsApiInfo {
75
+ className: string;
76
+ constructor: string;
77
+ constructorParams: Record<string, string>;
78
+ methods: string[];
79
+ staticMethods?: string[];
80
+ example: string;
81
+ }
82
+ export interface ComplianceRuleHint {
83
+ attr: string;
84
+ values: string[];
85
+ }
86
+ export interface ComplianceRuleMatch {
87
+ tag: string;
88
+ hints?: ComplianceRuleHint[];
89
+ }
90
+ export interface ComplianceRule {
91
+ match: ComplianceRuleMatch;
92
+ ncuaComponent: string;
93
+ confidence: 'high' | 'medium';
94
+ }
95
+ export interface ComplianceRulesData {
96
+ patterns: ComplianceRule[];
97
+ }
98
+ export interface ComplianceError {
99
+ type: 'ncua_not_used' | 'token_not_used' | 'invalid_token' | 'custom_not_separated';
100
+ severity?: 'warning';
101
+ target: string;
102
+ component: string;
103
+ message: string;
104
+ suggestion: string;
105
+ }
106
+ export interface ComplianceSummary {
107
+ score: number;
108
+ errors: ComplianceError[];
109
+ ncuaUsage: {
110
+ used: number;
111
+ available: number;
112
+ };
113
+ tokenUsage: {
114
+ correct: number;
115
+ missing: number;
116
+ invalid: number;
117
+ };
118
+ customSeparation: {
119
+ clean: number;
120
+ violated: number;
121
+ };
122
+ totalViolations: number;
123
+ }
package/bin/types.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ /**
3
+ * MCP 서버 공통 타입 정의
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * BEM 클래스 검증 순수 함수 — NCUA HTML의 BEM 클래스 정합성 검증 + CDN 링크 검증
3
+ *
4
+ * validateHtml.ts에서 분리. 모든 함수는 순수 함수(Calculation).
5
+ */
6
+ import type { parse } from 'node-html-parser';
7
+ import type { ComponentData } from '../types.js';
8
+ import type { CdnMeta } from './dataLoader.js';
9
+ export interface ValidationError {
10
+ type: 'invalid_modifier' | 'invalid_element' | 'unknown_component' | 'missing_root_class' | 'mixed_namespace' | 'missing_cdn';
11
+ class: string;
12
+ component: string;
13
+ message: string;
14
+ suggestion: string;
15
+ }
16
+ /** 엘리먼트에서 class 목록을 파싱 */
17
+ export declare const getClassList: (el: {
18
+ getAttribute: (name: string) => string | undefined;
19
+ }) => string[];
20
+ /** root class → component name 매핑 빌드 — server.ts에서 1회 호출 */
21
+ export declare const buildRootClassMap: (componentMap: Map<string, ComponentData>) => Map<string, string>;
22
+ /** 엘리먼트별 BEM 클래스 검증 → errors + warnings + invalidClasses 수집 */
23
+ export declare const collectBemErrors: (params: {
24
+ elements: ReturnType<ReturnType<typeof parse>['querySelectorAll']>;
25
+ rootClassMap: Map<string, string>;
26
+ componentMap: Map<string, ComponentData>;
27
+ }) => {
28
+ errors: ValidationError[];
29
+ warnings: ValidationError[];
30
+ invalidClassesSet: Set<string>;
31
+ hasNcuaClasses: boolean;
32
+ };
33
+ /** 잘못된 클래스를 제거하여 fixed_html 생성 */
34
+ export declare const buildFixedHtml: (root: ReturnType<typeof parse>, invalidClassesSet: Set<string>) => string;
35
+ /** CDN CSS/JS 링크 포함 여부 검증 */
36
+ export declare const checkCdnInclusion: (html: string, cdnMeta: CdnMeta) => ValidationError[];