sap-ai-dev-toolkit 0.9.9917 → 0.9.9920

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 (59) hide show
  1. package/.github/agents/sap-integration-architect.agent.md +32 -18
  2. package/.github/agents/sap-integration-developer.agent.md +28 -12
  3. package/.github/skills/integration-suite-delivery/SKILL.md +94 -37
  4. package/.github/skills/sap-integration-architect/SKILL.md +49 -25
  5. package/README.md +73 -14
  6. package/dist/checksums.txt +5 -5
  7. package/dist/vsp-darwin-arm64 +0 -0
  8. package/dist/vsp-darwin-x64 +0 -0
  9. package/dist/vsp-linux-arm64 +0 -0
  10. package/dist/vsp-linux-x64 +0 -0
  11. package/dist/vsp-win32-x64.exe +0 -0
  12. package/inventory.md +97 -57
  13. package/package.json +1 -1
  14. package/scripts/check-live-integration.mjs +151 -0
  15. package/scripts/install-local.mjs +11 -4
  16. package/scripts/run-live-integration.mjs +52 -60
  17. package/src/apim-bundle.mjs +416 -13
  18. package/src/apim-cleanup-tools.mjs +145 -0
  19. package/src/apim-content.mjs +157 -0
  20. package/src/apim-delete-tools.mjs +139 -0
  21. package/src/apim-documentation.mjs +162 -0
  22. package/src/apim-inspection.mjs +141 -0
  23. package/src/apim-monitoring.mjs +140 -0
  24. package/src/apim-policy-steps.mjs +31 -0
  25. package/src/apim-runtime.mjs +262 -0
  26. package/src/apim-tools.mjs +202 -109
  27. package/src/cf-integration.mjs +46 -7
  28. package/src/cpi-async-monitoring.mjs +177 -0
  29. package/src/cpi-content.mjs +92 -0
  30. package/src/cpi-monitoring.mjs +225 -0
  31. package/src/cpi-operational.mjs +129 -0
  32. package/src/cpi-release.mjs +273 -0
  33. package/src/cpi-security.mjs +48 -0
  34. package/src/cpi-tools.mjs +184 -319
  35. package/src/devhub-retirement.mjs +345 -0
  36. package/src/devhub-tools.mjs +177 -0
  37. package/src/edge-monitoring.mjs +101 -0
  38. package/src/iflow-bundle.mjs +295 -7
  39. package/src/integration-alerts.mjs +248 -0
  40. package/src/integration-analysis.mjs +488 -0
  41. package/src/integration-capabilities.mjs +181 -0
  42. package/src/integration-collections.mjs +203 -0
  43. package/src/integration-common.mjs +140 -35
  44. package/src/integration-config.mjs +20 -14
  45. package/src/integration-credentials.mjs +16 -10
  46. package/src/integration-http.mjs +255 -67
  47. package/src/integration-messaging.mjs +118 -0
  48. package/src/integration-optional-config.mjs +122 -0
  49. package/src/integration-optional-tools.mjs +153 -0
  50. package/src/integration-release-store.mjs +410 -0
  51. package/src/integration-server.mjs +34 -16
  52. package/src/integration-state.mjs +31 -0
  53. package/src/integration-tools.mjs +44 -8
  54. package/src/integration-transport.mjs +184 -0
  55. package/src/integration-validation.mjs +26 -0
  56. package/src/integration-verify.mjs +143 -62
  57. package/src/open-connectors-tools.mjs +143 -0
  58. package/src/setup.mjs +66 -44
  59. package/tools.md +193 -26
package/src/cpi-tools.mjs CHANGED
@@ -1,5 +1,10 @@
1
1
  // MCP tools for SAP Cloud Integration (CPI) over the /api/v1 OData API.
2
2
  import { z } from 'zod';
