sap-ai-dev-toolkit 0.9.9912 → 0.9.9914

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 (37) hide show
  1. package/.github/agents/abap-developer.agent.md +1 -1
  2. package/.github/agents/sap-integration-architect.agent.md +38 -0
  3. package/.github/agents/sap-integration-developer.agent.md +31 -0
  4. package/.github/agents/sap-solution-architect.agent.md +1 -0
  5. package/.github/skills/abap-development/SKILL.md +1 -1
  6. package/.github/skills/cds-development/SKILL.md +1 -1
  7. package/.github/skills/integration-suite-delivery/SKILL.md +74 -0
  8. package/.github/skills/sap-integration-architect/SKILL.md +76 -0
  9. package/NOTICE +10 -0
  10. package/README.md +36 -8
  11. package/dist/checksums.txt +5 -5
  12. package/dist/vsp-darwin-arm64 +0 -0
  13. package/dist/vsp-darwin-x64 +0 -0
  14. package/dist/vsp-linux-arm64 +0 -0
  15. package/dist/vsp-linux-x64 +0 -0
  16. package/dist/vsp-win32-x64.exe +0 -0
  17. package/inventory.md +66 -2
  18. package/package.json +5 -2
  19. package/patches/vsp-bas-proxy-auth.patch +88 -7
  20. package/scripts/run-live-integration.mjs +1079 -0
  21. package/src/apim-bundle.mjs +381 -0
  22. package/src/apim-tools.mjs +641 -0
  23. package/src/cpi-tools.mjs +1042 -0
  24. package/src/engineering-tools.mjs +105 -0
  25. package/src/iflow-bundle.mjs +282 -0
  26. package/src/integration-common.mjs +232 -0
  27. package/src/integration-config.mjs +167 -0
  28. package/src/integration-credentials.mjs +65 -0
  29. package/src/integration-http.mjs +266 -0
  30. package/src/integration-server.mjs +125 -0
  31. package/src/integration-templates/https-request-reply-http.iflw +391 -0
  32. package/src/integration-tools.mjs +27 -0
  33. package/src/integration-verify.mjs +118 -0
  34. package/src/launcher.mjs +14 -2
  35. package/src/mcp-config.mjs +18 -3
  36. package/src/setup.mjs +189 -11
  37. package/tools.md +81 -3
