@vadenai/mcp-server 0.1.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/LICENSE +21 -0
- package/README.md +356 -0
- package/dist/api-client.d.ts +15 -0
- package/dist/api-client.js +52 -0
- package/dist/guides/component-metadata.d.ts +25 -0
- package/dist/guides/component-metadata.js +789 -0
- package/dist/guides/generate-guide.d.ts +36 -0
- package/dist/guides/generate-guide.js +104 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +238 -0
- package/dist/tools/components.d.ts +21 -0
- package/dist/tools/components.js +234 -0
- package/dist/tools/concept.d.ts +26 -0
- package/dist/tools/concept.js +179 -0
- package/dist/tools/design-tokens.d.ts +15 -0
- package/dist/tools/design-tokens.js +115 -0
- package/dist/tools/wireframes.d.ts +28 -0
- package/dist/tools/wireframes.js +214 -0
- package/package.json +49 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
interface ComponentInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
categories: string[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* コンポーネント使用ガイドを生成する
|
|
9
|
+
*
|
|
10
|
+
* 出力例:
|
|
11
|
+
* ```
|
|
12
|
+
* # Button — Component Usage Guide
|
|
13
|
+
*
|
|
14
|
+
* > Displays a button or a component that looks like a button.
|
|
15
|
+
*
|
|
16
|
+
* **Category**: form
|
|
17
|
+
*
|
|
18
|
+
* ## Structure
|
|
19
|
+
* Single element component (cva-based).
|
|
20
|
+
*
|
|
21
|
+
* ## Variants
|
|
22
|
+
* - `variant`: default | destructive | outline | secondary | ghost | link
|
|
23
|
+
* - `size`: default | sm | lg | icon
|
|
24
|
+
*
|
|
25
|
+
* ## Accessibility
|
|
26
|
+
* - Use <button> for actions, <a> for navigation ...
|
|
27
|
+
*
|
|
28
|
+
* ## States
|
|
29
|
+
* hover, focus-visible, active, disabled
|
|
30
|
+
*
|
|
31
|
+
* ## Do NOT
|
|
32
|
+
* - Do not use color props — use variant instead
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function generateComponentGuide(info: ComponentInfo): string | null;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* コンポーネントメタデータから LLM 向け使用ガイドを生成する
|
|
3
|
+
*
|
|
4
|
+
* design-tokens の generateTokenUsageGuide と同じパターン。
|
|
5
|
+
* JSON 仕様だけでは不足する「どう使うべきか」を自然言語で補完する。
|
|
6
|
+
*/
|
|
7
|
+
import { componentMetadata } from "./component-metadata.js";
|
|
8
|
+
/**
|
|
9
|
+
* コンポーネント使用ガイドを生成する
|
|
10
|
+
*
|
|
11
|
+
* 出力例:
|
|
12
|
+
* ```
|
|
13
|
+
* # Button — Component Usage Guide
|
|
14
|
+
*
|
|
15
|
+
* > Displays a button or a component that looks like a button.
|
|
16
|
+
*
|
|
17
|
+
* **Category**: form
|
|
18
|
+
*
|
|
19
|
+
* ## Structure
|
|
20
|
+
* Single element component (cva-based).
|
|
21
|
+
*
|
|
22
|
+
* ## Variants
|
|
23
|
+
* - `variant`: default | destructive | outline | secondary | ghost | link
|
|
24
|
+
* - `size`: default | sm | lg | icon
|
|
25
|
+
*
|
|
26
|
+
* ## Accessibility
|
|
27
|
+
* - Use <button> for actions, <a> for navigation ...
|
|
28
|
+
*
|
|
29
|
+
* ## States
|
|
30
|
+
* hover, focus-visible, active, disabled
|
|
31
|
+
*
|
|
32
|
+
* ## Do NOT
|
|
33
|
+
* - Do not use color props — use variant instead
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function generateComponentGuide(info) {
|
|
37
|
+
if (!Object.hasOwn(componentMetadata, info.name))
|
|
38
|
+
return null;
|
|
39
|
+
const meta = componentMetadata[info.name];
|
|
40
|
+
const lines = [`# ${info.title} — Component Usage Guide`, ""];
|
|
41
|
+
if (info.description.trim()) {
|
|
42
|
+
lines.push(`> ${info.description}`, "");
|
|
43
|
+
}
|
|
44
|
+
lines.push(`**Categories**: ${info.categories.join(", ") || "unknown"}`, "");
|
|
45
|
+
// Structure
|
|
46
|
+
lines.push("## Structure");
|
|
47
|
+
if (meta.type === "single") {
|
|
48
|
+
const hasVariants = meta.variants && Object.keys(meta.variants).length > 0;
|
|
49
|
+
lines.push(hasVariants
|
|
50
|
+
? "Single element component (cva-based) with variants."
|
|
51
|
+
: "Single element component (cva-based), no variants.");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
lines.push(`Multipart component with ${meta.slots?.length ?? 0} slots.`);
|
|
55
|
+
if (meta.slots && meta.slots.length > 0) {
|
|
56
|
+
lines.push(`Slots: \`${meta.slots.join("` | `")}\``);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
lines.push("");
|
|
60
|
+
// Variants
|
|
61
|
+
if (meta.variants && Object.keys(meta.variants).length > 0) {
|
|
62
|
+
lines.push("## Variants");
|
|
63
|
+
for (const [key, options] of Object.entries(meta.variants)) {
|
|
64
|
+
lines.push(`- \`${key}\`: ${options.join(" | ")}`);
|
|
65
|
+
}
|
|
66
|
+
lines.push("");
|
|
67
|
+
}
|
|
68
|
+
// Dependencies
|
|
69
|
+
if (meta.dependencies && meta.dependencies.length > 0) {
|
|
70
|
+
lines.push("## Dependencies");
|
|
71
|
+
lines.push(meta.dependencies.map((d) => `- \`${d}\``).join("\n"));
|
|
72
|
+
lines.push("");
|
|
73
|
+
}
|
|
74
|
+
// Accessibility
|
|
75
|
+
if (meta.a11y && meta.a11y.length > 0) {
|
|
76
|
+
lines.push("## Accessibility");
|
|
77
|
+
for (const rule of meta.a11y) {
|
|
78
|
+
lines.push(`- ${rule}`);
|
|
79
|
+
}
|
|
80
|
+
lines.push("");
|
|
81
|
+
}
|
|
82
|
+
// States
|
|
83
|
+
if (meta.states && meta.states.length > 0) {
|
|
84
|
+
lines.push("## States");
|
|
85
|
+
lines.push(`Supported: ${meta.states.join(", ")}`);
|
|
86
|
+
lines.push("");
|
|
87
|
+
}
|
|
88
|
+
// Do NOT
|
|
89
|
+
if (meta.doNot && meta.doNot.length > 0) {
|
|
90
|
+
lines.push("## Do NOT");
|
|
91
|
+
for (const rule of meta.doNot) {
|
|
92
|
+
lines.push(`- ${rule}`);
|
|
93
|
+
}
|
|
94
|
+
lines.push("");
|
|
95
|
+
}
|
|
96
|
+
// Important rules (common to all)
|
|
97
|
+
lines.push("## Important Rules");
|
|
98
|
+
lines.push("- Always use design token CSS variables — never hardcode colors or spacing.");
|
|
99
|
+
lines.push("- Follow the variant system — do not create ad-hoc style overrides.");
|
|
100
|
+
if (meta.type === "multipart") {
|
|
101
|
+
lines.push("- Use the slot-based structure — each slot has specific styling responsibilities.");
|
|
102
|
+
}
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Vaden MCP Server エントリポイント
|
|
4
|
+
*
|
|
5
|
+
* 起動方法:
|
|
6
|
+
* node dist/index.js --project-id <id> --token <token> [--api-url <url>]
|
|
7
|
+
*
|
|
8
|
+
* 環境変数フォールバック:
|
|
9
|
+
* VADEN_PROJECT_ID, VADEN_TOKEN, VADEN_API_URL
|
|
10
|
+
*/
|
|
11
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
+
import { VadenMcpApiClient } from "./api-client.js";
|
|
15
|
+
import { handleGetComponentList, handleGetComponentSpec, handleSearchComponents, } from "./tools/components.js";
|
|
16
|
+
import { handleGetConcept, handleGetDesignRationale } from "./tools/concept.js";
|
|
17
|
+
import { handleGetDesignTokens, handleGetThemeCss, } from "./tools/design-tokens.js";
|
|
18
|
+
import { handleGetWireframeDetail, handleGetWireframes, } from "./tools/wireframes.js";
|
|
19
|
+
// --- コマンドライン引数パース ---
|
|
20
|
+
const VERSION = "0.1.0";
|
|
21
|
+
function parseArgs() {
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
24
|
+
console.log(`vaden-mcp-server v${VERSION}
|
|
25
|
+
|
|
26
|
+
Usage: vaden-mcp-server [options]
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--project-id <id> Vaden project ID (or VADEN_PROJECT_ID env)
|
|
30
|
+
--token <jwt> Registry token (or VADEN_TOKEN env)
|
|
31
|
+
--api-url <url> API endpoint (default: https://app.vaden.ai)
|
|
32
|
+
--help, -h Show this help message
|
|
33
|
+
--version, -v Show version number`);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
37
|
+
console.log(VERSION);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
let projectId = "";
|
|
41
|
+
let token = "";
|
|
42
|
+
let apiUrl = "";
|
|
43
|
+
for (let i = 0; i < args.length; i++) {
|
|
44
|
+
if (args[i] === "--project-id" && args[i + 1]) {
|
|
45
|
+
projectId = args[++i] ?? "";
|
|
46
|
+
}
|
|
47
|
+
else if (args[i] === "--token" && args[i + 1]) {
|
|
48
|
+
token = args[++i] ?? "";
|
|
49
|
+
}
|
|
50
|
+
else if (args[i] === "--api-url" && args[i + 1]) {
|
|
51
|
+
apiUrl = args[++i] ?? "";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// 環境変数フォールバック
|
|
55
|
+
projectId ||= process.env.VADEN_PROJECT_ID ?? "";
|
|
56
|
+
token ||= process.env.VADEN_TOKEN ?? "";
|
|
57
|
+
apiUrl ||= process.env.VADEN_API_URL ?? "https://app.vaden.ai";
|
|
58
|
+
if (!projectId)
|
|
59
|
+
throw new Error("--project-id または VADEN_PROJECT_ID が必要です");
|
|
60
|
+
if (!token)
|
|
61
|
+
throw new Error("--token または VADEN_TOKEN が必要です");
|
|
62
|
+
return { projectId, token, apiUrl };
|
|
63
|
+
}
|
|
64
|
+
// --- ツール定義 ---
|
|
65
|
+
const TOOLS = [
|
|
66
|
+
{
|
|
67
|
+
name: "get_design_tokens",
|
|
68
|
+
description: "デザイントークン(色・フォント・spacing・radius 等)を返します",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {},
|
|
72
|
+
required: [],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "get_theme_css",
|
|
77
|
+
description: "CSS 変数形式のテーマ(theme.css)を返します",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {},
|
|
81
|
+
required: [],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "get_component_list",
|
|
86
|
+
description: "利用可能なコンポーネントの一覧を返します",
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {},
|
|
90
|
+
required: [],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "get_component_spec",
|
|
95
|
+
description: "指定したコンポーネントの仕様(props・バリアント等)を返します",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {
|
|
99
|
+
component: {
|
|
100
|
+
type: "string",
|
|
101
|
+
description: "コンポーネント名(例: button, card)",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
required: ["component"],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "search_components",
|
|
109
|
+
description: "キーワードでコンポーネントを検索します(日本語対応)。'button', 'form', 'ボタン', 'primary' 等で検索できます",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
query: {
|
|
114
|
+
type: "string",
|
|
115
|
+
description: "検索クエリ(コンポーネント名・カテゴリ・バリアント・日本語キーワード)",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
required: ["query"],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "get_wireframes",
|
|
123
|
+
description: "ワイヤーフレームの画面一覧と遷移フローを返します。画面の目的・グループ・ジャーニーステージ・コンポーネント数と、画面間の遷移情報が含まれます",
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: "object",
|
|
126
|
+
properties: {},
|
|
127
|
+
required: [],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "get_wireframe_detail",
|
|
132
|
+
description: "指定した画面の詳細情報を返します。コンポーネント一覧・UI状態・ビヘイビア・モックデータ・関連画面が含まれます",
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: {
|
|
136
|
+
screen_id: {
|
|
137
|
+
type: "string",
|
|
138
|
+
description: "画面ID(get_wireframes で取得した id)",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
required: ["screen_id"],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "get_concept",
|
|
146
|
+
description: "コンセプト情報(ブランド人格・ムード・ビジュアルスタイル・ターゲットユーザー・デザイン原則)を返します",
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: {},
|
|
150
|
+
required: [],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "get_design_rationale",
|
|
155
|
+
description: "デザイン哲学・意思決定理由・UX原則・カラー戦略・タイポグラフィ選択の根拠を返します",
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: "object",
|
|
158
|
+
properties: {},
|
|
159
|
+
required: [],
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
// --- メイン ---
|
|
164
|
+
async function main() {
|
|
165
|
+
const { projectId, token, apiUrl } = parseArgs();
|
|
166
|
+
const client = new VadenMcpApiClient(apiUrl, token);
|
|
167
|
+
const server = new Server({ name: "vaden-mcp-server", version: VERSION }, { capabilities: { tools: {} } });
|
|
168
|
+
// ツール一覧を返す
|
|
169
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
170
|
+
tools: TOOLS.map((t) => ({
|
|
171
|
+
name: t.name,
|
|
172
|
+
description: t.description,
|
|
173
|
+
inputSchema: t.inputSchema,
|
|
174
|
+
})),
|
|
175
|
+
}));
|
|
176
|
+
// ツール呼び出し
|
|
177
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
178
|
+
const { name, arguments: args } = request.params;
|
|
179
|
+
switch (name) {
|
|
180
|
+
case "get_design_tokens": {
|
|
181
|
+
const content = await handleGetDesignTokens(client, projectId);
|
|
182
|
+
return { content };
|
|
183
|
+
}
|
|
184
|
+
case "get_theme_css": {
|
|
185
|
+
const content = await handleGetThemeCss(client, projectId);
|
|
186
|
+
return { content };
|
|
187
|
+
}
|
|
188
|
+
case "get_component_list": {
|
|
189
|
+
const content = await handleGetComponentList(client, projectId);
|
|
190
|
+
return { content };
|
|
191
|
+
}
|
|
192
|
+
case "get_component_spec": {
|
|
193
|
+
const componentName = String(args?.component ?? "");
|
|
194
|
+
if (!componentName) {
|
|
195
|
+
throw new Error("component パラメータが必要です");
|
|
196
|
+
}
|
|
197
|
+
const content = await handleGetComponentSpec(client, projectId, componentName);
|
|
198
|
+
return { content };
|
|
199
|
+
}
|
|
200
|
+
case "search_components": {
|
|
201
|
+
const query = String(args?.query ?? "");
|
|
202
|
+
if (!query) {
|
|
203
|
+
throw new Error("query パラメータが必要です");
|
|
204
|
+
}
|
|
205
|
+
const content = await handleSearchComponents(client, projectId, query);
|
|
206
|
+
return { content };
|
|
207
|
+
}
|
|
208
|
+
case "get_wireframes": {
|
|
209
|
+
const content = await handleGetWireframes(client, projectId);
|
|
210
|
+
return { content };
|
|
211
|
+
}
|
|
212
|
+
case "get_wireframe_detail": {
|
|
213
|
+
const screenId = String(args?.screen_id ?? "").trim();
|
|
214
|
+
if (!screenId) {
|
|
215
|
+
throw new Error("screen_id パラメータが必要です");
|
|
216
|
+
}
|
|
217
|
+
const content = await handleGetWireframeDetail(client, projectId, screenId);
|
|
218
|
+
return { content };
|
|
219
|
+
}
|
|
220
|
+
case "get_concept": {
|
|
221
|
+
const content = await handleGetConcept(client, projectId);
|
|
222
|
+
return { content };
|
|
223
|
+
}
|
|
224
|
+
case "get_design_rationale": {
|
|
225
|
+
const content = await handleGetDesignRationale(client, projectId);
|
|
226
|
+
return { content };
|
|
227
|
+
}
|
|
228
|
+
default:
|
|
229
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
const transport = new StdioServerTransport();
|
|
233
|
+
await server.connect(transport);
|
|
234
|
+
}
|
|
235
|
+
main().catch((err) => {
|
|
236
|
+
console.error(err);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* コンポーネント関連 MCP ツールハンドラー
|
|
3
|
+
*
|
|
4
|
+
* レジストリ API: GET /api/projects/{projectId}/design-system/registry
|
|
5
|
+
* レスポンス形式: shadcn/ui registry.json 互換
|
|
6
|
+
*/
|
|
7
|
+
import type { VadenMcpApiClient } from "../api-client.js";
|
|
8
|
+
export declare function handleGetComponentList(client: VadenMcpApiClient, projectId: string): Promise<{
|
|
9
|
+
type: "text";
|
|
10
|
+
text: string;
|
|
11
|
+
}[]>;
|
|
12
|
+
export declare function handleGetComponentSpec(client: VadenMcpApiClient, projectId: string, componentName: string): Promise<{
|
|
13
|
+
type: "text";
|
|
14
|
+
text: string;
|
|
15
|
+
}[]>;
|
|
16
|
+
export declare function handleSearchComponents(client: VadenMcpApiClient, projectId: string, query: string): Promise<{
|
|
17
|
+
type: "text";
|
|
18
|
+
text: string;
|
|
19
|
+
}[]>;
|
|
20
|
+
/** テスト用にキャッシュをクリアする */
|
|
21
|
+
export declare function clearRegistryCache(): void;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { componentMetadata } from "../guides/component-metadata.js";
|
|
2
|
+
import { generateComponentGuide } from "../guides/generate-guide.js";
|
|
3
|
+
let cache = new WeakMap();
|
|
4
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5分
|
|
5
|
+
function getClientCache(client) {
|
|
6
|
+
let clientCache = cache.get(client);
|
|
7
|
+
if (!clientCache) {
|
|
8
|
+
clientCache = new Map();
|
|
9
|
+
cache.set(client, clientCache);
|
|
10
|
+
}
|
|
11
|
+
return clientCache;
|
|
12
|
+
}
|
|
13
|
+
function getCached(client, projectId) {
|
|
14
|
+
const entry = getClientCache(client).get(projectId);
|
|
15
|
+
if (!entry)
|
|
16
|
+
return null;
|
|
17
|
+
if (Date.now() > entry.expiresAt) {
|
|
18
|
+
getClientCache(client).delete(projectId);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return entry.data;
|
|
22
|
+
}
|
|
23
|
+
function setCached(client, projectId, data) {
|
|
24
|
+
getClientCache(client).set(projectId, {
|
|
25
|
+
data,
|
|
26
|
+
expiresAt: Date.now() + CACHE_TTL_MS,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
let pending = new WeakMap();
|
|
30
|
+
function getClientPending(client) {
|
|
31
|
+
let clientPending = pending.get(client);
|
|
32
|
+
if (!clientPending) {
|
|
33
|
+
clientPending = new Map();
|
|
34
|
+
pending.set(client, clientPending);
|
|
35
|
+
}
|
|
36
|
+
return clientPending;
|
|
37
|
+
}
|
|
38
|
+
async function fetchRegistry(client, projectId) {
|
|
39
|
+
const cached = getCached(client, projectId);
|
|
40
|
+
if (cached)
|
|
41
|
+
return cached;
|
|
42
|
+
const inFlight = getClientPending(client).get(projectId);
|
|
43
|
+
if (inFlight)
|
|
44
|
+
return inFlight;
|
|
45
|
+
const request = client
|
|
46
|
+
.get(`/api/projects/${encodeURIComponent(projectId)}/design-system/registry`)
|
|
47
|
+
.then((data) => {
|
|
48
|
+
setCached(client, projectId, data);
|
|
49
|
+
return data;
|
|
50
|
+
})
|
|
51
|
+
.finally(() => {
|
|
52
|
+
getClientPending(client).delete(projectId);
|
|
53
|
+
});
|
|
54
|
+
getClientPending(client).set(projectId, request);
|
|
55
|
+
return request;
|
|
56
|
+
}
|
|
57
|
+
export async function handleGetComponentList(client, projectId) {
|
|
58
|
+
const registry = await fetchRegistry(client, projectId);
|
|
59
|
+
const components = registry.items.map((item) => ({
|
|
60
|
+
name: item.name,
|
|
61
|
+
title: item.title ?? item.name,
|
|
62
|
+
description: item.description ?? "",
|
|
63
|
+
categories: item.categories ?? [],
|
|
64
|
+
}));
|
|
65
|
+
return [{ type: "text", text: JSON.stringify({ components }, null, 2) }];
|
|
66
|
+
}
|
|
67
|
+
export async function handleGetComponentSpec(client, projectId, componentName) {
|
|
68
|
+
const registry = await fetchRegistry(client, projectId);
|
|
69
|
+
const item = registry.items.find((i) => i.name === componentName);
|
|
70
|
+
if (!item) {
|
|
71
|
+
const available = registry.items.map((i) => i.name).join(", ");
|
|
72
|
+
throw new Error(`Component "${componentName}" not found. Available: ${available}`);
|
|
73
|
+
}
|
|
74
|
+
const spec = {
|
|
75
|
+
name: item.name,
|
|
76
|
+
type: item.type,
|
|
77
|
+
title: item.title ?? item.name,
|
|
78
|
+
description: item.description ?? "",
|
|
79
|
+
categories: item.categories ?? [],
|
|
80
|
+
registryDependencies: item.registryDependencies ?? [],
|
|
81
|
+
dependencies: item.dependencies ?? [],
|
|
82
|
+
files: item.files ?? [],
|
|
83
|
+
cssVars: {
|
|
84
|
+
light: item.cssVars?.light ?? {},
|
|
85
|
+
dark: item.cssVars?.dark ?? {},
|
|
86
|
+
},
|
|
87
|
+
meta: item.meta ?? {},
|
|
88
|
+
};
|
|
89
|
+
// ガイドを生成(メタデータがあれば)
|
|
90
|
+
const guide = generateComponentGuide({
|
|
91
|
+
name: spec.name,
|
|
92
|
+
title: spec.title,
|
|
93
|
+
description: spec.description,
|
|
94
|
+
categories: spec.categories,
|
|
95
|
+
});
|
|
96
|
+
const parts = [];
|
|
97
|
+
if (guide) {
|
|
98
|
+
parts.push(guide);
|
|
99
|
+
parts.push("", "---", "");
|
|
100
|
+
}
|
|
101
|
+
parts.push("## Registry Spec (JSON)");
|
|
102
|
+
parts.push("```json");
|
|
103
|
+
parts.push(JSON.stringify(spec, null, 2));
|
|
104
|
+
parts.push("```");
|
|
105
|
+
return [{ type: "text", text: parts.join("\n") }];
|
|
106
|
+
}
|
|
107
|
+
// --- search_components ---
|
|
108
|
+
const JA_TO_EN = {
|
|
109
|
+
ボタン: ["button"],
|
|
110
|
+
入力: ["input", "textarea"],
|
|
111
|
+
カード: ["card"],
|
|
112
|
+
テーブル: ["table", "data-table"],
|
|
113
|
+
ダイアログ: ["dialog", "alert-dialog"],
|
|
114
|
+
モーダル: ["dialog"],
|
|
115
|
+
メニュー: ["menubar", "dropdown-menu", "context-menu"],
|
|
116
|
+
ナビ: ["navigation-menu", "breadcrumb", "sidebar"],
|
|
117
|
+
フォーム: ["form", "input", "select", "checkbox", "radio-group"],
|
|
118
|
+
タブ: ["tabs"],
|
|
119
|
+
アコーディオン: ["accordion", "collapsible"],
|
|
120
|
+
通知: ["toast", "alert", "sonner"],
|
|
121
|
+
アバター: ["avatar"],
|
|
122
|
+
バッジ: ["badge"],
|
|
123
|
+
スイッチ: ["switch"],
|
|
124
|
+
スライダー: ["slider"],
|
|
125
|
+
ツールチップ: ["tooltip"],
|
|
126
|
+
};
|
|
127
|
+
function scoreComponent(item, query) {
|
|
128
|
+
const q = query.toLowerCase();
|
|
129
|
+
const name = item.name.toLowerCase();
|
|
130
|
+
const title = (item.title ?? item.name).toLowerCase();
|
|
131
|
+
const description = (item.description ?? "").toLowerCase();
|
|
132
|
+
const categories = (item.categories ?? []).map((c) => c.toLowerCase());
|
|
133
|
+
const meta = componentMetadata[item.name];
|
|
134
|
+
const variantValues = [];
|
|
135
|
+
const variantKeys = [];
|
|
136
|
+
if (meta?.variants) {
|
|
137
|
+
for (const [key, vals] of Object.entries(meta.variants)) {
|
|
138
|
+
variantKeys.push(key.toLowerCase());
|
|
139
|
+
for (const v of vals) {
|
|
140
|
+
variantValues.push(v.toLowerCase());
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const slots = (meta?.slots ?? []).map((s) => s.toLowerCase());
|
|
145
|
+
const type = meta?.type ?? "";
|
|
146
|
+
let score = 0;
|
|
147
|
+
// 完全一致(100) > 前方一致(80) > 部分一致(60) > カテゴリ一致(40)
|
|
148
|
+
const checkField = (field) => {
|
|
149
|
+
if (field === q)
|
|
150
|
+
return 100;
|
|
151
|
+
if (field.startsWith(q))
|
|
152
|
+
return 80;
|
|
153
|
+
if (field.includes(q))
|
|
154
|
+
return 60;
|
|
155
|
+
return 0;
|
|
156
|
+
};
|
|
157
|
+
score = Math.max(score, checkField(name));
|
|
158
|
+
score = Math.max(score, checkField(title));
|
|
159
|
+
if (description.includes(q)) {
|
|
160
|
+
score = Math.max(score, 60);
|
|
161
|
+
}
|
|
162
|
+
for (const cat of categories) {
|
|
163
|
+
if (cat.includes(q) || q.includes(cat)) {
|
|
164
|
+
score = Math.max(score, 40);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const vk of variantKeys) {
|
|
168
|
+
if (vk === q || vk.includes(q))
|
|
169
|
+
score = Math.max(score, 60);
|
|
170
|
+
}
|
|
171
|
+
for (const vv of variantValues) {
|
|
172
|
+
if (vv === q || vv.includes(q))
|
|
173
|
+
score = Math.max(score, 60);
|
|
174
|
+
}
|
|
175
|
+
for (const slot of slots) {
|
|
176
|
+
if (slot === q || slot.includes(q))
|
|
177
|
+
score = Math.max(score, 40);
|
|
178
|
+
}
|
|
179
|
+
if (type === q)
|
|
180
|
+
score = Math.max(score, 60);
|
|
181
|
+
return score;
|
|
182
|
+
}
|
|
183
|
+
export async function handleSearchComponents(client, projectId, query) {
|
|
184
|
+
const normalizedQuery = query.trim();
|
|
185
|
+
if (!normalizedQuery) {
|
|
186
|
+
return [
|
|
187
|
+
{
|
|
188
|
+
type: "text",
|
|
189
|
+
text: `No components found matching "${query}". Use \`get_component_list\` to see all available components.`,
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
const registry = await fetchRegistry(client, projectId);
|
|
194
|
+
// 日本語→英語マッピング展開
|
|
195
|
+
const queries = [normalizedQuery];
|
|
196
|
+
for (const [ja, ens] of Object.entries(JA_TO_EN)) {
|
|
197
|
+
if (normalizedQuery.includes(ja)) {
|
|
198
|
+
queries.push(...ens);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const scored = registry.items.map((item) => {
|
|
202
|
+
const maxScore = Math.max(...queries.map((q) => scoreComponent(item, q)));
|
|
203
|
+
return {
|
|
204
|
+
name: item.name,
|
|
205
|
+
title: item.title ?? item.name,
|
|
206
|
+
description: item.description ?? "",
|
|
207
|
+
categories: item.categories ?? [],
|
|
208
|
+
matchScore: maxScore,
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
const matched = scored
|
|
212
|
+
.filter((c) => c.matchScore > 0)
|
|
213
|
+
.sort((a, b) => b.matchScore - a.matchScore)
|
|
214
|
+
.slice(0, 10);
|
|
215
|
+
if (matched.length === 0) {
|
|
216
|
+
return [
|
|
217
|
+
{
|
|
218
|
+
type: "text",
|
|
219
|
+
text: `No components found matching "${query}". Use \`get_component_list\` to see all available components.`,
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
const header = "| Component | Title | Categories | Score |\n|-----------|-------|------------|-------|\n";
|
|
224
|
+
const rows = matched
|
|
225
|
+
.map((c) => `| \`${c.name}\` | ${c.title} | ${c.categories.join(", ")} | ${c.matchScore} |`)
|
|
226
|
+
.join("\n");
|
|
227
|
+
const text = `## Search Results for "${query}"\n\n${header}${rows}\n\nUse \`get_component_spec\` with a component name for full details.`;
|
|
228
|
+
return [{ type: "text", text }];
|
|
229
|
+
}
|
|
230
|
+
/** テスト用にキャッシュをクリアする */
|
|
231
|
+
export function clearRegistryCache() {
|
|
232
|
+
cache = new WeakMap();
|
|
233
|
+
pending = new WeakMap();
|
|
234
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* コンセプト関連 MCP ツールハンドラー
|
|
3
|
+
*
|
|
4
|
+
* コンセプト API: GET /api/projects/{projectId}/concept
|
|
5
|
+
*/
|
|
6
|
+
import type { VadenMcpApiClient } from "../api-client.js";
|
|
7
|
+
/**
|
|
8
|
+
* コンセプト情報を返す
|
|
9
|
+
*
|
|
10
|
+
* @param client - Vaden MCP API クライアント
|
|
11
|
+
* @param projectId - プロジェクトID
|
|
12
|
+
*/
|
|
13
|
+
export declare function handleGetConcept(client: VadenMcpApiClient, projectId: string): Promise<{
|
|
14
|
+
type: "text";
|
|
15
|
+
text: string;
|
|
16
|
+
}[]>;
|
|
17
|
+
/**
|
|
18
|
+
* デザイン意思決定理由を返す
|
|
19
|
+
*
|
|
20
|
+
* @param client - Vaden MCP API クライアント
|
|
21
|
+
* @param projectId - プロジェクトID
|
|
22
|
+
*/
|
|
23
|
+
export declare function handleGetDesignRationale(client: VadenMcpApiClient, projectId: string): Promise<{
|
|
24
|
+
type: "text";
|
|
25
|
+
text: string;
|
|
26
|
+
}[]>;
|