@elixpo/lixsketch 5.6.2 → 5.6.3
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 +25 -0
- package/dist/mcp/index.js +466 -7
- package/dist/mcp/index.js.map +4 -4
- package/dist/mcp/node.js.map +2 -2
- package/dist/mcp/stdio.js +487 -20
- package/dist/mcp/stdio.js.map +4 -4
- package/package.json +1 -1
- package/src/mcp/index.js +2 -0
- package/src/mcp/lixscript.js +93 -0
- package/src/mcp/remoteStore.js +99 -0
- package/src/mcp/scene.js +12 -0
- package/src/mcp/server.js +19 -1
- package/src/mcp/stdio.js +10 -4
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.
|
|
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();
|
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
|
|
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 =
|
|
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
|
|