arc-1 0.4.4 → 0.6.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.
Files changed (84) hide show
  1. package/README.md +6 -5
  2. package/dist/adt/client.d.ts +11 -1
  3. package/dist/adt/client.d.ts.map +1 -1
  4. package/dist/adt/client.js +51 -1
  5. package/dist/adt/client.js.map +1 -1
  6. package/dist/adt/codeintel.js +1 -1
  7. package/dist/adt/codeintel.js.map +1 -1
  8. package/dist/adt/crud.d.ts.map +1 -1
  9. package/dist/adt/crud.js +1 -7
  10. package/dist/adt/crud.js.map +1 -1
  11. package/dist/adt/features.d.ts +29 -1
  12. package/dist/adt/features.d.ts.map +1 -1
  13. package/dist/adt/features.js +114 -2
  14. package/dist/adt/features.js.map +1 -1
  15. package/dist/adt/http.d.ts +24 -1
  16. package/dist/adt/http.d.ts.map +1 -1
  17. package/dist/adt/http.js +87 -28
  18. package/dist/adt/http.js.map +1 -1
  19. package/dist/adt/oauth.d.ts +19 -2
  20. package/dist/adt/oauth.d.ts.map +1 -1
  21. package/dist/adt/oauth.js +78 -28
  22. package/dist/adt/oauth.js.map +1 -1
  23. package/dist/adt/safety.d.ts +15 -7
  24. package/dist/adt/safety.d.ts.map +1 -1
  25. package/dist/adt/safety.js +49 -45
  26. package/dist/adt/safety.js.map +1 -1
  27. package/dist/adt/types.d.ts +33 -0
  28. package/dist/adt/types.d.ts.map +1 -1
  29. package/dist/adt/xml-parser.d.ts +10 -1
  30. package/dist/adt/xml-parser.d.ts.map +1 -1
  31. package/dist/adt/xml-parser.js +47 -0
  32. package/dist/adt/xml-parser.js.map +1 -1
  33. package/dist/aff/schemas/bdef-v1.json +62 -0
  34. package/dist/aff/schemas/clas-v1.json +276 -0
  35. package/dist/aff/schemas/ddls-v1.json +144 -0
  36. package/dist/aff/schemas/intf-v1.json +243 -0
  37. package/dist/aff/schemas/prog-v1.json +133 -0
  38. package/dist/aff/schemas/srvb-v1.json +115 -0
  39. package/dist/aff/schemas/srvd-v1.json +108 -0
  40. package/dist/aff/validator.d.ts +14 -0
  41. package/dist/aff/validator.d.ts.map +1 -0
  42. package/dist/aff/validator.js +83 -0
  43. package/dist/aff/validator.js.map +1 -0
  44. package/dist/handlers/hyperfocused.d.ts +1 -0
  45. package/dist/handlers/hyperfocused.d.ts.map +1 -1
  46. package/dist/handlers/hyperfocused.js +7 -6
  47. package/dist/handlers/hyperfocused.js.map +1 -1
  48. package/dist/handlers/intent.d.ts +17 -1
  49. package/dist/handlers/intent.d.ts.map +1 -1
  50. package/dist/handlers/intent.js +369 -27
  51. package/dist/handlers/intent.js.map +1 -1
  52. package/dist/handlers/schemas.d.ts +296 -0
  53. package/dist/handlers/schemas.d.ts.map +1 -0
  54. package/dist/handlers/schemas.js +250 -0
  55. package/dist/handlers/schemas.js.map +1 -0
  56. package/dist/handlers/tools.d.ts +1 -1
  57. package/dist/handlers/tools.d.ts.map +1 -1
  58. package/dist/handlers/tools.js +111 -42
  59. package/dist/handlers/tools.js.map +1 -1
  60. package/dist/handlers/zod-errors.d.ts +20 -0
  61. package/dist/handlers/zod-errors.d.ts.map +1 -0
  62. package/dist/handlers/zod-errors.js +43 -0
  63. package/dist/handlers/zod-errors.js.map +1 -0
  64. package/dist/server/config.d.ts +26 -0
  65. package/dist/server/config.d.ts.map +1 -1
  66. package/dist/server/config.js +156 -7
  67. package/dist/server/config.js.map +1 -1
  68. package/dist/server/http.d.ts +8 -0
  69. package/dist/server/http.d.ts.map +1 -1
  70. package/dist/server/http.js +134 -71
  71. package/dist/server/http.js.map +1 -1
  72. package/dist/server/server.d.ts +13 -2
  73. package/dist/server/server.d.ts.map +1 -1
  74. package/dist/server/server.js +82 -9
  75. package/dist/server/server.js.map +1 -1
  76. package/dist/server/types.d.ts +8 -1
  77. package/dist/server/types.d.ts.map +1 -1
  78. package/dist/server/types.js +2 -2
  79. package/dist/server/types.js.map +1 -1
  80. package/dist/server/xsuaa.d.ts +11 -1
  81. package/dist/server/xsuaa.d.ts.map +1 -1
  82. package/dist/server/xsuaa.js +127 -9
  83. package/dist/server/xsuaa.js.map +1 -1
  84. package/package.json +4 -3
