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.
@@ -0,0 +1,23 @@
1
+ import type { AbapSource, AbapVersion } from "./abap/engine.js";
2
+ import type { ReadinessReport } from "./abap/readiness.js";
3
+ export interface CliIo {
4
+ out: (s: string) => void;
5
+ err: (s: string) => void;
6
+ }
7
+ /** Recursively collect analyzable sources from file/dir paths. */
8
+ export declare function collectFiles(paths: string[], io: CliIo): AbapSource[];
9
+ export declare function parseFlags(argv: string[]): {
10
+ flags: Map<string, string | true>;
11
+ rest: string[];
12
+ };
13
+ export declare function cmdLint(argv: string[], io: CliIo): number;
14
+ /** Merge per-batch readiness reports into one repo-level report. */
15
+ export declare function mergeReadiness(reports: ReadinessReport[], baseline: AbapVersion): ReadinessReport;
16
+ export declare function cmdReadiness(argv: string[], io: CliIo): number;
17
+ export declare function cmdScaffold(argv: string[], io: CliIo): number;
18
+ export declare function cmdOutline(argv: string[], io: CliIo): number;
19
+ export declare function cmdExplain(argv: string[], io: CliIo): number;
20
+ export declare function cmdReleased(argv: string[], io: CliIo): number;
21
+ 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 function runCli(argv: string[], io: CliIo): number | null;
@@ -0,0 +1,347 @@
1
+ /**
2
+ * CLI layer over the same engine the MCP server exposes.
3
+ *
4
+ * Deliberate split: the MCP *server* stays text-in/no-filesystem (its security
5
+ * story); the *CLI* is a local developer tool, so reading files from disk here
6
+ * is fine. Both call the identical engine — one definition of "clean".
7
+ */
8
+ import { readdirSync, readFileSync, statSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
9
+ import { basename, extname, join } from "node:path";
10
+ import { ABAP_VERSIONS, MAX_FILES, runAbaplint } from "./abap/engine.js";
11
+ import { outlineAbap } from "./abap/outline.js";
12
+ import { checkCloudReadiness, SCOPE_NOTE } from "./abap/readiness.js";
13
+ import { lookupReleased, RELEASED_API_SNAPSHOT, suggestSuccessor } from "./abap/released.js";
14
+ import { explainRule, listRules } from "./abap/rules.js";
15
+ import { scaffoldRapBo } from "./abap/scaffold.js";
16
+ const ABAP_FILE_RE = /\.(clas\.abap|clas\.locals_imp\.abap|clas\.locals_def\.abap|clas\.testclasses\.abap|prog\.abap|intf\.abap|fugr\.abap|ddls\.asddls|bdef\.asbdef|srvd\.srvdsrv|ddlx\.asddlx)$/;
17
+ /** Recursively collect analyzable sources from file/dir paths. */
18
+ export function collectFiles(paths, io) {
19
+ const found = [];
20
+ const visit = (p) => {
21
+ const st = statSync(p);
22
+ if (st.isDirectory()) {
23
+ if (basename(p) === ".git" || basename(p) === "node_modules")
24
+ return;
25
+ for (const entry of readdirSync(p))
26
+ visit(join(p, entry));
27
+ return;
28
+ }
29
+ const name = basename(p).toLowerCase();
30
+ if (ABAP_FILE_RE.test(name)) {
31
+ found.push({ filename: name, source: readFileSync(p, "utf8") });
32
+ }
33
+ else if ([".abap", ".asddls", ".asbdef"].includes(extname(name))) {
34
+ io.err(`skip ${p}: not an abapGit-style filename (e.g. zcl_x.clas.abap)`);
35
+ }
36
+ };
37
+ for (const p of paths)
38
+ visit(p);
39
+ return found;
40
+ }
41
+ function chunk(arr, size) {
42
+ const out = [];
43
+ for (let i = 0; i < arr.length; i += size)
44
+ out.push(arr.slice(i, i + size));
45
+ return out;
46
+ }
47
+ export function parseFlags(argv) {
48
+ const flags = new Map();
49
+ const rest = [];
50
+ for (let i = 0; i < argv.length; i++) {
51
+ const a = argv[i];
52
+ if (a.startsWith("--")) {
53
+ const key = a.slice(2);
54
+ const next = argv[i + 1];
55
+ if (next !== undefined && !next.startsWith("--")) {
56
+ flags.set(key, next);
57
+ i++;
58
+ }
59
+ else {
60
+ flags.set(key, true);
61
+ }
62
+ }
63
+ else {
64
+ rest.push(a);
65
+ }
66
+ }
67
+ return { flags, rest };
68
+ }
69
+ function asVersion(v, fallback) {
70
+ if (typeof v !== "string")
71
+ return fallback;
72
+ if (ABAP_VERSIONS.includes(v))
73
+ return v;
74
+ throw new Error(`Unknown ABAP version "${v}". Valid: ${ABAP_VERSIONS.join(", ")}`);
75
+ }
76
+ function fmtFinding(f) {
77
+ return `${f.file}:${f.line}:${f.column} [${f.severity}] ${f.rule}: ${f.message}`;
78
+ }
79
+ export function cmdLint(argv, io) {
80
+ const { flags, rest } = parseFlags(argv);
81
+ const files = collectFiles(rest.length > 0 ? rest : ["."], io);
82
+ if (files.length === 0) {
83
+ io.err("No ABAP sources found.");
84
+ return 2;
85
+ }
86
+ const version = asVersion(flags.get("abap-version"), "v758");
87
+ const presetRaw = flags.get("preset");
88
+ const preset = presetRaw === "full" || presetRaw === "syntax-only" ? presetRaw : "style";
89
+ const all = [];
90
+ for (const batch of chunk(files, MAX_FILES)) {
91
+ all.push(...runAbaplint(batch, { version, preset }).findings);
92
+ }
93
+ if (flags.has("json")) {
94
+ io.out(JSON.stringify({ files: files.length, findings: all }, null, 2));
95
+ }
96
+ else {
97
+ for (const f of all)
98
+ io.out(fmtFinding(f));
99
+ io.out(`${all.length} finding(s) in ${files.length} file(s) [${preset} @ ${version}]`);
100
+ }
101
+ return all.some((f) => f.severity === "Error") ? 1 : 0;
102
+ }
103
+ /** Merge per-batch readiness reports into one repo-level report. */
104
+ export function mergeReadiness(reports, baseline) {
105
+ const categories = new Map();
106
+ let blockers = 0;
107
+ const broken = [];
108
+ const releasedApiFindings = [];
109
+ let snapshotDate = "";
110
+ for (const r of reports) {
111
+ blockers += r.cloudBlockerCount;
112
+ broken.push(...r.brokenAtBaseline);
113
+ releasedApiFindings.push(...r.releasedApiFindings);
114
+ snapshotDate = r.releasedApiSnapshotDate;
115
+ for (const c of r.categories) {
116
+ const cur = categories.get(c.category);
117
+ if (cur === undefined)
118
+ categories.set(c.category, { ...c, findings: [...c.findings] });
119
+ else {
120
+ cur.count += c.count;
121
+ cur.findings.push(...c.findings);
122
+ }
123
+ }
124
+ }
125
+ const score = Math.max(0, 100 - 5 * blockers);
126
+ const verdict = blockers === 0
127
+ ? "ready"
128
+ : blockers <= 5
129
+ ? "minor-rework"
130
+ : blockers <= 20
131
+ ? "moderate-rework"
132
+ : "significant-rework";
133
+ return {
134
+ verdict,
135
+ score,
136
+ cloudBlockerCount: blockers,
137
+ categories: [...categories.values()].sort((a, b) => b.count - a.count),
138
+ brokenAtBaseline: broken,
139
+ releasedApiFindings,
140
+ releasedApiSnapshotDate: snapshotDate,
141
+ baselineVersion: baseline,
142
+ scopeNote: SCOPE_NOTE,
143
+ };
144
+ }
145
+ export function cmdReadiness(argv, io) {
146
+ const { flags, rest } = parseFlags(argv);
147
+ const files = collectFiles(rest.length > 0 ? rest : ["."], io);
148
+ if (files.length === 0) {
149
+ io.err("No ABAP sources found.");
150
+ return 2;
151
+ }
152
+ const baseline = asVersion(flags.get("baseline"), "v758");
153
+ const reports = chunk(files, MAX_FILES).map((b) => checkCloudReadiness(b, baseline));
154
+ const merged = mergeReadiness(reports, baseline);
155
+ if (flags.has("json")) {
156
+ io.out(JSON.stringify({ files: files.length, ...merged }, null, 2));
157
+ }
158
+ else {
159
+ io.out(`ABAP Cloud readiness: ${merged.verdict} (score ${merged.score})`);
160
+ io.out(`${merged.cloudBlockerCount} cloud blocker(s) across ${files.length} file(s)`);
161
+ for (const c of merged.categories)
162
+ io.out(` ${c.category.padEnd(18)} ${String(c.count).padStart(4)} ${c.label}`);
163
+ if (merged.brokenAtBaseline.length > 0)
164
+ io.out(`${merged.brokenAtBaseline.length} finding(s) broken at ${baseline} regardless (fix first; not migration work)`);
165
+ if (merged.releasedApiFindings.length > 0) {
166
+ io.out(`${merged.releasedApiFindings.length} released-API note(s) (snapshot ${merged.releasedApiSnapshotDate}; informational, not scored):`);
167
+ for (const f of merged.releasedApiFindings)
168
+ io.out(` ${f.file}:${f.line} [${f.state}] ${f.object}${f.successor !== undefined ? ` → ${f.successor}` : ""}`);
169
+ }
170
+ io.out(`Note: ${merged.scopeNote}`);
171
+ }
172
+ const failBelow = flags.get("fail-below");
173
+ if (typeof failBelow === "string" && merged.score < Number(failBelow))
174
+ return 1;
175
+ return 0;
176
+ }
177
+ export function cmdScaffold(argv, io) {
178
+ const { flags } = parseFlags(argv);
179
+ const entityName = flags.get("entity");
180
+ const sqlTable = flags.get("table");
181
+ const keyField = flags.get("key");
182
+ if (typeof entityName !== "string" || typeof sqlTable !== "string" || typeof keyField !== "string") {
183
+ io.err("Usage: abap-mcp scaffold --entity Travel --table ztravel --key travel_id [--fields a:abap.char(6),b] [--no-draft] [--provided-key] [--out DIR]");
184
+ return 2;
185
+ }
186
+ const fields = [];
187
+ const fieldsRaw = flags.get("fields");
188
+ if (typeof fieldsRaw === "string") {
189
+ for (const part of fieldsRaw.split(",")) {
190
+ const [name, type] = part.split(":");
191
+ if (name !== undefined && name.length > 0)
192
+ fields.push(type !== undefined ? { name, type } : { name });
193
+ }
194
+ }
195
+ const result = scaffoldRapBo({
196
+ entityName,
197
+ sqlTable,
198
+ keyField,
199
+ managedUuidKey: !flags.has("provided-key"),
200
+ fields,
201
+ draft: !flags.has("no-draft"),
202
+ prefix: flags.get("prefix") === "Y" ? "Y" : "Z",
203
+ });
204
+ const outDir = typeof flags.get("out") === "string" ? flags.get("out") : null;
205
+ if (outDir !== null) {
206
+ if (!existsSync(outDir))
207
+ mkdirSync(outDir, { recursive: true });
208
+ for (const f of result.files) {
209
+ const target = join(outDir, f.filename);
210
+ if (existsSync(target) && !flags.has("force")) {
211
+ io.err(`refusing to overwrite ${target} (use --force)`);
212
+ return 1;
213
+ }
214
+ writeFileSync(target, f.content, "utf8");
215
+ io.out(`wrote ${target} [${f.validated}]`);
216
+ }
217
+ writeFileSync(join(outDir, `${sqlTable.toLowerCase()}.tabl.suggestion.txt`), result.suggestedTableDdl, "utf8");
218
+ }
219
+ else {
220
+ for (const f of result.files) {
221
+ io.out(`\n===== ${f.filename} [validated: ${f.validated}] =====`);
222
+ io.out(f.content);
223
+ }
224
+ io.out(`\n===== suggested table DDL =====\n${result.suggestedTableDdl}`);
225
+ }
226
+ io.out(`\nActivation order:\n${result.activationOrder.map((s, i) => ` ${i + 1}. ${s}`).join("\n")}`);
227
+ io.out(`\nNext steps:\n${result.nextSteps.map((s) => ` - ${s}`).join("\n")}`);
228
+ if (result.validationIssues.length > 0) {
229
+ io.err(`WARNING: ${result.validationIssues.length} abaplint finding(s) on generated code`);
230
+ return 1;
231
+ }
232
+ return 0;
233
+ }
234
+ export function cmdOutline(argv, io) {
235
+ const { flags, rest } = parseFlags(argv);
236
+ const files = collectFiles(rest.length > 0 ? rest : ["."], io);
237
+ if (files.length === 0) {
238
+ io.err("No ABAP sources found.");
239
+ return 2;
240
+ }
241
+ const outlines = chunk(files, MAX_FILES).flatMap((b) => outlineAbap(b));
242
+ if (flags.has("json")) {
243
+ io.out(JSON.stringify(outlines, null, 2));
244
+ return 0;
245
+ }
246
+ for (const o of outlines) {
247
+ if (!o.parseable)
248
+ continue;
249
+ for (const c of o.classes) {
250
+ io.out(`${o.file}: class ${c.name}${c.isGlobal ? "" : " (local)"}${c.superClass !== null ? ` extends ${c.superClass}` : ""}`);
251
+ for (const m of c.methods)
252
+ io.out(` ${m.visibility.padEnd(9)} ${m.name}`);
253
+ }
254
+ for (const i of o.interfaces)
255
+ io.out(`${o.file}: interface ${i}`);
256
+ for (const f of o.forms)
257
+ io.out(`${o.file}: form ${f}`);
258
+ }
259
+ return 0;
260
+ }
261
+ export function cmdExplain(argv, io) {
262
+ const { rest } = parseFlags(argv);
263
+ const key = rest[0];
264
+ if (key === undefined) {
265
+ io.err("Usage: abap-mcp explain <rule_key>");
266
+ return 2;
267
+ }
268
+ const d = explainRule(key);
269
+ io.out(`${d.key} — ${d.title}\n${d.shortDescription}\n${d.extendedInformation}\nDocs: ${d.docsUrl}`);
270
+ return 0;
271
+ }
272
+ export function cmdReleased(argv, io) {
273
+ const { flags, rest } = parseFlags(argv);
274
+ if (rest.length === 0) {
275
+ io.err("Usage: abap-mcp released <object-name…> [--type TABL|CDS_STOB|FUNC|…] [--json]");
276
+ return 2;
277
+ }
278
+ const type = typeof flags.get("type") === "string" ? flags.get("type") : undefined;
279
+ const results = rest.map((name) => {
280
+ const hit = lookupReleased(name, type);
281
+ const successor = suggestSuccessor(name);
282
+ return { ...hit, successor };
283
+ });
284
+ if (flags.has("json")) {
285
+ io.out(JSON.stringify({ snapshotDate: RELEASED_API_SNAPSHOT.snapshotDate, source: RELEASED_API_SNAPSHOT.source, results }, null, 2));
286
+ return 0;
287
+ }
288
+ io.out(`Released-API status (SAP Cloudification snapshot ${RELEASED_API_SNAPSHOT.snapshotDate}):`);
289
+ for (const r of results) {
290
+ const tail = r.successor !== undefined ? ` → use ${r.successor}` : "";
291
+ io.out(` ${r.name.padEnd(34)} ${r.state.padEnd(13)} ${(r.objectType ?? "").padEnd(9)}${tail}`);
292
+ }
293
+ return 0;
294
+ }
295
+ export function cmdRules(argv, io) {
296
+ const { flags } = parseFlags(argv);
297
+ const q = flags.get("query");
298
+ const t = flags.get("tag");
299
+ const rules = listRules(typeof q === "string" ? q : undefined, typeof t === "string" ? t : undefined);
300
+ for (const r of rules)
301
+ io.out(`${r.key.padEnd(36)} ${r.title}`);
302
+ io.out(`${rules.length} rule(s)`);
303
+ return 0;
304
+ }
305
+ export const USAGE = `abap-mcp — SAP ABAP analysis for AI agents (MCP server) and humans (CLI)
306
+
307
+ Usage:
308
+ abap-mcp start the MCP server on stdio (for AI clients)
309
+ abap-mcp lint [paths…] lint files/dirs [--abap-version v758|Cloud] [--preset style|full|syntax-only] [--json]
310
+ abap-mcp readiness [paths…] ABAP Cloud readiness diff [--baseline v758] [--fail-below N] [--json]
311
+ abap-mcp scaffold … generate a RAP managed BO (--entity --table --key [--fields n:type,…] [--no-draft] [--provided-key] [--out DIR])
312
+ abap-mcp outline [paths…] classes/methods/forms structure [--json]
313
+ abap-mcp released <names…> released-API status from the bundled SAP snapshot [--type TABL|FUNC|…] [--json]
314
+ abap-mcp explain <rule> explain an abaplint rule
315
+ abap-mcp rules list rules [--query q] [--tag Security]
316
+
317
+ Exit codes: 0 ok · 1 findings/validation failed · 2 usage error`;
318
+ export function runCli(argv, io) {
319
+ const [cmd, ...rest] = argv;
320
+ switch (cmd) {
321
+ case undefined:
322
+ case "serve":
323
+ return null; // caller starts the MCP server
324
+ case "lint":
325
+ return cmdLint(rest, io);
326
+ case "readiness":
327
+ return cmdReadiness(rest, io);
328
+ case "scaffold":
329
+ return cmdScaffold(rest, io);
330
+ case "outline":
331
+ return cmdOutline(rest, io);
332
+ case "released":
333
+ return cmdReleased(rest, io);
334
+ case "explain":
335
+ return cmdExplain(rest, io);
336
+ case "rules":
337
+ return cmdRules(rest, io);
338
+ case "help":
339
+ case "--help":
340
+ case "-h":
341
+ io.out(USAGE);
342
+ return 0;
343
+ default:
344
+ io.err(`Unknown command "${cmd}".\n\n${USAGE}`);
345
+ return 2;
346
+ }
347
+ }
package/dist/cli.js CHANGED
@@ -1,11 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * stdio entry point. stdout is the JSON-RPC channel anything human-facing
4
- * goes to stderr, or the protocol stream corrupts (mcp-kit invariant).
3
+ * Entry point. With a subcommand it acts as a local CLI; with none it starts
4
+ * the MCP server on stdio (back-compatible with `claude mcp add abap-mcp`).
5
+ * stdout is the JSON-RPC channel in server mode — banner goes to stderr.
5
6
  */
6
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { runCli } from "./cli-commands.js";
7
9
  import { buildServer, SERVER_NAME, SERVER_VERSION } from "./server.js";
8
10
  async function main() {
11
+ const code = runCli(process.argv.slice(2), {
12
+ out: (s) => console.log(s),
13
+ err: (s) => console.error(s),
14
+ });
15
+ if (code !== null) {
16
+ process.exit(code);
17
+ }
9
18
  const server = buildServer();
10
19
  await server.connect(new StdioServerTransport());
11
20
  console.error(`${SERVER_NAME} v${SERVER_VERSION} ready on stdio (offline; no SAP system required)`);