@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.
- package/README.md +31 -0
- package/dist/index.js +93 -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
|
|
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:
|
|
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) {
|