@@ -13,10 +13,11 @@ import { findDefinition, findReferences, findWhereUsed, getCompletion, } from '.
13
13
  import { createObject, deleteObject, lockObject, safeUpdateSource, unlockObject } from '../adt/crud.js';
14
14
  import { activate, activateBatch, runAtcCheck, runUnitTests, syntaxCheck } from '../adt/devtools.js';
15
15
  import { getDump, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listTraces, } from '../adt/diagnostics.js';
16
- import { AdtApiError, AdtNetworkError, AdtSafetyError } from '../adt/errors.js';
17
- import { mapSapReleaseToAbaplintVersion, probeFeatures } from '../adt/features.js';
18
- import { isOperationAllowed, OperationType } from '../adt/safety.js';
16
+ import { AdtApiError, AdtNetworkError, AdtSafetyError, isNotFoundError } from '../adt/errors.js';
17
+ import { classifyTextSearchError, mapSapReleaseToAbaplintVersion, probeFeatures } from '../adt/features.js';
18
+ import { checkPackage, isOperationAllowed, OperationType } from '../adt/safety.js';
19
19
  import { createTransport, getTransport, listTransports, releaseTransport } from '../adt/transport.js';
20
+ import { validateAffHeader } from '../aff/validator.js';
20
21
  import { extractCdsElements } from '../context/cds-deps.js';
21
22
  import { compressCdsContext, compressContext } from '../context/compressor.js';
22
23
  import { extractMethod, formatMethodListing, listMethods, spliceMethod } from '../context/method-surgery.js';
@@ -26,6 +27,8 @@ import { sanitizeArgs } from '../server/audit.js';
26
27
  import { generateRequestId, requestContext } from '../server/context.js';
27
28
  import { logger } from '../server/logger.js';
28
29
  import { expandHyperfocusedArgs, getHyperfocusedScope } from './hyperfocused.js';
30
+ import { getToolSchema } from './schemas.js';
31
+ import { formatZodError } from './zod-errors.js';
29
32
  /**
30
33
  * Scope required for each tool.
31
34
  *
@@ -39,7 +42,7 @@ import { expandHyperfocusedArgs, getHyperfocusedScope } from './hyperfocused.js'
39
42
  export const TOOL_SCOPES = {
40
43
  SAPRead: 'read',
41
44
  SAPSearch: 'read',
42
- SAPQuery: 'read',
45
+ SAPQuery: 'sql',
43
46
  SAPNavigate: 'read',
44
47
  SAPContext: 'read',
45
48
  SAPLint: 'read',
@@ -47,8 +50,24 @@ export const TOOL_SCOPES = {
47
50
  SAPWrite: 'write',
48
51
  SAPActivate: 'write',
49
52
  SAPManage: 'write',
50
- SAPTransport: 'admin',
53
+ SAPTransport: 'write',
51
54
  };
55
+ /**
56
+ * Check if authInfo has the required scope, respecting implied scopes:
57
+ * - `write` implies `read`
58
+ * - `sql` implies `data`
59
+ */
60
+ export function hasRequiredScope(authInfo, requiredScope) {
61
+ const scopes = authInfo.scopes;
62
+ if (scopes.includes(requiredScope))
63
+ return true;
64
+ // Implied scopes
65
+ if (requiredScope === 'read' && scopes.includes('write'))
66
+ return true;
67
+ if (requiredScope === 'data' && scopes.includes('sql'))
68
+ return true;
69
+ return false;
70
+ }
52
71
  function textResult(text) {
53
72
  return { content: [{ type: 'text', text }] };
54
73
  }
