sap-adt-mcp 0.8.2 → 0.8.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-adt-mcp",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
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);
@@ -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 environment, not a bug.
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 (err.code && NETWORK_CODES.has(err.code)) return false;
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;
@@ -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 = args.root.toUpperCase();
327
+ const root = rootArg.toUpperCase();
319
328
  const max = args.maxPackages ?? 200;
320
329
  const prefix = args.prefix ?? defaultDescendPrefix(root);
321
330
 
@@ -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({