abap-mcp 0.1.1 → 0.3.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
@@ -6,8 +6,9 @@ an abapGit export, a code review, CI.
6
6
 
7
7
  Built on [abaplint](https://abaplint.org) (the open-source ABAP parser/linter) and the
8
8
  [Model Context Protocol](https://modelcontextprotocol.io). TypeScript, 100% local — the server
9
- makes **zero network calls** and touches **no filesystem**: sources go in as text, findings come
10
- back as structured JSON.
9
+ makes **zero network calls** and reads **no user files**: sources go in as text, findings come
10
+ back as structured JSON. (The released-API list and abaplint's rule data are package-bundled
11
+ assets that ship inside the install — no network, no user filesystem, at runtime.)
11
12
 
12
13
  ## Why this exists
13
14
 
@@ -19,6 +20,7 @@ layer:
19
20
 
20
21
  - *"Does this ABAP parse? Is it clean?"* → `lint_abap`
21
22
  - *"How far is this classic report from ABAP Cloud?"* → `check_cloud_readiness`
23
+ - *"Is MARA a released API? What do I use instead?"* → `check_released_api`
22
24
  - *"Start me a correct RAP business object."* → `scaffold_rap_bo`
23
25
  - *"What's in this 4,000-line class?"* → `get_abap_outline`
24
26
 
@@ -46,12 +48,38 @@ claude mcp add abap-mcp -- node /path/to/abap-mcp/dist/cli.js
46
48
  Then ask your agent things like *"lint this class against ABAP Cloud"*, *"is zold_report
47
49
  cloud-ready?"*, or *"scaffold a RAP BO for entity Booking on table zbooking, draft enabled"*.
48
50
 
51
+ ## CLI — same engine, no AI required
52
+
53
+ Every tool is also a subcommand, so it works in terminals and CI where no MCP client exists:
54
+
55
+ ```bash
56
+ 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 scaffold --entity Travel --table ztravel --key travel_id --out ./out
59
+ npx abap-mcp outline src/zcl_monster.clas.abap # navigate big objects
60
+ npx abap-mcp released MARA I_Product # released-API status + CDS successor
61
+ npx abap-mcp explain exit_or_check # rule rationale
62
+ ```
63
+
64
+ Directories are walked recursively (abapGit naming), batched automatically, and `readiness`
65
+ merges batches into one scored, categorized repo report. Exit codes are CI-friendly
66
+ (`1` on error findings / failed threshold).
67
+
68
+ ## Recipes, agents & CI
69
+
70
+ **[docs/COOKBOOK.md](docs/COOKBOOK.md)** — practical recipes: the fix-until-clean loop,
71
+ PR review without a transport, whole-repo migration triage, CI gates, per-persona use cases.
72
+ **[examples/claude-code/](examples/claude-code/)** — drop-in agentic workflows: an
73
+ `abap-code-reviewer` subagent, an `abap-cloud-migrator` sweep loop (readiness score as the
74
+ loop condition), per-repo `.mcp.json`, and a GitHub Actions quality gate for abapGit repos.
75
+
49
76
  ## Tools
50
77
 
51
78
  | Tool | What it does |
52
79
  | --- | --- |
53
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. |
54
- | `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. |
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. |
82
+ | `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. |
55
83
  | `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. |
56
84
  | `list_abap_rules` | Browse abaplint's ~180 rules (filter by text or tag). |
57
85
  | `explain_abap_rule` | One rule in depth — rationale (often Clean ABAP), examples, docs URL. |
@@ -60,25 +88,29 @@ cloud-ready?"*, or *"scaffold a RAP BO for entity Booking on table zbooking, dra
60
88
 
61
89
  ## Honesty box — what this is *not*
62
90
 
63
- - **Not ATC.** Readiness here is *language-level*: statements ABAP Cloud removed. Whether your
64
- code calls **released APIs only** requires a system's released-API list (ATC check
65
- `SAP_CP_READINESS`) out of scope for an offline tool, and the readiness report says so on
66
- every call.
91
+ - **Not ATC.** The objective readiness *score* is still language-level: statements ABAP Cloud
92
+ removed. Released-API coverage is now **partial and offline**: `check_released_api` and the
93
+ `releasedApiFindings` in readiness reflect SAP's published Cloudification list *as of the
94
+ bundled snapshot date* — they cover tables and function modules referenced in your source, not
95
+ every API, and are only as current as the snapshot. A target system's own released-API list
96
+ (ATC check `API_RELEASE_STATE_CHECK` / `SAP_CP_READINESS`) remains authoritative; treat an
97
+ "absent from the list" result as "not released as of the snapshot", not as proof.
67
98
  - **Scaffold validation is tiered.** Generated classes and CDS views are round-tripped through
68
99
  abaplint at Cloud level before they're returned (the generator and the linter share one
69
100
  parser). Behavior/service definitions are outside abaplint's checked surface — they are
70
101
  golden-tested canonical templates, and ADT activation is the final arbiter. Each generated
71
102
  file is labeled `validated: "abaplint" | "template"`.
72
- - **Text-in only, by design.** No filesystem walking, no network — the entire attack surface is
73
- a parser over strings you explicitly pass. For linting whole directories, use the
74
- [abaplint CLI](https://abaplint.org) in CI, or the
103
+ - **Text-in only, by design.** No user-filesystem walking, no network — the entire attack
104
+ surface is a parser over strings you explicitly pass. (The released-API snapshot and abaplint's
105
+ rule data are package-bundled assets imported from the install, not fetched or read from your
106
+ disk.) For linting whole directories, use the [abaplint CLI](https://abaplint.org) in CI, or the
75
107
  [mcp-kit `wrap-abaplint` recipe](https://github.com/palimkarakshay/mcp-kit) this server grew out of.
76
108
 
77
109
  ## Develop
78
110
 
79
111
  ```bash
80
112
  npm install
81
- npm run check # typecheck + 80 tests + build — the CI gate
113
+ npm run check # typecheck + 115 tests + build — the CI gate
82
114
  node dist/cli.js # stdio MCP server
83
115
  npx @modelcontextprotocol/inspector --cli node dist/cli.js --method tools/list
84
116
  ```
@@ -86,7 +118,7 @@ npx @modelcontextprotocol/inspector --cli node dist/cli.js --method tools/list
86
118
  Tool descriptions are CI-graded (a rubric test enforces verb-first names, when-to-use,
87
119
  non-goals, described params, worked examples — the
88
120
  [mcp-kit](https://github.com/palimkarakshay/mcp-kit) discipline; the full mcp-kit lint scores all
89
- seven tools 100/100).
121
+ eight tools 100/100).
90
122
 
91
123
  ## Design
92
124
 
@@ -98,6 +130,11 @@ scaffolder validates its own output, what was deliberately left out — lives in
98
130
 
99
131
  - [abaplint](https://github.com/abaplint/abaplint) by Lars Hvam — the parser and rule engine
100
132
  underneath every tool here (MIT).
133
+ - [SAP/abap-atc-cr-cv-s4hc](https://github.com/SAP/abap-atc-cr-cv-s4hc) — SAP's official ABAP
134
+ Cloudification Repository (object release list), **Apache-2.0**. The bundled released-API
135
+ snapshot (`src/data/released-apis.json`, snapshot **2026-06-10**) is a compact transform of
136
+ that data, redistributed under Apache-2.0 with attribution; see
137
+ [docs/DESIGN.md](docs/DESIGN.md) and `scripts/build-released-api-index.mjs` for the pipeline.
101
138
  - [mcp-kit](https://github.com/palimkarakshay/mcp-kit) — the production-MCP patterns this server
102
139
  follows (typed tool specs, transport discipline, description lint).
103
140
 
@@ -44,3 +44,27 @@ export interface RunResult {
44
44
  fileCount: number;
45
45
  }
46
46
  export declare function runAbaplint(files: AbapSource[], opts: RunOptions): RunResult;
47
+ /** A statically-resolved reference to a repository object found in the source. */
48
+ export interface ObjectReference {
49
+ /** The referenced object name (upper-cased as written; tables are typically lowercased in source). */
50
+ name: string;
51
+ /** SAP object type: "TABL" for DB tables, "FUNC" for function modules. */
52
+ objectType: "TABL" | "FUNC";
53
+ /** How the reference appears, for the human-facing note. */
54
+ kind: "db-access" | "call-function";
55
+ file: string;
56
+ line: number;
57
+ }
58
+ /**
59
+ * Walk the parsed AST and collect references to repository objects we can
60
+ * resolve reliably: DB tables in ABAP-SQL statements (SELECT/INSERT/UPDATE/
61
+ * DELETE/MODIFY, including the tables in joins and FROM clauses) and function
62
+ * modules in CALL FUNCTION '<fm>'. Parsing is done at a classic baseline so the
63
+ * statements resolve even for code ABAP Cloud would reject; abaplint only
64
+ * parses, never executes. References are de-duplicated by name+type+location.
65
+ *
66
+ * Deliberately conservative: we extract only what the parser exposes as a
67
+ * first-class expression (DatabaseTable, FunctionName), not heuristic regexes,
68
+ * so a reference we report is a reference that exists.
69
+ */
70
+ export declare function extractObjectReferences(files: AbapSource[], baselineVersion?: AbapVersion): ObjectReference[];
@@ -141,3 +141,79 @@ export function runAbaplint(files, opts) {
141
141
  });
142
142
  return { findings, truncated: issues.length > MAX_FINDINGS, fileCount: bounded.length };
143
143
  }
144
+ const SQL_STATEMENTS = new Set([
145
+ "Select",
146
+ "SelectLoop",
147
+ "InsertDatabase",
148
+ "UpdateDatabase",
149
+ "DeleteDatabase",
150
+ "ModifyDatabase",
151
+ ]);
152
+ /**
153
+ * Walk the parsed AST and collect references to repository objects we can
154
+ * resolve reliably: DB tables in ABAP-SQL statements (SELECT/INSERT/UPDATE/
155
+ * DELETE/MODIFY, including the tables in joins and FROM clauses) and function
156
+ * modules in CALL FUNCTION '<fm>'. Parsing is done at a classic baseline so the
157
+ * statements resolve even for code ABAP Cloud would reject; abaplint only
158
+ * parses, never executes. References are de-duplicated by name+type+location.
159
+ *
160
+ * Deliberately conservative: we extract only what the parser exposes as a
161
+ * first-class expression (DatabaseTable, FunctionName), not heuristic regexes,
162
+ * so a reference we report is a reference that exists.
163
+ */
164
+ export function extractObjectReferences(files, baselineVersion = "v758") {
165
+ const bounded = boundFiles(files);
166
+ const registry = new abaplint.Registry(buildConfig({ version: baselineVersion, preset: "syntax-only" }));
167
+ for (const f of bounded) {
168
+ registry.addFile(new abaplint.MemoryFile(f.filename, f.source));
169
+ }
170
+ registry.parse();
171
+ const refs = [];
172
+ const seen = new Set();
173
+ const push = (ref) => {
174
+ const dedupeKey = `${ref.objectType}:${ref.name}:${ref.file}:${ref.line}`;
175
+ if (seen.has(dedupeKey))
176
+ return;
177
+ seen.add(dedupeKey);
178
+ refs.push(ref);
179
+ };
180
+ for (const obj of registry.getObjects()) {
181
+ if (!(obj instanceof abaplint.ABAPObject))
182
+ continue;
183
+ for (const file of obj.getABAPFiles()) {
184
+ const filename = file.getFilename();
185
+ for (const st of file.getStatements()) {
186
+ const type = st.get().constructor.name;
187
+ if (SQL_STATEMENTS.has(type)) {
188
+ for (const e of st.findAllExpressions(abaplint.Expressions.DatabaseTable)) {
189
+ const name = e.concatTokens().trim();
190
+ if (name.length === 0)
191
+ continue;
192
+ push({
193
+ name: name.toUpperCase(),
194
+ objectType: "TABL",
195
+ kind: "db-access",
196
+ file: filename,
197
+ line: e.getFirstToken().getStart().getRow(),
198
+ });
199
+ }
200
+ }
201
+ else if (type === "CallFunction") {
202
+ for (const e of st.findAllExpressions(abaplint.Expressions.FunctionName)) {
203
+ const name = e.concatTokens().replace(/'/g, "").trim();
204
+ if (name.length === 0)
205
+ continue;
206
+ push({
207
+ name: name.toUpperCase(),
208
+ objectType: "FUNC",
209
+ kind: "call-function",
210
+ file: filename,
211
+ line: e.getFirstToken().getStart().getRow(),
212
+ });
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+ return refs;
219
+ }
@@ -7,10 +7,12 @@
7
7
  * longer allows). A finding present at the baseline is just *broken code* —
8
8
  * reporting it as a migration item would overstate the migration.
9
9
  *
10
- * Honest scope: this is the static, parser-level slice of readiness. The
11
- * other half "does this call only RELEASED SAP APIs?" — requires the
12
- * system's released-API list (ATC check SAP_CP_READINESS) and is out of
13
- * scope for an offline tool. The report says so.
10
+ * Released-API check: separately from the parser-level diff, the source is
11
+ * walked for object references (DB tables, function modules) which are looked
12
+ * up against the bundled SAP Cloudification snapshot. These land in their own
13
+ * `releasedApiFindings` field they are NOT folded into cloudBlockerCount or
14
+ * score, because those are objective, parser-level numbers and the snapshot is
15
+ * only as current as its date.
14
16
  */
15
17
  import type { AbapSource, AbapVersion, Finding } from "./engine.js";
16
18
  export interface ReadinessCategory {
@@ -19,6 +21,25 @@ export interface ReadinessCategory {
19
21
  count: number;
20
22
  findings: Finding[];
21
23
  }
24
+ /**
25
+ * A released-API observation about a referenced object. Kept SEPARATE from the
26
+ * parser-level blocker counts/score: it reflects the bundled SAP snapshot
27
+ * (dated), not abaplint's objective parse, and a system's ATC is authoritative.
28
+ */
29
+ export interface ReleasedApiFinding {
30
+ /** Referenced object name (upper-cased). */
31
+ object: string;
32
+ /** SAP object type: "TABL" (DB table) or "FUNC" (function module). */
33
+ objectType: string;
34
+ /** Released-API state from the bundled snapshot. */
35
+ state: "deprecated" | "not-released";
36
+ /** Curated released CDS successor for a classic table, when known. */
37
+ successor?: string;
38
+ file: string;
39
+ line: number;
40
+ /** Human-facing explanation of why this was flagged. */
41
+ note: string;
42
+ }
22
43
  export interface ReadinessReport {
23
44
  verdict: "ready" | "minor-rework" | "moderate-rework" | "significant-rework";
24
45
  score: number;
@@ -26,6 +47,14 @@ export interface ReadinessReport {
26
47
  categories: ReadinessCategory[];
27
48
  /** Findings that fail even at the classic baseline — fix these first; they are not migration items. */
28
49
  brokenAtBaseline: Finding[];
50
+ /**
51
+ * Released-API observations from the bundled SAP Cloudification snapshot —
52
+ * deprecated API usage and direct access to non-released (classic) tables,
53
+ * with successor hints. Separate from cloudBlockerCount/score by design.
54
+ */
55
+ releasedApiFindings: ReleasedApiFinding[];
56
+ /** Date of the bundled released-API snapshot the releasedApiFindings reflect. */
57
+ releasedApiSnapshotDate: string;
29
58
  baselineVersion: AbapVersion;
30
59
  scopeNote: string;
31
60
  }
@@ -1,6 +1,11 @@
1
- import { runAbaplint } from "./engine.js";
2
- export const SCOPE_NOTE = "Static parser-level analysis (abaplint). It detects statements and syntax that ABAP Cloud removes, " +
3
- "but NOT usage of unreleased SAP APIs that requires a system's released-API list (ATC / SAP_CP_READINESS). " +
1
+ import { extractObjectReferences, runAbaplint } from "./engine.js";
2
+ import { lookupReleased, RELEASED_API_SNAPSHOT, suggestSuccessor } from "./released.js";
3
+ export const SCOPE_NOTE = "Static parser-level analysis (abaplint) PLUS a released-API cross-check against SAP's bundled Cloudification " +
4
+ `snapshot (dated ${RELEASED_API_SNAPSHOT.snapshotDate}). It detects statements ABAP Cloud removes (the objective ` +
5
+ "cloud-blocker count and score) and, separately, flags deprecated-API usage and direct access to non-released " +
6
+ "classic tables (releasedApiFindings — informational, NOT counted in the score). The bundled list is only as current " +
7
+ "as its snapshot date, and covers tables and function modules referenced here, not every API; a target system's own " +
8
+ "released-API list (ATC check API_RELEASE_STATE_CHECK / SAP_CP_READINESS) remains authoritative. " +
4
9
  "Treat 'ready' as 'no language-level blockers', not as a full Clean Core certification.";
5
10
  /** Map an offending line to a human category by its leading keyword(s). */
6
11
  function categorize(excerpt) {
@@ -50,7 +55,57 @@ export function checkCloudReadiness(files, baselineVersion = "v758") {
50
55
  cloudBlockerCount: n,
51
56
  categories: [...byCategory.values()].sort((a, b) => b.count - a.count),
52
57
  brokenAtBaseline: baseline.findings,
58
+ releasedApiFindings: computeReleasedApiFindings(files, baselineVersion),
59
+ releasedApiSnapshotDate: RELEASED_API_SNAPSHOT.snapshotDate,
53
60
  baselineVersion,
54
61
  scopeNote: SCOPE_NOTE,
55
62
  };
56
63
  }
64
+ /**
65
+ * Cross-check statically-extracted object references against the bundled SAP
66
+ * Cloudification snapshot. Flags two cases:
67
+ * - `deprecated` — the referenced object is a deprecated released API.
68
+ * - `not-released` — direct access to a classic/internal table that is not a
69
+ * released API (the typical "SELECT … FROM mara" case), with a curated CDS
70
+ * successor hint when one is known.
71
+ * Released objects and references the snapshot does not recognise are silent —
72
+ * absence from the list is "not known to be a problem", not proof either way.
73
+ */
74
+ function computeReleasedApiFindings(files, baselineVersion) {
75
+ const out = [];
76
+ for (const ref of extractObjectReferences(files, baselineVersion)) {
77
+ const hit = lookupReleased(ref.name, ref.objectType);
78
+ if (hit.state === "released")
79
+ continue;
80
+ if (hit.state === "deprecated") {
81
+ out.push({
82
+ object: ref.name,
83
+ objectType: ref.objectType,
84
+ state: "deprecated",
85
+ file: ref.file,
86
+ line: ref.line,
87
+ note: ref.kind === "db-access"
88
+ ? `${ref.name} is a deprecated released object as of the snapshot — migrate to its current successor before going to ABAP Cloud.`
89
+ : `Function module ${ref.name} is deprecated as of the snapshot — replace it with the released successor API.`,
90
+ });
91
+ continue;
92
+ }
93
+ // not-released. Only flag DB tables (direct table access is the cloud
94
+ // anti-pattern); a CALL FUNCTION to a module simply absent from the list is
95
+ // too noisy to report as a finding without a system to confirm against.
96
+ if (ref.objectType === "TABL") {
97
+ const successor = suggestSuccessor(ref.name);
98
+ out.push({
99
+ object: ref.name,
100
+ objectType: ref.objectType,
101
+ state: "not-released",
102
+ ...(successor !== undefined ? { successor } : {}),
103
+ file: ref.file,
104
+ line: ref.line,
105
+ note: `${ref.name} is not a released API — direct access to this classic table is not allowed in ABAP Cloud.` +
106
+ (successor !== undefined ? ` Use the released CDS view ${successor} instead.` : " Use a released CDS view instead."),
107
+ });
108
+ }
109
+ }
110
+ return out;
111
+ }
@@ -0,0 +1,36 @@
1
+ /** Snapshot metadata for the bundled released-API list. */
2
+ export declare const RELEASED_API_SNAPSHOT: {
3
+ readonly snapshotDate: string;
4
+ readonly source: string;
5
+ readonly formatVersion: string | null;
6
+ readonly recordCount: number;
7
+ };
8
+ export type ReleasedState = "released" | "deprecated" | "not-released";
9
+ export interface ReleasedLookup {
10
+ name: string;
11
+ /** The object type recorded by SAP (TABL, CDS_STOB, FUNC, CLAS, …), or the queried type when no record matched. */
12
+ objectType: string | undefined;
13
+ state: ReleasedState;
14
+ applicationComponent?: string | undefined;
15
+ }
16
+ /**
17
+ * Look up an object in the bundled released-API list. Case-insensitive.
18
+ *
19
+ * `not-released` means "not a released API as of the snapshot": either the name
20
+ * is absent from SAP's list, or it is present with state `notToBeReleased`
21
+ * (typical for classic DDIC tables). A `released`/`deprecated` result is taken
22
+ * verbatim from SAP's published data.
23
+ *
24
+ * When `objectType` is given it disambiguates the few hundred names that exist
25
+ * under more than one object type; otherwise the first recorded entry is used,
26
+ * preferring a `released` or `deprecated` record over a `notToBeReleased` one
27
+ * so a genuinely released API is never masked by a same-named internal object.
28
+ */
29
+ export declare function lookupReleased(objectName: string, objectType?: string): ReleasedLookup;
30
+ /**
31
+ * Suggest the canonical released CDS view-entity successor for a classic DB
32
+ * table, from the curated table-successors map. Case-insensitive. Returns
33
+ * undefined when no curated successor is known (the caller should fall back to
34
+ * the target system's released-API list).
35
+ */
36
+ export declare function suggestSuccessor(tableName: string): string | undefined;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Released-API lookup against SAP's published ABAP Cloudification list.
3
+ *
4
+ * The data shipped at src/data/released-apis.json is a compact transform of
5
+ * SAP's official, Apache-2.0 object release list (SAP/abap-atc-cr-cv-s4hc),
6
+ * built offline by scripts/build-released-api-index.mjs. It is a PACKAGE-BUNDLED
7
+ * asset (like abaplint's own bundled rule metadata): importing it touches no
8
+ * network and no user filesystem at runtime, so it does not violate the
9
+ * server's "text in, JSON out, offline" contract.
10
+ *
11
+ * SAP's list uses three states:
12
+ * - "released" — a released API, safe to consume in ABAP Cloud.
13
+ * - "deprecated" — was released, now on the way out; move to a successor.
14
+ * - "notToBeReleased" — will not be released as a public API (classic DDIC
15
+ * tables, internal objects); direct use is a cloud
16
+ * blocker. We surface these (and names absent from the
17
+ * list entirely) as state "not-released".
18
+ */
19
+ import index from "../data/released-apis.json" with { type: "json" };
20
+ import successorData from "../data/table-successors.json" with { type: "json" };
21
+ const data = index;
22
+ const successors = successorData;
23
+ /** Snapshot metadata for the bundled released-API list. */
24
+ export const RELEASED_API_SNAPSHOT = {
25
+ snapshotDate: data.snapshotDate,
26
+ source: data.source,
27
+ formatVersion: data.formatVersion,
28
+ recordCount: data.recordCount,
29
+ };
30
+ /** Map SAP's raw state to our three-value state. */
31
+ function toState(rawState) {
32
+ if (rawState === "released")
33
+ return "released";
34
+ if (rawState === "deprecated")
35
+ return "deprecated";
36
+ // "notToBeReleased" — present in the list but never a public API.
37
+ return "not-released";
38
+ }
39
+ /**
40
+ * Look up an object in the bundled released-API list. Case-insensitive.
41
+ *
42
+ * `not-released` means "not a released API as of the snapshot": either the name
43
+ * is absent from SAP's list, or it is present with state `notToBeReleased`
44
+ * (typical for classic DDIC tables). A `released`/`deprecated` result is taken
45
+ * verbatim from SAP's published data.
46
+ *
47
+ * When `objectType` is given it disambiguates the few hundred names that exist
48
+ * under more than one object type; otherwise the first recorded entry is used,
49
+ * preferring a `released` or `deprecated` record over a `notToBeReleased` one
50
+ * so a genuinely released API is never masked by a same-named internal object.
51
+ */
52
+ export function lookupReleased(objectName, objectType) {
53
+ const key = objectName.trim().toUpperCase();
54
+ const entries = data.objects[key];
55
+ if (entries === undefined || entries.length === 0) {
56
+ return { name: objectName, objectType, state: "not-released" };
57
+ }
58
+ const wantedType = objectType?.trim().toUpperCase();
59
+ let chosen;
60
+ if (wantedType !== undefined) {
61
+ chosen = entries.find((e) => e[0].toUpperCase() === wantedType);
62
+ }
63
+ if (chosen === undefined) {
64
+ // Prefer a released/deprecated record over notToBeReleased when ambiguous.
65
+ chosen =
66
+ entries.find((e) => e[1] === "released" || e[1] === "deprecated") ?? entries[0];
67
+ }
68
+ if (chosen === undefined) {
69
+ return { name: objectName, objectType, state: "not-released" };
70
+ }
71
+ return {
72
+ name: objectName,
73
+ objectType: chosen[0],
74
+ state: toState(chosen[1]),
75
+ applicationComponent: chosen[2],
76
+ };
77
+ }
78
+ /**
79
+ * Suggest the canonical released CDS view-entity successor for a classic DB
80
+ * table, from the curated table-successors map. Case-insensitive. Returns
81
+ * undefined when no curated successor is known (the caller should fall back to
82
+ * the target system's released-API list).
83
+ */
84
+ export function suggestSuccessor(tableName) {
85
+ return successors.successors[tableName.trim().toUpperCase()];
86
+ }
@@ -82,6 +82,12 @@ export declare const getAbapOutline: import("./tool.js").ToolSpec<{
82
82
  source: z.ZodString;
83
83
  }, z.core.$strip>>;
84
84
  }>;
85
+ export declare const checkReleasedApiTool: import("./tool.js").ToolSpec<{
86
+ objects: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
87
+ name: z.ZodString;
88
+ type: z.ZodOptional<z.ZodString>;
89
+ }, z.core.$strip>]>>;
90
+ }>;
85
91
  /** Every tool this server exposes. (`tools` alias = the registry-export shape @mcp-kit/lint discovers.) */
86
92
  export declare const ALL_TOOLS: readonly AnyToolSpec[];
87
93
  export declare const tools: readonly AnyToolSpec[];
@@ -11,6 +11,7 @@ import { ABAP_VERSIONS, runAbaplint } from "./abap/engine.js";
11
11
  import { formatAbap } from "./abap/formatter.js";
12
12
  import { outlineAbap } from "./abap/outline.js";
13
13
  import { checkCloudReadiness } from "./abap/readiness.js";
14
+ import { lookupReleased, RELEASED_API_SNAPSHOT, suggestSuccessor, } from "./abap/released.js";
14
15
  import { explainRule, listRules } from "./abap/rules.js";
15
16
  import { scaffoldRapBo } from "./abap/scaffold.js";
16
17
  import { invalidInput } from "./errors.js";
@@ -132,6 +133,12 @@ export const checkCloudReadinessTool = defineTool({
132
133
  brokenAtBaseline: z
133
134
  .array(z.unknown())
134
135
  .describe("Findings that fail even at the baseline version — fix first, they are not migration items."),
136
+ releasedApiFindings: z
137
+ .array(z.unknown())
138
+ .describe("Released-API observations from the bundled SAP Cloudification snapshot (deprecated-API usage, direct non-released table access with successor hints). Informational — NOT counted in cloudBlockerCount or score."),
139
+ releasedApiSnapshotDate: z
140
+ .string()
141
+ .describe("Date of the bundled released-API snapshot the releasedApiFindings reflect."),
135
142
  baselineVersion: z.string().describe("The baseline used."),
136
143
  scopeNote: z.string().describe("Exactly what this check does and does not cover."),
137
144
  },
@@ -151,6 +158,9 @@ export const checkCloudReadinessTool = defineTool({
151
158
  (catLine.length > 0 ? ` [${catLine}]` : "") +
152
159
  (report.brokenAtBaseline.length > 0
153
160
  ? `; ${report.brokenAtBaseline.length} finding(s) broken at ${report.baselineVersion} regardless`
161
+ : "") +
162
+ (report.releasedApiFindings.length > 0
163
+ ? `; ${report.releasedApiFindings.length} released-API note(s) (snapshot ${report.releasedApiSnapshotDate})`
154
164
  : "");
155
165
  return {
156
166
  content: [{ type: "text", text }],
@@ -417,11 +427,109 @@ export const getAbapOutline = defineTool({
417
427
  };
418
428
  },
419
429
  });
430
+ /** Accept either a bare name string or a { name, type? } reference. */
431
+ const objectRefField = z
432
+ .array(z.union([
433
+ z.string().describe('A bare object name, e.g. "MARA" or "I_Product".'),
434
+ z.object({
435
+ name: z.string().describe('Object name, e.g. "MARA", "I_Product", "BAPI_MATERIAL_GET_DETAIL".'),
436
+ type: z
437
+ .string()
438
+ .optional()
439
+ .describe('Optional SAP object type to disambiguate same-named objects: "TABL" (table), "CDS_STOB" (CDS view entity), "FUNC" (function module), "CLAS", "INTF", "BDEF". Omit if unsure.'),
440
+ }),
441
+ ]))
442
+ .min(1)
443
+ .max(200)
444
+ .describe('Objects to check, 1–200 per call. Each is a bare name string or a { name, type? } object, e.g. ["MARA", { "name": "I_Product", "type": "CDS_STOB" }].');
445
+ export const checkReleasedApiTool = defineTool({
446
+ name: "check_released_api",
447
+ title: "Check ABAP released-API status",
448
+ description: "Look up ABAP repository objects (DB tables, CDS view entities, function modules, classes, interfaces, …) in " +
449
+ "SAP's published ABAP Cloudification list and report, per object, whether it is a 'released' API (safe to use in " +
450
+ "ABAP Cloud / Clean Core), 'deprecated' (released but being retired), or 'not-released' (a classic/internal object " +
451
+ "that is not a public API — e.g. most classic DDIC tables) — with a curated CDS successor hint for common tables. " +
452
+ `This reflects SAP's official Cloudification list as bundled in this package (snapshot ${RELEASED_API_SNAPSHOT.snapshotDate}); ` +
453
+ "it ships offline with the server. " +
454
+ "Use this when you need to know if your code may reference a given object in ABAP Cloud, or which released CDS view " +
455
+ "to use instead of a classic table — the released-API half of readiness that check_cloud_readiness deliberately " +
456
+ "leaves to a system's ATC. " +
457
+ "It does not connect to any SAP system, does not run ATC, and is only as current as the bundled snapshot — a " +
458
+ `system's own released-API list (ATC check API_RELEASE_STATE_CHECK / SAP_CP_READINESS) remains authoritative; treat ` +
459
+ "an 'absent from the list' result as 'not-released as of the snapshot', not as proof. " +
460
+ 'Example: check_released_api({ "objects": ["MARA", "I_Product", "BAPI_MATERIAL_GET_DETAIL"] }).',
461
+ inputSchema: {
462
+ objects: objectRefField,
463
+ },
464
+ outputSchema: {
465
+ snapshotDate: z.string().describe("Date of the bundled SAP Cloudification snapshot these results reflect."),
466
+ source: z.string().describe("URL of the SAP Apache-2.0 source the snapshot was built from."),
467
+ results: z.array(z.object({
468
+ name: z.string().describe("The object name as queried."),
469
+ objectType: z
470
+ .string()
471
+ .optional()
472
+ .describe("SAP object type of the matched record (TABL, CDS_STOB, FUNC, …), if found."),
473
+ state: z
474
+ .enum(["released", "deprecated", "not-released"])
475
+ .describe("'released' = safe public API; 'deprecated' = retiring; 'not-released' = not a public API."),
476
+ applicationComponent: z
477
+ .string()
478
+ .optional()
479
+ .describe("Owning application component of the matched record, if found."),
480
+ successor: z
481
+ .string()
482
+ .optional()
483
+ .describe("Curated released CDS view-entity successor for a classic table, when one is known."),
484
+ })),
485
+ },
486
+ annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
487
+ examples: [
488
+ {
489
+ description: "Check a classic table, a released CDS view, and a BAPI in one call.",
490
+ arguments: { objects: ["MARA", "I_Product", "BAPI_MATERIAL_GET_DETAIL"] },
491
+ },
492
+ {
493
+ description: "Disambiguate a name that exists under more than one object type.",
494
+ arguments: { objects: [{ name: "I_ProcurementProjectTP", type: "CDS_STOB" }] },
495
+ },
496
+ ],
497
+ handler: (args) => {
498
+ const results = args.objects.map((ref) => {
499
+ const name = typeof ref === "string" ? ref : ref.name;
500
+ const type = typeof ref === "string" ? undefined : ref.type;
501
+ const hit = lookupReleased(name, type);
502
+ const successor = suggestSuccessor(name);
503
+ return {
504
+ name: hit.name,
505
+ objectType: hit.objectType,
506
+ state: hit.state,
507
+ applicationComponent: hit.applicationComponent,
508
+ ...(successor !== undefined ? { successor } : {}),
509
+ };
510
+ });
511
+ const text = results
512
+ .map((r) => {
513
+ const tail = r.successor !== undefined ? ` → use ${r.successor}` : "";
514
+ return `${r.name}: ${r.state}${r.objectType !== undefined ? ` (${r.objectType})` : ""}${tail}`;
515
+ })
516
+ .join("\n");
517
+ return {
518
+ content: [{ type: "text", text: `Snapshot ${RELEASED_API_SNAPSHOT.snapshotDate}\n${text}` }],
519
+ structuredContent: {
520
+ snapshotDate: RELEASED_API_SNAPSHOT.snapshotDate,
521
+ source: RELEASED_API_SNAPSHOT.source,
522
+ results,
523
+ },
524
+ };
525
+ },
526
+ });
420
527
  /** Every tool this server exposes. (`tools` alias = the registry-export shape @mcp-kit/lint discovers.) */
421
528
  export const ALL_TOOLS = [
422
529
  lintAbap,
423
530
  checkCloudReadinessTool,
424
531
  scaffoldRapBoTool,
532
+ checkReleasedApiTool,
425
533
  listAbapRules,
426
534
  explainAbapRule,
427
535
  formatAbapTool,