3
+ import { X509Certificate } from 'node:crypto';
4
+ import { registerCpiSecurityTools } from './cpi-security.mjs';
5
+ import { registerCpiOperationalTools } from './cpi-operational.mjs';
6
+ import { readCpiConfigurations, writeCpiConfigurations, cpiReleaseFailure, CPI_PACKAGE_ARTIFACT_SETS as PACKAGE_ARTIFACT_SETS } from './cpi-content.mjs';
7
+ import { registerCpiReleaseTools, CPI_ARTIFACT_TYPES as ARTIFACT_TYPES } from './cpi-release.mjs';
3
8
  import {
4
9
  CREATE_ANNOTATIONS,
5
10
  ERROR_TEXT_CAP,
@@ -13,8 +18,6 @@ import {
13
18
  confirmation,
14
19
  entity,
15
20
  fileEntry,
16
- isoDate,
17
- isoToOdataDateTime,
18
21
  key,
19
22
  list,
20
23
  pick,
@@ -27,6 +30,8 @@ import {
27
30
  top as topSchema
28
31
  } from './integration-common.mjs';
29
32
  import { IntegrationError, odataDate, odataKey } from './integration-http.mjs';
33
+ import { createCollectionPager, cursorSchema } from './integration-collections.mjs';
34
+ import { registerCpiMonitoringTools } from './cpi-monitoring.mjs';
30
35
  import {
31
36
  MANIFEST_PATH,
32
37
  PARAMETERS_PROP,
@@ -46,7 +51,6 @@ import {
46
51
 
47
52
  const CPI_BASE = '/api/v1';
48
53
  const MAX_UPDATE_FILE_BYTES = 256 * 1024;
49
- const MESSAGE_STATUSES = ['COMPLETED', 'PROCESSING', 'RETRY', 'ESCALATED', 'FAILED', 'CANCELLED', 'DISCARDED', 'ABANDONED'];
50
54
 
51
55
  const packageId = z.string().regex(/^[A-Za-z][A-Za-z0-9_]{0,59}$/);
52
56
  const iflowId = z.string().regex(/^[A-Za-z][A-Za-z0-9_.-]{0,99}$/);
@@ -72,7 +76,6 @@ const templateParameters = z.object({
72
76
  const scriptSchema = z.object({ name: z.string().max(90), content: z.string() }).strict();
73
77
  const artifactId = iflowId;
74
78
  const artifactVersion = z.string().regex(/^(?:active|\d+\.\d+\.\d+)$/).default('active');
75
- const messageGuid = z.string().regex(/^[A-Za-z0-9_-]{10,64}$/);
76
79
  const waitSeconds = fallback => z.number().int().min(0).max(50).default(fallback);
77
80
  // Discover (SAP standard) package ids are longer than design-time ids and may contain periods.
78
81
  const sourcePackageId = z.string().regex(/^[A-Za-z][A-Za-z0-9_.]{0,199}$/);
@@ -80,33 +83,26 @@ const securityName = z.string().regex(/^[A-Za-z0-9 _.@-]{1,150}$/);
80
83
  const partnerChars = /^[A-Za-z0-9\-._~@]+$/;
81
84
  const numberText = z.union([z.number().int().min(0).max(99_999_999_999_999), z.string().regex(/^\d{1,14}$/)]);
82
85
 
83
- // Design-time artifact kinds besides iFlows: entity set and Deploy<Kind>DesigntimeArtifact function import.
84
- const ARTIFACT_TYPES = Object.freeze({
85
- value_mapping: { set: 'ValueMappingDesigntimeArtifacts', deploy: 'DeployValueMappingDesigntimeArtifact' },
86
- script_collection: { set: 'ScriptCollectionDesigntimeArtifacts', deploy: 'DeployScriptCollectionDesigntimeArtifact' },
87
- message_mapping: { set: 'MessageMappingDesigntimeArtifacts', deploy: 'DeployMessageMappingDesigntimeArtifact' }
88
- });
89
86
  const artifactType = z.enum(Object.keys(ARTIFACT_TYPES));
90
- const PACKAGE_ARTIFACT_SETS = Object.freeze({
91
- iflows: 'IntegrationDesigntimeArtifacts',
92
- value_mappings: 'ValueMappingDesigntimeArtifacts',
93
- script_collections: 'ScriptCollectionDesigntimeArtifacts',
94
- message_mappings: 'MessageMappingDesigntimeArtifacts'
95
- });
96
87
  const RUNTIME_STATUSES = ['STARTED', 'STARTING', 'STOPPING', 'ERROR'];
97
88
  const KEYSTORES = ['system', 'backup_admin_system', 'KeyRenewal'];
98
89
  const PARTNER_CONTENT_TYPES = ['xml', 'xsl', 'xsd', 'json', 'text', 'zip', 'gz', 'zlib', 'crt'];
99
- const JMS_BROKER_FIELDS = ['Capacity', 'MaxCapacity', 'CapacityOk', 'CapacityWarning', 'CapacityError', 'QueueNumber', 'MaxQueueNumber',
100
- 'IsQueuesHigh', 'IsMessageSpoolHigh', 'IsTransactedSessionsHigh', 'IsConsumersHigh', 'IsProducersHigh'];
101
90
  const MAX_VALUES_PER_PAIR = 500;
102
- const MAX_LIST_ENTRIES = 500;
103
- const MAX_RUN_STEPS = 200;
104
- const STEP_ERROR_CAP = 1024;
91
+ const collectionInputs = { top: topSchema(200, 100), cursor: cursorSchema };
105
92
  const MAX_CERTIFICATE_BYTES = 10 * 1024;
106
93
 
107
- const time = value => Date.parse(odataDate(value)) || 0;
108
94
  const dates = (value, fields) => Object.fromEntries(fields.filter(field => value?.[field] !== undefined).map(field => [field, odataDate(value[field])]));
109
95
 
96
+ function endpointUrl(value) {
97
+ try {
98
+ const url = new URL(value);
99
+ if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password) return null;
100
+ url.search = '';
101
+ url.hash = '';
102
+ return url.href;
103
+ } catch { return null; }
104
+ }
105
+
110
106
  /** Tenant error text can echo request fields: strip the secret, including a whitespace-collapsed or truncated copy. */
111
107
  function withoutSecret(error, secret) {
112
108
  if (error?.name !== 'IntegrationError') return error;
@@ -122,8 +118,13 @@ function withoutSecret(error, secret) {
122
118
  }
123
119
 
124
120
  /** Register cpi_* tools for one CPI client. */
125
- export function registerCpiTools(register, client, { pollIntervalMs }) {
121
+ export function registerCpiTools(register, client, { pollIntervalMs, summary }) {
126
122
  const cpi = (method, path, options) => client.request(method, `${CPI_BASE}${path}`, options);
123
+ const pager = createCollectionPager(client, { root: CPI_BASE, request: cpi });
124
+ registerCpiMonitoringTools(register, client);
125
+ registerCpiSecurityTools(register, client);
126
+ registerCpiOperationalTools(register, client, { summary });
127
+ registerCpiReleaseTools(register, client, { pollIntervalMs });
127
128
  const artifactPath = (id, version = 'active') => `/IntegrationDesigntimeArtifacts(Id=${key(id)},Version=${key(version)})`;
128
129
  const runtimePath = id => `/IntegrationRuntimeArtifacts(${key(id)})`;
129
130
  const designtimePath = (type, id, version = 'active') => `/${ARTIFACT_TYPES[type].set}(Id=${key(id)},Version=${key(version)})`;
@@ -169,24 +170,27 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
169
170
  title: 'Read CPI OData API',
170
171
  service: 'Cloud Integration /api/v1',
171
172
  request: cpi,
173
+ client,
174
+ root: CPI_BASE,
172
175
  examples: "/JmsBrokers('Broker1') with $expand=QueueStates, /AccessPolicies, /KeystoreEntries with keystoreName=system, /MessageProcessingLogs('<guid>')/Runs"
173
176
  });
174
177
 
175
178
  register('cpi_list_packages', 'List CPI packages',
176
179
  'List Cloud Integration design-time packages (Id, Name, ShortText, Version, Mode).',
177
- { top: z.number().int().min(1).max(200).default(100) },
180
+ collectionInputs,
178
181
  READ_ONLY_ANNOTATIONS,
179
182
  // The CPI tenant rejects $select on IntegrationPackages (HTTP 501); project client-side.
180
- async ({ top }) => list(await cpi('GET', '/IntegrationPackages', {
181
- query: { $top: top, $orderby: 'Name' }
182
- })).map(entry => pick(entry, ['Id', 'Name', 'ShortText', 'Version', 'Mode'])));
183
+ async args => pager.page({ ...args, path: '/IntegrationPackages', orderBy: 'Name',
184
+ project: entry => pick(entry, ['Id', 'Name', 'ShortText', 'Version', 'Mode']) }));
183
185
 
184
186
  register('cpi_list_iflows', 'List iFlows in a package',
185
187
  'List the integration flows of one CPI package (Id, Name, Version, Description).',
186
- { package_id: packageId },
188
+ { package_id: packageId, ...collectionInputs },
187
189
  READ_ONLY_ANNOTATIONS,
188
- async ({ package_id }) => list(await cpi('GET', `/IntegrationPackages(${key(package_id)})/IntegrationDesigntimeArtifacts`))
189
- .map(artifact => pick(artifact, ['Id', 'Name', 'Version', 'Description'])));
190
+ async ({ package_id, ...args }) => pager.page({ ...args,
191
+ path: `/IntegrationPackages(${key(package_id)})/IntegrationDesigntimeArtifacts`,
192
+ mode: 'local', sourceComplete: false,
193
+ project: artifact => pick(artifact, ['Id', 'Name', 'Version', 'Description']) }));
190
194
 
191
195
  register('cpi_get_iflow', 'Get iFlow',
192
196
  'Read an iFlow: artifact metadata, externalized configuration and bundle files (text files capped at 48 KiB each). Pass file_path to return one file (up to 90 KiB).',
@@ -228,73 +232,17 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
228
232
 
229
233
  register('cpi_list_service_endpoints', 'List CPI service endpoints',
230
234
  'List deployed HTTP(S)/SOAP endpoint URLs; optionally only those of one iFlow. CPI registers an endpoint up to a few minutes after the iFlow reaches STARTED, so an empty list right after deployment means retry later.',
231
- { iflow_id: iflowId.optional() },
235
+ { iflow_id: iflowId.optional(), ...collectionInputs },
232
236
  READ_ONLY_ANNOTATIONS,
233
- async ({ iflow_id }) => {
234
- let endpoints = list(await cpi('GET', '/ServiceEndpoints', { query: { $expand: 'EntryPoints' } }));
235
- if (iflow_id) {
236
- const byName = endpoints.filter(endpoint => endpoint.Name === iflow_id);
237
- endpoints = byName.length ? byName : endpoints.filter(endpoint => String(endpoint.Id || '').includes(iflow_id));
238
- }
239
- return endpoints.map(endpoint => ({
237
+ async ({ iflow_id, ...args }) => pager.page({ ...args, path: '/ServiceEndpoints', mode: 'local',
238
+ query: { $expand: 'EntryPoints' }, identity: iflow_id,
239
+ filter: iflow_id ? endpoint => endpoint.Name === iflow_id || endpoint.Id === iflow_id : undefined,
240
+ project: endpoint => ({
240
241
  name: endpoint.Name,
241
- urls: list(endpoint.EntryPoints).map(entryPoint => entryPoint.Url).filter(Boolean)
242
- }));
243
- });
242
+ urls: list(endpoint.EntryPoints).map(entryPoint => endpointUrl(entryPoint.Url)).filter(Boolean)
243
+ })
244
+ }));
244
245
 
245
- register('cpi_list_message_logs', 'List message processing logs',
246
- 'List CPI message processing logs, newest first, filtered by iFlow, status, time window (ISO-8601), correlation id, application message id or one custom header property (name and value, exact match).',
247
- {
248
- iflow_id: iflowId.optional(),
249
- status: z.enum(MESSAGE_STATUSES).optional(),
250
- from: isoDate.optional(),
251
- to: isoDate.optional(),
252
- correlation_id: z.string().min(1).max(200).optional(),
253
- application_message_id: z.string().min(1).max(200).optional(),
254
- custom_header: z.object({ name: z.string().min(1).max(200), value: z.string().max(2000) }).strict().optional(),
255
- top: z.number().int().min(1).max(100).default(20)
256
- },
257
- READ_ONLY_ANNOTATIONS,
258
- async ({ iflow_id, status, from, to, correlation_id, application_message_id, custom_header, top }) => {
259
- const filter = [
260
- iflow_id && `IntegrationArtifact/Id eq ${odataKey(iflow_id)}`,
261
- status && `Status eq '${status}'`,
262
- from && `LogStart gt datetime'${isoToOdataDateTime(from)}'`,
263
- to && `LogEnd lt datetime'${isoToOdataDateTime(to)}'`,
264
- correlation_id && `CorrelationId eq ${odataKey(correlation_id)}`,
265
- application_message_id && `ApplicationMessageId eq ${odataKey(application_message_id)}`
266
- ].filter(Boolean).join(' and ');
267
- const parsed = await cpi('GET', '/MessageProcessingLogs', {
268
- query: {
269
- $filter: filter || undefined,
270
- $orderby: 'LogEnd desc',
271
- $top: top,
272
- $inlinecount: 'allpages',
273
- filterCustomHeaderProperties: custom_header ? `${odataKey(custom_header.name)} eq ${odataKey(custom_header.value)}` : undefined
274
- },
275
- unwrap: false
276
- });
277
- const logs = list(parsed?.d ?? parsed).map(log => ({
278
- ...pick(log, ['MessageGuid', 'CorrelationId', 'IntegrationFlowName', 'Status', 'CustomStatus', 'Sender', 'Receiver']),
279
- LogStart: odataDate(log.LogStart),
280
- LogEnd: odataDate(log.LogEnd)
281
- }));
282
- const total = Number(parsed?.d?.__count);
283
- return { total: Number.isFinite(total) ? total : null, returned: logs.length, logs };
284
- });
285
-
286
- register('cpi_get_message_log', 'Get message processing log',
287
- 'Read one message processing log with its error text and custom header properties. Payload attachments are never read.',
288
- { message_guid: messageGuid },
289
- READ_ONLY_ANNOTATIONS,
290
- async ({ message_guid }) => {
291
- const path = `/MessageProcessingLogs(${key(message_guid)})`;
292
- const log = entity(await cpi('GET', path));
293
- for (const field of ['LogStart', 'LogEnd']) if (field in log) log[field] = odataDate(log[field]);
294
- const errorText = await cpi('GET', `${path}/ErrorInformation/$value`, { responseType: 'text', accept: '*/*', expect: [200, 204, 404] });
295
- const headers = list(await cpi('GET', `${path}/CustomHeaderProperties`)).map(entry => pick(entry, ['Name', 'Value']));
296
- return { log, error_information: errorText ? capText(errorText, ERROR_TEXT_CAP).text : null, custom_header_properties: headers };
297
- });
298
246
 
299
247
  register('cpi_create_package', 'Create CPI package',
300
248
  'Create a Cloud Integration package. Requires confirmation exactly equal to CREATE CPI PACKAGE <package_id>.',
@@ -369,7 +317,7 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
369
317
  });
370
318
 
371
319
  register('cpi_update_iflow', 'Update iFlow',
372
- 'Replace or add files under src/main/resources in an iFlow bundle, merge externalized parameters, and bump the patch version. Fails without writing when expected_version differs from the current version. Never deletes bundle files. Requires confirmation exactly equal to UPDATE IFLOW <iflow_id>.',
320
+ 'Replace/add bundle resources in the active draft and bump its bundle version, preserving fresh configured values below explicit parameter overrides. Reapply surviving configuration after PUT and report exact partial progress. No retained history is created. Fails before writing on expected_version mismatch. Exact confirmation: UPDATE IFLOW <iflow_id>.',
373
321
  {
374
322
  iflow_id: iflowId,
375
323
  expected_version: z.string().min(1).max(40),
@@ -393,18 +341,22 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
393
341
  }
394
342
  const newVersion = bumpPatchVersion(current);
395
343
  const bundle = readBundle(await cpi('GET', `${path}/$value`, { responseType: 'bytes', accept: 'application/zip, application/octet-stream' }));
344
+ const previousDefaults = bundle.has(PARAMETERS_PROP) ? parseProperties(strFromU8(bundle.get(PARAMETERS_PROP))) : {};
396
345
  const changed = new Set();
397
346
  for (const file of files) {
398
347
  bundle.set(file.path, strToU8(file.content));
399
348
  changed.add(file.path);
400
349
  }
401
- if (parameters && Object.keys(parameters).length) {
402
- const existing = bundle.has(PARAMETERS_PROP) ? parseProperties(strFromU8(bundle.get(PARAMETERS_PROP))) : {};
403
- const merged = { ...existing, ...parameters };
404
- validateParameters(merged);
405
- bundle.set(PARAMETERS_PROP, strToU8(propertiesText(merged)));
350
+ // Read tenant configuration immediately before the draft replacement, not stale bundled defaults.
351
+ const configured = await readCpiConfigurations(client, iflow_id);
352
+ const defaults = bundle.has(PARAMETERS_PROP) ? parseProperties(strFromU8(bundle.get(PARAMETERS_PROP))) : {};
353
+ const removed = new Set(Object.keys(previousDefaults).filter(key => !Object.hasOwn(defaults, key) && !Object.hasOwn(parameters ?? {}, key)));
354
+ const retained = configured.filter(entry => !removed.has(entry.ParameterKey));
355
+ const effective = { ...defaults, ...Object.fromEntries(retained.map(entry => [entry.ParameterKey, entry.ParameterValue])), ...parameters };
356
+ if (retained.length || Object.keys(parameters ?? {}).length) {
357
+ bundle.set(PARAMETERS_PROP, strToU8(propertiesText(effective)));
406
358
  const propdef = bundle.has(PARAMETERS_PROPDEF) ? strFromU8(bundle.get(PARAMETERS_PROPDEF)) : '';
407
- const updatedPropdef = mergePropdef(propdef, Object.keys(merged));
359
+ const updatedPropdef = mergePropdef(propdef, Object.keys(effective));
408
360
  if (updatedPropdef !== propdef) {
409
361
  bundle.set(PARAMETERS_PROPDEF, strToU8(updatedPropdef));
410
362
  changed.add(PARAMETERS_PROPDEF);
@@ -414,11 +366,35 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
414
366
  if (!bundle.has(MANIFEST_PATH)) throw new IntegrationError(`iFlow ${iflow_id} bundle has no ${MANIFEST_PATH}`);
415
367
  bundle.set(MANIFEST_PATH, strToU8(replaceManifestVersion(strFromU8(bundle.get(MANIFEST_PATH)), newVersion)));
416
368
  changed.add(MANIFEST_PATH);
417
- await cpi('PUT', path, {
418
- json: { Name: name ?? artifact.Name, ArtifactContent: Buffer.from(writeBundle(bundle)).toString('base64') },
419
- expect: [200, 202, 204]
369
+ const reapply = new Map(retained.map(entry => [entry.ParameterKey, entry]));
370
+ for (const [ParameterKey, ParameterValue] of Object.entries(parameters ?? {})) reapply.set(ParameterKey, {
371
+ ParameterKey, ParameterValue, DataType: reapply.get(ParameterKey)?.DataType ?? 'xsd:string'
420
372
  });
421
- return { updated: true, previous_version: current, new_version: newVersion, changed_files: [...changed] };
373
+ const parameter_names = { source_defaults: Object.keys(defaults).sort(), configured: [...reapply.keys()].sort(),
374
+ explicit_overrides: Object.keys(parameters ?? {}).sort() };
375
+ // Preserve exact progress within the output budget; reject before the accepted content write otherwise.
376
+ if (Buffer.byteLength(JSON.stringify(parameter_names)) > 40000 || Buffer.byteLength(JSON.stringify([...reapply.keys()])) > 24000) {
377
+ throw new IntegrationError('CPI parameter names exceed the safe mutation progress-report limit');
378
+ }
379
+ const result = { updated: true, content_updated: true, previous_version: current, new_version: newVersion,
380
+ retained_version_created: false, changed_files: [...changed], parameter_names };
381
+ try {
382
+ await cpi('PUT', path, {
383
+ json: { Name: name ?? artifact.Name, ArtifactContent: Buffer.from(writeBundle(bundle)).toString('base64') },
384
+ expect: [200, 202, 204], responseType: 'none', accept: '*/*'
385
+ });
386
+ } catch (error) {
387
+ const status = Number(error?.status);
388
+ const accepted = status >= 200 && status < 300 ? true : status >= 400 ? false : null;
389
+ return { ...result, updated: accepted, content_updated: accepted, outcome: 'unverified',
390
+ configuration: { updated: [], failed: null, attempted: false }, source_result: cpiReleaseFailure(error) };
391
+ }
392
+ try {
393
+ const configuration = await writeCpiConfigurations(client, iflow_id, [...reapply.values()]);
394
+ return { ...result, configuration, outcome: configuration.failed ? 'partial' : 'succeeded' };
395
+ } catch (error) {
396
+ return { ...result, outcome: 'partial', configuration: { updated: [], failed: cpiReleaseFailure(error) } };
397
+ }
422
398
  });
423
399
 
424
400
  register('cpi_update_iflow_configuration', 'Configure iFlow parameters',
@@ -435,84 +411,18 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
435
411
  OVERWRITE_ANNOTATIONS,
436
412
  async ({ iflow_id, parameters, confirmation: confirmed }) => {
437
413
  requireConfirmation(confirmed, `CONFIGURE IFLOW ${iflow_id}`);
438
- const updated = [];
439
- for (const parameter of parameters) {
440
- try {
441
- await cpi('PUT', `${artifactPath(iflow_id)}/$links/Configurations(${key(parameter.key)})`, {
442
- json: { ParameterValue: parameter.value, DataType: parameter.data_type },
443
- expect: [200, 202, 204]
444
- });
445
- updated.push(parameter.key);
446
- } catch (error) {
447
- return { updated, failed: { key: parameter.key, error: toolError(error).content[0].text } };
448
- }
449
- }
450
- return { updated, failed: null };
414
+ return writeCpiConfigurations(client, iflow_id, parameters.map(parameter => ({
415
+ ParameterKey: parameter.key, ParameterValue: parameter.value, DataType: parameter.data_type
416
+ })));
451
417
  });
452
418
 
453
- // MCP clients commonly abort a tool call after 60 s, so the wait stays below that;
454
- // a long STARTING phase is reported as timed_out and continued with cpi_get_iflow_runtime_status.
455
- register('cpi_deploy_iflow', 'Deploy iFlow',
456
- 'Deploy the active iFlow version and wait (up to wait_seconds, max 50) for build and runtime status; returns runtime error text when it fails to start. When timed_out is true, poll cpi_get_iflow_runtime_status. Requires confirmation exactly equal to DEPLOY IFLOW <iflow_id>.',
457
- { iflow_id: iflowId, wait_seconds: z.number().int().min(0).max(50).default(45), confirmation },
458
- CREATE_ANNOTATIONS,
459
- async ({ iflow_id, wait_seconds, confirmation: confirmed }) => {
460
- requireConfirmation(confirmed, `DEPLOY IFLOW ${iflow_id}`);
461
- const deadline = Date.now() + wait_seconds * 1000;
462
- const taskId = String(await cpi('POST', '/DeployIntegrationDesigntimeArtifact', {
463
- query: { Id: odataKey(iflow_id), Version: "'active'" },
464
- responseType: 'text',
465
- accept: '*/*',
466
- expect: [200, 202]
467
- }) ?? '').trim().replace(/^"|"$/g, '');
468
- if (!taskId) throw new IntegrationError(`CPI deploy of ${iflow_id} returned no task id`);
469
-
470
- let buildStatus;
471
- for (;;) {
472
- const build = await cpi('GET', `/BuildAndDeployStatus(TaskId=${key(taskId)})`);
473
- buildStatus = String(build?.Status ?? '').toUpperCase();
474
- if (buildStatus !== 'DEPLOYING' || Date.now() >= deadline) break;
475
- await sleep(pollIntervalMs);
476
- }
477
- const result = { task_id: taskId, build_status: buildStatus, runtime_status: null, runtime_version: null, error_information: null, timed_out: false };
478
- const readRuntime = async () => {
479
- const runtime = await cpi('GET', runtimePath(iflow_id), { expect: [200, 404], withStatus: true });
480
- result.runtime_status = runtime.status === 404 ? null : String(runtime.data?.Status ?? '').toUpperCase();
481
- result.runtime_version = runtime.status === 404 ? null : runtime.data?.Version ?? null;
482
- };
483
- if (buildStatus === 'DEPLOYING') {
484
- return { ...result, timed_out: true, note: 'Build still running; poll cpi_get_iflow_runtime_status.' };
485
- }
486
- if (buildStatus !== 'SUCCESS') {
487
- // CPI occasionally reports a transient build FAIL for a deploy that does start; report the runtime truth too.
488
- await readRuntime();
489
- if (result.runtime_status === 'ERROR') result.error_information = await runtimeError(iflow_id);
490
- result.note = `CPI reported build status ${buildStatus || 'unknown'}; runtime_status/runtime_version show the iFlow currently deployed. Check cpi_get_iflow_runtime_status before redeploying.`;
491
- return result;
492
- }
493
-
494
- for (;;) {
495
- await readRuntime();
496
- const pending = result.runtime_status === null || result.runtime_status === 'STARTING';
497
- if (!pending) break;
498
- if (Date.now() >= deadline) {
499
- result.timed_out = true;
500
- result.note = 'Runtime still starting; poll cpi_get_iflow_runtime_status.';
501
- break;
502
- }
503
- await sleep(pollIntervalMs);
504
- }
505
- if (result.runtime_status === 'ERROR') result.error_information = await runtimeError(iflow_id);
506
- return result;
507
- });
508
419
 
509
420
  register('cpi_list_runtime_artifacts', 'List deployed CPI artifacts',
510
421
  'List deployed (runtime) Cloud Integration artifacts of every type (iFlows, value mappings, script collections, message mappings, …) with status and deployment time; optionally only one status.',
511
- { status: z.enum(RUNTIME_STATUSES).optional(), top: topSchema(500, 200) },
422
+ { status: z.enum(RUNTIME_STATUSES).optional(), top: topSchema(500, 200), cursor: cursorSchema },
512
423
  READ_ONLY_ANNOTATIONS,
513
- async ({ status, top }) => list(await cpi('GET', '/IntegrationRuntimeArtifacts', {
514
- query: { $filter: status ? `Status eq '${status}'` : undefined, $top: top }
515
- })).map(runtimeSummary));
424
+ async ({ status, ...args }) => pager.page({ ...args, path: '/IntegrationRuntimeArtifacts', orderBy: 'Id',
425
+ query: { $filter: status ? `Status eq '${status}'` : undefined }, project: runtimeSummary }));
516
426
 
517
427
  register('cpi_undeploy_artifact', 'Undeploy CPI artifact',
518
428
  'Undeploy a runtime artifact of any type and wait (up to wait_seconds, max 50) until it is gone; the design-time artifact is kept. When timed_out is true, poll cpi_list_runtime_artifacts. Requires confirmation exactly equal to UNDEPLOY ARTIFACT <artifact_id>.',
@@ -542,14 +452,16 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
542
452
 
543
453
  register('cpi_list_package_artifacts', 'List artifacts in a package',
544
454
  'List the iFlows, value mappings, script collections and message mappings of one CPI package (Id, Name, Version, Description), grouped by type.',
545
- { package_id: packageId },
455
+ z.object({ package_id: packageId, type: z.enum(Object.keys(PACKAGE_ARTIFACT_SETS)).optional(), ...collectionInputs }).strict()
456
+ .refine(value => !value.cursor || value.type, { path: ['cursor'], message: 'cursor requires a concrete artifact type' }),
546
457
  READ_ONLY_ANNOTATIONS,
547
- async ({ package_id }) => {
458
+ async ({ package_id, type, ...args }) => {
459
+ const read = group => pager.page({ ...args, maxBytes: type ? 95 * 1024 : 22 * 1024, path: `/IntegrationPackages(${key(package_id)})/${PACKAGE_ARTIFACT_SETS[group]}`,
460
+ mode: 'local', sourceComplete: false,
461
+ project: artifact => pick(artifact, ['Id', 'Name', 'Version', 'Description']) });
462
+ if (type) return read(type);
548
463
  const result = {};
549
- for (const [group, set] of Object.entries(PACKAGE_ARTIFACT_SETS)) {
550
- result[group] = list(await cpi('GET', `/IntegrationPackages(${key(package_id)})/${set}`))
551
- .map(artifact => pick(artifact, ['Id', 'Name', 'Version', 'Description']));
552
- }
464
+ for (const group of Object.keys(PACKAGE_ARTIFACT_SETS)) result[group] = await read(group);
553
465
  return result;
554
466
  });
555
467
 
@@ -565,26 +477,42 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
565
477
  });
566
478
 
567
479
  register('cpi_get_value_mappings', 'Get value mapping values',
568
- `Read the agency/identifier pairs of a value mapping with their value pairs (up to ${MAX_VALUES_PER_PAIR} per agency pair) and default values.`,
480
+ `Read bounded agency/identifier pairs of a value mapping (up to 20 pairs and ${MAX_VALUES_PER_PAIR} values/defaults per pair, subject to the response byte budget). Nested collection completeness is unverified; no continuation route is inferred.`,
569
481
  { artifact_id: artifactId, version: artifactVersion },
570
482
  READ_ONLY_ANNOTATIONS,
571
483
  async ({ artifact_id, version }) => {
572
484
  const path = designtimePath('value_mapping', artifact_id, version);
573
485
  const valMap = entry => ({ Id: entry.Id, SrcValue: entry.Value?.SrcValue ?? null, TgtValue: entry.Value?.TgtValue ?? null });
486
+ // These nested APIs have no verified traversal contract. Keep source counts, but never
487
+ // turn a capped or continuation-bearing response into an exhaustive value inventory.
488
+ const bounded = async (nestedPath, project, top, maxBytes) => {
489
+ const page = await pager.page({ path: nestedPath, mode: 'local', sourceComplete: false, project, top, maxBytes });
490
+ const envelope = { ...page, next_cursor: null, complete: false, stop_reason: 'source_limit' };
491
+ // Account for the actual pretty-printed result at its deepest nesting level.
492
+ const fits = count => Buffer.byteLength(JSON.stringify({ ...envelope, items: page.items.slice(0, count), returned: count }, null, 2)
493
+ .replaceAll('\n', '\n ')) <= maxBytes;
494
+ let low = 0;
495
+ let high = page.items.length;
496
+ while (low < high) {
497
+ const middle = Math.ceil((low + high) / 2);
498
+ if (fits(middle)) low = middle;
499
+ else high = middle - 1;
500
+ }
501
+ return { ...envelope, items: page.items.slice(0, low), returned: low };
502
+ };
503
+ const schemas = await bounded(`${path}/ValMapSchema`,
504
+ schema => pick(schema, ['SrcAgency', 'SrcId', 'TgtAgency', 'TgtId', 'State']), 20, 8 * 1024);
574
505
  const pairs = [];
575
- for (const schema of list(await cpi('GET', `${path}/ValMapSchema`))) {
506
+ const valueBudget = Math.floor(76 * 1024 / (Math.max(1, schemas.items.length) * 2));
507
+ for (const schema of schemas.items) {
576
508
  const pairPath = `${path}/ValMapSchema(SrcAgency=${key(schema.SrcAgency)},SrcId=${key(schema.SrcId)},TgtAgency=${key(schema.TgtAgency)},TgtId=${key(schema.TgtId)})`;
577
- const values = list(await cpi('GET', `${pairPath}/ValMaps`)).map(valMap);
578
- const defaults = list(await cpi('GET', `${pairPath}/DefaultValMaps`)).map(valMap);
579
509
  pairs.push({
580
- ...pick(schema, ['SrcAgency', 'SrcId', 'TgtAgency', 'TgtId', 'State']),
581
- value_count: values.length,
582
- values: values.slice(0, MAX_VALUES_PER_PAIR),
583
- ...(values.length > MAX_VALUES_PER_PAIR ? { values_truncated: true } : {}),
584
- default_values: defaults
510
+ ...schema,
511
+ values: await bounded(`${pairPath}/ValMaps`, valMap, MAX_VALUES_PER_PAIR, valueBudget),
512
+ default_values: await bounded(`${pairPath}/DefaultValMaps`, valMap, MAX_VALUES_PER_PAIR, valueBudget)
585
513
  });
586
514
  }
587
- return { artifact_id, version, agency_pairs: pairs };
515
+ return { artifact_id, version, agency_pairs: { ...schemas, items: pairs } };
588
516
  });
589
517
 
590
518
  const agencyField = z.string().min(1).max(200);
@@ -667,37 +595,6 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
667
595
  return { updated: true, artifact_id, version: artifact?.Version ?? null, changed_files: [...changed] };
668
596
  });
669
597
 
670
- register('cpi_deploy_artifact', 'Deploy CPI artifact',
671
- 'Deploy the active version of a value mapping, script collection or message mapping and wait (up to wait_seconds, max 50) until the runtime artifact leaves STARTING; returns runtime error text when it fails. A redeploy can briefly report the previously deployed artifact as STARTED, so check runtime_version and deployed_on. When timed_out is true, poll cpi_list_runtime_artifacts. Requires confirmation exactly equal to DEPLOY ARTIFACT <artifact_id>.',
672
- { type: artifactType, artifact_id: artifactId, wait_seconds: waitSeconds(45), confirmation },
673
- CREATE_ANNOTATIONS,
674
- async ({ type, artifact_id, wait_seconds, confirmation: confirmed }) => {
675
- requireConfirmation(confirmed, `DEPLOY ARTIFACT ${artifact_id}`);
676
- const deadline = Date.now() + wait_seconds * 1000;
677
- await cpi('POST', `/${ARTIFACT_TYPES[type].deploy}`, {
678
- query: { Id: odataKey(artifact_id), Version: "'active'" },
679
- responseType: 'text',
680
- accept: '*/*',
681
- expect: [200, 201, 202]
682
- });
683
- const result = { type, artifact_id, runtime_status: null, runtime_version: null, deployed_on: null, error_information: null, timed_out: false };
684
- for (;;) {
685
- const runtime = await cpi('GET', runtimePath(artifact_id), { expect: [200, 404], withStatus: true });
686
- const deployed = runtime.status === 200;
687
- result.runtime_status = deployed ? String(runtime.data?.Status ?? '').toUpperCase() : null;
688
- result.runtime_version = deployed ? runtime.data?.Version ?? null : null;
689
- result.deployed_on = deployed ? odataDate(runtime.data?.DeployedOn) ?? null : null;
690
- if (deployed && result.runtime_status !== 'STARTING') break;
691
- if (Date.now() >= deadline) {
692
- result.timed_out = true;
693
- result.note = 'Runtime artifact not started yet; poll cpi_list_runtime_artifacts.';
694
- break;
695
- }
696
- await sleep(pollIntervalMs);
697
- }
698
- if (result.runtime_status === 'ERROR') result.error_information = await runtimeError(artifact_id);
699
- return result;
700
- });
701
598
 
702
599
  register('cpi_update_package', 'Update CPI package',
703
600
  'Change the name, short text, description or version of a CPI package; omitted fields keep their current values. Requires confirmation exactly equal to UPDATE CPI PACKAGE <package_id>.',
@@ -740,7 +637,7 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
740
637
  try {
741
638
  copied = await cpi('POST', '/CopyIntegrationPackage', {
742
639
  query: { Id: odataKey(package_id), ImportMode: `'${import_mode}'`, Suffix: suffix ? `'${suffix}'` : undefined },
743
- expect: [200, 201]
640
+ expect: [200, 201], withStatus: true, responseType: 'text', accept: 'application/json'
744
641
  });
745
642
  } catch (error) {
746
643
  if (/\(HTTP 409\)/.test(error?.message)) {
@@ -748,7 +645,14 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
748
645
  }
749
646
  throw error;
750
647
  }
751
- return { copied: true, import_mode, package: pick(copied ?? {}, ['Id', 'Name', 'Version']) };
648
+ let metadata;
649
+ try {
650
+ const body = JSON.parse(copied.data);
651
+ metadata = entity(body?.d ?? body);
652
+ } catch { /* HTTP acceptance is still evidence when the reply is not usable JSON. */ }
653
+ const identified = typeof metadata?.Id === 'string' && metadata.Id.length > 0;
654
+ return { accepted: true, copied: identified, outcome: identified ? 'succeeded' : 'unverified',
655
+ http_status: copied.status, import_mode, package: pick(metadata, ['Id', 'Name', 'Version']) };
752
656
  });
753
657
 
754
658
  // ---------------------------------------------------------------- security material
@@ -758,26 +662,28 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
758
662
  'List user credentials, OAuth2 client credentials or keystore entries (keystore system, backup_admin_system or KeyRenewal; default system) with deployment status. Passwords, client secrets and keys are never returned.',
759
663
  z.object({
760
664
  kind: z.enum(['user_credentials', 'oauth2_client_credentials', 'keystore_entries']),
761
- keystore: z.enum(KEYSTORES).optional()
665
+ keystore: z.enum(KEYSTORES).optional(),
666
+ ...collectionInputs
762
667
  }).strict().refine(value => !value.keystore || value.kind === 'keystore_entries', { path: ['keystore'], message: 'keystore is only allowed with kind keystore_entries' }),
763
668
  READ_ONLY_ANNOTATIONS,
764
- async ({ kind, keystore }) => {
669
+ async ({ kind, keystore, ...args }) => {
765
670
  if (kind === 'user_credentials') {
766
- return redactSecrets(list(await cpi('GET', '/UserCredentials')).map(entry => ({
767
- ...pick(entry, ['Name', 'Kind', 'Description', 'User', 'CompanyId']),
671
+ return pager.page({ ...args, path: '/UserCredentials', mode: 'local', project: entry => redactSecrets({
672
+ ...pick(entry, ['Name', 'Kind', 'Description']),
768
673
  deployment: deployment(entry.SecurityArtifactDescriptor)
769
- })));
674
+ }) });
770
675
  }
771
676
  if (kind === 'oauth2_client_credentials') {
772
- return redactSecrets(list(await cpi('GET', '/OAuth2ClientCredentials')).map(entry => ({
773
- ...pick(entry, ['Name', 'Description', 'TokenServiceUrl', 'ClientId', 'ClientAuthentication', 'Scope', 'ScopeContentType', 'Resource', 'Audience']),
677
+ return pager.page({ ...args, path: '/OAuth2ClientCredentials', mode: 'local', project: entry => redactSecrets({
678
+ ...pick(entry, ['Name', 'Description', 'ClientAuthentication', 'ScopeContentType']),
774
679
  deployment: deployment(entry.SecurityArtifactDescriptor)
775
- })));
680
+ }) });
776
681
  }
777
- return redactSecrets(list(await cpi('GET', '/KeystoreEntries', { query: { keystoreName: keystore ?? 'system' } })).map(entry => ({
778
- ...pick(entry, ['Alias', 'Hexalias', 'Type', 'Owner', 'KeyType', 'SubjectDN', 'IssuerDN', 'Status']),
779
- ...dates(entry, ['ValidNotBefore', 'ValidNotAfter'])
780
- })));
682
+ return pager.page({ ...args, path: '/KeystoreEntries', mode: 'local', query: { keystoreName: keystore ?? 'system' },
683
+ project: entry => ({
684
+ ...pick(entry, ['Alias', 'Hexalias', 'Type', 'KeyType', 'Status']),
685
+ ...dates(entry, ['ValidNotBefore', 'ValidNotAfter'])
686
+ }) });
781
687
  });
782
688
 
783
689
  register('cpi_set_user_credential', 'Set CPI user credential',
@@ -816,9 +722,9 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
816
722
  token_service_url: z.string().max(2048).regex(/^https:\/\/\S+$/, 'must be an https URL'),
817
723
  client_id: z.string().min(1).max(1024),
818
724
  client_secret: secretValue,
819
- client_authentication: z.enum(['body', 'header']).default('body'),
725
+ client_authentication: z.enum(['body', 'header']).optional(),
820
726
  scope: z.string().max(2048).optional(),
821
- scope_content_type: z.enum(['urlencoded', 'json']).default('urlencoded'),
727
+ scope_content_type: z.enum(['urlencoded', 'json']).optional(),
822
728
  resource: z.string().max(2048).optional(),
823
729
  audience: z.string().max(2048).optional(),
824
730
  description: z.string().max(1024).optional(),
@@ -834,15 +740,16 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
834
740
  TokenServiceUrl: token_service_url,
835
741
  ClientId: client_id,
836
742
  ClientSecret: client_secret,
837
- ClientAuthentication: client_authentication,
838
- Scope: scope ?? '',
839
- ScopeContentType: scope_content_type,
840
- Resource: resource ?? '',
841
- Audience: audience ?? ''
743
+ ClientAuthentication: client_authentication ?? existing?.ClientAuthentication ?? 'body',
744
+ Scope: scope ?? existing?.Scope ?? '',
745
+ ScopeContentType: scope_content_type ?? existing?.ScopeContentType ?? 'urlencoded',
746
+ Resource: resource ?? existing?.Resource ?? '',
747
+ Audience: audience ?? existing?.Audience ?? ''
842
748
  }));
843
749
  return { [outcome]: true, name };
844
750
  } catch (error) {
845
- throw withoutSecret(error, client_secret);
751
+ const evidence = cpiReleaseFailure(error);
752
+ throw new IntegrationError(`CPI OAuth2 client credential write failed${evidence.status ? ` (HTTP ${evidence.status})` : ''}; tenant response withheld`, evidence);
846
753
  }
847
754
  });