@@ -0,0 +1,391 @@
1
+ <?xml version="1.0" encoding="UTF-8"?><bpmn2:definitions xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:ifl="http:///com.sap.ifl.model/Ifl.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_1">
2
+ <bpmn2:collaboration id="Collaboration_1" name="Default Collaboration">
3
+ <bpmn2:extensionElements>
4
+ <ifl:property>
5
+ <key>namespaceMapping</key>
6
+ <value/>
7
+ </ifl:property>
8
+ <ifl:property>
9
+ <key>httpSessionHandling</key>
10
+ <value>None</value>
11
+ </ifl:property>
12
+ <ifl:property>
13
+ <key>accessControlMaxAge</key>
14
+ <value/>
15
+ </ifl:property>
16
+ <ifl:property>
17
+ <key>returnExceptionToSender</key>
18
+ <value>false</value>
19
+ </ifl:property>
20
+ <ifl:property>
21
+ <key>log</key>
22
+ <value>All events</value>
23
+ </ifl:property>
24
+ <ifl:property>
25
+ <key>corsEnabled</key>
26
+ <value/>
27
+ </ifl:property>
28
+ <ifl:property>
29
+ <key>exposedHeaders</key>
30
+ <value/>
31
+ </ifl:property>
32
+ <ifl:property>
33
+ <key>componentVersion</key>
34
+ <value>1.2</value>
35
+ </ifl:property>
36
+ <ifl:property>
37
+ <key>allowedHeaderList</key>
38
+ <value/>
39
+ </ifl:property>
40
+ <ifl:property>
41
+ <key>ServerTrace</key>
42
+ <value>false</value>
43
+ </ifl:property>
44
+ <ifl:property>
45
+ <key>allowedOrigins</key>
46
+ <value/>
47
+ </ifl:property>
48
+ <ifl:property>
49
+ <key>accessControlAllowCredentials</key>
50
+ <value/>
51
+ </ifl:property>
52
+ <ifl:property>
53
+ <key>allowedHeaders</key>
54
+ <value/>
55
+ </ifl:property>
56
+ <ifl:property>
57
+ <key>allowedMethods</key>
58
+ <value/>
59
+ </ifl:property>
60
+ <ifl:property>
61
+ <key>cmdVariantUri</key>
62
+ <value>ctype::IFlowVariant/cname::IFlowConfiguration/version::1.2.3</value>
63
+ </ifl:property>
64
+ </bpmn2:extensionElements>
65
+ <bpmn2:participant id="Participant_1" ifl:type="EndpointSender" name="Sender">
66
+ <bpmn2:extensionElements>
67
+ <ifl:property>
68
+ <key>enableBasicAuthentication</key>
69
+ <value>false</value>
70
+ </ifl:property>
71
+ <ifl:property>
72
+ <key>ifl:type</key>
73
+ <value>EndpointSender</value>
74
+ </ifl:property>
75
+ </bpmn2:extensionElements>
76
+ </bpmn2:participant>
77
+ <bpmn2:participant id="Participant_2" ifl:type="EndpointRecevier" name="Receiver">
78
+ <bpmn2:extensionElements>
79
+ <ifl:property>
80
+ <key>ifl:type</key>
81
+ <value>EndpointRecevier</value>
82
+ </ifl:property>
83
+ </bpmn2:extensionElements>
84
+ </bpmn2:participant>
85
+ <bpmn2:participant id="Participant_Process_1" ifl:type="IntegrationProcess" name="Integration Process" processRef="Process_1">
86
+ <bpmn2:extensionElements/>
87
+ </bpmn2:participant>
88
+ <bpmn2:messageFlow id="MessageFlow_4" name="HTTPS" sourceRef="Participant_1" targetRef="StartEvent_2">
89
+ <bpmn2:extensionElements>
90
+ <ifl:property>
91
+ <key>ComponentType</key>
92
+ <value>HTTPS</value>
93
+ </ifl:property>
94
+ <ifl:property>
95
+ <key>Description</key>
96
+ <value/>
97
+ </ifl:property>
98
+ <ifl:property>
99
+ <key>maximumBodySize</key>
100
+ <value>40</value>
101
+ </ifl:property>
102
+ <ifl:property>
103
+ <key>ComponentNS</key>
104
+ <value>sap</value>
105
+ </ifl:property>
106
+ <ifl:property>
107
+ <key>componentVersion</key>
108
+ <value>1.4</value>
109
+ </ifl:property>
110
+ <ifl:property>
111
+ <key>urlPath</key>
112
+ <value>{{Sender_Path}}</value>
113
+ </ifl:property>
114
+ <ifl:property>
115
+ <key>Name</key>
116
+ <value>HTTPS</value>
117
+ </ifl:property>
118
+ <ifl:property>
119
+ <key>TransportProtocolVersion</key>
120
+ <value>1.4.3</value>
121
+ </ifl:property>
122
+ <ifl:property>
123
+ <key>ComponentSWCVName</key>
124
+ <value>external</value>
125
+ </ifl:property>
126
+ <ifl:property>
127
+ <key>system</key>
128
+ <value>Sender</value>
129
+ </ifl:property>
130
+ <ifl:property>
131
+ <key>xsrfProtection</key>
132
+ <value>0</value>
133
+ </ifl:property>
134
+ <ifl:property>
135
+ <key>TransportProtocol</key>
136
+ <value>HTTPS</value>
137
+ </ifl:property>
138
+ <ifl:property>
139
+ <key>cmdVariantUri</key>
140
+ <value>ctype::AdapterVariant/cname::sap:HTTPS/tp::HTTPS/mp::None/direction::Sender/version::1.4.3</value>
141
+ </ifl:property>
142
+ <ifl:property>
143
+ <key>userRole</key>
144
+ <value>ESBMessaging.send</value>
145
+ </ifl:property>
146
+ <ifl:property>
147
+ <key>senderAuthType</key>
148
+ <value>RoleBased</value>
149
+ </ifl:property>
150
+ <ifl:property>
151
+ <key>MessageProtocol</key>
152
+ <value>None</value>
153
+ </ifl:property>
154
+ <ifl:property>
155
+ <key>MessageProtocolVersion</key>
156
+ <value>1.4.3</value>
157
+ </ifl:property>
158
+ <ifl:property>
159
+ <key>ComponentSWCVId</key>
160
+ <value>1.4.3</value>
161
+ </ifl:property>
162
+ <ifl:property>
163
+ <key>direction</key>
164
+ <value>Sender</value>
165
+ </ifl:property>
166
+ <ifl:property>
167
+ <key>clientCertificates</key>
168
+ <value/>
169
+ </ifl:property>
170
+ </bpmn2:extensionElements>
171
+ </bpmn2:messageFlow>
172
+ <bpmn2:messageFlow id="MessageFlow_7" name="HTTP" sourceRef="ServiceTask_1" targetRef="Participant_2">
173
+ <bpmn2:extensionElements>
174
+ <ifl:property>
175
+ <key>Description</key>
176
+ <value/>
177
+ </ifl:property>
178
+ <ifl:property>
179
+ <key>methodSourceExpression</key>
180
+ <value/>
181
+ </ifl:property>
182
+ <ifl:property>
183
+ <key>ComponentNS</key>
184
+ <value>sap</value>
185
+ </ifl:property>
186
+ <ifl:property>
187
+ <key>privateKeyAlias</key>
188
+ <value/>
189
+ </ifl:property>
190
+ <ifl:property>
191
+ <key>httpMethod</key>
192
+ <value>{{Receiver_Method}}</value>
193
+ </ifl:property>
194
+ <ifl:property>
195
+ <key>Name</key>
196
+ <value>HTTP</value>
197
+ </ifl:property>
198
+ <ifl:property>
199
+ <key>TransportProtocolVersion</key>
200
+ <value>1.7.2</value>
201
+ </ifl:property>
202
+ <ifl:property>
203
+ <key>ComponentSWCVName</key>
204
+ <value>external</value>
205
+ </ifl:property>
206
+ <ifl:property>
207
+ <key>proxyPort</key>
208
+ <value/>
209
+ </ifl:property>
210
+ <ifl:property>
211
+ <key>httpAddressQuery</key>
212
+ <value>{{Receiver_Query}}</value>
213
+ </ifl:property>
214
+ <ifl:property>
215
+ <key>httpRequestTimeout</key>
216
+ <value>{{Receiver_Timeout}}</value>
217
+ </ifl:property>
218
+ <ifl:property>
219
+ <key>MessageProtocol</key>
220
+ <value>None</value>
221
+ </ifl:property>
222
+ <ifl:property>
223
+ <key>ComponentSWCVId</key>
224
+ <value>1.7.2</value>
225
+ </ifl:property>
226
+ <ifl:property>
227
+ <key>direction</key>
228
+ <value>Receiver</value>
229
+ </ifl:property>
230
+ <ifl:property>
231
+ <key>ComponentType</key>
232
+ <value>HTTP</value>
233
+ </ifl:property>
234
+ <ifl:property>
235
+ <key>httpShouldSendBody</key>
236
+ <value>true</value>
237
+ </ifl:property>
238
+ <ifl:property>
239
+ <key>proxyType</key>
240
+ <value>{{Receiver_Proxy_Type}}</value>
241
+ </ifl:property>
242
+ <ifl:property>
243
+ <key>componentVersion</key>
244
+ <value>1.7</value>
245
+ </ifl:property>
246
+ <ifl:property>
247
+ <key>proxyHost</key>
248
+ <value/>
249
+ </ifl:property>
250
+ <ifl:property>
251
+ <key>system</key>
252
+ <value>Receiver</value>
253
+ </ifl:property>
254
+ <ifl:property>
255
+ <key>authenticationMethod</key>
256
+ <value>{{Receiver_Authentication}}</value>
257
+ </ifl:property>
258
+ <ifl:property>
259
+ <key>locationID</key>
260
+ <value>{{Receiver_Location_ID}}</value>
261
+ </ifl:property>
262
+ <ifl:property>
263
+ <key>TransportProtocol</key>
264
+ <value>HTTP</value>
265
+ </ifl:property>
266
+ <ifl:property>
267
+ <key>cmdVariantUri</key>
268
+ <value>ctype::AdapterVariant/cname::sap:HTTP/tp::HTTP/mp::None/direction::Receiver/version::1.7.2</value>
269
+ </ifl:property>
270
+ <ifl:property>
271
+ <key>credentialName</key>
272
+ <value>{{Receiver_Credential_Name}}</value>
273
+ </ifl:property>
274
+ <ifl:property>
275
+ <key>MessageProtocolVersion</key>
276
+ <value>1.7.2</value>
277
+ </ifl:property>
278
+ <ifl:property>
279
+ <key>httpAddressWithoutQuery</key>
280
+ <value>{{Receiver_Address}}</value>
281
+ </ifl:property>
282
+ </bpmn2:extensionElements>
283
+ </bpmn2:messageFlow>
284
+ </bpmn2:collaboration>
285
+ <bpmn2:process id="Process_1" name="Integration Process">
286
+ <bpmn2:extensionElements>
287
+ <ifl:property>
288
+ <key>transactionTimeout</key>
289
+ <value>30</value>
290
+ </ifl:property>
291
+ <ifl:property>
292
+ <key>componentVersion</key>
293
+ <value>1.2</value>
294
+ </ifl:property>
295
+ <ifl:property>
296
+ <key>cmdVariantUri</key>
297
+ <value>ctype::FlowElementVariant/cname::IntegrationProcess/version::1.2.1</value>
298
+ </ifl:property>
299
+ <ifl:property>
300
+ <key>transactionalHandling</key>
301
+ <value>Not Required</value>
302
+ </ifl:property>
303
+ </bpmn2:extensionElements>
304
+ <bpmn2:startEvent id="StartEvent_2" name="Start">
305
+ <bpmn2:extensionElements>
306
+ <ifl:property>
307
+ <key>componentVersion</key>
308
+ <value>1.0</value>
309
+ </ifl:property>
310
+ <ifl:property>
311
+ <key>cmdVariantUri</key>
312
+ <value>ctype::FlowstepVariant/cname::MessageStartEvent/version::1.0</value>
313
+ </ifl:property>
314
+ </bpmn2:extensionElements>
315
+ <bpmn2:outgoing>SequenceFlow_3</bpmn2:outgoing>
316
+ <bpmn2:messageEventDefinition/>
317
+ </bpmn2:startEvent>
318
+ <bpmn2:endEvent id="EndEvent_2" name="End">
319
+ <bpmn2:extensionElements>
320
+ <ifl:property>
321
+ <key>componentVersion</key>
322
+ <value>1.1</value>
323
+ </ifl:property>
324
+ <ifl:property>
325
+ <key>cmdVariantUri</key>
326
+ <value>ctype::FlowstepVariant/cname::MessageEndEvent/version::1.1.0</value>
327
+ </ifl:property>
328
+ </bpmn2:extensionElements>
329
+ <bpmn2:incoming>SequenceFlow_6</bpmn2:incoming>
330
+ <bpmn2:messageEventDefinition/>
331
+ </bpmn2:endEvent>
332
+ <bpmn2:serviceTask id="ServiceTask_1" name="Request Reply 1">
333
+ <bpmn2:extensionElements>
334
+ <ifl:property>
335
+ <key>componentVersion</key>
336
+ <value>1.0</value>
337
+ </ifl:property>
338
+ <ifl:property>
339
+ <key>activityType</key>
340
+ <value>ExternalCall</value>
341
+ </ifl:property>
342
+ <ifl:property>
343
+ <key>cmdVariantUri</key>
344
+ <value>ctype::FlowstepVariant/cname::ExternalCall/version::1.0.4</value>
345
+ </ifl:property>
346
+ </bpmn2:extensionElements>
347
+ <bpmn2:incoming>SequenceFlow_3</bpmn2:incoming>
348
+ <bpmn2:outgoing>SequenceFlow_6</bpmn2:outgoing>
349
+ </bpmn2:serviceTask>
350
+ <bpmn2:sequenceFlow id="SequenceFlow_3" sourceRef="StartEvent_2" targetRef="ServiceTask_1"/>
351
+ <bpmn2:sequenceFlow id="SequenceFlow_6" sourceRef="ServiceTask_1" targetRef="EndEvent_2"/>
352
+ </bpmn2:process>
353
+ <bpmndi:BPMNDiagram id="BPMNDiagram_1" name="Default Collaboration Diagram">
354
+ <bpmndi:BPMNPlane bpmnElement="Collaboration_1" id="BPMNPlane_1">
355
+ <bpmndi:BPMNShape bpmnElement="EndEvent_2" id="BPMNShape_EndEvent_2">
356
+ <dc:Bounds height="32.0" width="32.0" x="703.0" y="142.0"/>
357
+ </bpmndi:BPMNShape>
358
+ <bpmndi:BPMNShape bpmnElement="StartEvent_2" id="BPMNShape_StartEvent_2">
359
+ <dc:Bounds height="32.0" width="32.0" x="292.0" y="142.0"/>
360
+ </bpmndi:BPMNShape>
361
+ <bpmndi:BPMNShape bpmnElement="ServiceTask_1" id="BPMNShape_ServiceTask_1">
362
+ <dc:Bounds height="60.0" width="100.0" x="412.0" y="132.0"/>
363
+ </bpmndi:BPMNShape>
364
+ <bpmndi:BPMNShape bpmnElement="Participant_2" id="BPMNShape_Participant_2">
365
+ <dc:Bounds height="140.0" width="100.0" x="900.0" y="100.0"/>
366
+ </bpmndi:BPMNShape>
367
+ <bpmndi:BPMNShape bpmnElement="Participant_1" id="BPMNShape_Participant_1">
368
+ <dc:Bounds height="140.0" width="100.0" x="40.0" y="100.0"/>
369
+ </bpmndi:BPMNShape>
370
+ <bpmndi:BPMNShape bpmnElement="Participant_Process_1" id="BPMNShape_Participant_Process_1">
371
+ <dc:Bounds height="220.0" width="540.0" x="250.0" y="60.0"/>
372
+ </bpmndi:BPMNShape>
373
+ <bpmndi:BPMNEdge bpmnElement="SequenceFlow_6" id="BPMNEdge_SequenceFlow_6" sourceElement="BPMNShape_ServiceTask_1" targetElement="BPMNShape_EndEvent_2">
374
+ <di:waypoint x="462.0" xsi:type="dc:Point" y="160.0"/>
375
+ <di:waypoint x="719.0" xsi:type="dc:Point" y="160.0"/>
376
+ </bpmndi:BPMNEdge>
377
+ <bpmndi:BPMNEdge bpmnElement="SequenceFlow_3" id="BPMNEdge_SequenceFlow_3" sourceElement="BPMNShape_StartEvent_2" targetElement="BPMNShape_ServiceTask_1">
378
+ <di:waypoint x="308.0" xsi:type="dc:Point" y="160.0"/>
379
+ <di:waypoint x="462.0" xsi:type="dc:Point" y="160.0"/>
380
+ </bpmndi:BPMNEdge>
381
+ <bpmndi:BPMNEdge bpmnElement="MessageFlow_4" id="BPMNEdge_MessageFlow_4" sourceElement="BPMNShape_Participant_1" targetElement="BPMNShape_StartEvent_2">
382
+ <di:waypoint x="90.0" xsi:type="dc:Point" y="170.0"/>
383
+ <di:waypoint x="308.0" xsi:type="dc:Point" y="158.0"/>
384
+ </bpmndi:BPMNEdge>
385
+ <bpmndi:BPMNEdge bpmnElement="MessageFlow_7" id="BPMNEdge_MessageFlow_7" sourceElement="BPMNShape_ServiceTask_1" targetElement="BPMNShape_Participant_2">
386
+ <di:waypoint x="462.0" xsi:type="dc:Point" y="162.0"/>
387
+ <di:waypoint x="950.0" xsi:type="dc:Point" y="170.0"/>
388
+ </bpmndi:BPMNEdge>
389
+ </bpmndi:BPMNPlane>
390
+ </bpmndi:BPMNDiagram>
391
+ </bpmn2:definitions>
@@ -0,0 +1,27 @@
1
+ // MCP tools for SAP Integration Suite: Cloud Integration (CPI) and API Management (APIM).
2
+ // Writes cover create, update, deploy and undeploy — there are no delete tools — and every write
3
+ // checks an exact confirmation string before any network call. Secrets are write-only.
4
+ import { z } from 'zod';
5
+ import { apimToolNames, registerApimTools } from './apim-tools.mjs';
6
+ import { cpiToolNames, registerCpiTools } from './cpi-tools.mjs';
7
+ import { READ_ONLY_ANNOTATIONS, resultOf } from './integration-common.mjs';
8
+
9
+ export { INTEGRATION_MAX_OUTPUT_BYTES } from './integration-common.mjs';
10
+
11
+ export const INTEGRATION_TOOL_NAMES = Object.freeze(['integration_connection_info', ...cpiToolNames(), ...apimToolNames()]);
12
+
13
+ /** Register Integration Suite tools; cpi_* and apim_* only appear when their client is configured. */
14
+ export function registerIntegrationTools(server, { cpi, apim, summary, verify, pollIntervalMs = 2000, readBackDelayMs = 5000 } = {}) {
15
+ const register = (name, title, description, inputSchema, annotations, handler) => {
16
+ server.registerTool(name, { title, description, inputSchema, annotations }, async args => resultOf(() => handler(args)));
17
+ };
18
+
19
+ register('integration_connection_info', 'Integration Suite connection info',
20
+ 'Return which Integration Suite services (CPI, APIM) are configured, their credential source and tenant hosts. With verify: true it also signs in and runs one read per tool area (packages, deployed artifacts, message logs, security material, number ranges, Partner Directory, data stores, JMS; API proxies, providers, products, key value maps, virtual hosts, and policy readiness via an API proxy bundle export) and reports areas whose role is missing. Never returns client ids or secrets.',
21
+ { verify: z.boolean().default(false) },
22
+ READ_ONLY_ANNOTATIONS,
23
+ async ({ verify: online }) => (online && verify ? { ...summary, verification: await verify() } : summary));
24
+
25
+ if (cpi) registerCpiTools(register, cpi, { pollIntervalMs });
26
+ if (apim) registerApimTools(register, apim, { readBackDelayMs });
27
+ }
@@ -0,0 +1,118 @@
1
+ // Online check of CPI/APIM credentials: sign in, then one read-only request per tool area so a
2
+ // missing role shows up as a named area instead of a failing tool call later. Never writes.
3
+ import { createServiceClient } from './integration-http.mjs';
4
+
5
+ const CSRF_PATHS = Object.freeze({ CPI: '/api/v1/', APIM: '/apiportal/api/1.0/Management.svc/' });
6
+ const APIM_BASE = '/apiportal/api/1.0/Management.svc';
7
+
8
+ const PROBES = Object.freeze({
9
+ CPI: [
10
+ { area: 'Packages and iFlows', path: '/api/v1/IntegrationPackages', query: { $top: 1 }, core: true },
11
+ { area: 'Deployed artifacts', path: '/api/v1/IntegrationRuntimeArtifacts', query: { $top: 1 } },
12
+ { area: 'Message logs', path: '/api/v1/MessageProcessingLogs', query: { $top: 1 } },
13
+ { area: 'Security material', path: '/api/v1/UserCredentials' },
14
+ { area: 'Number ranges', path: '/api/v1/NumberRanges' },
15
+ { area: 'Partner Directory', path: '/api/v1/StringParameters', query: { $top: 1 } },
16
+ { area: 'Data stores', path: '/api/v1/DataStores' },
17
+ { area: 'JMS queues', path: "/api/v1/JmsBrokers('Broker1')" }
18
+ ],
19
+ APIM: [
20
+ { area: 'API proxies', path: `${APIM_BASE}/APIProxies`, query: { $top: 1, $select: 'name' }, core: true },
21
+ { area: 'API providers', path: `${APIM_BASE}/APIProviders`, query: { $top: 1, $select: 'name' } },
22
+ { area: 'API products', path: `${APIM_BASE}/APIProducts`, query: { $top: 1, $select: 'name' } },
23
+ { area: 'Key value maps', path: `${APIM_BASE}/KeyMapEntries`, query: { $top: 1, $select: 'name' } },
24
+ { area: 'Virtual hosts', path: `${APIM_BASE}/VirtualHosts` }
25
+ ]
26
+ });
27
+
28
+ // Statuses accepted as answers so they can be classified instead of thrown.
29
+ const OBSERVED = [200, 204, 400, 401, 403, 404, 405, 500, 501, 502, 503, 504];
30
+
31
+ function classify(status) {
32
+ if (status >= 200 && status < 300) return { result: 'ok', detail: 'readable' };
33
+ if (status === 401 || status === 403) return { result: 'forbidden', detail: `HTTP ${status}: the service key lacks the role for this area` };
34
+ if (status === 404 || status === 501) return { result: 'unavailable', detail: `HTTP ${status}: not available on this tenant or plan` };
35
+ return { result: 'error', detail: `HTTP ${status}` };
36
+ }
37
+
38
+ /** OAuth clients for the resolved CPI/APIM config (null for an unconfigured side). */
39
+ export function createIntegrationClients(config, { env = process.env, fetchImpl } = {}) {
40
+ const clientFor = (label, side) => (side
41
+ ? createServiceClient({
42
+ label,
43
+ url: side.url,
44
+ tokenUrl: side.tokenUrl,
45
+ clientId: side.clientId,
46
+ clientSecret: side.clientSecret,
47
+ csrfPath: CSRF_PATHS[label],
48
+ env,
49
+ ...(fetchImpl ? { fetchImpl } : {})
50
+ })
51
+ : null);
52
+ return { cpi: clientFor('CPI', config?.cpi), apim: clientFor('APIM', config?.apim) };
53
+ }
54
+
55
+ // Policies are attached by exporting the proxy bundle (Transport.svc), editing it and re-importing it.
56
+ // The export of an existing proxy proves the read half without writing; no proxy means nothing to test.
57
+ async function policyCheck(client, coreText) {
58
+ const area = 'Policies (proxy bundle export)';
59
+ let name;
60
+ try {
61
+ const parsed = JSON.parse(coreText);
62
+ name = (parsed?.d?.results ?? parsed?.d ?? parsed?.value ?? [])[0]?.name;
63
+ } catch {
64
+ // Unparseable list: reported as unavailable below.
65
+ }
66
+ if (typeof name !== 'string' || !name) return { area, status: null, result: 'unavailable', detail: 'no API proxy exists yet to test the export with' };
67
+ try {
68
+ const { status, data } = await client.request('GET', '/apiportal/api/1.0/Transport.svc/APIProxies', {
69
+ query: { name }, expect: OBSERVED, withStatus: true, responseType: 'bytes', accept: 'application/zip, application/octet-stream'
70
+ });
71
+ const zip = data instanceof Uint8Array && data[0] === 0x50 && data[1] === 0x4b;
72
+ if (status >= 200 && status < 300 && !zip) return { area, status, result: 'error', detail: `HTTP ${status} but the export is not a zip bundle` };
73
+ return { area, status, ...classify(status) };
74
+ } catch (error) {
75
+ return { area, status: null, result: 'error', detail: String(error?.message || 'request failed').slice(0, 300) };
76
+ }
77
+ }
78
+
79
+ async function verifySide(label, client) {
80
+ if (!client) return { configured: false };
81
+ const probes = PROBES[label];
82
+ const run = async probe => {
83
+ try {
84
+ const { status, data } = await client.request('GET', probe.path, { query: probe.query, expect: OBSERVED, withStatus: true, responseType: 'text', accept: 'application/json' });
85
+ return { area: probe.area, status, ...classify(status), text: data };
86
+ } catch (error) {
87
+ return { area: probe.area, status: null, result: 'error', detail: String(error?.message || 'request failed').slice(0, 300) };
88
+ }
89
+ };
90
+ const { text: coreText, ...core } = await run(probes[0]);
91
+ // The core probe also fetches the token: a thrown request is a token or network failure, reported once.
92
+ if (core.status === null) {
93
+ return { configured: true, signed_in: false, ok: false, error: core.detail, checks: [] };
94
+ }
95
+ const others = probes.slice(1).map(run);
96
+ if (label === 'APIM' && core.result === 'ok') others.push(policyCheck(client, coreText));
97
+ const checks = [core, ...(await Promise.all(others)).map(({ text, ...check }) => check)];
98
+ return { configured: true, signed_in: true, ok: core.result === 'ok', checks };
99
+ }
100
+
101
+ /** Verify the configured sides online; `ok` means every configured side signed in and passed its core read. */
102
+ export async function verifyIntegrationClients({ cpi, apim } = {}) {
103
+ const [cpiResult, apimResult] = await Promise.all([verifySide('CPI', cpi), verifySide('APIM', apim)]);
104
+ const sides = [cpiResult, apimResult].filter(side => side.configured);
105
+ return { ok: sides.length > 0 && sides.every(side => side.ok), cpi: cpiResult, apim: apimResult };
106
+ }
107
+
108
+ /** Human-readable lines for one verified side (no secrets: only host, area names and HTTP statuses). */
109
+ export function verificationLines(label, host, side) {
110
+ if (!side?.configured) return [];
111
+ if (!side.signed_in) return [`✗ ${label} ${host}: sign-in failed — ${side.error}`];
112
+ const lines = [`${side.ok ? '✓' : '✗'} ${label} ${host}: signed in`];
113
+ for (const check of side.checks) {
114
+ const mark = check.result === 'ok' ? '✓' : (check.result === 'unavailable' ? '–' : '✗');
115
+ lines.push(` ${mark} ${check.area}${check.result === 'ok' ? '' : ` — ${check.detail}`}`);
116
+ }
117
+ return lines;
118
+ }
package/src/launcher.mjs CHANGED
@@ -10,7 +10,7 @@ import { adtBaseUrl, adtPort, detectAdtUrl, discoverSapGuiSystems, hasExplicitPo
10
10
  import { binaryTarget, cacheDirectory, downloadBinary, findBinary } from './binary.mjs';
11
11
  import { MCPProxy } from './mcp-proxy.mjs';
12
12
  import { configuredVersionHandoff, installMcpConfig, npxPackageLauncher, repairManagedMcpConfig } from './mcp-config.mjs';
13
- import { runSetup } from './setup.mjs';
13
+ import { runIntegrationSuiteSetup, runSetup } from './setup.mjs';
14
14
  import { runHarnessSetup } from './harness-setup.mjs';
15
15
  import { resolveConfiguredCloudFoundryDestination } from './cf-destination.mjs';
16
16
  import { createTlsServerNameAdtProxy } from './tls-adt-proxy.mjs';
@@ -37,6 +37,7 @@ function usage() {
37
37
  'Options:',
38
38
  ' --setup Configure generated BAS MCP servers',
39
39
  ' --tools Also offer full-stack SAP companion MCP servers (use with --setup)',
40
+ ' --setup-integration Only set up the Integration Suite MCP server: paste CPI/APIM service keys and verify them online',
40
41
  ' --npx Launch configured MCP servers through the pinned npm package (use with --setup)',
41
42
  ' --list-destinations List discovered BAS destinations',
42
43
  ' --list-destinations --json Print redacted JSON status',
@@ -369,6 +370,7 @@ async function configuredLocalSapGuiDestination(env) {
369
370
  }
370
371
 
371
372
  async function main() {
373
+ const setupIntegration = hasFlag('--setup-integration');
372
374
  const setup = hasFlag('--setup');
373
375
  const check = hasFlag('--check');
374
376
  const list = hasFlag('--list-destinations');
@@ -378,7 +380,7 @@ async function main() {
378
380
  process.stdout.write(usage());
379
381
  return;
380
382
  }
381
- if (!setup && !check && !list && !doctor && !demo && await handOffToConfiguredVersion()) return;
383
+ if (!setup && !setupIntegration && !check && !list && !doctor && !demo && await handOffToConfiguredVersion()) return;
382
384
  if (doctor && demo) throw new Error('Use either --doctor or --demo, not both.');
383
385
  if (demo) {
384
386
  await runOfflineDemo();
@@ -407,6 +409,16 @@ async function main() {
407
409
  if (!report.ok) process.exitCode = 1;
408
410
  return;
409
411
  }
412
+ if (setupIntegration) {
413
+ const options = {};
414
+ if (hasFlag('--npx')) {
415
+ const launcher = npxPackageLauncher(pkg.version);
416
+ options.install = (selected, installOptions) => installMcpConfig(selected, { ...installOptions, ...launcher });
417
+ }
418
+ const result = await runIntegrationSuiteSetup(options);
419
+ if (!result?.skipped) await runHarnessSetup(result?.servers, { env: runtimeEnv, root });
420
+ return;
421
+ }
410
422
  if (setup) {
411
423
  const setupOptions = {
412
424
  includeSapDevelopmentToolsPrompt: hasFlag('--tools') || hasFlag('--companion-tools'),
@@ -12,7 +12,7 @@ const LEGACY_MCP_SERVER_PREFIXES = ['sapAiDev_', 'basVspMcp_'];
12
12
  const COMPANION_SERVER_KIND = 'sap-development-companion';
13
13
  const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
14
14
 
15
- // The self-package companion (HANA inspector) must launch the exact version
15
+ // The self-package companions (HANA inspector, Integration Suite) must launch the exact version
16
16
  // the user installed; an unpinned `npx sap-ai-dev-toolkit` would re-resolve
17
17
  // executable code from the registry at every MCP host start.
18
18
  let selfPackageVersion;
@@ -67,6 +67,15 @@ export const SAP_DEVELOPMENT_MCP_SERVERS = Object.freeze([
67
67
  bin: 'sap-ai-hana',
68
68
  ignoreScripts: true,
69
69
  priority: 'optional'
70
+ }),
71
+ Object.freeze({
72
+ id: 'integration-suite',
73
+ name: 'Integration Suite (CPI + API Management)',
74
+ description: 'Build, deploy and monitor Cloud Integration content and manage API Management providers, proxies, policies, products and key value maps, using service keys from the host environment or pasted and verified with sap-ai-dev --setup-integration.',
75
+ packageName: 'sap-ai-dev-toolkit',
76
+ bin: 'sap-ai-integration',
77
+ ignoreScripts: true,
78
+ priority: 'optional'
70
79
  })
71
80
  ]);
72
81
 
@@ -596,7 +605,11 @@ export async function installMcpConfig(destinationsOrOptions, options = {}) {
596
605
  // instead of failing the whole setup (self-healing).
597
606
  const config = await readConfig(path, { recoverInvalid: true, warnings: installWarnings });
598
607
  const selfVersion = await runningPackageVersion();
599
- const companionVersions = selfVersion ? { 'sap-ai-dev-toolkit': selfVersion, 'hana-cloud-inspector': selfVersion } : {};
608
+ // Values are full npx package specs; a bare version would become --package=<version>.
609
+ const selfSpec = selfVersion ? `sap-ai-dev-toolkit@${selfVersion}` : '';
610
+ const companionVersions = selfSpec
611
+ ? { 'sap-ai-dev-toolkit': selfSpec, 'hana-cloud-inspector': selfSpec, 'integration-suite': selfSpec }
612
+ : {};
600
613
  const generated = {
601
614
  ...buildMcpEntries(destinations, env),
602
615
  ...buildSapDevelopmentMcpEntries(options.sapDevelopmentServers || [], { packageVersions: companionVersions })
@@ -642,7 +655,9 @@ export async function installMcpConfig(destinationsOrOptions, options = {}) {
642
655
  const exactManagedUpdate = managed && generatedNames.has(name)
643
656
  && destinationValue(entry.env) === generated[name]?.env?.SAP_AI_DEV_TOOLKIT_DESTINATION
644
657
  && destinationSource(entry.env) === generated[name]?.env?.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE;
645
- if (additive && (managed || isCompanionServer(entry)) && !exactManagedUpdate) managedEntries.push([name, entry]);
658
+ // A companion that is being regenerated is replaced (version refresh), not retained as a conflict.
659
+ const regeneratedCompanion = isCompanionServer(entry) && generatedNames.has(name) && isCompanionServer(generated[name]);
660
+ if (additive && (managed || isCompanionServer(entry)) && !exactManagedUpdate && !regeneratedCompanion) managedEntries.push([name, entry]);
646
661
  else if (!managed && !isCompanionServer(entry) && !LEGACY_MCP_SERVER_PREFIXES.some(prefix => name.startsWith(prefix))) servers[name] = entry;
647
662
  }
648
663
  const retainedNames = new Set(managedEntries.map(([name]) => name));