@tpitre/story-ui 4.2.0 → 4.4.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/README.md +65 -37
- package/dist/mcp-server/index.js +170 -0
- package/dist/mcp-server/mcp-stdio-server.js +2 -5
- package/dist/story-generator/framework-adapters/index.js +1 -1
- package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -1
- package/dist/templates/StoryUI/StoryUIPanel.js +74 -0
- package/package.json +1 -1
- package/templates/StoryUI/StoryUIPanel.tsx +82 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Story UI revolutionizes component documentation by automatically generating Stor
|
|
|
10
10
|
## Why Story UI?
|
|
11
11
|
|
|
12
12
|
- **Framework Agnostic**: Works with React, Vue, Angular, Svelte, and Web Components
|
|
13
|
-
- **Multi-Provider AI**: Choose between Claude
|
|
13
|
+
- **Multi-Provider AI**: Choose between Claude, OpenAI, or Google Gemini - always using the latest models
|
|
14
14
|
- **Design System Aware**: Learns your component library and generates appropriate code
|
|
15
15
|
- **Production Ready**: Deploy as a standalone web app with full MCP integration
|
|
16
16
|
- **Zero Lock-in**: Use any component library - Mantine, Vuetify, Angular Material, Shoelace, or your own
|
|
@@ -47,6 +47,12 @@ Story UI will guide you through:
|
|
|
47
47
|
- **TypeScript Support**: Full type-aware story generation
|
|
48
48
|
- **Vision Support**: Attach screenshots for visual component requests
|
|
49
49
|
|
|
50
|
+
### Story Management
|
|
51
|
+
- **Edit Existing Stories**: Modify any generated story through conversation
|
|
52
|
+
- **Delete Stories**: Remove stories directly from the Story UI panel
|
|
53
|
+
- **Orphan Detection**: Find and clean up stories without associated chat history
|
|
54
|
+
- **Full MCP Integration**: Manage stories via Claude Desktop or any MCP-compatible client
|
|
55
|
+
|
|
50
56
|
### Multi-Framework Support
|
|
51
57
|
|
|
52
58
|
| Framework | Design Systems | Status |
|
|
@@ -54,16 +60,16 @@ Story UI will guide you through:
|
|
|
54
60
|
| React | Mantine, Chakra UI, Material UI, Custom | Fully Supported |
|
|
55
61
|
| Vue | Vuetify, Custom | Fully Supported |
|
|
56
62
|
| Angular | Angular Material, Custom | Fully Supported |
|
|
57
|
-
| Svelte |
|
|
63
|
+
| Svelte | Flowbite-Svelte, Custom | Fully Supported |
|
|
58
64
|
| Web Components | Shoelace, Custom | Fully Supported |
|
|
59
65
|
|
|
60
66
|
### Multi-Provider LLM Support
|
|
61
67
|
|
|
62
68
|
| Provider | Models | Best For |
|
|
63
69
|
|----------|--------|----------|
|
|
64
|
-
| **Claude** (Anthropic) |
|
|
65
|
-
| **GPT** (OpenAI) |
|
|
66
|
-
| **Gemini** (Google) |
|
|
70
|
+
| **Claude** (Anthropic) | claude-opus-4-5, claude-sonnet-4-5, claude-haiku-4-5 | Complex reasoning, code quality |
|
|
71
|
+
| **GPT** (OpenAI) | gpt-5.2, gpt-5.1, gpt-4o, gpt-4o-mini | Versatility, latest capabilities |
|
|
72
|
+
| **Gemini** (Google) | gemini-2.5-pro, gemini-2.5-flash, gemini-2.0-flash | Fast generation, cost efficiency |
|
|
67
73
|
|
|
68
74
|
### Production Deployment
|
|
69
75
|
- **Railway**: Node.js backend with file-based story persistence
|
|
@@ -115,7 +121,7 @@ The interactive installer will ask:
|
|
|
115
121
|
```
|
|
116
122
|
? Which AI provider do you prefer?
|
|
117
123
|
> Claude (Anthropic) - Recommended
|
|
118
|
-
OpenAI
|
|
124
|
+
OpenAI
|
|
119
125
|
Google Gemini
|
|
120
126
|
|
|
121
127
|
? Enter your API key:
|
|
@@ -256,11 +262,18 @@ The easiest way to connect is via Claude Desktop's built-in connector UI:
|
|
|
256
262
|
2. Go to **Settings** → **Connectors**
|
|
257
263
|
3. Click **"Add custom connector"**
|
|
258
264
|
4. Enter:
|
|
259
|
-
- **Name**: `Story UI` (or any name
|
|
260
|
-
- **URL**:
|
|
265
|
+
- **Name**: `Story UI React` (or any descriptive name)
|
|
266
|
+
- **URL**: Your deployed Railway URL + `/mcp-remote/mcp`
|
|
267
|
+
- Example: `https://your-app-name.up.railway.app/mcp-remote/mcp`
|
|
261
268
|
5. Click **Add**
|
|
262
269
|
6. **Restart Claude Desktop**
|
|
263
270
|
|
|
271
|
+
> **Note**: The URL will be your own Railway deployment URL. See [Production Deployment](#production-deployment) to set up your instance.
|
|
272
|
+
|
|
273
|
+
**Multiple Projects**: If you have multiple Storybook projects, add a separate connector for each:
|
|
274
|
+
- `Story UI React` → `https://my-react-app.up.railway.app/mcp-remote/mcp`
|
|
275
|
+
- `Story UI Vue` → `https://my-vue-app.up.railway.app/mcp-remote/mcp`
|
|
276
|
+
|
|
264
277
|
Once connected, you'll have access to all Story UI tools directly in your Claude conversations:
|
|
265
278
|
- `generate-story` - Generate Storybook stories from natural language
|
|
266
279
|
- `list-components` - Discover available components
|
|
@@ -274,38 +287,52 @@ Once connected, you'll have access to all Story UI tools directly in your Claude
|
|
|
274
287
|
Connect via Claude Code's built-in MCP support:
|
|
275
288
|
|
|
276
289
|
```bash
|
|
277
|
-
# Add
|
|
278
|
-
claude mcp add --transport http story-ui https://
|
|
290
|
+
# Add your production Railway deployment
|
|
291
|
+
claude mcp add --transport http story-ui-react https://your-react-app.up.railway.app/mcp-remote/mcp
|
|
279
292
|
|
|
280
|
-
#
|
|
281
|
-
claude mcp add --transport http story-ui-
|
|
293
|
+
# Add another project (if needed)
|
|
294
|
+
claude mcp add --transport http story-ui-vue https://your-vue-app.up.railway.app/mcp-remote/mcp
|
|
295
|
+
|
|
296
|
+
# For local development (default port is 4001)
|
|
297
|
+
claude mcp add --transport http story-ui-local http://localhost:4001/mcp-remote/mcp
|
|
282
298
|
```
|
|
283
299
|
|
|
284
300
|
### Manual Configuration (Advanced)
|
|
285
301
|
|
|
286
|
-
For
|
|
302
|
+
For running multiple local Story UI instances with different ports, configure your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
287
303
|
|
|
288
304
|
```json
|
|
289
305
|
{
|
|
290
306
|
"mcpServers": {
|
|
291
|
-
"story-ui": {
|
|
307
|
+
"story-ui-react": {
|
|
308
|
+
"command": "npx",
|
|
309
|
+
"args": ["@tpitre/story-ui", "start", "--port", "4001"]
|
|
310
|
+
},
|
|
311
|
+
"story-ui-vue": {
|
|
312
|
+
"command": "npx",
|
|
313
|
+
"args": ["@tpitre/story-ui", "start", "--port", "4002"]
|
|
314
|
+
},
|
|
315
|
+
"story-ui-angular": {
|
|
292
316
|
"command": "npx",
|
|
293
|
-
"args": ["@tpitre/story-ui", "
|
|
294
|
-
"env": {
|
|
295
|
-
"ANTHROPIC_API_KEY": "your-api-key"
|
|
296
|
-
}
|
|
317
|
+
"args": ["@tpitre/story-ui", "start", "--port", "4003"]
|
|
297
318
|
}
|
|
298
319
|
}
|
|
299
320
|
}
|
|
300
321
|
```
|
|
301
322
|
|
|
323
|
+
> **Note**: When using Claude Desktop, API keys are managed through your Anthropic account - no need to configure them in the MCP server.
|
|
324
|
+
|
|
302
325
|
### Starting the Local MCP Server
|
|
303
326
|
|
|
304
327
|
```bash
|
|
328
|
+
# Start with default port (4001)
|
|
305
329
|
npx story-ui start
|
|
330
|
+
|
|
331
|
+
# Or specify a custom port
|
|
332
|
+
npx story-ui start --port 4002
|
|
306
333
|
```
|
|
307
334
|
|
|
308
|
-
This starts the Story UI HTTP server with MCP endpoint at `http://localhost
|
|
335
|
+
This starts the Story UI HTTP server with MCP endpoint at `http://localhost:<port>/mcp-remote/mcp`.
|
|
309
336
|
|
|
310
337
|
### Available MCP Commands
|
|
311
338
|
|
|
@@ -319,13 +346,13 @@ Once connected, you can use these commands in Claude Desktop:
|
|
|
319
346
|
|
|
320
347
|
## Production Deployment
|
|
321
348
|
|
|
322
|
-
Story UI
|
|
349
|
+
Story UI can be deployed as a standalone web application accessible from anywhere. We recommend Railway for its ease of use, but any Node.js hosting platform will work.
|
|
323
350
|
|
|
324
351
|
### Architecture
|
|
325
352
|
|
|
326
353
|
```
|
|
327
354
|
┌─────────────────────────────────────────────────────────────┐
|
|
328
|
-
│
|
|
355
|
+
│ Your Deployment (e.g., Railway) │
|
|
329
356
|
│ ┌─────────────────────────────────────────────────────────┐│
|
|
330
357
|
│ │ Express MCP Server (Node.js) ││
|
|
331
358
|
│ │ - Serves Storybook with Story UI addon ││
|
|
@@ -336,9 +363,9 @@ Story UI v3 can be deployed as a standalone web application accessible from anyw
|
|
|
336
363
|
└──────────────────────────────────────────────────────────────┘
|
|
337
364
|
```
|
|
338
365
|
|
|
339
|
-
### Deploy to Railway
|
|
366
|
+
### Deploy to Railway (Recommended)
|
|
340
367
|
|
|
341
|
-
Railway provides a straightforward deployment experience with file-based story persistence.
|
|
368
|
+
Railway provides a straightforward deployment experience with automatic HTTPS and file-based story persistence.
|
|
342
369
|
|
|
343
370
|
**Quick Start:**
|
|
344
371
|
|
|
@@ -347,23 +374,27 @@ Railway provides a straightforward deployment experience with file-based story p
|
|
|
347
374
|
npm install -g @railway/cli
|
|
348
375
|
railway login
|
|
349
376
|
|
|
350
|
-
# Initialize and deploy
|
|
377
|
+
# Initialize and deploy from your Storybook project
|
|
351
378
|
railway init
|
|
352
379
|
railway up
|
|
353
380
|
```
|
|
354
381
|
|
|
355
382
|
**Environment Variables (set in Railway Dashboard):**
|
|
356
383
|
- `ANTHROPIC_API_KEY` - Required for Claude models
|
|
357
|
-
- `OPENAI_API_KEY` - Optional for OpenAI models
|
|
358
|
-
- `GEMINI_API_KEY` - Optional for Gemini models
|
|
384
|
+
- `OPENAI_API_KEY` - Optional, for OpenAI models
|
|
385
|
+
- `GEMINI_API_KEY` - Optional, for Gemini models
|
|
386
|
+
|
|
387
|
+
**After Deployment:**
|
|
359
388
|
|
|
360
|
-
|
|
389
|
+
Your Railway app will have a URL like `https://your-app-name.up.railway.app`. Use this URL to connect MCP clients:
|
|
361
390
|
|
|
362
391
|
```bash
|
|
363
|
-
#
|
|
364
|
-
claude mcp add --transport http story-ui https://your-app.up.railway.app/mcp-remote/mcp
|
|
392
|
+
# In Claude Code
|
|
393
|
+
claude mcp add --transport http story-ui https://your-app-name.up.railway.app/mcp-remote/mcp
|
|
365
394
|
```
|
|
366
395
|
|
|
396
|
+
Or add it to Claude Desktop via **Settings** → **Connectors** → **Add custom connector**.
|
|
397
|
+
|
|
367
398
|
See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed deployment instructions and troubleshooting.
|
|
368
399
|
|
|
369
400
|
---
|
|
@@ -417,17 +448,14 @@ For simpler setups, use `story-ui-considerations.md`:
|
|
|
417
448
|
## CLI Reference
|
|
418
449
|
|
|
419
450
|
```bash
|
|
420
|
-
# Initialize Story UI
|
|
451
|
+
# Initialize Story UI in your project
|
|
421
452
|
npx story-ui init
|
|
422
453
|
|
|
423
|
-
# Start the
|
|
454
|
+
# Start the MCP server (default port: 4001)
|
|
424
455
|
npx story-ui start
|
|
425
|
-
npx story-ui start --port
|
|
426
|
-
|
|
427
|
-
# Deploy to production
|
|
428
|
-
npx story-ui deploy
|
|
456
|
+
npx story-ui start --port 4002 # Custom port
|
|
429
457
|
|
|
430
|
-
# Run MCP server
|
|
458
|
+
# Run MCP STDIO server (for Claude Desktop local integration)
|
|
431
459
|
npx story-ui mcp
|
|
432
460
|
```
|
|
433
461
|
|
|
@@ -461,7 +489,7 @@ npx story-ui mcp
|
|
|
461
489
|
|
|
462
490
|
## Upgrading from v2
|
|
463
491
|
|
|
464
|
-
Story UI
|
|
492
|
+
Story UI v4 is backwards compatible with previous configurations. However, to take advantage of new features:
|
|
465
493
|
|
|
466
494
|
1. **Multi-Provider Support**: Add `llmProvider` to your config
|
|
467
495
|
2. **Framework Detection**: Add `framework` to your config for non-React projects
|
package/dist/mcp-server/index.js
CHANGED
|
@@ -78,6 +78,176 @@ app.get('/mcp/frameworks/detect', detectCurrentFramework);
|
|
|
78
78
|
app.get('/mcp/frameworks/:type', getFrameworkDetails);
|
|
79
79
|
app.post('/mcp/frameworks/validate', validateStoryForFramework);
|
|
80
80
|
app.post('/mcp/frameworks/post-process', postProcessStoryForFramework);
|
|
81
|
+
// MCP story management routes - for Claude Desktop and other MCP clients
|
|
82
|
+
// List all stories
|
|
83
|
+
app.get('/mcp/stories', async (req, res) => {
|
|
84
|
+
try {
|
|
85
|
+
const storiesPath = config.generatedStoriesPath;
|
|
86
|
+
if (!fs.existsSync(storiesPath)) {
|
|
87
|
+
return res.json({ stories: [] });
|
|
88
|
+
}
|
|
89
|
+
const files = fs.readdirSync(storiesPath);
|
|
90
|
+
const stories = files
|
|
91
|
+
.filter(file => file.endsWith('.stories.tsx') || file.endsWith('.stories.ts') || file.endsWith('.stories.svelte'))
|
|
92
|
+
.map(file => {
|
|
93
|
+
const filePath = path.join(storiesPath, file);
|
|
94
|
+
const stats = fs.statSync(filePath);
|
|
95
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
96
|
+
// Extract title from story file
|
|
97
|
+
const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
|
|
98
|
+
let title = titleMatch ? titleMatch[1].replace('Generated/', '') : file.replace(/\.stories\.(tsx|ts|svelte)$/, '');
|
|
99
|
+
// Remove hash suffix from display title
|
|
100
|
+
title = title.replace(/\s*\([a-f0-9]{8}\)$/i, '');
|
|
101
|
+
return {
|
|
102
|
+
id: file.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
|
|
103
|
+
storyId: file.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
|
|
104
|
+
fileName: file,
|
|
105
|
+
title,
|
|
106
|
+
lastUpdated: stats.mtime.getTime(),
|
|
107
|
+
createdAt: stats.birthtime.getTime(),
|
|
108
|
+
content
|
|
109
|
+
};
|
|
110
|
+
})
|
|
111
|
+
.sort((a, b) => b.lastUpdated - a.lastUpdated);
|
|
112
|
+
return res.json({ stories });
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error('Error listing stories:', error);
|
|
116
|
+
return res.status(500).json({ error: 'Failed to list stories' });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// Get a specific story by ID
|
|
120
|
+
app.get('/mcp/stories/:storyId', async (req, res) => {
|
|
121
|
+
try {
|
|
122
|
+
const { storyId } = req.params;
|
|
123
|
+
const storiesPath = config.generatedStoriesPath;
|
|
124
|
+
if (!fs.existsSync(storiesPath)) {
|
|
125
|
+
return res.status(404).json({ error: 'Stories directory not found' });
|
|
126
|
+
}
|
|
127
|
+
const files = fs.readdirSync(storiesPath);
|
|
128
|
+
// Extract hash from story ID if in legacy format (story-a1b2c3d4)
|
|
129
|
+
const hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
|
|
130
|
+
const hash = hashMatch ? hashMatch[1] : null;
|
|
131
|
+
// Find matching file
|
|
132
|
+
const matchingFile = files.find(file => {
|
|
133
|
+
// Match by hash suffix
|
|
134
|
+
if (hash && file.includes(`-${hash}.stories.`))
|
|
135
|
+
return true;
|
|
136
|
+
// Match by exact ID
|
|
137
|
+
if (file.startsWith(`${storyId}.stories.`))
|
|
138
|
+
return true;
|
|
139
|
+
// Match by fileName
|
|
140
|
+
if (file === storyId)
|
|
141
|
+
return true;
|
|
142
|
+
// Match by ID without extension
|
|
143
|
+
if (file.replace(/\.stories\.(tsx|ts|svelte)$/, '') === storyId)
|
|
144
|
+
return true;
|
|
145
|
+
return false;
|
|
146
|
+
});
|
|
147
|
+
if (!matchingFile) {
|
|
148
|
+
return res.status(404).json({ error: `Story with ID ${storyId} not found` });
|
|
149
|
+
}
|
|
150
|
+
const filePath = path.join(storiesPath, matchingFile);
|
|
151
|
+
const stats = fs.statSync(filePath);
|
|
152
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
153
|
+
// Extract title from story file
|
|
154
|
+
const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
|
|
155
|
+
let title = titleMatch ? titleMatch[1].replace('Generated/', '') : matchingFile.replace(/\.stories\.(tsx|ts|svelte)$/, '');
|
|
156
|
+
title = title.replace(/\s*\([a-f0-9]{8}\)$/i, '');
|
|
157
|
+
return res.json({
|
|
158
|
+
id: matchingFile.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
|
|
159
|
+
storyId: matchingFile.replace(/\.stories\.(tsx|ts|svelte)$/, ''),
|
|
160
|
+
fileName: matchingFile,
|
|
161
|
+
title,
|
|
162
|
+
lastUpdated: stats.mtime.getTime(),
|
|
163
|
+
createdAt: stats.birthtime.getTime(),
|
|
164
|
+
timestamp: stats.mtime.getTime(),
|
|
165
|
+
content,
|
|
166
|
+
story: content
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
console.error('Error getting story:', error);
|
|
171
|
+
return res.status(500).json({ error: 'Failed to get story' });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
// Get story content (raw code)
|
|
175
|
+
app.get('/mcp/stories/:storyId/content', async (req, res) => {
|
|
176
|
+
try {
|
|
177
|
+
const { storyId } = req.params;
|
|
178
|
+
const storiesPath = config.generatedStoriesPath;
|
|
179
|
+
if (!fs.existsSync(storiesPath)) {
|
|
180
|
+
return res.status(404).send('Stories directory not found');
|
|
181
|
+
}
|
|
182
|
+
const files = fs.readdirSync(storiesPath);
|
|
183
|
+
// Extract hash from story ID if in legacy format
|
|
184
|
+
const hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
|
|
185
|
+
const hash = hashMatch ? hashMatch[1] : null;
|
|
186
|
+
// Find matching file
|
|
187
|
+
const matchingFile = files.find(file => {
|
|
188
|
+
if (hash && file.includes(`-${hash}.stories.`))
|
|
189
|
+
return true;
|
|
190
|
+
if (file.startsWith(`${storyId}.stories.`))
|
|
191
|
+
return true;
|
|
192
|
+
if (file === storyId)
|
|
193
|
+
return true;
|
|
194
|
+
if (file.replace(/\.stories\.(tsx|ts|svelte)$/, '') === storyId)
|
|
195
|
+
return true;
|
|
196
|
+
return false;
|
|
197
|
+
});
|
|
198
|
+
if (!matchingFile) {
|
|
199
|
+
return res.status(404).send(`Story with ID ${storyId} not found`);
|
|
200
|
+
}
|
|
201
|
+
const filePath = path.join(storiesPath, matchingFile);
|
|
202
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
203
|
+
res.type('text/plain').send(content);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
console.error('Error getting story content:', error);
|
|
207
|
+
return res.status(500).send('Failed to get story content');
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
// Delete a story by ID
|
|
211
|
+
app.delete('/mcp/stories/:storyId', async (req, res) => {
|
|
212
|
+
try {
|
|
213
|
+
const { storyId } = req.params;
|
|
214
|
+
const storiesPath = config.generatedStoriesPath;
|
|
215
|
+
if (!fs.existsSync(storiesPath)) {
|
|
216
|
+
return res.status(404).json({ error: 'Stories directory not found' });
|
|
217
|
+
}
|
|
218
|
+
const files = fs.readdirSync(storiesPath);
|
|
219
|
+
// Extract hash from story ID if in legacy format
|
|
220
|
+
const hashMatch = storyId.match(/^story-([a-f0-9]{8})$/);
|
|
221
|
+
const hash = hashMatch ? hashMatch[1] : null;
|
|
222
|
+
// Find matching file
|
|
223
|
+
const matchingFile = files.find(file => {
|
|
224
|
+
if (hash && file.includes(`-${hash}.stories.`))
|
|
225
|
+
return true;
|
|
226
|
+
if (file.startsWith(`${storyId}.stories.`))
|
|
227
|
+
return true;
|
|
228
|
+
if (file === storyId)
|
|
229
|
+
return true;
|
|
230
|
+
if (file.replace(/\.stories\.(tsx|ts|svelte)$/, '') === storyId)
|
|
231
|
+
return true;
|
|
232
|
+
return false;
|
|
233
|
+
});
|
|
234
|
+
if (!matchingFile) {
|
|
235
|
+
return res.status(404).json({ error: `Story with ID ${storyId} not found` });
|
|
236
|
+
}
|
|
237
|
+
const filePath = path.join(storiesPath, matchingFile);
|
|
238
|
+
fs.unlinkSync(filePath);
|
|
239
|
+
console.log(`🗑️ Deleted story via MCP endpoint: ${matchingFile}`);
|
|
240
|
+
return res.json({
|
|
241
|
+
success: true,
|
|
242
|
+
deleted: matchingFile,
|
|
243
|
+
message: `Story "${matchingFile}" has been deleted successfully.`
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
console.error('Error deleting story:', error);
|
|
248
|
+
return res.status(500).json({ error: 'Failed to delete story' });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
81
251
|
// File-based story routes - stories are generated as .stories.tsx files
|
|
82
252
|
// Storybook discovers these automatically via its native file system watching
|
|
83
253
|
// Proxy routes for frontend compatibility (maps /story-ui/ to /mcp/)
|
|
@@ -27,7 +27,8 @@ dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
|
|
27
27
|
process.env.STORY_UI_MCP_MODE = 'true';
|
|
28
28
|
// Get HTTP server port from environment variables (check multiple possible names)
|
|
29
29
|
const HTTP_PORT = process.env.VITE_STORY_UI_PORT || process.env.STORY_UI_HTTP_PORT || process.env.PORT || '4001';
|
|
30
|
-
|
|
30
|
+
// Allow configurable base URL for Railway/cloud deployments, fallback to localhost for local dev
|
|
31
|
+
const HTTP_BASE_URL = process.env.STORY_UI_HTTP_BASE_URL || `http://localhost:${HTTP_PORT}`;
|
|
31
32
|
// Initialize configuration
|
|
32
33
|
const config = loadUserConfig();
|
|
33
34
|
// Create MCP server instance
|
|
@@ -181,8 +182,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
181
182
|
throw new Error(`Failed to generate story: ${error}`);
|
|
182
183
|
}
|
|
183
184
|
const result = await response.json();
|
|
184
|
-
// Debug log to see what we're getting
|
|
185
|
-
console.error('Story generation result:', JSON.stringify(result, null, 2));
|
|
186
185
|
return {
|
|
187
186
|
content: [{
|
|
188
187
|
type: "text",
|
|
@@ -509,8 +508,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
509
508
|
throw new Error(`Failed to update story: ${error}`);
|
|
510
509
|
}
|
|
511
510
|
const result = await response.json();
|
|
512
|
-
// Debug log to see what we're getting
|
|
513
|
-
console.error('Story update result:', JSON.stringify(result, null, 2));
|
|
514
511
|
return {
|
|
515
512
|
content: [{
|
|
516
513
|
type: "text",
|
|
@@ -150,7 +150,7 @@ class AdapterRegistry {
|
|
|
150
150
|
return {
|
|
151
151
|
systemPrompt: adapter.generateSystemPrompt(config, options),
|
|
152
152
|
componentReference: adapter.generateComponentReference(components, config),
|
|
153
|
-
layoutInstructions: '',
|
|
153
|
+
layoutInstructions: '',
|
|
154
154
|
examples: adapter.generateExamples(config),
|
|
155
155
|
sampleStory: adapter.generateSampleStory(config, components),
|
|
156
156
|
framework: this.detectedFramework || {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,oBAAoB,CAAC;AAwwB5B,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,iBAAS,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,oBAAoB,CAAC;AAwwB5B,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,iBAAS,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,iBAAiB,2CA0sCnD;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -452,12 +452,79 @@ function StoryUIPanel({ mcpPort }) {
|
|
|
452
452
|
const fileInputRef = useRef(null);
|
|
453
453
|
const abortControllerRef = useRef(null);
|
|
454
454
|
const hasShownRefreshHint = useRef(false);
|
|
455
|
+
// Track stories for MCP external generation detection
|
|
456
|
+
// Used to detect when stories are created via MCP remote (Claude Desktop)
|
|
457
|
+
// and trigger automatic refresh since MCP has no browser context
|
|
458
|
+
const panelGeneratedStoryIds = useRef(new Set());
|
|
459
|
+
const knownStoryIds = useRef(new Set());
|
|
460
|
+
const isPollingInitialized = useRef(false);
|
|
455
461
|
// Set port override if provided
|
|
456
462
|
useEffect(() => {
|
|
457
463
|
if (mcpPort && typeof window !== 'undefined') {
|
|
458
464
|
window.STORY_UI_MCP_PORT = String(mcpPort);
|
|
459
465
|
}
|
|
460
466
|
}, [mcpPort]);
|
|
467
|
+
// Poll for MCP-generated stories (stories created externally via Claude Desktop/Code)
|
|
468
|
+
// This solves the Vite HMR issue where stories generated via MCP remote don't trigger
|
|
469
|
+
// a browser refresh because MCP has no browser context to call window.location.reload()
|
|
470
|
+
useEffect(() => {
|
|
471
|
+
const POLL_INTERVAL_MS = 5000; // Check every 5 seconds
|
|
472
|
+
const pollForExternalStories = async () => {
|
|
473
|
+
try {
|
|
474
|
+
const baseUrl = getApiBaseUrl();
|
|
475
|
+
const response = await fetch(`${baseUrl}/story-ui/stories`);
|
|
476
|
+
if (!response.ok)
|
|
477
|
+
return;
|
|
478
|
+
const data = await response.json();
|
|
479
|
+
const currentStoryIds = new Set(data.stories?.map((s) => s.id) || []);
|
|
480
|
+
// On first poll, just record what's already there
|
|
481
|
+
if (!isPollingInitialized.current) {
|
|
482
|
+
knownStoryIds.current = currentStoryIds;
|
|
483
|
+
isPollingInitialized.current = true;
|
|
484
|
+
console.log('[Story UI] MCP story polling initialized with', currentStoryIds.size, 'stories');
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
// Check for new stories not created by this panel session
|
|
488
|
+
for (const storyId of currentStoryIds) {
|
|
489
|
+
if (!knownStoryIds.current.has(storyId) && !panelGeneratedStoryIds.current.has(storyId)) {
|
|
490
|
+
// New story detected that wasn't created by this panel - must be from MCP remote
|
|
491
|
+
console.log('[Story UI] Detected externally generated story:', storyId);
|
|
492
|
+
console.log('[Story UI] Triggering refresh to register new story in Vite import map...');
|
|
493
|
+
// Update known stories before refresh
|
|
494
|
+
knownStoryIds.current = currentStoryIds;
|
|
495
|
+
// Trigger refresh with a short delay
|
|
496
|
+
setTimeout(() => {
|
|
497
|
+
try {
|
|
498
|
+
if (window.top && window.top !== window) {
|
|
499
|
+
window.top.location.reload();
|
|
500
|
+
}
|
|
501
|
+
else if (window.parent && window.parent !== window) {
|
|
502
|
+
window.parent.location.reload();
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
window.location.reload();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
console.warn('[Story UI] Could not auto-refresh for MCP-generated story');
|
|
510
|
+
}
|
|
511
|
+
}, 1000);
|
|
512
|
+
return; // Only trigger one refresh
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// Update known stories
|
|
516
|
+
knownStoryIds.current = currentStoryIds;
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
// Silently ignore polling errors - server may be unavailable temporarily
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
// Start polling
|
|
523
|
+
const intervalId = setInterval(pollForExternalStories, POLL_INTERVAL_MS);
|
|
524
|
+
// Initial poll
|
|
525
|
+
pollForExternalStories();
|
|
526
|
+
return () => clearInterval(intervalId);
|
|
527
|
+
}, []);
|
|
461
528
|
// Detect Storybook theme
|
|
462
529
|
useEffect(() => {
|
|
463
530
|
const detectTheme = () => {
|
|
@@ -863,6 +930,13 @@ function StoryUIPanel({ mcpPort }) {
|
|
|
863
930
|
title: completion.title,
|
|
864
931
|
action: completion.summary?.action,
|
|
865
932
|
});
|
|
933
|
+
// Track this story as panel-generated to prevent false MCP detection
|
|
934
|
+
// The story ID is the fileName without .stories.tsx extension
|
|
935
|
+
if (completion.success && completion.fileName) {
|
|
936
|
+
const storyId = completion.fileName.replace('.stories.tsx', '');
|
|
937
|
+
panelGeneratedStoryIds.current.add(storyId);
|
|
938
|
+
console.log('[Story UI] Tracking panel-generated story:', storyId);
|
|
939
|
+
}
|
|
866
940
|
const isUpdate = completion.summary.action === 'updated';
|
|
867
941
|
const responseMessage = buildConversationalResponse(completion, isUpdate);
|
|
868
942
|
const aiMsg = { role: 'ai', content: responseMessage };
|
package/package.json
CHANGED
|
@@ -800,6 +800,13 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
800
800
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
801
801
|
const hasShownRefreshHint = useRef(false);
|
|
802
802
|
|
|
803
|
+
// Track stories for MCP external generation detection
|
|
804
|
+
// Used to detect when stories are created via MCP remote (Claude Desktop)
|
|
805
|
+
// and trigger automatic refresh since MCP has no browser context
|
|
806
|
+
const panelGeneratedStoryIds = useRef<Set<string>>(new Set());
|
|
807
|
+
const knownStoryIds = useRef<Set<string>>(new Set());
|
|
808
|
+
const isPollingInitialized = useRef(false);
|
|
809
|
+
|
|
803
810
|
// Set port override if provided
|
|
804
811
|
useEffect(() => {
|
|
805
812
|
if (mcpPort && typeof window !== 'undefined') {
|
|
@@ -807,6 +814,73 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
807
814
|
}
|
|
808
815
|
}, [mcpPort]);
|
|
809
816
|
|
|
817
|
+
// Poll for MCP-generated stories (stories created externally via Claude Desktop/Code)
|
|
818
|
+
// This solves the Vite HMR issue where stories generated via MCP remote don't trigger
|
|
819
|
+
// a browser refresh because MCP has no browser context to call window.location.reload()
|
|
820
|
+
useEffect(() => {
|
|
821
|
+
const POLL_INTERVAL_MS = 5000; // Check every 5 seconds
|
|
822
|
+
|
|
823
|
+
const pollForExternalStories = async () => {
|
|
824
|
+
try {
|
|
825
|
+
const baseUrl = getApiBaseUrl();
|
|
826
|
+
const response = await fetch(`${baseUrl}/story-ui/stories`);
|
|
827
|
+
if (!response.ok) return;
|
|
828
|
+
|
|
829
|
+
const data = await response.json();
|
|
830
|
+
const currentStoryIds = new Set<string>(data.stories?.map((s: { id: string }) => s.id) || []);
|
|
831
|
+
|
|
832
|
+
// On first poll, just record what's already there
|
|
833
|
+
if (!isPollingInitialized.current) {
|
|
834
|
+
knownStoryIds.current = currentStoryIds;
|
|
835
|
+
isPollingInitialized.current = true;
|
|
836
|
+
console.log('[Story UI] MCP story polling initialized with', currentStoryIds.size, 'stories');
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Check for new stories not created by this panel session
|
|
841
|
+
for (const storyId of currentStoryIds) {
|
|
842
|
+
if (!knownStoryIds.current.has(storyId) && !panelGeneratedStoryIds.current.has(storyId)) {
|
|
843
|
+
// New story detected that wasn't created by this panel - must be from MCP remote
|
|
844
|
+
console.log('[Story UI] Detected externally generated story:', storyId);
|
|
845
|
+
console.log('[Story UI] Triggering refresh to register new story in Vite import map...');
|
|
846
|
+
|
|
847
|
+
// Update known stories before refresh
|
|
848
|
+
knownStoryIds.current = currentStoryIds;
|
|
849
|
+
|
|
850
|
+
// Trigger refresh with a short delay
|
|
851
|
+
setTimeout(() => {
|
|
852
|
+
try {
|
|
853
|
+
if (window.top && window.top !== window) {
|
|
854
|
+
window.top.location.reload();
|
|
855
|
+
} else if (window.parent && window.parent !== window) {
|
|
856
|
+
window.parent.location.reload();
|
|
857
|
+
} else {
|
|
858
|
+
window.location.reload();
|
|
859
|
+
}
|
|
860
|
+
} catch (error) {
|
|
861
|
+
console.warn('[Story UI] Could not auto-refresh for MCP-generated story');
|
|
862
|
+
}
|
|
863
|
+
}, 1000);
|
|
864
|
+
return; // Only trigger one refresh
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Update known stories
|
|
869
|
+
knownStoryIds.current = currentStoryIds;
|
|
870
|
+
} catch (error) {
|
|
871
|
+
// Silently ignore polling errors - server may be unavailable temporarily
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
// Start polling
|
|
876
|
+
const intervalId = setInterval(pollForExternalStories, POLL_INTERVAL_MS);
|
|
877
|
+
|
|
878
|
+
// Initial poll
|
|
879
|
+
pollForExternalStories();
|
|
880
|
+
|
|
881
|
+
return () => clearInterval(intervalId);
|
|
882
|
+
}, []);
|
|
883
|
+
|
|
810
884
|
// Detect Storybook theme
|
|
811
885
|
useEffect(() => {
|
|
812
886
|
const detectTheme = () => {
|
|
@@ -1219,6 +1293,14 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
1219
1293
|
action: completion.summary?.action,
|
|
1220
1294
|
});
|
|
1221
1295
|
|
|
1296
|
+
// Track this story as panel-generated to prevent false MCP detection
|
|
1297
|
+
// The story ID is the fileName without .stories.tsx extension
|
|
1298
|
+
if (completion.success && completion.fileName) {
|
|
1299
|
+
const storyId = completion.fileName.replace('.stories.tsx', '');
|
|
1300
|
+
panelGeneratedStoryIds.current.add(storyId);
|
|
1301
|
+
console.log('[Story UI] Tracking panel-generated story:', storyId);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1222
1304
|
const isUpdate = completion.summary.action === 'updated';
|
|
1223
1305
|
const responseMessage = buildConversationalResponse(completion, isUpdate);
|
|
1224
1306
|
const aiMsg: Message = { role: 'ai', content: responseMessage };
|