arc-1 0.9.5 → 0.9.6

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.
Files changed (63) hide show
  1. package/README.md +19 -1
  2. package/dist/adt/client.d.ts +38 -7
  3. package/dist/adt/client.d.ts.map +1 -1
  4. package/dist/adt/client.js +100 -9
  5. package/dist/adt/client.js.map +1 -1
  6. package/dist/adt/config.d.ts +7 -1
  7. package/dist/adt/config.d.ts.map +1 -1
  8. package/dist/adt/config.js.map +1 -1
  9. package/dist/adt/features.d.ts.map +1 -1
  10. package/dist/adt/features.js +27 -3
  11. package/dist/adt/features.js.map +1 -1
  12. package/dist/adt/http.d.ts +23 -0
  13. package/dist/adt/http.d.ts.map +1 -1
  14. package/dist/adt/http.js +82 -2
  15. package/dist/adt/http.js.map +1 -1
  16. package/dist/adt/xml-parser.d.ts +22 -0
  17. package/dist/adt/xml-parser.d.ts.map +1 -1
  18. package/dist/adt/xml-parser.js +32 -0
  19. package/dist/adt/xml-parser.js.map +1 -1
  20. package/dist/handlers/intent.d.ts +2 -1
  21. package/dist/handlers/intent.d.ts.map +1 -1
  22. package/dist/handlers/intent.js +184 -26
  23. package/dist/handlers/intent.js.map +1 -1
  24. package/dist/handlers/schemas.d.ts +10 -2
  25. package/dist/handlers/schemas.d.ts.map +1 -1
  26. package/dist/handlers/schemas.js +5 -0
  27. package/dist/handlers/schemas.js.map +1 -1
  28. package/dist/handlers/tools.d.ts.map +1 -1
  29. package/dist/handlers/tools.js +5 -0
  30. package/dist/handlers/tools.js.map +1 -1
  31. package/dist/lint/lint.d.ts.map +1 -1
  32. package/dist/lint/lint.js +6 -0
  33. package/dist/lint/lint.js.map +1 -1
  34. package/dist/lint/pre-write-hints.d.ts +45 -0
  35. package/dist/lint/pre-write-hints.d.ts.map +1 -0
  36. package/dist/lint/pre-write-hints.js +145 -0
  37. package/dist/lint/pre-write-hints.js.map +1 -0
  38. package/dist/server/audit.d.ts +27 -1
  39. package/dist/server/audit.d.ts.map +1 -1
  40. package/dist/server/audit.js.map +1 -1
  41. package/dist/server/auth-rate-limit.d.ts +78 -0
  42. package/dist/server/auth-rate-limit.d.ts.map +1 -0
  43. package/dist/server/auth-rate-limit.js +95 -0
  44. package/dist/server/auth-rate-limit.js.map +1 -0
  45. package/dist/server/config.d.ts.map +1 -1
  46. package/dist/server/config.js +32 -0
  47. package/dist/server/config.js.map +1 -1
  48. package/dist/server/http.d.ts.map +1 -1
  49. package/dist/server/http.js +73 -2
  50. package/dist/server/http.js.map +1 -1
  51. package/dist/server/mcp-rate-limit.d.ts +69 -0
  52. package/dist/server/mcp-rate-limit.d.ts.map +1 -0
  53. package/dist/server/mcp-rate-limit.js +92 -0
  54. package/dist/server/mcp-rate-limit.js.map +1 -0
  55. package/dist/server/server.d.ts +7 -5
  56. package/dist/server/server.d.ts.map +1 -1
  57. package/dist/server/server.js +43 -18
  58. package/dist/server/server.js.map +1 -1
  59. package/dist/server/types.d.ts +20 -1
  60. package/dist/server/types.d.ts.map +1 -1
  61. package/dist/server/types.js +2 -0
  62. package/dist/server/types.js.map +1 -1
  63. package/package.json +14 -12
@@ -37,6 +37,7 @@ import { detectFilename, lintAbapSource, lintAndFix, validateBeforeWrite } from
37
37
  import { sanitizeArgs } from '../server/audit.js';