848
755
 
@@ -859,6 +766,10 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
859
766
  OVERWRITE_ANNOTATIONS,
860
767
  async ({ alias, certificate, update, confirmation: confirmed }) => {
861
768
  requireConfirmation(confirmed, `UPLOAD CERTIFICATE ${alias}`);
769
+ let parsedCertificate;
770
+ try { parsedCertificate = new X509Certificate(certificate); }
771
+ catch { throw new IntegrationError('Malformed PEM certificate'); }
772
+ const fingerprint_sha256 = parsedCertificate.fingerprint256.replaceAll(':', '').toLowerCase();
862
773
  // Keystore keys are the hex of the UTF-8 alias because slashes are not allowed in the URI even when encoded.
863
774
  const hexalias = Buffer.from(alias, 'utf8').toString('hex');
864
775
  try {
@@ -870,22 +781,31 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
870
781
  expect: [200, 204]
871
782
  });
872
783
  } catch (error) {
873
- if (!update && /\(HTTP 400\)/.test(error?.message)) throw new IntegrationError(`Keystore alias ${alias} already exists; pass update: true to replace it`);
784
+ if (!update && (error?.status === 400 || /\(HTTP 400\)/.test(error?.message))) {
785
+ let exists = /\b(?:entry|alias|certificate)\b.{0,80}\b(?:already exists|exists already|exists)\b/i.test(error.message);
786
+ if (!exists) {
787
+ try {
788
+ const fresh = await cpi('GET', '/KeystoreEntries', { query: { keystoreName: 'system' } });
789
+ exists = list(fresh).some(entry => entry.Alias === alias || entry.Hexalias === hexalias);
790
+ } catch { /* Preserve the original upload cause when alias evidence is unavailable. */ }
791
+ }
792
+ if (exists) throw new IntegrationError(`Keystore alias ${alias} already exists; pass update: true to replace it`);
793
+ }
874
794
  throw error;
875
795
  }
