abap-mcp 0.1.0 → 0.2.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
@@ -46,6 +46,30 @@ claude mcp add abap-mcp -- node /path/to/abap-mcp/dist/cli.js
46
46
  Then ask your agent things like *"lint this class against ABAP Cloud"*, *"is zold_report
47
47
  cloud-ready?"*, or *"scaffold a RAP BO for entity Booking on table zbooking, draft enabled"*.
48
48
 
49
+ ## CLI — same engine, no AI required
50
+
51
+ Every tool is also a subcommand, so it works in terminals and CI where no MCP client exists:
52
+
53
+ ```bash
54
+ npx abap-mcp lint src/ # lint files or whole directories
55
+ npx abap-mcp readiness src/ --fail-below 80 # repo-level ABAP Cloud readiness, CI-gateable
56
+ npx abap-mcp scaffold --entity Travel --table ztravel --key travel_id --out ./out
57
+ npx abap-mcp outline src/zcl_monster.clas.abap # navigate big objects
58
+ npx abap-mcp explain exit_or_check # rule rationale
59
+ ```
60
+
61
+ Directories are walked recursively (abapGit naming), batched automatically, and `readiness`
62
+ merges batches into one scored, categorized repo report. Exit codes are CI-friendly
63
+ (`1` on error findings / failed threshold).
64
+
65
+ ## Recipes, agents & CI
66
+
67
+ **[docs/COOKBOOK.md](docs/COOKBOOK.md)** — practical recipes: the fix-until-clean loop,
68
+ PR review without a transport, whole-repo migration triage, CI gates, per-persona use cases.
69
+ **[examples/claude-code/](examples/claude-code/)** — drop-in agentic workflows: an
70
+ `abap-code-reviewer` subagent, an `abap-cloud-migrator` sweep loop (readiness score as the
71
+ loop condition), per-repo `.mcp.json`, and a GitHub Actions quality gate for abapGit repos.
72
+
49
73
  ## Tools
50
74
 
51
75
  | Tool | What it does |
@@ -72,7 +96,7 @@ cloud-ready?"*, or *"scaffold a RAP BO for entity Booking on table zbooking, dra
72
96
  - **Text-in only, by design.** No filesystem walking, no network — the entire attack surface is
73
97
  a parser over strings you explicitly pass. For linting whole directories, use the
