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.
- package/README.md +47 -0
- package/index.js +496 -0
- 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
|
+
}
|