abap-mcp 0.3.2 → 0.4.0

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 CHANGED
@@ -18,11 +18,12 @@ of their time where the *files* are — editing abapGit repos, reviewing diffs,
18
18
  long before anything reaches a system. This server gives agents the missing feedback loop at that
19
19
  layer:
20
20
 
21
- - *"Does this ABAP parse? Is it clean?"* → `lint_abap`
22
- - *"How far is this classic report from ABAP Cloud?"* → `check_cloud_readiness`
21
+ - *"Does this ABAP parse? Is it clean? How does it perform?"* → `lint_abap` (+ focus packs)
22
+ - *"How far is this classic report from ABAP Cloud? Grade it."* → `check_cloud_readiness` (A–D)
23
+ - *"Did this rework make the code better or worse?"* → `compare_abap`
23
24
  - *"Is MARA a released API? What do I use instead?"* → `check_released_api`
24
25
  - *"Start me a correct RAP business object."* → `scaffold_rap_bo`
25
- - *"What's in this 4,000-line class?"* → `get_abap_outline`
26
+ - *"What's in this 4,000-line class? Draw it."* → `get_abap_outline` (+ Mermaid)
26
27
 
27
28
  ## Quickstart
28
29
 
@@ -54,9 +55,12 @@ Every tool is also a subcommand, so it works in terminals and CI where no MCP cl
54
55
 
55
56
  ```bash
56
57
  npx abap-mcp lint src/ # lint files or whole directories
57
- npx abap-mcp readiness src/ --fail-below 80 # repo-level ABAP Cloud readiness, CI-gateable
58
+ npx abap-mcp lint src/ --focus Performance # themed pass: Performance | Security | Styleguide
59
+ npx abap-mcp lint src/ --rules-file org.json # your org's abaplint rule pack, same engine
60
+ npx abap-mcp readiness src/ --fail-below 80 # repo-level ABAP Cloud readiness, scored + graded A–D
61
+ npx abap-mcp compare old/ new/ # rework verdict: findings resolved/introduced, grade movement
58
62
  npx abap-mcp scaffold --entity Travel --table ztravel --key travel_id --out ./out
59
- npx abap-mcp outline src/zcl_monster.clas.abap # navigate big objects
63
+ npx abap-mcp outline src/zcl_monster.clas.abap # navigate big objects (--mermaid for a diagram)
60
64
  npx abap-mcp released MARA I_Product # released-API status + CDS successor
61
65
  npx abap-mcp explain exit_or_check # rule rationale
62
66
  ```
@@ -77,14 +81,15 @@ loop condition), per-repo `.mcp.json`, and a GitHub Actions quality gate for aba
77
81
 
78
82
  | Tool | What it does |
79
83
  | --- | --- |
80
- | `lint_abap` | abaplint static analysis over ABAP/CDS/BDEF sources → structured findings with rule docs links. Presets: `style` (default, snippet-friendly), `full`, `syntax-only`; per-rule overrides. |
81
- | `check_cloud_readiness` | Dual-parse diff (classic baseline vs `Cloud`): statements that are valid today but illegal in ABAP Cloud become categorized blockers (dynpro, list output, native SQL, …) with a transparent score; code broken at the baseline is reported separately, not counted as migration work. Now also surfaces a **separate, dated released-API cross-check** (`releasedApiFindings`): direct access to non-released classic tables and deprecated-API usage found in the source, with CDS successor hints — informational, not folded into the score. |
84
+ | `lint_abap` | abaplint static analysis over ABAP/CDS/BDEF sources → structured findings with rule docs links. Presets: `style` (default, snippet-friendly), `full`, `syntax-only`; per-rule overrides for org rule packs; `focus` lens (`Performance` / `Security` / `Styleguide`) for themed reviews. |
85
+ | `check_cloud_readiness` | Dual-parse diff (classic baseline vs `Cloud`): statements that are valid today but illegal in ABAP Cloud become categorized blockers (dynpro, list output, native SQL, …) with a transparent score **and a density-banded A–D tech-debt grade**; code broken at the baseline is reported separately, not counted as migration work. Also surfaces a **separate, dated released-API cross-check** (`releasedApiFindings`): direct access to non-released classic tables and deprecated-API usage found in the source, with CDS successor hints — informational, not folded into the score. |
86
+ | `compare_abap` | Before/after verdict on a rework: lint findings resolved vs introduced (matched by content, so moved code isn't noise), blocker/score/grade movement, and classes/methods/FORMs added or removed. The objective referee for refactors and AI rewrites. |
82
87
  | `check_released_api` | Looks up objects (tables, CDS views, function modules, classes, …) in SAP's bundled Cloudification snapshot → `released` / `deprecated` / `not-released` per object, plus a curated CDS successor for common classic tables. The released-API half of readiness, offline. |
83
88
  | `scaffold_rap_bo` | Generates the canonical RAP managed-BO stack (root view, behavior definition `strict(2)` + optional draft, behavior class + handler locals, projection, metadata extension, OData V4 service definition) plus suggested table DDL, activation order and next steps. |
84
89
  | `list_abap_rules` | Browse abaplint's ~180 rules (filter by text or tag). |
85
90
  | `explain_abap_rule` | One rule in depth — rationale (often Clean ABAP), examples, docs URL. |
86
91
  | `format_abap` | Offline pretty-printer (keyword case + indentation). |
87
- | `get_abap_outline` | Classes/methods/visibility/interfaces/FORMs of a source — navigate big objects without reading them whole. |
92
+ | `get_abap_outline` | Classes/methods/visibility/interfaces/FORMs of a source — navigate big objects without reading them whole. Optional Mermaid classDiagram output for instant structure visuals. |
88
93
 
89
94
  ## Honesty box — what this is *not*
90
95
 
@@ -110,7 +115,7 @@ loop condition), per-repo `.mcp.json`, and a GitHub Actions quality gate for aba
110
115
 
111
116
  ```bash
112
117
  npm install
113
- npm run check # typecheck + 122 tests + build — the CI gate
118
+ npm run check # typecheck + 149 tests + build — the CI gate
114
119
  node dist/cli.js # stdio MCP server
115
120
  npx @modelcontextprotocol/inspector --cli node dist/cli.js --method tools/list
116
121
  ```
@@ -118,7 +123,7 @@ npx @modelcontextprotocol/inspector --cli node dist/cli.js --method tools/list
118
123
  Tool descriptions are CI-graded (a rubric test enforces verb-first names, when-to-use,
119
124
  non-goals, described params, worked examples — the
120
125
  [mcp-kit](https://github.com/palimkarakshay/mcp-kit) discipline; the full mcp-kit lint scores all
121
- eight tools 100/100).
126
+ nine tools 100/100).
122
127
 
123
128
  ## Design
