sap-adt-mcp 0.8.2 → 0.8.4
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/package.json +1 -1
- package/src/adt-client.js +15 -0
- package/src/object-uris.js +3 -0
- package/src/reporter.js +25 -3
- package/src/tools/discovery.js +10 -1
- package/src/tools/source.js +14 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sap-adt-mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
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",
|
package/src/adt-client.js
CHANGED
|
@@ -240,6 +240,21 @@ export class AdtClient {
|
|
|
240
240
|
{ cause: err }
|
|
241
241
|
);
|
|
242
242
|
}
|
|
243
|
+
// undici throws a generic "fetch failed" whose real reason (ECONNRESET,
|
|
244
|
+
// ENOTFOUND, TLS error, socket hang up…) lives on err.cause. Surface a
|
|
245
|
+
// clear message and carry the underlying code so this reads as the
|
|
246
|
+
// network/environment problem it is — not a tool bug.
|
|
247
|
+
if (err instanceof TypeError && /fetch failed/i.test(err.message ?? "")) {
|
|
248
|
+
const cause = err.cause;
|
|
249
|
+
const reason = cause?.code || cause?.message || "network error";
|
|
250
|
+
const wrapped = new Error(
|
|
251
|
+
`ADT request failed (${method} ${adtPath}): ${reason}. ` +
|
|
252
|
+
`Check connectivity to the SAP host, VPN, and TLS/cert settings.`
|
|
253
|
+
);
|
|
254
|
+
wrapped.code = cause?.code ?? "ADT_FETCH_FAILED";
|
|
255
|
+
wrapped.cause = err;
|
|
256
|
+
throw wrapped;
|
|
257
|
+
}
|
|
243
258
|
throw err;
|
|
244
259
|
} finally {
|
|
245
260
|
clearTimeout(timer);
|
package/src/object-uris.js
CHANGED
|
@@ -72,6 +72,9 @@ export function normalizeType(input) {
|
|
|
72
72
|
|
|
73
73
|
export function objectUri({ type, name, group }) {
|
|
74
74
|
const t = normalizeType(type);
|
|
75
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
76
|
+
throw new Error("Object name is required");
|
|
77
|
+
}
|
|
75
78
|
const n = name.toLowerCase();
|
|
76
79
|
switch (t) {
|
|
77
80
|
case "PROG":
|
package/src/reporter.js
CHANGED
|
@@ -29,9 +29,10 @@ const MAX_FIELD = 4000;
|
|
|
29
29
|
|
|
30
30
|
const SKIP_NAMES = new Set(["ReadOnlyViolationError", "AbortError"]);
|
|
31
31
|
|
|
32
|
-
// Configuration / setup mistakes — the user's
|
|
32
|
+
// Configuration / setup mistakes and input-validation errors — the user's
|
|
33
|
+
// environment or a mis-shaped tool call, not a bug in the tool.
|
|
33
34
|
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
|
+
/(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|is required\b|Unsupported object type|ADT request failed|ADT request timed out|fetch failed)/i;
|
|
35
36
|
|
|
36
37
|
// Network / TLS problems live on the user's side (firewall, VPN, cert, host down).
|
|
37
38
|
const NETWORK_CODES = new Set([
|
|
@@ -48,15 +49,30 @@ const NETWORK_CODES = new Set([
|
|
|
48
49
|
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
49
50
|
"CERT_HAS_EXPIRED",
|
|
50
51
|
"ERR_TLS_CERT_ALTNAME_INVALID",
|
|
52
|
+
// undici / our wrapped network failures.
|
|
53
|
+
"ADT_FETCH_FAILED",
|
|
54
|
+
"UND_ERR_SOCKET",
|
|
55
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
56
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
57
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
58
|
+
"EPROTO",
|
|
59
|
+
"ECONNABORTED",
|
|
51
60
|
]);
|
|
52
61
|
|
|
62
|
+
function isNetworkError(err) {
|
|
63
|
+
if (err?.code && NETWORK_CODES.has(err.code)) return true;
|
|
64
|
+
// undici nests the real reason on .cause.
|
|
65
|
+
if (err?.cause?.code && NETWORK_CODES.has(err.cause.code)) return true;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
53
69
|
// Authentication / authorization — wrong credentials or missing SAP roles.
|
|
54
70
|
const AUTH_RE = /\b(401|403)\b|unauthor|forbidden|invalid credential|logon failed/i;
|
|
55
71
|
|
|
56
72
|
function shouldReport(err) {
|
|
57
73
|
if (!err) return false;
|
|
58
74
|
if (SKIP_NAMES.has(err.name)) return false;
|
|
59
|
-
if (
|
|
75
|
+
if (isNetworkError(err)) return false;
|
|
60
76
|
const msg = String(err.message ?? "");
|
|
61
77
|
if (SKIP_MESSAGE_RE.test(msg)) return false;
|
|
62
78
|
if (AUTH_RE.test(msg)) return false;
|
|
@@ -168,6 +184,12 @@ const BUSINESS_TYPE_RE =
|
|
|
168
184
|
// toward NOT reporting; the relay labels survivors `auto-adt-error` for cheap
|
|
169
185
|
// human triage.
|
|
170
186
|
function shouldReportAdt(meta = {}) {
|
|
187
|
+
// adt_request is the raw escape hatch — the caller fully specifies the
|
|
188
|
+
// method/path/headers, so a non-2xx is their request shape, not a tool defect.
|
|
189
|
+
// Never auto-file these (the agent improvising over the escape hatch would
|
|
190
|
+
// otherwise spam the tracker).
|
|
191
|
+
if (meta.tool === "adt_request") return false;
|
|
192
|
+
|
|
171
193
|
const s = Number(meta.status);
|
|
172
194
|
const type = String(meta.type ?? "");
|
|
173
195
|
const t100id = String(meta.t100?.id ?? "");
|
package/src/tools/discovery.js
CHANGED
|
@@ -314,8 +314,17 @@ export function register({ getClient }) {
|
|
|
314
314
|
},
|
|
315
315
|
|
|
316
316
|
adt_list_packages: async (args) => {
|
|
317
|
+
// `package` is the field name on the sibling adt_browse_package tool, so
|
|
318
|
+
// callers reach for it here too — accept it as an alias for `root`.
|
|
319
|
+
const rootArg = args.root ?? args.package;
|
|
320
|
+
if (typeof rootArg !== "string" || rootArg.length === 0) {
|
|
321
|
+
return textResult(
|
|
322
|
+
"adt_list_packages: `root` is required (the root package to walk from, e.g. 'ZFLEET').",
|
|
323
|
+
true
|
|
324
|
+
);
|
|
325
|
+
}
|
|
317
326
|
const { client, name: sys } = getClient(args.system);
|
|
318
|
-
const root =
|
|
327
|
+
const root = rootArg.toUpperCase();
|
|
319
328
|
const max = args.maxPackages ?? 200;
|
|
320
329
|
const prefix = args.prefix ?? defaultDescendPrefix(root);
|
|
321
330
|
|
package/src/tools/source.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sourceUri, objectUri, normalizeType, METADATA_XML_ACCEPT } from "../object-uris.js";
|
|
2
2
|
import { acquireLock, releaseLock } from "../lock.js";
|
|
3
|
-
import { errorResult, jsonResult } from "../result.js";
|
|
3
|
+
import { errorResult, jsonResult, textResult } from "../result.js";
|
|
4
4
|
import { OBJECT_TYPE_HINT, SYSTEM_HINT } from "./_shared.js";
|
|
5
5
|
|
|
6
6
|
const TOP_LEVEL_KEYWORDS = [
|
|
@@ -272,6 +272,19 @@ export const tools = [
|
|
|
272
272
|
export function register({ getClient }) {
|
|
273
273
|
return {
|
|
274
274
|
adt_get_source: async (args) => {
|
|
275
|
+
if (typeof args.object !== "string" || args.object.length === 0) {
|
|
276
|
+
const hint =
|
|
277
|
+
args.name !== undefined
|
|
278
|
+
? " (you passed `name` — the field is `object`)"
|
|
279
|
+
: "";
|
|
280
|
+
return textResult(
|
|
281
|
+
`adt_get_source: \`object\` is required${hint}. Also use \`firstLine\`/\`lastLine\` (not \`line\`/\`endLine\`) to paginate.`,
|
|
282
|
+
true
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (typeof args.type !== "string" || args.type.length === 0) {
|
|
286
|
+
return textResult("adt_get_source: `type` is required (e.g. 'class', 'program', 'dataelement').", true);
|
|
287
|
+
}
|
|
275
288
|
const { client, name: sys } = getClient(args.system);
|
|
276
289
|
const t = normalizeType(args.type);
|
|
277
290
|
const path = sourceUri({
|