@ncds/ui-admin-mcp 1.6.4-alpha.5 → 1.6.4-alpha.6
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 +5 -0
- package/bin/server.js +13 -0
- package/bin/tools/listIcons.d.ts +4 -1
- package/bin/tools/listIcons.js +8 -2
- package/bin/tools/ping.js +1 -0
- package/bin/tools/renderToHtml.d.ts +3 -0
- package/bin/tools/renderToHtml.js +91 -0
- package/bin/utils/dataLoader.d.ts +2 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,6 +79,8 @@ Claude에게 자연어로 물어보면 됩니다:
|
|
|
79
79
|
"어드민 컴포넌트 목록 보여줘"
|
|
80
80
|
"버튼 컴포넌트 어떤 속성 사용할 수 있어?"
|
|
81
81
|
"input 컴포넌트 HTML 마크업 보여줘"
|
|
82
|
+
"화살표 관련 아이콘 검색해줘"
|
|
83
|
+
"버튼 컴포넌트를 label은 저장, primary로 렌더링해줘"
|
|
82
84
|
"이 HTML에 사용된 클래스가 맞는지 검증해줘"
|
|
83
85
|
```
|
|
84
86
|
|
|
@@ -90,9 +92,12 @@ Claude에게 자연어로 물어보면 됩니다:
|
|
|
90
92
|
| --------------------- | -------------------------------------------------------------- |
|
|
91
93
|
| 📋 컴포넌트 목록 조회 | 사용 가능한 전체 UI 컴포넌트를 확인합니다 |
|
|
92
94
|
| 🔍 컴포넌트 검색 | 키워드로 원하는 컴포넌트를 찾습니다 |
|
|
95
|
+
| 🎨 아이콘 목록 조회 | 사용 가능한 아이콘 요약 정보를 확인합니다 |
|
|
96
|
+
| 🔍 아이콘 검색 | 키워드로 원하는 아이콘을 찾습니다 |
|
|
93
97
|
| 🧩 HTML 마크업 조회 | 컴포넌트의 정확한 HTML 구조와 React 사용 예시를 확인합니다 |
|
|
94
98
|
| ⚙️ 속성(Props) 조회 | 컴포넌트에 사용할 수 있는 옵션과 기본값을 확인합니다 |
|
|
95
99
|
| ✅ HTML 검증 | 작성한 HTML의 클래스명이 올바른지 검증하고 자동으로 수정합니다 |
|
|
100
|
+
| 🔄 동적 HTML 렌더링 | props를 전달하면 실제 렌더링된 정확한 HTML을 받을 수 있습니다 |
|
|
96
101
|
| 📡 서버 상태 확인 | 연결 상태, 버전, 사용 규칙을 확인합니다 |
|
|
97
102
|
|
|
98
103
|
<br />
|
package/bin/server.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
/**
|
|
4
7
|
* MCP 서버 진입점 — componentMap 소유, tool 등록, transport 연결
|
|
5
8
|
*/
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
6
10
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
7
11
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
8
12
|
const zod_1 = require("zod");
|
|
@@ -16,6 +20,7 @@ const validateHtml_js_1 = require("./tools/validateHtml.js");
|
|
|
16
20
|
const ping_js_1 = require("./tools/ping.js");
|
|
17
21
|
const listIcons_js_1 = require("./tools/listIcons.js");
|
|
18
22
|
const searchIcon_js_1 = require("./tools/searchIcon.js");
|
|
23
|
+
const renderToHtml_js_1 = require("./tools/renderToHtml.js");
|
|
19
24
|
const version_js_1 = require("./version.js");
|
|
20
25
|
const server = new mcp_js_1.McpServer({
|
|
21
26
|
name: 'ncds-ui-admin',
|
|
@@ -26,6 +31,7 @@ const main = async () => {
|
|
|
26
31
|
const componentMap = (0, dataLoader_js_1.loadComponentsFromDir)(dataLoader_js_1.DEFAULT_DATA_DIR);
|
|
27
32
|
const cdnMeta = (0, dataLoader_js_1.loadCdnMeta)(dataLoader_js_1.DEFAULT_DATA_DIR);
|
|
28
33
|
const iconData = (0, dataLoader_js_1.loadIconData)(dataLoader_js_1.DEFAULT_DATA_DIR);
|
|
34
|
+
const bundlePath = path_1.default.resolve(__dirname, '../bin/components.bundle.js');
|
|
29
35
|
server.registerTool('ping', {
|
|
30
36
|
description: 'NCUA MCP 서버 연결 확인 + 버전 + capabilities/rules 조회',
|
|
31
37
|
}, () => (0, ping_js_1.ping)(componentMap, cdnMeta, version_js_1.VERSION));
|
|
@@ -66,6 +72,13 @@ const main = async () => {
|
|
|
66
72
|
.describe('검증할 HTML 마크업 (예: \'<button class="ncua-btn ncua-btn--primary"></button>\')'),
|
|
67
73
|
},
|
|
68
74
|
}, ({ html }) => (0, validateHtml_js_1.validateHtml)(componentMap, html));
|
|
75
|
+
server.registerTool('render_to_html', {
|
|
76
|
+
description: '컴포넌트명과 props를 전달하면 실제 렌더링된 HTML을 반환합니다. 어떤 props 조합이든 정확한 DOM 구조의 HTML을 받을 수 있습니다.',
|
|
77
|
+
inputSchema: {
|
|
78
|
+
name: zod_1.z.string().min(1).describe('컴포넌트명 (예: "button", "modal", "input")'),
|
|
79
|
+
props: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional().describe('컴포넌트 props (예: { label: "확인", hierarchy: "primary" })'),
|
|
80
|
+
},
|
|
81
|
+
}, ({ name, props }) => (0, renderToHtml_js_1.renderToHtml)(componentMap, bundlePath, name, props));
|
|
69
82
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
70
83
|
await server.connect(transport);
|
|
71
84
|
logger_js_1.logger.info(`NCUA MCP 서버 시작됨 (ncds-ui-admin v${version_js_1.VERSION})`);
|
package/bin/tools/listIcons.d.ts
CHANGED
package/bin/tools/listIcons.js
CHANGED
|
@@ -2,14 +2,20 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.listIcons = void 0;
|
|
4
4
|
/**
|
|
5
|
-
* list_icons tool —
|
|
5
|
+
* list_icons tool — 아이콘 요약 정보 반환 (순수 함수)
|
|
6
|
+
*
|
|
7
|
+
* 아이콘 이름을 전부 반환하면 토큰 낭비이므로,
|
|
8
|
+
* 요약 정보만 반환하고 search_icon으로 검색을 유도한다.
|
|
6
9
|
*/
|
|
7
10
|
const response_js_1 = require("../utils/response.js");
|
|
8
11
|
const listIcons = (iconData) => {
|
|
12
|
+
const prefixes = new Set(iconData.icons.map((i) => i.name.replace(/Fill$/, '')));
|
|
9
13
|
return (0, response_js_1.successResponse)({
|
|
10
14
|
totalCount: iconData.totalCount,
|
|
11
15
|
fillCount: iconData.fillCount,
|
|
12
|
-
|
|
16
|
+
uniqueIcons: prefixes.size,
|
|
17
|
+
hint: '아이콘이 많으므로 search_icon tool로 키워드 검색을 권장합니다. 예: search_icon("arrow"), search_icon("check")',
|
|
18
|
+
sample: iconData.icons.slice(0, 20).map((i) => i.name),
|
|
13
19
|
});
|
|
14
20
|
};
|
|
15
21
|
exports.listIcons = listIcons;
|
package/bin/tools/ping.js
CHANGED
|
@@ -11,6 +11,7 @@ const CAPABILITIES = [
|
|
|
11
11
|
{ tool: 'get_component_html', description: '컴포넌트 HTML 마크업 조회 (variant 지원)' },
|
|
12
12
|
{ tool: 'get_component_props', description: '컴포넌트 Props 스펙 조회' },
|
|
13
13
|
{ tool: 'validate_html', description: 'HTML BEM 클래스 유효성 검증 + 자동 수정' },
|
|
14
|
+
{ tool: 'render_to_html', description: 'Props 기반 동적 HTML 렌더링' },
|
|
14
15
|
];
|
|
15
16
|
const RULES = [
|
|
16
17
|
'이 라이브러리의 컴포넌트를 사용할 때는 CDN CSS/JS를 포함하세요',
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ComponentData } from '../types.js';
|
|
2
|
+
import { type McpToolResponse } from '../utils/response.js';
|
|
3
|
+
export declare const renderToHtml: (componentMap: Map<string, ComponentData>, bundlePath: string, name: string, props?: Record<string, unknown>) => McpToolResponse;
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
6
|
+
const { JSDOM } = require('jsdom');
|
|
7
|
+
/** jsdom + createPortal mock 환경을 세팅한다 */
|
|
8
|
+
const setupDomEnvironment = () => {
|
|
9
|
+
if (typeof globalThis.document === 'undefined') {
|
|
10
|
+
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
|
|
11
|
+
globalThis.document = dom.window.document;
|
|
12
|
+
// window/navigator는 이미 존재할 수 있으므로 (vitest 등) 없을 때만 세팅
|
|
13
|
+
if (typeof globalThis.window === 'undefined') {
|
|
14
|
+
globalThis.window = dom.window;
|
|
15
|
+
}
|
|
16
|
+
if (typeof globalThis.navigator === 'undefined') {
|
|
17
|
+
Object.defineProperty(globalThis, 'navigator', { value: dom.window.navigator, writable: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
21
|
+
const ReactDOM = require('react-dom');
|
|
22
|
+
if (!ReactDOM._portalMocked) {
|
|
23
|
+
ReactDOM.createPortal = (children) => children;
|
|
24
|
+
ReactDOM._portalMocked = true;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/** kebab-case → PascalCase 변환 */
|
|
28
|
+
const toPascalCase = (kebab) => kebab
|
|
29
|
+
.split('-')
|
|
30
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
31
|
+
.join('');
|
|
32
|
+
/** 번들에서 컴포넌트를 찾는다 */
|
|
33
|
+
const findComponent = (bundle, name) => {
|
|
34
|
+
const pascalName = toPascalCase(name);
|
|
35
|
+
// 정확한 이름 매칭
|
|
36
|
+
if (bundle[pascalName])
|
|
37
|
+
return { Component: bundle[pascalName], exportName: pascalName };
|
|
38
|
+
// InputBase 등 특수 매핑
|
|
39
|
+
const SPECIAL_MAPPINGS = {
|
|
40
|
+
Input: 'InputBase',
|
|
41
|
+
PasswordInput: 'PasswordInput',
|
|
42
|
+
NumberInput: 'NumberInput',
|
|
43
|
+
Textarea: 'Textarea',
|
|
44
|
+
};
|
|
45
|
+
const special = SPECIAL_MAPPINGS[pascalName];
|
|
46
|
+
if (special && bundle[special])
|
|
47
|
+
return { Component: bundle[special], exportName: special };
|
|
48
|
+
// 대소문자 무관 검색
|
|
49
|
+
const key = Object.keys(bundle).find((k) => k.toLowerCase() === pascalName.toLowerCase());
|
|
50
|
+
if (key && bundle[key])
|
|
51
|
+
return { Component: bundle[key], exportName: key };
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
54
|
+
const renderToHtml = (componentMap, bundlePath, name, props) => {
|
|
55
|
+
const normalized = (0, response_js_1.normalizeName)(name);
|
|
56
|
+
const componentData = componentMap.get(normalized);
|
|
57
|
+
if (!componentData)
|
|
58
|
+
return (0, response_js_1.componentNotFoundResponse)(normalized);
|
|
59
|
+
setupDomEnvironment();
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
61
|
+
const React = require('react');
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
63
|
+
const { renderToStaticMarkup } = require('react-dom/server');
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
65
|
+
const bundle = require(bundlePath);
|
|
66
|
+
const found = findComponent(bundle, normalized);
|
|
67
|
+
if (!found) {
|
|
68
|
+
return (0, response_js_1.successResponse)({
|
|
69
|
+
error: 'COMPONENT_NOT_IN_BUNDLE',
|
|
70
|
+
message: `'${normalized}' 컴포넌트가 번들에 없습니다. get_component_html을 사용하세요.`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const element = React.createElement(found.Component, props ?? {});
|
|
75
|
+
const html = renderToStaticMarkup(element);
|
|
76
|
+
return (0, response_js_1.successResponse)({
|
|
77
|
+
html,
|
|
78
|
+
component: normalized,
|
|
79
|
+
exportName: found.exportName,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
84
|
+
return (0, response_js_1.successResponse)({
|
|
85
|
+
error: 'RENDER_FAILED',
|
|
86
|
+
message: `'${normalized}' 렌더링 실패: ${message}`,
|
|
87
|
+
component: normalized,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
exports.renderToHtml = renderToHtml;
|
|
@@ -8,15 +8,7 @@ export interface CdnMeta {
|
|
|
8
8
|
css: string;
|
|
9
9
|
js: string;
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
kebab: string;
|
|
14
|
-
fill: boolean;
|
|
15
|
-
}
|
|
16
|
-
export interface IconData {
|
|
17
|
-
totalCount: number;
|
|
18
|
-
fillCount: number;
|
|
19
|
-
icons: IconEntry[];
|
|
20
|
-
}
|
|
11
|
+
import type { IconData } from '../tools/listIcons.js';
|
|
12
|
+
export type { IconData };
|
|
21
13
|
export declare const loadIconData: (dataDir: string) => IconData;
|
|
22
14
|
export declare const loadCdnMeta: (dataDir: string) => CdnMeta | null;
|