124
129
 
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Before/after comparison of ABAP sources — the deterministic half of a
3
+ * rework review.
4
+ *
5
+ * Findings are matched by CONTENT (rule + message + offending line text),
6
+ * never by line number, so code that merely moved does not show up as
7
+ * regressed or fixed. Blocker count / score / grade movement comes from the
8
+ * same objective dual-parse diff check_cloud_readiness uses; this module
9
+ * adds no judgment of its own.
10
+ */
11
+ import type { AbapSource, AbapVersion, Finding, FocusTag } from "./engine.js";
12
+ import type { ReadinessGrade } from "./readiness.js";
13
+ export interface CompareSide {
14
+ findingCount: number;
15
+ cloudBlockerCount: number;
16
+ score: number;
17
+ grade: ReadinessGrade;
18
+ }
19
+ export interface OutlineChanges {
20
+ classesAdded: string[];
21
+ classesRemoved: string[];
22
+ /** "class.method", lower-cased. */
23
+ methodsAdded: string[];
24
+ methodsRemoved: string[];
25
+ formsAdded: string[];
26
+ formsRemoved: string[];
27
+ }
28
+ export interface CompareOptions {
29
+ version: AbapVersion;
30
+ preset: "style" | "full" | "syntax-only";
31
+ rules?: Record<string, unknown> | undefined;
32
+ focus?: FocusTag | undefined;
33
+ /** Baseline for the readiness halves; defaults to v758. */
34
+ baselineVersion?: AbapVersion | undefined;
35
+ }
36
+ export interface CompareReport {
37
+ /** Findings present before but gone after — improvements. */
38
+ resolved: Finding[];
39
+ /** Findings present only after — regressions. */
40
+ introduced: Finding[];
41
+ /** Findings present on both sides (content-matched). */
42
+ unchangedCount: number;
43
+ before: CompareSide;
44
+ after: CompareSide;
45
+ outlineChanges: OutlineChanges;
46
+ matchNote: string;
47
+ }
48
+ export declare const MATCH_NOTE: string;
49
+ export declare function compareAbap(before: AbapSource[], after: AbapSource[], opts: CompareOptions): CompareReport;
@@ -0,0 +1,80 @@
1
+ import { runAbaplint } from "./engine.js";
2
+ import { outlineAbap } from "./outline.js";
3
+ import { checkCloudReadiness } from "./readiness.js";
4
+ export const MATCH_NOTE = "Findings are matched by rule + message + offending line text, not line numbers — moved-but-unchanged code does " +
5
+ "not count as resolved or introduced. Lint numbers use the requested preset; blocker count, score and grade come " +
6
+ "from the same objective dual-parse diff as check_cloud_readiness. Lint-clean does not mean functionally " +
7
+ "equivalent — behavior can change while every number here improves.";
8
+ const findingKey = (f) => [f.rule, f.message, f.excerpt].join("\u0000");
9
+ function diffFindings(before, after) {
10
+ // Multiset match: two identical findings on the before side need two
11
+ // matches on the after side to count as unchanged.
12
+ const pool = new Map();
13
+ for (const f of before) {
14
+ const arr = pool.get(findingKey(f)) ?? [];
15
+ arr.push(f);
16
+ pool.set(findingKey(f), arr);
17
+ }
18
+ const introduced = [];
19
+ let unchangedCount = 0;
20
+ for (const f of after) {
21
+ const arr = pool.get(findingKey(f));
22
+ if (arr !== undefined && arr.length > 0) {
23
+ arr.pop();
24
+ unchangedCount += 1;
25
+ }
26
+ else {
27
+ introduced.push(f);
28
+ }
29
+ }
30
+ return { resolved: [...pool.values()].flat(), introduced, unchangedCount };
31
+ }
32
+ function outlineNames(files) {
33
+ const classes = new Set();
34
+ const methods = new Set();
35
+ const forms = new Set();
36
+ for (const o of outlineAbap(files)) {
37
+ for (const c of o.classes) {
38
+ classes.add(c.name.toLowerCase());
39
+ for (const m of c.methods)
40
+ methods.add(`${c.name}.${m.name}`.toLowerCase());
41
+ }
42
+ for (const f of o.forms)
43
+ forms.add(f.toLowerCase());
44
+ }
45
+ return { classes, methods, forms };
46
+ }
47
+ const addedFrom = (a, b) => [...b].filter((x) => !a.has(x)).sort();
48
+ const side = (findingCount, readiness) => ({
49
+ findingCount,
50
+ cloudBlockerCount: readiness.cloudBlockerCount,
51
+ score: readiness.score,
52
+ grade: readiness.grade,
53
+ });
54
+ export function compareAbap(before, after, opts) {
55
+ const lintOpts = { version: opts.version, preset: opts.preset, rules: opts.rules, focus: opts.focus };
56
+ const beforeLint = runAbaplint(before, lintOpts);
57
+ const afterLint = runAbaplint(after, lintOpts);
58
+ const { resolved, introduced, unchangedCount } = diffFindings(beforeLint.findings, afterLint.findings);
59
+ const baseline = opts.baselineVersion ?? "v758";
60
+ const beforeReadiness = checkCloudReadiness(before, baseline);
61
+ const afterReadiness = checkCloudReadiness(after, baseline);
62
+ const b = outlineNames(before);
63
+ const a = outlineNames(after);
64
+ return {
65
+ resolved,
66
+ introduced,
67
+ unchangedCount,
68
+ before: side(beforeLint.findings.length, beforeReadiness),
69
+ after: side(afterLint.findings.length, afterReadiness),
70
+ outlineChanges: {
71
+ classesAdded: addedFrom(b.classes, a.classes),
72
+ classesRemoved: addedFrom(a.classes, b.classes),
73
+ methodsAdded: addedFrom(b.methods, a.methods),
74
+ methodsRemoved: addedFrom(a.methods, b.methods),
75
+ formsAdded: addedFrom(b.forms, a.forms),
76
+ formsRemoved: addedFrom(a.forms, b.forms),
77
+ },
78
+ matchNote: MATCH_NOTE,
79
+ };
80
+ }
@@ -24,10 +24,22 @@ export type AbapVersion = (typeof ABAP_VERSIONS)[number];
24
24
  * statement, so agents can lint snippets without knowing the convention.
25
25
  */
26
26
  export declare function inferFilename(source: string, given?: string): string;
27
+ /**
28
+ * Curated rule-pack lenses — abaplint's own tags, so the pack is the
29
+ * analyzer's taxonomy, not a hand-maintained list that can drift.
30
+ */
31
+ export declare const FOCUS_TAGS: readonly ["Performance", "Security", "Styleguide"];
32
+ export type FocusTag = (typeof FOCUS_TAGS)[number];
27
33
  export interface RunOptions {
28
34
  version: AbapVersion;
29
- /** abaplint rule config; merged over the preset. */
35
+ /** abaplint rule config; merged over the preset (and over a focus filter). */
30
36
  rules?: Record<string, unknown> | undefined;
37
+ /**
38
+ * Keep only rules carrying this abaplint tag (parser errors always stay
39
+ * on — focused findings on unparseable code would be garbage). Ignored
40
+ * for "syntax-only", which has no opinionated rules to focus.
41
+ */
42
+ focus?: FocusTag | undefined;
31
43
  /**
32
44
  * "style" — abaplint's default ruleset minus whole-program semantic
33
45
  * checks, so isolated snippets don't drown in noise about
@@ -10,6 +10,7 @@
10
10
  * Callers may omit them; we infer from the source's leading statement.
11
11
  */
12
12
  import * as abaplint from "@abaplint/core";
13
+ import { listRules } from "./rules.js";
13
14
  export const MAX_FILES = 32;
14
15
  export const MAX_FILE_CHARS = 100_000;
15
16
  export const MAX_FINDINGS = 500;
@@ -89,6 +90,11 @@ function boundFiles(files) {
89
90
  return { filename, source: f.source };
90
91
  });
91
92
  }
