forma-dsl-mcp 0.3.1 → 0.3.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/README.md +16 -0
- package/package.json +2 -2
- package/src/tools.js +25 -6
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/kalpak44/forma-dsl/actions/workflows/release.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/forma-dsl-mcp)
|
|
5
|
+
[](https://m8ven.ai/verified/verify?id=d3529b4756004447)
|
|
5
6
|
|
|
6
7
|
A [Model Context Protocol](https://modelcontextprotocol.io) server that knows
|
|
7
8
|
[forma](https://www.npmjs.com/package/forma-dsl) — the declarative DSL for 3D solids — and
|
|
@@ -234,6 +235,21 @@ node packages/forma-dsl-mcp/src/bin.js # the server, on stdio
|
|
|
234
235
|
Point a client at `node /absolute/path/to/packages/forma-dsl-mcp/src/bin.js` to use the
|
|
235
236
|
working tree rather than the published package.
|
|
236
237
|
|
|
238
|
+
## Privacy
|
|
239
|
+
|
|
240
|
+
This server makes **no network requests**. It imports no HTTP, socket or DNS module and calls
|
|
241
|
+
no `fetch`; the geometry kernel is WebAssembly loaded from disk and the manual is bundled in
|
|
242
|
+
this tarball, so it works with no network at all. There is no telemetry, no analytics, no
|
|
243
|
+
update check and no account.
|
|
244
|
+
|
|
245
|
+
It writes only inside the directories your client declares as MCP roots, never replaces a file
|
|
246
|
+
unless told to, and leaves nothing elsewhere — no cache, no config, no logs.
|
|
247
|
+
|
|
248
|
+
Your MCP client is a separate program with its own policy. Documents you ask a model to write
|
|
249
|
+
travel wherever that client sends them, which this server neither controls nor sees.
|
|
250
|
+
|
|
251
|
+
Full text: [PRIVACY.md](https://github.com/kalpak44/forma-dsl/blob/main/PRIVACY.md).
|
|
252
|
+
|
|
237
253
|
## License
|
|
238
254
|
|
|
239
255
|
MIT — see [LICENSE](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forma-dsl-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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.3.
|
|
49
|
+
"forma-dsl": "^0.3.3",
|
|
50
50
|
"zod": "^4.1.13"
|
|
51
51
|
}
|
|
52
52
|
}
|
package/src/tools.js
CHANGED
|
@@ -17,6 +17,25 @@ import { GUIDE_SECTIONS, guide } from './guide.js';
|
|
|
17
17
|
import { PAGES, readPage, searchReference } from './reference.js';
|
|
18
18
|
import { checkDocument, exportStl } from './document.js';
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* What a tool that only reads what this package already carries is.
|
|
22
|
+
*
|
|
23
|
+
* All four hints are stated rather than just `readOnlyHint`, because the omitted ones are not
|
|
24
|
+
* absent: the specification gives `destructiveHint` a default of **true** and `idempotentHint`
|
|
25
|
+
* a default of **false**. A tool declaring neither is therefore advertising itself as
|
|
26
|
+
* destructive and unrepeatable — which, for a call that hands back a page of the manual, is
|
|
27
|
+
* simply untrue, and a cautious client is entitled to act on it.
|
|
28
|
+
*
|
|
29
|
+
* `openWorldHint` is false because every answer is bundled with the package. Nothing here
|
|
30
|
+
* reaches the network, so the domain is closed and stays closed offline.
|
|
31
|
+
*/
|
|
32
|
+
const READS_ONLY = Object.freeze({
|
|
33
|
+
readOnlyHint: true,
|
|
34
|
+
destructiveHint: false,
|
|
35
|
+
idempotentHint: true,
|
|
36
|
+
openWorldHint: false,
|
|
37
|
+
});
|
|
38
|
+
|
|
20
39
|
/**
|
|
21
40
|
* Wraps text as a tool result.
|
|
22
41
|
*
|
|
@@ -228,7 +247,7 @@ function registerGuide(server) {
|
|
|
228
247
|
section: z.enum(/** @type {[string, ...string[]]} */ (GUIDE_SECTIONS)).optional()
|
|
229
248
|
.describe('One section, instead of the whole guide.'),
|
|
230
249
|
},
|
|
231
|
-
annotations:
|
|
250
|
+
annotations: READS_ONLY,
|
|
232
251
|
}, async ({ section }) => attempt(async () => say(guide(section))));
|
|
233
252
|
}
|
|
234
253
|
|
|
@@ -256,7 +275,7 @@ function registerBlocks(server) {
|
|
|
256
275
|
group: z.string().optional()
|
|
257
276
|
.describe('One group, e.g. "2D shapes", "Transforms", "Refinement", "Declarations".'),
|
|
258
277
|
},
|
|
259
|
-
annotations:
|
|
278
|
+
annotations: READS_ONLY,
|
|
260
279
|
}, async ({ names, group }) => attempt(async () => {
|
|
261
280
|
const all = [...CONSTRUCTS.values()];
|
|
262
281
|
|
|
@@ -312,7 +331,7 @@ function registerFunctions(server) {
|
|
|
312
331
|
'Every function an expression may call, with its arity, and the two named constants. '
|
|
313
332
|
+ 'There are no user-defined functions in forma, so this is the complete set.',
|
|
314
333
|
inputSchema: {},
|
|
315
|
-
annotations:
|
|
334
|
+
annotations: READS_ONLY,
|
|
316
335
|
}, async () => attempt(async () => say([
|
|
317
336
|
'# Functions',
|
|
318
337
|
'',
|
|
@@ -350,7 +369,7 @@ function registerReference(server) {
|
|
|
350
369
|
query: z.string().optional().describe('Words to search for, e.g. "revolve profile axis".'),
|
|
351
370
|
page: z.string().optional().describe('One page to read, e.g. "reference/conversions.md".'),
|
|
352
371
|
},
|
|
353
|
-
annotations:
|
|
372
|
+
annotations: READS_ONLY,
|
|
354
373
|
}, async ({ query, page }) => attempt(async () => {
|
|
355
374
|
if (page) {
|
|
356
375
|
const found = await readPage(page);
|
|
@@ -400,7 +419,7 @@ function registerExamples(server) {
|
|
|
400
419
|
inputSchema: {
|
|
401
420
|
name: z.string().optional().describe('One example to read in full.'),
|
|
402
421
|
},
|
|
403
|
-
annotations:
|
|
422
|
+
annotations: READS_ONLY,
|
|
404
423
|
}, async ({ name }) => attempt(async () => {
|
|
405
424
|
if (name) {
|
|
406
425
|
const example = EXAMPLES_BY_NAME.get(name);
|
|
@@ -505,7 +524,7 @@ function registerRead(server, workspace) {
|
|
|
505
524
|
solve: z.boolean().optional()
|
|
506
525
|
.describe('Run the geometry kernel. Default true; false parses only, which is faster.'),
|
|
507
526
|
},
|
|
508
|
-
annotations:
|
|
527
|
+
annotations: READS_ONLY,
|
|
509
528
|
}, async ({ path, solve }) => attempt(async () => {
|
|
510
529
|
const source = await workspace.read(path);
|
|
511
530
|
const report = await checkDocument(source, { solve: solve ?? true });
|