@endiagram/mcp 0.2.37 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +31 -0
  2. package/dist/index.js +93 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -104,6 +104,37 @@ waiter do: deliver needs: meal yields: served customer
104
104
 
105
105
  Learn more at [endiagram.com](https://endiagram.com).
106
106
 
107
+ ## Telemetry
108
+
109
+ `@endiagram/mcp` generates a random install ID on first run, stored at
110
+ `~/.endiagram/install-id` (mode `0600`). It is sent with every request as
111
+ the `X-Endiagram-Install-Id` HTTP header so we can correlate requests
112
+ from the same install for debugging issues that the per-IP signal alone
113
+ cannot track (mobile networks, VPNs, CGNAT all collapse or churn IPs).
114
+
115
+ **No source code, no file paths, no environment variables, and no PII
116
+ are sent.** The install ID is a random opaque UUIDv4 generated locally.
117
+
118
+ A first-run notice prints to **stderr** (never stdout — stdout is the
119
+ MCP JSON-RPC channel) with the disclosure and the opt-out instructions.
120
+ The notice fires once per install and never again.
121
+
122
+ ### Opting out
123
+
124
+ Any of these three methods disables the install ID:
125
+
126
+ 1. Set `ENDIAGRAM_TELEMETRY=off` as an environment variable (also
127
+ accepts `0`, `false`, `no`).
128
+ 2. Create a file at `~/.endiagram/telemetry` containing the word `off`.
129
+ 3. Delete `~/.endiagram/install-id`. (A new one is generated on next
130
+ run unless option 1 or 2 is also set.)
131
+
132
+ When any of these is active, the `X-Endiagram-Install-Id` header is not
133
+ sent at all — the server falls back to its per-IP HMAC `cid` for
134
+ correlation, which works fine for short-term per-session tracing.
135
+
136
+ Full privacy policy: [endiagram.com/privacy](https://endiagram.com/privacy)
137
+
107
138
  ## License
108
139
 
109
140
  MIT
package/dist/index.js CHANGED
@@ -2,12 +2,95 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
5
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "node:fs";
6
6
  import { join, dirname, resolve } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { homedir } from "node:os";
9
+ import { randomUUID } from "node:crypto";
8
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
11
  const toolsConfig = JSON.parse(readFileSync(join(__dirname, "../tools.json"), "utf-8"));
12
+ const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
10
13
  const EN_API_URL = process.env.EN_API_URL ?? "https://api.endiagram.com";
14
+ // ─────────────────────────────────────────────────────────────────────
15
+ // Install ID — persistent, opt-out by env var or file
16
+ //
17
+ // On first run, generate a UUIDv4 and store it at ~/.endiagram/install-id
18
+ // (chmod 0600). Sent on every request as the X-Endiagram-Install-Id header
19
+ // so the server can correlate requests from the same install for debugging
20
+ // per-installation issues that the per-IP cid HMAC can't track (mobile
21
+ // rotation, CGNAT, VPN).
22
+ //
23
+ // No source code, no file paths, no environment variables, no PII are
24
+ // sent. The install ID is a random opaque UUID generated locally.
25
+ //
26
+ // Three opt-out mechanisms (any one disables the header):
27
+ // 1. ENDIAGRAM_TELEMETRY=off (env var, also accepts 0/false/no)
28
+ // 2. ~/.endiagram/telemetry (file containing "off")
29
+ // 3. delete ~/.endiagram/install-id (regenerated unless 1 or 2 set)
30
+ // ─────────────────────────────────────────────────────────────────────
31
+ const UUIDV4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
32
+ function resolveInstallId() {
33
+ // Tier 1: env var opt-out
34
+ const envFlag = process.env.ENDIAGRAM_TELEMETRY?.trim().toLowerCase();
35
+ if (envFlag && /^(off|0|false|no)$/.test(envFlag)) {
36
+ return null;
37
+ }
38
+ const stateDir = join(homedir(), ".endiagram");
39
+ const telemetryFile = join(stateDir, "telemetry");
40
+ const installFile = join(stateDir, "install-id");
41
+ // Tier 2: disk flag opt-out
42
+ if (existsSync(telemetryFile)) {
43
+ try {
44
+ const flag = readFileSync(telemetryFile, "utf-8").trim().toLowerCase();
45
+ if (/^(off|0|false|no)$/.test(flag))
46
+ return null;
47
+ }
48
+ catch {
49
+ // unreadable telemetry file — fall through, prefer enabled state
50
+ }
51
+ }
52
+ // Read existing install-id
53
+ if (existsSync(installFile)) {
54
+ try {
55
+ const existing = readFileSync(installFile, "utf-8").trim().toLowerCase();
56
+ if (UUIDV4_REGEX.test(existing))
57
+ return existing;
58
+ // malformed — fall through to regenerate
59
+ }
60
+ catch {
61
+ // unreadable — fall through to regenerate
62
+ }
63
+ }
64
+ // First run (or corrupted file): generate, persist, print notice
65
+ const fresh = randomUUID();
66
+ try {
67
+ mkdirSync(stateDir, { recursive: true });
68
+ writeFileSync(installFile, fresh, { encoding: "utf-8" });
69
+ // Restrict to owner rw only — install ID is effectively a stable
70
+ // pseudonymous identifier; treat it like a low-sensitivity secret.
71
+ try {
72
+ chmodSync(installFile, 0o600);
73
+ }
74
+ catch {
75
+ // Some filesystems don't support POSIX perms — best effort.
76
+ }
77
+ // First-run disclosure to STDERR (never stdout — stdout is the
78
+ // MCP JSON-RPC channel and any bytes there corrupt Claude Desktop).
79
+ process.stderr.write(`[endiagram] First run: generated install ID at ${installFile}\n` +
80
+ `[endiagram] This ID is sent with requests so we can correlate per\n` +
81
+ ` installation for debugging. No source code, file paths,\n` +
82
+ ` env vars, or PII are sent.\n` +
83
+ ` Opt out: ENDIAGRAM_TELEMETRY=off (or delete the file)\n` +
84
+ ` Privacy: https://endiagram.com/privacy\n`);
85
+ }
86
+ catch {
87
+ // Could not persist (read-only home dir, etc.) — return the
88
+ // generated id anyway so the current process still gets correlation
89
+ // within its own lifetime. Next run will try again.
90
+ }
91
+ return fresh;
92
+ }
93
+ const INSTALL_ID = resolveInstallId();
11
94
  /**
12
95
  * Resolve the `source` parameter: if it looks like a file path (.en, .txt,
13
96
  * or starts with / or ~), read the file and return its contents.
@@ -30,9 +113,16 @@ function resolveSource(source) {
30
113
  }
31
114
  async function callApi(toolName, args) {
32
115
  try {
116
+ const headers = {
117
+ "Content-Type": "application/json",
118
+ "User-Agent": `endiagram-mcp/${pkg.version} node/${process.version.replace(/^v/, "")}`,
119
+ };
120
+ if (INSTALL_ID) {
121
+ headers["X-Endiagram-Install-Id"] = INSTALL_ID;
122
+ }
33
123
  const response = await fetch(`${EN_API_URL}/mcp`, {
34
124
  method: "POST",
35
- headers: { "Content-Type": "application/json" },
125
+ headers,
36
126
  body: JSON.stringify({
37
127
  jsonrpc: "2.0",
38
128
  id: Date.now(),
@@ -76,7 +166,7 @@ async function callApi(toolName, args) {
76
166
  }
77
167
  }
78
168
  const EN_INSTRUCTIONS = toolsConfig.instructions;
79
- const server = new McpServer({ name: "endiagram", version: "0.2.0" }, { instructions: EN_INSTRUCTIONS });
169
+ const server = new McpServer({ name: "endiagram", version: pkg.version }, { instructions: EN_INSTRUCTIONS });
80
170
  for (const tool of toolsConfig.tools) {
81
171
  const schemaProps = {};
82
172
  for (const param of tool.parameters) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@endiagram/mcp",
3
- "version": "0.2.37",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for EN Diagram — deterministic structural analysis backed by named theorems",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",