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 +27 -3
- package/dist/cli-commands.d.ts +22 -0
- package/dist/cli-commands.js +309 -0
- package/dist/cli.js +11 -2
- package/dist/errors.d.ts +1 -1
- package/dist/tool.d.ts +1 -1
- package/package.json +2 -2
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/
|
|
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/
|
|
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/
|
|
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
|
-
*
|
|
4
|
-
*
|
|
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/
|
|
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/
|
|
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.
|
|
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/
|
|
9
|
+
"url": "git+https://github.com/palimkarakshay/abap-mcp.git"
|
|
10
10
|
},
|
|
11
11
|
"keywords": [
|
|
12
12
|
"mcp",
|