verso-mcp 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.
Files changed (3) hide show
  1. package/README.md +47 -0
  2. package/index.js +496 -0
  3. package/package.json +17 -0
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # verso-mcp
2
+
3
+ MCP server for [Verso](https://useverso.app) — AI-powered mobile app design tool.
4
+
5
+ Connects Claude Code to Verso so you can design mobile app screens with AI.
6
+
7
+ ## Setup
8
+
9
+ 1. Get your API token from [useverso.app](https://useverso.app) (click your avatar > copy API token)
10
+
11
+ 2. Add to your Claude Code MCP config (`~/.claude/mcp.json`):
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "verso": {
17
+ "command": "npx",
18
+ "args": ["verso-mcp", "--token=vrs_your_token_here"]
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ 3. Restart Claude Code and start designing!
25
+
26
+ ## Usage
27
+
28
+ Once connected, Claude can:
29
+
30
+ - **list_projects** — See all your Verso projects
31
+ - **open_project** — Connect to a specific project
32
+ - **write_html** — Design full screen layouts
33
+ - **get_tree** — Inspect the current design structure
34
+ - **set_text / set_style** — Make targeted edits
35
+ - **get_screenshot** — Capture the current design
36
+ - **create_screen / switch_screen** — Manage multiple screens
37
+
38
+ ## Options
39
+
40
+ ```
41
+ verso-mcp --token=vrs_xxx # Connect to useverso.app
42
+ verso-mcp --token=vrs_xxx --url=URL # Connect to custom server
43
+ ```
44
+
45
+ ## License
46
+
47
+ MIT
package/index.js ADDED
@@ -0,0 +1,496 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Verso MCP Server
4
+ // Communicates with Claude Code via stdio (JSON-RPC / MCP protocol)
5
+ // Forwards tool calls to the Verso app via WebSocket
6
+
7
+ const WebSocket = require('ws')
8
+ const readline = require('readline')
9
+ const https = require('https')
10
+ const http = require('http')
11
+
12
+ const log = (...args) => process.stderr.write(`[verso-mcp] ${args.join(' ')}\n`)
13
+
14
+ // --- CLI Args ---
15
+ const args = process.argv.slice(2)
16
+ let API_TOKEN = ''
17
+ let SERVER_URL = 'https://useverso.app' // default production
18
+
19
+ for (let i = 0; i < args.length; i++) {
20
+ if (args[i] === '--token' && args[i + 1]) { API_TOKEN = args[++i]; continue }
21
+ if (args[i].startsWith('--token=')) { API_TOKEN = args[i].split('=')[1]; continue }
22
+ if (args[i] === '--url' && args[i + 1]) { SERVER_URL = args[++i]; continue }
23
+ if (args[i].startsWith('--url=')) { SERVER_URL = args[i].split('=')[1]; continue }
24
+ if (args[i] === '--help' || args[i] === '-h') {
25
+ process.stderr.write(`
26
+ Verso MCP Server
27
+
28
+ Usage:
29
+ verso-mcp --token=vrs_xxx
30
+ verso-mcp --token=vrs_xxx --url=http://localhost:4444
31
+
32
+ Options:
33
+ --token API token (get from useverso.app settings)
34
+ --url Server URL (default: https://useverso.app)
35
+ --help Show this help
36
+ `)
37
+ process.exit(0)
38
+ }
39
+ }
40
+
41
+ if (!API_TOKEN) {
42
+ process.stderr.write(`[verso-mcp] Error: --token is required. Get your API token from useverso.app settings.\n`)
43
+ process.exit(1)
44
+ }
45
+
46
+ // --- Derive WebSocket URL from server URL ---
47
+ function getWsUrl() {
48
+ const url = new URL(SERVER_URL)
49
+ const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
50
+ return `${protocol}//${url.host}?token=${encodeURIComponent(API_TOKEN)}`
51
+ }
52
+
53
+ // --- Active project ---
54
+ let activeProjectId = null
55
+
56
+ function apiRequest(method, path, body) {
57
+ return new Promise((resolve, reject) => {
58
+ const url = new URL(path, SERVER_URL)
59
+ const mod = url.protocol === 'https:' ? https : http
60
+ const opts = {
61
+ hostname: url.hostname,
62
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
63
+ path: url.pathname,
64
+ method,
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ 'Authorization': `Bearer ${API_TOKEN}`,
68
+ },
69
+ }
70
+ const req = mod.request(opts, (res) => {
71
+ let data = ''
72
+ res.on('data', chunk => data += chunk)
73
+ res.on('end', () => {
74
+ try { resolve(JSON.parse(data)) } catch (e) { resolve(data) }
75
+ })
76
+ })
77
+ req.on('error', reject)
78
+ if (body) req.write(JSON.stringify(body))
79
+ req.end()
80
+ })
81
+ }
82
+
83
+ // --- WebSocket Client ---
84
+ let ws = null
85
+ const pendingRequests = new Map()
86
+
87
+ function registerProject() {
88
+ if (ws && ws.readyState === WebSocket.OPEN && activeProjectId) {
89
+ ws.send(JSON.stringify({ type: 'register', projectId: activeProjectId }))
90
+ log('Registered for project', activeProjectId)
91
+ }
92
+ }
93
+
94
+ function connectWS() {
95
+ const wsUrl = getWsUrl()
96
+ ws = new WebSocket(wsUrl)
97
+
98
+ ws.on('open', () => {
99
+ log('Connected to Verso')
100
+ registerProject()
101
+ })
102
+
103
+ ws.on('message', (data) => {
104
+ try {
105
+ const msg = JSON.parse(data.toString())
106
+ if (msg.id && pendingRequests.has(msg.id)) {
107
+ const { resolve } = pendingRequests.get(msg.id)
108
+ pendingRequests.delete(msg.id)
109
+ resolve(msg)
110
+ }
111
+ } catch (e) {
112
+ log('Parse error:', e.message)
113
+ }
114
+ })
115
+
116
+ ws.on('close', () => {
117
+ log('Disconnected, reconnecting...')
118
+ setTimeout(connectWS, 2000)
119
+ })
120
+
121
+ ws.on('error', (err) => {
122
+ log('WebSocket error:', err.message)
123
+ })
124
+ }
125
+
126
+ function sendToolCall(tool, args) {
127
+ return new Promise((resolve, reject) => {
128
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
129
+ reject(new Error('Not connected to Verso. Make sure the server is running and your token is valid.'))
130
+ return
131
+ }
132
+
133
+ const id = Math.random().toString(36).slice(2) + Date.now().toString(36)
134
+ pendingRequests.set(id, { resolve, reject })
135
+
136
+ ws.send(JSON.stringify({ id, tool, args }))
137
+
138
+ setTimeout(() => {
139
+ if (pendingRequests.has(id)) {
140
+ pendingRequests.delete(id)
141
+ reject(new Error('Tool call timed out'))
142
+ }
143
+ }, 30000)
144
+ })
145
+ }
146
+
147
+ // --- MCP Protocol Handler ---
148
+ function sendResponse(response) {
149
+ const str = JSON.stringify(response)
150
+ process.stdout.write(str + '\n')
151
+ }
152
+
153
+ async function handleMessage(message) {
154
+ const id = message.id
155
+ const method = message.method || ''
156
+
157
+ switch (method) {
158
+ case 'initialize':
159
+ return {
160
+ jsonrpc: '2.0',
161
+ id,
162
+ result: {
163
+ protocolVersion: '2024-11-05',
164
+ capabilities: { tools: {} },
165
+ serverInfo: { name: 'verso', version: '0.2.0' },
166
+ },
167
+ }
168
+
169
+ case 'notifications/initialized':
170
+ return null
171
+
172
+ case 'tools/list':
173
+ return {
174
+ jsonrpc: '2.0',
175
+ id,
176
+ result: { tools: getToolDefinitions() },
177
+ }
178
+
179
+ case 'tools/call': {
180
+ const params = message.params || {}
181
+ const toolName = params.name || ''
182
+ const toolArgs = params.arguments || {}
183
+
184
+ // Local tools (API calls, not WebSocket)
185
+ if (toolName === 'list_projects') {
186
+ try {
187
+ const projects = await apiRequest('GET', '/api/projects')
188
+ const text = JSON.stringify(projects, null, 2)
189
+ return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }] } }
190
+ } catch (err) {
191
+ return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true } }
192
+ }
193
+ }
194
+
195
+ if (toolName === 'open_project') {
196
+ activeProjectId = toolArgs.projectId
197
+ registerProject()
198
+ return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: `Switched to project ${activeProjectId}` }] } }
199
+ }
200
+
201
+ try {
202
+ const response = await sendToolCall(toolName, toolArgs)
203
+ const result = response.result
204
+
205
+ // Screenshot response
206
+ if (result && result._screenshot && result.data) {
207
+ return {
208
+ jsonrpc: '2.0',
209
+ id,
210
+ result: {
211
+ content: [{ type: 'image', data: result.data, mimeType: 'image/png' }],
212
+ },
213
+ }
214
+ }
215
+
216
+ const text = typeof result === 'string' ? result : JSON.stringify(result)
217
+ return {
218
+ jsonrpc: '2.0',
219
+ id,
220
+ result: { content: [{ type: 'text', text }] },
221
+ }
222
+ } catch (err) {
223
+ return {
224
+ jsonrpc: '2.0',
225
+ id,
226
+ result: {
227
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
228
+ isError: true,
229
+ },
230
+ }
231
+ }
232
+ }
233
+
234
+ case 'ping':
235
+ return { jsonrpc: '2.0', id, result: {} }
236
+
237
+ default:
238
+ if (id) {
239
+ return {
240
+ jsonrpc: '2.0',
241
+ id,
242
+ error: { code: -32601, message: `Method not found: ${method}` },
243
+ }
244
+ }
245
+ return null
246
+ }
247
+ }
248
+
249
+ // --- Tool Definitions ---
250
+ function getToolDefinitions() {
251
+ return [
252
+ {
253
+ name: 'write_html',
254
+ description: `You are an elite mobile app designer creating world-class, production-quality app screens.
255
+ Write standalone HTML using Tailwind CSS v4 utility classes + Iconify icons.
256
+ Your designs MUST look like premium, polished REAL production apps (Airbnb, Uber, Spotify, Revolut, etc.) — never wireframes or prototypes.
257
+
258
+ The renderer pre-loads: Tailwind CSS v4 (browser CDN), Iconify, Google Fonts preconnect.
259
+
260
+ === DESIGN TOKEN SYSTEM ===
261
+ EVERY screen MUST begin with a Google Fonts <link> tag and a <style type="text/tailwindcss"> block that defines your design tokens.
262
+ Choose a cohesive, intentional color palette for the app. Example:
263
+
264
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
265
+ <style type="text/tailwindcss">
266
+ @theme inline {
267
+ --color-background: var(--background);
268
+ --color-foreground: var(--foreground);
269
+ --color-primary: var(--primary);
270
+ --color-primary-foreground: var(--primary-foreground);
271
+ --color-secondary: var(--secondary);
272
+ --color-secondary-foreground: var(--secondary-foreground);
273
+ --color-muted: var(--muted);
274
+ --color-muted-foreground: var(--muted-foreground);
275
+ --color-accent: var(--accent);
276
+ --color-accent-foreground: var(--accent-foreground);
277
+ --color-destructive: var(--destructive);
278
+ --color-card: var(--card);
279
+ --color-card-foreground: var(--card-foreground);
280
+ --color-border: var(--border);
281
+ --color-input: var(--input);
282
+ --color-ring: var(--ring);
283
+ --font-font-sans: var(--font-sans);
284
+ --font-font-heading: var(--font-heading);
285
+ --radius-sm: calc(var(--radius) - 4px);
286
+ --radius-md: calc(var(--radius) - 2px);
287
+ --radius-lg: var(--radius);
288
+ --radius-xl: calc(var(--radius) + 4px);
289
+ }
290
+ :root {
291
+ --background: #FFFFFF;
292
+ --foreground: #1A1C1E;
293
+ --primary: #FF6B35;
294
+ --primary-foreground: #FFFFFF;
295
+ --secondary: #F5F7F9;
296
+ --secondary-foreground: #1A1C1E;
297
+ --muted: #F5F7F9;
298
+ --muted-foreground: #8A9198;
299
+ --accent: #FFF0F0;
300
+ --accent-foreground: #C81E1E;
301
+ --destructive: #EF4444;
302
+ --card: #FFFFFF;
303
+ --card-foreground: #1A1C1E;
304
+ --border: #F0F2F5;
305
+ --input: #F5F7F9;
306
+ --ring: #FF6B35;
307
+ --font-sans: "Plus Jakarta Sans";
308
+ --font-heading: "Plus Jakarta Sans";
309
+ --radius: 1rem;
310
+ }
311
+ </style>
312
+
313
+ Customize ALL colors to match the app's brand/mood. Be creative with palettes — don't default to blue.
314
+ For dark themes, invert: --background: #0A0A0A, --foreground: #FAFAFA, --card: #1C1C1E, etc.
315
+
316
+ === ICON SYNTAX ===
317
+ Use Iconify with the Solar icon set (recommended — beautiful, consistent):
318
+ <iconify-icon icon="solar:home-2-bold" class="size-6"></iconify-icon>
319
+ <iconify-icon icon="solar:heart-linear" class="size-5"></iconify-icon>
320
+ <iconify-icon icon="solar:magnifer-linear" class="size-5"></iconify-icon>
321
+
322
+ Use bold/filled variants for active/selected states, linear/outline for inactive.
323
+ Other great sets: lucide, ph (Phosphor), mdi, tabler, fluent.
324
+
325
+ === SCREEN STRUCTURE ===
326
+ IMPORTANT: The renderer is an iframe sized to the phone frame (393x852). Use w-full, NOT max-w-md. The iframe IS the phone viewport.
327
+
328
+ <div class="flex flex-col min-h-screen bg-background text-foreground font-sans w-full relative overflow-hidden pb-20">
329
+ <!-- Status bar -->
330
+ <!-- Header -->
331
+ <!-- Scrollable content -->
332
+ <!-- Tab bar at bottom -->
333
+ </div>
334
+
335
+ === IMAGES ===
336
+ NEVER use emoji as image placeholders. Use real photos:
337
+ - Avatars: https://i.pravatar.cc/SIZE?u=UNIQUE_NAME
338
+ - Products/Content: https://picsum.photos/W/H?random=N
339
+ - Specific: https://images.unsplash.com/photo-PHOTO_ID?w=W&h=H&fit=crop
340
+ All images: object-cover, proper border-radius, aspect ratios.
341
+
342
+ === QUALITY RULES ===
343
+ 1. Study real apps. Your design must be indistinguishable from a shipping product.
344
+ 2. Visual hierarchy: font-bold for titles, font-semibold for labels, font-medium for secondary, regular for body.
345
+ 3. Spacing: px-6 for screen padding. gap-2/3/4 for flex items. Generous whitespace.
346
+ 4. Icons: size-4 (12px), size-5 (20px), size-6 (24px). Consistent within context.
347
+ 5. Borders: border-border/50 (subtle). Never harsh borders.
348
+ 6. Shadows: shadow-sm for cards. Never heavy shadows.
349
+ 7. Backdrop blur: backdrop-blur-md for overlays and tab bars.
350
+ 8. Color with intention: primary for CTAs, muted-foreground for secondary text, destructive for alerts.
351
+ 9. Rounded corners: rounded-full for pills/avatars, rounded-2xl for cards, rounded-[20px]+ for large cards.
352
+ 10. Touch targets: minimum 44px (h-11).
353
+ 11. Text overflow: truncate for constrained text.
354
+ 12. Consistent icon style: don't mix filled and outline in same context.
355
+ 13. pb-24 on the main container when using a fixed tab bar.
356
+ 14. Real data: use realistic names, prices, dates — never "Lorem ipsum".
357
+
358
+ If targetNodeId is provided, writes into that specific node instead of replacing the entire canvas.`,
359
+ inputSchema: {
360
+ type: 'object',
361
+ properties: {
362
+ html: {
363
+ type: 'string',
364
+ description: 'Complete HTML content including <style type="text/tailwindcss"> theme block, Google Font <link> tags, and body markup.',
365
+ },
366
+ platform: { type: 'string', enum: ['iPhone 16', 'Web'], description: "Target platform." },
367
+ accentColor: { type: 'string', description: "Primary accent/brand color as hex." },
368
+ fontFamily: { type: 'string', description: "Primary font family used." },
369
+ theme: { type: 'string', enum: ['Light', 'Dark'], description: 'Color theme.' },
370
+ iconLibrary: { type: 'string', description: "Icon set used." },
371
+ targetNodeId: { type: 'string', description: 'Optional data-ps-id to write into instead of replacing entire canvas.' },
372
+ changeDescription: {
373
+ type: 'string',
374
+ description: 'REQUIRED. Write a specific, meaningful commit-style message describing what you changed. Bad: "Design update". Good: "Add bottom tab bar with 4 tabs", "Change font to Google Sans".',
375
+ },
376
+ },
377
+ required: ['html', 'platform', 'accentColor', 'fontFamily', 'theme', 'iconLibrary', 'changeDescription'],
378
+ },
379
+ },
380
+ {
381
+ name: 'get_tree',
382
+ description: 'Get the current layer tree of the canvas. Returns a JSON array of nodes with id, tag, name, and children.',
383
+ inputSchema: { type: 'object', properties: {} },
384
+ },
385
+ {
386
+ name: 'set_text',
387
+ description: 'Set the text content of a specific node by its data-ps-id.',
388
+ inputSchema: {
389
+ type: 'object',
390
+ properties: {
391
+ nodeId: { type: 'string', description: 'The data-ps-id of the target node' },
392
+ text: { type: 'string', description: 'New text content' },
393
+ changeDescription: { type: 'string', description: 'REQUIRED. Specific commit-style message. Bad: "Edit text". Good: "Change welcome heading to Turkish".' },
394
+ },
395
+ required: ['nodeId', 'text', 'changeDescription'],
396
+ },
397
+ },
398
+ {
399
+ name: 'set_style',
400
+ description: 'Set inline CSS styles on a specific node by its data-ps-id.',
401
+ inputSchema: {
402
+ type: 'object',
403
+ properties: {
404
+ nodeId: { type: 'string', description: 'The data-ps-id of the target node' },
405
+ css: { type: 'string', description: "CSS properties to apply (e.g. 'color: red; font-size: 16px')" },
406
+ changeDescription: { type: 'string', description: 'REQUIRED. Specific commit-style message. Bad: "Edit style". Good: "Increase button border-radius to 16px".' },
407
+ },
408
+ required: ['nodeId', 'css', 'changeDescription'],
409
+ },
410
+ },
411
+ {
412
+ name: 'remove_node',
413
+ description: 'Remove a node from the canvas by its data-ps-id.',
414
+ inputSchema: {
415
+ type: 'object',
416
+ properties: {
417
+ nodeId: { type: 'string', description: 'The data-ps-id of the node to remove' },
418
+ changeDescription: { type: 'string', description: 'REQUIRED. Specific commit-style message. Bad: "Remove element". Good: "Remove promotional banner from homepage".' },
419
+ },
420
+ required: ['nodeId', 'changeDescription'],
421
+ },
422
+ },
423
+ {
424
+ name: 'get_screenshot',
425
+ description: 'Capture a screenshot of the current canvas as a base64-encoded PNG image.',
426
+ inputSchema: { type: 'object', properties: {} },
427
+ },
428
+ {
429
+ name: 'create_screen',
430
+ description: 'Create a new blank screen in the project.',
431
+ inputSchema: {
432
+ type: 'object',
433
+ properties: {
434
+ name: { type: 'string', description: 'Name of the new screen' },
435
+ },
436
+ },
437
+ },
438
+ {
439
+ name: 'list_screens',
440
+ description: 'List all screens in the project with their indices and active status.',
441
+ inputSchema: { type: 'object', properties: {} },
442
+ },
443
+ {
444
+ name: 'switch_screen',
445
+ description: 'Switch to a different screen by its index or name.',
446
+ inputSchema: {
447
+ type: 'object',
448
+ properties: {
449
+ index: { type: 'number', description: 'Index of the screen to switch to' },
450
+ name: { type: 'string', description: 'Name of the screen to switch to' },
451
+ },
452
+ },
453
+ },
454
+ {
455
+ name: 'get_design_settings',
456
+ description: 'Returns the current design system state (theme, accentColor, iconLibrary, fontFamily, device, viewport).',
457
+ inputSchema: { type: 'object', properties: {} },
458
+ },
459
+ {
460
+ name: 'list_projects',
461
+ description: 'List all your Verso projects. Returns project id, name, screen count, and last updated time.',
462
+ inputSchema: { type: 'object', properties: {} },
463
+ },
464
+ {
465
+ name: 'open_project',
466
+ description: 'Connect to a specific Verso project by its ID. Must be called before using any design tools.',
467
+ inputSchema: {
468
+ type: 'object',
469
+ properties: {
470
+ projectId: { type: 'string', description: 'The project ID to connect to (from list_projects)' },
471
+ },
472
+ required: ['projectId'],
473
+ },
474
+ },
475
+ ]
476
+ }
477
+
478
+ // --- Start ---
479
+ connectWS()
480
+
481
+ const rl = readline.createInterface({ input: process.stdin, terminal: false })
482
+ rl.on('line', async (line) => {
483
+ if (!line.trim()) return
484
+
485
+ try {
486
+ const message = JSON.parse(line)
487
+ const response = await handleMessage(message)
488
+ if (response) {
489
+ sendResponse(response)
490
+ }
491
+ } catch (err) {
492
+ log('Error processing message:', err.message)
493
+ }
494
+ })
495
+
496
+ log('MCP server started')
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "verso-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Verso — AI-powered mobile app design tool",
5
+ "bin": {
6
+ "verso-mcp": "./index.js"
7
+ },
8
+ "dependencies": {
9
+ "ws": "^8.18.0"
10
+ },
11
+ "keywords": ["mcp", "verso", "design", "ai", "claude", "mobile"],
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/niceeee/verso-mcp"
16
+ }
17
+ }