cyrus-studio 1.0.7 → 1.0.8

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.
@@ -0,0 +1,394 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Cyrus Studio Model Context Protocol (MCP) CLI Entrypoint v1.0.8
5
+ *
6
+ * Standalone JSON-RPC 2.0 / MCP Server for:
7
+ * - Claude Desktop
8
+ * - Cursor
9
+ * - Antigravity / Gemini CLI
10
+ * - Autonomous AI Agents
11
+ */
12
+
13
+ import readline from 'node:readline';
14
+ import { randomBytes } from 'node:crypto';
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import os from 'node:os';
18
+
19
+ function generateId(prefix = 'el') {
20
+ return `${prefix}_${randomBytes(5).toString('hex')}`;
21
+ }
22
+
23
+ // ─── LOCAL ATOMIC PERSISTENCE ───────────────────────────────────────
24
+ const DB_DIR = path.join(os.homedir(), '.cyrus-studio');
25
+ const DB_FILE = path.join(DB_DIR, 'db.json');
26
+
27
+ function loadMemoryDb() {
28
+ try {
29
+ if (!fs.existsSync(DB_DIR)) fs.mkdirSync(DB_DIR, { recursive: true });
30
+ if (fs.existsSync(DB_FILE)) {
31
+ return JSON.parse(fs.readFileSync(DB_FILE, 'utf8'));
32
+ }
33
+ } catch {}
34
+ return { version: 1, projects: [] };
35
+ }
36
+
37
+ function saveMemoryDb(db) {
38
+ try {
39
+ if (!fs.existsSync(DB_DIR)) fs.mkdirSync(DB_DIR, { recursive: true });
40
+ fs.writeFileSync(DB_FILE, JSON.stringify(db, null, 2), 'utf8');
41
+ } catch {}
42
+ }
43
+
44
+ // ─── MCP TOOL SCHEMAS ───────────────────────────────────────────────
45
+ export const MCP_TOOLS = [
46
+ {
47
+ name: 'cyrus_generate_ui',
48
+ description: 'Generates a complete, structured visual UI page layout on the Cyrus Canvas from a text prompt. Creates the project, saves it, and returns the interactive canvas URLs and clean React code.',
49
+ inputSchema: {
50
+ type: 'object',
51
+ properties: {
52
+ prompt: { type: 'string', description: 'Description of the UI to generate' },
53
+ type: { type: 'string', enum: ['webapp', 'website', 'dashboard', 'landing', 'mobile'], default: 'webapp' },
54
+ theme: { type: 'string', enum: ['light', 'dark'], default: 'dark' },
55
+ preset: { type: 'string', enum: ['apple-hig', 'shadcn-zinc', 'shadcn-slate', 'shadcn-violet', 'shadcn-rose', 'tailwind-cyberpunk'], default: 'shadcn-zinc' }
56
+ },
57
+ required: ['prompt']
58
+ }
59
+ },
60
+ {
61
+ name: 'cyrus_create_element',
62
+ description: 'Creates and places a new high-fidelity UI widget/component onto the Cyrus Canvas (navbar, hero, button, pricingCard, metricCard, lineChart, searchBar, container, footer).',
63
+ inputSchema: {
64
+ type: 'object',
65
+ properties: {
66
+ projectId: { type: 'string', description: 'Target project ID' },
67
+ type: { type: 'string', description: 'Component type' },
68
+ name: { type: 'string', description: 'Human-readable element label' },
69
+ x: { type: 'number' },
70
+ y: { type: 'number' },
71
+ width: { type: 'number' },
72
+ height: { type: 'number' },
73
+ styles: { type: 'object' },
74
+ content: { type: 'object' }
75
+ },
76
+ required: ['type']
77
+ }
78
+ },
79
+ {
80
+ name: 'cyrus_modify_element',
81
+ description: 'Updates specific visual styles, text content, or dimensions of an existing canvas element by ID.',
82
+ inputSchema: {
83
+ type: 'object',
84
+ properties: {
85
+ projectId: { type: 'string' },
86
+ elementId: { type: 'string' },
87
+ updates: { type: 'object' }
88
+ },
89
+ required: ['projectId', 'elementId', 'updates']
90
+ }
91
+ },
92
+ {
93
+ name: 'cyrus_delete_element',
94
+ description: 'Deletes an element from the Cyrus canvas by its element ID.',
95
+ inputSchema: {
96
+ type: 'object',
97
+ properties: {
98
+ projectId: { type: 'string' },
99
+ elementId: { type: 'string' }
100
+ },
101
+ required: ['projectId', 'elementId']
102
+ }
103
+ },
104
+ {
105
+ name: 'cyrus_apply_design_system',
106
+ description: 'Applies global design system tokens (colors, typography, radii, theme presets) across the canvas.',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ projectId: { type: 'string' },
111
+ preset: { type: 'string', enum: ['apple-hig', 'shadcn-zinc', 'shadcn-slate', 'shadcn-violet', 'shadcn-rose', 'tailwind-cyberpunk'] },
112
+ colors: { type: 'object' },
113
+ radii: { type: 'object' }
114
+ },
115
+ required: ['projectId']
116
+ }
117
+ },
118
+ {
119
+ name: 'cyrus_get_canvas_document',
120
+ description: 'Retrieves the complete structured Cyrus IR JSON document of a canvas project.',
121
+ inputSchema: {
122
+ type: 'object',
123
+ properties: {
124
+ projectId: { type: 'string' }
125
+ }
126
+ }
127
+ },
128
+ {
129
+ name: 'cyrus_list_projects',
130
+ description: 'Lists all Cyrus Studio projects available with IDs, names, and preview URLs.',
131
+ inputSchema: {
132
+ type: 'object',
133
+ properties: {}
134
+ }
135
+ },
136
+ {
137
+ name: 'cyrus_export_code',
138
+ description: 'Exports production-ready code (React + Tailwind CSS, Flutter, SwiftUI, or HTML + CSS) for the current visual canvas.',
139
+ inputSchema: {
140
+ type: 'object',
141
+ properties: {
142
+ projectId: { type: 'string' },
143
+ framework: { type: 'string', enum: ['react-tailwind', 'flutter', 'swiftui', 'html-css'], default: 'react-tailwind' }
144
+ },
145
+ required: ['projectId']
146
+ }
147
+ }
148
+ ];
149
+
150
+ // ─── PURE ESM CODE GENERATOR ─────────────────────────────────────────
151
+ function generateReactTailwindCode(page) {
152
+ const elements = page?.elements || [];
153
+ const componentName = (page?.name || 'Page').replace(/[^a-zA-Z0-9]/g, '') || 'CyrusApp';
154
+
155
+ const rendered = elements.map(el => {
156
+ const c = el.content || {};
157
+ return ` {/* ${el.name || el.type} */}
158
+ <div className="p-6 rounded-2xl border border-white/10 bg-neutral-900/60 shadow-xl">
159
+ <h3 className="text-xl font-bold text-white">${c.title || c.headline || c.text || el.name || 'Component'}</h3>
160
+ </div>`;
161
+ });
162
+
163
+ return `import React from 'react';
164
+
165
+ export default function ${componentName}() {
166
+ return (
167
+ <div className="min-h-screen bg-[#09090b] text-neutral-100 p-8 space-y-6">
168
+ ${rendered.join('\n\n')}
169
+ </div>
170
+ );
171
+ }`;
172
+ }
173
+
174
+ export async function handleToolCall(name, args = {}) {
175
+ const db = loadMemoryDb();
176
+
177
+ switch (name) {
178
+ case 'cyrus_generate_ui': {
179
+ const prompt = String(args.prompt || 'Modern Web Application').trim();
180
+ const theme = args.theme || 'dark';
181
+ const type = args.type || 'webapp';
182
+ const preset = args.preset || 'shadcn-zinc';
183
+ const projectId = `proj_${generateId()}`;
184
+
185
+ const elements = [
186
+ {
187
+ id: `el_${generateId()}`,
188
+ type: 'navbar',
189
+ name: 'Main Navigation Bar',
190
+ x: 40,
191
+ y: 30,
192
+ width: 1200,
193
+ height: 64,
194
+ styles: { backgroundColor: '#18181b', borderRadius: 12 },
195
+ content: { brandName: 'Cyrus Studio AI', buttonText: 'Launch App' }
196
+ },
197
+ {
198
+ id: `el_${generateId()}`,
199
+ type: 'hero',
200
+ name: 'Hero Section',
201
+ x: 40,
202
+ y: 110,
203
+ width: 1200,
204
+ height: 320,
205
+ styles: { backgroundColor: '#09090b', borderRadius: 16 },
206
+ content: {
207
+ badge: 'AI Generated Layout',
208
+ headline: prompt.slice(0, 60),
209
+ subtitle: `Generated for ${type.toUpperCase()} layout using ${preset} design tokens.`,
210
+ buttonText: 'Get Started'
211
+ }
212
+ },
213
+ {
214
+ id: `el_${generateId()}`,
215
+ type: 'pricingCard',
216
+ name: 'Subscription Plan',
217
+ x: 40,
218
+ y: 450,
219
+ width: 380,
220
+ height: 360,
221
+ styles: { backgroundColor: '#18181b', borderRadius: 16 },
222
+ content: {
223
+ badge: 'PRO TIER',
224
+ planName: 'Enterprise Engine',
225
+ price: '$49/mo',
226
+ features: ['Unlimited Canvas Projects', 'Real-time MCP Sync', 'React & Flutter Export']
227
+ }
228
+ }
229
+ ];
230
+
231
+ const page = { id: `page_${generateId()}`, name: 'Home', elements };
232
+ const projectDoc = {
233
+ id: projectId,
234
+ name: prompt.slice(0, 40),
235
+ version: '1.0.8',
236
+ theme,
237
+ type,
238
+ preset,
239
+ pages: [page],
240
+ createdAt: new Date().toISOString(),
241
+ updatedAt: new Date().toISOString()
242
+ };
243
+
244
+ db.projects.push({ document: projectDoc });
245
+ saveMemoryDb(db);
246
+
247
+ return {
248
+ success: true,
249
+ projectId,
250
+ title: projectDoc.name,
251
+ elementCount: elements.length,
252
+ editorUrl: `https://cyrustudio.vercel.app/#/editor/${projectId}`,
253
+ previewUrl: `https://cyrustudio.vercel.app/#/embed/${projectId}?theme=${theme}`,
254
+ document: projectDoc,
255
+ code: generateReactTailwindCode(page)
256
+ };
257
+ }
258
+
259
+ case 'cyrus_create_element': {
260
+ let targetProj = db.projects[db.projects.length - 1]?.document;
261
+ if (args.projectId) {
262
+ const found = db.projects.find(p => p.document?.id === args.projectId);
263
+ if (found) targetProj = found.document;
264
+ }
265
+ if (!targetProj) {
266
+ targetProj = {
267
+ id: `proj_${generateId()}`,
268
+ name: 'Cyrus Project',
269
+ pages: [{ id: `page_${generateId()}`, name: 'Home', elements: [] }]
270
+ };
271
+ db.projects.push({ document: targetProj });
272
+ }
273
+
274
+ const activePage = targetProj.pages[0];
275
+ const newEl = {
276
+ id: `el_${generateId()}`,
277
+ type: args.type || 'button',
278
+ name: args.name || args.type || 'New Element',
279
+ x: args.x || 100,
280
+ y: args.y || 120,
281
+ width: args.width || 320,
282
+ height: args.height || 120,
283
+ styles: args.styles || { backgroundColor: '#18181b', borderRadius: 12 },
284
+ content: args.content || { title: args.name || 'New Widget' }
285
+ };
286
+
287
+ activePage.elements.push(newEl);
288
+ saveMemoryDb(db);
289
+
290
+ return { success: true, projectId: targetProj.id, createdElement: newEl, totalElements: activePage.elements.length };
291
+ }
292
+
293
+ case 'cyrus_modify_element': {
294
+ const { projectId, elementId, updates = {} } = args;
295
+ const proj = db.projects.find(p => p.document?.id === projectId);
296
+ if (!proj) throw new Error(`Project "${projectId}" not found.`);
297
+
298
+ let targetEl = null;
299
+ for (const page of (proj.document.pages || [])) {
300
+ targetEl = (page.elements || []).find(e => e.id === elementId);
301
+ if (targetEl) break;
302
+ }
303
+ if (!targetEl) throw new Error(`Element "${elementId}" not found.`);
304
+
305
+ if (updates.styles) targetEl.styles = { ...targetEl.styles, ...updates.styles };
306
+ if (updates.content) targetEl.content = { ...targetEl.content, ...updates.content };
307
+ saveMemoryDb(db);
308
+
309
+ return { success: true, projectId, elementId, updatedElement: targetEl };
310
+ }
311
+
312
+ case 'cyrus_list_projects': {
313
+ const list = (db.projects || []).map(p => ({
314
+ id: p.document?.id,
315
+ name: p.document?.name || 'Untitled',
316
+ previewUrl: `https://cyrustudio.vercel.app/#/embed/${p.document?.id}`
317
+ }));
318
+ return { success: true, count: list.length, projects: list };
319
+ }
320
+
321
+ case 'cyrus_export_code': {
322
+ const { projectId, framework = 'react-tailwind' } = args;
323
+ const proj = db.projects.find(p => p.document?.id === projectId);
324
+ if (!proj) throw new Error(`Project "${projectId}" not found.`);
325
+ const page = proj.document.pages?.[0] || { name: 'Home', elements: [] };
326
+ return { success: true, projectId, framework, code: generateReactTailwindCode(page) };
327
+ }
328
+
329
+ default:
330
+ throw new Error(`Unknown Cyrus MCP tool: "${name}"`);
331
+ }
332
+ }
333
+
334
+ // ─── JSON-RPC 2.0 PROTOCOL HANDLER ───────────────────────────────────
335
+ export async function handleMcpJsonRpc(request) {
336
+ if (!request || typeof request !== 'object') {
337
+ return { jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } };
338
+ }
339
+
340
+ const { id, method, params } = request;
341
+
342
+ if (method === 'initialize') {
343
+ return {
344
+ jsonrpc: '2.0',
345
+ id,
346
+ result: {
347
+ protocolVersion: '2024-11-05',
348
+ capabilities: { tools: { listChanged: false } },
349
+ serverInfo: { name: 'cyrus-studio', version: '1.0.8' }
350
+ }
351
+ };
352
+ }
353
+
354
+ if (method === 'notifications/initialized' || method === 'initialized') return null;
355
+ if (method === 'ping') return { jsonrpc: '2.0', id, result: {} };
356
+ if (method === 'tools/list') return { jsonrpc: '2.0', id, result: { tools: MCP_TOOLS } };
357
+
358
+ if (method === 'tools/call') {
359
+ try {
360
+ const data = await handleToolCall(params?.name, params?.arguments);
361
+ return {
362
+ jsonrpc: '2.0',
363
+ id,
364
+ result: { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], isError: false }
365
+ };
366
+ } catch (err) {
367
+ return {
368
+ jsonrpc: '2.0',
369
+ id,
370
+ result: { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true }
371
+ };
372
+ }
373
+ }
374
+
375
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method "${method}" not found.` } };
376
+ }
377
+
378
+ // ─── STDIO STREAM ───────────────────────────────────────────────────
379
+ if (process.argv.includes('--stdio') || !process.stdin.isTTY || process.argv.includes('mcp-server')) {
380
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
381
+ rl.on('line', async (line) => {
382
+ const trimmed = line.trim();
383
+ if (!trimmed) return;
384
+ try {
385
+ const res = await handleMcpJsonRpc(JSON.parse(trimmed));
386
+ if (res) process.stdout.write(JSON.stringify(res) + '\n');
387
+ } catch (err) {
388
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: null, error: { message: err.message } }) + '\n');
389
+ }
390
+ });
391
+ } else {
392
+ console.log('🤖 Cyrus Studio MCP Server v1.0.8');
393
+ console.log('Use with Claude Desktop or Cursor: npx cyrus-studio mcp-server');
394
+ }