@miyucy/storybook-mcp 1.0.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/dist/index.js +254 -0
- package/package.json +29 -0
package/dist/index.js
ADDED
@@ -0,0 +1,254 @@
|
|
1
|
+
import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
3
|
+
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
4
|
+
import path from "node:path";
|
5
|
+
import fs from "node:fs/promises";
|
6
|
+
import process from "node:process";
|
7
|
+
import { exec } from "node:child_process";
|
8
|
+
import { z } from "zod";
|
9
|
+
const escapeRegExp = (str) => str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
|
10
|
+
class Storybook {
|
11
|
+
constructor(storybookRoot) {
|
12
|
+
this.CHUNK_SIZE = 6;
|
13
|
+
this.server = new McpServer({
|
14
|
+
name: "storybook",
|
15
|
+
description: "",
|
16
|
+
version: "0.1.0",
|
17
|
+
});
|
18
|
+
this.storybookRoot = storybookRoot;
|
19
|
+
this.componentInformation = [];
|
20
|
+
this.loadStorybookData();
|
21
|
+
this.registerStorybook();
|
22
|
+
}
|
23
|
+
async run() {
|
24
|
+
await this.server.connect(new StdioServerTransport());
|
25
|
+
}
|
26
|
+
registerStorybook() {
|
27
|
+
this.server.resource("Storybookの情報を返します", "storybook://stories", async (uri) => {
|
28
|
+
const inf = this.componentInformation;
|
29
|
+
if (inf.length === 0) {
|
30
|
+
return { contents: [{ uri: uri.href, text: "No stories found." }] };
|
31
|
+
}
|
32
|
+
return {
|
33
|
+
contents: [{ uri: uri.href, text: JSON.stringify(inf, null, 2) }],
|
34
|
+
};
|
35
|
+
});
|
36
|
+
this.server.tool("get_storybooks", "Storybookの情報を返します", {}, async () => {
|
37
|
+
const inf = this.componentInformation;
|
38
|
+
if (inf.length === 0) {
|
39
|
+
return {
|
40
|
+
content: [{ type: "text", text: "No stories found." }],
|
41
|
+
};
|
42
|
+
}
|
43
|
+
return {
|
44
|
+
content: inf.map((entry) => ({
|
45
|
+
type: "text",
|
46
|
+
text: JSON.stringify(entry, null, 2),
|
47
|
+
})),
|
48
|
+
};
|
49
|
+
});
|
50
|
+
this.server.resource("Storybookのタイトルや説明をクエリーします\nクエリーは大文字・小文字を区別しません", new ResourceTemplate("storybook://stories?{query}", { list: undefined }), async (uri, { query }) => {
|
51
|
+
const inf = this.componentInformation;
|
52
|
+
if (inf.length === 0) {
|
53
|
+
return { contents: [{ uri: uri.href, text: "No stories found." }] };
|
54
|
+
}
|
55
|
+
// queryがない場合はエラー
|
56
|
+
if (!query) {
|
57
|
+
throw new McpError(ErrorCode.InvalidParams, "Query is required.");
|
58
|
+
}
|
59
|
+
const filtered = this.queryComponentInformation(inf, query);
|
60
|
+
return {
|
61
|
+
contents: [
|
62
|
+
{ uri: uri.href, text: JSON.stringify(filtered, null, 2) },
|
63
|
+
],
|
64
|
+
};
|
65
|
+
});
|
66
|
+
this.server.tool("query_storybooks", "Storybookのタイトルや説明をクエリーします\nクエリーは大文字・小文字を区別しません", { query: z.string() }, async ({ query }) => {
|
67
|
+
const inf = this.componentInformation;
|
68
|
+
if (inf.length === 0) {
|
69
|
+
return {
|
70
|
+
content: [{ type: "text", text: "No stories found." }],
|
71
|
+
};
|
72
|
+
}
|
73
|
+
// queryがない場合はエラー
|
74
|
+
if (!query) {
|
75
|
+
throw new McpError(ErrorCode.InvalidParams, "Query is required.");
|
76
|
+
}
|
77
|
+
const filtered = this.queryComponentInformation(inf, query);
|
78
|
+
return {
|
79
|
+
content: filtered.map((entry) => ({
|
80
|
+
type: "text",
|
81
|
+
text: JSON.stringify(entry, null, 2),
|
82
|
+
})),
|
83
|
+
};
|
84
|
+
});
|
85
|
+
}
|
86
|
+
queryComponentInformation(inf, query) {
|
87
|
+
const isMatch = (entry, q) => {
|
88
|
+
const regex = new RegExp(escapeRegExp(q), "i");
|
89
|
+
return (entry.title.match(regex) ||
|
90
|
+
entry.name.match(regex) ||
|
91
|
+
entry.docgenInfo.some((info) => {
|
92
|
+
if (info.displayName.match(regex)) {
|
93
|
+
return true;
|
94
|
+
}
|
95
|
+
if (info.description?.match(regex)) {
|
96
|
+
return true;
|
97
|
+
}
|
98
|
+
return false;
|
99
|
+
}));
|
100
|
+
};
|
101
|
+
if (Array.isArray(query)) {
|
102
|
+
return inf.filter((entry) => query.some((q) => isMatch(entry, q)));
|
103
|
+
}
|
104
|
+
return inf.filter((entry) => isMatch(entry, query));
|
105
|
+
}
|
106
|
+
async loadStorybookData() {
|
107
|
+
// storybook-static/index.jsonを読み込む
|
108
|
+
console.debug("Loading storybook data...");
|
109
|
+
const content = await fs.readFile(path.resolve(this.storybookRoot, "storybook-static", "index.json"), "utf-8");
|
110
|
+
const data = JSON.parse(content);
|
111
|
+
console.debug("Loaded storybook data successfully.");
|
112
|
+
if (data.v !== 5) {
|
113
|
+
throw new Error("Invalid version (expected 5)");
|
114
|
+
}
|
115
|
+
const entries = [];
|
116
|
+
// entriesの中身を確認する
|
117
|
+
for (const entry of Object.values(data.entries)) {
|
118
|
+
// typeがstoryでないものは無視する
|
119
|
+
if (entry.type !== "story") {
|
120
|
+
continue;
|
121
|
+
}
|
122
|
+
// idがないものは無視する
|
123
|
+
if (!entry.id) {
|
124
|
+
continue;
|
125
|
+
}
|
126
|
+
// nameがないものは無視する
|
127
|
+
if (!entry.name) {
|
128
|
+
continue;
|
129
|
+
}
|
130
|
+
// titleがないものは無視する
|
131
|
+
if (!entry.title) {
|
132
|
+
continue;
|
133
|
+
}
|
134
|
+
// componentPathがないものは無視する
|
135
|
+
if (!entry.componentPath) {
|
136
|
+
continue;
|
137
|
+
}
|
138
|
+
// importPathがないものは無視する
|
139
|
+
if (!entry.importPath) {
|
140
|
+
continue;
|
141
|
+
}
|
142
|
+
entries.push(entry);
|
143
|
+
}
|
144
|
+
console.debug(`Found ${entries.length} entries.`);
|
145
|
+
{
|
146
|
+
const chunks = Array.from(new Set(entries.map((entry) => entry.componentPath))).reduce((r, e) => {
|
147
|
+
const last = r[r.length - 1];
|
148
|
+
if (last.length < this.CHUNK_SIZE) {
|
149
|
+
last.push(e);
|
150
|
+
}
|
151
|
+
else {
|
152
|
+
r.push([e]);
|
153
|
+
}
|
154
|
+
return r;
|
155
|
+
}, [[]]);
|
156
|
+
for await (const chunk of chunks) {
|
157
|
+
const docgenInfos = {};
|
158
|
+
// docgenInfoを取得する
|
159
|
+
const results = await Promise.allSettled(chunk.map((componentPath) => this.getComponentInfo(componentPath)));
|
160
|
+
for (let i = 0; i < results.length; i++) {
|
161
|
+
const result = results[i];
|
162
|
+
if (result.status === "fulfilled") {
|
163
|
+
docgenInfos[chunk[i]] = result.value;
|
164
|
+
}
|
165
|
+
else {
|
166
|
+
console.error(`Failed to get component info for ${chunk[i]}:`, result.reason);
|
167
|
+
}
|
168
|
+
}
|
169
|
+
// componentInformationを作成する(随時更新)
|
170
|
+
for (const entry of entries) {
|
171
|
+
for (const componentPath of chunk) {
|
172
|
+
if (entry.componentPath === componentPath) {
|
173
|
+
this.componentInformation.push({
|
174
|
+
title: entry.title,
|
175
|
+
id: entry.id,
|
176
|
+
name: entry.name,
|
177
|
+
importPath: entry.importPath,
|
178
|
+
componentPath: entry.componentPath,
|
179
|
+
docgenInfo: docgenInfos[entry.componentPath] || [],
|
180
|
+
});
|
181
|
+
}
|
182
|
+
}
|
183
|
+
}
|
184
|
+
}
|
185
|
+
}
|
186
|
+
// for await (const entry of entries) {
|
187
|
+
// if (docgenInfos[entry.componentPath]) {
|
188
|
+
// // すでに取得済みの場合はスキップ
|
189
|
+
// continue;
|
190
|
+
// }
|
191
|
+
// try {
|
192
|
+
// console.debug(`Getting component info for ${entry.componentPath}...`);
|
193
|
+
// const infos = await this.getComponentInfo(entry.componentPath);
|
194
|
+
// if (infos.length > 0) {
|
195
|
+
// console.debug(`Got ${infos.length} component info for ${entry.componentPath}.`);
|
196
|
+
// docgenInfos[entry.componentPath] = infos;
|
197
|
+
// }
|
198
|
+
// } catch (error) {
|
199
|
+
// console.error(`Failed to get component info for ${entry.componentPath}:`, error);
|
200
|
+
// }
|
201
|
+
// }
|
202
|
+
// componentInformationを作成する
|
203
|
+
// for (const entry of entries) {
|
204
|
+
// this.componentInformation.push({
|
205
|
+
// id: entry.id,
|
206
|
+
// name: entry.name,
|
207
|
+
// title: entry.title,
|
208
|
+
// importPath: entry.importPath,
|
209
|
+
// componentPath: entry.componentPath,
|
210
|
+
// docgenInfo: docgenInfos[entry.componentPath] || [],
|
211
|
+
// });
|
212
|
+
// }
|
213
|
+
console.debug("Component information loaded successfully.");
|
214
|
+
}
|
215
|
+
// npx -y @react-docgen/cli --resolver find-all-exported-components を実行して標準出力からコンポーネントの情報を取得する
|
216
|
+
async getComponentInfo(importPath) {
|
217
|
+
const regex = /\.[jt]sx$/;
|
218
|
+
const argImportPath = regex.test(importPath)
|
219
|
+
? importPath
|
220
|
+
: `${importPath}/index.[jt]sx`;
|
221
|
+
return new Promise((resolve, reject) => {
|
222
|
+
console.debug(`Executing command to get component info for ${argImportPath}...`);
|
223
|
+
exec(`npx -y @react-docgen/cli --resolver find-all-exported-components '${argImportPath}'`, {
|
224
|
+
cwd: this.storybookRoot,
|
225
|
+
env: { ...process.env, NO_COLOR: "true" },
|
226
|
+
}, (error, stdout, _stderr) => {
|
227
|
+
if (error) {
|
228
|
+
reject(error);
|
229
|
+
return;
|
230
|
+
}
|
231
|
+
if (stdout === "") {
|
232
|
+
resolve([]);
|
233
|
+
return;
|
234
|
+
}
|
235
|
+
const data = JSON.parse(stdout);
|
236
|
+
resolve(Object.values(data).flat());
|
237
|
+
});
|
238
|
+
});
|
239
|
+
}
|
240
|
+
}
|
241
|
+
// --- サーバー起動 ---
|
242
|
+
async function main() {
|
243
|
+
const storybookRoot = process.env.STORYBOOK_ROOT;
|
244
|
+
if (!storybookRoot) {
|
245
|
+
console.error("STORYBOOK_ROOT environment variable is not set.");
|
246
|
+
process.exit(1);
|
247
|
+
}
|
248
|
+
const absStorybookRoot = path.resolve(storybookRoot);
|
249
|
+
await new Storybook(absStorybookRoot).run();
|
250
|
+
}
|
251
|
+
main().catch((error) => {
|
252
|
+
console.error("Error starting server:", error);
|
253
|
+
process.exit(1);
|
254
|
+
});
|
package/package.json
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
{
|
2
|
+
"name": "@miyucy/storybook-mcp",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"description": "",
|
5
|
+
"main": "dist/index.js",
|
6
|
+
"scripts": {
|
7
|
+
"build": "rm -rf dist && tsc",
|
8
|
+
"start": "node dist/index.js"
|
9
|
+
},
|
10
|
+
"author": "miyucy",
|
11
|
+
"publishConfig": {
|
12
|
+
"access": "public"
|
13
|
+
},
|
14
|
+
"license": "MIT",
|
15
|
+
"files": [
|
16
|
+
"dist",
|
17
|
+
"package.json"
|
18
|
+
],
|
19
|
+
"type": "module",
|
20
|
+
"dependencies": {
|
21
|
+
"@modelcontextprotocol/sdk": "1.10.1",
|
22
|
+
"zod": "3.24.3"
|
23
|
+
},
|
24
|
+
"devDependencies": {
|
25
|
+
"@types/node": "22.14.1",
|
26
|
+
"typescript": "5.8.3"
|
27
|
+
},
|
28
|
+
"bin": "dist/index.js"
|
29
|
+
}
|