@sknoble/slvsx-mcp-server 0.1.1 → 0.2.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/mcp-server.js CHANGED
@@ -23,30 +23,38 @@ import { fileURLToPath } from 'url';
23
23
  const __filename = fileURLToPath(import.meta.url);
24
24
  const __dirname = path.dirname(__filename);
25
25
 
26
- // Find slvsx binary - check env var, PATH, then local build
26
+ // Find slvsx binary - check env var, bundled, PATH, then local build
27
27
  function findSlvsxBinary() {
28
+ const ext = process.platform === 'win32' ? '.exe' : '';
29
+
28
30
  // 1. Check explicit env var
29
31
  if (process.env.SLVSX_BINARY) {
30
32
  return process.env.SLVSX_BINARY;
31
33
  }
32
34
 
33
- // 2. Check if slvsx is in PATH
35
+ // 2. Check bundled binary (installed via postinstall)
36
+ const bundled = path.join(__dirname, 'bin', `slvsx${ext}`);
37
+ if (fs.existsSync(bundled)) {
38
+ return bundled;
39
+ }
40
+
41
+ // 3. Check if slvsx is in PATH
34
42
  try {
35
43
  const which = execSync('which slvsx 2>/dev/null || where slvsx 2>nul', { encoding: 'utf-8' }).trim();
36
- if (which) return which.split('\n')[0];
44
+ // Handle Windows CRLF line endings by trimming each line
45
+ if (which) return which.split('\n')[0].trim();
37
46
  } catch (e) {
38
47
  // Not in PATH
39
48
  }
40
49
 
41
- // 3. Check local build
42
- const localBuild = './target/release/slvsx';
50
+ // 4. Check local build
51
+ const localBuild = `./target/release/slvsx${ext}`;
43
52
  if (fs.existsSync(localBuild)) {
44
53
  return localBuild;
45
54
  }
46
55
 
47
- // 4. Check relative to this script (for development)
48
- const scriptDir = path.dirname(__filename);
49
- const devBuild = path.join(scriptDir, 'target/release/slvsx');
56
+ // 5. Check relative to this script (for development)
57
+ const devBuild = path.join(__dirname, 'target/release', `slvsx${ext}`);
50
58
  if (fs.existsSync(devBuild)) {
51
59
  return devBuild;
52
60
  }
@@ -433,49 +441,49 @@ class SlvsxServer {
433
441
  schema: 'slvs-json/1',
434
442
  units: 'mm',
435
443
  entities: [
436
- { id: 'p1', type: 'Point', x: 0, y: 0 },
437
- { id: 'p2', type: 'Point', x: 100, y: 0 },
438
- { id: 'p3', type: 'Point', x: 50, y: 86.6 }
444
+ { type: 'point', id: 'p1', at: [0, 0, 0] },
445
+ { type: 'point', id: 'p2', at: [100, 0, 0] },
446
+ { type: 'point', id: 'p3', at: [50, 86.6, 0] }
439
447
  ],
440
448
  constraints: [
441
- { type: 'Fixed', entity: 'p1' },
442
- { type: 'Distance', entities: ['p1', 'p2'], distance: 100 },
443
- { type: 'Distance', entities: ['p2', 'p3'], distance: 100 },
444
- { type: 'Distance', entities: ['p3', 'p1'], distance: 100 }
449
+ { type: 'fixed', entity: 'p1' },
450
+ { type: 'distance', between: ['p1', 'p2'], value: 100 },
451
+ { type: 'distance', between: ['p2', 'p3'], value: 100 },
452
+ { type: 'distance', between: ['p3', 'p1'], value: 100 }
445
453
  ]
446
454
  },
447
455
  square: {
448
456
  schema: 'slvs-json/1',
449
457
  units: 'mm',
450
458
  entities: [
451
- { id: 'p1', type: 'Point', x: 0, y: 0 },
452
- { id: 'p2', type: 'Point', x: 100, y: 0 },
453
- { id: 'p3', type: 'Point', x: 100, y: 100 },
454
- { id: 'p4', type: 'Point', x: 0, y: 100 },
455
- { id: 'l1', type: 'Line', points: ['p1', 'p2'] },
456
- { id: 'l2', type: 'Line', points: ['p2', 'p3'] },
457
- { id: 'l3', type: 'Line', points: ['p3', 'p4'] },
458
- { id: 'l4', type: 'Line', points: ['p4', 'p1'] }
459
+ { type: 'point', id: 'p1', at: [0, 0, 0] },
460
+ { type: 'point', id: 'p2', at: [100, 0, 0] },
461
+ { type: 'point', id: 'p3', at: [100, 100, 0] },
462
+ { type: 'point', id: 'p4', at: [0, 100, 0] },
463
+ { type: 'line', id: 'l1', p1: 'p1', p2: 'p2' },
464
+ { type: 'line', id: 'l2', p1: 'p2', p2: 'p3' },
465
+ { type: 'line', id: 'l3', p1: 'p3', p2: 'p4' },
466
+ { type: 'line', id: 'l4', p1: 'p4', p2: 'p1' }
459
467
  ],
460
468
  constraints: [
461
- { type: 'Fixed', entity: 'p1' },
462
- { type: 'Fixed', entity: 'p2' },
463
- { type: 'Perpendicular', entities: ['l1', 'l2'] },
464
- { type: 'Perpendicular', entities: ['l2', 'l3'] },
465
- { type: 'Perpendicular', entities: ['l3', 'l4'] },
466
- { type: 'Equal', entities: ['l1', 'l2'] }
469
+ { type: 'fixed', entity: 'p1' },
470
+ { type: 'fixed', entity: 'p2' },
471
+ { type: 'perpendicular', a: 'l1', b: 'l2' },
472
+ { type: 'perpendicular', a: 'l2', b: 'l3' },
473
+ { type: 'perpendicular', a: 'l3', b: 'l4' },
474
+ { type: 'equal_length', entities: ['l1', 'l2'] }
467
475
  ]
468
476
  },
469
477
  circle: {
470
478
  schema: 'slvs-json/1',
471
479
  units: 'mm',
472
480
  entities: [
473
- { id: 'center', type: 'Point', x: 50, y: 50 },
474
- { id: 'c1', type: 'Circle', center: 'center', radius: 30 }
481
+ { type: 'point', id: 'center', at: [50, 50, 0] },
482
+ { type: 'circle', id: 'c1', center: [50, 50, 0], diameter: 60 }
475
483
  ],
476
484
  constraints: [
477
- { type: 'Fixed', entity: 'center' },
478
- { type: 'Radius', entity: 'c1', radius: 30 }
485
+ { type: 'fixed', entity: 'center' },
486
+ { type: 'diameter', circle: 'c1', value: 60 }
479
487
  ]
480
488
  },
481
489
  linkage: {
@@ -485,21 +493,20 @@ class SlvsxServer {
485
493
  input_angle: 45
486
494
  },
487
495
  entities: [
488
- { id: 'ground1', type: 'Point', x: 0, y: 0 },
489
- { id: 'ground2', type: 'Point', x: 100, y: 0 },
490
- { id: 'joint1', type: 'Point', x: 30, y: 30 },
491
- { id: 'joint2', type: 'Point', x: 70, y: 40 },
492
- { id: 'link1', type: 'Line', points: ['ground1', 'joint1'] },
493
- { id: 'link2', type: 'Line', points: ['joint1', 'joint2'] },
494
- { id: 'link3', type: 'Line', points: ['joint2', 'ground2'] }
496
+ { type: 'point', id: 'ground1', at: [0, 0, 0] },
497
+ { type: 'point', id: 'ground2', at: [100, 0, 0] },
498
+ { type: 'point', id: 'joint1', at: [30, 30, 0] },
499
+ { type: 'point', id: 'joint2', at: [70, 40, 0] },
500
+ { type: 'line', id: 'link1', p1: 'ground1', p2: 'joint1' },
501
+ { type: 'line', id: 'link2', p1: 'joint1', p2: 'joint2' },
502
+ { type: 'line', id: 'link3', p1: 'joint2', p2: 'ground2' }
495
503
  ],
496
504
  constraints: [
497
- { type: 'Fixed', entity: 'ground1' },
498
- { type: 'Fixed', entity: 'ground2' },
499
- { type: 'Distance', entities: ['ground1', 'joint1'], distance: 40 },
500
- { type: 'Distance', entities: ['joint1', 'joint2'], distance: 50 },
501
- { type: 'Distance', entities: ['joint2', 'ground2'], distance: 35 },
502
- { type: 'Angle', entities: ['link1'], angle: '$input_angle' }
505
+ { type: 'fixed', entity: 'ground1' },
506
+ { type: 'fixed', entity: 'ground2' },
507
+ { type: 'distance', between: ['ground1', 'joint1'], value: 40 },
508
+ { type: 'distance', between: ['joint1', 'joint2'], value: 50 },
509
+ { type: 'distance', between: ['joint2', 'ground2'], value: 35 }
503
510
  ]
504
511
  },
505
512
  parametric: {
@@ -508,48 +515,61 @@ class SlvsxServer {
508
515
  parameters: {
509
516
  width: 150,
510
517
  height: 100,
511
- hole_radius: 10
518
+ hole_diameter: 20
512
519
  },
513
520
  entities: [
514
- { id: 'p1', type: 'Point', x: 0, y: 0 },
515
- { id: 'p2', type: 'Point', x: '$width', y: 0 },
516
- { id: 'p3', type: 'Point', x: '$width', y: '$height' },
517
- { id: 'p4', type: 'Point', x: 0, y: '$height' },
518
- { id: 'hole_center', type: 'Point', x: 75, y: 50 },
519
- { id: 'hole', type: 'Circle', center: 'hole_center', radius: '$hole_radius' }
521
+ { type: 'point', id: 'p1', at: [0, 0, 0] },
522
+ { type: 'point', id: 'p2', at: ['$width', 0, 0] },
523
+ { type: 'point', id: 'p3', at: ['$width', '$height', 0] },
524
+ { type: 'point', id: 'p4', at: [0, '$height', 0] },
525
+ { type: 'line', id: 'bottom', p1: 'p1', p2: 'p2' },
526
+ { type: 'line', id: 'right', p1: 'p2', p2: 'p3' },
527
+ { type: 'line', id: 'top', p1: 'p3', p2: 'p4' },
528
+ { type: 'line', id: 'left', p1: 'p4', p2: 'p1' },
529
+ { type: 'circle', id: 'hole', center: [75, 50, 0], diameter: '$hole_diameter' }
520
530
  ],
521
531
  constraints: [
522
- { type: 'Fixed', entity: 'p1' },
523
- { type: 'HorizontalDistance', entities: ['p1', 'p2'], distance: '$width' },
524
- { type: 'VerticalDistance', entities: ['p1', 'p4'], distance: '$height' },
525
- { type: 'Horizontal', entity: 'p2' },
526
- { type: 'Vertical', entity: 'p4' },
527
- { type: 'Radius', entity: 'hole', radius: '$hole_radius' }
532
+ { type: 'fixed', entity: 'p1' },
533
+ { type: 'distance', between: ['p1', 'p2'], value: '$width' },
534
+ { type: 'distance', between: ['p1', 'p4'], value: '$height' },
535
+ { type: 'perpendicular', a: 'bottom', b: 'right' },
536
+ { type: 'perpendicular', a: 'right', b: 'top' },
537
+ { type: 'diameter', circle: 'hole', value: '$hole_diameter' }
528
538
  ]
529
539
  },
530
540
  '3d': {
531
541
  schema: 'slvs-json/1',
532
542
  units: 'mm',
533
543
  entities: [
534
- { id: 'p1', type: 'Point', x: 0, y: 0, z: 0 },
535
- { id: 'p2', type: 'Point', x: 100, y: 0, z: 0 },
536
- { id: 'p3', type: 'Point', x: 100, y: 100, z: 0 },
537
- { id: 'p4', type: 'Point', x: 0, y: 100, z: 0 },
538
- { id: 'p5', type: 'Point', x: 0, y: 0, z: 50 },
539
- { id: 'p6', type: 'Point', x: 100, y: 0, z: 50 },
540
- { id: 'p7', type: 'Point', x: 100, y: 100, z: 50 },
541
- { id: 'p8', type: 'Point', x: 0, y: 100, z: 50 }
544
+ { type: 'point', id: 'p1', at: [0, 0, 0] },
545
+ { type: 'point', id: 'p2', at: [100, 0, 0] },
546
+ { type: 'point', id: 'p3', at: [100, 100, 0] },
547
+ { type: 'point', id: 'p4', at: [0, 100, 0] },
548
+ { type: 'point', id: 'p5', at: [0, 0, 50] },
549
+ { type: 'point', id: 'p6', at: [100, 0, 50] },
550
+ { type: 'point', id: 'p7', at: [100, 100, 50] },
551
+ { type: 'point', id: 'p8', at: [0, 100, 50] },
552
+ { type: 'line', id: 'base1', p1: 'p1', p2: 'p2' },
553
+ { type: 'line', id: 'base2', p1: 'p2', p2: 'p3' },
554
+ { type: 'line', id: 'base3', p1: 'p3', p2: 'p4' },
555
+ { type: 'line', id: 'base4', p1: 'p4', p2: 'p1' },
556
+ { type: 'line', id: 'top1', p1: 'p5', p2: 'p6' },
557
+ { type: 'line', id: 'top2', p1: 'p6', p2: 'p7' },
558
+ { type: 'line', id: 'top3', p1: 'p7', p2: 'p8' },
559
+ { type: 'line', id: 'top4', p1: 'p8', p2: 'p5' },
560
+ { type: 'line', id: 'vert1', p1: 'p1', p2: 'p5' },
561
+ { type: 'line', id: 'vert2', p1: 'p2', p2: 'p6' },
562
+ { type: 'line', id: 'vert3', p1: 'p3', p2: 'p7' },
563
+ { type: 'line', id: 'vert4', p1: 'p4', p2: 'p8' }
542
564
  ],
543
565
  constraints: [
544
- { type: 'Fixed', entity: 'p1' },
545
- { type: 'Distance', entities: ['p1', 'p2'], distance: 100 },
546
- { type: 'Distance', entities: ['p2', 'p3'], distance: 100 },
547
- { type: 'Distance', entities: ['p3', 'p4'], distance: 100 },
548
- { type: 'Distance', entities: ['p4', 'p1'], distance: 100 },
549
- { type: 'Distance', entities: ['p1', 'p5'], distance: 50 },
550
- { type: 'Distance', entities: ['p2', 'p6'], distance: 50 },
551
- { type: 'Distance', entities: ['p3', 'p7'], distance: 50 },
552
- { type: 'Distance', entities: ['p4', 'p8'], distance: 50 }
566
+ { type: 'fixed', entity: 'p1' },
567
+ { type: 'fixed', entity: 'p2' },
568
+ { type: 'fixed', entity: 'p4' },
569
+ { type: 'distance', between: ['p1', 'p5'], value: 50 },
570
+ { type: 'distance', between: ['p2', 'p6'], value: 50 },
571
+ { type: 'distance', between: ['p3', 'p7'], value: 50 },
572
+ { type: 'distance', between: ['p4', 'p8'], value: 50 }
553
573
  ]
554
574
  }
555
575
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sknoble/slvsx-mcp-server",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for SLVSX geometric constraint solver",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,8 @@
10
10
  "scripts": {
11
11
  "start": "node mcp-server.js",
12
12
  "build:docs": "npx tsx scripts/build-docs.ts",
13
- "test": "node tests/mcp-server.test.js"
13
+ "test": "node tests/mcp-server.test.js",
14
+ "postinstall": "node scripts/postinstall.js"
14
15
  },
15
16
  "dependencies": {
16
17
  "@langchain/textsplitters": "^0.1.0",
@@ -38,6 +39,7 @@
38
39
  "files": [
39
40
  "mcp-server.js",
40
41
  "dist/docs.json",
42
+ "scripts/postinstall.js",
41
43
  "README.md"
42
44
  ],
43
45
  "engines": {
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Postinstall script that downloads the slvsx binary for the current platform
5
+ * from GitHub releases.
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import https from 'https';
12
+ import { createWriteStream } from 'fs';
13
+ import { exec } from 'child_process';
14
+ import { promisify } from 'util';
15
+
16
+ const execAsync = promisify(exec);
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = path.dirname(__filename);
20
+ const packageRoot = path.resolve(__dirname, '..');
21
+
22
+ // Map Node.js platform/arch to our release asset naming
23
+ function getPlatformInfo() {
24
+ const platform = process.platform;
25
+ const arch = process.arch;
26
+
27
+ // Map to actual release asset names like: slvsx-macos-arm64.tar.gz, slvsx-linux.tar.gz
28
+ if (platform === 'darwin') {
29
+ if (arch === 'arm64') {
30
+ return { assetPattern: 'macos-arm64', extension: '' };
31
+ } else if (arch === 'x64') {
32
+ return { assetPattern: 'macos-x86_64', extension: '' };
33
+ }
34
+ } else if (platform === 'linux') {
35
+ // Linux release is just "linux" (x86_64)
36
+ if (arch === 'x64') {
37
+ return { assetPattern: 'linux', extension: '' };
38
+ }
39
+ } else if (platform === 'win32') {
40
+ if (arch === 'x64') {
41
+ return { assetPattern: 'windows', extension: '.exe' };
42
+ }
43
+ }
44
+
45
+ return null;
46
+ }
47
+
48
+ async function getLatestRelease() {
49
+ return new Promise((resolve, reject) => {
50
+ const options = {
51
+ hostname: 'api.github.com',
52
+ path: '/repos/snoble/slvsx-cli/releases/latest',
53
+ headers: {
54
+ 'User-Agent': 'slvsx-mcp-server-installer'
55
+ }
56
+ };
57
+
58
+ https.get(options, (res) => {
59
+ let data = '';
60
+ res.on('data', chunk => data += chunk);
61
+ res.on('end', () => {
62
+ try {
63
+ resolve(JSON.parse(data));
64
+ } catch (e) {
65
+ reject(new Error('Failed to parse release info'));
66
+ }
67
+ });
68
+ }).on('error', reject);
69
+ });
70
+ }
71
+
72
+ async function downloadFile(url, dest) {
73
+ return new Promise((resolve, reject) => {
74
+ const follow = (url) => {
75
+ https.get(url, (res) => {
76
+ if (res.statusCode === 302 || res.statusCode === 301) {
77
+ // Consume the redirect response to release the connection
78
+ res.resume();
79
+ follow(res.headers.location);
80
+ return;
81
+ }
82
+
83
+ if (res.statusCode !== 200) {
84
+ res.resume(); // Consume response body
85
+ reject(new Error(`Download failed with status ${res.statusCode}`));
86
+ return;
87
+ }
88
+
89
+ const file = createWriteStream(dest);
90
+ // Handle file stream errors to prevent hanging
91
+ file.on('error', (err) => {
92
+ file.close();
93
+ fs.unlink(dest, () => {}); // Clean up partial file
94
+ reject(err);
95
+ });
96
+ res.pipe(file);
97
+ file.on('finish', () => {
98
+ file.close();
99
+ resolve();
100
+ });
101
+ }).on('error', reject);
102
+ };
103
+
104
+ follow(url);
105
+ });
106
+ }
107
+
108
+ async function extractTarGz(file, dest) {
109
+ await execAsync(`tar -xzf "${file}" -C "${dest}"`);
110
+ }
111
+
112
+ async function extractZip(file, dest) {
113
+ if (process.platform === 'win32') {
114
+ await execAsync(`powershell -Command "Expand-Archive -Path '${file}' -DestinationPath '${dest}'"`);
115
+ } else {
116
+ await execAsync(`unzip -o "${file}" -d "${dest}"`);
117
+ }
118
+ }
119
+
120
+ async function main() {
121
+ const binDir = path.join(packageRoot, 'bin');
122
+ const binaryPath = path.join(binDir, process.platform === 'win32' ? 'slvsx.exe' : 'slvsx');
123
+
124
+ // Skip if binary already exists
125
+ if (fs.existsSync(binaryPath)) {
126
+ console.log('slvsx binary already installed.');
127
+ return;
128
+ }
129
+
130
+ const platformInfo = getPlatformInfo();
131
+ if (!platformInfo) {
132
+ console.error(`Unsupported platform: ${process.platform}-${process.arch}`);
133
+ console.error('Please install slvsx manually from https://github.com/snoble/slvsx-cli/releases');
134
+ process.exit(0); // Don't fail install, just warn
135
+ }
136
+
137
+ console.log(`Installing slvsx for ${process.platform}-${process.arch}...`);
138
+
139
+ try {
140
+ const release = await getLatestRelease();
141
+
142
+ if (!release.assets) {
143
+ console.error('No release assets found. Please install slvsx manually.');
144
+ process.exit(0);
145
+ }
146
+
147
+ // Find the right asset
148
+ const asset = release.assets.find(a =>
149
+ a.name.includes(platformInfo.assetPattern) &&
150
+ (a.name.endsWith('.tar.gz') || a.name.endsWith('.zip'))
151
+ );
152
+
153
+ if (!asset) {
154
+ console.error(`No binary found for ${process.platform}-${process.arch} (looking for '${platformInfo.assetPattern}')`);
155
+ console.error('Available assets:', release.assets.map(a => a.name).join(', '));
156
+ console.error('Please install slvsx manually from https://github.com/snoble/slvsx-cli/releases');
157
+ process.exit(0);
158
+ }
159
+
160
+ // Create bin directory
161
+ fs.mkdirSync(binDir, { recursive: true });
162
+
163
+ // Download
164
+ const tempFile = path.join(binDir, asset.name);
165
+ console.log(`Downloading ${asset.name}...`);
166
+ await downloadFile(asset.browser_download_url, tempFile);
167
+
168
+ // Extract
169
+ console.log('Extracting...');
170
+ if (asset.name.endsWith('.tar.gz')) {
171
+ await extractTarGz(tempFile, binDir);
172
+ } else {
173
+ await extractZip(tempFile, binDir);
174
+ }
175
+
176
+ // Clean up archive
177
+ fs.unlinkSync(tempFile);
178
+
179
+ // Make executable on Unix
180
+ if (process.platform !== 'win32') {
181
+ fs.chmodSync(binaryPath, 0o755);
182
+ }
183
+
184
+ // Verify
185
+ if (fs.existsSync(binaryPath)) {
186
+ console.log(`✓ slvsx installed successfully to ${binaryPath}`);
187
+ } else {
188
+ // Binary might be in a subdirectory after extraction
189
+ const files = fs.readdirSync(binDir);
190
+ console.log('Extracted files:', files);
191
+ console.error('Binary not found at expected location. Please check extraction.');
192
+ }
193
+
194
+ } catch (error) {
195
+ console.error('Failed to download slvsx:', error.message);
196
+ console.error('Please install manually from https://github.com/snoble/slvsx-cli/releases');
197
+ process.exit(0); // Don't fail the npm install
198
+ }
199
+ }
200
+
201
+ main();
202
+