@elixpo/lixsketch 5.6.2 → 5.6.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elixpo/lixsketch",
3
- "version": "5.6.2",
3
+ "version": "5.6.4",
4
4
  "description": "Open-source SVG whiteboard engine + React canvas component with hand-drawn aesthetics — the core of LixSketch",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -48,6 +48,9 @@
48
48
  "url": "https://github.com/elixpo"
49
49
  },
50
50
  "license": "MIT",
51
+ "engines": {
52
+ "node": ">=20.0.0"
53
+ },
51
54
  "homepage": "https://sketch.elixpo.com",
52
55
  "repository": {
53
56
  "type": "git",
package/src/mcp/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export { createLixSketchMcpServer, LixSketchMcpServer, LIXSKETCH_MCP_PROTOCOL_VERSION, LIXSKETCH_MCP_TOOLS } from './server.js';
2
2
  export { MemorySceneStore } from './store.js';
3
+ export { RemoteSceneStore, decryptRemoteScene, encryptRemoteScene } from './remoteStore.js';
4
+ export { compileLixScript } from './lixscript.js';
3
5
  export { MarketplaceTemplateProvider, decryptPublicTemplate } from './templates.js';
4
6
  export { applyScenePatch, createEmptyScene, getSceneBounds, getSceneSummary, mergeTemplateScene, normalizeShape, validateScene, MCP_LIMITS } from './scene.js';
5
7
  export { renderSceneSvg } from './preview.js';
@@ -0,0 +1,93 @@
1
+ import { parseLixScript, resolveShapeRefs } from '../core/LixScriptParser.js';
2
+
3
+ const MAX_SOURCE_LENGTH = 100_000;
4
+
5
+ function options(def) {
6
+ return {
7
+ stroke: def.props.stroke || def.props.color || '#8b76d6',
8
+ strokeWidth: Number(def.props.strokeWidth) || 2,
9
+ fill: def.props.fill || 'transparent',
10
+ fillStyle: def.props.fillStyle || 'solid',
11
+ roughness: def.props.roughness === undefined ? 1.2 : Number(def.props.roughness),
12
+ opacity: def.props.opacity === undefined ? 1 : Number(def.props.opacity),
13
+ };
14
+ }
15
+
16
+ function bounds(def) {
17
+ const width = Number(def.width) || (def.type === 'frame' ? 600 : def.type === 'rect' ? 160 : 80);
18
+ const height = Number(def.height) || (def.type === 'frame' ? 400 : def.type === 'rect' ? 60 : 80);
19
+ return { x: Number(def.x) || 0, y: Number(def.y) || 0, width, height };
20
+ }
21
+
22
+ function endpoint(point, definitions) {
23
+ if (Number.isFinite(point?.x) && Number.isFinite(point?.y)) return { x: point.x, y: point.y };
24
+ const target = definitions.get(point?.ref);
25
+ if (!target) throw new Error(`Cannot resolve LixScript connection target "${point?.ref || ''}"`);
26
+ const box = bounds(target);
27
+ const offset = Number(point.offset) || 0;
28
+ const side = point.side || 'center';
29
+ if (side === 'top') return { x: box.x + box.width / 2 + offset, y: box.y };
30
+ if (side === 'bottom') return { x: box.x + box.width / 2 + offset, y: box.y + box.height };
31
+ if (side === 'left') return { x: box.x, y: box.y + box.height / 2 + offset };
32
+ if (side === 'right') return { x: box.x + box.width, y: box.y + box.height / 2 + offset };
33
+ return { x: box.x + box.width / 2 + offset, y: box.y + box.height / 2 };
34
+ }
35
+
36
+ function labelShape(def, shapeID, parentFrame) {
37
+ if (!def.props.label) return null;
38
+ const box = bounds(def);
39
+ return {
40
+ type: 'text', shapeID: `${shapeID}-label`, x: box.x + box.width / 2, y: box.y + box.height / 2,
41
+ text: String(def.props.label), fontSize: Number(def.props.labelFontSize) || 14,
42
+ color: def.props.labelColor || '#e8e3f3', parentFrame,
43
+ };
44
+ }
45
+
46
+ export function compileLixScript(source, { x = 0, y = 0 } = {}) {
47
+ const input = String(source || '');
48
+ if (!input.trim()) throw new Error('LixScript source is required');
49
+ if (input.length > MAX_SOURCE_LENGTH) throw new Error('LixScript source exceeds 100 KB');
50
+ const parsed = parseLixScript(input);
51
+ if (parsed.errors.length) throw new Error(`LixScript parse failed: ${parsed.errors.map((entry) => `line ${entry.line}: ${entry.message}`).join('; ')}`);
52
+ resolveShapeRefs(parsed.shapes);
53
+ const prefix = `lix-${crypto.randomUUID().slice(0, 8)}`;
54
+ const shapeId = (id) => `${prefix}-${id}`;
55
+ const definitions = new Map(parsed.shapes.map((shape) => [shape.id, shape]));
56
+ const frame = parsed.shapes.find((shape) => shape.type === 'frame');
57
+ const frameId = frame ? shapeId(frame.id) : `${prefix}-frame`;
58
+ const frameMembers = frame?.props.contains ? new Set(String(frame.props.contains).split(',').map((value) => value.trim()).filter(Boolean)) : null;
59
+ const shapes = [];
60
+ for (const def of parsed.shapes) {
61
+ const box = bounds(def);
62
+ const parentFrame = def === frame || (frameMembers && !frameMembers.has(def.id)) ? null : frameId;
63
+ let shape;
64
+ const id = shapeId(def.id);
65
+ if (def.type === 'rect') shape = { type: 'rectangle', shapeID: id, x: box.x + x, y: box.y + y, width: box.width, height: box.height, rotation: Number(def.props.rotation) || 0, options: options(def), parentFrame };
66
+ else if (def.type === 'circle' || def.type === 'ellipse') shape = { type: 'circle', shapeID: id, x: box.x + box.width / 2 + x, y: box.y + box.height / 2 + y, rx: box.width / 2, ry: box.height / 2, rotation: Number(def.props.rotation) || 0, options: options(def), parentFrame };
67
+ else if (def.type === 'text') shape = { type: 'text', shapeID: id, x: box.x + x, y: box.y + y, text: String(def.props.content || def.props.text || 'Text'), fontSize: Number(def.props.fontSize) || 16, color: def.props.color || def.props.fill || '#e8e3f3', fontFamily: def.props.fontFamily || 'lixFont', parentFrame };
68
+ else if (def.type === 'frame') shape = { type: 'frame', shapeID: id, x: box.x + x, y: box.y + y, width: box.width, height: box.height, frameName: String(def.props.frameName || def.props.name || def.id), fillStyle: def.props.fillStyle || 'transparent', fillColor: def.props.fillColor || def.props.fill || '#1e1e28', options: options(def) };
69
+ else if (def.type === 'freehand') {
70
+ const points = String(def.props.points || '').split(';').map((value) => value.split(',').map(Number)).filter((point) => point.length >= 2 && point.every(Number.isFinite)).map(([px, py, pressure = 0.5]) => [px + x, py + y, pressure]);
71
+ shape = { type: 'freehandStroke', shapeID: id, points, options: options(def), parentFrame };
72
+ } else if (def.type === 'line' || def.type === 'arrow') {
73
+ const startPoint = endpoint(def.from, definitions), endPoint = endpoint(def.to, definitions);
74
+ startPoint.x += x; startPoint.y += y; endPoint.x += x; endPoint.y += y;
75
+ shape = def.type === 'line'
76
+ ? { type: 'line', shapeID: id, startPoint, endPoint, isCurved: def.props.curve === true || def.props.curve === 'true', options: options(def), parentFrame }
77
+ : { type: 'arrow', shapeID: id, startPoint, endPoint, arrowHeadStyle: def.props.head || 'triangle', arrowOutlineStyle: def.props.style || 'solid', arrowCurved: def.props.curve && def.props.curve !== 'straight', arrowCurveAmount: Number(def.props.curveAmount) || 0.2, options: options(def), parentFrame };
78
+ } else throw new Error(`LixScript ${def.type} is not writable through MCP`);
79
+ shapes.push(shape);
80
+ const label = labelShape(def, id, parentFrame);
81
+ if (label) { label.x += x; label.y += y; shapes.push(label); }
82
+ }
83
+ if (!frame && shapes.length) {
84
+ const boxes = parsed.shapes.filter((def) => !['arrow', 'line'].includes(def.type)).map(bounds);
85
+ const pointShapes = shapes.filter((shape) => shape.startPoint && shape.endPoint);
86
+ const minX = boxes.length ? Math.min(...boxes.map((box) => box.x)) + x : Math.min(...pointShapes.flatMap((shape) => [shape.startPoint.x, shape.endPoint.x]));
87
+ const minY = boxes.length ? Math.min(...boxes.map((box) => box.y)) + y : Math.min(...pointShapes.flatMap((shape) => [shape.startPoint.y, shape.endPoint.y]));
88
+ const maxX = boxes.length ? Math.max(...boxes.map((box) => box.x + box.width)) + x : Math.max(...pointShapes.flatMap((shape) => [shape.startPoint.x, shape.endPoint.x]));
89
+ const maxY = boxes.length ? Math.max(...boxes.map((box) => box.y + box.height)) + y : Math.max(...pointShapes.flatMap((shape) => [shape.startPoint.y, shape.endPoint.y]));
90
+ shapes.unshift({ type: 'frame', shapeID: frameId, x: minX - 40, y: minY - 40, width: Math.max(80, maxX - minX + 80), height: Math.max(80, maxY - minY + 80), frameName: 'LixScript', fillStyle: 'transparent', fillColor: '#1e1e28' });
91
+ }
92
+ return { shapes, operations: shapes.map((shape) => ({ op: 'add', shape })), sourceShapeCount: parsed.shapes.length };
93
+ }
@@ -0,0 +1,99 @@
1
+ import { validateScene } from './scene.js';
2
+
3
+ function decodeBase64Url(value) {
4
+ const base64 = String(value).replaceAll('-', '+').replaceAll('_', '/');
5
+ const padded = base64 + '='.repeat((4 - base64.length % 4) % 4);
6
+ const binary = atob(padded);
7
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
8
+ }
9
+
10
+ function encodeBase64Url(bytes) {
11
+ let binary = '';
12
+ for (const byte of bytes) binary += String.fromCharCode(byte);
13
+ return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
14
+ }
15
+
16
+ async function importWorkspaceKey(keyValue, usages) {
17
+ const bytes = decodeBase64Url(keyValue);
18
+ if (bytes.byteLength !== 32) throw new Error('The workspace encryption key is not AES-256');
19
+ return crypto.subtle.importKey('raw', bytes, { name: 'AES-GCM', length: 256 }, false, usages);
20
+ }
21
+
22
+ export async function decryptRemoteScene(ciphertext, keyValue) {
23
+ const combined = decodeBase64Url(ciphertext);
24
+ if (combined.byteLength < 28) throw new Error('The encrypted workspace payload is invalid');
25
+ const key = await importWorkspaceKey(keyValue, ['decrypt']);
26
+ const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: combined.slice(0, 12) }, key, combined.slice(12));
27
+ return JSON.parse(new TextDecoder().decode(plaintext));
28
+ }
29
+
30
+ export async function encryptRemoteScene(scene, keyValue) {
31
+ const key = await importWorkspaceKey(keyValue, ['encrypt']);
32
+ const iv = crypto.getRandomValues(new Uint8Array(12));
33
+ const plaintext = new TextEncoder().encode(JSON.stringify(scene));
34
+ const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext));
35
+ const combined = new Uint8Array(iv.byteLength + ciphertext.byteLength);
36
+ combined.set(iv);
37
+ combined.set(ciphertext, iv.byteLength);
38
+ return encodeBase64Url(combined);
39
+ }
40
+
41
+ export class RemoteSceneStore {
42
+ constructor({ baseUrl = 'https://sketch.elixpo.com', workspaceId, token, encryptionKey, fetchImpl = globalThis.fetch } = {}) {
43
+ if (!workspaceId) throw new Error('RemoteSceneStore requires workspaceId');
44
+ if (!token) throw new Error('RemoteSceneStore requires an agent grant token');
45
+ if (!encryptionKey) throw new Error('RemoteSceneStore requires the workspace encryption key');
46
+ if (typeof fetchImpl !== 'function') throw new Error('RemoteSceneStore requires fetch');
47
+ this.url = new URL(`/api/mcp/workspaces/${encodeURIComponent(workspaceId)}`, String(baseUrl).replace(/\/$/, ''));
48
+ this.workspaceId = workspaceId;
49
+ this.token = token;
50
+ this.encryptionKey = encryptionKey;
51
+ this.fetch = fetchImpl;
52
+ this.remoteRevision = null;
53
+ }
54
+
55
+ async read() {
56
+ const response = await this.fetch(this.url, { headers: this.headers(), cache: 'no-store' });
57
+ const body = await readJson(response);
58
+ if (!response.ok) throw remoteError(response, body);
59
+ const scene = await decryptRemoteScene(body.encryptedData, this.encryptionKey);
60
+ const validation = validateScene(scene);
61
+ if (!validation.valid) throw new Error(`Remote workspace is invalid: ${validation.errors.join('; ')}`);
62
+ this.remoteRevision = Number(body.revision || 0);
63
+ scene.mcpRevision = this.remoteRevision;
64
+ return scene;
65
+ }
66
+
67
+ async write(scene) {
68
+ const validation = validateScene(scene);
69
+ if (!validation.valid) throw new Error(`Refusing to store invalid remote scene: ${validation.errors.join('; ')}`);
70
+ if (!Number.isInteger(this.remoteRevision)) throw new Error('Read the remote workspace before writing it');
71
+ const encryptedData = await encryptRemoteScene(scene, this.encryptionKey);
72
+ const response = await this.fetch(this.url, {
73
+ method: 'PUT',
74
+ headers: { ...this.headers(), 'Content-Type': 'application/json' },
75
+ body: JSON.stringify({ encryptedData, expectedRevision: this.remoteRevision, workspaceName: scene.name }),
76
+ });
77
+ const body = await readJson(response);
78
+ if (!response.ok) throw remoteError(response, body);
79
+ this.remoteRevision = Number(body.revision);
80
+ return structuredClone({ ...scene, mcpRevision: this.remoteRevision });
81
+ }
82
+
83
+ headers() {
84
+ return { Accept: 'application/json', Authorization: `Bearer ${this.token}` };
85
+ }
86
+ }
87
+
88
+ async function readJson(response) {
89
+ return response.json().catch(() => ({}));
90
+ }
91
+
92
+ function remoteError(response, body) {
93
+ const error = new Error(body.error === 'REVISION_CONFLICT'
94
+ ? `Revision conflict: expected ${body.expectedRevision}, current ${body.currentRevision}`
95
+ : body.error || `Remote workspace request failed (${response.status})`);
96
+ error.status = response.status;
97
+ error.details = body;
98
+ return error;
99
+ }
package/src/mcp/scene.js CHANGED
@@ -168,6 +168,7 @@ export function applyScenePatch(sceneInput, operations, { expectedRevision, dryR
168
168
  scene.name = String(operation.name || '').trim().slice(0, 72) || scene.name;
169
169
  } else throw new Error(`Unsupported operation "${operation.op}"`);
170
170
  }
