forma-dsl-mcp 0.3.1 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +16 -0
  2. package/package.json +2 -2
  3. package/src/tools.js +25 -6
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![Release](https://github.com/kalpak44/forma-dsl/actions/workflows/release.yml/badge.svg)](https://github.com/kalpak44/forma-dsl/actions/workflows/release.yml)
4
4
  [![npm](https://img.shields.io/npm/v/forma-dsl-mcp.svg)](https://www.npmjs.com/package/forma-dsl-mcp)
5
+ [![M8ven Verified](https://m8ven.ai/api/agent-verify/badge?score=100)](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.1",
3
+ "version": "0.3.2",
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.1",
49
+ "forma-dsl": "^0.3.2",
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: { readOnlyHint: true, openWorldHint: false },
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: { readOnlyHint: true, openWorldHint: false },
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: { readOnlyHint: true, openWorldHint: false },
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: { readOnlyHint: true, openWorldHint: false },
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: { readOnlyHint: true, openWorldHint: false },
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: { readOnlyHint: true, openWorldHint: false },
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 });