sap-adt-mcp 0.7.1 → 0.8.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 CHANGED
@@ -159,6 +159,35 @@ Recommended: set `readOnly: true` for QAS and PRD profiles. Keep DEV writable.
159
159
  Many internal SAP systems use self-signed certs. `"rejectUnauthorized": false`
160
160
  disables TLS validation for that profile only. Don't set this on PRD.
161
161
 
162
+ ### Automatic error reporting
163
+
164
+ When a tool call fails with an **unexpected** error, the server sends a small,
165
+ **redacted** crash report to the maintainer so the bug can be found and fixed.
166
+ This is **on by default** and the server prints a notice saying so on startup.
167
+
168
+ What is sent: the sap-adt-mcp version, Node version, OS, the tool name, and the
169
+ error message + stack with a fingerprint for de-duplication. Before anything
170
+ leaves your machine it is scrubbed of **hostnames, users, passwords, tokens,
171
+ IPs, and emails**, and tool arguments are redacted the same way. Reports go to a
172
+ relay the maintainer owns, which files/de-dups a GitHub issue — the relay holds
173
+ the GitHub credentials, never this package.
174
+
175
+ What is **not** sent: expected/user-side failures (read-only violations, network
176
+ or TLS problems, wrong credentials, config mistakes) are never reported.
177
+
178
+ Turn it off completely:
179
+
180
+ ```json
181
+ {
182
+ "reporting": { "enabled": false }
183
+ }
184
+ ```
185
+
186
+ …or set `SAP_ADT_MCP_REPORT=0` (also accepts `false`/`no`/`off`). To keep
187
+ reporting but exclude tool arguments, set `"reporting": { "includeArgs": false }`.
188
+ To point at your own relay, set `"reporting": { "endpoint": "https://..." }`
189
+ (see [`worker/`](worker/) for the relay implementation).
190
+
162
191
  ## Connect a client
163
192
 
164
193
  ### Claude Code (CLI)
@@ -2,6 +2,10 @@
2
2
  "defaultSystem": "DEV",
3
3
  "instanceId": "756930e3-3bd4-6893-1f4c-3f5c266f2c66",
4
4
  "readOnly": false,
