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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-adt-mcp",
3
- "version": "0.8.42",
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",
@@ -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
  }
@@ -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: args.entity.toUpperCase(), rowNumber: String(max) },
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: args.entity, parseError: err.message, raw: text.slice(0, 8000) });
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: args.entity.toUpperCase(),
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: args.entity });
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: args.entity.toUpperCase(),
150
+ entity: entity.toUpperCase(),
135
151
  uri,
136
152
  dependencyCount: refs.length,
137
153
  dependencies: refs,
@@ -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 as an alias for `root`.
319
- const rootArg = args.root ?? args.package;
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').",
@@ -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.request({
203
- method: "POST",
202
+ const { res, text } = await postCreate(client, {
204
203
  path: req.path,
205
- query,
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.request({
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
- headers: { "Content-Type": info.contentType },
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
  }
@@ -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
- const t = normalizeType(args.type);
290
- const path = sourceUri({
291
- type: args.type,
292
- name: args.object,
293
- group: args.group,
294
- include: args.include,
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