abap-mcp 0.2.0 → 0.3.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.
@@ -11,6 +11,7 @@ import { ABAP_VERSIONS, runAbaplint } from "./abap/engine.js";
11
11
  import { formatAbap } from "./abap/formatter.js";
12
12
  import { outlineAbap } from "./abap/outline.js";
13
13
  import { checkCloudReadiness } from "./abap/readiness.js";
14
+ import { lookupReleased, RELEASED_API_SNAPSHOT, suggestSuccessor, } from "./abap/released.js";
14
15
  import { explainRule, listRules } from "./abap/rules.js";
15
16
  import { scaffoldRapBo } from "./abap/scaffold.js";
16
17
  import { invalidInput } from "./errors.js";
@@ -132,6 +133,12 @@ export const checkCloudReadinessTool = defineTool({
132
133
  brokenAtBaseline: z
133
134
  .array(z.unknown())
134
135
  .describe("Findings that fail even at the baseline version — fix first, they are not migration items."),
136
+ releasedApiFindings: z
137
+ .array(z.unknown())
138
+ .describe("Released-API observations from the bundled SAP Cloudification snapshot (deprecated-API usage, direct non-released table access with successor hints). Informational — NOT counted in cloudBlockerCount or score."),
139
+ releasedApiSnapshotDate: z
140
+ .string()
141
+ .describe("Date of the bundled released-API snapshot the releasedApiFindings reflect."),
135
142
  baselineVersion: z.string().describe("The baseline used."),
136
143
  scopeNote: z.string().describe("Exactly what this check does and does not cover."),
137
144
  },
@@ -151,6 +158,9 @@ export const checkCloudReadinessTool = defineTool({
151
158
  (catLine.length > 0 ? ` [${catLine}]` : "") +
152
159
  (report.brokenAtBaseline.length > 0
153
160
  ? `; ${report.brokenAtBaseline.length} finding(s) broken at ${report.baselineVersion} regardless`
161
+ : "") +
162
+ (report.releasedApiFindings.length > 0
163
+ ? `; ${report.releasedApiFindings.length} released-API note(s) (snapshot ${report.releasedApiSnapshotDate})`
154
164
  : "");
155
165
  return {
156
166
  content: [{ type: "text", text }],
@@ -417,11 +427,114 @@ export const getAbapOutline = defineTool({
417
427
  };
418
428
  },
419
429
  });
430
+ /** Accept either a bare name string or a { name, type? } reference. */
431
+ const objectRefField = z
432
+ .array(z.union([
433
+ z.string().describe('A bare object name, e.g. "MARA" or "I_Product".'),
434
+ z.object({
435
+ name: z.string().describe('Object name, e.g. "MARA", "I_Product", "BAPI_MATERIAL_GET_DETAIL".'),
436
+ type: z
437
+ .string()
438
+ .optional()
439
+ .describe('Optional SAP object type to disambiguate same-named objects: "TABL" (table), "CDS_STOB" (CDS view entity), "FUNC" (function module), "CLAS", "INTF", "BDEF". Omit if unsure.'),
440
+ }),
441
+ ]))
442
+ .min(1)
443
+ .max(200)
444
+ .describe('Objects to check, 1–200 per call. Each is a bare name string or a { name, type? } object, e.g. ["MARA", { "name": "I_Product", "type": "CDS_STOB" }].');
445
+ export const checkReleasedApiTool = defineTool({
446
+ name: "check_released_api",
447
+ title: "Check ABAP released-API status",
448
+ description: "Look up ABAP repository objects (DB tables, CDS view entities, function modules, classes, interfaces, …) in " +
449
+ "SAP's published ABAP Cloudification list and report, per object, whether it is a 'released' API (safe to use in " +
450
+ "ABAP Cloud / Clean Core), 'deprecated' (released but being retired), or 'not-released' (a classic/internal object " +
451
+ "that is not a public API — e.g. most classic DDIC tables) — with a curated CDS successor hint for common tables. " +
452
+ `This reflects SAP's official Cloudification list as bundled in this package (snapshot ${RELEASED_API_SNAPSHOT.snapshotDate}); ` +
453
+ "it ships offline with the server. " +
454
+ "Use this when you need to know if your code may reference a given object in ABAP Cloud, or which released CDS view " +
455
+ "to use instead of a classic table — the released-API half of readiness that check_cloud_readiness deliberately " +
456
+ "leaves to a system's ATC. " +
457
+ "It does not connect to any SAP system, does not run ATC, and is only as current as the bundled snapshot — a " +
458
+ `system's own released-API list (ATC check API_RELEASE_STATE_CHECK / SAP_CP_READINESS) remains authoritative; treat ` +
459
+ "an 'absent from the list' result as 'not-released as of the snapshot', not as proof. " +
460
+ 'Example: check_released_api({ "objects": ["MARA", "I_Product", "BAPI_MATERIAL_GET_DETAIL"] }).',
461
+ inputSchema: {
462
+ objects: objectRefField,
463
+ },
464
+ outputSchema: {
465
+ snapshotDate: z.string().describe("Date of the bundled SAP Cloudification snapshot these results reflect."),
466
+ source: z.string().describe("URL of the SAP Apache-2.0 source the snapshot was built from."),
467
+ results: z.array(z.object({
468
+ name: z.string().describe("The object name as queried."),
469
+ objectType: z
470
+ .string()
471
+ .optional()
472
+ .describe("SAP object type of the matched record (TABL, CDS_STOB, FUNC, …), if found."),
473
+ state: z
474
+ .enum(["released", "deprecated", "not-released"])
475
+ .describe("'released' = safe public API; 'deprecated' = retiring; 'not-released' = not a public API."),
476
+ recorded: z
477
+ .boolean()
478
+ .describe("true = explicitly present in SAP's snapshot (under the requested type, if one was given); false = absent — 'not released as of the snapshot' by omission only."),
479
+ applicationComponent: z
480
+ .string()
481
+ .optional()
482
+ .describe("Owning application component of the matched record, if found."),
483
+ successor: z
484
+ .string()
485
+ .optional()
486
+ .describe("Curated released CDS view-entity successor for a classic table, when one is known."),
487
+ })),
488
+ },
489
+ annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
490
+ examples: [
491
+ {
492
+ description: "Check a classic table, a released CDS view, and a BAPI in one call.",
493
+ arguments: { objects: ["MARA", "I_Product", "BAPI_MATERIAL_GET_DETAIL"] },
494
+ },
495
+ {
496
+ description: "Disambiguate a name that exists under more than one object type.",
497
+ arguments: { objects: [{ name: "I_ProcurementProjectTP", type: "CDS_STOB" }] },
498
+ },
499
+ ],
500
+ handler: (args) => {
501
+ const results = args.objects.map((ref) => {
502
+ const name = typeof ref === "string" ? ref : ref.name;
503
+ const type = typeof ref === "string" ? undefined : ref.type;
504
+ const hit = lookupReleased(name, type);
505
+ const successor = suggestSuccessor(name);
506
+ return {
507
+ name: hit.name,
508
+ objectType: hit.objectType,
509
+ state: hit.state,
510
+ recorded: hit.recorded,
511
+ applicationComponent: hit.applicationComponent,
512
+ ...(successor !== undefined ? { successor } : {}),
513
+ };
514
+ });
515
+ const text = results
516
+ .map((r) => {
517
+ const tail = r.successor !== undefined ? ` → use ${r.successor}` : "";
518
+ const provenance = r.recorded ? "" : " (not in snapshot)";
519
+ return `${r.name}: ${r.state}${r.objectType !== undefined ? ` (${r.objectType})` : ""}${provenance}${tail}`;
520
+ })
521
+ .join("\n");
522
+ return {
523
+ content: [{ type: "text", text: `Snapshot ${RELEASED_API_SNAPSHOT.snapshotDate}\n${text}` }],
524
+ structuredContent: {
525
+ snapshotDate: RELEASED_API_SNAPSHOT.snapshotDate,
526
+ source: RELEASED_API_SNAPSHOT.source,
527
+ results,
528
+ },
529
+ };
530
+ },
531
+ });
420
532
  /** Every tool this server exposes. (`tools` alias = the registry-export shape @mcp-kit/lint discovers.) */
421
533
  export const ALL_TOOLS = [
422
534
  lintAbap,
423
535
  checkCloudReadinessTool,
424
536
  scaffoldRapBoTool,
537
+ checkReleasedApiTool,
425
538
  listAbapRules,
426
539
  explainAbapRule,
427
540
  formatAbapTool,
@@ -17,6 +17,7 @@ export declare function cmdReadiness(argv: string[], io: CliIo): number;
17
17
  export declare function cmdScaffold(argv: string[], io: CliIo): number;
18
18
  export declare function cmdOutline(argv: string[], io: CliIo): number;
19
19
  export declare function cmdExplain(argv: string[], io: CliIo): number;
20
+ export declare function cmdReleased(argv: string[], io: CliIo): number;
20
21
  export declare function cmdRules(argv: string[], io: CliIo): number;
21
- export declare const USAGE = "abap-mcp \u2014 SAP ABAP analysis for AI agents (MCP server) and humans (CLI)\n\nUsage:\n abap-mcp start the MCP server on stdio (for AI clients)\n abap-mcp lint [paths\u2026] lint files/dirs [--abap-version v758|Cloud] [--preset style|full|syntax-only] [--json]\n abap-mcp readiness [paths\u2026] ABAP Cloud readiness diff [--baseline v758] [--fail-below N] [--json]\n abap-mcp scaffold \u2026 generate a RAP managed BO (--entity --table --key [--fields n:type,\u2026] [--no-draft] [--provided-key] [--out DIR])\n abap-mcp outline [paths\u2026] classes/methods/forms structure [--json]\n abap-mcp explain <rule> explain an abaplint rule\n abap-mcp rules list rules [--query q] [--tag Security]\n\nExit codes: 0 ok \u00B7 1 findings/validation failed \u00B7 2 usage error";
22
+ export declare const USAGE = "abap-mcp \u2014 SAP ABAP analysis for AI agents (MCP server) and humans (CLI)\n\nUsage:\n abap-mcp start the MCP server on stdio (for AI clients)\n abap-mcp lint [paths\u2026] lint files/dirs [--abap-version v758|Cloud] [--preset style|full|syntax-only] [--json]\n abap-mcp readiness [paths\u2026] ABAP Cloud readiness diff [--baseline v758] [--fail-below N] [--json]\n abap-mcp scaffold \u2026 generate a RAP managed BO (--entity --table --key [--fields n:type,\u2026] [--no-draft] [--provided-key] [--out DIR])\n abap-mcp outline [paths\u2026] classes/methods/forms structure [--json]\n abap-mcp released <names\u2026> released-API status from the bundled SAP snapshot [--type TABL|FUNC|\u2026] [--json]\n abap-mcp explain <rule> explain an abaplint rule\n abap-mcp rules list rules [--query q] [--tag Security]\n\nExit codes: 0 ok \u00B7 1 findings/validation failed \u00B7 2 usage error";
22
23
  export declare function runCli(argv: string[], io: CliIo): number | null;
@@ -5,22 +5,30 @@
5
5
  * story); the *CLI* is a local developer tool, so reading files from disk here
6
6
  * is fine. Both call the identical engine — one definition of "clean".
7
7
  */
8
- import { readdirSync, readFileSync, statSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
8
+ import { readdirSync, readFileSync, realpathSync, statSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
9
9
  import { basename, extname, join } from "node:path";
10
10
  import { ABAP_VERSIONS, MAX_FILES, runAbaplint } from "./abap/engine.js";
11
11
  import { outlineAbap } from "./abap/outline.js";
12
12
  import { checkCloudReadiness, SCOPE_NOTE } from "./abap/readiness.js";
13
+ import { lookupReleased, RELEASED_API_SNAPSHOT, suggestSuccessor } from "./abap/released.js";
13
14
  import { explainRule, listRules } from "./abap/rules.js";
14
15
  import { scaffoldRapBo } from "./abap/scaffold.js";
15
16
  const ABAP_FILE_RE = /\.(clas\.abap|clas\.locals_imp\.abap|clas\.locals_def\.abap|clas\.testclasses\.abap|prog\.abap|intf\.abap|fugr\.abap|ddls\.asddls|bdef\.asbdef|srvd\.srvdsrv|ddlx\.asddlx)$/;
16
17
  /** Recursively collect analyzable sources from file/dir paths. */
17
18
  export function collectFiles(paths, io) {
18
19
  const found = [];
20
+ const visitedDirs = new Set();
19
21
  const visit = (p) => {
20
22
  const st = statSync(p);
21
23
  if (st.isDirectory()) {
22
24
  if (basename(p) === ".git" || basename(p) === "node_modules")
23
25
  return;
26
+ // statSync follows symlinks — track real paths so a symlink cycle
27
+ // can't recurse forever.
28
+ const real = realpathSync(p);
29
+ if (visitedDirs.has(real))
30
+ return;
31
+ visitedDirs.add(real);
24
32
  for (const entry of readdirSync(p))
25
33
  visit(join(p, entry));
26
34
  return;
@@ -104,9 +112,13 @@ export function mergeReadiness(reports, baseline) {
104
112
  const categories = new Map();
105
113
  let blockers = 0;
106
114
  const broken = [];
115
+ const releasedApiFindings = [];
116
+ let snapshotDate = "";
107
117
  for (const r of reports) {
108
118
  blockers += r.cloudBlockerCount;
109
119
  broken.push(...r.brokenAtBaseline);
120
+ releasedApiFindings.push(...r.releasedApiFindings);
121
+ snapshotDate = r.releasedApiSnapshotDate;
110
122
  for (const c of r.categories) {
111
123
  const cur = categories.get(c.category);
112
124
  if (cur === undefined)
@@ -131,6 +143,8 @@ export function mergeReadiness(reports, baseline) {
131
143
  cloudBlockerCount: blockers,
132
144
  categories: [...categories.values()].sort((a, b) => b.count - a.count),
133
145
  brokenAtBaseline: broken,
146
+ releasedApiFindings,
147
+ releasedApiSnapshotDate: snapshotDate,
134
148
  baselineVersion: baseline,
135
149
  scopeNote: SCOPE_NOTE,
136
150
  };
@@ -155,6 +169,11 @@ export function cmdReadiness(argv, io) {
155
169
  io.out(` ${c.category.padEnd(18)} ${String(c.count).padStart(4)} ${c.label}`);
156
170
  if (merged.brokenAtBaseline.length > 0)
157
171
  io.out(`${merged.brokenAtBaseline.length} finding(s) broken at ${baseline} regardless (fix first; not migration work)`);
172
+ if (merged.releasedApiFindings.length > 0) {
173
+ io.out(`${merged.releasedApiFindings.length} released-API note(s) (snapshot ${merged.releasedApiSnapshotDate}; informational, not scored):`);
174
+ for (const f of merged.releasedApiFindings)
175
+ io.out(` ${f.file}:${f.line} [${f.state}] ${f.object}${f.successor !== undefined ? ` → ${f.successor}` : ""}`);
176
+ }
158
177
  io.out(`Note: ${merged.scopeNote}`);
159
178
  }
160
179
  const failBelow = flags.get("fail-below");
@@ -202,7 +221,13 @@ export function cmdScaffold(argv, io) {
202
221
  writeFileSync(target, f.content, "utf8");
203
222
  io.out(`wrote ${target} [${f.validated}]`);
204
223
  }
205
- writeFileSync(join(outDir, `${sqlTable.toLowerCase()}.tabl.suggestion.txt`), result.suggestedTableDdl, "utf8");
224
+ const suggestionTarget = join(outDir, `${sqlTable.toLowerCase()}.tabl.suggestion.txt`);
225
+ if (existsSync(suggestionTarget) && !flags.has("force")) {
226
+ io.err(`refusing to overwrite ${suggestionTarget} (use --force)`);
227
+ return 1;
228
+ }
229
+ writeFileSync(suggestionTarget, result.suggestedTableDdl, "utf8");
230
+ io.out(`wrote ${suggestionTarget}`);
206
231
  }
207
232
  else {
208
233
  for (const f of result.files) {
@@ -257,6 +282,30 @@ export function cmdExplain(argv, io) {
257
282
  io.out(`${d.key} — ${d.title}\n${d.shortDescription}\n${d.extendedInformation}\nDocs: ${d.docsUrl}`);
258
283
  return 0;
259
284
  }
285
+ export function cmdReleased(argv, io) {
286
+ const { flags, rest } = parseFlags(argv);
287
+ if (rest.length === 0) {
288
+ io.err("Usage: abap-mcp released <object-name…> [--type TABL|CDS_STOB|FUNC|…] [--json]");
289
+ return 2;
290
+ }
291
+ const type = typeof flags.get("type") === "string" ? flags.get("type") : undefined;
292
+ const results = rest.map((name) => {
293
+ const hit = lookupReleased(name, type);
294
+ const successor = suggestSuccessor(name);
295
+ return { ...hit, successor };
296
+ });
297
+ if (flags.has("json")) {
298
+ io.out(JSON.stringify({ snapshotDate: RELEASED_API_SNAPSHOT.snapshotDate, source: RELEASED_API_SNAPSHOT.source, results }, null, 2));
299
+ return 0;
300
+ }
301
+ io.out(`Released-API status (SAP Cloudification snapshot ${RELEASED_API_SNAPSHOT.snapshotDate}):`);
302
+ for (const r of results) {
303
+ const tail = r.successor !== undefined ? ` → use ${r.successor}` : "";
304
+ const provenance = r.recorded ? "" : " (not in snapshot)";
305
+ io.out(` ${r.name.padEnd(34)} ${r.state.padEnd(13)} ${(r.objectType ?? "").padEnd(9)}${provenance}${tail}`);
306
+ }
307
+ return 0;
308
+ }
260
309
  export function cmdRules(argv, io) {
261
310
  const { flags } = parseFlags(argv);
262
311
  const q = flags.get("query");
@@ -275,6 +324,7 @@ Usage:
275
324
  abap-mcp readiness [paths…] ABAP Cloud readiness diff [--baseline v758] [--fail-below N] [--json]
276
325
  abap-mcp scaffold … generate a RAP managed BO (--entity --table --key [--fields n:type,…] [--no-draft] [--provided-key] [--out DIR])
277
326
  abap-mcp outline [paths…] classes/methods/forms structure [--json]
327
+ abap-mcp released <names…> released-API status from the bundled SAP snapshot [--type TABL|FUNC|…] [--json]
278
328
  abap-mcp explain <rule> explain an abaplint rule
279
329
  abap-mcp rules list rules [--query q] [--tag Security]
280
330
 
@@ -293,6 +343,8 @@ export function runCli(argv, io) {
293
343
  return cmdScaffold(rest, io);
294
344
  case "outline":
295
345
  return cmdOutline(rest, io);
346
+ case "released":
347
+ return cmdReleased(rest, io);
296
348
  case "explain":
297
349
  return cmdExplain(rest, io);
298
350
  case "rules":