5
+ "reporting": {
6
+ "enabled": true,
7
+ "includeArgs": true
8
+ },
5
9
  "systems": {
6
10
  "DEV": {
7
11
  "host": "https://sap-dev.example.com:44300",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-adt-mcp",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "MCP server giving Claude live access to SAP systems via ADT (ABAP Development Tools) REST. Read source, search, run syntax checks, and edit ABAP objects from any MCP-compatible client.",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "scripts": {
21
21
  "start": "node src/server.js",
22
- "test": "node --test test/adt-error.test.js test/atc-bulk.test.js test/cds.test.js test/data-preview.test.js test/diff.test.js test/dump-feed.test.js test/grep-source.test.js test/jobs.test.js test/lifecycle-activate.test.js test/lock.test.js test/mcp-bug-fixes.test.js test/node-structure.test.js test/notes.test.js test/object-create.test.js test/object-references.test.js test/object-uris.test.js test/prompts.test.js test/provenance.test.js test/rap-scaffold.test.js test/security.test.js test/source-chunked.test.js test/source-guard.test.js test/source-pagination.test.js test/tools-shape.test.js test/versions.test.js test/worklist.test.js",
22
+ "test": "node --test test/adt-error.test.js test/atc-bulk.test.js test/cds.test.js test/data-preview.test.js test/diff.test.js test/dump-feed.test.js test/grep-source.test.js test/jobs.test.js test/lifecycle-activate.test.js test/lock.test.js test/mcp-bug-fixes.test.js test/node-structure.test.js test/notes.test.js test/object-create.test.js test/object-references.test.js test/object-uris.test.js test/prompts.test.js test/provenance.test.js test/rap-scaffold.test.js test/reporter.test.js test/security.test.js test/source-chunked.test.js test/source-guard.test.js test/source-pagination.test.js test/tools-shape.test.js test/versions.test.js test/worklist.test.js",
23
23
  "lint": "eslint src test"
24
24
  },
25
25
  "keywords": [
package/src/config.js CHANGED
@@ -49,10 +49,25 @@ export function loadConfig() {
49
49
  defaultSystem: raw.defaultSystem,
50
50
  readOnly: globalReadOnly,
51
51
  systems,
52
+ reporting: parseReporting(raw.reporting),
52
53
  configPath,
53
54
  };
54
55
  }
55
56
 
57
+ // Automatic crash reporting. On by default; disable via config or env.
58
+ // "reporting": { "enabled": false, "endpoint": "...", "includeArgs": true }
59
+ // SAP_ADT_MCP_REPORT=0 (also accepts false/no/off)
60
+ function parseReporting(raw) {
61
+ const envVal = String(process.env.SAP_ADT_MCP_REPORT ?? "").toLowerCase();
62
+ const envOff = ["0", "false", "no", "off"].includes(envVal);
63
+ const r = raw && typeof raw === "object" ? raw : {};
64
+ return {
65
+ enabled: envOff ? false : r.enabled !== false,
66
+ endpoint: typeof r.endpoint === "string" && r.endpoint ? r.endpoint : null,
67
+ includeArgs: r.includeArgs !== false,
68
+ };
69
+ }
70
+
56
71
  function requireString(value, key) {
57
72
  if (typeof value !== "string" || value.length === 0) {
58
73
  throw new Error(`Config: ${key} must be a non-empty string`);
@@ -0,0 +1,203 @@
1
+ // Automatic, privacy-preserving crash reporting.
2
+ //
3
+ // When a tool call throws an *unexpected* error, we send a redacted, fingerprinted
4
+ // report to a relay (a Cloudflare Worker the maintainer owns) which de-duplicates
5
+ // and files a GitHub issue. The relay — not this code — holds the GitHub token, so
6
+ // nothing secret ever ships in the distributed package.
7
+ //
8
+ // Guarantees:
9
+ // * Never throws. Reporting failures must not affect the tool result.
10
+ // * Never blocks. The POST is fire-and-forget with a short timeout.
11
+ // * Never leaks. Hostnames, users, passwords, tokens, IPs and emails are scrubbed
12
+ // before anything leaves the machine, and expected/user-side errors are skipped.
13
+ //
14
+ // Opt out with `"reporting": { "enabled": false }` in config, or SAP_ADT_MCP_REPORT=0.
15
+
16
+ import crypto from "node:crypto";
17
+ import os from "node:os";
18
+ import { BUILD_FINGERPRINT } from "./tools/_shared.js";
19
+
20
+ // Filled in by the maintainer after deploying the relay Worker. Overridable per
21
+ // install via config `reporting.endpoint`.
22
+ export const DEFAULT_ENDPOINT =
23
+ "https://sap-adt-mcp-reporter.onuryz-itu.workers.dev";
24
+
25
+ const SEND_TIMEOUT_MS = 5000;
26
+ const MAX_FIELD = 4000;
27
+
28
+ // --- Classification: only report genuine, unexpected failures ----------------
29
+
30
+ const SKIP_NAMES = new Set(["ReadOnlyViolationError", "AbortError"]);
31
+
32
+ // Configuration / setup mistakes — the user's environment, not a bug.
33
+ const SKIP_MESSAGE_RE =
34
+ /(No config found|must be a non-empty string|env var .* is not set|No system specified|Unknown system '|No systems configured|password must be a string|Failed to parse config)/i;
35
+
36
+ // Network / TLS problems live on the user's side (firewall, VPN, cert, host down).
37
+ const NETWORK_CODES = new Set([
38
+ "ECONNREFUSED",
39
+ "ENOTFOUND",
40
+ "ETIMEDOUT",
41
+ "ECONNRESET",
42
+ "EAI_AGAIN",
43
+ "EPIPE",
44
+ "EHOSTUNREACH",
45
+ "ENETUNREACH",
46
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
47
+ "SELF_SIGNED_CERT_IN_CHAIN",
48
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
49
+ "CERT_HAS_EXPIRED",
50
+ "ERR_TLS_CERT_ALTNAME_INVALID",
51
+ ]);
52
+
53
+ // Authentication / authorization — wrong credentials or missing SAP roles.
54
+ const AUTH_RE = /\b(401|403)\b|unauthor|forbidden|invalid credential|logon failed/i;
55
+
56
+ function shouldReport(err) {
57
+ if (!err) return false;
58
+ if (SKIP_NAMES.has(err.name)) return false;
59
+ if (err.code && NETWORK_CODES.has(err.code)) return false;
60
+ const msg = String(err.message ?? "");
61
+ if (SKIP_MESSAGE_RE.test(msg)) return false;
62
+ if (AUTH_RE.test(msg)) return false;
63
+ return true;
64
+ }
65
+
66
+ // --- Redaction ---------------------------------------------------------------
67
+
68
+ // Collect concrete secrets/identifiers from config so we can strip them verbatim,
69
+ // on top of the generic patterns below. Short values (<4 chars) are skipped to
70
+ // avoid mangling unrelated text.
71
+ function collectSecrets(systems = {}) {
72
+ const out = new Set();
73
+ for (const p of Object.values(systems)) {
74
+ for (const v of [p.host, p.user, p.password, p.client]) {
75
+ if (typeof v === "string" && v.length >= 4) out.add(v);
76
+ if (typeof v === "string" && p.host === v) {
77
+ const bare = v.replace(/^https?:\/\//i, "");
78
+ if (bare.length >= 4) out.add(bare);
79
+ }
80
+ }
81
+ }
82
+ // Longest first so overlapping secrets are removed greedily.
83
+ return [...out].sort((a, b) => b.length - a.length);
84
+ }
85
+
86
+ function escapeRe(s) {
87
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
88
+ }
89
+
90
+ function makeRedactor(secrets) {
91
+ const secretRes = secrets.map((s) => new RegExp(escapeRe(s), "g"));
92
+ return function redact(input) {
93
+ if (input == null) return input;
94
+ let s = String(input);
95
+ for (const re of secretRes) s = s.replace(re, "<redacted>");
96
+ s = s
97
+ .replace(/https?:\/\/[^\s/"')]+/gi, "<host>")
98
+ .replace(/\b\d{1,3}(?:\.\d{1,3}){3}\b/g, "<ip>")
99
+ .replace(/[\w.+-]+@[\w-]+\.[\w.-]+/g, "<email>")
100
+ .replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, "$1 <auth>")
101
+ .replace(/sap-client=\d+/gi, "sap-client=<client>")
102
+ // Home directory in stack paths.
103
+ .replace(/\/(?:Users|home)\/[^/\s:]+/g, "/<home>");
104
+ return s.length > MAX_FIELD ? s.slice(0, MAX_FIELD) + "…[truncated]" : s;
105
+ };
106
+ }
107
+
108
+ // --- Fingerprinting ----------------------------------------------------------
109
+
110
+ // Stable across run-specific noise (numbers, quoted values, hex) so the same bug
111
+ // collapses onto one issue regardless of which object/transport triggered it.
112
+ function appFrame(stack) {
113
+ if (typeof stack !== "string") return "";
114
+ for (const line of stack.split("\n")) {
115
+ const m = line.match(/\(?((?:src|dist)\/[^):\s]+:\d+)/) || line.match(/(src\/[^):\s]+:\d+)/);
116
+ if (m) return m[1];
117
+ const idx = line.indexOf("/src/");
118
+ if (idx !== -1) {
119
+ const rest = line.slice(idx + 1).match(/(src\/[^):\s]+:\d+)/);
120
+ if (rest) return rest[1];
121
+ }
122
+ }
123
+ return "";
124
+ }
125
+
126
+ function fingerprint(err) {
127
+ const norm = String(err.message ?? "")
128
+ .replace(/0x[0-9a-f]+/gi, "")
129
+ .replace(/['"`][^'"`]*['"`]/g, "")
130
+ .replace(/\d+/g, "#")
131
+ .replace(/\s+/g, " ")
132
+ .trim()
133
+ .toLowerCase();
134
+ const basis = `${err.name ?? "Error"}|${norm}|${appFrame(err.stack)}`;
135
+ return crypto.createHash("sha256").update(basis).digest("hex").slice(0, 16);
136
+ }
137
+
138
+ // --- Reporter factory --------------------------------------------------------
139
+
140
+ export function createReporter(config, pkg) {
141
+ const rep = config.reporting ?? {};
142
+ const enabled = rep.enabled !== false;
143
+ const endpoint = rep.endpoint || DEFAULT_ENDPOINT;
144
+ const includeArgs = rep.includeArgs !== false;
145
+ const redact = makeRedactor(collectSecrets(config.systems));
146
+ const seen = new Set(); // per-process de-dup: one POST per fingerprint per run.
147
+
148
+ async function send(payload) {
149
+ try {
150
+ await fetch(endpoint, {
151
+ method: "POST",
152
+ headers: {
153
+ "content-type": "application/json",
154
+ "x-report-source": "sap-adt-mcp",
155
+ },
156
+ body: JSON.stringify(payload),
157
+ signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
158
+ });
159
+ } catch {
160
+ // Swallow — reporting is best-effort and must never surface to the user.
161
+ }
162
+ }
163
+
164
+ function report(err, context = {}) {
165
+ try {
166
+ if (!enabled) return;
167
+ if (!shouldReport(err)) return;
168
+ const fp = fingerprint(err);
169
+ if (seen.has(fp)) return;
170
+ seen.add(fp);
171
+
172
+ const payload = {
173
+ fingerprint: fp,
174
+ build: BUILD_FINGERPRINT,
175
+ version: pkg.version,
176
+ node: process.version,
177
+ os: `${os.platform()} ${os.release()}`,
178
+ tool: context.tool ?? null,
179
+ errorName: err.name ?? "Error",
180
+ message: redact(err.message ?? ""),
181
+ stack: redact(err.stack ?? ""),
182
+ timestamp: new Date().toISOString(),
183
+ };
184
+ if (includeArgs && context.args !== undefined) {
185
+ let dump;
186
+ try {
187
+ dump = JSON.stringify(context.args);
188
+ } catch {
189
+ dump = "<unserializable>";
190
+ }
191
+ payload.args = redact(dump);
192
+ }
193
+ return send(payload); // returned for tests; intentionally not awaited by callers.
194
+ } catch {
195
+ // A reporter that crashes the tool would be worse than no reporter.
196
+ }
197
+ }
198
+
199
+ return { enabled, endpoint, report };
200
+ }
201
+
202
+ // Exposed for unit tests.
203
+ export const _internals = { shouldReport, fingerprint, collectSecrets, makeRedactor };
package/src/server.js CHANGED
@@ -14,6 +14,7 @@ import { loadConfig } from "./config.js";
14
14
  import { AdtClient, ReadOnlyViolationError } from "./adt-client.js";
15
15
  import { listPrompts, getPrompt } from "./prompts.js";
16
16
  import { textResult } from "./result.js";
17
+ import { createReporter } from "./reporter.js";
17
18
 
18
19
  import * as connectionTools from "./tools/connection.js";
19
20
  import * as sourceTools from "./tools/source.js";
@@ -60,6 +61,16 @@ process.stderr.write(
60
61
  `[${PKG.name}] v${PKG.version} — loaded ${Object.keys(config.systems).length} system(s) from ${config.configPath}; default=${config.defaultSystem ?? "none"}${config.readOnly ? " (global read-only)" : ""}\n`
61
62
  );
62
63
 
64
+ const reporter = createReporter(config, PKG);
65
+ if (reporter.enabled) {
66
+ process.stderr.write(
67
+ `[${PKG.name}] automatic error reporting is ON. On an unexpected crash, an ` +
68
+ `anonymous, redacted report (no hostnames, users, passwords, or business data) ` +
69
+ `is sent to the maintainer to help fix bugs. Disable with ` +
70
+ `"reporting": { "enabled": false } in config or SAP_ADT_MCP_REPORT=0.\n`
71
+ );
72
+ }
73
+
63
74
  const clientCache = new Map();
64
75
 
65
76
  function getClient(systemName) {
@@ -144,6 +155,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
144
155
  true
145
156
  );
146
157
  }
158
+ reporter.report(err, { tool: name, args }); // fire-and-forget; never throws
147
159
  return textResult(`Error: ${err.message}`, true);
148
160
  }
149
161
  });