forma-dsl-mcp 0.2.2 → 0.3.1
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/package.json +2 -2
- package/src/document.js +15 -2
- package/src/tools.js +109 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forma-dsl-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "A stdio MCP server that knows the forma DSL and writes .forma documents that render.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
49
|
-
"forma-dsl": "^0.
|
|
49
|
+
"forma-dsl": "^0.3.1",
|
|
50
50
|
"zod": "^4.1.13"
|
|
51
51
|
}
|
|
52
52
|
}
|
package/src/document.js
CHANGED
|
@@ -20,6 +20,19 @@ import {
|
|
|
20
20
|
/** How many lines of source to show either side of an error. */
|
|
21
21
|
const CONTEXT_LINES = 2;
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* How much geometry one document may build before the server refuses it.
|
|
25
|
+
*
|
|
26
|
+
* This server exists to build text a model wrote, which is the case the library leaves a
|
|
27
|
+
* budget off for. Nesting limits and `range` each bound one loop; only this bounds two of
|
|
28
|
+
* them nested, and that shape asks for more geometry than the process can finish — on a pipe,
|
|
29
|
+
* with no request timeout of its own, that is a server that never answers again.
|
|
30
|
+
*
|
|
31
|
+
* Set far above any honest document: the worked examples build tens of blocks. A model that
|
|
32
|
+
* reaches this has written a loop it did not mean to.
|
|
33
|
+
*/
|
|
34
|
+
const MAX_NODES = 50_000;
|
|
35
|
+
|
|
23
36
|
/**
|
|
24
37
|
* The context reused across calls, so an edit-and-recheck loop pays only for the subtrees
|
|
25
38
|
* that changed.
|
|
@@ -195,7 +208,7 @@ export async function checkDocument(source, options = {}) {
|
|
|
195
208
|
/** @type {RenderResult} */
|
|
196
209
|
let result;
|
|
197
210
|
try {
|
|
198
|
-
result = await render(source, { model, params, context: shared });
|
|
211
|
+
result = await render(source, { model, params, context: shared, maxNodes: MAX_NODES });
|
|
199
212
|
} catch (error) {
|
|
200
213
|
return { ok: false, stage: 'render', ...declared, error: diagnose('render', error, source) };
|
|
201
214
|
}
|
|
@@ -255,7 +268,7 @@ export async function exportStl(source, options = {}) {
|
|
|
255
268
|
const { model = null, params = {}, part = null } = options;
|
|
256
269
|
|
|
257
270
|
shared ??= await EvaluationContext.create();
|
|
258
|
-
const result = await render(source, { model, params, context: shared });
|
|
271
|
+
const result = await render(source, { model, params, context: shared, maxNodes: MAX_NODES });
|
|
259
272
|
|
|
260
273
|
let solid;
|
|
261
274
|
let name;
|
package/src/tools.js
CHANGED
|
@@ -209,15 +209,14 @@ async function documentFrom(workspace, { source, path }) {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
/**
|
|
212
|
-
*
|
|
212
|
+
* The language in one page, or one section of it.
|
|
213
|
+
*
|
|
214
|
+
* Registered first because it is what the server's instructions tell a model to read first.
|
|
213
215
|
*
|
|
214
216
|
* @param {McpServer} server The server.
|
|
215
|
-
* @param {Workspace} workspace The patch of filesystem the tools may touch.
|
|
216
217
|
* @returns {void}
|
|
217
218
|
*/
|
|
218
|
-
|
|
219
|
-
// --- knowing the language ---------------------------------------------------------------
|
|
220
|
-
|
|
219
|
+
function registerGuide(server) {
|
|
221
220
|
server.registerTool('forma_guide', {
|
|
222
221
|
title: 'The forma language in brief',
|
|
223
222
|
description:
|
|
@@ -231,7 +230,18 @@ export function registerTools(server, workspace) {
|
|
|
231
230
|
},
|
|
232
231
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
233
232
|
}, async ({ section }) => attempt(async () => say(guide(section))));
|
|
233
|
+
}
|
|
234
234
|
|
|
235
|
+
/**
|
|
236
|
+
* What every block takes, read from the library's own registry.
|
|
237
|
+
*
|
|
238
|
+
* Names are answered in full rather than as a list, because a caller that has to guess at
|
|
239
|
+
* an attribute spends a build finding out it guessed wrong.
|
|
240
|
+
*
|
|
241
|
+
* @param {McpServer} server The server.
|
|
242
|
+
* @returns {void}
|
|
243
|
+
*/
|
|
244
|
+
function registerBlocks(server) {
|
|
235
245
|
server.registerTool('forma_blocks', {
|
|
236
246
|
title: 'Block and declaration reference',
|
|
237
247
|
description:
|
|
@@ -284,7 +294,18 @@ export function registerTools(server, workspace) {
|
|
|
284
294
|
lines.push('_Call again with `names` for the attribute tables._');
|
|
285
295
|
return say(lines.join('\n'));
|
|
286
296
|
}));
|
|
297
|
+
}
|
|
287
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Every function an expression may call, and the two named constants.
|
|
301
|
+
*
|
|
302
|
+
* There are no user-defined functions in forma, so this answer is complete rather than a
|
|
303
|
+
* sample — which is worth saying, because it is what stops a caller inventing one.
|
|
304
|
+
*
|
|
305
|
+
* @param {McpServer} server The server.
|
|
306
|
+
* @returns {void}
|
|
307
|
+
*/
|
|
308
|
+
function registerFunctions(server) {
|
|
288
309
|
server.registerTool('forma_functions', {
|
|
289
310
|
title: 'Builtin functions and constants',
|
|
290
311
|
description:
|
|
@@ -306,7 +327,18 @@ export function registerTools(server, workspace) {
|
|
|
306
327
|
'',
|
|
307
328
|
table(['Name', 'Value'], LANGUAGE_CONSTANTS.map((c) => [`\`${c.name}\``, c.value])),
|
|
308
329
|
].join('\n'))));
|
|
330
|
+
}
|
|
309
331
|
|
|
332
|
+
/**
|
|
333
|
+
* The bundled manual: search it, read a page, or list what there is.
|
|
334
|
+
*
|
|
335
|
+
* The manual ships with the package rather than being fetched, so the answers stay true to
|
|
336
|
+
* the version installed and work with no network at all.
|
|
337
|
+
*
|
|
338
|
+
* @param {McpServer} server The server.
|
|
339
|
+
* @returns {void}
|
|
340
|
+
*/
|
|
341
|
+
function registerReference(server) {
|
|
310
342
|
server.registerTool('forma_reference', {
|
|
311
343
|
title: 'The reference manual',
|
|
312
344
|
description:
|
|
@@ -347,7 +379,18 @@ export function registerTools(server, workspace) {
|
|
|
347
379
|
table(['Page', 'Title', 'About'], PAGES.map((each) => [`\`${each.path}\``, each.title, each.about])),
|
|
348
380
|
].join('\n'));
|
|
349
381
|
}));
|
|
382
|
+
}
|
|
350
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Complete documents that render, to read before writing something new.
|
|
386
|
+
*
|
|
387
|
+
* Every one is rendered by this package's tests at both ends of every parameter's range, so
|
|
388
|
+
* an example cannot teach a mistake without the build failing.
|
|
389
|
+
*
|
|
390
|
+
* @param {McpServer} server The server.
|
|
391
|
+
* @returns {void}
|
|
392
|
+
*/
|
|
393
|
+
function registerExamples(server) {
|
|
351
394
|
server.registerTool('forma_examples', {
|
|
352
395
|
title: 'Worked documents',
|
|
353
396
|
description:
|
|
@@ -384,7 +427,20 @@ export function registerTools(server, workspace) {
|
|
|
384
427
|
}));
|
|
385
428
|
|
|
386
429
|
// --- writing and building ----------------------------------------------------------------
|
|
430
|
+
}
|
|
387
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Build a document, and write it only if it built.
|
|
434
|
+
*
|
|
435
|
+
* The measurement is the point, not the file: a caller that is told the bounding box, the
|
|
436
|
+
* volume and the triangle count knows whether the model is the size that was asked for and
|
|
437
|
+
* has not silently solved to nothing. A refusal leaves nothing on disk.
|
|
438
|
+
*
|
|
439
|
+
* @param {McpServer} server The server.
|
|
440
|
+
* @param {Workspace} workspace The patch of filesystem the tool may touch.
|
|
441
|
+
* @returns {void}
|
|
442
|
+
*/
|
|
443
|
+
function registerWrite(server, workspace) {
|
|
388
444
|
server.registerTool('forma_write', {
|
|
389
445
|
title: 'Check, then write a .forma file',
|
|
390
446
|
description:
|
|
@@ -425,7 +481,19 @@ export function registerTools(server, workspace) {
|
|
|
425
481
|
formatReport(report),
|
|
426
482
|
].join('\n'));
|
|
427
483
|
}));
|
|
484
|
+
}
|
|
428
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Read a document back, with its current measurements.
|
|
488
|
+
*
|
|
489
|
+
* Source and measurements together, because editing something last touched in another
|
|
490
|
+
* session means needing both.
|
|
491
|
+
*
|
|
492
|
+
* @param {McpServer} server The server.
|
|
493
|
+
* @param {Workspace} workspace The patch of filesystem the tool may touch.
|
|
494
|
+
* @returns {void}
|
|
495
|
+
*/
|
|
496
|
+
function registerRead(server, workspace) {
|
|
429
497
|
server.registerTool('forma_read', {
|
|
430
498
|
title: 'Read a document back',
|
|
431
499
|
description:
|
|
@@ -443,7 +511,19 @@ export function registerTools(server, workspace) {
|
|
|
443
511
|
const report = await checkDocument(source, { solve: solve ?? true });
|
|
444
512
|
return say(['```hcl', source.trimEnd(), '```', '', formatReport(report)].join('\n'));
|
|
445
513
|
}));
|
|
514
|
+
}
|
|
446
515
|
|
|
516
|
+
/**
|
|
517
|
+
* Render a document and write one part, or every part unioned, as binary STL.
|
|
518
|
+
*
|
|
519
|
+
* Takes a document either as source or as a path already on disk, so a caller that has just
|
|
520
|
+
* written one does not have to hold it a second time.
|
|
521
|
+
*
|
|
522
|
+
* @param {McpServer} server The server.
|
|
523
|
+
* @param {Workspace} workspace The patch of filesystem the tool may touch.
|
|
524
|
+
* @returns {void}
|
|
525
|
+
*/
|
|
526
|
+
function registerExportStl(server, workspace) {
|
|
447
527
|
server.registerTool('forma_export_stl', {
|
|
448
528
|
title: 'Render and write a binary STL',
|
|
449
529
|
description:
|
|
@@ -481,3 +561,27 @@ export function registerTools(server, workspace) {
|
|
|
481
561
|
);
|
|
482
562
|
}));
|
|
483
563
|
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Registers every tool on a server.
|
|
567
|
+
*
|
|
568
|
+
* One function per tool, so this reads as the list of what the server offers and each tool's
|
|
569
|
+
* schema, description and handler stay together where they are edited.
|
|
570
|
+
*
|
|
571
|
+
* @param {McpServer} server The server.
|
|
572
|
+
* @param {Workspace} workspace The patch of filesystem the file tools may touch.
|
|
573
|
+
* @returns {void}
|
|
574
|
+
*/
|
|
575
|
+
export function registerTools(server, workspace) {
|
|
576
|
+
// Knowing the language.
|
|
577
|
+
registerGuide(server);
|
|
578
|
+
registerBlocks(server);
|
|
579
|
+
registerFunctions(server);
|
|
580
|
+
registerReference(server);
|
|
581
|
+
registerExamples(server);
|
|
582
|
+
|
|
583
|
+
// Writing and building.
|
|
584
|
+
registerWrite(server, workspace);
|
|
585
|
+
registerRead(server, workspace);
|
|
586
|
+
registerExportStl(server, workspace);
|
|
587
|
+
}
|