abap-mcp 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/abap/readiness.js +31 -9
- package/dist/abap/released.d.ts +14 -4
- package/dist/abap/released.js +16 -8
- package/dist/abap.tools.js +6 -1
- package/dist/cli-commands.js +17 -3
- package/dist/errors.js +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ loop condition), per-repo `.mcp.json`, and a GitHub Actions quality gate for aba
|
|
|
110
110
|
|
|
111
111
|
```bash
|
|
112
112
|
npm install
|
|
113
|
-
npm run check # typecheck +
|
|
113
|
+
npm run check # typecheck + 121 tests + build — the CI gate
|
|
114
114
|
node dist/cli.js # stdio MCP server
|
|
115
115
|
npx @modelcontextprotocol/inspector --cli node dist/cli.js --method tools/list
|
|
116
116
|
```
|
package/dist/abap/readiness.js
CHANGED
|
@@ -65,16 +65,24 @@ export function checkCloudReadiness(files, baselineVersion = "v758") {
|
|
|
65
65
|
* Cross-check statically-extracted object references against the bundled SAP
|
|
66
66
|
* Cloudification snapshot. Flags two cases:
|
|
67
67
|
* - `deprecated` — the referenced object is a deprecated released API.
|
|
68
|
-
* - `not-released` —
|
|
69
|
-
*
|
|
70
|
-
* successor hint when one is
|
|
68
|
+
* - `not-released` — the reference is explicitly recorded as notToBeReleased
|
|
69
|
+
* in SAP's list: direct access to a classic/internal table (the typical
|
|
70
|
+
* "SELECT … FROM mara" case, with a curated CDS successor hint when one is
|
|
71
|
+
* known) or a CALL FUNCTION to an internal-only function module.
|
|
71
72
|
* Released objects and references the snapshot does not recognise are silent —
|
|
72
|
-
* absence from the list
|
|
73
|
+
* absence from the list (every customer Z/Y-object, for a start) is "not known
|
|
74
|
+
* to be a problem", not proof either way.
|
|
73
75
|
*/
|
|
74
76
|
function computeReleasedApiFindings(files, baselineVersion) {
|
|
75
77
|
const out = [];
|
|
76
78
|
for (const ref of extractObjectReferences(files, baselineVersion)) {
|
|
77
|
-
|
|
79
|
+
let hit = lookupReleased(ref.name, ref.objectType);
|
|
80
|
+
// An ABAP-SQL FROM clause names either a DDIC table or a CDS entity, but
|
|
81
|
+
// the extractor labels both TABL — fall back to the CDS record so a
|
|
82
|
+
// deprecated CDS view in a SELECT is still caught.
|
|
83
|
+
if (!hit.recorded && ref.objectType === "TABL") {
|
|
84
|
+
hit = lookupReleased(ref.name, "CDS_STOB");
|
|
85
|
+
}
|
|
78
86
|
if (hit.state === "released")
|
|
79
87
|
continue;
|
|
80
88
|
if (hit.state === "deprecated") {
|
|
@@ -90,10 +98,24 @@ function computeReleasedApiFindings(files, baselineVersion) {
|
|
|
90
98
|
});
|
|
91
99
|
continue;
|
|
92
100
|
}
|
|
93
|
-
// not-released. Only flag
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
101
|
+
// not-released. Only flag names SAP's snapshot explicitly records as
|
|
102
|
+
// notToBeReleased — a name merely absent from the list (every customer
|
|
103
|
+
// Z/Y-object, for a start) is silent: absence is "not known to be a
|
|
104
|
+
// problem", not evidence of one.
|
|
105
|
+
if (!hit.recorded)
|
|
106
|
+
continue;
|
|
107
|
+
if (ref.objectType === "FUNC") {
|
|
108
|
+
out.push({
|
|
109
|
+
object: ref.name,
|
|
110
|
+
objectType: ref.objectType,
|
|
111
|
+
state: "not-released",
|
|
112
|
+
file: ref.file,
|
|
113
|
+
line: ref.line,
|
|
114
|
+
note: `Function module ${ref.name} is recorded as not-to-be-released in SAP's Cloudification list — it will not become a public API in ABAP Cloud; use a released successor API instead.`,
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (ref.objectType === "TABL" && hit.objectType === "TABL") {
|
|
97
119
|
const successor = suggestSuccessor(ref.name);
|
|
98
120
|
out.push({
|
|
99
121
|
object: ref.name,
|
package/dist/abap/released.d.ts
CHANGED
|
@@ -12,6 +12,13 @@ export interface ReleasedLookup {
|
|
|
12
12
|
objectType: string | undefined;
|
|
13
13
|
state: ReleasedState;
|
|
14
14
|
applicationComponent?: string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* true when the name was found in SAP's snapshot (under the requested type,
|
|
17
|
+
* if one was given). false means absent — "not released as of the snapshot"
|
|
18
|
+
* by omission, which is weaker evidence than an explicit notToBeReleased
|
|
19
|
+
* record and must not be reported as a violation on its own.
|
|
20
|
+
*/
|
|
21
|
+
recorded: boolean;
|
|
15
22
|
}
|
|
16
23
|
/**
|
|
17
24
|
* Look up an object in the bundled released-API list. Case-insensitive.
|
|
@@ -21,10 +28,13 @@ export interface ReleasedLookup {
|
|
|
21
28
|
* (typical for classic DDIC tables). A `released`/`deprecated` result is taken
|
|
22
29
|
* verbatim from SAP's published data.
|
|
23
30
|
*
|
|
24
|
-
* When `objectType` is given
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
31
|
+
* When `objectType` is given the lookup is strict: only a record of exactly
|
|
32
|
+
* that type answers the query — a same-named record under a different type is
|
|
33
|
+
* a miss (`recorded: false`), never a substitute (a released class must not
|
|
34
|
+
* make a non-released table look released). Untyped lookups use the first
|
|
35
|
+
* recorded entry, preferring a `released` or `deprecated` record over a
|
|
36
|
+
* `notToBeReleased` one so a genuinely released API is never masked by a
|
|
37
|
+
* same-named internal object.
|
|
28
38
|
*/
|
|
29
39
|
export declare function lookupReleased(objectName: string, objectType?: string): ReleasedLookup;
|
|
30
40
|
/**
|
package/dist/abap/released.js
CHANGED
|
@@ -44,35 +44,43 @@ function toState(rawState) {
|
|
|
44
44
|
* (typical for classic DDIC tables). A `released`/`deprecated` result is taken
|
|
45
45
|
* verbatim from SAP's published data.
|
|
46
46
|
*
|
|
47
|
-
* When `objectType` is given
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
47
|
+
* When `objectType` is given the lookup is strict: only a record of exactly
|
|
48
|
+
* that type answers the query — a same-named record under a different type is
|
|
49
|
+
* a miss (`recorded: false`), never a substitute (a released class must not
|
|
50
|
+
* make a non-released table look released). Untyped lookups use the first
|
|
51
|
+
* recorded entry, preferring a `released` or `deprecated` record over a
|
|
52
|
+
* `notToBeReleased` one so a genuinely released API is never masked by a
|
|
53
|
+
* same-named internal object.
|
|
51
54
|
*/
|
|
52
55
|
export function lookupReleased(objectName, objectType) {
|
|
53
56
|
const key = objectName.trim().toUpperCase();
|
|
54
57
|
const entries = data.objects[key];
|
|
58
|
+
const wantedType = objectType?.trim().toUpperCase();
|
|
55
59
|
if (entries === undefined || entries.length === 0) {
|
|
56
|
-
return { name: objectName, objectType, state: "not-released" };
|
|
60
|
+
return { name: objectName, objectType: wantedType, state: "not-released", recorded: false };
|
|
57
61
|
}
|
|
58
|
-
const wantedType = objectType?.trim().toUpperCase();
|
|
59
62
|
let chosen;
|
|
60
63
|
if (wantedType !== undefined) {
|
|
61
64
|
chosen = entries.find((e) => e[0].toUpperCase() === wantedType);
|
|
65
|
+
if (chosen === undefined) {
|
|
66
|
+
// Typed query, no record of that type: a miss, not a cross-type answer.
|
|
67
|
+
return { name: objectName, objectType: wantedType, state: "not-released", recorded: false };
|
|
68
|
+
}
|
|
62
69
|
}
|
|
63
|
-
|
|
70
|
+
else {
|
|
64
71
|
// Prefer a released/deprecated record over notToBeReleased when ambiguous.
|
|
65
72
|
chosen =
|
|
66
73
|
entries.find((e) => e[1] === "released" || e[1] === "deprecated") ?? entries[0];
|
|
67
74
|
}
|
|
68
75
|
if (chosen === undefined) {
|
|
69
|
-
return { name: objectName, objectType, state: "not-released" };
|
|
76
|
+
return { name: objectName, objectType: wantedType, state: "not-released", recorded: false };
|
|
70
77
|
}
|
|
71
78
|
return {
|
|
72
79
|
name: objectName,
|
|
73
80
|
objectType: chosen[0],
|
|
74
81
|
state: toState(chosen[1]),
|
|
75
82
|
applicationComponent: chosen[2],
|
|
83
|
+
recorded: true,
|
|
76
84
|
};
|
|
77
85
|
}
|
|
78
86
|
/**
|
package/dist/abap.tools.js
CHANGED
|
@@ -473,6 +473,9 @@ export const checkReleasedApiTool = defineTool({
|
|
|
473
473
|
state: z
|
|
474
474
|
.enum(["released", "deprecated", "not-released"])
|
|
475
475
|
.describe("'released' = safe public API; 'deprecated' = retiring; 'not-released' = not a public API."),
|
|
476
|
+
recorded: z
|
|
477
|
+
.boolean()
|
|
478
|
+
.describe("true = explicitly present in SAP's snapshot (under the requested type, if one was given); false = absent — 'not released as of the snapshot' by omission only."),
|
|
476
479
|
applicationComponent: z
|
|
477
480
|
.string()
|
|
478
481
|
.optional()
|
|
@@ -504,6 +507,7 @@ export const checkReleasedApiTool = defineTool({
|
|
|
504
507
|
name: hit.name,
|
|
505
508
|
objectType: hit.objectType,
|
|
506
509
|
state: hit.state,
|
|
510
|
+
recorded: hit.recorded,
|
|
507
511
|
applicationComponent: hit.applicationComponent,
|
|
508
512
|
...(successor !== undefined ? { successor } : {}),
|
|
509
513
|
};
|
|
@@ -511,7 +515,8 @@ export const checkReleasedApiTool = defineTool({
|
|
|
511
515
|
const text = results
|
|
512
516
|
.map((r) => {
|
|
513
517
|
const tail = r.successor !== undefined ? ` → use ${r.successor}` : "";
|
|
514
|
-
|
|
518
|
+
const provenance = r.recorded ? "" : " (not in snapshot)";
|
|
519
|
+
return `${r.name}: ${r.state}${r.objectType !== undefined ? ` (${r.objectType})` : ""}${provenance}${tail}`;
|
|
515
520
|
})
|
|
516
521
|
.join("\n");
|
|
517
522
|
return {
|
package/dist/cli-commands.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* story); the *CLI* is a local developer tool, so reading files from disk here
|
|
6
6
|
* is fine. Both call the identical engine — one definition of "clean".
|
|
7
7
|
*/
|
|
8
|
-
import { readdirSync, readFileSync, statSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
8
|
+
import { readdirSync, readFileSync, realpathSync, statSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
9
9
|
import { basename, extname, join } from "node:path";
|
|
10
10
|
import { ABAP_VERSIONS, MAX_FILES, runAbaplint } from "./abap/engine.js";
|
|
11
11
|
import { outlineAbap } from "./abap/outline.js";
|
|
@@ -17,11 +17,18 @@ const ABAP_FILE_RE = /\.(clas\.abap|clas\.locals_imp\.abap|clas\.locals_def\.aba
|
|
|
17
17
|
/** Recursively collect analyzable sources from file/dir paths. */
|
|
18
18
|
export function collectFiles(paths, io) {
|
|
19
19
|
const found = [];
|
|
20
|
+
const visitedDirs = new Set();
|
|
20
21
|
const visit = (p) => {
|
|
21
22
|
const st = statSync(p);
|
|
22
23
|
if (st.isDirectory()) {
|
|
23
24
|
if (basename(p) === ".git" || basename(p) === "node_modules")
|
|
24
25
|
return;
|
|
26
|
+
// statSync follows symlinks — track real paths so a symlink cycle
|
|
27
|
+
// can't recurse forever.
|
|
28
|
+
const real = realpathSync(p);
|
|
29
|
+
if (visitedDirs.has(real))
|
|
30
|
+
return;
|
|
31
|
+
visitedDirs.add(real);
|
|
25
32
|
for (const entry of readdirSync(p))
|
|
26
33
|
visit(join(p, entry));
|
|
27
34
|
return;
|
|
@@ -214,7 +221,13 @@ export function cmdScaffold(argv, io) {
|
|
|
214
221
|
writeFileSync(target, f.content, "utf8");
|
|
215
222
|
io.out(`wrote ${target} [${f.validated}]`);
|
|
216
223
|
}
|
|
217
|
-
|
|
224
|
+
const suggestionTarget = join(outDir, `${sqlTable.toLowerCase()}.tabl.suggestion.txt`);
|
|
225
|
+
if (existsSync(suggestionTarget) && !flags.has("force")) {
|
|
226
|
+
io.err(`refusing to overwrite ${suggestionTarget} (use --force)`);
|
|
227
|
+
return 1;
|
|
228
|
+
}
|
|
229
|
+
writeFileSync(suggestionTarget, result.suggestedTableDdl, "utf8");
|
|
230
|
+
io.out(`wrote ${suggestionTarget}`);
|
|
218
231
|
}
|
|
219
232
|
else {
|
|
220
233
|
for (const f of result.files) {
|
|
@@ -288,7 +301,8 @@ export function cmdReleased(argv, io) {
|
|
|
288
301
|
io.out(`Released-API status (SAP Cloudification snapshot ${RELEASED_API_SNAPSHOT.snapshotDate}):`);
|
|
289
302
|
for (const r of results) {
|
|
290
303
|
const tail = r.successor !== undefined ? ` → use ${r.successor}` : "";
|
|
291
|
-
|
|
304
|
+
const provenance = r.recorded ? "" : " (not in snapshot)";
|
|
305
|
+
io.out(` ${r.name.padEnd(34)} ${r.state.padEnd(13)} ${(r.objectType ?? "").padEnd(9)}${provenance}${tail}`);
|
|
292
306
|
}
|
|
293
307
|
return 0;
|
|
294
308
|
}
|
package/dist/errors.js
CHANGED
|
@@ -21,9 +21,15 @@ export function errorResult(err) {
|
|
|
21
21
|
const e = err instanceof McpToolError
|
|
22
22
|
? err
|
|
23
23
|
: new McpToolError("internal", err instanceof Error ? err.message : String(err));
|
|
24
|
+
// IMPORTANT: do NOT attach `structuredContent` here. Every tool declares an `outputSchema`
|
|
25
|
+
// describing its *success* shape, and the MCP SDK validates `structuredContent` against that
|
|
26
|
+
// schema even when `isError: true`. An error payload ({ error: … }) cannot satisfy a success
|
|
27
|
+
// schema, so strict clients (OpenClaw, the MCP inspector, the VSCode/ADT integrations) reject
|
|
28
|
+
// the whole result with `-32602` and the model never sees the real, actionable message —
|
|
29
|
+
// it just gives up and answers from memory. The spec permits an absent `structuredContent`
|
|
30
|
+
// on error results, so the human-readable `code: message` text carries the error instead.
|
|
24
31
|
return {
|
|
25
32
|
isError: true,
|
|
26
33
|
content: [{ type: "text", text: `${e.code}: ${e.message}` }],
|
|
27
|
-
structuredContent: { error: { code: e.code, message: e.message, details: e.details ?? null } },
|
|
28
34
|
};
|
|
29
35
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abap-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"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.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Akshay Palimkar",
|