93
+ /**
94
+ * Curated rule-pack lenses — abaplint's own tags, so the pack is the
95
+ * analyzer's taxonomy, not a hand-maintained list that can drift.
96
+ */
97
+ export const FOCUS_TAGS = ["Performance", "Security", "Styleguide"];
92
98
  function buildConfig(opts) {
93
99
  let raw;
94
100
  if (opts.preset === "syntax-only") {
@@ -107,7 +113,16 @@ function buildConfig(opts) {
107
113
  }
108
114
  raw = def;
109
115
  }
116
+ if (opts.focus !== undefined && opts.preset !== "syntax-only") {
117
+ const tagged = new Set(listRules(undefined, opts.focus).map((r) => r.key));
118
+ const rules = raw["rules"];
119
+ for (const key of Object.keys(rules)) {
120
+ if (!tagged.has(key) && key !== "parser_error" && key !== "cds_parser_error")
121
+ rules[key] = false;
122
+ }
123
+ }
110
124
  if (opts.rules !== undefined) {
125
+ // Explicit per-rule overrides win over the preset AND over a focus filter.
111
126
  const rules = raw["rules"];
112
127
  for (const [k, v] of Object.entries(opts.rules))
113
128
  rules[k] = v;
@@ -23,3 +23,11 @@ export interface FileOutline {
23
23
  parseable: boolean;
24
24
  }
25
25
  export declare function outlineAbap(files: AbapSource[]): FileOutline[];
26
+ /**
27
+ * Render outlines as a Mermaid classDiagram: classes with method visibility
28
+ * (+/#/-) and attributes, inheritance (<|--), interface realization (<|..),
29
+ * and legacy FORMs as one pseudo-class per file. Deterministic text out —
30
+ * paste into any Mermaid renderer (GitHub, docs sites, diagram tools) for
31
+ * the visual; nothing here draws pixels.
32
+ */
33
+ export declare function outlineToMermaid(outlines: FileOutline[]): string;
@@ -68,3 +68,59 @@ export function outlineAbap(files) {
68
68
  }
69
69
  return out;
70
70
  }
71
+ const VIS_PREFIX = {
72
+ public: "+",
73
+ protected: "#",
74
+ private: "-",
75
+ };
76
+ /** Mermaid identifiers cannot carry ABAP's ~ / namespace slashes / dots. */
77
+ function mermaidName(name) {
78
+ return name.replace(/[^A-Za-z0-9_]/g, "_");
79
+ }
80
+ /**
81
+ * Render outlines as a Mermaid classDiagram: classes with method visibility
82
+ * (+/#/-) and attributes, inheritance (<|--), interface realization (<|..),
83
+ * and legacy FORMs as one pseudo-class per file. Deterministic text out —
84
+ * paste into any Mermaid renderer (GitHub, docs sites, diagram tools) for
85
+ * the visual; nothing here draws pixels.
86
+ */
87
+ export function outlineToMermaid(outlines) {
88
+ const lines = ["classDiagram"];
89
+ const declaredInterfaces = new Set();
90
+ const declareInterface = (name) => {
91
+ const id = mermaidName(name);
92
+ if (declaredInterfaces.has(id))
93
+ return;
94
+ declaredInterfaces.add(id);
95
+ lines.push(` class ${id} {`, " <<interface>>", " }");
96
+ };
97
+ for (const o of outlines) {
98
+ for (const i of o.interfaces)
99
+ declareInterface(i);
100
+ for (const c of o.classes) {
101
+ const id = mermaidName(c.name);
102
+ lines.push(` class ${id} {`);
103
+ if (c.isAbstract)
104
+ lines.push(" <<abstract>>");
105
+ for (const m of c.methods)
106
+ lines.push(` ${VIS_PREFIX[m.visibility]}${mermaidName(m.name)}()`);
107
+ for (const a of c.attributes)
108
+ lines.push(` ${mermaidName(a)}`);
109
+ lines.push(" }");
110
+ if (c.superClass !== null)
111
+ lines.push(` ${mermaidName(c.superClass)} <|-- ${id}`);
112
+ for (const i of c.interfaces) {
113
+ declareInterface(i);
114
+ lines.push(` ${mermaidName(i)} <|.. ${id}`);
115
+ }
116
+ }
117
+ if (o.forms.length > 0) {
118
+ const id = `${mermaidName(o.file.replace(/\..*$/, ""))}_forms`;
119
+ lines.push(` class ${id} {`);
120
+ for (const f of o.forms)
121
+ lines.push(` +${mermaidName(f)}()`);
122
+ lines.push(" }");
123
+ }
124
+ }
125
+ return lines.join("\n");
126
+ }
@@ -40,10 +40,24 @@ export interface ReleasedApiFinding {
40
40
  /** Human-facing explanation of why this was flagged. */
41
41
  note: string;
42
42
  }
43
+ /** Letter grade for tech-debt assessments — a banding of blocker density. */
44
+ export type ReadinessGrade = "A" | "B" | "C" | "D";
45
+ /**
46
+ * Band the objective blocker count into an A–D Clean Core tech-debt grade,
47
+ * normalized by file count so a single object and a whole package grade on
48
+ * the same scale: A = no blockers, B = ≤ 0.5 blockers/file, C = ≤ 2
49
+ * blockers/file, D = worse. Same number as the score, different lens —
50
+ * nothing subjective is mixed in.
51
+ */
52
+ export declare function gradeReadiness(cloudBlockerCount: number, fileCount: number): ReadinessGrade;
43
53
  export interface ReadinessReport {
44
54
  verdict: "ready" | "minor-rework" | "moderate-rework" | "significant-rework";
45
55
  score: number;
56
+ /** A–D banding of blocker density (blockers / files) — see gradeReadiness. */
57
+ grade: ReadinessGrade;
46
58
  cloudBlockerCount: number;
59
+ /** Files analyzed — the denominator of the grade's density banding. */
60
+ fileCount: number;
47
61
  categories: ReadinessCategory[];
48
62
  /** Findings that fail even at the classic baseline — fix these first; they are not migration items. */
49
63
  brokenAtBaseline: Finding[];
@@ -1,5 +1,22 @@
1
1
  import { extractObjectReferences, runAbaplint } from "./engine.js";
2
2
  import { lookupReleased, RELEASED_API_SNAPSHOT, suggestSuccessor } from "./released.js";
3
+ /**
4
+ * Band the objective blocker count into an A–D Clean Core tech-debt grade,
5
+ * normalized by file count so a single object and a whole package grade on
6
+ * the same scale: A = no blockers, B = ≤ 0.5 blockers/file, C = ≤ 2
7
+ * blockers/file, D = worse. Same number as the score, different lens —
8
+ * nothing subjective is mixed in.
9
+ */
10
+ export function gradeReadiness(cloudBlockerCount, fileCount) {
11
+ if (cloudBlockerCount === 0)
12
+ return "A";
13
+ const perFile = cloudBlockerCount / Math.max(1, fileCount);
14
+ if (perFile <= 0.5)
15
+ return "B";
16
+ if (perFile <= 2)
17
+ return "C";
18
+ return "D";
19
+ }
3
20
  export const SCOPE_NOTE = "Static parser-level analysis (abaplint) PLUS a released-API cross-check against SAP's bundled Cloudification " +
4
21
  `snapshot (dated ${RELEASED_API_SNAPSHOT.snapshotDate}). It detects statements ABAP Cloud removes (the objective ` +
5
22
  "cloud-blocker count and score) and, separately, flags deprecated-API usage and direct access to non-released " +
@@ -52,7 +69,9 @@ export function checkCloudReadiness(files, baselineVersion = "v758") {
52
69
  return {
53
70
  verdict,
54
71
  score,
72
+ grade: gradeReadiness(n, files.length),
55
73
  cloudBlockerCount: n,
74
+ fileCount: files.length,
56
75
  categories: [...byCategory.values()].sort((a, b) => b.count - a.count),
57
76
  brokenAtBaseline: baseline.findings,
58
77
  releasedApiFindings: computeReleasedApiFindings(files, baselineVersion),
@@ -31,6 +31,11 @@ export declare const lintAbap: import("./tool.js").ToolSpec<{
31
31
  "syntax-only": "syntax-only";
32
32
  }>>;
33
33
  rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
34
+ focus: z.ZodOptional<z.ZodEnum<{
35
+ Performance: "Performance";
36
+ Security: "Security";
37
+ Styleguide: "Styleguide";
38
+ }>>;
34
39
  }>;
35
40
  export declare const checkCloudReadinessTool: import("./tool.js").ToolSpec<{
36
41
  files: z.ZodArray<z.ZodObject<{
@@ -81,6 +86,7 @@ export declare const getAbapOutline: import("./tool.js").ToolSpec<{
81
86
  filename: z.ZodOptional<z.ZodString>;
82
87
  source: z.ZodString;
83
88
  }, z.core.$strip>>;
89
+ mermaid: z.ZodDefault<z.ZodBoolean>;
84
90
  }>;
85
91
  export declare const checkReleasedApiTool: import("./tool.js").ToolSpec<{
86
92
  objects: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
@@ -88,6 +94,39 @@ export declare const checkReleasedApiTool: import("./tool.js").ToolSpec<{
88
94
  type: z.ZodOptional<z.ZodString>;
89
95
  }, z.core.$strip>]>>;
90
96
  }>;
97
+ export declare const compareAbapTool: import("./tool.js").ToolSpec<{
98
+ before: z.ZodArray<z.ZodObject<{
99
+ filename: z.ZodOptional<z.ZodString>;
100
+ source: z.ZodString;
101
+ }, z.core.$strip>>;
102
+ after: z.ZodArray<z.ZodObject<{
103
+ filename: z.ZodOptional<z.ZodString>;
104
+ source: z.ZodString;
105
+ }, z.core.$strip>>;
106
+ abapVersion: z.ZodDefault<z.ZodEnum<{
107
+ Cloud: "Cloud";
108
+ v750: "v750";
109
+ v751: "v751";
110
+ v752: "v752";
111
+ v753: "v753";
112
+ v754: "v754";
113
+ v755: "v755";
114
+ v756: "v756";
115
+ v757: "v757";
116
+ v758: "v758";
117
+ }>>;
118
+ preset: z.ZodDefault<z.ZodEnum<{
119
+ style: "style";
120
+ full: "full";
121
+ "syntax-only": "syntax-only";
122
+ }>>;
123
+ rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
124
+ focus: z.ZodOptional<z.ZodEnum<{
125
+ Performance: "Performance";
126
+ Security: "Security";
127
+ Styleguide: "Styleguide";
128
+ }>>;
129
+ }>;
91
130
  /** Every tool this server exposes. (`tools` alias = the registry-export shape @mcp-kit/lint discovers.) */
92
131
  export declare const ALL_TOOLS: readonly AnyToolSpec[];
93
132
  export declare const tools: readonly AnyToolSpec[];
@@ -7,9 +7,10 @@
7
7
  * documentation is this file — treat it as the public API surface.
8
8
  */
9
9
  import { z } from "zod";
10
- import { ABAP_VERSIONS, runAbaplint } from "./abap/engine.js";
10
+ import { compareAbap } from "./abap/compare.js";
11
+ import { ABAP_VERSIONS, FOCUS_TAGS, runAbaplint } from "./abap/engine.js";
11
12
  import { formatAbap } from "./abap/formatter.js";
12
- import { outlineAbap } from "./abap/outline.js";
13
+ import { outlineAbap, outlineToMermaid } from "./abap/outline.js";
13
14
  import { checkCloudReadiness } from "./abap/readiness.js";
14
15
  import { lookupReleased, RELEASED_API_SNAPSHOT, suggestSuccessor, } from "./abap/released.js";
15
16
  import { explainRule, listRules } from "./abap/rules.js";
@@ -29,6 +30,22 @@ const filesField = z
29
30
  .max(32)
30
31
  .describe("Source files to analyze, up to 32 per call, 100k chars each.");
31
32
  const sevenSampleAbap = 'lint_abap({ "files": [ { "source": "REPORT ztest.\\nDATA foo TYPE i.\\nIF foo = 1.\\nENDIF." } ] })';
33
+ const focusField = z
34
+ .enum(FOCUS_TAGS)
35
+ .optional()
36
+ .describe('Curated rule-pack lens: report only rules carrying this abaplint tag — "Performance" for a tuning pass, ' +
37
+ '"Security" for a security sweep, "Styleguide" for Clean ABAP adherence. Parser errors always surface. ' +
38
+ 'Ignored with preset "syntax-only". Combine with rules to re-tune individual rules in the pack.');
39
+ const findingShape = z.object({
40
+ rule: z.string().describe("abaplint rule key."),
41
+ message: z.string().describe("Human-readable finding."),
42
+ severity: z.string().describe("Error, Warning or Info."),
43
+ file: z.string().describe("Filename the finding is in."),
44
+ line: z.number().describe("1-based line."),
45
+ column: z.number().describe("1-based column."),
46
+ excerpt: z.string().describe("The offending line, trimmed."),
47
+ docsUrl: z.string().describe("Rule documentation at rules.abaplint.org."),
48
+ });
32
49
  export const lintAbap = defineTool({
33
50
  name: "lint_abap",
34
51
  title: "Lint ABAP source",
@@ -38,8 +55,9 @@ export const lintAbap = defineTool({
38
55
  "anywhere near a system — it runs entirely offline on the provided text. " +
39
56
  "It does not connect to any SAP system, does not run ATC, and cannot judge whether referenced objects exist " +
40
57
  "unless you provide them in the same call (preset \"style\", the default, skips whole-program checks for that reason; " +
41
- "preset \"full\" enables them when you provide every dependency). For an ABAP-Cloud migration verdict use " +
42
- "check_cloud_readiness instead. " +
58
+ "preset \"full\" enables them when you provide every dependency). A focus tag turns a pass into a themed review " +
59
+ "(performance / security / Clean ABAP style) without hand-picking rules; rule overrides layer a team's own pack " +
60
+ "on top. For an ABAP-Cloud migration verdict use check_cloud_readiness instead. " +
43
61
  `Example: ${sevenSampleAbap}.`,
44
62
  inputSchema: {
45
63
  files: filesField,
@@ -51,19 +69,11 @@ export const lintAbap = defineTool({
51
69
  rules: z
52
70
  .record(z.string(), z.unknown())
53
71
  .optional()
54
- .describe('abaplint rule overrides merged onto the preset, e.g. { "line_length": { "length": 120 }, "7bit_ascii": false }.'),
72
+ .describe('abaplint rule overrides merged onto the preset (and onto a focus filter), e.g. { "line_length": { "length": 120 }, "7bit_ascii": false } — encode an org\'s best-practice pack here.'),
73
+ focus: focusField,
55
74
  },
56
75
  outputSchema: {
57
- findings: z.array(z.object({
58
- rule: z.string().describe("abaplint rule key."),
59
- message: z.string().describe("Human-readable finding."),
60
- severity: z.string().describe("Error, Warning or Info."),
61
- file: z.string().describe("Filename the finding is in."),
62
- line: z.number().describe("1-based line."),
63
- column: z.number().describe("1-based column."),
64
- excerpt: z.string().describe("The offending line, trimmed."),
65
- docsUrl: z.string().describe("Rule documentation at rules.abaplint.org."),
66
- })),
76
+ findings: z.array(findingShape),
67
77
  truncated: z.boolean().describe("True if more than 500 findings existed and the list was cut."),
68
78
  fileCount: z.number().describe("Number of files analyzed."),
69
79
  },
@@ -83,12 +93,20 @@ export const lintAbap = defineTool({
83
93
  rules: { line_length: { length: 120 } },
84
94
  },
85
95
  },
96
+ {
97
+ description: "Performance-focused pass over a report.",
98
+ arguments: {
99
+ files: [{ source: "REPORT zperf.\nSELECT * FROM mara INTO TABLE @DATA(lt_mara)." }],
100
+ focus: "Performance",
101
+ },
102
+ },
86
103
  ],
87
104
  handler: (args) => {
88
105
  const result = runAbaplint(args.files, {
89
106
  version: args.abapVersion,
90
107
  preset: args.preset,
91
108
  rules: args.rules,
109
+ focus: args.focus,
92
110
  });
93
111
  const text = result.findings.length === 0
94
112
  ? `No findings in ${result.fileCount} file(s).`
@@ -107,10 +125,10 @@ export const checkCloudReadinessTool = defineTool({
107
125
  description: "Assess how far ABAP source is from ABAP Cloud (Clean Core tier 1) by parsing it twice — once at a classic " +
108
126
  "baseline (default v758) and once at version Cloud — and diffing: findings that appear only at Cloud are genuine " +
109
127
  "cloud blockers (statements ABAP Cloud removed), reported in categories (dynpro, list output, native SQL, report " +
110
- "events, …) with a transparent score and verdict; findings already present at the baseline are reported separately " +
111
- "as broken code, not migration work. " +
112
- "Use this when someone asks 'is this code cloud-ready / Clean Core compliant / S/4HANA-cloud safe' or before " +
113
- "porting classic ABAP into an ABAP Cloud environment. " +
128
+ "events, …) with a transparent score, an A–D tech-debt grade and a verdict; findings already present at the " +
129
+ "baseline are reported separately as broken code, not migration work. " +
130
+ "Use this when someone asks 'is this code cloud-ready / Clean Core compliant / S/4HANA-cloud safe', before " +
131
+ "porting classic ABAP into an ABAP Cloud environment, or for a graded tech-debt assessment of an abapGit export. " +
114
132
  "It is static and parser-level: it does not check released-API usage (that needs a system's ATC), does not " +
115
133
  "connect to any SAP system, and a 'ready' verdict means no language-level blockers — not a certification. " +
116
134
  'Example: check_cloud_readiness({ "files": [ { "source": "REPORT zold.\\nWRITE: / \'hi\'." } ] }).',
@@ -123,7 +141,11 @@ export const checkCloudReadinessTool = defineTool({
123
141
  .enum(["ready", "minor-rework", "moderate-rework", "significant-rework"])
124
142
  .describe("Banded verdict from the blocker count (0 / ≤5 / ≤20 / >20)."),
125
143
  score: z.number().describe("100 − 5×blockers, floored at 0. Transparent, not an oracle."),
144
+ grade: z
145
+ .enum(["A", "B", "C", "D"])
146
+ .describe("Clean Core tech-debt grade banded on blocker density: A = no blockers, B = ≤ 0.5 blockers/file, C = ≤ 2 blockers/file, D = more. The same objective count as the score, sized for assessment reports."),
126
147
  cloudBlockerCount: z.number().describe("Statements valid at the baseline but not in ABAP Cloud."),
148
+ fileCount: z.number().describe("Files analyzed — the denominator of the grade's density banding."),
127
149
  categories: z.array(z.object({
128
150
  category: z.string().describe("Stable category id, e.g. dynpro, list-output, native-sql."),
129
151
  label: z.string().describe("What this category means and the usual remediation."),
@@ -154,7 +176,7 @@ export const checkCloudReadinessTool = defineTool({
154
176
  handler: (args) => {
155
177
  const report = checkCloudReadiness(args.files, args.baselineVersion);
156
178
  const catLine = report.categories.map((c) => `${c.category}=${c.count}`).join(", ");
157
- const text = `${report.verdict} (score ${report.score}): ${report.cloudBlockerCount} cloud blocker(s)` +
179
+ const text = `${report.verdict} (score ${report.score}, grade ${report.grade}): ${report.cloudBlockerCount} cloud blocker(s)` +
158
180
  (catLine.length > 0 ? ` [${catLine}]` : "") +
159
181
  (report.brokenAtBaseline.length > 0
160
182
  ? `; ${report.brokenAtBaseline.length} finding(s) broken at ${report.baselineVersion} regardless`
@@ -378,13 +400,23 @@ export const getAbapOutline = defineTool({
378
400
  description: "Return the structural outline of ABAP sources — classes (with methods, visibility, attributes, interfaces, " +
379
401
  "inheritance), interfaces, and FORM routines — without you having to read the whole file. " +
380
402
  "Use this when navigating a large class or legacy program to decide which part to read or edit next; it is the " +
381
- "cheap first call before pulling thousands of lines into context. It does not return method bodies or analyze " +
403
+ "cheap first call before pulling thousands of lines into context. Set mermaid: true to also get the structure as " +
404
+ "a Mermaid classDiagram (inheritance, interface realization, method visibility) for documentation visuals. " +
405
+ "It does not return method bodies or analyze " +
382
406
  "code quality (use lint_abap for that), and CDS/behavior-definition files yield an empty outline. " +
383
407
  'Example: get_abap_outline({ "files": [ { "filename": "zcl_big.clas.abap", "source": "CLASS zcl_big DEFINITION…" } ] }).',
384
408
  inputSchema: {
385
409
  files: filesField,
410
+ mermaid: z
411
+ .boolean()
412
+ .default(false)
413
+ .describe("Also return the outline as Mermaid classDiagram source — render it anywhere Mermaid renders (GitHub, docs sites) for an instant structure diagram."),
386
414
  },
387
415
  outputSchema: {
416
+ mermaid: z
417
+ .string()
418
+ .optional()
419
+ .describe("Mermaid classDiagram source for all files; present only when requested."),
388
420
  outlines: z.array(z.object({
389
421
  file: z.string().describe("Filename."),
390
422
  parseable: z.boolean().describe("False when the file is not an ABAP object (or unparseable)."),
@@ -421,9 +453,13 @@ export const getAbapOutline = defineTool({
421
453
  return `${o.file}: ${parts.join("; ") || "(empty)"}`;
422
454
  })
423
455
  .join("\n");
456
+ const mermaid = args.mermaid ? outlineToMermaid(outlines) : undefined;
424
457
  return {
425
- content: [{ type: "text", text }],
426
- structuredContent: { outlines: outlines },
458
+ content: [{ type: "text", text: mermaid !== undefined ? `${text}\n\n\`\`\`mermaid\n${mermaid}\n\`\`\`` : text }],
459
+ structuredContent: {
460
+ outlines: outlines,
461
+ ...(mermaid !== undefined ? { mermaid } : {}),
462
+ },
427
463
  };
428
464
  },
429
465
  });
@@ -529,10 +565,100 @@ export const checkReleasedApiTool = defineTool({
529
565
  };
530
566
  },
531
567
  });
568
+ const compareSideShape = z.object({
569
+ findingCount: z.number().describe("Total lint findings on this side."),
570
+ cloudBlockerCount: z.number().describe("ABAP Cloud blockers on this side (objective dual-parse diff)."),
571
+ score: z.number().describe("Readiness score on this side (100 − 5×blockers, floored at 0)."),
572
+ grade: z.enum(["A", "B", "C", "D"]).describe("Density-banded Clean Core grade on this side."),
573
+ });
574
+ export const compareAbapTool = defineTool({
575
+ name: "compare_abap",
576
+ title: "Compare two ABAP versions",
577
+ description: "Compare a BEFORE and an AFTER version of ABAP source and report what a rework actually changed: lint findings " +
578
+ "resolved and introduced (matched by content, so moved-but-unchanged code is not noise), cloud-blocker / score / " +
579
+ "A–D grade movement from the same dual-parse diff as check_cloud_readiness, and structural changes — classes, " +
580
+ "methods and FORMs added or removed. " +
581
+ "Use this when reviewing a refactor, a modernization step or an AI-generated rewrite of an existing object and " +
582
+ "you need an objective better-or-worse verdict instead of eyeballing a diff. " +
583
+ "It is not a textual diff tool (use git diff to see the edits) and it cannot judge functional equivalence — " +
584
+ "behavior can change while every number improves; it does not connect to any SAP system. " +
585
+ 'Example: compare_abap({ "before": [ { "source": "REPORT zr.\\nWRITE 1." } ], "after": [ { "source": "REPORT zr.\\nWRITE 2." } ] }).',
586
+ inputSchema: {
587
+ before: filesField.describe("The BEFORE sources — the current/old version of the object(s). Up to 32 files, 100k chars each."),
588
+ after: filesField.describe("The AFTER sources — the reworked version being judged. Up to 32 files, 100k chars each."),
589
+ abapVersion: VERSION_ENUM.default("v758").describe('ABAP language version both sides are linted against. "v758" (default) is current on-prem; "Cloud" is ABAP Cloud.'),
590
+ preset: z
591
+ .enum(["style", "full", "syntax-only"])
592
+ .default("style")
593
+ .describe('Lint preset applied identically to both sides: "style" (default) for isolated snippets, "full" when every referenced object is provided, "syntax-only" for parser errors only.'),
594
+ rules: z
595
+ .record(z.string(), z.unknown())
596
+ .optional()
597
+ .describe('abaplint rule overrides applied to both sides, e.g. { "line_length": { "length": 120 } }.'),
598
+ focus: focusField,
599
+ },
600
+ outputSchema: {
601
+ resolved: z.array(findingShape).describe("Findings present before but gone after — improvements."),
602
+ introduced: z.array(findingShape).describe("Findings present only after — regressions to fix."),
603
+ unchangedCount: z.number().describe("Findings present on both sides (content-matched)."),
604
+ before: compareSideShape.describe("Lint and readiness numbers for the BEFORE side."),
605
+ after: compareSideShape.describe("Lint and readiness numbers for the AFTER side."),
606
+ outlineChanges: z.object({
607
+ classesAdded: z.array(z.string()).describe("Class names present only after."),
608
+ classesRemoved: z.array(z.string()).describe("Class names present only before."),
609
+ methodsAdded: z.array(z.string()).describe('Methods present only after, as "class.method".'),
610
+ methodsRemoved: z.array(z.string()).describe('Methods present only before, as "class.method".'),
611
+ formsAdded: z.array(z.string()).describe("FORM routines present only after."),
612
+ formsRemoved: z.array(z.string()).describe("FORM routines present only before (removing FORMs is usually progress)."),
613
+ }),
614
+ matchNote: z.string().describe("How findings were matched and what the numbers do and do not mean."),
615
+ },
616
+ annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
617
+ examples: [
618
+ {
619
+ description: "Judge a WRITE-report rewritten as a class.",
620
+ arguments: {
621
+ before: [{ source: "REPORT zold.\nWRITE: / 'hi'." }],
622
+ after: [
623
+ {
624
+ source: "CLASS zcl_new DEFINITION PUBLIC FINAL CREATE PUBLIC.\n PUBLIC SECTION.\n METHODS get RETURNING VALUE(rv) TYPE string.\nENDCLASS.\nCLASS zcl_new IMPLEMENTATION.\n METHOD get.\n rv = 'hi'.\n ENDMETHOD.\nENDCLASS.",
625
+ },
626
+ ],
627
+ },
628
+ },
629
+ {
630
+ description: "Performance-focused before/after check of a tuning change.",
631
+ arguments: {
632
+ before: [{ source: "REPORT zperf.\nSELECT * FROM mara INTO TABLE @DATA(lt)." }],
633
+ after: [{ source: "REPORT zperf.\nSELECT matnr FROM mara INTO TABLE @DATA(lt)." }],
634
+ focus: "Performance",
635
+ },
636
+ },
637
+ ],
638
+ handler: (args) => {
639
+ const report = compareAbap(args.before, args.after, {
640
+ version: args.abapVersion,
641
+ preset: args.preset,
642
+ rules: args.rules,
643
+ focus: args.focus,
644
+ });
645
+ const oc = report.outlineChanges;
646
+ const structural = oc.classesAdded.length + oc.classesRemoved.length + oc.methodsAdded.length + oc.methodsRemoved.length + oc.formsAdded.length + oc.formsRemoved.length;
647
+ const text = `${report.introduced.length} introduced, ${report.resolved.length} resolved, ${report.unchangedCount} unchanged finding(s); ` +
648
+ `blockers ${report.before.cloudBlockerCount}→${report.after.cloudBlockerCount}, ` +
649
+ `score ${report.before.score}→${report.after.score}, grade ${report.before.grade}→${report.after.grade}` +
650
+ (structural > 0 ? `; ${structural} structural change(s)` : "");
651
+ return {
652
+ content: [{ type: "text", text }],
653
+ structuredContent: report,
654
+ };
655
+ },
656
+ });
532
657
  /** Every tool this server exposes. (`tools` alias = the registry-export shape @mcp-kit/lint discovers.) */
533
658
  export const ALL_TOOLS = [
534
659
  lintAbap,
535
660
  checkCloudReadinessTool,
661
+ compareAbapTool,
536
662
  scaffoldRapBoTool,
537
663
  checkReleasedApiTool,
538
664
  listAbapRules,
@@ -16,8 +16,9 @@ export declare function mergeReadiness(reports: ReadinessReport[], baseline: Aba
16
16
  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
+ export declare function cmdCompare(argv: string[], io: CliIo): number;
19
20
  export declare function cmdExplain(argv: string[], io: CliIo): number;
20
21
  export declare function cmdReleased(argv: string[], io: CliIo): number;
21
22
  export declare function cmdRules(argv: string[], io: CliIo): number;
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";
23
+ 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] [--focus Performance|Security|Styleguide] [--rules-file abaplint.json] [--json]\n abap-mcp readiness [paths\u2026] ABAP Cloud readiness diff, scored + graded A\u2013D [--baseline v758] [--fail-below N] [--json]\n abap-mcp compare BEFORE AFTER what a rework changed: findings resolved/introduced, blocker/score/grade movement, structure [--preset \u2026] [--focus \u2026] [--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 [--mermaid] [--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";
23
24
  export declare function runCli(argv: string[], io: CliIo): number | null;
@@ -7,9 +7,10 @@
7
7
  */
8
8
  import { readdirSync, readFileSync, realpathSync, statSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
9
9
  import { basename, extname, join } from "node:path";
10
- import { ABAP_VERSIONS, MAX_FILES, runAbaplint } from "./abap/engine.js";
11
- import { outlineAbap } from "./abap/outline.js";
12
- import { checkCloudReadiness, SCOPE_NOTE } from "./abap/readiness.js";
10
+ import { compareAbap } from "./abap/compare.js";
11
+ import { ABAP_VERSIONS, FOCUS_TAGS, MAX_FILES, runAbaplint } from "./abap/engine.js";
12
+ import { outlineAbap, outlineToMermaid } from "./abap/outline.js";
13
+ import { checkCloudReadiness, gradeReadiness, SCOPE_NOTE } from "./abap/readiness.js";
13
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";
@@ -80,6 +81,31 @@ function asVersion(v, fallback) {
80
81
  return v;
81
82
  throw new Error(`Unknown ABAP version "${v}". Valid: ${ABAP_VERSIONS.join(", ")}`);
82
83
  }
84
+ function asFocus(v) {
85
+ if (typeof v !== "string")
86
+ return undefined;
87
+ const match = FOCUS_TAGS.find((t) => t.toLowerCase() === v.toLowerCase());
88
+ if (match === undefined)
89
+ throw new Error(`Unknown focus "${v}". Valid: ${FOCUS_TAGS.join(", ")}`);
90
+ return match;
91
+ }
92
+ function asPreset(v) {
93
+ return v === "full" || v === "syntax-only" ? v : "style";
94
+ }
95
+ /** Read rule overrides from a JSON file — either a bare rules map or a full abaplint.json with a "rules" key. */
96
+ function rulesFromFile(v) {
97
+ if (typeof v !== "string")
98
+ return undefined;
99
+ let parsed;
100
+ try {
101
+ parsed = JSON.parse(readFileSync(v, "utf8"));
102
+ }
103
+ catch (err) {
104
+ throw new Error(`Cannot read --rules-file ${v}: ${err instanceof Error ? err.message : String(err)}`);
105
+ }
106
+ const inner = parsed["rules"];
107
+ return typeof inner === "object" && inner !== null ? inner : parsed;
108
+ }
83
109
  function fmtFinding(f) {
84
110
  return `${f.file}:${f.line}:${f.column} [${f.severity}] ${f.rule}: ${f.message}`;
85
111
  }
@@ -91,11 +117,12 @@ export function cmdLint(argv, io) {
91
117
  return 2;
92
118
  }
93
119
  const version = asVersion(flags.get("abap-version"), "v758");
94
- const presetRaw = flags.get("preset");
95
- const preset = presetRaw === "full" || presetRaw === "syntax-only" ? presetRaw : "style";
120
+ const preset = asPreset(flags.get("preset"));
121
+ const focus = asFocus(flags.get("focus"));
122
+ const rules = rulesFromFile(flags.get("rules-file"));
96
123
  const all = [];
97
124
  for (const batch of chunk(files, MAX_FILES)) {
98
- all.push(...runAbaplint(batch, { version, preset }).findings);
125
+ all.push(...runAbaplint(batch, { version, preset, focus, rules }).findings);
99
126
  }
100
127
  if (flags.has("json")) {
101
128
  io.out(JSON.stringify({ files: files.length, findings: all }, null, 2));
@@ -103,7 +130,7 @@ export function cmdLint(argv, io) {
103
130
  else {
104
131
  for (const f of all)
105
132
  io.out(fmtFinding(f));
106
- io.out(`${all.length} finding(s) in ${files.length} file(s) [${preset} @ ${version}]`);
133
+ io.out(`${all.length} finding(s) in ${files.length} file(s) [${preset}${focus !== undefined ? `:${focus}` : ""} @ ${version}]`);
107
134
  }
108
135
  return all.some((f) => f.severity === "Error") ? 1 : 0;
109
136
  }
@@ -111,11 +138,13 @@ export function cmdLint(argv, io) {
111
138
  export function mergeReadiness(reports, baseline) {
112
139
  const categories = new Map();
113
140
  let blockers = 0;
141
+ let fileCount = 0;
114
142
  const broken = [];
115
143
  const releasedApiFindings = [];
116
144
  let snapshotDate = "";
117
145
  for (const r of reports) {
118
146
  blockers += r.cloudBlockerCount;
147
+ fileCount += r.fileCount;
119
148
  broken.push(...r.brokenAtBaseline);
120
149
  releasedApiFindings.push(...r.releasedApiFindings);
121
150
  snapshotDate = r.releasedApiSnapshotDate;
@@ -140,7 +169,9 @@ export function mergeReadiness(reports, baseline) {
140
169
  return {
141
170
  verdict,
142
171
  score,
172
+ grade: gradeReadiness(blockers, fileCount),
143
173
  cloudBlockerCount: blockers,
174
+ fileCount,
144
175
  categories: [...categories.values()].sort((a, b) => b.count - a.count),
145
176
  brokenAtBaseline: broken,
146
177
  releasedApiFindings,
@@ -163,7 +194,7 @@ export function cmdReadiness(argv, io) {
163
194
  io.out(JSON.stringify({ files: files.length, ...merged }, null, 2));
164
195
  }
165
196
  else {
166
- io.out(`ABAP Cloud readiness: ${merged.verdict} (score ${merged.score})`);
197
+ io.out(`ABAP Cloud readiness: ${merged.verdict} (score ${merged.score}, grade ${merged.grade})`);
167
198
  io.out(`${merged.cloudBlockerCount} cloud blocker(s) across ${files.length} file(s)`);
168
199
  for (const c of merged.categories)
169
200
  io.out(` ${c.category.padEnd(18)} ${String(c.count).padStart(4)} ${c.label}`);
@@ -252,6 +283,10 @@ export function cmdOutline(argv, io) {
252
283
  return 2;
253
284
  }
254
285
  const outlines = chunk(files, MAX_FILES).flatMap((b) => outlineAbap(b));
286
+ if (flags.has("mermaid")) {
287
+ io.out(outlineToMermaid(outlines));
288
+ return 0;
289
+ }
255
290
  if (flags.has("json")) {
256
291
  io.out(JSON.stringify(outlines, null, 2));
257
292
  return 0;
@@ -271,6 +306,57 @@ export function cmdOutline(argv, io) {
271
306
  }
272
307
  return 0;
273
308
  }
309
+ export function cmdCompare(argv, io) {
310
+ const { flags, rest } = parseFlags(argv);
311
+ if (rest.length !== 2) {
312
+ io.err("Usage: abap-mcp compare BEFORE_PATH AFTER_PATH [--abap-version v758|Cloud] [--preset style|full|syntax-only] [--focus Performance|Security|Styleguide] [--rules-file abaplint.json] [--json]");
313
+ return 2;
314
+ }
315
+ const before = collectFiles([rest[0]], io);
316
+ const after = collectFiles([rest[1]], io);
317
+ if (before.length === 0 || after.length === 0) {
318
+ io.err("No ABAP sources found on one side.");
319
+ return 2;
320
+ }
321
+ if (before.length > MAX_FILES || after.length > MAX_FILES) {
322
+ io.err(`compare is object-level: at most ${MAX_FILES} files per side — narrow each path to the object(s) under review.`);
323
+ return 2;
324
+ }
325
+ const report = compareAbap(before, after, {
326
+ version: asVersion(flags.get("abap-version"), "v758"),
327
+ preset: asPreset(flags.get("preset")),
328
+ focus: asFocus(flags.get("focus")),
329
+ rules: rulesFromFile(flags.get("rules-file")),
330
+ });
331
+ if (flags.has("json")) {
332
+ io.out(JSON.stringify(report, null, 2));
333
+ }
334
+ else {
335
+ io.out(`lint: ${report.introduced.length} introduced, ${report.resolved.length} resolved, ${report.unchangedCount} unchanged`);
336
+ for (const f of report.introduced)
337
+ io.out(` + ${fmtFinding(f)}`);
338
+ for (const f of report.resolved)
339
+ io.out(` - ${fmtFinding(f)}`);
340
+ io.out(`readiness: blockers ${report.before.cloudBlockerCount} → ${report.after.cloudBlockerCount}, ` +
341
+ `score ${report.before.score} → ${report.after.score}, grade ${report.before.grade} → ${report.after.grade}`);
342
+ const oc = report.outlineChanges;
343
+ const structural = [
344
+ ...oc.classesAdded.map((s) => `+ class ${s}`),
345
+ ...oc.classesRemoved.map((s) => `- class ${s}`),
346
+ ...oc.methodsAdded.map((s) => `+ method ${s}`),
347
+ ...oc.methodsRemoved.map((s) => `- method ${s}`),
348
+ ...oc.formsAdded.map((s) => `+ form ${s}`),
349
+ ...oc.formsRemoved.map((s) => `- form ${s}`),
350
+ ];
351
+ if (structural.length > 0) {
352
+ io.out("structure:");
353
+ for (const s of structural)
354
+ io.out(` ${s}`);
355
+ }
356
+ }
357
+ // Regression gate: new findings or more cloud blockers fail the rework.
358
+ return report.introduced.length > 0 || report.after.cloudBlockerCount > report.before.cloudBlockerCount ? 1 : 0;
359
+ }
274
360
  export function cmdExplain(argv, io) {
275
361
  const { rest } = parseFlags(argv);
276
362
  const key = rest[0];
@@ -320,10 +406,11 @@ export const USAGE = `abap-mcp — SAP ABAP analysis for AI agents (MCP server)
320
406
 
321
407
  Usage:
322
408
  abap-mcp start the MCP server on stdio (for AI clients)
323
- abap-mcp lint [paths…] lint files/dirs [--abap-version v758|Cloud] [--preset style|full|syntax-only] [--json]
324
- abap-mcp readiness [paths…] ABAP Cloud readiness diff [--baseline v758] [--fail-below N] [--json]
409
+ abap-mcp lint [paths…] lint files/dirs [--abap-version v758|Cloud] [--preset style|full|syntax-only] [--focus Performance|Security|Styleguide] [--rules-file abaplint.json] [--json]
410
+ abap-mcp readiness [paths…] ABAP Cloud readiness diff, scored + graded A–D [--baseline v758] [--fail-below N] [--json]
411
+ abap-mcp compare BEFORE AFTER what a rework changed: findings resolved/introduced, blocker/score/grade movement, structure [--preset …] [--focus …] [--json]
325
412
  abap-mcp scaffold … generate a RAP managed BO (--entity --table --key [--fields n:type,…] [--no-draft] [--provided-key] [--out DIR])
326
- abap-mcp outline [paths…] classes/methods/forms structure [--json]
413
+ abap-mcp outline [paths…] classes/methods/forms structure [--mermaid] [--json]
327
414
  abap-mcp released <names…> released-API status from the bundled SAP snapshot [--type TABL|FUNC|…] [--json]
328
415
  abap-mcp explain <rule> explain an abaplint rule
329
416
  abap-mcp rules list rules [--query q] [--tag Security]
@@ -339,6 +426,8 @@ export function runCli(argv, io) {
339
426
  return cmdLint(rest, io);
340
427
  case "readiness":
341
428
  return cmdReadiness(rest, io);
429
+ case "compare":
430
+ return cmdCompare(rest, io);
342
431
  case "scaffold":
343
432
  return cmdScaffold(rest, io);
344
433
  case "outline":
package/dist/index.d.ts CHANGED
@@ -1,17 +1,20 @@
1
1
  /** Library surface — embed the tools or the server in your own process. */
2
2
  export { buildServer, SERVER_NAME, SERVER_VERSION } from "./server.js";
3
3
  export { ALL_TOOLS } from "./abap.tools.js";
4
- export { runAbaplint, inferFilename, ABAP_VERSIONS } from "./abap/engine.js";
5
- export type { AbapSource, AbapVersion, Finding } from "./abap/engine.js";
6
- export { checkCloudReadiness } from "./abap/readiness.js";
7
- export type { ReadinessReport, ReleasedApiFinding } from "./abap/readiness.js";
4
+ export { runAbaplint, inferFilename, ABAP_VERSIONS, FOCUS_TAGS } from "./abap/engine.js";
5
+ export type { AbapSource, AbapVersion, Finding, FocusTag } from "./abap/engine.js";
6
+ export { checkCloudReadiness, gradeReadiness } from "./abap/readiness.js";
7
+ export type { ReadinessReport, ReadinessGrade, ReleasedApiFinding } from "./abap/readiness.js";
8
+ export { compareAbap } from "./abap/compare.js";
9
+ export type { CompareReport, CompareOptions, CompareSide, OutlineChanges } from "./abap/compare.js";
8
10
  export { lookupReleased, suggestSuccessor, RELEASED_API_SNAPSHOT } from "./abap/released.js";
9
11
  export type { ReleasedLookup, ReleasedState } from "./abap/released.js";
10
12
  export { scaffoldRapBo, snakeToCamel } from "./abap/scaffold.js";
11
13
  export type { ScaffoldOptions, ScaffoldResult } from "./abap/scaffold.js";
12
14
  export { listRules, explainRule } from "./abap/rules.js";
13
15
  export { formatAbap } from "./abap/formatter.js";
14
- export { outlineAbap } from "./abap/outline.js";
16
+ export { outlineAbap, outlineToMermaid } from "./abap/outline.js";
17
+ export type { FileOutline, ClassOutline, MethodOutline } from "./abap/outline.js";
15
18
  export { defineTool, registerTool, registerTools } from "./tool.js";
16
19
  export type { ToolSpec, AnyToolSpec, ToolExample } from "./tool.js";
17
20
  export { McpToolError, invalidInput, notFound } from "./errors.js";
package/dist/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  /** Library surface — embed the tools or the server in your own process. */
2
2
  export { buildServer, SERVER_NAME, SERVER_VERSION } from "./server.js";
3
3
  export { ALL_TOOLS } from "./abap.tools.js";
4
- export { runAbaplint, inferFilename, ABAP_VERSIONS } from "./abap/engine.js";
5
- export { checkCloudReadiness } from "./abap/readiness.js";
4
+ export { runAbaplint, inferFilename, ABAP_VERSIONS, FOCUS_TAGS } from "./abap/engine.js";
5
+ export { checkCloudReadiness, gradeReadiness } from "./abap/readiness.js";
6
+ export { compareAbap } from "./abap/compare.js";
6
7
  export { lookupReleased, suggestSuccessor, RELEASED_API_SNAPSHOT } from "./abap/released.js";
7
8
  export { scaffoldRapBo, snakeToCamel } from "./abap/scaffold.js";
8
9
  export { listRules, explainRule } from "./abap/rules.js";
9
10
  export { formatAbap } from "./abap/formatter.js";
10
- export { outlineAbap } from "./abap/outline.js";
11
+ export { outlineAbap, outlineToMermaid } from "./abap/outline.js";
11
12
  export { defineTool, registerTool, registerTools } from "./tool.js";
12
13
  export { McpToolError, invalidInput, notFound } from "./errors.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abap-mcp",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "mcpName": "io.github.palimkarakshay/abap-mcp",
5
5
  "description": "MCP server for SAP ABAP: offline static analysis (abaplint), ABAP Cloud / Clean Core readiness checks, and RAP scaffolding — no SAP system or credentials required.",
6
6
  "license": "MIT",