abap-mcp 0.1.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,435 @@
1
+ /**
2
+ * The abap-mcp tool registry.
3
+ *
4
+ * Descriptions follow the mcp-kit rubric: verb-first snake_case name, what
5
+ * the tool operates on, an explicit "Use this when …", explicit non-goals,
6
+ * every parameter described, at least one worked example. The model's only
7
+ * documentation is this file — treat it as the public API surface.
8
+ */
9
+ import { z } from "zod";
10
+ import { ABAP_VERSIONS, runAbaplint } from "./abap/engine.js";
11
+ import { formatAbap } from "./abap/formatter.js";
12
+ import { outlineAbap } from "./abap/outline.js";
13
+ import { checkCloudReadiness } from "./abap/readiness.js";
14
+ import { explainRule, listRules } from "./abap/rules.js";
15
+ import { scaffoldRapBo } from "./abap/scaffold.js";
16
+ import { invalidInput } from "./errors.js";
17
+ import { defineTool } from "./tool.js";
18
+ const VERSION_ENUM = z.enum(ABAP_VERSIONS);
19
+ const filesField = z
20
+ .array(z.object({
21
+ filename: z
22
+ .string()
23
+ .optional()
24
+ .describe('abapGit-style name, e.g. "zcl_invoice.clas.abap", "ztrip.prog.abap", "zr_trip.ddls.asddls". Omit it and the type is inferred from the source (CLASS → class, REPORT → program, define view → CDS).'),
25
+ source: z.string().describe("The complete ABAP / CDS / behavior-definition source text."),
26
+ }))
27
+ .min(1)
28
+ .max(32)
29
+ .describe("Source files to analyze, up to 32 per call, 100k chars each.");
30
+ const sevenSampleAbap = 'lint_abap({ "files": [ { "source": "REPORT ztest.\\nDATA foo TYPE i.\\nIF foo = 1.\\nENDIF." } ] })';
31
+ export const lintAbap = defineTool({
32
+ name: "lint_abap",
33
+ title: "Lint ABAP source",
34
+ description: "Run abaplint static analysis over ABAP, CDS or behavior-definition sources and return structured findings " +
35
+ "(rule key, message, severity, file/line/column, the offending line, and a docs URL per finding). " +
36
+ "Use this when you have written or modified ABAP code and want style and correctness feedback before it goes " +
37
+ "anywhere near a system — it runs entirely offline on the provided text. " +
38
+ "It does not connect to any SAP system, does not run ATC, and cannot judge whether referenced objects exist " +
39
+ "unless you provide them in the same call (preset \"style\", the default, skips whole-program checks for that reason; " +
40
+ "preset \"full\" enables them when you provide every dependency). For an ABAP-Cloud migration verdict use " +
41
+ "check_cloud_readiness instead. " +
42
+ `Example: ${sevenSampleAbap}.`,
43
+ inputSchema: {
44
+ files: filesField,
45
+ abapVersion: VERSION_ENUM.default("v758").describe('ABAP language version to parse against. "v758" (default) is current on-prem; "Cloud" is ABAP Cloud / Steampunk.'),
46
+ preset: z
47
+ .enum(["style", "full", "syntax-only"])
48
+ .default("style")
49
+ .describe('"style" (default): abaplint default rules minus whole-program semantic checks — right for isolated snippets. "full": every default rule, expects all referenced objects provided. "syntax-only": parser errors only.'),
50
+ rules: z
51
+ .record(z.string(), z.unknown())
52
+ .optional()
53
+ .describe('abaplint rule overrides merged onto the preset, e.g. { "line_length": { "length": 120 }, "7bit_ascii": false }.'),
54
+ },
55
+ outputSchema: {
56
+ findings: z.array(z.object({
57
+ rule: z.string().describe("abaplint rule key."),
58
+ message: z.string().describe("Human-readable finding."),
59
+ severity: z.string().describe("Error, Warning or Info."),
60
+ file: z.string().describe("Filename the finding is in."),
61
+ line: z.number().describe("1-based line."),
62
+ column: z.number().describe("1-based column."),
63
+ excerpt: z.string().describe("The offending line, trimmed."),
64
+ docsUrl: z.string().describe("Rule documentation at rules.abaplint.org."),
65
+ })),
66
+ truncated: z.boolean().describe("True if more than 500 findings existed and the list was cut."),
67
+ fileCount: z.number().describe("Number of files analyzed."),
68
+ },
69
+ annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
70
+ examples: [
71
+ {
72
+ description: "Lint a small report with the default style preset.",
73
+ arguments: {
74
+ files: [{ source: "REPORT ztest.\nDATA foo TYPE i.\nIF foo = 1.\nENDIF." }],
75
+ },
76
+ },
77
+ {
78
+ description: "Lint a class against ABAP Cloud with a relaxed line length.",
79
+ arguments: {
80
+ files: [{ filename: "zcl_demo.clas.abap", source: "CLASS zcl_demo DEFINITION PUBLIC.\nENDCLASS.\nCLASS zcl_demo IMPLEMENTATION.\nENDCLASS." }],
81
+ abapVersion: "Cloud",
82
+ rules: { line_length: { length: 120 } },
83
+ },
84
+ },
85
+ ],
86
+ handler: (args) => {
87
+ const result = runAbaplint(args.files, {
88
+ version: args.abapVersion,
89
+ preset: args.preset,
90
+ rules: args.rules,
91
+ });
92
+ const text = result.findings.length === 0
93
+ ? `No findings in ${result.fileCount} file(s).`
94
+ : result.findings
95
+ .map((f) => `${f.file}:${f.line} [${f.severity}] ${f.rule}: ${f.message}`)
96
+ .join("\n");
97
+ return {
98
+ content: [{ type: "text", text }],
99
+ structuredContent: result,
100
+ };
101
+ },
102
+ });
103
+ export const checkCloudReadinessTool = defineTool({
104
+ name: "check_cloud_readiness",
105
+ title: "Check ABAP Cloud readiness",
106
+ description: "Assess how far ABAP source is from ABAP Cloud (Clean Core tier 1) by parsing it twice — once at a classic " +
107
+ "baseline (default v758) and once at version Cloud — and diffing: findings that appear only at Cloud are genuine " +
108
+ "cloud blockers (statements ABAP Cloud removed), reported in categories (dynpro, list output, native SQL, report " +
109
+ "events, …) with a transparent score and verdict; findings already present at the baseline are reported separately " +
110
+ "as broken code, not migration work. " +
111
+ "Use this when someone asks 'is this code cloud-ready / Clean Core compliant / S/4HANA-cloud safe' or before " +
112
+ "porting classic ABAP into an ABAP Cloud environment. " +
113
+ "It is static and parser-level: it does not check released-API usage (that needs a system's ATC), does not " +
114
+ "connect to any SAP system, and a 'ready' verdict means no language-level blockers — not a certification. " +
115
+ 'Example: check_cloud_readiness({ "files": [ { "source": "REPORT zold.\\nWRITE: / \'hi\'." } ] }).',
116
+ inputSchema: {
117
+ files: filesField,
118
+ baselineVersion: VERSION_ENUM.default("v758").describe("Classic ABAP version the code is assumed to run on today; used to separate broken-anyway code from cloud blockers."),
119
+ },
120
+ outputSchema: {
121
+ verdict: z
122
+ .enum(["ready", "minor-rework", "moderate-rework", "significant-rework"])
123
+ .describe("Banded verdict from the blocker count (0 / ≤5 / ≤20 / >20)."),
124
+ score: z.number().describe("100 − 5×blockers, floored at 0. Transparent, not an oracle."),
125
+ cloudBlockerCount: z.number().describe("Statements valid at the baseline but not in ABAP Cloud."),
126
+ categories: z.array(z.object({
127
+ category: z.string().describe("Stable category id, e.g. dynpro, list-output, native-sql."),
128
+ label: z.string().describe("What this category means and the usual remediation."),
129
+ count: z.number().describe("Blockers in this category."),
130
+ findings: z.array(z.unknown()).describe("The individual findings (same shape as lint_abap)."),
131
+ })),
132
+ brokenAtBaseline: z
133
+ .array(z.unknown())
134
+ .describe("Findings that fail even at the baseline version — fix first, they are not migration items."),
135
+ baselineVersion: z.string().describe("The baseline used."),
136
+ scopeNote: z.string().describe("Exactly what this check does and does not cover."),
137
+ },
138
+ annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
139
+ examples: [
140
+ {
141
+ description: "Check a classic report for cloud blockers.",
142
+ arguments: {
143
+ files: [{ source: "REPORT zold.\nWRITE: / 'hi'.\nCALL SCREEN 100." }],
144
+ },
145
+ },
146
+ ],
147
+ handler: (args) => {
148
+ const report = checkCloudReadiness(args.files, args.baselineVersion);
149
+ const catLine = report.categories.map((c) => `${c.category}=${c.count}`).join(", ");
150
+ const text = `${report.verdict} (score ${report.score}): ${report.cloudBlockerCount} cloud blocker(s)` +
151
+ (catLine.length > 0 ? ` [${catLine}]` : "") +
152
+ (report.brokenAtBaseline.length > 0
153
+ ? `; ${report.brokenAtBaseline.length} finding(s) broken at ${report.baselineVersion} regardless`
154
+ : "");
155
+ return {
156
+ content: [{ type: "text", text }],
157
+ structuredContent: report,
158
+ };
159
+ },
160
+ });
161
+ export const scaffoldRapBoTool = defineTool({
162
+ name: "scaffold_rap_bo",
163
+ title: "Scaffold a RAP business object",
164
+ description: "Generate the complete, canonical RAP managed business-object stack for one root entity: root CDS view entity, " +
165
+ "behavior definition (managed, strict(2), optional draft), behavior implementation class with handler locals, " +
166
+ "projection view with transactional_query, projection behavior definition, UI metadata extension, and an OData V4 " +
167
+ "service definition — plus a suggested table DDL, the activation order, and next steps. " +
168
+ "Use this when starting a new RAP business object in ABAP Cloud or S/4HANA and you want correct boilerplate that " +
169
+ "follows the SAP /DMO reference shape instead of writing it by hand. " +
170
+ "Generated classes and CDS views are round-trip validated through abaplint at ABAP-Cloud level before being " +
171
+ "returned; behavior and service definitions are canonical templates (abaplint does not parse those deeply) and " +
172
+ "ADT activation is the final check. It does not create the table or the service binding (binding is not a source " +
173
+ "artifact — create it in ADT), and it generates single-entity BOs: model compositions (parent-child) yourself for now. " +
174
+ 'Example: scaffold_rap_bo({ "entityName": "Travel", "sqlTable": "ztravel", "keyField": "travel_id", ' +
175
+ '"fields": [ { "name": "agency_id", "type": "abap.char(6)" } ], "draft": true }).',
176
+ inputSchema: {
177
+ entityName: z
178
+ .string()
179
+ .describe('Entity name in UpperCamelCase, e.g. "Travel" — drives ZR_/ZC_/ZBP_/ZUI_ artifact names.'),
180
+ sqlTable: z
181
+ .string()
182
+ .describe('Persistent table the BO is backed by, e.g. "ztravel". Must start with the namespace prefix.'),
183
+ keyField: z.string().describe('snake_case key field of that table, e.g. "travel_id".'),
184
+ managedUuidKey: z
185
+ .boolean()
186
+ .default(true)
187
+ .describe("true (default): UUID key filled by managed numbering — modern RAP default. false: the caller provides the key on create."),
188
+ fields: z
189
+ .array(z.object({
190
+ name: z.string().describe('snake_case table field, e.g. "agency_id".'),
191
+ type: z
192
+ .string()
193
+ .optional()
194
+ .describe('Suggested DDL type for the table proposal, e.g. "abap.char(6)". Defaults to abap.char(30).'),
195
+ }))
196
+ .max(60)
197
+ .default([])
198
+ .describe("Non-key business fields. Admin fields (created_by/created_at/…) are added automatically."),
199
+ draft: z.boolean().default(true).describe("Generate draft handling (draft table reference, draft actions, use draft)."),
200
+ prefix: z.enum(["Z", "Y"]).default("Z").describe("Customer namespace prefix for all generated names."),
201
+ },
202
+ outputSchema: {
203
+ files: z.array(z.object({
204
+ filename: z.string().describe("abapGit-conventional filename."),
205
+ content: z.string().describe("Complete source, ready to paste into ADT or commit via abapGit."),
206
+ validated: z
207
+ .enum(["abaplint", "template"])
208
+ .describe('"abaplint" = machine-parsed at Cloud level; "template" = golden-tested canonical template.'),
209
+ })),
210
+ activationOrder: z.array(z.string()).describe("The order to create/activate artifacts in ADT."),
211
+ nextSteps: z.array(z.string()).describe("What the generator cannot do for you (table, binding, draft table)."),
212
+ suggestedTableDdl: z.string().describe("Starting-point DDL for the persistent table; adjust types."),
213
+ validationIssues: z
214
+ .array(z.unknown())
215
+ .describe("abaplint findings on the generated sources — empty in normal operation."),
216
+ },
217
+ annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
218
+ examples: [
219
+ {
220
+ description: "Draft-enabled Travel BO with one business field.",
221
+ arguments: {
222
+ entityName: "Travel",
223
+ sqlTable: "ztravel",
224
+ keyField: "travel_id",
225
+ fields: [{ name: "agency_id", type: "abap.char(6)" }],
226
+ draft: true,
227
+ },
228
+ },
229
+ {
230
+ description: "Minimal no-draft BO with a caller-provided key.",
231
+ arguments: {
232
+ entityName: "CostCenter",
233
+ sqlTable: "zcostcenter",
234
+ keyField: "cost_center_id",
235
+ managedUuidKey: false,
236
+ draft: false,
237
+ },
238
+ },
239
+ ],
240
+ handler: (args) => {
241
+ const result = scaffoldRapBo({
242
+ entityName: args.entityName,
243
+ sqlTable: args.sqlTable,
244
+ keyField: args.keyField,
245
+ managedUuidKey: args.managedUuidKey,
246
+ fields: args.fields,
247
+ draft: args.draft,
248
+ prefix: args.prefix,
249
+ });
250
+ const text = `Generated ${result.files.length} artifacts for ${args.entityName} ` +
251
+ `(${result.files.map((f) => f.filename).join(", ")}). ` +
252
+ (result.validationIssues.length === 0
253
+ ? "All machine-checkable sources passed abaplint at Cloud level."
254
+ : `WARNING: ${result.validationIssues.length} abaplint finding(s) on generated code.`);
255
+ return {
256
+ content: [{ type: "text", text }],
257
+ structuredContent: result,
258
+ };
259
+ },
260
+ });
261
+ export const listAbapRules = defineTool({
262
+ name: "list_abap_rules",
263
+ title: "List abaplint rules",
264
+ description: "List the abaplint rules this server can check, optionally filtered by a free-text query or a tag, returning " +
265
+ "key, title, one-line description, tags and a documentation URL per rule. " +
266
+ "Use this when deciding which rules to enable or override in lint_abap, or to discover what a Clean-ABAP-style " +
267
+ "check exists for. It does not run any analysis and does not change configuration — it is a read-only catalog. " +
268
+ 'Example: list_abap_rules({ "query": "obsolete" }).',
269
+ inputSchema: {
270
+ query: z
271
+ .string()
272
+ .optional()
273
+ .describe('Case-insensitive substring matched against rule key, title and description, e.g. "select" or "obsolete".'),
274
+ tag: z
275
+ .string()
276
+ .optional()
277
+ .describe('Filter by abaplint tag, e.g. "Styleguide", "Security", "Performance", "Quickfix", "SingleFile".'),
278
+ },
279
+ outputSchema: {
280
+ count: z.number().describe("Number of rules returned."),
281
+ rules: z.array(z.object({
282
+ key: z.string().describe("Rule key, usable in lint_abap rules overrides."),
283
+ title: z.string().describe("Short rule title."),
284
+ shortDescription: z.string().describe("One-line description."),
285
+ tags: z.array(z.string()).describe("abaplint tags."),
286
+ docsUrl: z.string().describe("Documentation at rules.abaplint.org."),
287
+ })),
288
+ },
289
+ annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
290
+ examples: [
291
+ { description: "All rules about SELECT statements.", arguments: { query: "select" } },
292
+ { description: "Everything tagged Security.", arguments: { tag: "Security" } },
293
+ ],
294
+ handler: (args) => {
295
+ const rules = listRules(args.query, args.tag);
296
+ const text = rules.length === 0
297
+ ? "No rules matched."
298
+ : rules
299
+ .slice(0, 50)
300
+ .map((r) => `${r.key} — ${r.title}`)
301
+ .join("\n") + (rules.length > 50 ? `\n… and ${rules.length - 50} more.` : "");
302
+ return { content: [{ type: "text", text }], structuredContent: { count: rules.length, rules } };
303
+ },
304
+ });
305
+ export const explainAbapRule = defineTool({
306
+ name: "explain_abap_rule",
307
+ title: "Explain an abaplint rule",
308
+ description: "Explain one abaplint rule in depth: title, description, extended rationale (often citing the Clean ABAP style " +
309
+ "guide), tags, documentation URL, and good/bad code examples where the rule defines them. " +
310
+ "Use this when a lint_abap or check_cloud_readiness finding needs justification — to explain to a developer why " +
311
+ "the finding matters and how to fix it. It does not run analysis and only knows abaplint rules; SAP ATC check " +
312
+ "documentation is out of scope. " +
313
+ 'Example: explain_abap_rule({ "rule": "exit_or_check" }).',
314
+ inputSchema: {
315
+ rule: z.string().describe('The abaplint rule key from a finding, e.g. "exit_or_check" or "obsolete_statement".'),
316
+ },
317
+ outputSchema: {
318
+ key: z.string().describe("Rule key."),
319
+ title: z.string().describe("Rule title."),
320
+ shortDescription: z.string().describe("One-line description."),
321
+ extendedInformation: z.string().describe("Extended rationale; may cite Clean ABAP."),
322
+ tags: z.array(z.string()).describe("abaplint tags."),
323
+ docsUrl: z.string().describe("Documentation URL."),
324
+ badExample: z.string().optional().describe("Code the rule flags, if the rule ships an example."),
325
+ goodExample: z.string().optional().describe("The compliant version, if the rule ships one."),
326
+ },
327
+ annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
328
+ examples: [{ description: "Why EXIT outside a loop is flagged.", arguments: { rule: "exit_or_check" } }],
329
+ handler: (args) => {
330
+ const detail = explainRule(args.rule);
331
+ return {
332
+ content: [{ type: "text", text: `${detail.key}: ${detail.title}\n${detail.shortDescription}\n${detail.docsUrl}` }],
333
+ structuredContent: detail,
334
+ };
335
+ },
336
+ });
337
+ export const formatAbapTool = defineTool({
338
+ name: "format_abap",
339
+ title: "Format ABAP source",
340
+ description: "Pretty-print one ABAP source: normalize keyword casing and indentation using abaplint's formatter — the offline " +
341
+ "equivalent of Pretty Printer in ADT/SE80. " +
342
+ "Use this when generated or hand-written ABAP has inconsistent casing/indentation and you want it normalized " +
343
+ "before review or commit. It does not reformat CDS views or behavior definitions, does not change any logic, " +
344
+ "and fails cleanly on source it cannot parse. " +
345
+ 'Example: format_abap({ "source": "report ztest.\\nwrite \'hi\'." }).',
346
+ inputSchema: {
347
+ source: z.string().describe("The complete ABAP source to format."),
348
+ filename: z
349
+ .string()
350
+ .optional()
351
+ .describe('abapGit-style name if known, e.g. "zcl_x.clas.abap"; inferred from the source when omitted.'),
352
+ },
353
+ outputSchema: {
354
+ formatted: z.string().describe("The pretty-printed source."),
355
+ },
356
+ annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
357
+ examples: [
358
+ { description: "Uppercase keywords in a lowercase report.", arguments: { source: "report ztest.\nwrite 'hi'." } },
359
+ ],
360
+ handler: (args) => {
361
+ const formatted = formatAbap(args.source, args.filename);
362
+ return { content: [{ type: "text", text: formatted }], structuredContent: { formatted } };
363
+ },
364
+ });
365
+ export const getAbapOutline = defineTool({
366
+ name: "get_abap_outline",
367
+ title: "Get ABAP source outline",
368
+ description: "Return the structural outline of ABAP sources — classes (with methods, visibility, attributes, interfaces, " +
369
+ "inheritance), interfaces, and FORM routines — without you having to read the whole file. " +
370
+ "Use this when navigating a large class or legacy program to decide which part to read or edit next; it is the " +
371
+ "cheap first call before pulling thousands of lines into context. It does not return method bodies or analyze " +
372
+ "code quality (use lint_abap for that), and CDS/behavior-definition files yield an empty outline. " +
373
+ 'Example: get_abap_outline({ "files": [ { "filename": "zcl_big.clas.abap", "source": "CLASS zcl_big DEFINITION…" } ] }).',
374
+ inputSchema: {
375
+ files: filesField,
376
+ },
377
+ outputSchema: {
378
+ outlines: z.array(z.object({
379
+ file: z.string().describe("Filename."),
380
+ parseable: z.boolean().describe("False when the file is not an ABAP object (or unparseable)."),
381
+ classes: z.array(z.unknown()).describe("Class definitions with methods/visibility/attributes."),
382
+ interfaces: z.array(z.string()).describe("Interface names defined in the file."),
383
+ forms: z.array(z.string()).describe("Legacy FORM routine names."),
384
+ })),
385
+ },
386
+ annotations: { readOnlyHint: true, openWorldHint: false, idempotentHint: true },
387
+ examples: [
388
+ {
389
+ description: "Outline a class to find its methods before editing.",
390
+ arguments: {
391
+ files: [
392
+ {
393
+ filename: "zcl_demo.clas.abap",
394
+ source: "CLASS zcl_demo DEFINITION PUBLIC.\n PUBLIC SECTION.\n METHODS run.\nENDCLASS.\nCLASS zcl_demo IMPLEMENTATION.\n METHOD run.\n ENDMETHOD.\nENDCLASS.",
395
+ },
396
+ ],
397
+ },
398
+ },
399
+ ],
400
+ handler: (args) => {
401
+ const outlines = outlineAbap(args.files);
402
+ const text = outlines
403
+ .map((o) => {
404
+ if (!o.parseable)
405
+ return `${o.file}: (no ABAP outline)`;
406
+ const parts = [
407
+ ...o.classes.map((c) => `class ${c.name} (${c.methods.length} methods: ${c.methods.map((m) => m.name).join(", ")})`),
408
+ ...o.interfaces.map((i) => `interface ${i}`),
409
+ ...o.forms.map((f) => `form ${f}`),
410
+ ];
411
+ return `${o.file}: ${parts.join("; ") || "(empty)"}`;
412
+ })
413
+ .join("\n");
414
+ return {
415
+ content: [{ type: "text", text }],
416
+ structuredContent: { outlines: outlines },
417
+ };
418
+ },
419
+ });
420
+ /** Every tool this server exposes. (`tools` alias = the registry-export shape @mcp-kit/lint discovers.) */
421
+ export const ALL_TOOLS = [
422
+ lintAbap,
423
+ checkCloudReadinessTool,
424
+ scaffoldRapBoTool,
425
+ listAbapRules,
426
+ explainAbapRule,
427
+ formatAbapTool,
428
+ getAbapOutline,
429
+ ];
430
+ export const tools = ALL_TOOLS;
431
+ // Guard against accidental duplicate registration as tools get added.
432
+ const names = new Set(ALL_TOOLS.map((t) => t.name));
433
+ if (names.size !== ALL_TOOLS.length) {
434
+ throw invalidInput("Duplicate tool names in ALL_TOOLS.");
435
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
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).
5
+ */
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { buildServer, SERVER_NAME, SERVER_VERSION } from "./server.js";
8
+ async function main() {
9
+ const server = buildServer();
10
+ await server.connect(new StdioServerTransport());
11
+ console.error(`${SERVER_NAME} v${SERVER_VERSION} ready on stdio (offline; no SAP system required)`);
12
+ }
13
+ main().catch((err) => {
14
+ console.error("fatal:", err);
15
+ process.exit(1);
16
+ });
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Structured tool failure — the one way tools in this server fail.
3
+ *
4
+ * Pattern adapted from @mcp-kit/core (github.com/lumivarahq/mcp-kit, MIT):
5
+ * handlers throw `McpToolError` (or anything), the registration wrapper turns
6
+ * it into an MCP error result instead of crashing the request.
7
+ */
8
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
9
+ export type ToolErrorCode = "invalid_input" | "not_found" | "internal";
10
+ export declare class McpToolError extends Error {
11
+ readonly code: ToolErrorCode;
12
+ readonly details?: Record<string, unknown> | undefined;
13
+ constructor(code: ToolErrorCode, message: string, details?: Record<string, unknown> | undefined);
14
+ }
15
+ /** The caller passed arguments the tool cannot work with. */
16
+ export declare function invalidInput(message: string, details?: Record<string, unknown>): McpToolError;
17
+ /** The named thing (rule key, …) does not exist. */
18
+ export declare function notFound(message: string, details?: Record<string, unknown>): McpToolError;
19
+ /** Convert any thrown value into a structured MCP error result. */
20
+ export declare function errorResult(err: unknown): CallToolResult;
package/dist/errors.js ADDED
@@ -0,0 +1,29 @@
1
+ export class McpToolError extends Error {
2
+ code;
3
+ details;
4
+ constructor(code, message, details) {
5
+ super(message);
6
+ this.code = code;
7
+ this.details = details;
8
+ this.name = "McpToolError";
9
+ }
10
+ }
11
+ /** The caller passed arguments the tool cannot work with. */
12
+ export function invalidInput(message, details) {
13
+ return new McpToolError("invalid_input", message, details);
14
+ }
15
+ /** The named thing (rule key, …) does not exist. */
16
+ export function notFound(message, details) {
17
+ return new McpToolError("not_found", message, details);
18
+ }
19
+ /** Convert any thrown value into a structured MCP error result. */
20
+ export function errorResult(err) {
21
+ const e = err instanceof McpToolError
22
+ ? err
23
+ : new McpToolError("internal", err instanceof Error ? err.message : String(err));
24
+ return {
25
+ isError: true,
26
+ content: [{ type: "text", text: `${e.code}: ${e.message}` }],
27
+ structuredContent: { error: { code: e.code, message: e.message, details: e.details ?? null } },
28
+ };
29
+ }
@@ -0,0 +1,15 @@
1
+ /** Library surface — embed the tools or the server in your own process. */
2
+ export { buildServer, SERVER_NAME, SERVER_VERSION } from "./server.js";
3
+ export { ALL_TOOLS } from "./abap.tools.js";
4
+ export { runAbaplint, inferFilename, ABAP_VERSIONS } from "./abap/engine.js";
5
+ export type { AbapSource, AbapVersion, Finding } from "./abap/engine.js";
6
+ export { checkCloudReadiness } from "./abap/readiness.js";
7
+ export type { ReadinessReport } from "./abap/readiness.js";
8
+ export { scaffoldRapBo, snakeToCamel } from "./abap/scaffold.js";
9
+ export type { ScaffoldOptions, ScaffoldResult } from "./abap/scaffold.js";
10
+ export { listRules, explainRule } from "./abap/rules.js";
11
+ export { formatAbap } from "./abap/formatter.js";
12
+ export { outlineAbap } from "./abap/outline.js";
13
+ export { defineTool, registerTool, registerTools } from "./tool.js";
14
+ export type { ToolSpec, AnyToolSpec, ToolExample } from "./tool.js";
15
+ export { McpToolError, invalidInput, notFound } from "./errors.js";
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /** Library surface — embed the tools or the server in your own process. */
2
+ export { buildServer, SERVER_NAME, SERVER_VERSION } from "./server.js";
3
+ export { ALL_TOOLS } from "./abap.tools.js";
4
+ export { runAbaplint, inferFilename, ABAP_VERSIONS } from "./abap/engine.js";
5
+ export { checkCloudReadiness } from "./abap/readiness.js";
6
+ export { scaffoldRapBo, snakeToCamel } from "./abap/scaffold.js";
7
+ export { listRules, explainRule } from "./abap/rules.js";
8
+ export { formatAbap } from "./abap/formatter.js";
9
+ export { outlineAbap } from "./abap/outline.js";
10
+ export { defineTool, registerTool, registerTools } from "./tool.js";
11
+ export { McpToolError, invalidInput, notFound } from "./errors.js";
@@ -0,0 +1,5 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare const SERVER_NAME = "abap-mcp";
3
+ export declare const SERVER_VERSION = "0.1.0";
4
+ /** Build a fully-wired MCP server instance (one per transport connection). */
5
+ export declare function buildServer(): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,11 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ALL_TOOLS } from "./abap.tools.js";
3
+ import { registerTools } from "./tool.js";
4
+ export const SERVER_NAME = "abap-mcp";
5
+ export const SERVER_VERSION = "0.1.0";
6
+ /** Build a fully-wired MCP server instance (one per transport connection). */
7
+ export function buildServer() {
8
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
9
+ registerTools(server, ALL_TOOLS);
10
+ return server;
11
+ }
package/dist/tool.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Typed tool helper: one `ToolSpec` object is the single source of truth for a
3
+ * tool's name, description, schemas, annotations and worked examples — the
4
+ * same object that registers the tool is the one a description lint can grade,
5
+ * so model-facing docs can't drift from what's enforced.
6
+ *
7
+ * Pattern adapted from @mcp-kit/core (github.com/lumivarahq/mcp-kit, MIT).
8
+ */
9
+ import type { McpServer, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+ import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
11
+ import type { ZodRawShape } from "zod";
12
+ export interface ToolExample {
13
+ /** One line on why you would make this call. */
14
+ description: string;
15
+ /** Concrete arguments, matching the tool's input schema. */
16
+ arguments: Record<string, unknown>;
17
+ }
18
+ export interface ToolSpec<InputShape extends ZodRawShape = ZodRawShape> {
19
+ /** Verb-first, snake_case, unique within the server. */
20
+ name: string;
21
+ title?: string;
22
+ /** What it operates on, "Use this when …", and what it does NOT handle. */
23
+ description: string;
24
+ /** Zod raw shape; every field `.describe(...)`d. */
25
+ inputSchema: InputShape;
26
+ outputSchema?: ZodRawShape;
27
+ annotations?: ToolAnnotations;
28
+ /** At least one worked example — examples are documentation. */
29
+ examples?: ToolExample[];
30
+ handler: ToolCallback<InputShape>;
31
+ }
32
+ export type AnyToolSpec = ToolSpec<any>;
33
+ /** Identity helper that pins the generic so handler args are typed. */
34
+ export declare function defineTool<InputShape extends ZodRawShape>(spec: ToolSpec<InputShape>): ToolSpec<InputShape>;
35
+ export declare function registerTool(server: McpServer, spec: AnyToolSpec): void;
36
+ export declare function registerTools(server: McpServer, specs: readonly AnyToolSpec[]): void;
package/dist/tool.js ADDED
@@ -0,0 +1,33 @@
1
+ import { errorResult } from "./errors.js";
2
+ /** Identity helper that pins the generic so handler args are typed. */
3
+ export function defineTool(spec) {
4
+ return spec;
5
+ }
6
+ function wrapHandler(handler) {
7
+ const wrapped = async (...args) => {
8
+ try {
9
+ return await handler(...args);
10
+ }
11
+ catch (err) {
12
+ return errorResult(err);
13
+ }
14
+ };
15
+ return wrapped;
16
+ }
17
+ export function registerTool(server, spec) {
18
+ const config = {
19
+ description: spec.description,
20
+ inputSchema: spec.inputSchema,
21
+ };
22
+ if (spec.title !== undefined)
23
+ config.title = spec.title;
24
+ if (spec.outputSchema !== undefined)
25
+ config.outputSchema = spec.outputSchema;
26
+ if (spec.annotations !== undefined)
27
+ config.annotations = spec.annotations;
28
+ server.registerTool(spec.name, config, wrapHandler(spec.handler));
29
+ }
30
+ export function registerTools(server, specs) {
31
+ for (const spec of specs)
32
+ registerTool(server, spec);
33
+ }