sap-adt-mcp 0.8.47 → 0.8.49

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.47",
3
+ "version": "0.8.49",
4
4
  "mcpName": "io.github.yzonur/sap-adt-mcp",
5
5
  "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.",
6
6
  "type": "module",
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "scripts": {
23
23
  "start": "node src/server.js",
24
- "test": "node --test test/adt-error.test.js test/atc-bulk.test.js test/audit.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/panel.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",
24
+ "test": "node --test test/adt-error.test.js test/atc-bulk.test.js test/audit.test.js test/cds.test.js test/config.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/panel.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",
25
25
  "lint": "eslint src test"
26
26
  },
27
27
  "keywords": [
package/src/adt-client.js CHANGED
@@ -168,8 +168,15 @@ export class AdtClient {
168
168
  const token = res.headers.get("x-csrf-token");
169
169
  if (!token || token.toLowerCase() === "required") {
170
170
  const body = await res.text().catch(() => "");
171
+ // An HTML body means the host answered with a web page (SSO/login form or a
172
+ // web-dispatcher error), not the ADT service — usually a wrong host/system
173
+ // or a system fronted by SSO that basic auth can't pass.
174
+ const looksHtml = /^\s*<(!doctype html|html\b)/i.test(body);
175
+ const hint = looksHtml
176
+ ? " — the host returned an HTML page (likely an SSO/login page or wrong host), not the ADT service. Check the system's host and that it accepts basic-auth ADT access."
177
+ : "";
171
178
  throw new Error(
172
- `Failed to fetch CSRF token (status ${res.status}): ${body.slice(0, 200)}`
179
+ `Failed to fetch CSRF token (status ${res.status})${hint}: ${body.slice(0, 200)}`
173
180
  );
174
181
  }
175
182
  this.csrfToken = token;
package/src/config.js CHANGED
@@ -2,14 +2,18 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import os from "node:os";
4
4
 
5
- const CANDIDATE_PATHS = [
6
- process.env.SAP_ADT_MCP_CONFIG,
7
- path.join(os.homedir(), ".sap-adt-mcp", "config.json"),
8
- path.join(process.cwd(), "config.json"),
9
- ].filter(Boolean);
5
+ // Resolved at call time (not import time) so SAP_ADT_MCP_CONFIG is honoured
6
+ // whenever loadConfig runs, not just whatever it was when this module loaded.
7
+ function candidatePaths() {
8
+ return [
9
+ process.env.SAP_ADT_MCP_CONFIG,
10
+ path.join(os.homedir(), ".sap-adt-mcp", "config.json"),
11
+ path.join(process.cwd(), "config.json"),
12
+ ].filter(Boolean);
13
+ }
10
14
 
11
15
  export function loadConfig() {
12
- const configPath = CANDIDATE_PATHS.find((p) => fs.existsSync(p));
16
+ const configPath = candidatePaths().find((p) => fs.existsSync(p));
13
17
  if (!configPath) {
14
18
  throw new Error(
15
19
  "No config found. Set SAP_ADT_MCP_CONFIG or create ~/.sap-adt-mcp/config.json " +
@@ -31,7 +35,7 @@ export function loadConfig() {
31
35
  const systems = {};
32
36
  for (const [name, profile] of Object.entries(raw.systems ?? {})) {
33
37
  systems[name] = {
34
- host: requireString(profile.host, `${name}.host`),
38
+ host: requireHost(profile.host, name),
35
39
  client: profile.client != null ? String(profile.client) : undefined,
36
40
  language: profile.language != null ? String(profile.language) : undefined,
37
41
  user: requireString(profile.user, `${name}.user`),
@@ -122,6 +126,29 @@ function requireString(value, key) {
122
126
  return value;
123
127
  }
124
128
 
129
+ // A host without an http(s):// scheme (or an otherwise unparseable one) makes
130
+ // every request throw the cryptic "ADT path is not a valid URL component" at
131
+ // call time — once per tool call, far from the real cause. Validate the scheme
132
+ // up front so a bad config fails loudly at load with an actionable message.
133
+ function requireHost(value, systemName) {
134
+ const host = requireString(value, `${systemName}.host`);
135
+ let url;
136
+ try {
137
+ url = new URL(host);
138
+ } catch {
139
+ throw new Error(
140
+ `Config: ${systemName}.host must be a full URL including scheme, ` +
141
+ `e.g. "https://sap.example.com:44300" (got "${host}").`
142
+ );
143
+ }
144
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
145
+ throw new Error(
146
+ `Config: ${systemName}.host must use http:// or https:// (got "${url.protocol}//" in "${host}").`
147
+ );
148
+ }
149
+ return host;
150
+ }
151
+
125
152
  function resolveSecret(value, systemName) {
126
153
  if (typeof value !== "string") {
127
154
  throw new Error(`System ${systemName}: password must be a string`);
package/src/reporter.js CHANGED
@@ -32,7 +32,11 @@ const SKIP_NAMES = new Set(["ReadOnlyViolationError", "AbortError"]);
32
32
  // Configuration / setup mistakes and input-validation errors — the user's
33
33
  // environment or a mis-shaped tool call, not a bug in the tool.
34
34
  const SKIP_MESSAGE_RE =
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
+ // "Failed to fetch CSRF token": the host didn't behave like an ADT endpoint
36
+ // (SSO/login page, web dispatcher, wrong host/system) — environmental/auth, not
37
+ // a tool defect. The CSRF fetch is a fixed simple GET, so it never mis-shapes a
38
+ // request on our side (#62, and the deleted #58-60 cluster).
39
+ /(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|Failed to fetch CSRF token)/i;
36
40
 
37
41
  // Network / TLS problems live on the user's side (firewall, VPN, cert, host down).
38
42
  const NETWORK_CODES = new Set([
@@ -177,7 +181,9 @@ const BUSINESS_T100 = new Set([
177
181
  "ADT_DATAPREVIEW_MSG",
178
182
  ]);
179
183
  const BUSINESS_TYPE_RE =
180
- /SaveFailure|Lock|Enqueue|CTS_|ExceptionResourceAlreadyExists|NotFound/i;
184
+ // NoDependencyGraphDataCalculationPossible: SAP can't compute the dependency
185
+ // graph for a given CDS entity (system/data-side, not a tool defect — see #22/#61).
186
+ /SaveFailure|Lock|Enqueue|CTS_|ExceptionResourceAlreadyExists|NotFound|NoDependencyGraphDataCalculation/i;
181
187
 
182
188
  // Decide whether a non-2xx ADT response that a handler returned (not threw) is
183
189
  // likely a defect in *this* tool rather than a user/business condition. Errs
package/src/tools/cds.js CHANGED
@@ -101,7 +101,10 @@ export function register({ getClient }) {
101
101
  method: "POST",
102
102
  path: CDS_PREVIEW_PATH,
103
103
  query: { ddlSourceName: entity.toUpperCase(), rowNumber: String(max) },
104
- accept: "application/xml",
104
+ // The Data Preview endpoint only serializes a result set as this media
105
+ // type; plain application/xml 406s (ExceptionResourceNotAcceptable),
106
+ // same as the adt_read_table fix.
107
+ accept: "application/vnd.sap.adt.datapreview.table.v1+xml",
105
108
  });
106
109
  const text = await res.text();
107
110
  if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"));
@@ -1,9 +1,33 @@
1
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
- import { errorResult, jsonResult } from "../result.js";
4
+ import { errorResult, jsonResult, textResult } from "../result.js";
5
5
  import { OBJECT_TYPE_HINT, SYSTEM_HINT } from "./_shared.js";
6
6
 
7
+ // Guard the `objects` array before the handlers iterate it. Callers frequently
8
+ // reach for the singular `object`/`type` shape (as adt_get_source uses), which
9
+ // left `args.objects` undefined and crashed on `.map`. Return a clean tool
10
+ // error pointing at the right shape instead of throwing.
11
+ function validateObjects(tool, objects) {
12
+ if (!Array.isArray(objects) || objects.length === 0) {
13
+ return textResult(
14
+ `${tool}: \`objects\` is required — a non-empty array of { name, type[, group] }. ` +
15
+ "Note the plural: pass `objects: [{ name: '…', type: '…' }]`, not a singular `object`/`type`.",
16
+ true
17
+ );
18
+ }
19
+ for (let i = 0; i < objects.length; i++) {
20
+ const o = objects[i];
21
+ if (!o || typeof o.name !== "string" || typeof o.type !== "string") {
22
+ return textResult(
23
+ `${tool}: objects[${i}] must be { name: string, type: string }.`,
24
+ true
25
+ );
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+
7
31
  // Parse <atcfinding .../> elements from an ATC worklist result. Attribute names
8
32
  // vary slightly across releases — collect them all, normalized without prefix.
9
33
  const FINDING_RE = /<(?:atcfinding|atcworklist:finding|finding)\b([\s\S]*?)(?:\/>|>)/gi;
@@ -343,6 +367,8 @@ export function register({ getClient }) {
343
367
  },
344
368
 
345
369
  adt_run_unit_tests: async (args) => {
370
+ const objectsError = validateObjects("adt_run_unit_tests", args.objects);
371
+ if (objectsError) return objectsError;
346
372
  const { client, name: sys } = getClient(args.system);
347
373
  const refs = args.objects
348
374
  .map((o) => {
@@ -369,6 +395,8 @@ export function register({ getClient }) {
369
395
  },
370
396
 
371
397
  adt_run_atc: async (args) => {
398
+ const objectsError = validateObjects("adt_run_atc", args.objects);
399
+ if (objectsError) return objectsError;
372
400
  const { client, name: sys } = getClient(args.system);
373
401
  const refs = args.objects
374
402
  .map((o) => {