876
- return { uploaded: true, alias, hexalias, replaced: update };
796
+ return { uploaded: true, alias, hexalias, replaced: update, fingerprint_sha256, fingerprint_source: 'local_X509Certificate', tenant_fingerprint_verified: false };
877
797
  });
878
798
 
879
799
  // ---------------------------------------------------------------- number ranges, partner directory
880
800
 
881
801
  register('cpi_list_number_ranges', 'List CPI number ranges',
882
802
  'List number range objects (Name, Description, MinValue, MaxValue, CurrentValue = next value, Rotate, FieldLength, deployment).',
883
- {},
803
+ collectionInputs,
884
804
  READ_ONLY_ANNOTATIONS,
885
- async () => list(await cpi('GET', '/NumberRanges')).map(range => ({
886
- ...pick(range, ['Name', 'Description', 'MinValue', 'MaxValue', 'CurrentValue', 'Rotate', 'FieldLength', 'DeployedBy']),
805
+ async args => pager.page({ ...args, path: '/NumberRanges', mode: 'local', project: range => ({
806
+ ...pick(range, ['Name', 'Description', 'MinValue', 'MaxValue', 'CurrentValue', 'Rotate', 'FieldLength']),
887
807
  ...dates(range, ['DeployedOn'])
888
- })));
808
+ }) }));
889
809
 
