@miyucy/storybook-mcp 1.0.2 → 1.0.3
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 +71 -94
- package/dist/parser.js +32 -0
- package/package.json +3 -1
package/dist/index.js
CHANGED
@@ -5,12 +5,14 @@ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import path from "node:path";
|
6
6
|
import fs from "node:fs/promises";
|
7
7
|
import process from "node:process";
|
8
|
-
import
|
8
|
+
import os from "node:os";
|
9
9
|
import { z } from "zod";
|
10
|
+
import glob from "fast-glob";
|
11
|
+
import { Worker } from "node:worker_threads";
|
10
12
|
const escapeRegExp = (str) => str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
|
11
13
|
class Storybook {
|
12
14
|
constructor(storybookRoot) {
|
13
|
-
this.
|
15
|
+
this.AVAILABLE_PARALLELISM = os.availableParallelism();
|
14
16
|
this.server = new McpServer({
|
15
17
|
name: "storybook",
|
16
18
|
description: "",
|
@@ -26,7 +28,7 @@ class Storybook {
|
|
26
28
|
}
|
27
29
|
registerStorybook() {
|
28
30
|
this.server.resource("Storybookの情報を返します", "storybook://stories", async (uri) => {
|
29
|
-
const inf = this.componentInformation;
|
31
|
+
const inf = [...this.componentInformation];
|
30
32
|
if (inf.length === 0) {
|
31
33
|
return { contents: [{ uri: uri.href, text: "No stories found." }] };
|
32
34
|
}
|
@@ -35,7 +37,7 @@ class Storybook {
|
|
35
37
|
};
|
36
38
|
});
|
37
39
|
this.server.tool("get_storybooks", "Storybookの情報を返します", {}, async () => {
|
38
|
-
const inf = this.componentInformation;
|
40
|
+
const inf = [...this.componentInformation];
|
39
41
|
if (inf.length === 0) {
|
40
42
|
return {
|
41
43
|
content: [{ type: "text", text: "No stories found." }],
|
@@ -49,7 +51,7 @@ class Storybook {
|
|
49
51
|
};
|
50
52
|
});
|
51
53
|
this.server.resource("Storybookのタイトルや説明をクエリーします\nクエリーは大文字・小文字を区別しません", new ResourceTemplate("storybook://stories?{query}", { list: undefined }), async (uri, { query }) => {
|
52
|
-
const inf = this.componentInformation;
|
54
|
+
const inf = [...this.componentInformation];
|
53
55
|
if (inf.length === 0) {
|
54
56
|
return { contents: [{ uri: uri.href, text: "No stories found." }] };
|
55
57
|
}
|
@@ -65,7 +67,7 @@ class Storybook {
|
|
65
67
|
};
|
66
68
|
});
|
67
69
|
this.server.tool("query_storybooks", "Storybookのタイトルや説明をクエリーします\nクエリーは大文字・小文字を区別しません", { query: z.string() }, async ({ query }) => {
|
68
|
-
const inf = this.componentInformation;
|
70
|
+
const inf = [...this.componentInformation];
|
69
71
|
if (inf.length === 0) {
|
70
72
|
return {
|
71
73
|
content: [{ type: "text", text: "No stories found." }],
|
@@ -143,100 +145,75 @@ class Storybook {
|
|
143
145
|
entries.push(entry);
|
144
146
|
}
|
145
147
|
console.debug(`Found ${entries.length} entries.`);
|
146
|
-
|
147
|
-
const chunks = Array.from(new Set(entries.map((entry) => entry.componentPath))).reduce((r, e) => {
|
148
|
-
const last = r[r.length - 1];
|
149
|
-
if (last.length < this.CHUNK_SIZE) {
|
150
|
-
last.push(e);
|
151
|
-
}
|
152
|
-
else {
|
153
|
-
r.push([e]);
|
154
|
-
}
|
155
|
-
return r;
|
156
|
-
}, [[]]);
|
157
|
-
for await (const chunk of chunks) {
|
158
|
-
const docgenInfos = {};
|
159
|
-
// docgenInfoを取得する
|
160
|
-
const results = await Promise.allSettled(chunk.map((componentPath) => this.getComponentInfo(componentPath)));
|
161
|
-
for (let i = 0; i < results.length; i++) {
|
162
|
-
const result = results[i];
|
163
|
-
if (result.status === "fulfilled") {
|
164
|
-
docgenInfos[chunk[i]] = result.value;
|
165
|
-
}
|
166
|
-
else {
|
167
|
-
console.error(`Failed to get component info for ${chunk[i]}:`, result.reason);
|
168
|
-
}
|
169
|
-
}
|
170
|
-
// componentInformationを作成する(随時更新)
|
171
|
-
for (const entry of entries) {
|
172
|
-
for (const componentPath of chunk) {
|
173
|
-
if (entry.componentPath === componentPath) {
|
174
|
-
this.componentInformation.push({
|
175
|
-
title: entry.title,
|
176
|
-
id: entry.id,
|
177
|
-
name: entry.name,
|
178
|
-
importPath: entry.importPath,
|
179
|
-
componentPath: entry.componentPath,
|
180
|
-
docgenInfo: docgenInfos[entry.componentPath] || [],
|
181
|
-
});
|
182
|
-
}
|
183
|
-
}
|
184
|
-
}
|
185
|
-
}
|
186
|
-
}
|
187
|
-
// for await (const entry of entries) {
|
188
|
-
// if (docgenInfos[entry.componentPath]) {
|
189
|
-
// // すでに取得済みの場合はスキップ
|
190
|
-
// continue;
|
191
|
-
// }
|
192
|
-
// try {
|
193
|
-
// console.debug(`Getting component info for ${entry.componentPath}...`);
|
194
|
-
// const infos = await this.getComponentInfo(entry.componentPath);
|
195
|
-
// if (infos.length > 0) {
|
196
|
-
// console.debug(`Got ${infos.length} component info for ${entry.componentPath}.`);
|
197
|
-
// docgenInfos[entry.componentPath] = infos;
|
198
|
-
// }
|
199
|
-
// } catch (error) {
|
200
|
-
// console.error(`Failed to get component info for ${entry.componentPath}:`, error);
|
201
|
-
// }
|
202
|
-
// }
|
203
|
-
// componentInformationを作成する
|
204
|
-
// for (const entry of entries) {
|
205
|
-
// this.componentInformation.push({
|
206
|
-
// id: entry.id,
|
207
|
-
// name: entry.name,
|
208
|
-
// title: entry.title,
|
209
|
-
// importPath: entry.importPath,
|
210
|
-
// componentPath: entry.componentPath,
|
211
|
-
// docgenInfo: docgenInfos[entry.componentPath] || [],
|
212
|
-
// });
|
213
|
-
// }
|
148
|
+
this.loadStorybookEntries(entries);
|
214
149
|
console.debug("Component information loaded successfully.");
|
215
150
|
}
|
216
|
-
//
|
217
|
-
async
|
151
|
+
//
|
152
|
+
async loadStorybookEntries(entries) {
|
153
|
+
const componentPaths = Array.from(new Set(entries.map((entry) => entry.componentPath)));
|
154
|
+
const resolved = {};
|
218
155
|
const regex = /\.[jt]sx$/;
|
219
|
-
const
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
156
|
+
for await (const componentPath of componentPaths) {
|
157
|
+
if (regex.test(componentPath)) {
|
158
|
+
resolved[componentPath] = path.resolve(this.storybookRoot, componentPath);
|
159
|
+
}
|
160
|
+
else {
|
161
|
+
const paths = await glob(`${componentPath}/index.[jt]sx`, {
|
162
|
+
cwd: this.storybookRoot,
|
163
|
+
onlyFiles: true,
|
164
|
+
absolute: true,
|
165
|
+
});
|
166
|
+
if (paths.length > 0) {
|
167
|
+
resolved[componentPath] = path.resolve(this.storybookRoot, paths[0]);
|
231
168
|
}
|
232
|
-
|
233
|
-
|
234
|
-
|
169
|
+
}
|
170
|
+
}
|
171
|
+
const tasks = componentPaths.map((componentPath) => {
|
172
|
+
const filepath = resolved[componentPath];
|
173
|
+
return { filepath, componentPath };
|
174
|
+
});
|
175
|
+
const workers = [];
|
176
|
+
const processNext = () => {
|
177
|
+
const worker = workers.pop();
|
178
|
+
if (!worker) {
|
179
|
+
return;
|
180
|
+
}
|
181
|
+
const task = tasks.pop();
|
182
|
+
if (!task) {
|
183
|
+
worker.terminate();
|
184
|
+
return;
|
185
|
+
}
|
186
|
+
worker.postMessage(task);
|
187
|
+
};
|
188
|
+
for (let i = 0; i < this.AVAILABLE_PARALLELISM; i++) {
|
189
|
+
const worker = new Worker("./dist/parser.js");
|
190
|
+
worker.on("message", (event) => {
|
191
|
+
// console.log("message", event);
|
192
|
+
const { componentPath, data } = event;
|
193
|
+
if (event.type === "error") {
|
194
|
+
console.error(`Failed to get component info for ${event.filepath}:`, event.error);
|
235
195
|
}
|
236
|
-
const
|
237
|
-
|
196
|
+
for (const entry of entries) {
|
197
|
+
if (entry.componentPath === componentPath) {
|
198
|
+
this.componentInformation.push({
|
199
|
+
title: entry.title,
|
200
|
+
id: entry.id,
|
201
|
+
name: entry.name,
|
202
|
+
importPath: entry.importPath,
|
203
|
+
componentPath: entry.componentPath,
|
204
|
+
docgenInfo: data || [],
|
205
|
+
});
|
206
|
+
}
|
207
|
+
}
|
208
|
+
console.log(`${this.componentInformation.length} components loaded.`);
|
209
|
+
workers.push(worker);
|
210
|
+
processNext();
|
238
211
|
});
|
239
|
-
|
212
|
+
workers.push(worker);
|
213
|
+
}
|
214
|
+
for (let i = 0; i < Math.min(tasks.length, workers.length); i++) {
|
215
|
+
processNext();
|
216
|
+
}
|
240
217
|
}
|
241
218
|
}
|
242
219
|
// --- サーバー起動 ---
|
package/dist/parser.js
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
import { builtinHandlers, parse, builtinResolvers } from "react-docgen";
|
2
|
+
import { parentPort } from "node:worker_threads";
|
3
|
+
import { readFile } from "node:fs/promises";
|
4
|
+
const resolver = new builtinResolvers.FindExportedDefinitionsResolver();
|
5
|
+
const handlers = Object.values(builtinHandlers);
|
6
|
+
parentPort?.on("message", async (message) => {
|
7
|
+
// console.log("message", message);
|
8
|
+
if (message.filepath) {
|
9
|
+
try {
|
10
|
+
const content = await readFile(message.filepath, "utf-8");
|
11
|
+
const result = parse(content, {
|
12
|
+
filename: message.filepath,
|
13
|
+
handlers,
|
14
|
+
resolver,
|
15
|
+
});
|
16
|
+
parentPort?.postMessage({
|
17
|
+
type: "result",
|
18
|
+
filepath: message.filepath,
|
19
|
+
componentPath: message.componentPath,
|
20
|
+
data: result,
|
21
|
+
});
|
22
|
+
}
|
23
|
+
catch (error) {
|
24
|
+
parentPort?.postMessage({
|
25
|
+
type: "error",
|
26
|
+
filepath: message.filepath,
|
27
|
+
componentPath: message.componentPath,
|
28
|
+
error: error instanceof Error ? error.message : String(error),
|
29
|
+
});
|
30
|
+
}
|
31
|
+
}
|
32
|
+
});
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@miyucy/storybook-mcp",
|
3
|
-
"version": "1.0.
|
3
|
+
"version": "1.0.3",
|
4
4
|
"description": "",
|
5
5
|
"main": "dist/index.js",
|
6
6
|
"scripts": {
|
@@ -19,6 +19,8 @@
|
|
19
19
|
"type": "module",
|
20
20
|
"dependencies": {
|
21
21
|
"@modelcontextprotocol/sdk": "1.10.1",
|
22
|
+
"fast-glob": "^3.3.3",
|
23
|
+
"react-docgen": "^7.1.1",
|
22
24
|
"zod": "3.24.3"
|
23
25
|
},
|
24
26
|
"devDependencies": {
|