sap-adt-mcp 0.8.41 → 0.8.43

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.41",
3
+ "version": "0.8.43",
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
  }
@@ -25,7 +25,10 @@ const TYPE_ALIASES = {
25
25
  fugr: "FUGR",
26
26
  table: "TABL",
27
27
  tabl: "TABL",
28
- structure: "TABL",
28
+ // Structures live at their own ADT endpoint (/ddic/structures), NOT
29
+ // /ddic/tables — routing them to TABL sent reads/locks to the wrong URI.
30
+ structure: "STRU",
31
+ stru: "STRU",
29
32
  dataelement: "DTEL",
30
33
  dtel: "DTEL",
31
34
  domain: "DOMA",
@@ -105,6 +108,8 @@ export function objectUri({ type, name, group }) {
105
108
  }
106
109
  case "TABL":
107
110
  return `/sap/bc/adt/ddic/tables/${enc(n)}`;
111
+ case "STRU":
112
+ return `/sap/bc/adt/ddic/structures/${enc(n)}`;
108
113
  case "DTEL":
109
114
  return `/sap/bc/adt/ddic/dataelements/${enc(n)}`;
110
115
  case "DOMA":
@@ -160,7 +165,7 @@ export function sourceUri({ type, name, group, include }) {
160
165
  // via the object URI directly, returned as XML. CDS / DCLS / DDLX / BDEF do
161
166
  // use /source/main.
162
167
  if (t === "DTEL" || t === "DOMA" || t === "MSAG") return base;
163
- if (t === "TABL") return `${base}/source/main`;
168
+ if (t === "TABL" || t === "STRU") return `${base}/source/main`;
164
169
 
165
170
  return `${base}/source/main`;
166
171
  }
@@ -1,5 +1,5 @@
1
1
  export const OBJECT_TYPE_HINT =
2
- "Object type — friendly alias (program, class, interface, function, functiongroup, include, table, dataelement, domain, cds) or TADIR code (PROG, CLAS, INTF, FUGR, FUGR/FF, INCL, TABL, DTEL, DOMA, DDLS).";
2
+ "Object type — friendly alias (program, class, interface, function, functiongroup, include, table, structure, dataelement, domain, cds) or TADIR code (PROG, CLAS, INTF, FUGR, FUGR/FF, INCL, TABL, DTEL, DOMA, DDLS).";
3
3
 
4
4
  export const SYSTEM_HINT = "System name. Omit for default.";
5
5
 
@@ -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
  }