@sknoble/slvsx-mcp-server 0.1.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/LICENSE +40 -0
- package/README.md +181 -0
- package/dist/docs.json +69214 -0
- package/mcp-server.js +598 -0
- package/package.json +46 -0
package/mcp-server.js
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP Server for SLVSX Constraint Solver
|
|
5
|
+
*
|
|
6
|
+
* Exposes the SLVSX constraint solver as an MCP server that can be used
|
|
7
|
+
* by Claude and other AI assistants to solve geometric constraints.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
11
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
|
+
import {
|
|
13
|
+
CallToolRequestSchema,
|
|
14
|
+
ListToolsRequestSchema
|
|
15
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
16
|
+
import { execSync, spawn } from 'child_process';
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import * as os from 'os';
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
|
|
22
|
+
// Get directory of this script
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = path.dirname(__filename);
|
|
25
|
+
|
|
26
|
+
// Check if slvsx binary exists
|
|
27
|
+
const SLVSX_BINARY = process.env.SLVSX_BINARY || './target/release/slvsx';
|
|
28
|
+
|
|
29
|
+
// Load documentation embeddings if available
|
|
30
|
+
let docsIndex = null;
|
|
31
|
+
let embedder = null;
|
|
32
|
+
|
|
33
|
+
const DOCS_INDEX_PATH = path.join(__dirname, 'dist', 'docs.json');
|
|
34
|
+
const EMBEDDING_MODEL = 'Xenova/all-MiniLM-L6-v2';
|
|
35
|
+
|
|
36
|
+
async function loadDocsIndex() {
|
|
37
|
+
if (fs.existsSync(DOCS_INDEX_PATH)) {
|
|
38
|
+
try {
|
|
39
|
+
const data = fs.readFileSync(DOCS_INDEX_PATH, 'utf-8');
|
|
40
|
+
const parsed = JSON.parse(data);
|
|
41
|
+
// Validate structure before assigning
|
|
42
|
+
if (!parsed.chunks || !Array.isArray(parsed.chunks)) {
|
|
43
|
+
throw new Error('Invalid docs.json structure: missing or invalid chunks array');
|
|
44
|
+
}
|
|
45
|
+
// Validate model matches runtime embedder
|
|
46
|
+
if (parsed.model && parsed.model !== EMBEDDING_MODEL) {
|
|
47
|
+
throw new Error(`Model mismatch: docs.json uses '${parsed.model}' but runtime uses '${EMBEDDING_MODEL}'. Rebuild with 'npm run build:docs'.`);
|
|
48
|
+
}
|
|
49
|
+
docsIndex = parsed;
|
|
50
|
+
console.error(`Loaded ${docsIndex.chunks.length} documentation chunks (model: ${EMBEDDING_MODEL})`);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
docsIndex = null; // Reset to null on any error
|
|
53
|
+
console.error('Warning: Failed to load docs index:', e.message);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
console.error('Note: docs.json not found. Run "npm run build:docs" to enable documentation search.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function getEmbedder() {
|
|
61
|
+
if (!embedder) {
|
|
62
|
+
const { pipeline } = await import('@xenova/transformers');
|
|
63
|
+
embedder = await pipeline('feature-extraction', EMBEDDING_MODEL);
|
|
64
|
+
}
|
|
65
|
+
return embedder;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function cosineSimilarity(a, b) {
|
|
69
|
+
let dotProduct = 0;
|
|
70
|
+
let normA = 0;
|
|
71
|
+
let normB = 0;
|
|
72
|
+
for (let i = 0; i < a.length; i++) {
|
|
73
|
+
dotProduct += a[i] * b[i];
|
|
74
|
+
normA += a[i] * a[i];
|
|
75
|
+
normB += b[i] * b[i];
|
|
76
|
+
}
|
|
77
|
+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function searchDocumentation(query, topK = 3) {
|
|
81
|
+
if (!docsIndex) {
|
|
82
|
+
return { error: 'Documentation index not loaded. Run "npm run build:docs" first.' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const embed = await getEmbedder();
|
|
87
|
+
const output = await embed(query, { pooling: 'mean', normalize: true });
|
|
88
|
+
const queryEmbedding = Array.from(output.data);
|
|
89
|
+
|
|
90
|
+
// Calculate similarities
|
|
91
|
+
const results = docsIndex.chunks.map(chunk => ({
|
|
92
|
+
...chunk,
|
|
93
|
+
similarity: cosineSimilarity(queryEmbedding, chunk.embedding),
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
// Sort by similarity and take top K
|
|
97
|
+
results.sort((a, b) => b.similarity - a.similarity);
|
|
98
|
+
const topResults = results.slice(0, topK);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
results: topResults.map(r => ({
|
|
102
|
+
source: r.source,
|
|
103
|
+
content: r.content,
|
|
104
|
+
score: r.similarity.toFixed(4),
|
|
105
|
+
})),
|
|
106
|
+
};
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return { error: `Search failed: ${e.message}` };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
class SlvsxServer {
|
|
113
|
+
constructor() {
|
|
114
|
+
this.server = new Server(
|
|
115
|
+
{
|
|
116
|
+
name: 'slvsx-mcp',
|
|
117
|
+
version: '0.1.0',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
capabilities: {
|
|
121
|
+
tools: {},
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
this.setupHandlers();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
setupHandlers() {
|
|
130
|
+
// List available tools
|
|
131
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
132
|
+
tools: [
|
|
133
|
+
{
|
|
134
|
+
name: 'solve_constraints',
|
|
135
|
+
description: 'Solve geometric constraints using SLVSX solver',
|
|
136
|
+
inputSchema: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
properties: {
|
|
139
|
+
constraints: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
description: 'JSON constraint document following SLVSX schema',
|
|
142
|
+
properties: {
|
|
143
|
+
schema: { type: 'string', enum: ['slvs-json/1'] },
|
|
144
|
+
units: { type: 'string', enum: ['mm', 'cm', 'm', 'in', 'ft'] },
|
|
145
|
+
parameters: { type: 'object' },
|
|
146
|
+
entities: { type: 'array' },
|
|
147
|
+
constraints: { type: 'array' }
|
|
148
|
+
},
|
|
149
|
+
required: ['schema', 'entities', 'constraints']
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
required: ['constraints'],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'validate_constraints',
|
|
157
|
+
description: 'Validate a constraint document without solving',
|
|
158
|
+
inputSchema: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: {
|
|
161
|
+
constraints: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
description: 'JSON constraint document to validate',
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
required: ['constraints'],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'export_to_svg',
|
|
171
|
+
description: 'Solve constraints and export result to SVG',
|
|
172
|
+
inputSchema: {
|
|
173
|
+
type: 'object',
|
|
174
|
+
properties: {
|
|
175
|
+
constraints: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
description: 'JSON constraint document',
|
|
178
|
+
},
|
|
179
|
+
width: {
|
|
180
|
+
type: 'number',
|
|
181
|
+
description: 'SVG width in pixels',
|
|
182
|
+
default: 800
|
|
183
|
+
},
|
|
184
|
+
height: {
|
|
185
|
+
type: 'number',
|
|
186
|
+
description: 'SVG height in pixels',
|
|
187
|
+
default: 600
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
required: ['constraints'],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: 'get_schema',
|
|
195
|
+
description: 'Get the JSON schema for constraint documents',
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: 'create_example',
|
|
203
|
+
description: 'Create an example constraint document',
|
|
204
|
+
inputSchema: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties: {
|
|
207
|
+
type: {
|
|
208
|
+
type: 'string',
|
|
209
|
+
description: 'Type of example to create',
|
|
210
|
+
enum: ['triangle', 'square', 'circle', 'linkage', 'parametric', '3d']
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
required: ['type'],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'search_documentation',
|
|
218
|
+
description: 'Search SLVSX documentation for relevant information about using the constraint solver, JSON schema, examples, and best practices',
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: 'object',
|
|
221
|
+
properties: {
|
|
222
|
+
query: {
|
|
223
|
+
type: 'string',
|
|
224
|
+
description: 'Search query to find relevant documentation',
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
required: ['query'],
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
],
|
|
231
|
+
}));
|
|
232
|
+
|
|
233
|
+
// Handle tool calls
|
|
234
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
235
|
+
const { name, arguments: args } = request.params;
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
switch (name) {
|
|
239
|
+
case 'solve_constraints':
|
|
240
|
+
return await this.solveConstraints(args.constraints);
|
|
241
|
+
|
|
242
|
+
case 'validate_constraints':
|
|
243
|
+
return await this.validateConstraints(args.constraints);
|
|
244
|
+
|
|
245
|
+
case 'export_to_svg':
|
|
246
|
+
return await this.exportToSvg(args.constraints, args.width, args.height);
|
|
247
|
+
|
|
248
|
+
case 'get_schema':
|
|
249
|
+
return await this.getSchema();
|
|
250
|
+
|
|
251
|
+
case 'create_example':
|
|
252
|
+
return await this.createExample(args.type);
|
|
253
|
+
|
|
254
|
+
case 'search_documentation':
|
|
255
|
+
return await this.searchDocs(args.query);
|
|
256
|
+
|
|
257
|
+
default:
|
|
258
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return {
|
|
262
|
+
content: [
|
|
263
|
+
{
|
|
264
|
+
type: 'text',
|
|
265
|
+
text: `Error: ${error.message}`,
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async solveConstraints(constraints) {
|
|
274
|
+
// Write constraints to temp file
|
|
275
|
+
const tmpFile = path.join(os.tmpdir(), `slvsx-${Date.now()}.json`);
|
|
276
|
+
fs.writeFileSync(tmpFile, JSON.stringify(constraints, null, 2));
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
// Run slvsx solve
|
|
280
|
+
const result = execSync(`${SLVSX_BINARY} solve ${tmpFile}`, {
|
|
281
|
+
encoding: 'utf-8',
|
|
282
|
+
maxBuffer: 10 * 1024 * 1024 // 10MB
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Parse the result
|
|
286
|
+
const lines = result.split('\n');
|
|
287
|
+
let jsonResult = '';
|
|
288
|
+
let inJson = false;
|
|
289
|
+
|
|
290
|
+
for (const line of lines) {
|
|
291
|
+
if (line.startsWith('{')) inJson = true;
|
|
292
|
+
if (inJson) jsonResult += line + '\n';
|
|
293
|
+
if (line.startsWith('}')) inJson = false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const solved = jsonResult ? JSON.parse(jsonResult) : { error: 'No solution found' };
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
content: [
|
|
300
|
+
{
|
|
301
|
+
type: 'text',
|
|
302
|
+
text: JSON.stringify(solved, null, 2),
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
};
|
|
306
|
+
} finally {
|
|
307
|
+
// Clean up temp file
|
|
308
|
+
if (fs.existsSync(tmpFile)) {
|
|
309
|
+
fs.unlinkSync(tmpFile);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async validateConstraints(constraints) {
|
|
315
|
+
const tmpFile = path.join(os.tmpdir(), `slvsx-validate-${Date.now()}.json`);
|
|
316
|
+
fs.writeFileSync(tmpFile, JSON.stringify(constraints, null, 2));
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const result = execSync(`${SLVSX_BINARY} validate ${tmpFile}`, {
|
|
320
|
+
encoding: 'utf-8'
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
content: [
|
|
325
|
+
{
|
|
326
|
+
type: 'text',
|
|
327
|
+
text: result.includes('✓') ? 'Valid constraint document' : result,
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
} catch (error) {
|
|
332
|
+
return {
|
|
333
|
+
content: [
|
|
334
|
+
{
|
|
335
|
+
type: 'text',
|
|
336
|
+
text: `Validation failed: ${error.message}`,
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
};
|
|
340
|
+
} finally {
|
|
341
|
+
if (fs.existsSync(tmpFile)) {
|
|
342
|
+
fs.unlinkSync(tmpFile);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async exportToSvg(constraints, width = 800, height = 600) {
|
|
348
|
+
const tmpInput = path.join(os.tmpdir(), `slvsx-input-${Date.now()}.json`);
|
|
349
|
+
const tmpOutput = path.join(os.tmpdir(), `slvsx-output-${Date.now()}.svg`);
|
|
350
|
+
|
|
351
|
+
fs.writeFileSync(tmpInput, JSON.stringify(constraints, null, 2));
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
execSync(`${SLVSX_BINARY} export --format svg --output ${tmpOutput} ${tmpInput}`, {
|
|
355
|
+
encoding: 'utf-8'
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const svg = fs.readFileSync(tmpOutput, 'utf-8');
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
content: [
|
|
362
|
+
{
|
|
363
|
+
type: 'text',
|
|
364
|
+
text: svg,
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
};
|
|
368
|
+
} finally {
|
|
369
|
+
if (fs.existsSync(tmpInput)) fs.unlinkSync(tmpInput);
|
|
370
|
+
if (fs.existsSync(tmpOutput)) fs.unlinkSync(tmpOutput);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async getSchema() {
|
|
375
|
+
try {
|
|
376
|
+
const schema = execSync(`${SLVSX_BINARY} schema`, {
|
|
377
|
+
encoding: 'utf-8'
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
content: [
|
|
382
|
+
{
|
|
383
|
+
type: 'text',
|
|
384
|
+
text: schema,
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
};
|
|
388
|
+
} catch (error) {
|
|
389
|
+
return {
|
|
390
|
+
content: [
|
|
391
|
+
{
|
|
392
|
+
type: 'text',
|
|
393
|
+
text: `Failed to get schema: ${error.message}`,
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async createExample(type) {
|
|
401
|
+
const examples = {
|
|
402
|
+
triangle: {
|
|
403
|
+
schema: 'slvs-json/1',
|
|
404
|
+
units: 'mm',
|
|
405
|
+
entities: [
|
|
406
|
+
{ id: 'p1', type: 'Point', x: 0, y: 0 },
|
|
407
|
+
{ id: 'p2', type: 'Point', x: 100, y: 0 },
|
|
408
|
+
{ id: 'p3', type: 'Point', x: 50, y: 86.6 }
|
|
409
|
+
],
|
|
410
|
+
constraints: [
|
|
411
|
+
{ type: 'Fixed', entity: 'p1' },
|
|
412
|
+
{ type: 'Distance', entities: ['p1', 'p2'], distance: 100 },
|
|
413
|
+
{ type: 'Distance', entities: ['p2', 'p3'], distance: 100 },
|
|
414
|
+
{ type: 'Distance', entities: ['p3', 'p1'], distance: 100 }
|
|
415
|
+
]
|
|
416
|
+
},
|
|
417
|
+
square: {
|
|
418
|
+
schema: 'slvs-json/1',
|
|
419
|
+
units: 'mm',
|
|
420
|
+
entities: [
|
|
421
|
+
{ id: 'p1', type: 'Point', x: 0, y: 0 },
|
|
422
|
+
{ id: 'p2', type: 'Point', x: 100, y: 0 },
|
|
423
|
+
{ id: 'p3', type: 'Point', x: 100, y: 100 },
|
|
424
|
+
{ id: 'p4', type: 'Point', x: 0, y: 100 },
|
|
425
|
+
{ id: 'l1', type: 'Line', points: ['p1', 'p2'] },
|
|
426
|
+
{ id: 'l2', type: 'Line', points: ['p2', 'p3'] },
|
|
427
|
+
{ id: 'l3', type: 'Line', points: ['p3', 'p4'] },
|
|
428
|
+
{ id: 'l4', type: 'Line', points: ['p4', 'p1'] }
|
|
429
|
+
],
|
|
430
|
+
constraints: [
|
|
431
|
+
{ type: 'Fixed', entity: 'p1' },
|
|
432
|
+
{ type: 'Fixed', entity: 'p2' },
|
|
433
|
+
{ type: 'Perpendicular', entities: ['l1', 'l2'] },
|
|
434
|
+
{ type: 'Perpendicular', entities: ['l2', 'l3'] },
|
|
435
|
+
{ type: 'Perpendicular', entities: ['l3', 'l4'] },
|
|
436
|
+
{ type: 'Equal', entities: ['l1', 'l2'] }
|
|
437
|
+
]
|
|
438
|
+
},
|
|
439
|
+
circle: {
|
|
440
|
+
schema: 'slvs-json/1',
|
|
441
|
+
units: 'mm',
|
|
442
|
+
entities: [
|
|
443
|
+
{ id: 'center', type: 'Point', x: 50, y: 50 },
|
|
444
|
+
{ id: 'c1', type: 'Circle', center: 'center', radius: 30 }
|
|
445
|
+
],
|
|
446
|
+
constraints: [
|
|
447
|
+
{ type: 'Fixed', entity: 'center' },
|
|
448
|
+
{ type: 'Radius', entity: 'c1', radius: 30 }
|
|
449
|
+
]
|
|
450
|
+
},
|
|
451
|
+
linkage: {
|
|
452
|
+
schema: 'slvs-json/1',
|
|
453
|
+
units: 'mm',
|
|
454
|
+
parameters: {
|
|
455
|
+
input_angle: 45
|
|
456
|
+
},
|
|
457
|
+
entities: [
|
|
458
|
+
{ id: 'ground1', type: 'Point', x: 0, y: 0 },
|
|
459
|
+
{ id: 'ground2', type: 'Point', x: 100, y: 0 },
|
|
460
|
+
{ id: 'joint1', type: 'Point', x: 30, y: 30 },
|
|
461
|
+
{ id: 'joint2', type: 'Point', x: 70, y: 40 },
|
|
462
|
+
{ id: 'link1', type: 'Line', points: ['ground1', 'joint1'] },
|
|
463
|
+
{ id: 'link2', type: 'Line', points: ['joint1', 'joint2'] },
|
|
464
|
+
{ id: 'link3', type: 'Line', points: ['joint2', 'ground2'] }
|
|
465
|
+
],
|
|
466
|
+
constraints: [
|
|
467
|
+
{ type: 'Fixed', entity: 'ground1' },
|
|
468
|
+
{ type: 'Fixed', entity: 'ground2' },
|
|
469
|
+
{ type: 'Distance', entities: ['ground1', 'joint1'], distance: 40 },
|
|
470
|
+
{ type: 'Distance', entities: ['joint1', 'joint2'], distance: 50 },
|
|
471
|
+
{ type: 'Distance', entities: ['joint2', 'ground2'], distance: 35 },
|
|
472
|
+
{ type: 'Angle', entities: ['link1'], angle: '$input_angle' }
|
|
473
|
+
]
|
|
474
|
+
},
|
|
475
|
+
parametric: {
|
|
476
|
+
schema: 'slvs-json/1',
|
|
477
|
+
units: 'mm',
|
|
478
|
+
parameters: {
|
|
479
|
+
width: 150,
|
|
480
|
+
height: 100,
|
|
481
|
+
hole_radius: 10
|
|
482
|
+
},
|
|
483
|
+
entities: [
|
|
484
|
+
{ id: 'p1', type: 'Point', x: 0, y: 0 },
|
|
485
|
+
{ id: 'p2', type: 'Point', x: '$width', y: 0 },
|
|
486
|
+
{ id: 'p3', type: 'Point', x: '$width', y: '$height' },
|
|
487
|
+
{ id: 'p4', type: 'Point', x: 0, y: '$height' },
|
|
488
|
+
{ id: 'hole_center', type: 'Point', x: 75, y: 50 },
|
|
489
|
+
{ id: 'hole', type: 'Circle', center: 'hole_center', radius: '$hole_radius' }
|
|
490
|
+
],
|
|
491
|
+
constraints: [
|
|
492
|
+
{ type: 'Fixed', entity: 'p1' },
|
|
493
|
+
{ type: 'HorizontalDistance', entities: ['p1', 'p2'], distance: '$width' },
|
|
494
|
+
{ type: 'VerticalDistance', entities: ['p1', 'p4'], distance: '$height' },
|
|
495
|
+
{ type: 'Horizontal', entity: 'p2' },
|
|
496
|
+
{ type: 'Vertical', entity: 'p4' },
|
|
497
|
+
{ type: 'Radius', entity: 'hole', radius: '$hole_radius' }
|
|
498
|
+
]
|
|
499
|
+
},
|
|
500
|
+
'3d': {
|
|
501
|
+
schema: 'slvs-json/1',
|
|
502
|
+
units: 'mm',
|
|
503
|
+
entities: [
|
|
504
|
+
{ id: 'p1', type: 'Point', x: 0, y: 0, z: 0 },
|
|
505
|
+
{ id: 'p2', type: 'Point', x: 100, y: 0, z: 0 },
|
|
506
|
+
{ id: 'p3', type: 'Point', x: 100, y: 100, z: 0 },
|
|
507
|
+
{ id: 'p4', type: 'Point', x: 0, y: 100, z: 0 },
|
|
508
|
+
{ id: 'p5', type: 'Point', x: 0, y: 0, z: 50 },
|
|
509
|
+
{ id: 'p6', type: 'Point', x: 100, y: 0, z: 50 },
|
|
510
|
+
{ id: 'p7', type: 'Point', x: 100, y: 100, z: 50 },
|
|
511
|
+
{ id: 'p8', type: 'Point', x: 0, y: 100, z: 50 }
|
|
512
|
+
],
|
|
513
|
+
constraints: [
|
|
514
|
+
{ type: 'Fixed', entity: 'p1' },
|
|
515
|
+
{ type: 'Distance', entities: ['p1', 'p2'], distance: 100 },
|
|
516
|
+
{ type: 'Distance', entities: ['p2', 'p3'], distance: 100 },
|
|
517
|
+
{ type: 'Distance', entities: ['p3', 'p4'], distance: 100 },
|
|
518
|
+
{ type: 'Distance', entities: ['p4', 'p1'], distance: 100 },
|
|
519
|
+
{ type: 'Distance', entities: ['p1', 'p5'], distance: 50 },
|
|
520
|
+
{ type: 'Distance', entities: ['p2', 'p6'], distance: 50 },
|
|
521
|
+
{ type: 'Distance', entities: ['p3', 'p7'], distance: 50 },
|
|
522
|
+
{ type: 'Distance', entities: ['p4', 'p8'], distance: 50 }
|
|
523
|
+
]
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const example = examples[type];
|
|
528
|
+
if (!example) {
|
|
529
|
+
return {
|
|
530
|
+
content: [
|
|
531
|
+
{
|
|
532
|
+
type: 'text',
|
|
533
|
+
text: `Unknown example type: ${type}. Available: ${Object.keys(examples).join(', ')}`,
|
|
534
|
+
},
|
|
535
|
+
],
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
content: [
|
|
541
|
+
{
|
|
542
|
+
type: 'text',
|
|
543
|
+
text: JSON.stringify(example, null, 2),
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async searchDocs(query) {
|
|
550
|
+
const result = await searchDocumentation(query, 3);
|
|
551
|
+
|
|
552
|
+
if (result.error) {
|
|
553
|
+
return {
|
|
554
|
+
content: [
|
|
555
|
+
{
|
|
556
|
+
type: 'text',
|
|
557
|
+
text: result.error,
|
|
558
|
+
},
|
|
559
|
+
],
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Format results as readable text
|
|
564
|
+
const formatted = result.results.map((r, i) =>
|
|
565
|
+
`--- Result ${i + 1} (${r.source}, score: ${r.score}) ---\n${r.content}`
|
|
566
|
+
).join('\n\n');
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
content: [
|
|
570
|
+
{
|
|
571
|
+
type: 'text',
|
|
572
|
+
text: formatted || 'No results found.',
|
|
573
|
+
},
|
|
574
|
+
],
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async run() {
|
|
579
|
+
// Load documentation index for search
|
|
580
|
+
await loadDocsIndex();
|
|
581
|
+
|
|
582
|
+
const transport = new StdioServerTransport();
|
|
583
|
+
await this.server.connect(transport);
|
|
584
|
+
console.error('SLVSX MCP Server running on stdio');
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Check if slvsx binary exists
|
|
589
|
+
if (!fs.existsSync(SLVSX_BINARY)) {
|
|
590
|
+
console.error(`Error: SLVSX binary not found at ${SLVSX_BINARY}`);
|
|
591
|
+
console.error('Please build the project first with: cargo build --release');
|
|
592
|
+
console.error('Or set SLVSX_BINARY environment variable to point to the binary');
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Run the server
|
|
597
|
+
const server = new SlvsxServer();
|
|
598
|
+
server.run().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sknoble/slvsx-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for SLVSX geometric constraint solver",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"slvsx-mcp": "./mcp-server.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "mcp-server.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node mcp-server.js",
|
|
12
|
+
"build:docs": "npx tsx scripts/build-docs.ts",
|
|
13
|
+
"test": "node tests/mcp-server.test.js"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@langchain/textsplitters": "^0.1.0",
|
|
17
|
+
"@modelcontextprotocol/sdk": "^0.5.0",
|
|
18
|
+
"@xenova/transformers": "^2.17.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"tsx": "^4.7.0",
|
|
22
|
+
"typescript": "^5.3.0",
|
|
23
|
+
"@types/node": "^20.10.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mcp",
|
|
27
|
+
"constraint-solver",
|
|
28
|
+
"geometry",
|
|
29
|
+
"cad",
|
|
30
|
+
"solvespace"
|
|
31
|
+
],
|
|
32
|
+
"author": "slvsx contributors",
|
|
33
|
+
"license": "GPL-3.0-or-later",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/snoble/slvsx-cli.git"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"mcp-server.js",
|
|
40
|
+
"dist/docs.json",
|
|
41
|
+
"README.md"
|
|
42
|
+
],
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
}
|
|
46
|
+
}
|