@sknoble/slvsx-mcp-server 0.1.0 → 0.1.2
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 +56 -6
- package/package.json +4 -2
- package/scripts/postinstall.js +193 -0
package/mcp-server.js
CHANGED
|
@@ -23,8 +23,45 @@ import { fileURLToPath } from 'url';
|
|
|
23
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
24
|
const __dirname = path.dirname(__filename);
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
|
|
26
|
+
// Find slvsx binary - check env var, bundled, PATH, then local build
|
|
27
|
+
function findSlvsxBinary() {
|
|
28
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
29
|
+
|
|
30
|
+
// 1. Check explicit env var
|
|
31
|
+
if (process.env.SLVSX_BINARY) {
|
|
32
|
+
return process.env.SLVSX_BINARY;
|
|
33
|
+
}
|
|
34
|
+
|
|
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
|
|
42
|
+
try {
|
|
43
|
+
const which = execSync('which slvsx 2>/dev/null || where slvsx 2>nul', { encoding: 'utf-8' }).trim();
|
|
44
|
+
if (which) return which.split('\n')[0];
|
|
45
|
+
} catch (e) {
|
|
46
|
+
// Not in PATH
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 4. Check local build
|
|
50
|
+
const localBuild = `./target/release/slvsx${ext}`;
|
|
51
|
+
if (fs.existsSync(localBuild)) {
|
|
52
|
+
return localBuild;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 5. Check relative to this script (for development)
|
|
56
|
+
const devBuild = path.join(__dirname, 'target/release', `slvsx${ext}`);
|
|
57
|
+
if (fs.existsSync(devBuild)) {
|
|
58
|
+
return devBuild;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const SLVSX_BINARY = findSlvsxBinary();
|
|
28
65
|
|
|
29
66
|
// Load documentation embeddings if available
|
|
30
67
|
let docsIndex = null;
|
|
@@ -586,10 +623,23 @@ class SlvsxServer {
|
|
|
586
623
|
}
|
|
587
624
|
|
|
588
625
|
// Check if slvsx binary exists
|
|
589
|
-
if (!fs.existsSync(SLVSX_BINARY)) {
|
|
590
|
-
console.error(
|
|
591
|
-
console.error('
|
|
592
|
-
console.error('
|
|
626
|
+
if (!SLVSX_BINARY || !fs.existsSync(SLVSX_BINARY)) {
|
|
627
|
+
console.error('Error: SLVSX binary not found.');
|
|
628
|
+
console.error('');
|
|
629
|
+
console.error('The MCP server requires the slvsx CLI binary. Install it via one of:');
|
|
630
|
+
console.error('');
|
|
631
|
+
console.error(' Option 1: Install via Homebrew (macOS/Linux)');
|
|
632
|
+
console.error(' brew install sknoble/tap/slvsx');
|
|
633
|
+
console.error('');
|
|
634
|
+
console.error(' Option 2: Build from source');
|
|
635
|
+
console.error(' git clone https://github.com/snoble/slvsx-cli');
|
|
636
|
+
console.error(' cd slvsx-cli && cargo build --release');
|
|
637
|
+
console.error(' export SLVSX_BINARY=$PWD/target/release/slvsx');
|
|
638
|
+
console.error('');
|
|
639
|
+
console.error(' Option 3: Download binary from GitHub releases');
|
|
640
|
+
console.error(' https://github.com/snoble/slvsx-cli/releases');
|
|
641
|
+
console.error('');
|
|
642
|
+
console.error('Then restart the MCP server.');
|
|
593
643
|
process.exit(1);
|
|
594
644
|
}
|
|
595
645
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sknoble/slvsx-mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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,193 @@
|
|
|
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
|
+
follow(res.headers.location);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (res.statusCode !== 200) {
|
|
82
|
+
reject(new Error(`Download failed with status ${res.statusCode}`));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const file = createWriteStream(dest);
|
|
87
|
+
res.pipe(file);
|
|
88
|
+
file.on('finish', () => {
|
|
89
|
+
file.close();
|
|
90
|
+
resolve();
|
|
91
|
+
});
|
|
92
|
+
}).on('error', reject);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
follow(url);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function extractTarGz(file, dest) {
|
|
100
|
+
await execAsync(`tar -xzf "${file}" -C "${dest}"`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function extractZip(file, dest) {
|
|
104
|
+
if (process.platform === 'win32') {
|
|
105
|
+
await execAsync(`powershell -Command "Expand-Archive -Path '${file}' -DestinationPath '${dest}'"`);
|
|
106
|
+
} else {
|
|
107
|
+
await execAsync(`unzip -o "${file}" -d "${dest}"`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function main() {
|
|
112
|
+
const binDir = path.join(packageRoot, 'bin');
|
|
113
|
+
const binaryPath = path.join(binDir, process.platform === 'win32' ? 'slvsx.exe' : 'slvsx');
|
|
114
|
+
|
|
115
|
+
// Skip if binary already exists
|
|
116
|
+
if (fs.existsSync(binaryPath)) {
|
|
117
|
+
console.log('slvsx binary already installed.');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const platformInfo = getPlatformInfo();
|
|
122
|
+
if (!platformInfo) {
|
|
123
|
+
console.error(`Unsupported platform: ${process.platform}-${process.arch}`);
|
|
124
|
+
console.error('Please install slvsx manually from https://github.com/snoble/slvsx-cli/releases');
|
|
125
|
+
process.exit(0); // Don't fail install, just warn
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(`Installing slvsx for ${process.platform}-${process.arch}...`);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const release = await getLatestRelease();
|
|
132
|
+
|
|
133
|
+
if (!release.assets) {
|
|
134
|
+
console.error('No release assets found. Please install slvsx manually.');
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Find the right asset
|
|
139
|
+
const asset = release.assets.find(a =>
|
|
140
|
+
a.name.includes(platformInfo.assetPattern) &&
|
|
141
|
+
(a.name.endsWith('.tar.gz') || a.name.endsWith('.zip'))
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (!asset) {
|
|
145
|
+
console.error(`No binary found for ${process.platform}-${process.arch} (looking for '${platformInfo.assetPattern}')`);
|
|
146
|
+
console.error('Available assets:', release.assets.map(a => a.name).join(', '));
|
|
147
|
+
console.error('Please install slvsx manually from https://github.com/snoble/slvsx-cli/releases');
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Create bin directory
|
|
152
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
153
|
+
|
|
154
|
+
// Download
|
|
155
|
+
const tempFile = path.join(binDir, asset.name);
|
|
156
|
+
console.log(`Downloading ${asset.name}...`);
|
|
157
|
+
await downloadFile(asset.browser_download_url, tempFile);
|
|
158
|
+
|
|
159
|
+
// Extract
|
|
160
|
+
console.log('Extracting...');
|
|
161
|
+
if (asset.name.endsWith('.tar.gz')) {
|
|
162
|
+
await extractTarGz(tempFile, binDir);
|
|
163
|
+
} else {
|
|
164
|
+
await extractZip(tempFile, binDir);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Clean up archive
|
|
168
|
+
fs.unlinkSync(tempFile);
|
|
169
|
+
|
|
170
|
+
// Make executable on Unix
|
|
171
|
+
if (process.platform !== 'win32') {
|
|
172
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Verify
|
|
176
|
+
if (fs.existsSync(binaryPath)) {
|
|
177
|
+
console.log(`✓ slvsx installed successfully to ${binaryPath}`);
|
|
178
|
+
} else {
|
|
179
|
+
// Binary might be in a subdirectory after extraction
|
|
180
|
+
const files = fs.readdirSync(binDir);
|
|
181
|
+
console.log('Extracted files:', files);
|
|
182
|
+
console.error('Binary not found at expected location. Please check extraction.');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error('Failed to download slvsx:', error.message);
|
|
187
|
+
console.error('Please install manually from https://github.com/snoble/slvsx-cli/releases');
|
|
188
|
+
process.exit(0); // Don't fail the npm install
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
main();
|
|
193
|
+
|