@@ -92,7 +111,7 @@ function classifyError(err) {
92
111
  * all tools are allowed (backward compatibility).
93
112
  * @param server - MCP Server instance for elicitation support.
94
113
  */
95
- export async function handleToolCall(client, config, toolName, args, authInfo, _server, cachingLayer) {
114
+ export async function handleToolCall(client, config, toolName, args, authInfo, _server, cachingLayer, isPerUserClient) {
96
115
  const reqId = generateRequestId();
97
116
  const start = Date.now();
98
117
  // Build user context for audit logging
@@ -112,7 +131,7 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
112
131
  // Scope enforcement — only when authInfo is present (XSUAA/OIDC mode)
113
132
  if (authInfo) {
114
133
  const requiredScope = TOOL_SCOPES[toolName];
115
- if (requiredScope && !authInfo.scopes.includes(requiredScope)) {
134
+ if (requiredScope && !hasRequiredScope(authInfo, requiredScope)) {
116
135
  logger.emitAudit({
117
136
  timestamp: new Date().toISOString(),
118
137
  level: 'warn',
@@ -127,6 +146,29 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
127
146
  return errorResult(`Insufficient scope: '${requiredScope}' required for ${toolName}. Your scopes: [${authInfo.scopes.join(', ')}]`);
128
147
  }
129
148
  }
149
+ // Validate tool arguments with Zod schema
150
+ const isBtp = config.systemType === 'btp';
151
+ // Always use the full search schema for validation — the handler checks text search availability
152
+ // and returns a proper error message with the probe reason when source_code search is unavailable
153
+ const schema = getToolSchema(toolName, isBtp);
154
+ if (schema) {
155
+ const parsed = schema.safeParse(args);
156
+ if (!parsed.success) {
157
+ const validationError = formatZodError(parsed.error, toolName);
158
+ logger.emitAudit({
159
+ timestamp: new Date().toISOString(),
160
+ level: 'warn',
161
+ event: 'safety_blocked',
162
+ requestId: reqId,
163
+ user,
164
+ clientId,
165
+ operation: toolName,
166
+ reason: 'Input validation failed',
167
+ });
168
+ return errorResult(validationError);
169
+ }
170
+ args = parsed.data;
171
+ }
130
172
  // Run within request context so HTTP-level logs get the requestId
131
173
  return requestContext.run({ requestId: reqId, user, tool: toolName }, async () => {
132
174
  try {
@@ -163,7 +205,7 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
163
205
  result = await handleSAPContext(client, args, cachingLayer);
164
206
  break;
165
207
  case 'SAPManage':
166
- result = await handleSAPManage(client, config, args, cachingLayer);
208
+ result = await handleSAPManage(client, config, args, cachingLayer, isPerUserClient);
167
209
  break;
168
210
  case 'SAP': {
169
211
  // Hyperfocused mode: route to the appropriate handler
@@ -175,13 +217,13 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
175
217
  // Check scope for the delegated action
176
218
  if (authInfo) {
177
219
  const requiredScope = getHyperfocusedScope(String(args.action ?? ''));
178
- if (!authInfo.scopes.includes(requiredScope)) {
220
+ if (!hasRequiredScope(authInfo, requiredScope)) {
179
221
  result = errorResult(`Insufficient scope: '${requiredScope}' required for SAP(action="${args.action}"). Your scopes: [${authInfo.scopes.join(', ')}]`);
180
222
  break;
181
223
  }
182
224
  }
183
225
  // Delegate to the real handler (recursive call, but with the mapped tool name)
184
- result = await handleToolCall(client, config, expanded.toolName, expanded.expandedArgs, authInfo, _server, cachingLayer);
226
+ result = await handleToolCall(client, config, expanded.toolName, expanded.expandedArgs, authInfo, _server, cachingLayer, isPerUserClient);
185
227
  break;
186
228
  }
187
229
  default:
@@ -257,10 +299,19 @@ async function handleSAPRead(client, args, cachingLayer) {
257
299
  const { source } = await cachingLayer.getSource(objType, objName, fetcher);
258
300
  return source;
259
301
  };
302
+ // Structured format is only supported for CLAS type
303
+ if (args.format === 'structured' && type !== 'CLAS') {
304
+ return errorResult('The "structured" format is only supported for CLAS type. Other types return text format.');
305
+ }
260
306
  switch (type) {
261
307
  case 'PROG':
262
308
  return textResult(await cachedGet('PROG', name, () => client.getProgram(name)));
263
309
  case 'CLAS': {
310
+ // Structured format: return JSON with metadata + decomposed source
311
+ if (args.format === 'structured') {
312
+ const structured = await client.getClassStructured(name);
313
+ return textResult(JSON.stringify(structured, null, 2));
314
+ }
264
315
  const methodParam = args.method;
265
316
  if (methodParam && !args.include) {
266
317
  // Method-level read — fetch full source then extract
@@ -336,8 +387,17 @@ async function handleSAPRead(client, args, cachingLayer) {
336
387
  return textResult(await cachedGet('BDEF', name, () => client.getBdef(name)));
337
388
  case 'SRVD':
338
389
  return textResult(await cachedGet('SRVD', name, () => client.getSrvd(name)));
339
- case 'DDLX':
340
- return textResult(await cachedGet('DDLX', name, () => client.getDdlx(name)));
390
+ case 'DDLX': {
391
+ try {
392
+ return textResult(await cachedGet('DDLX', name, () => client.getDdlx(name)));
393
+ }
394
+ catch (err) {
395
+ if (isNotFoundError(err)) {
396
+ return textResult(`No metadata extension (DDLX) found for "${name}". This means no @UI annotations are defined via DDLX for this view. The view may use inline annotations in the DDLS source, or the Fiori app may configure columns via manifest.json / app descriptor.`);
397
+ }
398
+ throw err;
399
+ }
400
+ }
341
401
  case 'SRVB':
342
402
  return textResult(await cachedGet('SRVB', name, () => client.getSrvb(name)));
343
403
  case 'TABL':
@@ -421,7 +481,9 @@ async function handleSAPRead(client, args, cachingLayer) {
421
481
  case 'VARIANTS':
422
482
  return textResult(await client.getVariants(name));
423
483
  default:
424
- return errorResult(`Unknown SAPRead type: ${type}. Supported: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DDLX, BDEF, SRVD, SRVB, TABL, VIEW, STRU, DOMA, DTEL, TRAN, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS`);
484
+ return errorResult(`Unknown SAPRead type: "${type}". Supported types: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DDLX, BDEF, SRVD, SRVB, TABL, VIEW, STRU, DOMA, DTEL, TRAN, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS. ` +
485
+ 'Tip: Map objectType from SAPSearch results by dropping the slash suffix (e.g., DDLS/DF → type="DDLS", CLAS/OC → type="CLAS", PROG/P → type="PROG"). ' +
486
+ 'Do not pass a URI — use the "type" and "name" parameters instead.');
425
487
  }
426
488
  }
427
489
  async function handleSAPSearch(client, args) {
@@ -429,6 +491,11 @@ async function handleSAPSearch(client, args) {
429
491
  const maxResults = Number(args.maxResults ?? 100);
430
492
  const searchType = String(args.searchType ?? 'object');
431
493
  if (searchType === 'source_code') {
494
+ // If probe already determined textSearch is unavailable, return the precise reason
495
+ if (cachedFeatures?.textSearch && !cachedFeatures.textSearch.available) {
496
+ return errorResult(`Source code search is not available on this SAP system. ${cachedFeatures.textSearch.reason ?? ''}` +
497
+ `\nUse SAPSearch with searchType="object" to search by object name instead, or use SAPQuery to search metadata tables.`);
498
+ }
432
499
  const objectType = args.objectType;
433
500
  const packageName = args.packageName;
434
501
  try {
@@ -436,9 +503,13 @@ async function handleSAPSearch(client, args) {
436
503
  return textResult(JSON.stringify(results, null, 2));
437
504
  }
438
505
  catch (err) {
439
- if (err instanceof AdtApiError && (err.statusCode === 404 || err.statusCode === 501)) {
440
- return errorResult(`Source code search is not available on this SAP system (requires SAP_BASIS ≥ 7.51). ` +
441
- `Use SAPSearch with searchType="object" to search by object name instead, or use SAPQuery to search metadata tables.`);
506
+ if (err instanceof AdtApiError) {
507
+ const permanentCodes = [401, 403, 404, 501];
508
+ if (permanentCodes.includes(err.statusCode)) {
509
+ const classified = classifyTextSearchError(err.statusCode);
510
+ return errorResult(`Source code search is not available on this SAP system. ${classified.reason ?? ''}` +
511
+ `\nUse SAPSearch with searchType="object" to search by object name instead, or use SAPQuery to search metadata tables.`);
512
+ }
442
513
  }
443
514
  throw err;
444
515
  }
@@ -474,6 +545,10 @@ async function handleSAPQuery(client, args) {
474
545
  }
475
546
  }
476
547
  }
548
+ // JOIN-aware error: ADT freestyle SQL parser has known edge cases with JOINs (SAP Note 3605050)
549
+ if (err instanceof AdtApiError && err.statusCode === 400 && /\bJOIN\b/i.test(sql)) {
550
+ return errorResult(`${err.message}\n\nMulti-table JOIN query failed. The ADT freestyle SQL endpoint has known parser edge cases with JOINs (SAP Note 3605050). Try splitting into separate single-table queries.`);
551
+ }
477
552
  throw err;
478
553
  }
479
554
  }
@@ -540,6 +615,129 @@ function buildLintConfigOptions(config, ruleOverrides) {
540
615
  ruleOverrides,
541
616
  };
542
617
  }
618
+ // ─── Object Creation XML ─────────────────────────────────────────────
619
+ /**
620
+ * Build the type-specific XML body for ADT object creation.
621
+ *
622
+ * SAP ADT requires each object type to have its own root XML element.
623
+ * Using a generic body (e.g. adtcore:objectReferences) returns 400:
624
+ * "System expected the element '{http://www.sap.com/adt/programs/programs}abapProgram'"
625
+ */
626
+ export function buildCreateXml(type, name, pkg, description) {
627
+ switch (type) {
628
+ case 'PROG':
629
+ return `<?xml version="1.0" encoding="UTF-8"?>
630
+ <program:abapProgram xmlns:program="http://www.sap.com/adt/programs/programs"
631
+ xmlns:adtcore="http://www.sap.com/adt/core"
632
+ adtcore:description="${escapeXml(description)}"
633
+ adtcore:name="${escapeXml(name)}"
634
+ adtcore:type="PROG/P"
635
+ adtcore:masterLanguage="EN"
636
+ adtcore:masterSystem="H00"
637
+ adtcore:responsible="DEVELOPER">
638
+ <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
639
+ </program:abapProgram>`;
640
+ case 'CLAS':
641
+ return `<?xml version="1.0" encoding="UTF-8"?>
642
+ <class:abapClass xmlns:class="http://www.sap.com/adt/oo/classes"
643
+ xmlns:adtcore="http://www.sap.com/adt/core"
644
+ adtcore:description="${escapeXml(description)}"
645
+ adtcore:name="${escapeXml(name)}"
646
+ adtcore:type="CLAS/OC"
647
+ adtcore:masterLanguage="EN"
648
+ adtcore:masterSystem="H00"
649
+ adtcore:responsible="DEVELOPER">
650
+ <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
651
+ </class:abapClass>`;
652
+ case 'INTF':
653
+ return `<?xml version="1.0" encoding="UTF-8"?>
654
+ <intf:abapInterface xmlns:intf="http://www.sap.com/adt/oo/interfaces"
655
+ xmlns:adtcore="http://www.sap.com/adt/core"
656
+ adtcore:description="${escapeXml(description)}"
657
+ adtcore:name="${escapeXml(name)}"
658
+ adtcore:type="INTF/OI"
659
+ adtcore:masterLanguage="EN"
660
+ adtcore:masterSystem="H00"
661
+ adtcore:responsible="DEVELOPER">
662
+ <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
663
+ </intf:abapInterface>`;
664
+ case 'INCL':
665
+ return `<?xml version="1.0" encoding="UTF-8"?>
666
+ <include:abapInclude xmlns:include="http://www.sap.com/adt/programs/includes"
667
+ xmlns:adtcore="http://www.sap.com/adt/core"
668
+ adtcore:description="${escapeXml(description)}"
669
+ adtcore:name="${escapeXml(name)}"
670
+ adtcore:type="PROG/I"
671
+ adtcore:masterLanguage="EN"
672
+ adtcore:masterSystem="H00"
673
+ adtcore:responsible="DEVELOPER">
674
+ <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
675
+ </include:abapInclude>`;
676
+ case 'DDLS':
677
+ return `<?xml version="1.0" encoding="UTF-8"?>
678
+ <ddl:ddlSource xmlns:ddl="http://www.sap.com/adt/ddic/ddlsources"
679
+ xmlns:adtcore="http://www.sap.com/adt/core"
680
+ adtcore:description="${escapeXml(description)}"
681
+ adtcore:name="${escapeXml(name)}"
682
+ adtcore:type="DDLS/DF"
683
+ adtcore:masterLanguage="EN"
684
+ adtcore:masterSystem="H00"
685
+ adtcore:responsible="DEVELOPER">
686
+ <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
687
+ </ddl:ddlSource>`;
688
+ case 'BDEF':
689
+ return `<?xml version="1.0" encoding="UTF-8"?>
690
+ <bdef:behaviorDefinition xmlns:bdef="http://www.sap.com/adt/bo/behaviordefinitions"
691
+ xmlns:adtcore="http://www.sap.com/adt/core"
692
+ adtcore:description="${escapeXml(description)}"
693
+ adtcore:name="${escapeXml(name)}"
694
+ adtcore:type="BDEF/BDO"
695
+ adtcore:masterLanguage="EN"
696
+ adtcore:masterSystem="H00"
697
+ adtcore:responsible="DEVELOPER">
698
+ <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
699
+ </bdef:behaviorDefinition>`;
700
+ case 'SRVD':
701
+ return `<?xml version="1.0" encoding="UTF-8"?>
702
+ <srvd:srvdSource xmlns:srvd="http://www.sap.com/adt/ddic/srvd/sources"
703
+ xmlns:adtcore="http://www.sap.com/adt/core"
704
+ adtcore:description="${escapeXml(description)}"
705
+ adtcore:name="${escapeXml(name)}"
706
+ adtcore:type="SRVD/SRV"
707
+ adtcore:masterLanguage="EN"
708
+ adtcore:masterSystem="H00"
709
+ adtcore:responsible="DEVELOPER">
710
+ <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
711
+ </srvd:srvdSource>`;
712
+ case 'DDLX':
713
+ return `<?xml version="1.0" encoding="UTF-8"?>
714
+ <ddlx:ddlxSource xmlns:ddlx="http://www.sap.com/adt/ddic/ddlx/sources"
715
+ xmlns:adtcore="http://www.sap.com/adt/core"
716
+ adtcore:description="${escapeXml(description)}"
717
+ adtcore:name="${escapeXml(name)}"
718
+ adtcore:type="DDLX/EX"
719
+ adtcore:masterLanguage="EN"
720
+ adtcore:masterSystem="H00"
721
+ adtcore:responsible="DEVELOPER">
722
+ <adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
723
+ </ddlx:ddlxSource>`;
724
+ default:
725
+ // Fallback — generic objectReferences using the correct URL for the type
726
+ return `<?xml version="1.0" encoding="UTF-8"?>
727
+ <adtcore:objectReferences xmlns:adtcore="http://www.sap.com/adt/core">
728
+ <adtcore:objectReference adtcore:uri="${escapeXml(objectUrlForType(type, name))}" adtcore:type="${escapeXml(type)}" adtcore:name="${escapeXml(name)}" adtcore:packageName="${escapeXml(pkg)}"/>
729
+ </adtcore:objectReferences>`;
730
+ }
731
+ }
732
+ /** Escape special characters for XML attribute values */
733
+ function escapeXml(s) {
734
+ return s
735
+ .replace(/&/g, '&amp;')
736
+ .replace(/"/g, '&quot;')
737
+ .replace(/'/g, '&apos;')
738
+ .replace(/</g, '&lt;')
739
+ .replace(/>/g, '&gt;');
740
+ }
543
741
  // ─── Object URL Mapping ──────────────────────────────────────────────
544
742
  /** Map object type + name to the ADT object URL used by CRUD/DevTools/etc. */
545
743
  function objectUrlForType(type, name) {
@@ -592,6 +790,10 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
592
790
  const name = String(args.name ?? '');
593
791
  const source = String(args.source ?? '');
594
792
  const transport = args.transport;
793
+ // type and name are required for all actions except batch_create
794
+ if (action !== 'batch_create' && (!type || !name)) {
795
+ return errorResult('"type" and "name" are required for this action.');
796
+ }
595
797
  const objectUrl = objectUrlForType(type, name);
596
798
  const srcUrl = sourceUrlForType(type, name);
597
799
  switch (action) {
@@ -607,12 +809,33 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
607
809
  }
608
810
  case 'create': {
609
811
  const pkg = String(args.package ?? '$TMP');
610
- // Build creation XML body
611
- const body = `<?xml version="1.0" encoding="UTF-8"?>
612
- <adtcore:objectReferences xmlns:adtcore="http://www.sap.com/adt/core">
613
- <adtcore:objectReference adtcore:uri="${objectUrl}" adtcore:type="${type}" adtcore:name="${name}" adtcore:packageName="${pkg}"/>
614
- </adtcore:objectReferences>`;
615
- const result = await createObject(client.http, client.safety, objectUrl, body, 'application/xml', transport);
812
+ checkPackage(client.safety, pkg);
813
+ const description = String(args.description ?? name);
814
+ checkPackage(client.safety, pkg);
815
+ // AFF header validation (if schema available for this type)
816
+ const affResult = validateAffHeader(type, { description, originalLanguage: 'en' });
817
+ if (!affResult.valid) {
818
+ return errorResult(`AFF metadata validation failed for ${type} ${name}:\n- ${(affResult.errors ?? []).join('\n- ')}\n\nFix the metadata and retry.`);
819
+ }
820
+ // Build type-specific creation XML body.
821
+ // SAP ADT requires the root element to match the object type —
822
+ // a generic objectReferences body returns 400 "System expected the element ...".
823
+ const body = buildCreateXml(type, name, pkg, description);
824
+ // Step 1: Create the object (metadata only)
825
+ const createUrl = objectUrl.replace(/\/[^/]+$/, ''); // parent collection URL
826
+ const result = await createObject(client.http, client.safety, createUrl, body, 'application/xml', transport);
827
+ // Step 2: Write source code if provided
828
+ if (source) {
829
+ // Pre-write lint validation
830
+ const lintWarnings = runPreWriteLint(source, type, name, config);
831
+ if (lintWarnings.blocked) {
832
+ return textResult(`Created ${type} ${name} in package ${pkg}, but source was rejected by lint:\n${lintWarnings.result.content[0].text}`);
833
+ }
834
+ await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, transport);
835
+ cachingLayer?.invalidate(type, name);
836
+ const msg = `Created ${type} ${name} in package ${pkg} and wrote source code.`;
837
+ return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
838
+ }
616
839
  return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}`);
617
840
  }
618
841
  case 'edit_method': {
@@ -665,8 +888,104 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
665
888
  cachingLayer?.invalidate(type, name);
666
889
  return textResult(`Deleted ${type} ${name}.`);
667
890
  }
891
+ case 'batch_create': {
892
+ const objects = args.objects;
893
+ if (!objects || !Array.isArray(objects) || objects.length === 0) {
894
+ return errorResult('"objects" array is required and must be non-empty for batch_create action.');
895
+ }
896
+ const pkg = String(args.package ?? '$TMP');
897
+ // Check package is allowed before starting any creates
898
+ checkPackage(client.safety, pkg);
899
+ const results = [];
900
+ for (const obj of objects) {
901
+ const objType = String(obj.type ?? '');
902
+ const objName = String(obj.name ?? '');
903
+ const objSource = obj.source ? String(obj.source) : undefined;
904
+ const objDescription = String(obj.description ?? objName);
905
+ // AFF header validation per object (if schema available)
906
+ const affResult = validateAffHeader(objType, { description: objDescription, originalLanguage: 'en' });
907
+ if (!affResult.valid) {
908
+ results.push({
909
+ type: objType,
910
+ name: objName,
911
+ status: 'failed',
912
+ error: `AFF metadata validation failed:\n- ${(affResult.errors ?? []).join('\n- ')}`,
913
+ });
914
+ break;
915
+ }
916
+ try {
917
+ // Pre-validate source with lint BEFORE creating the object to avoid orphaned objects
918
+ if (objSource) {
919
+ const lintWarnings = runPreWriteLint(objSource, objType, objName, config);
920
+ if (lintWarnings.blocked) {
921
+ results.push({
922
+ type: objType,
923
+ name: objName,
924
+ status: 'failed',
925
+ error: `source rejected by lint: ${lintWarnings.result.content[0].text}`,
926
+ });
927
+ break;
928
+ }
929
+ }
930
+ // Step 1: Create the object
931
+ const objUrl = objectUrlForType(objType, objName);
932
+ const createUrl = objUrl.replace(/\/[^/]+$/, '');
933
+ const body = buildCreateXml(objType, objName, pkg, objDescription);
934
+ await createObject(client.http, client.safety, createUrl, body, 'application/xml', transport);
935
+ // Step 2: Write source if provided
936
+ if (objSource) {
937
+ const srcUrl = sourceUrlForType(objType, objName);
938
+ await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, transport);
939
+ }
940
+ // Step 3: Activate the object
941
+ const activationResult = await activate(client.http, client.safety, objUrl);
942
+ if (!activationResult.success) {
943
+ results.push({
944
+ type: objType,
945
+ name: objName,
946
+ status: 'failed',
947
+ error: `activation failed: ${activationResult.messages.join('; ')}`,
948
+ });
949
+ break;
950
+ }
951
+ cachingLayer?.invalidate(objType, objName);
952
+ results.push({ type: objType, name: objName, status: 'success' });
953
+ }
954
+ catch (err) {
955
+ results.push({
956
+ type: objType,
957
+ name: objName,
958
+ status: 'failed',
959
+ error: err instanceof Error ? err.message : String(err),
960
+ });
961
+ break;
962
+ }
963
+ }
964
+ // Add 'skipped' entries for objects that were never attempted due to early break
965
+ for (let i = results.length; i < objects.length; i++) {
966
+ const skipped = objects[i];
967
+ results.push({
968
+ type: String(skipped.type ?? ''),
969
+ name: String(skipped.name ?? ''),
970
+ status: 'failed',
971
+ error: 'skipped — stopped after previous failure',
972
+ });
973
+ }
974
+ const summary = results
975
+ .map((r) => `${r.name} (${r.type}) ${r.status === 'success' ? '✓' : `✗ — ${r.error}`}`)
976
+ .join(', ');
977
+ const successCount = results.filter((r) => r.status === 'success').length;
978
+ const hasFailure = results.some((r) => r.status === 'failed');
979
+ if (hasFailure) {
980
+ const cleanupHint = successCount > 0
981
+ ? ` Note: ${successCount} already-created object(s) remain on the SAP system and may need manual cleanup.`
982
+ : '';
983
+ return errorResult(`Batch created ${successCount}/${objects.length} objects in package ${pkg}: ${summary}${cleanupHint}`);
984
+ }
985
+ return textResult(`Batch created ${successCount} objects in package ${pkg}: ${summary}`);
986
+ }
668
987
  default:
669
- return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete, edit_method`);
988
+ return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete, edit_method, batch_create`);
670
989
  }
671
990
  }
672
991
  /**
@@ -1033,7 +1352,7 @@ async function handleSAPContext(client, args, cachingLayer) {
1033
1352
  // ─── SAPManage Handler ────────────────────────────────────────────────
1034
1353
  /** Cached feature status — populated on first probe */
1035
1354
  let cachedFeatures;
1036
- async function handleSAPManage(client, config, args, cachingLayer) {
1355
+ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClient) {
1037
1356
  const action = String(args.action ?? '');
1038
1357
  switch (action) {
1039
1358
  case 'features': {
@@ -1063,11 +1382,30 @@ async function handleSAPManage(client, config, args, cachingLayer) {
1063
1382
  featureConfig.amdp = config.featureAmdp;
1064
1383
  featureConfig.ui5 = config.featureUi5;
1065
1384
  featureConfig.transport = config.featureTransport;
1066
- cachedFeatures = await probeFeatures(client.http, featureConfig, config.systemType);
1067
- return textResult(JSON.stringify(cachedFeatures, null, 2));
1385
+ const probed = await probeFeatures(client.http, featureConfig, config.systemType);
1386
+ // In PP mode with a per-user client, auth-sensitive results (401/403 on any
1387
+ // feature) must not poison the global cache — another user may have different
1388
+ // authorizations. Return the per-user result to the caller but keep the global
1389
+ // cache unchanged. However, when PP is enabled but the request fell back to the
1390
+ // shared/default client (no JWT, missing btpConfig, or non-strict fallback), the
1391
+ // probe ran with the same service-account credentials as the startup probe, so
1392
+ // updating the cache is safe and allows a manual probe to repair a failed startup.
1393
+ // Apply the same auth-failure sanitization as the startup probe: in PP mode,
1394
+ // shared-client 401/403 on textSearch must not hide source_code from users who
1395
+ // might have authorization via per-user clients.
1396
+ if (!isPerUserClient) {
1397
+ if (config.ppEnabled && probed.textSearch && !probed.textSearch.available) {
1398
+ const reason = probed.textSearch.reason ?? '';
1399
+ if (reason.includes('authorization') || reason.includes('401') || reason.includes('403')) {
1400
+ probed.textSearch = undefined;
1401
+ }
1402
+ }
1403
+ cachedFeatures = probed;
1404
+ }
1405
+ return textResult(JSON.stringify(probed, null, 2));
1068
1406
  }
1069
1407
  default:
1070
- return errorResult(`Unknown SAPManage action: ${action}. Supported: features, probe`);
1408
+ return errorResult(`Unknown SAPManage action: ${action}. Supported: features, probe, cache_stats`);
1071
1409
  }
1072
1410
  }
1073
1411
  /** Reset cached features (for testing) */
@@ -1078,4 +1416,8 @@ export function resetCachedFeatures() {
1078
1416
  export function setCachedFeatures(features) {
1079
1417
  cachedFeatures = features;
1080
1418
  }
1419
+ /** Get cached features (for tool definition adaptation) */
1420
+ export function getCachedFeatures() {
1421
+ return cachedFeatures;
1422
+ }
1081
1423
  //# sourceMappingURL=intent.js.map