171
+ reconcileFrameContainment(scene);
171
172
  scene.mcpRevision = revision + 1;
172
173
  scene.updatedAt = new Date().toISOString();
173
174
  const result = validateScene(scene);
@@ -175,6 +176,17 @@ export function applyScenePatch(sceneInput, operations, { expectedRevision, dryR
175
176
  return { scene, revision: scene.mcpRevision, dryRun: Boolean(dryRun), changedShapeIDs: [...changedIds] };
176
177
  }
177
178
 
179
+ function reconcileFrameContainment(scene) {
180
+ const frames = new Map(scene.shapes.filter((shape) => shape.type === 'frame').map((shape) => [shape.shapeID, shape]));
181
+ for (const frame of frames.values()) frame.containedShapeIDs = [];
182
+ for (const shape of scene.shapes) {
183
+ if (!shape.parentFrame) continue;
184
+ const frame = frames.get(shape.parentFrame);
185
+ if (!frame) throw new Error(`Shape "${shape.shapeID}" references missing frame "${shape.parentFrame}"`);
186
+ frame.containedShapeIDs.push(shape.shapeID);
187
+ }
188
+ }
189
+
178
190
  function applyShapeChanges(shape, changes) {
179
191
  if (!changes || typeof changes !== 'object' || Array.isArray(changes)) throw new Error('Shape changes must be an object');
180
192
  const allowed = {
package/src/mcp/server.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { applyScenePatch, createEmptyScene, getSceneSummary, mergeTemplateScene, validateScene, MCP_LIMITS } from './scene.js';
2
2
  import { MarketplaceTemplateProvider } from './templates.js';
3
3
  import { renderSceneSvg } from './preview.js';
4
+ import { compileLixScript } from './lixscript.js';
4
5
 
5
6
  const SERVER_NAME = 'lixsketch';
6
- const SERVER_VERSION = '1.0.0';
7
+ const SERVER_VERSION = '1.1.0';
7
8
  const PROTOCOL_VERSION = '2025-11-25';
8
9
  const SUPPORTED_PROTOCOL_VERSIONS = new Set([PROTOCOL_VERSION, '2025-06-18', '2024-11-05']);
9
10
 
@@ -53,6 +54,13 @@ export const LIXSKETCH_MCP_TOOLS = Object.freeze([
53
54
  inputSchema: { type: 'object', required: ['confirm'], properties: { name: { type: 'string', maxLength: 72 }, confirm: { const: true } }, additionalProperties: false },
54
55
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
55
56
  },
57
+ {
58
+ name: 'lixscript_apply',
59
+ title: 'Apply LixScript diagram',
60
+ description: 'Compile LixScript into the same validated atomic scene patch used by structured canvas edits. Supports revisions and dry runs.',
61
+ inputSchema: { type: 'object', required: ['source'], properties: { source: { type: 'string', maxLength: 100000 }, x: { type: 'number' }, y: { type: 'number' }, expectedRevision: { type: 'integer', minimum: 0 }, dryRun: { type: 'boolean', default: false } }, additionalProperties: false },
62
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
63
+ },
56
64
  {
57
65
  name: 'templates_search',
58
66
  title: 'Search LixSketch templates',
@@ -130,10 +138,20 @@ export class LixSketchMcpServer {
130
138
  case 'canvas_new':
131
139
  if (args.confirm !== true) throw new Error('canvas_new requires confirm=true');
132
140
  return await this.enqueueMutation(async () => {
141
+ const current = await this.store.read();
133
142
  const scene = createEmptyScene(args.name);
143
+ scene.mcpRevision = Number(current.mcpRevision || 0) + 1;
134
144
  await this.store.write(scene);
135
145
  return toolResult({ summary: getSceneSummary(scene) }, 'Blank canvas created.');
136
146
  });
147
+ case 'lixscript_apply':
148
+ return await this.enqueueMutation(async () => {
149
+ const scene = await this.store.read();
150
+ const compiled = compileLixScript(args.source, args);
151
+ const result = applyScenePatch(scene, compiled.operations, args);
152
+ if (!args.dryRun) await this.store.write(result.scene);
153
+ return toolResult({ revision: result.revision, dryRun: result.dryRun, sourceShapeCount: compiled.sourceShapeCount, createdShapeIDs: result.changedShapeIDs, summary: getSceneSummary(result.scene) }, args.dryRun ? 'LixScript is valid. No changes were saved.' : `LixScript added ${result.changedShapeIDs.length} canvas elements.`);
154
+ });
137
155
  case 'template_insert':
138
156
  return await this.enqueueMutation(async () => {
139
157
  const scene = await this.store.read();
@@ -162,7 +180,7 @@ export class LixSketchMcpServer {
162
180
  if (method === 'initialize') {
163
181
  const requested = request.params?.protocolVersion;
164
182
  const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.has(requested) ? requested : PROTOCOL_VERSION;
165
- return { protocolVersion, capabilities: { tools: { listChanged: false }, resources: { subscribe: false, listChanged: false } }, serverInfo: this.serverInfo, instructions: 'Read canvas_get before mutations. Use expectedRevision and dryRun for safe edits. Prefer template_insert for reusable component packs.' };
183
+ return { protocolVersion, capabilities: { tools: { listChanged: false }, resources: { subscribe: false, listChanged: false } }, serverInfo: this.serverInfo, instructions: 'Before any mutation, call canvas_get and retain its revision. Preserve unrelated shapes. Send expectedRevision and dryRun: true first, inspect canvas_preview for layout changes, then commit the same validated operation with dryRun: false. Never clear or replace a canvas unless the user explicitly requests it. Prefer lixscript_apply for diagrams and template_insert for reusable component packs.' };
166
184
  }
167
185
  if (method === 'ping') return {};
168
186
  if (method === 'tools/list') return { tools: this.listTools() };
package/src/mcp/stdio.js CHANGED
@@ -4,34 +4,40 @@ import { createLixSketchMcpServer } from './server.js';
4
4
  import { FileSceneStore } from './fileStore.js';
5
5
  import { MarketplaceTemplateProvider } from './templates.js';
6
6
  import { serveLixSketchStdio } from './stdioTransport.js';
7
+ import { RemoteSceneStore } from './remoteStore.js';
7
8
 
8
9
  function parseArguments(argv) {
9
- const options = { scene: process.env.LIXSKETCH_SCENE_FILE || './lixsketch-mcp.lixjson', marketplaceUrl: process.env.LIXSKETCH_MARKETPLACE_URL || 'https://sketch.elixpo.com' };
10
+ const options = { scene: process.env.LIXSKETCH_SCENE_FILE || './lixsketch-mcp.lixjson', remote: process.env.LIXSKETCH_REMOTE_URL || '', workspace: process.env.LIXSKETCH_WORKSPACE_ID || '', marketplaceUrl: process.env.LIXSKETCH_MARKETPLACE_URL || 'https://sketch.elixpo.com' };
10
11
  for (let index = 0; index < argv.length; index += 1) {
11
12
  const argument = argv[index];
12
13
  if (argument === '--scene') options.scene = argv[++index];
14
+ else if (argument === '--remote') options.remote = argv[++index];
15
+ else if (argument === '--workspace') options.workspace = argv[++index];
13
16
  else if (argument === '--marketplace-url') options.marketplaceUrl = argv[++index];
14
17
  else if (argument === '--help' || argument === '-h') options.help = true;
15
18
  else throw new Error(`Unknown argument: ${argument}`);
16
19
  }
17
20
  if (!options.scene) throw new Error('--scene requires a file path');
21
+ if (options.remote && !options.workspace) throw new Error('--remote requires --workspace or LIXSKETCH_WORKSPACE_ID');
18
22
  return options;
19
23
  }
20
24
 
21
25
  function printHelp() {
22
- process.stderr.write(`LixSketch MCP server\n\nUsage:\n lixsketch-mcp --scene ./diagram.lixjson\n\nOptions:\n --scene <path> Atomic .lixjson scene file (default: ./lixsketch-mcp.lixjson)\n --marketplace-url <url> Template marketplace origin\n -h, --help Show this help\n\nEnvironment:\n LIXSKETCH_SCENE_FILE\n LIXSKETCH_MARKETPLACE_URL\n`);
26
+ process.stderr.write(`LixSketch MCP server\n\nUsage:\n lixsketch-mcp --scene ./diagram.lixjson\n lixsketch-mcp --remote https://sketch.elixpo.com --workspace lx-...\n\nOptions:\n --scene <path> Atomic local .lixjson scene file\n --remote <origin> Remote LixSketch deployment origin\n --workspace <id> Remote workspace session ID\n --marketplace-url <url> Template marketplace origin\n -h, --help Show this help\n\nRemote environment (never pass secrets as arguments):\n LIXSKETCH_AGENT_TOKEN\n LIXSKETCH_ENCRYPTION_KEY\n\nOther environment:\n LIXSKETCH_SCENE_FILE\n LIXSKETCH_REMOTE_URL\n LIXSKETCH_WORKSPACE_ID\n LIXSKETCH_MARKETPLACE_URL\n`);
23
27
  }
24
28
 
25
29
  async function main() {
26
30
  const options = parseArguments(process.argv.slice(2));
27
31
  if (options.help) { printHelp(); return; }
28
- const store = new FileSceneStore(options.scene);
32
+ const store = options.remote
33
+ ? new RemoteSceneStore({ baseUrl: options.remote, workspaceId: options.workspace, token: process.env.LIXSKETCH_AGENT_TOKEN, encryptionKey: process.env.LIXSKETCH_ENCRYPTION_KEY })
34
+ : new FileSceneStore(options.scene);
29
35
  const server = createLixSketchMcpServer({ store, templateProvider: new MarketplaceTemplateProvider({ baseUrl: options.marketplaceUrl }) });
30
36
  const transport = serveLixSketchStdio(server);
31
37
  const close = () => { void transport.close().finally(() => process.exit(0)); };
32
38
  process.once('SIGINT', close);
33
39
  process.once('SIGTERM', close);
34
- process.stderr.write(`LixSketch MCP ready: ${store.filePath}\n`);
40
+ process.stderr.write(`LixSketch MCP ready: ${store.filePath || `${options.remote}/c/${options.workspace}`}\n`);
35
41
  await transport.closed;
36
42
  }
37
43