@elixpo/lixsketch 5.6.1 → 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 +110 -6
- package/dist/mcp/index.js +1044 -0
- package/dist/mcp/index.js.map +7 -0
- package/dist/mcp/node.js +184 -0
- package/dist/mcp/node.js.map +7 -0
- package/dist/mcp/stdio.js +1190 -0
- package/dist/mcp/stdio.js.map +7 -0
- package/package.json +7 -1
- package/src/mcp/fileStore.js +70 -0
- package/src/mcp/index.js +7 -0
- package/src/mcp/lixscript.js +93 -0
- package/src/mcp/node.js +2 -0
- package/src/mcp/preview.js +39 -0
- package/src/mcp/remoteStore.js +99 -0
- package/src/mcp/scene.js +287 -0
- package/src/mcp/server.js +213 -0
- package/src/mcp/stdio.js +47 -0
- package/src/mcp/stdioTransport.js +46 -0
- package/src/mcp/store.js +23 -0
- package/src/mcp/templates.js +51 -0
package/src/mcp/stdio.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createLixSketchMcpServer } from './server.js';
|
|
4
|
+
import { FileSceneStore } from './fileStore.js';
|
|
5
|
+
import { MarketplaceTemplateProvider } from './templates.js';
|
|
6
|
+
import { serveLixSketchStdio } from './stdioTransport.js';
|
|
7
|
+
import { RemoteSceneStore } from './remoteStore.js';
|
|
8
|
+
|
|
9
|
+
function parseArguments(argv) {
|
|
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' };
|
|
11
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
12
|
+
const argument = argv[index];
|
|
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];
|
|
16
|
+
else if (argument === '--marketplace-url') options.marketplaceUrl = argv[++index];
|
|
17
|
+
else if (argument === '--help' || argument === '-h') options.help = true;
|
|
18
|
+
else throw new Error(`Unknown argument: ${argument}`);
|
|
19
|
+
}
|
|
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');
|
|
22
|
+
return options;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function printHelp() {
|
|
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`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
const options = parseArguments(process.argv.slice(2));
|
|
31
|
+
if (options.help) { printHelp(); return; }
|
|
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);
|
|
35
|
+
const server = createLixSketchMcpServer({ store, templateProvider: new MarketplaceTemplateProvider({ baseUrl: options.marketplaceUrl }) });
|
|
36
|
+
const transport = serveLixSketchStdio(server);
|
|
37
|
+
const close = () => { void transport.close().finally(() => process.exit(0)); };
|
|
38
|
+
process.once('SIGINT', close);
|
|
39
|
+
process.once('SIGTERM', close);
|
|
40
|
+
process.stderr.write(`LixSketch MCP ready: ${store.filePath || `${options.remote}/c/${options.workspace}`}\n`);
|
|
41
|
+
await transport.closed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
main().catch((error) => {
|
|
45
|
+
process.stderr.write(`LixSketch MCP failed: ${error?.message || error}\n`);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
|
|
3
|
+
const MAX_MESSAGE_BYTES = 10 * 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
export function serveLixSketchStdio(server, { input = process.stdin, output = process.stdout } = {}) {
|
|
6
|
+
if (!server?.handleRequest) throw new Error('A LixSketch MCP server is required');
|
|
7
|
+
const lines = createInterface({ input, crlfDelay: Infinity, terminal: false });
|
|
8
|
+
let processing = Promise.resolve();
|
|
9
|
+
let resolveClosed;
|
|
10
|
+
const closed = new Promise((resolve) => { resolveClosed = resolve; });
|
|
11
|
+
const send = (message) => output.write(`${JSON.stringify(message)}\n`);
|
|
12
|
+
|
|
13
|
+
lines.on('line', (line) => {
|
|
14
|
+
processing = processing.then(async () => {
|
|
15
|
+
if (!line.trim()) return;
|
|
16
|
+
if (Buffer.byteLength(line, 'utf8') > MAX_MESSAGE_BYTES) {
|
|
17
|
+
send({ jsonrpc: '2.0', id: null, error: { code: -32600, message: 'MCP request exceeds the 10 MB limit' } });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
let request;
|
|
21
|
+
try { request = JSON.parse(line); }
|
|
22
|
+
catch { send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }); return; }
|
|
23
|
+
if (request.jsonrpc !== '2.0' || typeof request.method !== 'string') {
|
|
24
|
+
if (request.id !== undefined) send({ jsonrpc: '2.0', id: request.id ?? null, error: { code: -32600, message: 'Invalid Request' } });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const result = await server.handleRequest(request);
|
|
29
|
+
if (request.id !== undefined && result !== undefined) send({ jsonrpc: '2.0', id: request.id, result });
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (request.id !== undefined) send({ jsonrpc: '2.0', id: request.id, error: { code: Number(error?.code) || -32603, message: error?.message || 'Internal error' } });
|
|
32
|
+
}
|
|
33
|
+
}).catch((error) => {
|
|
34
|
+
process.stderr.write(`LixSketch MCP transport error: ${error?.message || error}\n`);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
lines.once('close', () => processing.finally(resolveClosed));
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
closed,
|
|
41
|
+
close: async () => {
|
|
42
|
+
lines.close();
|
|
43
|
+
await processing;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/mcp/store.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createEmptyScene, validateScene } from './scene.js';
|
|
2
|
+
|
|
3
|
+
export class MemorySceneStore {
|
|
4
|
+
#scene;
|
|
5
|
+
|
|
6
|
+
constructor(scene = createEmptyScene()) {
|
|
7
|
+
const validation = validateScene(scene);
|
|
8
|
+
if (!validation.valid) throw new Error(`Invalid initial scene: ${validation.errors.join('; ')}`);
|
|
9
|
+
this.#scene = structuredClone(scene);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async read() {
|
|
13
|
+
return structuredClone(this.#scene);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async write(scene) {
|
|
17
|
+
const validation = validateScene(scene);
|
|
18
|
+
if (!validation.valid) throw new Error(`Refusing to store invalid scene: ${validation.errors.join('; ')}`);
|
|
19
|
+
this.#scene = structuredClone(scene);
|
|
20
|
+
return this.read();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const DEFAULT_MARKETPLACE_URL = 'https://sketch.elixpo.com';
|
|
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
|
+
export async function decryptPublicTemplate(ciphertext, keyValue) {
|
|
11
|
+
const keyBytes = decodeBase64Url(keyValue);
|
|
12
|
+
if (keyBytes.byteLength !== 32) throw new Error('Template key is not AES-256');
|
|
13
|
+
const combined = decodeBase64Url(ciphertext);
|
|
14
|
+
if (combined.byteLength < 28) throw new Error('Template ciphertext is invalid');
|
|
15
|
+
const key = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM', length: 256 }, false, ['decrypt']);
|
|
16
|
+
const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: combined.slice(0, 12) }, key, combined.slice(12));
|
|
17
|
+
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class MarketplaceTemplateProvider {
|
|
21
|
+
constructor({ baseUrl = DEFAULT_MARKETPLACE_URL, fetchImpl = globalThis.fetch } = {}) {
|
|
22
|
+
if (typeof fetchImpl !== 'function') throw new Error('MarketplaceTemplateProvider requires fetch');
|
|
23
|
+
this.baseUrl = String(baseUrl).replace(/\/$/, '');
|
|
24
|
+
this.fetch = fetchImpl;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async search({ query = '', tag = '', limit = 12 } = {}) {
|
|
28
|
+
const url = new URL('/api/templates', this.baseUrl);
|
|
29
|
+
if (query) url.searchParams.set('q', String(query).slice(0, 80));
|
|
30
|
+
if (tag) url.searchParams.set('tag', String(tag).slice(0, 24));
|
|
31
|
+
url.searchParams.set('limit', String(Math.min(24, Math.max(1, Number(limit) || 12))));
|
|
32
|
+
const response = await this.fetch(url, { headers: { accept: 'application/json' } });
|
|
33
|
+
const body = await response.json();
|
|
34
|
+
if (!response.ok) throw new Error(body.error || `Marketplace request failed (${response.status})`);
|
|
35
|
+
return body.templates || [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async load(slug) {
|
|
39
|
+
const safeSlug = String(slug || '').trim();
|
|
40
|
+
if (!/^[a-z0-9-]{1,80}$/.test(safeSlug)) throw new Error('Template slug is invalid');
|
|
41
|
+
const url = new URL(`/api/templates/${encodeURIComponent(safeSlug)}`, this.baseUrl);
|
|
42
|
+
url.searchParams.set('snapshot', '1');
|
|
43
|
+
const response = await this.fetch(url, { headers: { accept: 'application/json' } });
|
|
44
|
+
const body = await response.json();
|
|
45
|
+
if (!response.ok) throw new Error(body.error || `Template request failed (${response.status})`);
|
|
46
|
+
const template = body.template;
|
|
47
|
+
if (!template?.encryptedData || !template?.publicKey) throw new Error('Template snapshot is unavailable');
|
|
48
|
+
return { metadata: { ...template, encryptedData: undefined, publicKey: undefined, encryptedDocData: undefined }, scene: await decryptPublicTemplate(template.encryptedData, template.publicKey) };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|