74
98
  [abaplint CLI](https://abaplint.org) in CI, or the
75
- [mcp-kit `wrap-abaplint` recipe](https://github.com/lumivarahq/mcp-kit) this server grew out of.
99
+ [mcp-kit `wrap-abaplint` recipe](https://github.com/palimkarakshay/mcp-kit) this server grew out of.
76
100
 
77
101
  ## Develop
78
102
 
@@ -85,7 +109,7 @@ npx @modelcontextprotocol/inspector --cli node dist/cli.js --method tools/list
85
109
 
86
110
  Tool descriptions are CI-graded (a rubric test enforces verb-first names, when-to-use,
87
111
  non-goals, described params, worked examples — the
88
- [mcp-kit](https://github.com/lumivarahq/mcp-kit) discipline; the full mcp-kit lint scores all
112
+ [mcp-kit](https://github.com/palimkarakshay/mcp-kit) discipline; the full mcp-kit lint scores all
89
113
  seven tools 100/100).
90
114
 
91
115
  ## Design
@@ -98,7 +122,7 @@ scaffolder validates its own output, what was deliberately left out — lives in
98
122
 
99
123
  - [abaplint](https://github.com/abaplint/abaplint) by Lars Hvam — the parser and rule engine
100
124
  underneath every tool here (MIT).
101
- - [mcp-kit](https://github.com/lumivarahq/mcp-kit) — the production-MCP patterns this server
125
+ - [mcp-kit](https://github.com/palimkarakshay/mcp-kit) — the production-MCP patterns this server
102
126
  follows (typed tool specs, transport discipline, description lint).
103
127
 
104
128
  MIT © Akshay Palimkar. Not affiliated with or endorsed by SAP SE. "SAP", "ABAP" and "RAP" are
@@ -0,0 +1,22 @@
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 cmdRules(argv: string[], io: CliIo): number;
21
+ 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 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";
22
+ export declare function runCli(argv: string[], io: CliIo): number | null;
@@ -0,0 +1,309 @@
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 { explainRule, listRules } from "./abap/rules.js";
14
+ import { scaffoldRapBo } from "./abap/scaffold.js";
15
+ 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)$/;
16
+ /** Recursively collect analyzable sources from file/dir paths. */
17
+ export function collectFiles(paths, io) {
18
+ const found = [];
19
+ const visit = (p) => {
20
+ const st = statSync(p);
21
+ if (st.isDirectory()) {
22
+ if (basename(p) === ".git" || basename(p) === "node_modules")
23
+ return;
24
+ for (const entry of readdirSync(p))
25
+ visit(join(p, entry));
26
+ return;
27
+ }
28
+ const name = basename(p).toLowerCase();
29
+ if (ABAP_FILE_RE.test(name)) {
30
+ found.push({ filename: name, source: readFileSync(p, "utf8") });
31
+ }
32
+ else if ([".abap", ".asddls", ".asbdef"].includes(extname(name))) {
33
+ io.err(`skip ${p}: not an abapGit-style filename (e.g. zcl_x.clas.abap)`);
34
+ }
35
+ };
36
+ for (const p of paths)
37
+ visit(p);
38
+ return found;
39
+ }
40
+ function chunk(arr, size) {
41
+ const out = [];
42
+ for (let i = 0; i < arr.length; i += size)
43
+ out.push(arr.slice(i, i + size));
44
+ return out;
45
+ }
46
+ export function parseFlags(argv) {
47
+ const flags = new Map();
48
+ const rest = [];
49
+ for (let i = 0; i < argv.length; i++) {
50
+ const a = argv[i];
51
+ if (a.startsWith("--")) {
52
+ const key = a.slice(2);
53
+ const next = argv[i + 1];
54
+ if (next !== undefined && !next.startsWith("--")) {
55
+ flags.set(key, next);
56
+ i++;
57
+ }
58
+ else {
59
+ flags.set(key, true);
60
+ }
61
+ }
62
+ else {
63
+ rest.push(a);
64
+ }
65
+ }
66
+ return { flags, rest };
67
+ }
68
+ function asVersion(v, fallback) {
69
+ if (typeof v !== "string")
70
+ return fallback;
71
+ if (ABAP_VERSIONS.includes(v))
72
+ return v;
73
+ throw new Error(`Unknown ABAP version "${v}". Valid: ${ABAP_VERSIONS.join(", ")}`);
74
+ }
75
+ function fmtFinding(f) {
76
+ return `${f.file}:${f.line}:${f.column} [${f.severity}] ${f.rule}: ${f.message}`;
77
+ }
78
+ export function cmdLint(argv, io) {
79
+ const { flags, rest } = parseFlags(argv);
80
+ const files = collectFiles(rest.length > 0 ? rest : ["."], io);
81
+ if (files.length === 0) {
82
+ io.err("No ABAP sources found.");
83
+ return 2;
84
+ }
85
+ const version = asVersion(flags.get("abap-version"), "v758");
86
+ const presetRaw = flags.get("preset");
87
+ const preset = presetRaw === "full" || presetRaw === "syntax-only" ? presetRaw : "style";
88
+ const all = [];
89
+ for (const batch of chunk(files, MAX_FILES)) {
90
+ all.push(...runAbaplint(batch, { version, preset }).findings);
91
+ }
92
+ if (flags.has("json")) {
93
+ io.out(JSON.stringify({ files: files.length, findings: all }, null, 2));
94
+ }
95
+ else {
96
+ for (const f of all)
97
+ io.out(fmtFinding(f));
98
+ io.out(`${all.length} finding(s) in ${files.length} file(s) [${preset} @ ${version}]`);
99
+ }
100
+ return all.some((f) => f.severity === "Error") ? 1 : 0;
101
+ }
102
+ /** Merge per-batch readiness reports into one repo-level report. */
103
+ export function mergeReadiness(reports, baseline) {
104
+ const categories = new Map();
105
+ let blockers = 0;
106
+ const broken = [];
107
+ for (const r of reports) {
108
+ blockers += r.cloudBlockerCount;
109
+ broken.push(...r.brokenAtBaseline);
110
+ for (const c of r.categories) {
111
+ const cur = categories.get(c.category);
112
+ if (cur === undefined)
113
+ categories.set(c.category, { ...c, findings: [...c.findings] });
114
+ else {
115
+ cur.count += c.count;
116
+ cur.findings.push(...c.findings);
117
+ }
118
+ }
119
+ }
120
+ const score = Math.max(0, 100 - 5 * blockers);
121
+ const verdict = blockers === 0
122
+ ? "ready"
123
+ : blockers <= 5
124
+ ? "minor-rework"
125
+ : blockers <= 20
126
+ ? "moderate-rework"
127
+ : "significant-rework";
128
+ return {
129
+ verdict,
130
+ score,
131
+ cloudBlockerCount: blockers,
132
+ categories: [...categories.values()].sort((a, b) => b.count - a.count),
133
+ brokenAtBaseline: broken,
134
+ baselineVersion: baseline,
135
+ scopeNote: SCOPE_NOTE,
136
+ };
137
+ }
138
+ export function cmdReadiness(argv, io) {
139
+ const { flags, rest } = parseFlags(argv);
140
+ const files = collectFiles(rest.length > 0 ? rest : ["."], io);
141
+ if (files.length === 0) {
142
+ io.err("No ABAP sources found.");
143
+ return 2;
144
+ }
145
+ const baseline = asVersion(flags.get("baseline"), "v758");
146
+ const reports = chunk(files, MAX_FILES).map((b) => checkCloudReadiness(b, baseline));
147
+ const merged = mergeReadiness(reports, baseline);
148
+ if (flags.has("json")) {
149
+ io.out(JSON.stringify({ files: files.length, ...merged }, null, 2));
150
+ }
151
+ else {
152
+ io.out(`ABAP Cloud readiness: ${merged.verdict} (score ${merged.score})`);
153
+ io.out(`${merged.cloudBlockerCount} cloud blocker(s) across ${files.length} file(s)`);
154
+ for (const c of merged.categories)
155
+ io.out(` ${c.category.padEnd(18)} ${String(c.count).padStart(4)} ${c.label}`);
156
+ if (merged.brokenAtBaseline.length > 0)
157
+ io.out(`${merged.brokenAtBaseline.length} finding(s) broken at ${baseline} regardless (fix first; not migration work)`);
158
+ io.out(`Note: ${merged.scopeNote}`);
159
+ }
160
+ const failBelow = flags.get("fail-below");
161
+ if (typeof failBelow === "string" && merged.score < Number(failBelow))
162
+ return 1;
163
+ return 0;
164
+ }
165
+ export function cmdScaffold(argv, io) {
166
+ const { flags } = parseFlags(argv);
167
+ const entityName = flags.get("entity");
168
+ const sqlTable = flags.get("table");
169
+ const keyField = flags.get("key");
170
+ if (typeof entityName !== "string" || typeof sqlTable !== "string" || typeof keyField !== "string") {
171
+ 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]");
172
+ return 2;
173
+ }
174
+ const fields = [];
175
+ const fieldsRaw = flags.get("fields");
176
+ if (typeof fieldsRaw === "string") {
177
+ for (const part of fieldsRaw.split(",")) {
178
+ const [name, type] = part.split(":");
179
+ if (name !== undefined && name.length > 0)
180
+ fields.push(type !== undefined ? { name, type } : { name });
181
+ }
182
+ }
183
+ const result = scaffoldRapBo({
184
+ entityName,
185
+ sqlTable,
186
+ keyField,
187
+ managedUuidKey: !flags.has("provided-key"),
188
+ fields,
189
+ draft: !flags.has("no-draft"),
190
+ prefix: flags.get("prefix") === "Y" ? "Y" : "Z",
191
+ });
192
+ const outDir = typeof flags.get("out") === "string" ? flags.get("out") : null;
193
+ if (outDir !== null) {
194
+ if (!existsSync(outDir))
195
+ mkdirSync(outDir, { recursive: true });
196
+ for (const f of result.files) {
197
+ const target = join(outDir, f.filename);
198
+ if (existsSync(target) && !flags.has("force")) {
199
+ io.err(`refusing to overwrite ${target} (use --force)`);
200
+ return 1;
201
+ }
202
+ writeFileSync(target, f.content, "utf8");
203
+ io.out(`wrote ${target} [${f.validated}]`);
204
+ }
205
+ writeFileSync(join(outDir, `${sqlTable.toLowerCase()}.tabl.suggestion.txt`), result.suggestedTableDdl, "utf8");
206
+ }
207
+ else {
208
+ for (const f of result.files) {
209
+ io.out(`\n===== ${f.filename} [validated: ${f.validated}] =====`);
210
+ io.out(f.content);
211
+ }
212
+ io.out(`\n===== suggested table DDL =====\n${result.suggestedTableDdl}`);
213
+ }
214
+ io.out(`\nActivation order:\n${result.activationOrder.map((s, i) => ` ${i + 1}. ${s}`).join("\n")}`);
215
+ io.out(`\nNext steps:\n${result.nextSteps.map((s) => ` - ${s}`).join("\n")}`);
216
+ if (result.validationIssues.length > 0) {
217
+ io.err(`WARNING: ${result.validationIssues.length} abaplint finding(s) on generated code`);
218
+ return 1;
219
+ }
220
+ return 0;
221
+ }
222
+ export function cmdOutline(argv, io) {
223
+ const { flags, rest } = parseFlags(argv);
224
+ const files = collectFiles(rest.length > 0 ? rest : ["."], io);
225
+ if (files.length === 0) {
226
+ io.err("No ABAP sources found.");
227
+ return 2;
228
+ }
229
+ const outlines = chunk(files, MAX_FILES).flatMap((b) => outlineAbap(b));
230
+ if (flags.has("json")) {
231
+ io.out(JSON.stringify(outlines, null, 2));
232
+ return 0;
233
+ }
234
+ for (const o of outlines) {
235
+ if (!o.parseable)
236
+ continue;
237
+ for (const c of o.classes) {
238
+ io.out(`${o.file}: class ${c.name}${c.isGlobal ? "" : " (local)"}${c.superClass !== null ? ` extends ${c.superClass}` : ""}`);
239
+ for (const m of c.methods)
240
+ io.out(` ${m.visibility.padEnd(9)} ${m.name}`);
241
+ }
242
+ for (const i of o.interfaces)
243
+ io.out(`${o.file}: interface ${i}`);
244
+ for (const f of o.forms)
245
+ io.out(`${o.file}: form ${f}`);
246
+ }
247
+ return 0;
248
+ }
249
+ export function cmdExplain(argv, io) {
250
+ const { rest } = parseFlags(argv);
251
+ const key = rest[0];
252
+ if (key === undefined) {
253
+ io.err("Usage: abap-mcp explain <rule_key>");
254
+ return 2;
255
+ }
256
+ const d = explainRule(key);
257
+ io.out(`${d.key} — ${d.title}\n${d.shortDescription}\n${d.extendedInformation}\nDocs: ${d.docsUrl}`);
258
+ return 0;
259
+ }
260
+ export function cmdRules(argv, io) {
261
+ const { flags } = parseFlags(argv);
262
+ const q = flags.get("query");
263
+ const t = flags.get("tag");
264
+ const rules = listRules(typeof q === "string" ? q : undefined, typeof t === "string" ? t : undefined);
265
+ for (const r of rules)
266
+ io.out(`${r.key.padEnd(36)} ${r.title}`);
267
+ io.out(`${rules.length} rule(s)`);
268
+ return 0;
269
+ }
270
+ export const USAGE = `abap-mcp — SAP ABAP analysis for AI agents (MCP server) and humans (CLI)
271
+
272
+ Usage:
273
+ abap-mcp start the MCP server on stdio (for AI clients)
274
+ abap-mcp lint [paths…] lint files/dirs [--abap-version v758|Cloud] [--preset style|full|syntax-only] [--json]
275
+ abap-mcp readiness [paths…] ABAP Cloud readiness diff [--baseline v758] [--fail-below N] [--json]
276
+ abap-mcp scaffold … generate a RAP managed BO (--entity --table --key [--fields n:type,…] [--no-draft] [--provided-key] [--out DIR])
277
+ abap-mcp outline [paths…] classes/methods/forms structure [--json]
278
+ abap-mcp explain <rule> explain an abaplint rule
279
+ abap-mcp rules list rules [--query q] [--tag Security]
280
+
281
+ Exit codes: 0 ok · 1 findings/validation failed · 2 usage error`;
282
+ export function runCli(argv, io) {
283
+ const [cmd, ...rest] = argv;
284
+ switch (cmd) {
285
+ case undefined:
286
+ case "serve":
287
+ return null; // caller starts the MCP server
288
+ case "lint":
289
+ return cmdLint(rest, io);
290
+ case "readiness":
291
+ return cmdReadiness(rest, io);
292
+ case "scaffold":
293
+ return cmdScaffold(rest, io);
294
+ case "outline":
295
+ return cmdOutline(rest, io);
296
+ case "explain":
297
+ return cmdExplain(rest, io);
298
+ case "rules":
299
+ return cmdRules(rest, io);
300
+ case "help":
301
+ case "--help":
302
+ case "-h":
303
+ io.out(USAGE);
304
+ return 0;
305
+ default:
306
+ io.err(`Unknown command "${cmd}".\n\n${USAGE}`);
307
+ return 2;
308
+ }
309
+ }
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)`);
package/dist/errors.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Structured tool failure — the one way tools in this server fail.
3
3
  *
4
- * Pattern adapted from @mcp-kit/core (github.com/lumivarahq/mcp-kit, MIT):
4
+ * Pattern adapted from @mcp-kit/core (github.com/palimkarakshay/mcp-kit, MIT):
5
5
  * handlers throw `McpToolError` (or anything), the registration wrapper turns
6
6
  * it into an MCP error result instead of crashing the request.
7
7
  */
package/dist/tool.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * same object that registers the tool is the one a description lint can grade,
5
5
  * so model-facing docs can't drift from what's enforced.
6
6
  *
7
- * Pattern adapted from @mcp-kit/core (github.com/lumivarahq/mcp-kit, MIT).
7
+ * Pattern adapted from @mcp-kit/core (github.com/palimkarakshay/mcp-kit, MIT).
8
8
  */
9
9
  import type { McpServer, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
10
10
  import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "abap-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/lumivarahq/abap-mcp.git"
9
+ "url": "git+https://github.com/palimkarakshay/abap-mcp.git"
10
10
  },
11
11
  "keywords": [
12
12
  "mcp",