890
810
  register('cpi_set_number_range', 'Set CPI number range',
891
811
  'Create or replace a number range object. Values are non-negative integers of at most 14 digits with min_value ≤ current_value ≤ max_value; current_value is the next value handed out (default: kept on update, min_value on create). field_length 0 means no zero padding. Requires confirmation exactly equal to SET NUMBER RANGE <name>.',
@@ -931,15 +851,19 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
931
851
  });
932
852
 
933
853
  register('cpi_list_partner_parameters', 'List Partner Directory parameters',
934
- 'List Partner Directory string parameters (Pid, Id, Value) and binary parameter metadata (Pid, Id, ContentType; content is not returned), optionally for one partner id.',
935
- { pid: z.string().max(60).regex(partnerChars).optional(), top: topSchema(500, 200) },
854
+ 'List Partner Directory parameter identities and binary content types, optionally for one partner id. Parameter values and binary content are not returned.',
855
+ z.object({ pid: z.string().max(60).regex(partnerChars).optional(), type: z.enum(['string', 'binary', 'all']).default('all'), ...collectionInputs }).strict()
856
+ .refine(value => !value.cursor || value.type !== 'all', { path: ['cursor'], message: 'cursor requires string or binary type' }),
936
857
  READ_ONLY_ANNOTATIONS,
937
- async ({ pid, top }) => {
938
- const query = { $filter: pid ? `Pid eq ${odataKey(pid)}` : undefined, $top: top };
939
- const audit = entry => ({ ...pick(entry, ['LastModifiedBy']), ...dates(entry, ['LastModifiedTime']) });
940
- const strings = list(await cpi('GET', '/StringParameters', { query })).map(entry => ({ ...pick(entry, ['Pid', 'Id', 'Value']), ...audit(entry) }));
941
- const binaries = list(await cpi('GET', '/BinaryParameters', { query })).map(entry => ({ ...pick(entry, ['Pid', 'Id', 'ContentType']), ...audit(entry) }));
942
- return { string_parameters: strings, binary_parameters: binaries };
858
+ async ({ pid, type, ...args }) => {
859
+ const query = { $filter: pid ? `Pid eq ${odataKey(pid)}` : undefined };
860
+ const read = selected => pager.page({ ...args, maxBytes: type === 'all' ? 46 * 1024 : 95 * 1024, path: selected === 'string' ? '/StringParameters' : '/BinaryParameters',
861
+ query, orderBy: 'Pid,Id', project: entry => ({
862
+ ...pick(entry, selected === 'string' ? ['Pid', 'Id'] : ['Pid', 'Id', 'ContentType']),
863
+ ...dates(entry, ['LastModifiedTime'])
864
+ }) });
865
+ if (type !== 'all') return read(type);
866
+ return { string_parameters: await read('string'), binary_parameters: await read('binary') };
943
867
  });
944
868
 
945
869
  register('cpi_set_partner_parameter', 'Set Partner Directory parameter',
@@ -973,65 +897,6 @@ export function registerCpiTools(register, client, { pollIntervalMs }) {
973
897
  return { [outcome]: true, pid, id, type };
974
898
  });
975
899
 
976
- // ---------------------------------------------------------------- data stores, variables, JMS (the data store APIs accept no query options)
977
-
978
- register('cpi_list_data_stores', 'List CPI data stores',
979
- 'List data stores with their iFlow, type, visibility and message counts; optionally only stores with overdue entries.',
980
- { overdue_only: z.boolean().default(false) },
981
- READ_ONLY_ANNOTATIONS,
982
- async ({ overdue_only }) => list(await cpi('GET', '/DataStores', { query: { overdueonly: overdue_only ? 'true' : undefined } }))
983
- .map(store => pick(store, ['DataStoreName', 'IntegrationFlow', 'Type', 'Visibility', 'NumberOfMessages', 'NumberOfOverdueMessages'])));
984
-
985
- register('cpi_list_data_store_entries', 'List data store entries',
986
- `List entries of one data store (Id, Status, MessageId, due/created/retain dates; up to ${MAX_LIST_ENTRIES}). Entry payloads are never read. type is empty for Global and iFlow data stores.`,
987
- { data_store: z.string().min(1).max(200), integration_flow: z.string().min(1).max(200), type: z.string().max(40).default('') },
988
- READ_ONLY_ANNOTATIONS,
989
- async ({ data_store, integration_flow, type }) => {
990
- const entries = list(await cpi('GET', `/DataStores(DataStoreName=${key(data_store)},IntegrationFlow=${key(integration_flow)},Type=${key(type)})/Entries`))
991
- .map(entry => ({ ...pick(entry, ['Id', 'Status', 'MessageId']), ...dates(entry, ['DueAt', 'CreatedAt', 'RetainUntil']) }));
992
- return { total: entries.length, returned: Math.min(entries.length, MAX_LIST_ENTRIES), entries: entries.slice(0, MAX_LIST_ENTRIES) };
993
- });
994
-
995
- register('cpi_list_variables', 'List CPI variables',
996
- 'List global and iFlow variables (name, iFlow, visibility, updated/retain dates). Values are never read.',
997
- {},
998
- READ_ONLY_ANNOTATIONS,
999
- async () => list(await cpi('GET', '/Variables'))
1000
- .map(variable => ({ ...pick(variable, ['VariableName', 'IntegrationFlow', 'Visibility']), ...dates(variable, ['UpdatedAt', 'RetainUntil']) })));
1001
-
1002
- register('cpi_get_jms_broker_status', 'Get JMS broker status',
1003
- 'Return JMS broker capacity and limit flags (0 ok, 1 over limit) and the queues in warning (1) or critical (2) state.',
1004
- {},
1005
- READ_ONLY_ANNOTATIONS,
1006
- async () => {
1007
- const broker = await cpi('GET', `/JmsBrokers(${key('Broker1')})`, { query: { $expand: 'QueueStates' } });
1008
- const queues = list(broker?.QueueStates);
1009
- const unhealthy = queues.filter(queue => Number(queue.State) !== 0).map(queue => pick(queue, ['Name', 'State']));
1010
- return { broker: pick(broker, JMS_BROKER_FIELDS), queue_count: queues.length, unhealthy_queue_count: unhealthy.length, unhealthy_queues: unhealthy };
1011
- });
1012
-
1013
- register('cpi_get_message_log_steps', 'Get message processing steps',
1014
- `Return the processing steps of the latest run of a message (StepId, ModelStepId, Activity, Status, Error, start/stop), ordered by start time, up to ${MAX_RUN_STEPS}. Payloads are never read.`,
1015
- { message_guid: messageGuid },
1016
- READ_ONLY_ANNOTATIONS,
1017
- async ({ message_guid }) => {
1018
- const runs = list(await cpi('GET', `/MessageProcessingLogs(${key(message_guid)})/Runs`));
1019
- if (!runs.length) return { run_count: 0, run: null, total_steps: 0, steps: [] };
1020
- const latest = runs.reduce((best, run) => (time(run.RunStart) > time(best.RunStart) ? run : best));
1021
- const steps = list(await cpi('GET', `/MessageProcessingLogRuns(${key(latest.Id)})/RunSteps`))
1022
- .sort((left, right) => time(left.StepStart) - time(right.StepStart));
1023
- return {
1024
- run_count: runs.length,
1025
- run: { ...pick(latest, ['Id', 'OverallState', 'LogLevel']), ...dates(latest, ['RunStart', 'RunStop']) },
1026
- total_steps: steps.length,
1027
- ...(steps.length > MAX_RUN_STEPS ? { truncated: true } : {}),
1028
- steps: steps.slice(0, MAX_RUN_STEPS).map(step => ({
1029
- ...pick(step, ['StepId', 'ModelStepId', 'Activity', 'Status']),
1030
- Error: step.Error ? capText(String(step.Error), STEP_ERROR_CAP).text : null,
1031
- ...dates(step, ['StepStart', 'StepStop'])
1032
- }))
1033
- };
1034
- });
1035
900
  }
1036
901
 
1037
902
  /** Names of every cpi_* tool, in registration order. */