cyclecad 0.2.2 → 0.2.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/API-BUILD-MANIFEST.txt +339 -0
- package/API-SERVER.md +535 -0
- package/Architecture-Deck.pptx +0 -0
- package/CLAUDE.md +172 -11
- package/CLI-BUILD-SUMMARY.md +504 -0
- package/CLI-INDEX.md +356 -0
- package/CLI-README.md +466 -0
- package/COLLABORATION-INTEGRATION-GUIDE.md +325 -0
- package/CONNECTED_FABS_GUIDE.md +612 -0
- package/CONNECTED_FABS_README.md +310 -0
- package/DELIVERABLES.md +343 -0
- package/DFM-ANALYZER-INTEGRATION.md +368 -0
- package/DFM-QUICK-START.js +253 -0
- package/Dockerfile +69 -0
- package/IMPLEMENTATION.md +327 -0
- package/LICENSE +31 -0
- package/MARKETPLACE_QUICK_REFERENCE.txt +294 -0
- package/MCP-INDEX.md +264 -0
- package/QUICKSTART-API.md +388 -0
- package/QUICKSTART-CLI.md +211 -0
- package/QUICKSTART-MCP.md +196 -0
- package/README-MCP.md +208 -0
- package/TEST-TOKEN-ENGINE.md +319 -0
- package/TOKEN-ENGINE-SUMMARY.md +266 -0
- package/TOKENS-README.md +263 -0
- package/TOOLS-REFERENCE.md +254 -0
- package/app/index.html +168 -3
- package/app/js/TOKEN-INTEGRATION.md +391 -0
- package/app/js/agent-api.js +3 -3
- package/app/js/ai-copilot.js +1435 -0
- package/app/js/cam-pipeline.js +840 -0
- package/app/js/collaboration-ui.js +995 -0
- package/app/js/collaboration.js +1116 -0
- package/app/js/connected-fabs-example.js +404 -0
- package/app/js/connected-fabs.js +1449 -0
- package/app/js/dfm-analyzer.js +1760 -0
- package/app/js/marketplace.js +1994 -0
- package/app/js/material-library.js +2115 -0
- package/app/js/token-dashboard.js +563 -0
- package/app/js/token-engine.js +743 -0
- package/app/test-agent.html +1801 -0
- package/bin/cyclecad-cli.js +662 -0
- package/bin/cyclecad-mcp +2 -0
- package/bin/server.js +242 -0
- package/cycleCAD-Architecture.pptx +0 -0
- package/cycleCAD-Investor-Deck.pptx +0 -0
- package/demo-mcp.sh +60 -0
- package/docs/API-SERVER-SUMMARY.md +375 -0
- package/docs/API-SERVER.md +667 -0
- package/docs/CAM-EXAMPLES.md +344 -0
- package/docs/CAM-INTEGRATION.md +612 -0
- package/docs/CAM-QUICK-REFERENCE.md +199 -0
- package/docs/CLI-INTEGRATION.md +510 -0
- package/docs/CLI.md +872 -0
- package/docs/MARKETPLACE-API-SCHEMA.json +564 -0
- package/docs/MARKETPLACE-INTEGRATION.md +467 -0
- package/docs/MARKETPLACE-SETUP.html +439 -0
- package/docs/MCP-SERVER.md +403 -0
- package/examples/api-client-example.js +488 -0
- package/examples/api-client-example.py +359 -0
- package/examples/batch-manufacturing.txt +28 -0
- package/examples/batch-simple.txt +26 -0
- package/model-marketplace.html +1273 -0
- package/package.json +14 -3
- package/server/api-server.js +1120 -0
- package/server/mcp-server.js +1161 -0
- package/test-api-server.js +432 -0
- package/test-mcp.js +198 -0
- package/~$cycleCAD-Investor-Deck.pptx +0 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test suite for cycleCAD API Server
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npm run test:api
|
|
8
|
+
* node test-api-server.js
|
|
9
|
+
*
|
|
10
|
+
* Verifies all API endpoints and core functionality without external dependencies.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const http = require('http');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// TEST FRAMEWORK
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
const colors = {
|
|
21
|
+
reset: '\x1b[0m',
|
|
22
|
+
green: '\x1b[32m',
|
|
23
|
+
red: '\x1b[31m',
|
|
24
|
+
yellow: '\x1b[33m',
|
|
25
|
+
blue: '\x1b[36m',
|
|
26
|
+
dim: '\x1b[2m'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let passCount = 0;
|
|
30
|
+
let failCount = 0;
|
|
31
|
+
|
|
32
|
+
function assert(condition, message) {
|
|
33
|
+
if (!condition) {
|
|
34
|
+
console.log(`${colors.red}✗ ${message}${colors.reset}`);
|
|
35
|
+
failCount++;
|
|
36
|
+
throw new Error(`Assertion failed: ${message}`);
|
|
37
|
+
}
|
|
38
|
+
console.log(`${colors.green}✓${colors.reset} ${message}`);
|
|
39
|
+
passCount++;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function section(title) {
|
|
43
|
+
console.log(`\n${colors.blue}${title}${colors.reset}`);
|
|
44
|
+
console.log(colors.dim + '='.repeat(60) + colors.reset);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// HTTP CLIENT
|
|
49
|
+
// ============================================================================
|
|
50
|
+
|
|
51
|
+
function request(method, path, data = null) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const options = {
|
|
54
|
+
hostname: 'localhost',
|
|
55
|
+
port: 3000,
|
|
56
|
+
path,
|
|
57
|
+
method,
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/json'
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const req = http.request(options, (res) => {
|
|
64
|
+
let body = '';
|
|
65
|
+
res.on('data', chunk => body += chunk);
|
|
66
|
+
res.on('end', () => {
|
|
67
|
+
try {
|
|
68
|
+
const json = JSON.parse(body);
|
|
69
|
+
resolve({ status: res.statusCode, body: json, headers: res.headers });
|
|
70
|
+
} catch (e) {
|
|
71
|
+
resolve({ status: res.statusCode, body, headers: res.headers });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
req.on('error', reject);
|
|
77
|
+
|
|
78
|
+
if (data) req.write(JSON.stringify(data));
|
|
79
|
+
req.end();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// TESTS
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
async function testHealth() {
|
|
88
|
+
section('1. Health Check');
|
|
89
|
+
const { status, body } = await request('GET', '/api/health');
|
|
90
|
+
assert(status === 200, 'Health endpoint returns 200');
|
|
91
|
+
assert(body.ok === true || body.status === 'ok', 'Server status is OK');
|
|
92
|
+
assert(body.version, 'Version is present');
|
|
93
|
+
assert(body.sessionId, 'Session ID is present');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function testSchema() {
|
|
97
|
+
section('2. API Schema');
|
|
98
|
+
const { status, body } = await request('GET', '/api/schema');
|
|
99
|
+
assert(status === 200, 'Schema endpoint returns 200');
|
|
100
|
+
assert(body.version, 'Schema has version');
|
|
101
|
+
assert(body.namespaces, 'Schema has namespaces');
|
|
102
|
+
assert(Object.keys(body.namespaces).length > 0, 'Schema has at least one namespace');
|
|
103
|
+
assert(body.totalCommands > 0, 'Schema lists commands');
|
|
104
|
+
console.log(` ${colors.dim}(${body.totalCommands} total commands)${colors.reset}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function testExecuteSingleCommand() {
|
|
108
|
+
section('3. Execute Single Command');
|
|
109
|
+
|
|
110
|
+
// Valid command
|
|
111
|
+
let { status, body } = await request('POST', '/api/execute', {
|
|
112
|
+
method: 'meta.ping',
|
|
113
|
+
params: {}
|
|
114
|
+
});
|
|
115
|
+
assert(status === 200, 'Execute endpoint returns 200');
|
|
116
|
+
assert(body.ok === true, 'Ping command succeeds');
|
|
117
|
+
assert(body.result.status === 'pong', 'Ping returns pong');
|
|
118
|
+
assert(body.elapsed >= 0, 'Elapsed time is tracked');
|
|
119
|
+
|
|
120
|
+
// Invalid method
|
|
121
|
+
({ status, body } = await request('POST', '/api/execute', {
|
|
122
|
+
method: 'invalid.method',
|
|
123
|
+
params: {}
|
|
124
|
+
}));
|
|
125
|
+
assert(status === 400, 'Invalid method returns 400');
|
|
126
|
+
assert(body.ok === false, 'Invalid method fails gracefully');
|
|
127
|
+
assert(body.error, 'Error message is provided');
|
|
128
|
+
|
|
129
|
+
// Missing method
|
|
130
|
+
({ status, body } = await request('POST', '/api/execute', {
|
|
131
|
+
params: {}
|
|
132
|
+
}));
|
|
133
|
+
assert(status === 400, 'Missing method returns 400');
|
|
134
|
+
assert(body.error.includes('method'), 'Error mentions missing method');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function testBatchCommands() {
|
|
138
|
+
section('4. Batch Commands');
|
|
139
|
+
const { status, body } = await request('POST', '/api/batch', {
|
|
140
|
+
commands: [
|
|
141
|
+
{ method: 'meta.ping', params: {} },
|
|
142
|
+
{ method: 'query.materials', params: {} },
|
|
143
|
+
{ method: 'meta.version', params: {} }
|
|
144
|
+
]
|
|
145
|
+
});
|
|
146
|
+
assert(status === 200, 'Batch endpoint returns 200');
|
|
147
|
+
assert(body.ok === true, 'Batch succeeds');
|
|
148
|
+
assert(body.results.length === 3, 'All 3 commands executed');
|
|
149
|
+
assert(body.executed === 3, 'Executed count is correct');
|
|
150
|
+
assert(body.total === 3, 'Total count is correct');
|
|
151
|
+
assert(body.elapsed >= 0, 'Total elapsed time is tracked');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function testModelManagement() {
|
|
155
|
+
section('5. Model Management');
|
|
156
|
+
|
|
157
|
+
// Get models (empty at start)
|
|
158
|
+
let { status, body } = await request('GET', '/api/models');
|
|
159
|
+
assert(status === 200, 'Get models returns 200');
|
|
160
|
+
assert(body.ok === true, 'Models endpoint succeeds');
|
|
161
|
+
assert(Array.isArray(body.models), 'Models is an array');
|
|
162
|
+
const initialCount = body.count;
|
|
163
|
+
|
|
164
|
+
// Add component
|
|
165
|
+
({ status, body } = await request('POST', '/api/execute', {
|
|
166
|
+
method: 'assembly.addComponent',
|
|
167
|
+
params: { name: 'TestComponent', position: [0, 0, 0] }
|
|
168
|
+
}));
|
|
169
|
+
assert(status === 200, 'Add component returns 200');
|
|
170
|
+
assert(body.result.id, 'Component has ID');
|
|
171
|
+
assert(body.result.name === 'TestComponent', 'Component name is correct');
|
|
172
|
+
const componentId = body.result.id;
|
|
173
|
+
|
|
174
|
+
// List models
|
|
175
|
+
({ status, body } = await request('GET', '/api/models'));
|
|
176
|
+
assert(body.count > initialCount, 'Model count increased');
|
|
177
|
+
|
|
178
|
+
// Get specific model
|
|
179
|
+
({ status, body } = await request('GET', `/api/models/${componentId}`));
|
|
180
|
+
assert(status === 200, 'Get specific model returns 200');
|
|
181
|
+
assert(body.model.id === componentId, 'Model ID matches');
|
|
182
|
+
|
|
183
|
+
// Delete model
|
|
184
|
+
({ status, body } = await request('DELETE', `/api/models/${componentId}`));
|
|
185
|
+
assert(status === 200, 'Delete model returns 200');
|
|
186
|
+
assert(body.ok === true, 'Delete succeeds');
|
|
187
|
+
|
|
188
|
+
// Verify deletion
|
|
189
|
+
({ status, body } = await request('GET', `/api/models/${componentId}`));
|
|
190
|
+
assert(status === 404, 'Deleted model returns 404');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function testHistory() {
|
|
194
|
+
section('6. Command History');
|
|
195
|
+
|
|
196
|
+
// Execute some commands first
|
|
197
|
+
await request('POST', '/api/execute', { method: 'meta.ping', params: {} });
|
|
198
|
+
await request('POST', '/api/execute', { method: 'query.materials', params: {} });
|
|
199
|
+
|
|
200
|
+
// Get history
|
|
201
|
+
const { status, body } = await request('GET', '/api/history?count=10');
|
|
202
|
+
assert(status === 200, 'History endpoint returns 200');
|
|
203
|
+
assert(body.sessionId, 'History has session ID');
|
|
204
|
+
assert(body.total >= 2, 'History records previous commands');
|
|
205
|
+
assert(Array.isArray(body.recent), 'Recent is an array');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function testRateLimiting() {
|
|
209
|
+
section('7. Rate Limiting');
|
|
210
|
+
|
|
211
|
+
// Check rate limit headers
|
|
212
|
+
const { headers } = await request('GET', '/api/health');
|
|
213
|
+
assert(headers['ratelimit-limit'], 'Rate limit header present');
|
|
214
|
+
assert(headers['ratelimit-remaining'], 'Rate limit remaining header present');
|
|
215
|
+
assert(headers['ratelimit-reset'], 'Rate limit reset header present');
|
|
216
|
+
const limit = parseInt(headers['ratelimit-limit']);
|
|
217
|
+
const remaining = parseInt(headers['ratelimit-remaining']);
|
|
218
|
+
assert(remaining <= limit, 'Remaining is less than limit');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function testCORS() {
|
|
222
|
+
section('8. CORS Headers');
|
|
223
|
+
|
|
224
|
+
const { headers } = await request('GET', '/api/health');
|
|
225
|
+
assert(headers['access-control-allow-origin'] === '*', 'CORS origin header present');
|
|
226
|
+
assert(headers['access-control-allow-methods'], 'CORS methods header present');
|
|
227
|
+
assert(headers['access-control-allow-headers'], 'CORS headers header present');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function testCOOPCOEP() {
|
|
231
|
+
section('9. COOP/COEP Headers');
|
|
232
|
+
|
|
233
|
+
const { headers } = await request('GET', '/api/health');
|
|
234
|
+
assert(headers['cross-origin-opener-policy'], 'COOP header present');
|
|
235
|
+
assert(headers['cross-origin-embedder-policy'], 'COEP header present');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function testSketchCommands() {
|
|
239
|
+
section('10. Sketch Commands');
|
|
240
|
+
|
|
241
|
+
// Start sketch
|
|
242
|
+
let { status, body } = await request('POST', '/api/execute', {
|
|
243
|
+
method: 'sketch.start',
|
|
244
|
+
params: { plane: 'XY' }
|
|
245
|
+
});
|
|
246
|
+
assert(status === 200, 'Sketch start returns 200');
|
|
247
|
+
assert(body.result.status === 'active', 'Sketch is active');
|
|
248
|
+
|
|
249
|
+
// Draw circle
|
|
250
|
+
({ status, body } = await request('POST', '/api/execute', {
|
|
251
|
+
method: 'sketch.circle',
|
|
252
|
+
params: { cx: 0, cy: 0, radius: 25 }
|
|
253
|
+
}));
|
|
254
|
+
assert(status === 200, 'Circle command returns 200');
|
|
255
|
+
assert(body.result.radius === 25, 'Circle radius is correct');
|
|
256
|
+
|
|
257
|
+
// Draw line
|
|
258
|
+
({ status, body } = await request('POST', '/api/execute', {
|
|
259
|
+
method: 'sketch.line',
|
|
260
|
+
params: { x1: 0, y1: 0, x2: 100, y2: 50 }
|
|
261
|
+
}));
|
|
262
|
+
assert(status === 200, 'Line command returns 200');
|
|
263
|
+
assert(body.result.length > 0, 'Line length calculated');
|
|
264
|
+
|
|
265
|
+
// End sketch
|
|
266
|
+
({ status, body } = await request('POST', '/api/execute', {
|
|
267
|
+
method: 'sketch.end',
|
|
268
|
+
params: {}
|
|
269
|
+
}));
|
|
270
|
+
assert(status === 200, 'Sketch end returns 200');
|
|
271
|
+
assert(body.result.status === 'complete', 'Sketch completed');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function testOperationCommands() {
|
|
275
|
+
section('11. Operation Commands');
|
|
276
|
+
|
|
277
|
+
// Extrude
|
|
278
|
+
let { status, body } = await request('POST', '/api/execute', {
|
|
279
|
+
method: 'ops.extrude',
|
|
280
|
+
params: { height: 50, material: 'steel' }
|
|
281
|
+
});
|
|
282
|
+
assert(status === 200, 'Extrude returns 200');
|
|
283
|
+
assert(body.result.height === 50, 'Extrude height is correct');
|
|
284
|
+
|
|
285
|
+
// Fillet
|
|
286
|
+
({ status, body } = await request('POST', '/api/execute', {
|
|
287
|
+
method: 'ops.fillet',
|
|
288
|
+
params: { target: 'extrude_1', radius: 5 }
|
|
289
|
+
}));
|
|
290
|
+
assert(status === 200, 'Fillet returns 200');
|
|
291
|
+
assert(body.result.radius === 5, 'Fillet radius is correct');
|
|
292
|
+
|
|
293
|
+
// Chamfer
|
|
294
|
+
({ status, body } = await request('POST', '/api/execute', {
|
|
295
|
+
method: 'ops.chamfer',
|
|
296
|
+
params: { target: 'extrude_1', distance: 2 }
|
|
297
|
+
}));
|
|
298
|
+
assert(status === 200, 'Chamfer returns 200');
|
|
299
|
+
assert(body.result.distance === 2, 'Chamfer distance is correct');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function testViewCommands() {
|
|
303
|
+
section('12. View Commands');
|
|
304
|
+
|
|
305
|
+
const views = ['isometric', 'top', 'front'];
|
|
306
|
+
for (const view of views) {
|
|
307
|
+
const { status, body } = await request('POST', '/api/execute', {
|
|
308
|
+
method: 'view.set',
|
|
309
|
+
params: { view }
|
|
310
|
+
});
|
|
311
|
+
assert(status === 200, `Setting view to ${view} returns 200`);
|
|
312
|
+
assert(body.result.view === view, `View set to ${view}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function testValidationCommands() {
|
|
317
|
+
section('13. Validation Commands');
|
|
318
|
+
|
|
319
|
+
// Mass calculation
|
|
320
|
+
let { status, body } = await request('POST', '/api/execute', {
|
|
321
|
+
method: 'validate.mass',
|
|
322
|
+
params: { target: 'test', material: 'steel' }
|
|
323
|
+
});
|
|
324
|
+
assert(status === 200, 'Mass calculation returns 200');
|
|
325
|
+
assert(typeof body.result.mass === 'number', 'Mass is a number');
|
|
326
|
+
|
|
327
|
+
// Cost estimation
|
|
328
|
+
({ status, body } = await request('POST', '/api/execute', {
|
|
329
|
+
method: 'validate.cost',
|
|
330
|
+
params: { target: 'test', process: 'FDM', material: 'PLA' }
|
|
331
|
+
}));
|
|
332
|
+
assert(status === 200, 'Cost estimation returns 200');
|
|
333
|
+
assert(body.result.estimatedCost >= 0, 'Cost is non-negative');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function testQueryCommands() {
|
|
337
|
+
section('14. Query Commands');
|
|
338
|
+
|
|
339
|
+
// Query materials
|
|
340
|
+
let { status, body } = await request('POST', '/api/execute', {
|
|
341
|
+
method: 'query.materials',
|
|
342
|
+
params: {}
|
|
343
|
+
});
|
|
344
|
+
assert(status === 200, 'Query materials returns 200');
|
|
345
|
+
assert(Array.isArray(body.result.materials), 'Materials is an array');
|
|
346
|
+
assert(body.result.materials.length > 0, 'Materials list not empty');
|
|
347
|
+
|
|
348
|
+
// Query features
|
|
349
|
+
({ status, body } = await request('POST', '/api/execute', {
|
|
350
|
+
method: 'query.features',
|
|
351
|
+
params: {}
|
|
352
|
+
}));
|
|
353
|
+
assert(status === 200, 'Query features returns 200');
|
|
354
|
+
assert(Array.isArray(body.result.features), 'Features is an array');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function testErrorHandling() {
|
|
358
|
+
section('15. Error Handling');
|
|
359
|
+
|
|
360
|
+
// Invalid JSON
|
|
361
|
+
let { status } = await request('POST', '/api/execute', 'invalid json');
|
|
362
|
+
assert(status === 400, 'Invalid JSON returns 400');
|
|
363
|
+
|
|
364
|
+
// Method typo (should suggest correction)
|
|
365
|
+
const { body } = await request('POST', '/api/execute', {
|
|
366
|
+
method: 'sketch.circl', // typo
|
|
367
|
+
params: {}
|
|
368
|
+
});
|
|
369
|
+
assert(body.error.includes('Did you mean'), 'Server suggests correction for typo');
|
|
370
|
+
|
|
371
|
+
// Unknown endpoint
|
|
372
|
+
const { status: notFoundStatus } = await request('GET', '/api/unknown');
|
|
373
|
+
assert(notFoundStatus === 404, 'Unknown endpoint returns 404');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ============================================================================
|
|
377
|
+
// RUNNER
|
|
378
|
+
// ============================================================================
|
|
379
|
+
|
|
380
|
+
async function main() {
|
|
381
|
+
console.log('\n' + colors.blue + '█'.repeat(60));
|
|
382
|
+
console.log('█ cycleCAD API Server — Test Suite');
|
|
383
|
+
console.log('█'.repeat(60) + colors.reset);
|
|
384
|
+
|
|
385
|
+
// Check if server is running
|
|
386
|
+
console.log('\n' + colors.dim + 'Checking if server is running on localhost:3000...');
|
|
387
|
+
try {
|
|
388
|
+
await request('GET', '/api/health');
|
|
389
|
+
console.log(colors.reset + '✓ Server is running\n');
|
|
390
|
+
} catch (e) {
|
|
391
|
+
console.log(colors.red + `✗ Server is not running!`);
|
|
392
|
+
console.log(`\nStart the server with: npm run server\n` + colors.reset);
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Run all tests
|
|
397
|
+
try {
|
|
398
|
+
await testHealth();
|
|
399
|
+
await testSchema();
|
|
400
|
+
await testExecuteSingleCommand();
|
|
401
|
+
await testBatchCommands();
|
|
402
|
+
await testModelManagement();
|
|
403
|
+
await testHistory();
|
|
404
|
+
await testRateLimiting();
|
|
405
|
+
await testCORS();
|
|
406
|
+
await testCOOPCOEP();
|
|
407
|
+
await testSketchCommands();
|
|
408
|
+
await testOperationCommands();
|
|
409
|
+
await testViewCommands();
|
|
410
|
+
await testValidationCommands();
|
|
411
|
+
await testQueryCommands();
|
|
412
|
+
await testErrorHandling();
|
|
413
|
+
} catch (e) {
|
|
414
|
+
// Error already printed by assert()
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Summary
|
|
418
|
+
const total = passCount + failCount;
|
|
419
|
+
const percentage = total > 0 ? Math.round((passCount / total) * 100) : 0;
|
|
420
|
+
|
|
421
|
+
console.log('\n' + colors.blue + '='.repeat(60));
|
|
422
|
+
console.log(`${colors.green}✓ ${passCount} passed ${colors.reset}| ${colors.red}✗ ${failCount} failed${colors.reset}`);
|
|
423
|
+
console.log(`${percentage}% success rate (${passCount}/${total} tests)`);
|
|
424
|
+
console.log(colors.blue + '='.repeat(60) + colors.reset + '\n');
|
|
425
|
+
|
|
426
|
+
process.exit(failCount > 0 ? 1 : 0);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
main().catch(e => {
|
|
430
|
+
console.log(`${colors.red}✗ Test suite error: ${e.message}${colors.reset}`);
|
|
431
|
+
process.exit(1);
|
|
432
|
+
});
|
package/test-mcp.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test utility for cyclecad-mcp server
|
|
5
|
+
* Tests the MCP protocol without needing Claude API
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node test-mcp.js --help
|
|
9
|
+
* node test-mcp.js --list-tools
|
|
10
|
+
* node test-mcp.js --test-initialize
|
|
11
|
+
* node test-mcp.js --test-tool sketch_rect --args '{"width": 50, "height": 30}'
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { spawn } = require('child_process');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
|
|
19
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
20
|
+
console.log(`
|
|
21
|
+
Test utility for cyclecad-mcp server
|
|
22
|
+
|
|
23
|
+
Usage: node test-mcp.js [OPTIONS]
|
|
24
|
+
|
|
25
|
+
OPTIONS:
|
|
26
|
+
--help Show this help message
|
|
27
|
+
--list-tools List all available tools
|
|
28
|
+
--test-initialize Test server initialization
|
|
29
|
+
--test-tool NAME Test a specific tool
|
|
30
|
+
--args JSON Arguments for the tool (as JSON)
|
|
31
|
+
--debug Show debug output
|
|
32
|
+
|
|
33
|
+
EXAMPLES:
|
|
34
|
+
node test-mcp.js --list-tools
|
|
35
|
+
node test-mcp.js --test-initialize
|
|
36
|
+
node test-mcp.js --test-tool sketch_rect --args '{"width": 50, "height": 30}'
|
|
37
|
+
node test-mcp.js --test-tool ops_primitive --args '{"shape": "sphere", "radius": 10}'
|
|
38
|
+
node test-mcp.js --test-tool query_materials
|
|
39
|
+
node test-mcp.js --test-tool validate_dimensions --args '{"target": "extrude_1"}'
|
|
40
|
+
|
|
41
|
+
`);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const debug = args.includes('--debug');
|
|
46
|
+
|
|
47
|
+
async function runTest() {
|
|
48
|
+
const mcpProcess = spawn('node', [path.join(__dirname, 'server', 'mcp-server.js')], {
|
|
49
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
let output = '';
|
|
53
|
+
let errorOutput = '';
|
|
54
|
+
|
|
55
|
+
mcpProcess.stdout.on('data', (data) => {
|
|
56
|
+
output += data.toString();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
mcpProcess.stderr.on('data', (data) => {
|
|
60
|
+
errorOutput += data.toString();
|
|
61
|
+
if (debug) console.error('[MCP]', data.toString().trim());
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
mcpProcess.on('error', (err) => {
|
|
65
|
+
console.error('Failed to start MCP server:', err);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Wait a bit for server to be ready
|
|
70
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
71
|
+
|
|
72
|
+
async function send(req) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const timeout = setTimeout(() => {
|
|
75
|
+
mcpProcess.kill();
|
|
76
|
+
reject(new Error('No response from MCP server (timeout)'));
|
|
77
|
+
}, 5000);
|
|
78
|
+
|
|
79
|
+
const lines = [];
|
|
80
|
+
const dataHandler = (data) => {
|
|
81
|
+
const text = data.toString();
|
|
82
|
+
lines.push(text);
|
|
83
|
+
try {
|
|
84
|
+
const combined = lines.join('');
|
|
85
|
+
if (combined.includes('{')) {
|
|
86
|
+
const json = JSON.parse(combined.trim());
|
|
87
|
+
clearTimeout(timeout);
|
|
88
|
+
mcpProcess.stdout.removeListener('data', dataHandler);
|
|
89
|
+
resolve(json);
|
|
90
|
+
}
|
|
91
|
+
} catch (e) {
|
|
92
|
+
// Not complete JSON yet
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
mcpProcess.stdout.on('data', dataHandler);
|
|
97
|
+
mcpProcess.stdin.write(JSON.stringify(req) + '\n');
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Initialize
|
|
103
|
+
if (debug) console.log('Sending initialize request...');
|
|
104
|
+
const initResp = await send({ jsonrpc: '2.0', id: 1, method: 'initialize' });
|
|
105
|
+
if (debug) console.log('Initialize response:', JSON.stringify(initResp, null, 2));
|
|
106
|
+
|
|
107
|
+
if (args.includes('--list-tools')) {
|
|
108
|
+
// Get tools list
|
|
109
|
+
if (debug) console.log('\nSending tools/list request...');
|
|
110
|
+
const listResp = await send({ jsonrpc: '2.0', id: 2, method: 'tools/list' });
|
|
111
|
+
const tools = listResp.result.tools;
|
|
112
|
+
console.log(`\nAvailable Tools (${tools.length}):`);
|
|
113
|
+
console.log('=' .repeat(70));
|
|
114
|
+
|
|
115
|
+
// Group by namespace
|
|
116
|
+
const byNamespace = {};
|
|
117
|
+
tools.forEach(t => {
|
|
118
|
+
const ns = t.name.split('_')[0];
|
|
119
|
+
if (!byNamespace[ns]) byNamespace[ns] = [];
|
|
120
|
+
byNamespace[ns].push(t.name);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
Object.keys(byNamespace).sort().forEach(ns => {
|
|
124
|
+
console.log(`\n${ns.toUpperCase()} (${byNamespace[ns].length} tools)`);
|
|
125
|
+
byNamespace[ns].forEach(name => {
|
|
126
|
+
const tool = tools.find(t => t.name === name);
|
|
127
|
+
const desc = tool.description || 'No description';
|
|
128
|
+
console.log(` • ${name}`);
|
|
129
|
+
console.log(` ${desc}`);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
console.log('\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (args.includes('--test-initialize')) {
|
|
136
|
+
console.log('\nInitialization successful!');
|
|
137
|
+
console.log('Server info:', initResp.result.serverInfo);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (args.includes('--test-tool')) {
|
|
141
|
+
const toolIdx = args.indexOf('--test-tool');
|
|
142
|
+
const toolName = args[toolIdx + 1];
|
|
143
|
+
if (!toolName) {
|
|
144
|
+
console.error('Missing tool name after --test-tool');
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const argsIdx = args.indexOf('--args');
|
|
149
|
+
let toolArgs = {};
|
|
150
|
+
if (argsIdx !== -1 && args[argsIdx + 1]) {
|
|
151
|
+
try {
|
|
152
|
+
toolArgs = JSON.parse(args[argsIdx + 1]);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.error('Invalid JSON in --args:', e.message);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(`\nTesting tool: ${toolName}`);
|
|
160
|
+
if (Object.keys(toolArgs).length > 0) {
|
|
161
|
+
console.log('Arguments:', JSON.stringify(toolArgs, null, 2));
|
|
162
|
+
} else {
|
|
163
|
+
console.log('Arguments: (none)');
|
|
164
|
+
}
|
|
165
|
+
console.log('');
|
|
166
|
+
|
|
167
|
+
const callResp = await send({
|
|
168
|
+
jsonrpc: '2.0',
|
|
169
|
+
id: 3,
|
|
170
|
+
method: 'tools/call',
|
|
171
|
+
params: {
|
|
172
|
+
name: toolName,
|
|
173
|
+
arguments: toolArgs
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (callResp.error) {
|
|
178
|
+
console.error('Tool call failed:', callResp.error.message);
|
|
179
|
+
} else {
|
|
180
|
+
console.log('Tool call successful!');
|
|
181
|
+
console.log('Result:', callResp.result.content[0].text);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
} catch (e) {
|
|
186
|
+
console.error('Test failed:', e.message);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
} finally {
|
|
189
|
+
mcpProcess.kill();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
runTest().then(() => {
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}).catch(e => {
|
|
196
|
+
console.error('Unexpected error:', e.message);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
});
|
|
Binary file
|