vue storybook + mcp をやった
概要
storybook と mcpで storybook経由で接続する
参考にしました!
vue3 + storybook
vue3とstorybookでつくっています.いい感じに環境は用意する.
mcpサーバ
storybookのindex.jsonをキーにcomponentをサーチ.その情報をもとに,component.vueやcomponent.stories.tsをaiに調べてもらう.カタログからコンポーネントを探してもらい自分の実装したい内容をプロンプトにいれて,aiにつくってもらうイメージ.
storybookのindex.jsonは以下
{
"v":5,
"entries":{
"example-button--primary":{
"type":"story",
"id":"example-button--primary",
"name":"Primary",
"title":"Example/Button",
"importPath":"./src/components/Button.stories.ts",
"componentPath":"./src/components/Button.vue",
"tags":[
"dev",
"test",
"autodocs"
]
},
}}
今回は簡単な例で:使い方を聞くことができるようにする
コードは以下.説明が必要そうなところだけ書く
const server = new McpServer({
name: SERVER_NAME,
version: SERVER_VERSION,
})
server.tool(
'component-usage',
'Explain how to use',
{
componentName: z.string(),
},
async ({ componentName }) => {
let responseText = `Component Information for: ${componentName}\n(Based on data from index.json)\n\n`
// サーチ
const relevantEntry = findComponent(componentName)
componentの使い方の tool
を作成. findComponent
にてpromptで入ってきたcomponentをサーチする.プロンプト的には Buttonについての使い方や実際の使用例を教えてください.
みたいな感じ. 今回は単一のcomponentを探すことにしているが,server.tool('component-list'
などで書くことで一覧的な意味を含めることもできると思う.
以下でサーチする. "importPath":"./src/components/Button.stories.ts","componentPath":"./src/components/Button.vue"
この辺を使う
function findComponent(componentName: string): EntryInfo | null {
const lowerComponentName = componentName.toLowerCase()
let bestMatch: EntryInfo | null = null
for (const storyId in storiesData.entries) {
const story = storiesData.entries[storyId]
// 1. title がコンポーネント名に部分一致するか (例: "Example/Button" と "Button")
const titleParts = story.title.split('/')
const storyComponentName = titleParts[titleParts.length - 1]
if (storyComponentName?.toLocaleLowerCase() === lowerComponentName) {
if (!bestMatch || bestMatch.title !== story.title) {
bestMatch = story
}
}
// 2. importPath がコンポーネント名を含んでいるか
if (!bestMatch && story.componentPath?.toLowerCase().includes(`/${lowerComponentName}.vue`)) {
bestMatch = story
}
if (!bestMatch && story.name.toLowerCase() === lowerComponentName) {
bestMatch = story
}
}
if (!bestMatch) {
console.warn(`No component: ${componentName}`)
}
return bestMatch
}
ここで,mcpサーバとstorybookのパス絶対位置は以下となっている
- /Users/hogeuser/development/mcp/mcp-storybook/mcp/build/index.js
- /Users/hogeuser/development/mcp/mcp-storybook/storybook-static/index.json
コンポーネントの場所が"componentPath":"./src/components/Button.vue",
なので,絶対パスをいい感じに合わせてファイルの場所を教えてあげる必要がある.github上にある場合はいい感じに修正すれば良いです.レスポンスの文言とかはaiに生成させている.
if (relevantEntry) {
const indexJsonDir = path.dirname(storiesJsonPath)
const baseDirectory = path.dirname(indexJsonDir)
let absoluteComponentPath: string | undefined
if (relevantEntry.componentPath) {
try {
// コンポーネントパスを解決
absoluteComponentPath = path.resolve(baseDirectory, relevantEntry.componentPath)
responseText += `コンポーネントファイルパス: ${absoluteComponentPath}\n`
} catch (e) {
responseText += `コンポーネントパスを解決できませんでした: ${relevantEntry.componentPath}\n`
}
}
let absoluteImportPath: string | undefined
if (relevantEntry.importPath) {
try {
// インポートパス (ストーリー/ドキュメントファイル) を解決
absoluteImportPath = path.resolve(baseDirectory, relevantEntry.importPath)
responseText += `ストーリー/ドキュメントファイルパス: ${absoluteImportPath}\n`
} catch (e) {
responseText += `ストーリー/ドキュメントパスを解決できませんでした: ${relevantEntry.importPath}\n`
}
} else {
responseText += `ストーリー/ドキュメントファイルパス: index.json エントリーに指定されていません。\n`
}
responseText +=
'\nAIへの指示:\n上記で提供された絶対パスにあるファイルの内容を読み取り、コンポーネントの実装と使用例およびどのようなコンポーネントであるかを説明してください。\n'
}
これで準備が整う
cursol
cursolを使う
{
"mcpServers": {
"mcp-storybook": {
"command": "node",
"args": ["/Users/hogeuser/development/mcp/mcp-storybook/mcp/build/index.js","/Users/hogeuser/development/mcp/mcp-storybook/storybook-static/index.json"]
}
}
}
まとめ
他にも design-token
などを取得するresource
などをつくっておけばデザイン実装もお任せでできる見込みはある
終わり
Discussion