sap-adt-mcp 0.8.0 → 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 +28 -19
- package/config.example.json +3 -1
- package/package.json +2 -2
- package/src/config.js +4 -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 +176 -22
- package/src/result.js +17 -1
- package/src/server.js +11 -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
|
@@ -161,32 +161,41 @@ disables TLS validation for that profile only. Don't set this on PRD.
|
|
|
161
161
|
|
|
162
162
|
### Automatic error reporting
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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.
|
|
167
176
|
|
|
168
177
|
What is sent: the sap-adt-mcp version, Node version, OS, the tool name, and the
|
|
169
|
-
error
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
relay the maintainer owns, which files/de-dups a GitHub issue — the
|
|
173
|
-
the GitHub credentials, never this package.
|
|
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.
|
|
174
183
|
|
|
175
|
-
|
|
176
|
-
or TLS problems, wrong credentials, config mistakes) are never reported.
|
|
177
|
-
|
|
178
|
-
Turn it off completely:
|
|
184
|
+
Turn it all off:
|
|
179
185
|
|
|
180
186
|
```json
|
|
181
|
-
{
|
|
182
|
-
"reporting": { "enabled": false }
|
|
183
|
-
}
|
|
187
|
+
{ "reporting": { "enabled": false } }
|
|
184
188
|
```
|
|
185
189
|
|
|
186
|
-
…or set `SAP_ADT_MCP_REPORT=0` (also accepts `false`/`no`/`off`).
|
|
187
|
-
|
|
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/)). |
|
|
190
199
|
|
|
191
200
|
## Connect a client
|
|
192
201
|
|
package/config.example.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sap-adt-mcp",
|
|
3
|
-
"version": "0.8.
|
|
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/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",
|
|
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
|
@@ -65,6 +65,10 @@ function parseReporting(raw) {
|
|
|
65
65
|
enabled: envOff ? false : r.enabled !== false,
|
|
66
66
|
endpoint: typeof r.endpoint === "string" && r.endpoint ? r.endpoint : null,
|
|
67
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,
|
|
68
72
|
};
|
|
69
73
|
}
|
|
70
74
|
|
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
CHANGED
|
@@ -123,16 +123,84 @@ function appFrame(stack) {
|
|
|
123
123
|
return "";
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
function
|
|
127
|
-
|
|
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 ?? "")
|
|
128
132
|
.replace(/0x[0-9a-f]+/gi, "")
|
|
129
133
|
.replace(/['"`][^'"`]*['"`]/g, "")
|
|
130
134
|
.replace(/\d+/g, "#")
|
|
131
135
|
.replace(/\s+/g, " ")
|
|
132
136
|
.trim()
|
|
133
137
|
.toLowerCase();
|
|
134
|
-
|
|
135
|
-
|
|
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)}`);
|
|
136
204
|
}
|
|
137
205
|
|
|
138
206
|
// --- Reporter factory --------------------------------------------------------
|
|
@@ -142,16 +210,20 @@ export function createReporter(config, pkg) {
|
|
|
142
210
|
const enabled = rep.enabled !== false;
|
|
143
211
|
const endpoint = rep.endpoint || DEFAULT_ENDPOINT;
|
|
144
212
|
const includeArgs = rep.includeArgs !== false;
|
|
213
|
+
const adtErrors = rep.adtErrors !== false;
|
|
214
|
+
const allowManual = rep.allowManual !== false;
|
|
145
215
|
const redact = makeRedactor(collectSecrets(config.systems));
|
|
146
216
|
const seen = new Set(); // per-process de-dup: one POST per fingerprint per run.
|
|
147
217
|
|
|
148
|
-
|
|
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) {
|
|
149
221
|
try {
|
|
150
222
|
await fetch(endpoint, {
|
|
151
223
|
method: "POST",
|
|
152
224
|
headers: {
|
|
153
225
|
"content-type": "application/json",
|
|
154
|
-
"x-report-source":
|
|
226
|
+
"x-report-source": source,
|
|
155
227
|
},
|
|
156
228
|
body: JSON.stringify(payload),
|
|
157
229
|
signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
|
|
@@ -161,6 +233,19 @@ export function createReporter(config, pkg) {
|
|
|
161
233
|
}
|
|
162
234
|
}
|
|
163
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.
|
|
164
249
|
function report(err, context = {}) {
|
|
165
250
|
try {
|
|
166
251
|
if (!enabled) return;
|
|
@@ -170,34 +255,103 @@ export function createReporter(config, pkg) {
|
|
|
170
255
|
seen.add(fp);
|
|
171
256
|
|
|
172
257
|
const payload = {
|
|
173
|
-
|
|
174
|
-
build: BUILD_FINGERPRINT,
|
|
175
|
-
version: pkg.version,
|
|
176
|
-
node: process.version,
|
|
177
|
-
os: `${os.platform()} ${os.release()}`,
|
|
258
|
+
...envelope("crash", fp),
|
|
178
259
|
tool: context.tool ?? null,
|
|
179
260
|
errorName: err.name ?? "Error",
|
|
180
261
|
message: redact(err.message ?? ""),
|
|
181
262
|
stack: redact(err.stack ?? ""),
|
|
182
|
-
timestamp: new Date().toISOString(),
|
|
183
263
|
};
|
|
184
264
|
if (includeArgs && context.args !== undefined) {
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
dump = JSON.stringify(context.args);
|
|
188
|
-
} catch {
|
|
189
|
-
dump = "<unserializable>";
|
|
190
|
-
}
|
|
191
|
-
payload.args = redact(dump);
|
|
265
|
+
payload.args = redact(safeStringify(context.args));
|
|
192
266
|
}
|
|
193
|
-
return send(payload);
|
|
267
|
+
return send(payload, "sap-adt-mcp");
|
|
194
268
|
} catch {
|
|
195
269
|
// A reporter that crashes the tool would be worse than no reporter.
|
|
196
270
|
}
|
|
197
271
|
}
|
|
198
272
|
|
|
199
|
-
|
|
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
|
+
};
|
|
200
346
|
}
|
|
201
347
|
|
|
202
348
|
// Exposed for unit tests.
|
|
203
|
-
export const _internals = {
|
|
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
|
@@ -32,6 +32,7 @@ import * as cdsTools from "./tools/cds.js";
|
|
|
32
32
|
import * as worklistTools from "./tools/worklist.js";
|
|
33
33
|
import * as jobTools from "./tools/jobs.js";
|
|
34
34
|
import * as rapTools from "./tools/rap.js";
|
|
35
|
+
import * as reportTools from "./tools/report.js";
|
|
35
36
|
|
|
36
37
|
const PKG = JSON.parse(
|
|
37
38
|
readFileSync(
|
|
@@ -91,7 +92,7 @@ function getClient(systemName) {
|
|
|
91
92
|
return { name, client: clientCache.get(name), profile };
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
const ctx = { getClient, config };
|
|
95
|
+
const ctx = { getClient, config, reporter };
|
|
95
96
|
|
|
96
97
|
const TOOL_MODULES = [
|
|
97
98
|
connectionTools,
|
|
@@ -110,6 +111,7 @@ const TOOL_MODULES = [
|
|
|
110
111
|
worklistTools,
|
|
111
112
|
jobTools,
|
|
112
113
|
rapTools,
|
|
114
|
+
reportTools,
|
|
113
115
|
];
|
|
114
116
|
|
|
115
117
|
const tools = [];
|
|
@@ -147,7 +149,14 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
147
149
|
const handler = handlers[name];
|
|
148
150
|
if (!handler) return textResult(`Unknown tool: ${name}`, true);
|
|
149
151
|
try {
|
|
150
|
-
|
|
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;
|
|
151
160
|
} catch (err) {
|
|
152
161
|
if (err instanceof ReadOnlyViolationError) {
|
|
153
162
|
return textResult(
|
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"));
|