sap-adt-mcp 0.8.42 → 0.8.44
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/package.json +1 -1
- package/src/object-create.js +42 -0
- package/src/object-uris.js +15 -2
- package/src/tools/cds.js +22 -6
- package/src/tools/discovery.js +3 -2
- package/src/tools/lifecycle.js +4 -6
- package/src/tools/rap.js +5 -7
- package/src/tools/source.js +20 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sap-adt-mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.44",
|
|
4
4
|
"mcpName": "io.github.yzonur/sap-adt-mcp",
|
|
5
5
|
"description": "MCP server giving Claude live access to SAP systems via ADT (ABAP Development Tools) REST. Read source, search, run syntax checks, and edit ABAP objects from any MCP-compatible client.",
|
|
6
6
|
"type": "module",
|
package/src/object-create.js
CHANGED
|
@@ -201,6 +201,48 @@ export function buildCreateRequest({
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
// Most ADT create endpoints version their media type (…v2+xml, …v3+xml). The
|
|
205
|
+
// shapes above target the newest version a modern stack accepts, but older
|
|
206
|
+
// systems only know an earlier one and answer the newer media type with
|
|
207
|
+
// 415 ExceptionUnsupportedMediaType. Given a content-type, return the chain to
|
|
208
|
+
// try: the requested version first, then every lower version down to v1.
|
|
209
|
+
// A content-type with no `.vN+xml` segment is returned unchanged (single item).
|
|
210
|
+
export function mediaTypeFallbacks(contentType) {
|
|
211
|
+
const m = /^(.*\.)v(\d+)(\+xml)$/.exec(contentType);
|
|
212
|
+
if (!m) return [contentType];
|
|
213
|
+
const [, prefix, ver, suffix] = m;
|
|
214
|
+
const chain = [];
|
|
215
|
+
for (let v = Number(ver); v >= 1; v--) {
|
|
216
|
+
chain.push(`${prefix}v${v}${suffix}`);
|
|
217
|
+
}
|
|
218
|
+
return chain;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// POST a create request, retrying with successively lower media-type versions
|
|
222
|
+
// when the server rejects the content-type with 415. Returns
|
|
223
|
+
// { res, text, contentType } for the first attempt that is not a 415 — or the
|
|
224
|
+
// last attempt if every version is rejected. The body is read once here so
|
|
225
|
+
// callers must use the returned `text` rather than calling res.text() again.
|
|
226
|
+
export async function postCreate(client, { path, contentType, body, query }) {
|
|
227
|
+
const types = mediaTypeFallbacks(contentType);
|
|
228
|
+
let res;
|
|
229
|
+
let text;
|
|
230
|
+
let used;
|
|
231
|
+
for (let i = 0; i < types.length; i++) {
|
|
232
|
+
used = types[i];
|
|
233
|
+
res = await client.request({
|
|
234
|
+
method: "POST",
|
|
235
|
+
path,
|
|
236
|
+
query,
|
|
237
|
+
headers: { "Content-Type": used },
|
|
238
|
+
body,
|
|
239
|
+
});
|
|
240
|
+
text = await res.text();
|
|
241
|
+
if (res.status !== 415 || i === types.length - 1) break;
|
|
242
|
+
}
|
|
243
|
+
return { res, text, contentType: used };
|
|
244
|
+
}
|
|
245
|
+
|
|
204
246
|
function xmlDecl() {
|
|
205
247
|
return `<?xml version="1.0" encoding="UTF-8"?>`;
|
|
206
248
|
}
|
package/src/object-uris.js
CHANGED
|
@@ -73,8 +73,21 @@ export function normalizeType(input) {
|
|
|
73
73
|
return aliased ?? upper;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// ADT types arrive either as a bare key (CLAS, SRVD) or in TADIR-style
|
|
77
|
+
// TYPE/SUBTYPE form (SRVD/SRV, CLAS/OC, DDLS/DF). The subtype only changes the
|
|
78
|
+
// endpoint for function-group members (FUGR/FF module, FUGR/I include); for
|
|
79
|
+
// every other kind it is decoration, so collapse X/Y to its base type X. Without
|
|
80
|
+
// this, a TYPE/SUBTYPE input misses the dispatch table and throws "Unsupported
|
|
81
|
+
// object type".
|
|
82
|
+
function collapseSubtype(t) {
|
|
83
|
+
if (t.includes("/") && t !== "FUGR/FF" && t !== "FUGR/I") {
|
|
84
|
+
return t.split("/")[0];
|
|
85
|
+
}
|
|
86
|
+
return t;
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
export function objectUri({ type, name, group }) {
|
|
77
|
-
const t = normalizeType(type);
|
|
90
|
+
const t = collapseSubtype(normalizeType(type));
|
|
78
91
|
if (typeof name !== "string" || name.length === 0) {
|
|
79
92
|
throw new Error("Object name is required");
|
|
80
93
|
}
|
|
@@ -145,7 +158,7 @@ export const METADATA_XML_ACCEPT = {
|
|
|
145
158
|
};
|
|
146
159
|
|
|
147
160
|
export function sourceUri({ type, name, group, include }) {
|
|
148
|
-
const t = normalizeType(type);
|
|
161
|
+
const t = collapseSubtype(normalizeType(type));
|
|
149
162
|
const base = objectUri({ type: t, name, group });
|
|
150
163
|
|
|
151
164
|
if (t === "CLAS") {
|
package/src/tools/cds.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { objectUri } from "../object-uris.js";
|
|
2
2
|
import { parseDataPreview } from "../data-preview.js";
|
|
3
3
|
import { parseObjectReferences } from "../object-references.js";
|
|
4
|
-
import { errorResult, jsonResult } from "../result.js";
|
|
4
|
+
import { errorResult, jsonResult, textResult } from "../result.js";
|
|
5
5
|
import { SYSTEM_HINT } from "./_shared.js";
|
|
6
6
|
|
|
7
7
|
const CDS_PREVIEW_PATH = "/sap/bc/adt/datapreview/cds";
|
|
@@ -86,12 +86,21 @@ export const tools = [
|
|
|
86
86
|
export function register({ getClient }) {
|
|
87
87
|
return {
|
|
88
88
|
adt_cds_data_preview: async (args) => {
|
|
89
|
+
// `entity` is the documented field, but callers reach for `name`/`cdsName`;
|
|
90
|
+
// accept those too and fail cleanly rather than throwing on `.toUpperCase`.
|
|
91
|
+
const entity = args.entity ?? args.name ?? args.cdsName;
|
|
92
|
+
if (typeof entity !== "string" || entity.length === 0) {
|
|
93
|
+
return textResult(
|
|
94
|
+
"adt_cds_data_preview: `entity` is required (the CDS view/entity name, e.g. 'I_CompanyCode').",
|
|
95
|
+
true
|
|
96
|
+
);
|
|
97
|
+
}
|
|
89
98
|
const { client, name: sys } = getClient(args.system);
|
|
90
99
|
const max = Math.min(args.maxRows ?? DEFAULT_MAX_ROWS, HARD_CAP_ROWS);
|
|
91
100
|
const res = await client.request({
|
|
92
101
|
method: "POST",
|
|
93
102
|
path: CDS_PREVIEW_PATH,
|
|
94
|
-
query: { ddlSourceName:
|
|
103
|
+
query: { ddlSourceName: entity.toUpperCase(), rowNumber: String(max) },
|
|
95
104
|
accept: "application/xml",
|
|
96
105
|
});
|
|
97
106
|
const text = await res.text();
|
|
@@ -100,12 +109,12 @@ export function register({ getClient }) {
|
|
|
100
109
|
try {
|
|
101
110
|
parsed = parseDataPreview(text);
|
|
102
111
|
} catch (err) {
|
|
103
|
-
return jsonResult({ system: sys, entity
|
|
112
|
+
return jsonResult({ system: sys, entity, parseError: err.message, raw: text.slice(0, 8000) });
|
|
104
113
|
}
|
|
105
114
|
const rowCount = parsed.rows.length;
|
|
106
115
|
return jsonResult({
|
|
107
116
|
system: sys,
|
|
108
|
-
entity:
|
|
117
|
+
entity: entity.toUpperCase(),
|
|
109
118
|
rowCount,
|
|
110
119
|
truncated: rowCount >= max,
|
|
111
120
|
totalRows: parsed.totalRows,
|
|
@@ -116,8 +125,15 @@ export function register({ getClient }) {
|
|
|
116
125
|
},
|
|
117
126
|
|
|
118
127
|
adt_cds_dependencies: async (args) => {
|
|
128
|
+
const entity = args.entity ?? args.name ?? args.cdsName;
|
|
129
|
+
if (typeof entity !== "string" || entity.length === 0) {
|
|
130
|
+
return textResult(
|
|
131
|
+
"adt_cds_dependencies: `entity` is required (the CDS view/entity name, e.g. 'I_CompanyCode').",
|
|
132
|
+
true
|
|
133
|
+
);
|
|
134
|
+
}
|
|
119
135
|
const { client, name: sys } = getClient(args.system);
|
|
120
|
-
const uri = objectUri({ type: "ddls", name:
|
|
136
|
+
const uri = objectUri({ type: "ddls", name: entity });
|
|
121
137
|
const res = await client.request({ path: GRAPHDATA_PATH, query: { uri }, accept: "application/xml" });
|
|
122
138
|
const text = await res.text();
|
|
123
139
|
if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"), { stage: "graphdata" });
|
|
@@ -131,7 +147,7 @@ export function register({ getClient }) {
|
|
|
131
147
|
}
|
|
132
148
|
return jsonResult({
|
|
133
149
|
system: sys,
|
|
134
|
-
entity:
|
|
150
|
+
entity: entity.toUpperCase(),
|
|
135
151
|
uri,
|
|
136
152
|
dependencyCount: refs.length,
|
|
137
153
|
dependencies: refs,
|
package/src/tools/discovery.js
CHANGED
|
@@ -315,8 +315,9 @@ export function register({ getClient }) {
|
|
|
315
315
|
|
|
316
316
|
adt_list_packages: async (args) => {
|
|
317
317
|
// `package` is the field name on the sibling adt_browse_package tool, so
|
|
318
|
-
// callers reach for it here too — accept it
|
|
319
|
-
|
|
318
|
+
// callers reach for it here too — accept it (and a bare `name`) as an
|
|
319
|
+
// alias for `root`.
|
|
320
|
+
const rootArg = args.root ?? args.package ?? args.name;
|
|
320
321
|
if (typeof rootArg !== "string" || rootArg.length === 0) {
|
|
321
322
|
return textResult(
|
|
322
323
|
"adt_list_packages: `root` is required (the root package to walk from, e.g. 'ZFLEET').",
|
package/src/tools/lifecycle.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { objectUri, normalizeType } from "../object-uris.js";
|
|
2
2
|
import { escapeXml } from "../xml.js";
|
|
3
3
|
import { acquireLock, releaseLock } from "../lock.js";
|
|
4
|
-
import { buildCreateRequest } from "../object-create.js";
|
|
4
|
+
import { buildCreateRequest, postCreate } from "../object-create.js";
|
|
5
5
|
import { errorResult, jsonResult, textResult } from "../result.js";
|
|
6
6
|
import { OBJECT_TYPE_HINT, SYSTEM_HINT } from "./_shared.js";
|
|
7
7
|
|
|
@@ -199,14 +199,12 @@ export function register({ getClient }) {
|
|
|
199
199
|
}
|
|
200
200
|
const query = {};
|
|
201
201
|
if (args.transport) query.corrNr = args.transport;
|
|
202
|
-
const res = await client
|
|
203
|
-
method: "POST",
|
|
202
|
+
const { res, text } = await postCreate(client, {
|
|
204
203
|
path: req.path,
|
|
205
|
-
|
|
206
|
-
headers: { "Content-Type": req.contentType },
|
|
204
|
+
contentType: req.contentType,
|
|
207
205
|
body: req.body,
|
|
206
|
+
query,
|
|
208
207
|
});
|
|
209
|
-
const text = await res.text();
|
|
210
208
|
if (!res.ok) return errorResult(sys, res.status, text, res.headers.get("content-type"));
|
|
211
209
|
const newUri = objectUri({
|
|
212
210
|
type: args.type,
|
package/src/tools/rap.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildCreateRequest } from "../object-create.js";
|
|
1
|
+
import { buildCreateRequest, postCreate } from "../object-create.js";
|
|
2
2
|
import { sourceUri } from "../object-uris.js";
|
|
3
3
|
import { acquireLock, releaseLock } from "../lock.js";
|
|
4
4
|
import { escapeXml } from "../xml.js";
|
|
@@ -159,15 +159,13 @@ function createInfoFor(artifact, spec) {
|
|
|
159
159
|
|
|
160
160
|
async function createAndWrite(client, artifact, spec) {
|
|
161
161
|
const info = createInfoFor(artifact, spec);
|
|
162
|
-
// 1) create the object
|
|
163
|
-
const createRes = await client
|
|
164
|
-
method: "POST",
|
|
162
|
+
// 1) create the object (retrying with lower media-type versions on 415)
|
|
163
|
+
const { res: createRes, text: createText } = await postCreate(client, {
|
|
165
164
|
path: info.path,
|
|
166
|
-
|
|
167
|
-
query: spec.transport ? { corrNr: spec.transport } : undefined,
|
|
165
|
+
contentType: info.contentType,
|
|
168
166
|
body: info.body,
|
|
167
|
+
query: spec.transport ? { corrNr: spec.transport } : undefined,
|
|
169
168
|
});
|
|
170
|
-
const createText = await createRes.text();
|
|
171
169
|
if (!createRes.ok) {
|
|
172
170
|
return { name: artifact.name, stage: "create", status: createRes.status, error: createText.slice(0, 400) };
|
|
173
171
|
}
|
package/src/tools/source.js
CHANGED
|
@@ -286,13 +286,26 @@ export function register({ getClient }) {
|
|
|
286
286
|
return textResult("adt_get_source: `type` is required (e.g. 'class', 'program', 'dataelement').", true);
|
|
287
287
|
}
|
|
288
288
|
const { client, name: sys } = getClient(args.system);
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
289
|
+
let t;
|
|
290
|
+
let path;
|
|
291
|
+
try {
|
|
292
|
+
t = normalizeType(args.type);
|
|
293
|
+
path = sourceUri({
|
|
294
|
+
type: args.type,
|
|
295
|
+
name: args.object,
|
|
296
|
+
group: args.group,
|
|
297
|
+
include: args.include,
|
|
298
|
+
});
|
|
299
|
+
} catch (err) {
|
|
300
|
+
// Unknown/unsupported object types (e.g. WAPA) reach the dispatch
|
|
301
|
+
// tables and throw. Return that as a clean tool error with an escape
|
|
302
|
+
// hatch hint instead of letting it surface as a crash.
|
|
303
|
+
return textResult(
|
|
304
|
+
`adt_get_source: ${err.message}. If this type has no high-level mapping yet, ` +
|
|
305
|
+
`fetch it with adt_request against its ADT source URI.`,
|
|
306
|
+
true
|
|
307
|
+
);
|
|
308
|
+
}
|
|
296
309
|
|
|
297
310
|
// DDIC primitives (data element / domain / message class) have no
|
|
298
311
|
// plain-text source — fetch their XML metadata with the right media type
|