38
38
  import { generateRequestId, requestContext } from '../server/context.js';
39
39
  import { logger } from '../server/logger.js';
40
+ import { resolveRateLimitUserKey } from '../server/mcp-rate-limit.js';
40
41
  import { expandHyperfocusedArgs } from './hyperfocused.js';
41
42
  import { getToolSchema } from './schemas.js';
42
43
  import { formatZodError } from './zod-errors.js';
@@ -225,7 +226,7 @@ function buildBaseErrorMessage(err, message, tool, args, config) {
225
226
  if (err instanceof AdtApiError) {
226
227
  // Append additional SAP messages (line numbers, secondary errors) if available
227
228
  const enriched = enrichWithSapDetails(err, message);
228
- const argType = String(args.type ?? '').toUpperCase();
229
+ const argType = canonicalTablType(String(args.type ?? '').toUpperCase());
229
230
  const classification = classifySapDomainError(err.statusCode, err.responseBody, err.path);
230
231
  if (classification) {
231
232
  const transactionLine = classification.transaction ? `\nSAP Transaction: ${classification.transaction}` : '';
@@ -295,7 +296,7 @@ function buildBaseErrorMessage(err, message, tool, args, config) {
295
296
  return enriched;
296
297
  }
297
298
  if (err instanceof AdtSafetyError) {
298
- const argType = String(args.type ?? '').toUpperCase();
299
+ const argType = canonicalTablType(String(args.type ?? '').toUpperCase());
299
300
  if (tool === 'SAPRead' && argType === 'TABLE_CONTENTS') {
300
301
  return (`${message}\n\nHint: TABLE_CONTENTS is blocked by safety configuration or missing data scope. ` +
301
302
  'Set SAP_ALLOW_DATA_PREVIEW=true at the server level and, in authenticated HTTP mode, ' +
@@ -658,7 +659,7 @@ async function inactiveSyntaxDiagnostic(client, type, name) {
658
659
  }
659
660
  }
660
661
  async function tryPostSaveSyntaxCheck(client, type, name) {
661
- if (!DDIC_POST_SAVE_CHECK_TYPES.has(type.toUpperCase()))
662
+ if (!DDIC_POST_SAVE_CHECK_TYPES.has(canonicalTablType(type.toUpperCase())))
662
663
  return '';
663
664
  return inactiveSyntaxDiagnostic(client, type, name);
664
665
  }
@@ -746,7 +747,7 @@ function classifyError(err) {
746
747
  * all tools are allowed (backward compatibility).
747
748
  * @param server - MCP Server instance for elicitation support.
748
749
  */
749
- export async function handleToolCall(client, config, toolName, args, authInfo, _server, cachingLayer, isPerUserClient) {
750
+ export async function handleToolCall(client, config, toolName, args, authInfo, _server, cachingLayer, isPerUserClient, mcpRateLimiter) {
750
751
  const reqId = generateRequestId();
751
752
  const start = Date.now();
752
753
  // Build user context for audit logging
@@ -763,6 +764,47 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
763
764
  tool: toolName,
764
765
  args: sanitizeArgs(args),
765
766
  });
767
+ // ─── Layer 2: per-user MCP tool-call rate limit ─────────────────────
768
+ // Applied immediately so we don't waste any work on denied calls. Stdio mode
769
+ // (no authInfo) is exempt — there's no user identity to key on. On denial we
770
+ // return an MCP tool error (not HTTP 429) so the LLM client surfaces it as a
771
+ // tool failure and the agent loop backs off via its own retry policy.
772
+ // See docs_page/rate-limiting.md (Layer 2). Cost weighting per tool is deferred
773
+ // to v2 — every consume call counts as one point.
774
+ if (mcpRateLimiter && authInfo) {
775
+ // Walks the most-specific identity claim first (userName → email → sub →
776
+ // preferred_username → clientId) so OIDC users sharing one `azp` clientId
777
+ // don't collapse into a single bucket. See resolveRateLimitUserKey.
778
+ const userKey = resolveRateLimitUserKey(authInfo);
779
+ const decision = await mcpRateLimiter.consume(userKey, toolName);
780
+ if (!decision.allowed) {
781
+ const retryAfter = Math.ceil(decision.retryAfterMs / 1000);
782
+ logger.emitAudit({
783
+ timestamp: new Date().toISOString(),
784
+ level: 'warn',
785
+ event: 'mcp_rate_limited',
786
+ requestId: reqId,
787
+ clientId,
788
+ user: userKey,
789
+ tool: toolName,
790
+ limitPerMinute: decision.limitPerMinute,
791
+ retryAfterMs: decision.retryAfterMs,
792
+ });
793
+ return {
794
+ content: [
795
+ {
796
+ type: 'text',
797
+ text: JSON.stringify({
798
+ error: 'rate_limited',
799
+ retryAfter,
800
+ message: `Rate limit exceeded (${decision.limitPerMinute}/min per user). Retry after ${retryAfter} seconds.`,
801
+ }),
802
+ },
803
+ ],
804
+ isError: true,
805
+ };
806
+ }
807
+ }
766
808
  // Unified scope enforcement via ACTION_POLICY — routes through action/type-aware lookup.
767
809
  // For SAPRead, the policy key is Tool.{type}; for other action-bearing tools, Tool.{action};
768
810
  // for tools without an action/type enum (SAPSearch, SAPQuery), the tool-level default applies.
@@ -945,6 +987,28 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
945
987
  function isBtpSystem() {
946
988
  return cachedFeatures?.systemType === 'btp';
947
989
  }
990
+ /** Return whether the SAP ADT discovery feed advertises the /sap/bc/adt/ddic/tables
991
+ * collection (the transparent-table editor endpoint). Absent on NW 7.50/7.51 —
992
+ * SAP added it in NW 7.52 along with the new database-table editor. When the
993
+ * discovery cache is empty (e.g. probe never ran, tests that bypass SAPManage),
994
+ * returns `undefined` so callers can decide whether to default-allow.
995
+ * See issue #285. */
996
+ function isTablesEndpointAvailable() {
997
+ const map = cachedFeatures?.discoveryMap ?? cachedDiscovery;
998
+ if (!map || map.size === 0)
999
+ return undefined;
1000
+ return map.has('/sap/bc/adt/ddic/tables');
1001
+ }
1002
+ /** Stable hint surfaced when ARC-1 refuses a TABL/DT write because the connected
1003
+ * system does not expose /sap/bc/adt/ddic/tables/. Shared between the
1004
+ * resolver-driven update/delete/activate paths and the discovery-gated create
1005
+ * paths so the LLM always sees the same recovery instructions. */
1006
+ const TABL_DT_WRITE_UNAVAILABLE_HINT = 'Transparent table writes via ADT REST are not available on this system ' +
1007
+ '(/sap/bc/adt/ddic/tables/ is not exposed — NW 7.50/7.51 ship the DDIC ' +
1008
+ 'structures endpoint only; the table editor was added in NW 7.52). ' +
1009
+ 'Use SE11 in SAPGUI, or connect ARC-1 to an SAP_BASIS ≥ 7.52 system. ' +
1010
+ 'Writing the source via /sap/bc/adt/ddic/structures/ would silently flip ' +
1011
+ 'DD02L-TABCLASS to INTTAB and corrupt the table.';
948
1012
  /** BTP-specific error messages for unavailable operations */
949
1013
  const BTP_HINTS = {
950
1014
  PROG: 'Executable programs (reports) are not available on BTP ABAP Environment. Use CLAS with IF_OO_ADT_CLASSRUN for console applications.',
@@ -2253,18 +2317,24 @@ export function buildCreateXml(type, name, pkg, description, properties) {
2253
2317
  <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
2254
2318
  </dcl:dclSource>`;
2255
2319
  case 'TABL':
2256
- // TABL creation also uses SAP's "blue" framework envelope, then source is written via /source/main.
2320
+ case 'TABL/DT':
2321
+ case 'TABL/DS': {
2322
+ // Bare TABL is the legacy alias for TABL/DT (transparent table). The same
2323
+ // <blue:blueSource> envelope works for both subtypes — only adtcore:type
2324
+ // and the POST URL differ. See docs/plans/completed/fix-tabl-ds-create-routing.md.
2325
+ const adtType = type === 'TABL/DS' ? 'TABL/DS' : 'TABL/DT';
2257
2326
  return `<?xml version="1.0" encoding="UTF-8"?>
2258
2327
  <blue:blueSource xmlns:blue="http://www.sap.com/wbobj/blue"
2259
2328
  xmlns:adtcore="http://www.sap.com/adt/core"
2260
2329
  adtcore:description="${escapeXml(description)}"
2261
2330
  adtcore:name="${escapeXml(name)}"
2262
- adtcore:type="TABL/DT"
2331
+ adtcore:type="${adtType}"
2263
2332
  adtcore:masterLanguage="EN"
2264
2333
  adtcore:masterSystem="H00"
2265
2334
  adtcore:responsible="DEVELOPER">
2266
2335
  <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
2267
2336
  </blue:blueSource>`;
2337
+ }
2268
2338
  case 'BDEF':
2269
2339
  // BDEF uses SAP's "blue" framework — blue:blueSource with http://www.sap.com/wbobj/blue namespace.
2270
2340
  // Confirmed by vibing-steampunk (Go) and fr0ster (TypeScript) reference implementations.
@@ -2569,6 +2639,37 @@ export function normalizeObjectType(type) {
2569
2639
  return '';
2570
2640
  return SLASH_TYPE_MAP[normalized] ?? normalized;
2571
2641
  }
2642
+ /** TABL subtypes that SAPWrite preserves (instead of collapsing to bare 'TABL' via
2643
+ * SLASH_TYPE_MAP) so the create path can route TABL/DT → /ddic/tables and
2644
+ * TABL/DS → /ddic/structures. See docs/plans/completed/fix-tabl-ds-create-routing.md. */
2645
+ const TABL_WRITE_SUBTYPES = new Set(['TABL/DT', 'TABL/DS']);
2646
+ /** Legacy slash-form aliases SAPWrite remaps to a canonical subtype before
2647
+ * SLASH_TYPE_MAP runs — otherwise STRU/DS would collapse to bare 'TABL' and
2648
+ * route the structure create to /ddic/tables. */
2649
+ const SAPWRITE_TABL_ALIAS = {
2650
+ 'STRU/DS': 'TABL/DS',
2651
+ };
2652
+ /** SAPWrite-only normalizer: preserves TABL/DT and TABL/DS and remaps STRU/DS
2653
+ * to TABL/DS. Every other tool keeps the global collapsing behaviour of
2654
+ * `normalizeObjectType`. */
2655
+ function normalizeWriteObjectType(type) {
2656
+ const normalized = String(type).trim().toUpperCase();
2657
+ if (!normalized)
2658
+ return '';
2659
+ const aliased = SAPWRITE_TABL_ALIAS[normalized];
2660
+ if (aliased)
2661
+ return aliased;
2662
+ if (TABL_WRITE_SUBTYPES.has(normalized))
2663
+ return normalized;
2664
+ return SLASH_TYPE_MAP[normalized] ?? normalized;
2665
+ }
2666
+ /** Collapse TABL/DT and TABL/DS back to bare 'TABL' for downstream Set-membership
2667
+ * checks (DDIC hints, RAP preflight, CDS dependency hints, cache invalidation)
2668
+ * that only know about canonical types. The slash form survives at URL routing
2669
+ * + XML envelope sites. */
2670
+ function canonicalTablType(type) {
2671
+ return type === 'TABL/DT' || type === 'TABL/DS' ? 'TABL' : type;
2672
+ }
2572
2673
  /** Normalize type fields before schema validation so slash/case aliases are accepted. */
2573
2674
  function normalizeTypeArgsForValidation(toolName, args) {
2574
2675
  switch (toolName) {
@@ -2579,14 +2680,15 @@ function normalizeTypeArgsForValidation(toolName, args) {
2579
2680
  objectType: args.objectType === undefined ? undefined : normalizeObjectType(String(args.objectType ?? '')),
2580
2681
  };
2581
2682
  case 'SAPWrite':
2683
+ // SAPWrite preserves TABL/DT and TABL/DS so the create path can route by subtype.
2582
2684
  return {
2583
2685
  ...args,
2584
- type: args.type === undefined ? undefined : normalizeObjectType(String(args.type ?? '')),
2686
+ type: args.type === undefined ? undefined : normalizeWriteObjectType(String(args.type ?? '')),
2585
2687
  objects: Array.isArray(args.objects)
2586
2688
  ? args.objects.map((obj) => typeof obj === 'object' && obj !== null
2587
2689
  ? {
2588
2690
  ...obj,
2589
- type: normalizeObjectType(String(obj.type ?? '')),
2691
+ type: normalizeWriteObjectType(String(obj.type ?? '')),
2590
2692
  }
2591
2693
  : obj)
2592
2694
  : args.objects,
@@ -2688,10 +2790,13 @@ export function objectBasePath(type) {
2688
2790
  case 'SRVB':
2689
2791
  return '/sap/bc/adt/businessservices/bindings/';
2690
2792
  case 'TABL':
2691
- // Default URL prefix for TABL: /tables/ (transparent tables). DDIC structures
2692
- // live at /sap/bc/adt/ddic/structures/<name>; for those, callers must use
2693
- // AdtClient.resolveTablObjectUrl(name) which falls back on 404.
2793
+ case 'TABL/DT':
2794
+ // Bare TABL defaults to transparent table. For reads, callers should use
2795
+ // AdtClient.resolveTablObjectUrl(name) which falls back to /structures/ on 404.
2694
2796
  return '/sap/bc/adt/ddic/tables/';
2797
+ case 'TABL/DS':
2798
+ // DDIC structures only route through this collection; see follow-up to #285.
2799
+ return '/sap/bc/adt/ddic/structures/';
2695
2800
  case 'DOMA':
2696
2801
  return '/sap/bc/adt/ddic/domains/';
2697
2802
  case 'DTEL':
@@ -2821,7 +2926,7 @@ function stripIncludeHeader(source) {
2821
2926
  // ─── SAPWrite Handler ────────────────────────────────────────────────
2822
2927
  async function handleSAPWrite(client, args, config, cachingLayer) {
2823
2928
  const action = String(args.action ?? '');
2824
- const type = normalizeObjectType(String(args.type ?? ''));
2929
+ const type = normalizeWriteObjectType(String(args.type ?? ''));
2825
2930
  const name = String(args.name ?? '');
2826
2931
  const source = String(args.source ?? '');
2827
2932
  const hasSource = typeof args.source === 'string';
@@ -2860,8 +2965,22 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2860
2965
  // `buildCreateXml('FUNC', …, properties)` finds it.
2861
2966
  let objectUrl;
2862
2967
  let srcUrl;
2863
- if (type === 'TABL' && action !== 'create' && action !== 'batch_create') {
2864
- objectUrl = await client.resolveTablObjectUrl(name);
2968
+ if ((type === 'TABL' || type === 'TABL/DT' || type === 'TABL/DS') &&
2969
+ action !== 'create' &&
2970
+ action !== 'batch_create') {
2971
+ // All TABL forms route through the search-first resolver on update/delete/activate
2972
+ // so the PR #286 SE11-hint refusal applies even when callers pass an explicit slash form.
2973
+ try {
2974
+ objectUrl = await client.resolveTablObjectUrlForWrite(name, {
2975
+ tablesEndpointAvailable: isTablesEndpointAvailable(),
2976
+ });
2977
+ }
2978
+ catch (resolveErr) {
2979
+ if (resolveErr instanceof AdtSafetyError) {
2980
+ return errorResult(resolveErr.message);
2981
+ }
2982
+ throw resolveErr;
2983
+ }
2865
2984
  srcUrl = `${objectUrl}/source/main`;
2866
2985
  }
2867
2986
  else if (type === 'FUNC') {
@@ -2886,11 +3005,20 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
2886
3005
  args.group = group;
2887
3006
  }
2888
3007
  else {
3008
+ // Discovery gate: refuse transparent-table creates upfront on systems that
3009
+ // don't expose /ddic/tables/ (NW 7.50/7.51). TABL/DS skips this — /structures/
3010
+ // is always available. See issue #285.
3011
+ if ((type === 'TABL' || type === 'TABL/DT') && (action === 'create' || action === 'batch_create')) {
3012
+ if (isTablesEndpointAvailable() === false) {
3013
+ return errorResult(TABL_DT_WRITE_UNAVAILABLE_HINT);
3014
+ }
3015
+ }
2889
3016
  objectUrl = objectUrlForType(type, name);
2890
3017
  srcUrl = sourceUrlForType(type, name);
2891
3018
  }
2892
3019
  const invalidateWrittenObject = (objType = type, objName = name) => {
2893
- cachingLayer?.invalidate(objType, objName, 'all');
3020
+ // Source cache is keyed by canonical type (SAPRead collapses TABL/DT, TABL/DS).
3021
+ cachingLayer?.invalidate(canonicalTablType(objType), objName, 'all');
2894
3022
  cachingLayer?.inactiveLists.invalidate(client.username);
2895
3023
  };
2896
3024
  // Helper: enforce allowedPackages for existing objects (update/delete/edit_method/scaffold_rap_handlers).
@@ -3135,7 +3263,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3135
3263
  // 'application/*' — the wildcard lets the SAP server resolve the correct
3136
3264
  // handler (matching how ADT Eclipse and abap-adt-api send requests).
3137
3265
  const contentType = createContentTypeForType(type);
3138
- const needsPackageParam = type === 'BDEF' || type === 'TABL';
3266
+ const needsPackageParam = type === 'BDEF' || type === 'TABL' || type === 'TABL/DT' || type === 'TABL/DS';
3139
3267
  let result;
3140
3268
  try {
3141
3269
  result = await createObject(client.http, client.safety, createUrl, body, contentType, effectiveTransport, needsPackageParam ? pkg : undefined, cachedFeatures?.abapRelease);
@@ -3581,7 +3709,9 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3581
3709
  });
3582
3710
  }
3583
3711
  catch (err) {
3584
- if (err instanceof AdtApiError && CDS_DEPENDENCY_SENSITIVE_TYPES.has(type) && isDeleteDependencyError(err)) {
3712
+ if (err instanceof AdtApiError &&
3713
+ CDS_DEPENDENCY_SENSITIVE_TYPES.has(canonicalTablType(type)) &&
3714
+ isDeleteDependencyError(err)) {
3585
3715
  const hint = await buildCdsDeleteDependencyHint(client, type, name, objectUrl);
3586
3716
  if (hint) {
3587
3717
  // Attach via extraHint so the LLM-facing formatter renders it after
@@ -3608,7 +3738,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3608
3738
  const activateAtEnd = args.activateAtEnd === true || String(args.activateAtEnd) === 'true';
3609
3739
  const defaultPackage = normalizePackageOverride(args.package, '$TMP');
3610
3740
  const batchPlan = objects.map((obj) => {
3611
- const objType = normalizeObjectType(String(obj.type ?? ''));
3741
+ const objType = normalizeWriteObjectType(String(obj.type ?? ''));
3612
3742
  const objName = String(obj.name ?? '');
3613
3743
  const objPackage = normalizePackageOverride(obj.package, defaultPackage);
3614
3744
  const explicitTransport = normalizeTransportOverride(obj.transport) ?? transport;
@@ -3749,13 +3879,24 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
3749
3879
  break;
3750
3880
  }
3751
3881
  }
3752
- // Step 1: Create the object
3882
+ // Step 1: Create the object (per-entry transparent-table discovery gate;
3883
+ // mirrors the single-create site above. TABL/DS skips it — /structures/ always exists.)
3884
+ if ((objType === 'TABL' || objType === 'TABL/DT') && isTablesEndpointAvailable() === false) {
3885
+ results.push({
3886
+ type: objType,
3887
+ name: objName,
3888
+ packageName: objPackage,
3889
+ status: 'failed',
3890
+ error: TABL_DT_WRITE_UNAVAILABLE_HINT,
3891
+ });
3892
+ break;
3893
+ }
3753
3894
  const objUrl = objectUrlForType(objType, objName);
3754
3895
  const createUrl = objUrl.replace(/\/[^/]+$/, '');
3755
3896
  const objMetadataProps = getMetadataWriteProperties(obj);
3756
3897
  const body = buildCreateXml(objType, objName, objPackage, objDescription, objMetadataProps);
3757
3898
  const contentType = createContentTypeForType(objType);
3758
- const needsPackageParam = objType === 'BDEF' || objType === 'TABL';
3899
+ const needsPackageParam = objType === 'BDEF' || objType === 'TABL' || objType === 'TABL/DT' || objType === 'TABL/DS';
3759
3900
  try {
3760
3901
  await createObject(client.http, client.safety, createUrl, body, contentType, objTransport, needsPackageParam ? objPackage : undefined, cachedFeatures?.abapRelease);
3761
3902
  }
@@ -3942,7 +4083,8 @@ function runRapPreflightValidation(source, type, name, features, configSystemTyp
3942
4083
  return { blocked: false };
3943
4084
  }
3944
4085
  const systemType = features?.systemType ?? (configSystemType !== 'auto' ? configSystemType : undefined);
3945
- const result = validateRapSource(type, source, {
4086
+ // Canonicalize so validateRapSource's 'TABL' case matches TABL/DT and TABL/DS.
4087
+ const result = validateRapSource(canonicalTablType(type), source, {
3946
4088
  systemType,
3947
4089
  abapRelease: features?.abapRelease,
3948
4090
  });
@@ -4196,7 +4338,12 @@ async function handleSAPActivate(client, args, cachingLayer) {
4196
4338
  const objName = String(o.name ?? '');
4197
4339
  let url;
4198
4340
  if (objType === 'TABL') {
4199
- url = await client.resolveTablObjectUrl(objName);
4341
+ // Use the write-path resolver: refuses TABL/DT activation on systems
4342
+ // that don't expose /sap/bc/adt/ddic/tables/ (NW 7.50/7.51), where
4343
+ // activate would hit the wrong endpoint. See issue #285.
4344
+ url = await client.resolveTablObjectUrlForWrite(objName, {
4345
+ tablesEndpointAvailable: isTablesEndpointAvailable(),
4346
+ });
4200
4347
  }
4201
4348
  else if (objType === 'FUNC') {
4202
4349
  let group = String(o.group ?? args.group ?? '').trim();
@@ -4238,15 +4385,26 @@ async function handleSAPActivate(client, args, cachingLayer) {
4238
4385
  .join('');
4239
4386
  return errorResult(`Batch activation failed for: ${names}.${statusDetails}\n${formatActivationMessages(result)}${combinedDiag}`);
4240
4387
  }
4241
- // Single activation (existing behavior). For TABL we resolve the URL because
4242
- // the existing object may live at /tables/ (transparent) or /structures/
4243
- // (DDIC structure); using the wrong one would produce a confusing 404.
4388
+ // Single activation (existing behavior). For TABL we use the write-path
4389
+ // resolver so transparent-table activations on NW 7.50/7.51 are refused
4390
+ // with the SE11 hint instead of silently activating against /structures/
4391
+ // (which would not even be the right object). See issue #285.
4244
4392
  // For FUNC the URL needs the parent function group baked into the path
4245
4393
  // (issue #250) — `objectBasePath('FUNC')` deliberately throws so generic
4246
4394
  // builders fail loudly. Auto-resolve the group when omitted.
4247
4395
  let objectUrl;
4248
4396
  if (type === 'TABL') {
4249
- objectUrl = await client.resolveTablObjectUrl(name);
4397
+ try {
4398
+ objectUrl = await client.resolveTablObjectUrlForWrite(name, {
4399
+ tablesEndpointAvailable: isTablesEndpointAvailable(),
4400
+ });
4401
+ }
4402
+ catch (resolveErr) {
4403
+ if (resolveErr instanceof AdtSafetyError) {
4404
+ return errorResult(resolveErr.message);
4405
+ }
4406
+ throw resolveErr;
4407
+ }
4250
4408
  }
4251
4409
  else if (type === 'FUNC') {
4252
4410
  let group = String(args.group ?? '').trim();