abap-mcp 0.3.2 → 0.4.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.
- package/README.md +15 -10
- package/dist/abap/compare.d.ts +49 -0
- package/dist/abap/compare.js +80 -0
- package/dist/abap/engine.d.ts +13 -1
- package/dist/abap/engine.js +15 -0
- package/dist/abap/outline.d.ts +8 -0
- package/dist/abap/outline.js +56 -0
- package/dist/abap/readiness.d.ts +14 -0
- package/dist/abap/readiness.js +19 -0
- package/dist/abap.tools.d.ts +39 -0
- package/dist/abap.tools.js +149 -23
- package/dist/cli-commands.d.ts +2 -1
- package/dist/cli-commands.js +100 -11
- package/dist/index.d.ts +8 -5
- package/dist/index.js +4 -3
- package/package.json +5 -5
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
|
|
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
|
|
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 +
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/abap/engine.d.ts
CHANGED
|
@@ -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
|
package/dist/abap/engine.js
CHANGED
|
@@ -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;
|
package/dist/abap/outline.d.ts
CHANGED
|
@@ -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;
|
package/dist/abap/outline.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/abap/readiness.d.ts
CHANGED
|
@@ -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[];
|
package/dist/abap/readiness.js
CHANGED
|
@@ -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),
|
package/dist/abap.tools.d.ts
CHANGED
|
@@ -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[];
|
package/dist/abap.tools.js
CHANGED
|
@@ -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 {
|
|
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).
|
|
42
|
-
"
|
|
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(
|
|
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
|
|
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'
|
|
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.
|
|
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: {
|
|
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,
|
package/dist/cli-commands.d.ts
CHANGED
|
@@ -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;
|
package/dist/cli-commands.js
CHANGED
|
@@ -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 {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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
|
|
95
|
-
const
|
|
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
|
+
"version": "0.4.1",
|
|
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",
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"start": "node dist/cli.js"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@abaplint/core": "^2.119.
|
|
46
|
+
"@abaplint/core": "^2.119.27",
|
|
47
47
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
48
48
|
"zod": "^4.4.3"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@types/node": "^
|
|
52
|
-
"typescript": "^
|
|
53
|
-
"vitest": "^
|
|
51
|
+
"@types/node": "^25.9.3",
|
|
52
|
+
"typescript": "^6.0.3",
|
|
53
|
+
"vitest": "^4.1.8"
|
|
54
54
|
}
|
|
55
55
|
}
|