sap-adt-mcp 0.7.1 → 0.8.1
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 +38 -0
- package/config.example.json +6 -0
- package/package.json +2 -2
- package/src/config.js +19 -0
- package/src/data-preview.js +57 -14
- package/src/dump-feed.js +10 -0
- package/src/object-uris.js +11 -0
- package/src/reporter.js +357 -0
- package/src/result.js +17 -1
- package/src/server.js +23 -2
- package/src/tools/quality.js +67 -5
- package/src/tools/report.js +72 -0
- package/src/tools/runtime.js +14 -7
- package/src/tools/source.js +25 -2
package/README.md
CHANGED
|
@@ -159,6 +159,44 @@ 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
|
+
The server sends small, **redacted** reports to the maintainer so defects get
|
|
165
|
+
found and fixed. This is **on by default** and the server prints a notice saying
|
|
166
|
+
so on startup. There are three channels:
|
|
167
|
+
|
|
168
|
+
1. **Crash** — a tool handler throws an unexpected error.
|
|
169
|
+
2. **ADT error** — a tool returns a non-2xx ADT response that the classifier
|
|
170
|
+
flags as a likely tool bug (406/415 content negotiation, malformed requests,
|
|
171
|
+
server dispatcher blow-ups). User/business-side responses (401/403/404, lock
|
|
172
|
+
and enqueue conflicts, data-preview SQL errors) are **not** reported.
|
|
173
|
+
3. **Agent-reported** — the calling agent files a defect the other two channels
|
|
174
|
+
can't see (wrong data in a successful response, an ignored parameter, a
|
|
175
|
+
missing capability) via the **`adt_report_issue`** tool.
|
|
176
|
+
|
|
177
|
+
What is sent: the sap-adt-mcp version, Node version, OS, the tool name, and the
|
|
178
|
+
error/finding with a fingerprint for de-duplication. Before anything leaves your
|
|
179
|
+
machine it is scrubbed of **hostnames, users, passwords, tokens, IPs, and
|
|
180
|
+
emails**; tool arguments and free-text fields are redacted the same way. Reports
|
|
181
|
+
go to a relay the maintainer owns, which files/de-dups a GitHub issue — the
|
|
182
|
+
relay holds the GitHub credentials, never this package.
|
|
183
|
+
|
|
184
|
+
Turn it all off:
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{ "reporting": { "enabled": false } }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
…or set `SAP_ADT_MCP_REPORT=0` (also accepts `false`/`no`/`off`). Finer control:
|
|
191
|
+
|
|
192
|
+
| Key | Default | Effect |
|
|
193
|
+
| --- | --- | --- |
|
|
194
|
+
| `reporting.enabled` | `true` | Master switch for all three channels. |
|
|
195
|
+
| `reporting.adtErrors` | `true` | Channel 2 (auto-report flagged ADT errors). |
|
|
196
|
+
| `reporting.allowManual` | `true` | Channel 3 (the `adt_report_issue` tool). |
|
|
197
|
+
| `reporting.includeArgs` | `true` | Include redacted tool args / repro args. Note: object names can appear here. |
|
|
198
|
+
| `reporting.endpoint` | relay URL | Point at your own relay (see [`worker/`](worker/)). |
|
|
199
|
+
|
|
162
200
|
## Connect a client
|
|
163
201
|
|
|
164
202
|
### Claude Code (CLI)
|
package/config.example.json
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
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
|
+
"adtErrors": true,
|
|
9
|
+
"allowManual": true
|
|
10
|
+
},
|
|
5
11
|
"systems": {
|
|
6
12
|
"DEV": {
|
|
7
13
|
"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.
|
|
3
|
+
"version": "0.8.1",
|
|
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/syntax-context.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,29 @@ 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
|
+
// Channel 1: auto-report selected non-2xx ADT results (406/415/malformed).
|
|
69
|
+
adtErrors: r.adtErrors !== false,
|
|
70
|
+
// Channel 2: allow the adt_report_issue tool to file agent-initiated reports.
|
|
71
|
+
allowManual: r.allowManual !== false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
56
75
|
function requireString(value, key) {
|
|
57
76
|
if (typeof value !== "string" || value.length === 0) {
|
|
58
77
|
throw new Error(`Config: ${key} must be a non-empty string`);
|
package/src/data-preview.js
CHANGED
|
@@ -54,6 +54,13 @@ const ROW_RE = /<[a-zA-Z]+:row\b[^>]*>([\s\S]*?)<\/[a-zA-Z]+:row>/g;
|
|
|
54
54
|
const VALUE_RE = /<[a-zA-Z]+:value\b[^>]*>([\s\S]*?)<\/[a-zA-Z]+:value>/g;
|
|
55
55
|
const DATA_CELL_RE =
|
|
56
56
|
/<[a-zA-Z]+:data\b[^>]*\b(?:dataPreview:)?columnName="([^"]+)"[^>]*>([\s\S]*?)<\/[a-zA-Z]+:data>/g;
|
|
57
|
+
// Column-major table.v1+xml: one <columns> block per column, each carrying a
|
|
58
|
+
// <metadata> (the column) and a <dataSet> of <data> cells, one per row.
|
|
59
|
+
const COLUMN_BLOCK_RE =
|
|
60
|
+
/<[a-zA-Z]+:columns\b[^>]*>([\s\S]*?)<\/[a-zA-Z]+:columns>/g;
|
|
61
|
+
// A <data> cell, matching both <data>val</data> and self-closing <data/> (null).
|
|
62
|
+
const DATA_RE =
|
|
63
|
+
/<[a-zA-Z]+:data\b[^>]*?(?:\/>|>([\s\S]*?)<\/[a-zA-Z]+:data>)/g;
|
|
57
64
|
const ATTR_RE = (attr) =>
|
|
58
65
|
new RegExp(`\\b(?:[a-zA-Z]+:)?${attr}="([^"]*)"`, "i");
|
|
59
66
|
|
|
@@ -77,6 +84,35 @@ function pickTagText(xml, tag) {
|
|
|
77
84
|
return m ? decodeEntities(m[1].trim()) : undefined;
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
// Parse the column-major table.v1+xml shape into row objects. Each
|
|
88
|
+
// <dataPreview:columns> block holds one <metadata> (the column) and a
|
|
89
|
+
// <dataSet> whose <data> children are that column's values across all rows.
|
|
90
|
+
// Returns [] when the document isn't column-major (no per-column data cells).
|
|
91
|
+
function parseColumnMajor(xml) {
|
|
92
|
+
const cols = [];
|
|
93
|
+
let nrows = 0;
|
|
94
|
+
for (const block of xml.matchAll(COLUMN_BLOCK_RE)) {
|
|
95
|
+
const inner = block[1];
|
|
96
|
+
const metaMatch = inner.match(/<[a-zA-Z]+:metadata\b([^>]*)>/);
|
|
97
|
+
const name = metaMatch ? pickAttr(metaMatch[1], "name") : undefined;
|
|
98
|
+
const values = [...inner.matchAll(DATA_RE)].map((d) =>
|
|
99
|
+
decodeEntities((d[1] ?? "").trim())
|
|
100
|
+
);
|
|
101
|
+
// A metadata-only <columns> wrapper (Shape A) carries no data cells — skip.
|
|
102
|
+
if (!name || values.length === 0) continue;
|
|
103
|
+
cols.push({ name, values });
|
|
104
|
+
if (values.length > nrows) nrows = values.length;
|
|
105
|
+
}
|
|
106
|
+
if (cols.length === 0) return [];
|
|
107
|
+
const rows = [];
|
|
108
|
+
for (let i = 0; i < nrows; i++) {
|
|
109
|
+
const row = {};
|
|
110
|
+
for (const c of cols) row[c.name] = c.values[i] ?? null;
|
|
111
|
+
rows.push(row);
|
|
112
|
+
}
|
|
113
|
+
return rows;
|
|
114
|
+
}
|
|
115
|
+
|
|
80
116
|
export function parseDataPreview(xml) {
|
|
81
117
|
const columns = [];
|
|
82
118
|
for (const m of xml.matchAll(METADATA_RE)) {
|
|
@@ -114,21 +150,28 @@ export function parseDataPreview(xml) {
|
|
|
114
150
|
}
|
|
115
151
|
}
|
|
116
152
|
} else {
|
|
117
|
-
// Shape
|
|
118
|
-
//
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
153
|
+
// Shape C: column-major table.v1+xml — each <columns> block carries the
|
|
154
|
+
// column metadata plus its own <dataSet> of cells. Transpose to rows.
|
|
155
|
+
const colMajor = parseColumnMajor(xml);
|
|
156
|
+
if (colMajor.length > 0) {
|
|
157
|
+
rows.push(...colMajor);
|
|
158
|
+
} else {
|
|
159
|
+
// Shape B: flat <data columnName="X">val</data> blocks. We group every N
|
|
160
|
+
// cells into a row where N = column count.
|
|
161
|
+
const cells = [...xml.matchAll(DATA_CELL_RE)].map((m) => ({
|
|
162
|
+
column: m[1],
|
|
163
|
+
value: decodeEntities(m[2].trim()),
|
|
164
|
+
}));
|
|
165
|
+
if (columns.length > 0 && cells.length > 0) {
|
|
166
|
+
const stride = columns.length;
|
|
167
|
+
for (let i = 0; i < cells.length; i += stride) {
|
|
168
|
+
const row = {};
|
|
169
|
+
for (let j = 0; j < stride && i + j < cells.length; j++) {
|
|
170
|
+
const cell = cells[i + j];
|
|
171
|
+
row[cell.column] = cell.value;
|
|
172
|
+
}
|
|
173
|
+
rows.push(row);
|
|
130
174
|
}
|
|
131
|
-
rows.push(row);
|
|
132
175
|
}
|
|
133
176
|
}
|
|
134
177
|
}
|
package/src/dump-feed.js
CHANGED
|
@@ -92,6 +92,16 @@ export function parseDumpFeed(xml) {
|
|
|
92
92
|
return entries;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// Client-side user filter. Several on-prem releases ignore the feed's `user`
|
|
96
|
+
// query parameter and return every user's dumps, so we enforce the filter here
|
|
97
|
+
// the same way adt_list_dumps already trims maxResults client-side. Match is
|
|
98
|
+
// case-insensitive on the author/user the feed parser surfaced per entry.
|
|
99
|
+
export function filterDumpsByUser(entries, user) {
|
|
100
|
+
if (!user) return entries;
|
|
101
|
+
const want = String(user).trim().toUpperCase();
|
|
102
|
+
return entries.filter((e) => (e.user ?? "").trim().toUpperCase() === want);
|
|
103
|
+
}
|
|
104
|
+
|
|
95
105
|
export function parseDumpDetail(xml) {
|
|
96
106
|
// The detail response can be either an Atom entry or a richer rba-namespaced
|
|
97
107
|
// document; surface what we can extract plus the raw body.
|
package/src/object-uris.js
CHANGED
|
@@ -125,6 +125,17 @@ export function objectUri({ type, name, group }) {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// DDIC primitives don't serve plain-text source at their object URI — they
|
|
129
|
+
// return XML metadata behind a dedicated media type. Requesting "text/plain"
|
|
130
|
+
// makes the resource answer 406 ExceptionResourceNotAcceptable. Map the type to
|
|
131
|
+
// the Accept it actually serves; consistent with the v2 media types this
|
|
132
|
+
// codebase already uses for domains/message classes elsewhere.
|
|
133
|
+
export const METADATA_XML_ACCEPT = {
|
|
134
|
+
DTEL: "application/vnd.sap.adt.dataelements.v2+xml",
|
|
135
|
+
DOMA: "application/vnd.sap.adt.domains.v2+xml",
|
|
136
|
+
MSAG: "application/vnd.sap.adt.messageclass.v2+xml",
|
|
137
|
+
};
|
|
138
|
+
|
|
128
139
|
export function sourceUri({ type, name, group, include }) {
|
|
129
140
|
const t = normalizeType(type);
|
|
130
141
|
const base = objectUri({ type: t, name, group });
|
package/src/reporter.js
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
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 hash16(s) {
|
|
127
|
+
return crypto.createHash("sha256").update(s).digest("hex").slice(0, 16);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeText(s) {
|
|
131
|
+
return String(s ?? "")
|
|
132
|
+
.replace(/0x[0-9a-f]+/gi, "")
|
|
133
|
+
.replace(/['"`][^'"`]*['"`]/g, "")
|
|
134
|
+
.replace(/\d+/g, "#")
|
|
135
|
+
.replace(/\s+/g, " ")
|
|
136
|
+
.trim()
|
|
137
|
+
.toLowerCase();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function fingerprint(err) {
|
|
141
|
+
const basis = `${err.name ?? "Error"}|${normalizeText(err.message)}|${appFrame(err.stack)}`;
|
|
142
|
+
return hash16(basis);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function safeStringify(v) {
|
|
146
|
+
try {
|
|
147
|
+
return JSON.stringify(v);
|
|
148
|
+
} catch {
|
|
149
|
+
return "<unserializable>";
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --- ADT errorResult classification (channel 1) ------------------------------
|
|
154
|
+
|
|
155
|
+
// Known business / user-side T100 message ids and exception types: never a tool
|
|
156
|
+
// bug, so never auto-reported.
|
|
157
|
+
const BUSINESS_T100 = new Set([
|
|
158
|
+
"SLOCK",
|
|
159
|
+
"S_LOCK",
|
|
160
|
+
"CTS_WBO_API",
|
|
161
|
+
"ADT_DATAPREVIEW_MSG",
|
|
162
|
+
]);
|
|
163
|
+
const BUSINESS_TYPE_RE =
|
|
164
|
+
/SaveFailure|Lock|Enqueue|CTS_|ExceptionResourceAlreadyExists|NotFound/i;
|
|
165
|
+
|
|
166
|
+
// Decide whether a non-2xx ADT response that a handler returned (not threw) is
|
|
167
|
+
// likely a defect in *this* tool rather than a user/business condition. Errs
|
|
168
|
+
// toward NOT reporting; the relay labels survivors `auto-adt-error` for cheap
|
|
169
|
+
// human triage.
|
|
170
|
+
function shouldReportAdt(meta = {}) {
|
|
171
|
+
const s = Number(meta.status);
|
|
172
|
+
const type = String(meta.type ?? "");
|
|
173
|
+
const t100id = String(meta.t100?.id ?? "");
|
|
174
|
+
const msg = String(meta.message ?? "");
|
|
175
|
+
|
|
176
|
+
if (BUSINESS_T100.has(t100id)) return false;
|
|
177
|
+
if (BUSINESS_TYPE_RE.test(type)) return false;
|
|
178
|
+
if (/datapreview/i.test(type) || /datapreview/i.test(t100id)) return false;
|
|
179
|
+
|
|
180
|
+
// Content negotiation: historically always a missing/wrong header on our side.
|
|
181
|
+
if (s === 406 || s === 415) return true;
|
|
182
|
+
// Clear user/business statuses.
|
|
183
|
+
if (s === 401 || s === 403 || s === 404 || s === 409 || s === 423) return false;
|
|
184
|
+
// 400: report only when it reads like a malformed request (bad/missing media
|
|
185
|
+
// type or query parameter) rather than a data/syntax error.
|
|
186
|
+
if (s === 400) {
|
|
187
|
+
return /content[- ]?type|not acceptable|media type|missing|could not be found|parameter/i.test(
|
|
188
|
+
msg + " " + type
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
// Server-side dispatcher/parser blow-ups on a request we shaped.
|
|
192
|
+
if (s >= 500) return true;
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function adtFingerprint(meta) {
|
|
197
|
+
return hash16(
|
|
198
|
+
`adt|${meta.tool ?? ""}|${meta.status ?? ""}|${meta.type ?? ""}|${meta.t100?.id ?? ""}|${meta.t100?.number ?? ""}`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function manualFingerprint(tool, kind, summary) {
|
|
203
|
+
return hash16(`manual|${tool}|${kind}|${normalizeText(summary)}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// --- Reporter factory --------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
export function createReporter(config, pkg) {
|
|
209
|
+
const rep = config.reporting ?? {};
|
|
210
|
+
const enabled = rep.enabled !== false;
|
|
211
|
+
const endpoint = rep.endpoint || DEFAULT_ENDPOINT;
|
|
212
|
+
const includeArgs = rep.includeArgs !== false;
|
|
213
|
+
const adtErrors = rep.adtErrors !== false;
|
|
214
|
+
const allowManual = rep.allowManual !== false;
|
|
215
|
+
const redact = makeRedactor(collectSecrets(config.systems));
|
|
216
|
+
const seen = new Set(); // per-process de-dup: one POST per fingerprint per run.
|
|
217
|
+
|
|
218
|
+
// source = the x-report-source header (crash/adt-error use "sap-adt-mcp";
|
|
219
|
+
// agent-initiated reports use "sap-adt-mcp-manual"). The relay routes on it.
|
|
220
|
+
async function send(payload, source) {
|
|
221
|
+
try {
|
|
222
|
+
await fetch(endpoint, {
|
|
223
|
+
method: "POST",
|
|
224
|
+
headers: {
|
|
225
|
+
"content-type": "application/json",
|
|
226
|
+
"x-report-source": source,
|
|
227
|
+
},
|
|
228
|
+
body: JSON.stringify(payload),
|
|
229
|
+
signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
|
|
230
|
+
});
|
|
231
|
+
} catch {
|
|
232
|
+
// Swallow — reporting is best-effort and must never surface to the user.
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function envelope(kind, fingerprint) {
|
|
237
|
+
return {
|
|
238
|
+
kind,
|
|
239
|
+
fingerprint,
|
|
240
|
+
build: BUILD_FINGERPRINT,
|
|
241
|
+
version: pkg.version,
|
|
242
|
+
node: process.version,
|
|
243
|
+
os: `${os.platform()} ${os.release()}`,
|
|
244
|
+
timestamp: new Date().toISOString(),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Channel 0 — crash: a tool handler threw an unexpected error.
|
|
249
|
+
function report(err, context = {}) {
|
|
250
|
+
try {
|
|
251
|
+
if (!enabled) return;
|
|
252
|
+
if (!shouldReport(err)) return;
|
|
253
|
+
const fp = fingerprint(err);
|
|
254
|
+
if (seen.has(fp)) return;
|
|
255
|
+
seen.add(fp);
|
|
256
|
+
|
|
257
|
+
const payload = {
|
|
258
|
+
...envelope("crash", fp),
|
|
259
|
+
tool: context.tool ?? null,
|
|
260
|
+
errorName: err.name ?? "Error",
|
|
261
|
+
message: redact(err.message ?? ""),
|
|
262
|
+
stack: redact(err.stack ?? ""),
|
|
263
|
+
};
|
|
264
|
+
if (includeArgs && context.args !== undefined) {
|
|
265
|
+
payload.args = redact(safeStringify(context.args));
|
|
266
|
+
}
|
|
267
|
+
return send(payload, "sap-adt-mcp");
|
|
268
|
+
} catch {
|
|
269
|
+
// A reporter that crashes the tool would be worse than no reporter.
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Channel 1 — adt-error: a handler RETURNED a non-2xx ADT result that the
|
|
274
|
+
// classifier judges a likely tool defect (content negotiation, malformed
|
|
275
|
+
// request, server dispatcher blow-up).
|
|
276
|
+
function reportAdtError(meta = {}) {
|
|
277
|
+
try {
|
|
278
|
+
if (!enabled || !adtErrors) return;
|
|
279
|
+
if (!shouldReportAdt(meta)) return;
|
|
280
|
+
const fp = adtFingerprint(meta);
|
|
281
|
+
if (seen.has(fp)) return;
|
|
282
|
+
seen.add(fp);
|
|
283
|
+
|
|
284
|
+
const payload = {
|
|
285
|
+
...envelope("adt-error", fp),
|
|
286
|
+
tool: meta.tool ?? null,
|
|
287
|
+
status: meta.status ?? null,
|
|
288
|
+
errorType: meta.type ?? null,
|
|
289
|
+
namespace: meta.namespace ?? null,
|
|
290
|
+
t100: meta.t100 ?? null,
|
|
291
|
+
message: redact(meta.message ?? ""),
|
|
292
|
+
};
|
|
293
|
+
if (includeArgs && meta.args !== undefined) {
|
|
294
|
+
payload.args = redact(safeStringify(meta.args));
|
|
295
|
+
}
|
|
296
|
+
return send(payload, "sap-adt-mcp");
|
|
297
|
+
} catch {
|
|
298
|
+
// best-effort
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Channel 2 — manual: the calling agent files a defect the classifier can't
|
|
303
|
+
// see (wrong data in a 200, ignored parameter, missing capability). Returns a
|
|
304
|
+
// small status object to the tool so the agent gets feedback.
|
|
305
|
+
function reportManual(input = {}) {
|
|
306
|
+
try {
|
|
307
|
+
if (!enabled) return { ok: false, reason: "reporting disabled" };
|
|
308
|
+
if (!allowManual) return { ok: false, reason: "manual reporting disabled" };
|
|
309
|
+
const tool = String(input.tool ?? "").trim();
|
|
310
|
+
const summary = String(input.summary ?? "").trim();
|
|
311
|
+
if (!tool || !summary) {
|
|
312
|
+
return { ok: false, reason: "tool and summary are required" };
|
|
313
|
+
}
|
|
314
|
+
const issueKind = input.kind === "enhancement" ? "enhancement" : "bug";
|
|
315
|
+
const fp = manualFingerprint(tool, issueKind, summary);
|
|
316
|
+
|
|
317
|
+
const payload = {
|
|
318
|
+
...envelope("manual", fp),
|
|
319
|
+
tool,
|
|
320
|
+
issueKind,
|
|
321
|
+
summary: redact(summary),
|
|
322
|
+
expected: input.expected ? redact(String(input.expected)) : undefined,
|
|
323
|
+
actual: input.actual ? redact(String(input.actual)) : undefined,
|
|
324
|
+
};
|
|
325
|
+
if (includeArgs && input.reproArgs !== undefined) {
|
|
326
|
+
payload.reproArgs = redact(safeStringify(input.reproArgs));
|
|
327
|
+
}
|
|
328
|
+
// Explicit user action — no per-process de-dup here; the relay collapses
|
|
329
|
+
// repeats onto one issue by fingerprint.
|
|
330
|
+
send(payload, "sap-adt-mcp-manual");
|
|
331
|
+
return { ok: true, fingerprint: fp, issueKind };
|
|
332
|
+
} catch {
|
|
333
|
+
return { ok: false, reason: "internal error" };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
enabled,
|
|
339
|
+
endpoint,
|
|
340
|
+
adtErrors,
|
|
341
|
+
allowManual,
|
|
342
|
+
report,
|
|
343
|
+
reportAdtError,
|
|
344
|
+
reportManual,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Exposed for unit tests.
|
|
349
|
+
export const _internals = {
|
|
350
|
+
shouldReport,
|
|
351
|
+
shouldReportAdt,
|
|
352
|
+
fingerprint,
|
|
353
|
+
adtFingerprint,
|
|
354
|
+
manualFingerprint,
|
|
355
|
+
collectSecrets,
|
|
356
|
+
makeRedactor,
|
|
357
|
+
};
|
package/src/result.js
CHANGED
|
@@ -10,7 +10,7 @@ export function jsonResult(value, isError = false) {
|
|
|
10
10
|
|
|
11
11
|
export function errorResult(system, status, body, contentType, extra = {}) {
|
|
12
12
|
const parsed = parseAdtError(body, contentType);
|
|
13
|
-
|
|
13
|
+
const result = jsonResult(
|
|
14
14
|
{
|
|
15
15
|
system,
|
|
16
16
|
status,
|
|
@@ -20,4 +20,20 @@ export function errorResult(system, status, body, contentType, extra = {}) {
|
|
|
20
20
|
},
|
|
21
21
|
true
|
|
22
22
|
);
|
|
23
|
+
// Attach structured metadata for the crash-report wrapper to classify against,
|
|
24
|
+
// without changing the content the client sees. Non-enumerable so it never
|
|
25
|
+
// serializes into the MCP result.
|
|
26
|
+
Object.defineProperty(result, "_adtError", {
|
|
27
|
+
value: {
|
|
28
|
+
system,
|
|
29
|
+
status,
|
|
30
|
+
type: parsed?.type,
|
|
31
|
+
namespace: parsed?.namespace,
|
|
32
|
+
t100: parsed?.properties?.t100,
|
|
33
|
+
message: parsed?.message ?? parsed?.localizedMessage,
|
|
34
|
+
stage: extra.stage,
|
|
35
|
+
},
|
|
36
|
+
enumerable: false,
|
|
37
|
+
});
|
|
38
|
+
return result;
|
|
23
39
|
}
|
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";
|
|
@@ -31,6 +32,7 @@ import * as cdsTools from "./tools/cds.js";
|
|
|
31
32
|
import * as worklistTools from "./tools/worklist.js";
|
|
32
33
|
import * as jobTools from "./tools/jobs.js";
|
|
33
34
|
import * as rapTools from "./tools/rap.js";
|
|
35
|
+
import * as reportTools from "./tools/report.js";
|
|
34
36
|
|
|
35
37
|
const PKG = JSON.parse(
|
|
36
38
|
readFileSync(
|
|
@@ -60,6 +62,16 @@ process.stderr.write(
|
|
|
60
62
|
`[${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
63
|
);
|
|
62
64
|
|
|
65
|
+
const reporter = createReporter(config, PKG);
|
|
66
|
+
if (reporter.enabled) {
|
|
67
|
+
process.stderr.write(
|
|
68
|
+
`[${PKG.name}] automatic error reporting is ON. On an unexpected crash, an ` +
|
|
69
|
+
`anonymous, redacted report (no hostnames, users, passwords, or business data) ` +
|
|
70
|
+
`is sent to the maintainer to help fix bugs. Disable with ` +
|
|
71
|
+
`"reporting": { "enabled": false } in config or SAP_ADT_MCP_REPORT=0.\n`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
const clientCache = new Map();
|
|
64
76
|
|
|
65
77
|
function getClient(systemName) {
|
|
@@ -80,7 +92,7 @@ function getClient(systemName) {
|
|
|
80
92
|
return { name, client: clientCache.get(name), profile };
|
|
81
93
|
}
|
|
82
94
|
|
|
83
|
-
const ctx = { getClient, config };
|
|
95
|
+
const ctx = { getClient, config, reporter };
|
|
84
96
|
|
|
85
97
|
const TOOL_MODULES = [
|
|
86
98
|
connectionTools,
|
|
@@ -99,6 +111,7 @@ const TOOL_MODULES = [
|
|
|
99
111
|
worklistTools,
|
|
100
112
|
jobTools,
|
|
101
113
|
rapTools,
|
|
114
|
+
reportTools,
|
|
102
115
|
];
|
|
103
116
|
|
|
104
117
|
const tools = [];
|
|
@@ -136,7 +149,14 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
136
149
|
const handler = handlers[name];
|
|
137
150
|
if (!handler) return textResult(`Unknown tool: ${name}`, true);
|
|
138
151
|
try {
|
|
139
|
-
|
|
152
|
+
const out = await handler(args);
|
|
153
|
+
// A handler that RETURNED a non-2xx ADT result (vs threw) carries structured
|
|
154
|
+
// metadata on _adtError — let the reporter's classifier decide if it's a
|
|
155
|
+
// likely tool defect worth auto-filing. Fire-and-forget; never throws.
|
|
156
|
+
if (out && out._adtError) {
|
|
157
|
+
reporter.reportAdtError({ tool: name, args, ...out._adtError });
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
140
160
|
} catch (err) {
|
|
141
161
|
if (err instanceof ReadOnlyViolationError) {
|
|
142
162
|
return textResult(
|
|
@@ -144,6 +164,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
144
164
|
true
|
|
145
165
|
);
|
|
146
166
|
}
|
|
167
|
+
reporter.report(err, { tool: name, args }); // fire-and-forget; never throws
|
|
147
168
|
return textResult(`Error: ${err.message}`, true);
|
|
148
169
|
}
|
|
149
170
|
});
|
package/src/tools/quality.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { objectUri } from "../object-uris.js";
|
|
1
|
+
import { objectUri, sourceUri, normalizeType } from "../object-uris.js";
|
|
2
2
|
import { escapeXml } from "../xml.js";
|
|
3
3
|
import { parseObjectReferences } from "../object-references.js";
|
|
4
4
|
import { errorResult, jsonResult } from "../result.js";
|
|
@@ -131,10 +131,34 @@ async function runAtcWorklist(client, sys, { uris, checkVariant, maxResults }) {
|
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// Normalize a caller-supplied include context into an ADT object URI. Accepts a
|
|
135
|
+
// full ADT path as-is; treats a bare token as a program name.
|
|
136
|
+
export function toContextUri(input) {
|
|
137
|
+
const s = String(input).trim();
|
|
138
|
+
if (s.startsWith("/")) return s;
|
|
139
|
+
return `/sap/bc/adt/programs/programs/${encodeURIComponent(s.toLowerCase())}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Best-effort: resolve an include's first main program via its /mainprograms
|
|
143
|
+
// sub-resource. Any failure (older release, no main program, 4xx) returns
|
|
144
|
+
// undefined so the check still runs (and the response carries a hint).
|
|
145
|
+
async function deriveMainProgram(client, includeObjUri) {
|
|
146
|
+
try {
|
|
147
|
+
const res = await client.request({ path: `${includeObjUri}/mainprograms` });
|
|
148
|
+
if (!res.ok) return undefined;
|
|
149
|
+
const text = await res.text();
|
|
150
|
+
const m = text.match(/adtcore:uri="([^"]+)"/i);
|
|
151
|
+
return m ? m[1].replace(/&/g, "&") : undefined;
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
134
157
|
export const tools = [
|
|
135
158
|
{
|
|
136
159
|
name: "adt_syntax_check",
|
|
137
|
-
description:
|
|
160
|
+
description:
|
|
161
|
+
"Run an ADT syntax check on an object. Returns the raw <chkrun:reports> XML. Includes only compile in the context of a main program: for type=include, pass `context` (the main program / function group), otherwise the check returns status='notProcessed'. When omitted for an include, the tool tries to auto-resolve the include's first main program.",
|
|
138
162
|
inputSchema: {
|
|
139
163
|
type: "object",
|
|
140
164
|
properties: {
|
|
@@ -142,6 +166,11 @@ export const tools = [
|
|
|
142
166
|
object: { type: "string", description: "Object name." },
|
|
143
167
|
type: { type: "string", description: OBJECT_TYPE_HINT },
|
|
144
168
|
group: { type: "string", description: "Function group (for FUGR/FF or FUGR/I)." },
|
|
169
|
+
context: {
|
|
170
|
+
type: "string",
|
|
171
|
+
description:
|
|
172
|
+
"Main-program context for checking an include. Either a full ADT object URI (e.g. '/sap/bc/adt/functions/groups/v61a' or '/sap/bc/adt/programs/programs/zmain') or a bare program name. Only used for includes; auto-resolved from the include's main programs when omitted.",
|
|
173
|
+
},
|
|
145
174
|
},
|
|
146
175
|
required: ["object", "type"],
|
|
147
176
|
},
|
|
@@ -258,15 +287,35 @@ export function register({ getClient }) {
|
|
|
258
287
|
return {
|
|
259
288
|
adt_syntax_check: async (args) => {
|
|
260
289
|
const { client, name: sys } = getClient(args.system);
|
|
261
|
-
const
|
|
290
|
+
const t = normalizeType(args.type);
|
|
291
|
+
const isInclude = t === "INCL" || t === "FUGR/I";
|
|
292
|
+
|
|
293
|
+
let checkUri = objectUri({
|
|
262
294
|
type: args.type,
|
|
263
295
|
name: args.object,
|
|
264
296
|
group: args.group,
|
|
265
297
|
});
|
|
298
|
+
let contextUri;
|
|
299
|
+
if (isInclude) {
|
|
300
|
+
contextUri = args.context
|
|
301
|
+
? toContextUri(args.context)
|
|
302
|
+
: await deriveMainProgram(client, checkUri);
|
|
303
|
+
// Includes are checked through their source URI with the main program
|
|
304
|
+
// attached as ?context= (the form ADT itself uses in include links).
|
|
305
|
+
const incSource = sourceUri({
|
|
306
|
+
type: args.type,
|
|
307
|
+
name: args.object,
|
|
308
|
+
group: args.group,
|
|
309
|
+
});
|
|
310
|
+
checkUri = contextUri
|
|
311
|
+
? `${incSource}?context=${encodeURIComponent(contextUri)}`
|
|
312
|
+
: incSource;
|
|
313
|
+
}
|
|
314
|
+
|
|
266
315
|
const body =
|
|
267
316
|
`<?xml version="1.0" encoding="UTF-8"?>` +
|
|
268
317
|
`<chkrun:checkObjectList xmlns:chkrun="http://www.sap.com/adt/checkrun" xmlns:adtcore="http://www.sap.com/adt/core">` +
|
|
269
|
-
`<chkrun:checkObject adtcore:uri="${escapeXml(
|
|
318
|
+
`<chkrun:checkObject adtcore:uri="${escapeXml(checkUri)}"/>` +
|
|
270
319
|
`</chkrun:checkObjectList>`;
|
|
271
320
|
const res = await client.request({
|
|
272
321
|
method: "POST",
|
|
@@ -277,7 +326,20 @@ export function register({ getClient }) {
|
|
|
277
326
|
});
|
|
278
327
|
const text = await res.text();
|
|
279
328
|
if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"));
|
|
280
|
-
|
|
329
|
+
const notProcessed = /chkrun:status="notProcessed"/i.test(text);
|
|
330
|
+
return jsonResult({
|
|
331
|
+
system: sys,
|
|
332
|
+
object: args.object,
|
|
333
|
+
context: contextUri,
|
|
334
|
+
result: text,
|
|
335
|
+
...(isInclude && notProcessed
|
|
336
|
+
? {
|
|
337
|
+
hint:
|
|
338
|
+
"Include not processed — no main-program context resolved. Pass `context` " +
|
|
339
|
+
"(the main program / function group ADT URI, e.g. '/sap/bc/adt/programs/programs/zmain').",
|
|
340
|
+
}
|
|
341
|
+
: {}),
|
|
342
|
+
});
|
|
281
343
|
},
|
|
282
344
|
|
|
283
345
|
adt_run_unit_tests: async (args) => {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsonResult } from "../result.js";
|
|
2
|
+
|
|
3
|
+
export const tools = [
|
|
4
|
+
{
|
|
5
|
+
name: "adt_report_issue",
|
|
6
|
+
description:
|
|
7
|
+
"File a bug or enhancement about a sap-adt-mcp TOOL back to the maintainer. The report is redacted (hosts, users, passwords, IPs, emails stripped), de-duplicated, and becomes a GitHub issue. Use this for defects the automatic crash reporter cannot see: wrong data in an otherwise-successful response, a parameter that is silently ignored, or a missing capability. Do NOT use it for problems in the user's ABAP code or SAP system — only for defects in this MCP server's own tools.",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
tool: {
|
|
12
|
+
type: "string",
|
|
13
|
+
description:
|
|
14
|
+
"The sap-adt-mcp tool the issue is about, e.g. 'adt_read_table'.",
|
|
15
|
+
},
|
|
16
|
+
kind: {
|
|
17
|
+
type: "string",
|
|
18
|
+
enum: ["bug", "enhancement"],
|
|
19
|
+
description:
|
|
20
|
+
"bug = a tool produces wrong/missing output; enhancement = a missing capability. Default bug.",
|
|
21
|
+
},
|
|
22
|
+
summary: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description:
|
|
25
|
+
"One-line summary of the defect. Used in the issue title and the de-duplication key.",
|
|
26
|
+
},
|
|
27
|
+
expected: { type: "string", description: "What you expected to happen." },
|
|
28
|
+
actual: { type: "string", description: "What actually happened." },
|
|
29
|
+
reproArgs: {
|
|
30
|
+
type: "object",
|
|
31
|
+
description:
|
|
32
|
+
"Optional tool arguments that reproduce the issue. Redacted like every report (hosts/users/secrets stripped); suppressed entirely when reporting.includeArgs is off.",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ["tool", "summary"],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export function register({ reporter }) {
|
|
41
|
+
return {
|
|
42
|
+
adt_report_issue: async (args) => {
|
|
43
|
+
if (!reporter) {
|
|
44
|
+
return jsonResult({ ok: false, reason: "reporter unavailable" });
|
|
45
|
+
}
|
|
46
|
+
const r = reporter.reportManual({
|
|
47
|
+
tool: args.tool,
|
|
48
|
+
kind: args.kind,
|
|
49
|
+
summary: args.summary,
|
|
50
|
+
expected: args.expected,
|
|
51
|
+
actual: args.actual,
|
|
52
|
+
reproArgs: args.reproArgs,
|
|
53
|
+
});
|
|
54
|
+
if (!r || !r.ok) {
|
|
55
|
+
return jsonResult({
|
|
56
|
+
ok: false,
|
|
57
|
+
reason: r?.reason ?? "not submitted",
|
|
58
|
+
hint:
|
|
59
|
+
"Reporting may be disabled (reporting.enabled / reporting.allowManual in config, or SAP_ADT_MCP_REPORT=0).",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return jsonResult({
|
|
63
|
+
ok: true,
|
|
64
|
+
submitted: true,
|
|
65
|
+
kind: r.issueKind,
|
|
66
|
+
fingerprint: r.fingerprint,
|
|
67
|
+
note:
|
|
68
|
+
"Submitted to the maintainer's relay; it will appear as a de-duplicated GitHub issue.",
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
package/src/tools/runtime.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
parseDumpFeed,
|
|
4
4
|
parseDumpMetadata,
|
|
5
5
|
parseDumpChapters,
|
|
6
|
+
filterDumpsByUser,
|
|
6
7
|
CRITICAL_CHAPTER_KEYS,
|
|
7
8
|
} from "../dump-feed.js";
|
|
8
9
|
import { SYSTEM_HINT } from "./_shared.js";
|
|
@@ -22,7 +23,8 @@ export const tools = [
|
|
|
22
23
|
system: { type: "string", description: SYSTEM_HINT },
|
|
23
24
|
user: {
|
|
24
25
|
type: "string",
|
|
25
|
-
description:
|
|
26
|
+
description:
|
|
27
|
+
"Filter by the user who triggered the dump (case-insensitive). Enforced client-side — several on-prem releases ignore the server-side user filter.",
|
|
26
28
|
},
|
|
27
29
|
host: {
|
|
28
30
|
type: "string",
|
|
@@ -140,12 +142,16 @@ export function register({ getClient }) {
|
|
|
140
142
|
});
|
|
141
143
|
}
|
|
142
144
|
// Server-side cap is unreliable; some on-prem releases return every
|
|
143
|
-
// available entry regardless of maxResults.
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
145
|
+
// available entry regardless of maxResults. The `user` feed filter is
|
|
146
|
+
// likewise ignored on several releases, so enforce it client-side before
|
|
147
|
+
// trimming. Strip the per-entry summary too — on real systems it's a
|
|
148
|
+
// 10+KB HTML chunk (chapter index + back-link) that bloats list responses
|
|
149
|
+
// past the tool-output token limit. Agents that need detail call
|
|
150
|
+
// adt_get_dump.
|
|
147
151
|
const total = entries.length;
|
|
148
|
-
const
|
|
152
|
+
const filtered = filterDumpsByUser(entries, args.user);
|
|
153
|
+
const matched = filtered.length;
|
|
154
|
+
const trimmed = filtered.slice(0, max).map((e) => {
|
|
149
155
|
// eslint-disable-next-line no-unused-vars
|
|
150
156
|
const { summary, ...rest } = e;
|
|
151
157
|
return rest;
|
|
@@ -154,7 +160,8 @@ export function register({ getClient }) {
|
|
|
154
160
|
system: sys,
|
|
155
161
|
count: trimmed.length,
|
|
156
162
|
totalReturnedByServer: total,
|
|
157
|
-
|
|
163
|
+
matchedFilter: args.user ? matched : undefined,
|
|
164
|
+
truncated: matched > trimmed.length,
|
|
158
165
|
dumps: trimmed,
|
|
159
166
|
raw: trimmed.length === 0 ? text.slice(0, 4000) : undefined,
|
|
160
167
|
});
|
package/src/tools/source.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sourceUri, objectUri, normalizeType } from "../object-uris.js";
|
|
1
|
+
import { sourceUri, objectUri, normalizeType, METADATA_XML_ACCEPT } from "../object-uris.js";
|
|
2
2
|
import { acquireLock, releaseLock } from "../lock.js";
|
|
3
3
|
import { errorResult, jsonResult } from "../result.js";
|
|
4
4
|
import { OBJECT_TYPE_HINT, SYSTEM_HINT } from "./_shared.js";
|
|
@@ -120,7 +120,7 @@ export const tools = [
|
|
|
120
120
|
{
|
|
121
121
|
name: "adt_get_source",
|
|
122
122
|
description:
|
|
123
|
-
"Fetch the ABAP source of an object (program, class, interface, function module, include, CDS, table). Returns plain text. For sources larger than the MCP per-call output cap (~64 KB), use firstLine/lastLine to paginate or onlyMethod to slice a single method body.",
|
|
123
|
+
"Fetch the ABAP source of an object (program, class, interface, function module, include, CDS, table). Returns plain text. DDIC primitives without plain-text source (data element, domain, message class) return their ADT XML metadata instead (format: 'xml'). For sources larger than the MCP per-call output cap (~64 KB), use firstLine/lastLine to paginate or onlyMethod to slice a single method body.",
|
|
124
124
|
inputSchema: {
|
|
125
125
|
type: "object",
|
|
126
126
|
properties: {
|
|
@@ -273,12 +273,35 @@ export function register({ getClient }) {
|
|
|
273
273
|
return {
|
|
274
274
|
adt_get_source: async (args) => {
|
|
275
275
|
const { client, name: sys } = getClient(args.system);
|
|
276
|
+
const t = normalizeType(args.type);
|
|
276
277
|
const path = sourceUri({
|
|
277
278
|
type: args.type,
|
|
278
279
|
name: args.object,
|
|
279
280
|
group: args.group,
|
|
280
281
|
include: args.include,
|
|
281
282
|
});
|
|
283
|
+
|
|
284
|
+
// DDIC primitives (data element / domain / message class) have no
|
|
285
|
+
// plain-text source — fetch their XML metadata with the right media type
|
|
286
|
+
// (text/plain would 406) and return it as-is.
|
|
287
|
+
const metaAccept = METADATA_XML_ACCEPT[t];
|
|
288
|
+
if (metaAccept) {
|
|
289
|
+
const res = await client.request({ path, accept: metaAccept });
|
|
290
|
+
const text = await res.text();
|
|
291
|
+
if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"));
|
|
292
|
+
return jsonResult({
|
|
293
|
+
system: sys,
|
|
294
|
+
object: args.object,
|
|
295
|
+
type: t,
|
|
296
|
+
path,
|
|
297
|
+
format: "xml",
|
|
298
|
+
source: text,
|
|
299
|
+
bytes: text.length,
|
|
300
|
+
totalLines: text.split(/\r?\n/).length,
|
|
301
|
+
note: `${t} has no plain-text source; returning ADT XML metadata (${metaAccept.split("+")[0]}+xml).`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
282
305
|
const res = await client.request({ path, accept: "text/plain" });
|
|
283
306
|
const text = await res.text();
|
|
284
307
|
if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"));
|