@sfdxy/anypoint-connect 0.8.1 → 0.9.1

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 (45) hide show
  1. package/README.md +57 -2
  2. package/dist/api/CloudHub2Api.d.ts +116 -45
  3. package/dist/api/CloudHub2Api.d.ts.map +1 -1
  4. package/dist/api/CloudHub2Api.js +56 -2
  5. package/dist/api/CloudHub2Api.js.map +1 -1
  6. package/dist/api/ExchangeApi.d.ts +17 -0
  7. package/dist/api/ExchangeApi.d.ts.map +1 -1
  8. package/dist/api/ExchangeApi.js +30 -0
  9. package/dist/api/ExchangeApi.js.map +1 -1
  10. package/dist/api/index.d.ts +2 -2
  11. package/dist/api/index.d.ts.map +1 -1
  12. package/dist/api/index.js +1 -1
  13. package/dist/api/index.js.map +1 -1
  14. package/dist/client/HttpClient.d.ts +8 -0
  15. package/dist/client/HttpClient.d.ts.map +1 -1
  16. package/dist/client/HttpClient.js +16 -0
  17. package/dist/client/HttpClient.js.map +1 -1
  18. package/dist/commands/apps.d.ts.map +1 -1
  19. package/dist/commands/apps.js +6 -6
  20. package/dist/commands/apps.js.map +1 -1
  21. package/dist/commands/deploy.d.ts.map +1 -1
  22. package/dist/commands/deploy.js +17 -48
  23. package/dist/commands/deploy.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/mcp/tools/applications.d.ts.map +1 -1
  29. package/dist/mcp/tools/applications.js +306 -144
  30. package/dist/mcp/tools/applications.js.map +1 -1
  31. package/dist/mcp/tools/exchange.d.ts +2 -1
  32. package/dist/mcp/tools/exchange.d.ts.map +1 -1
  33. package/dist/mcp/tools/exchange.js +229 -6
  34. package/dist/mcp/tools/exchange.js.map +1 -1
  35. package/dist/mcp/tools/shared.d.ts +18 -0
  36. package/dist/mcp/tools/shared.d.ts.map +1 -1
  37. package/dist/mcp/tools/shared.js +17 -0
  38. package/dist/mcp/tools/shared.js.map +1 -1
  39. package/dist/safety/deployment.d.ts +85 -0
  40. package/dist/safety/deployment.d.ts.map +1 -0
  41. package/dist/safety/deployment.js +170 -0
  42. package/dist/safety/deployment.js.map +1 -0
  43. package/dist/safety/guards.js +1 -1
  44. package/dist/safety/guards.js.map +1 -1
  45. package/package.json +1 -1
@@ -1,5 +1,7 @@
1
1
  import { z } from 'zod';
2
- import { mcpError } from './shared.js';
2
+ import { mcpError, mcpText, dryRunPreview } from './shared.js';
3
+ import { buildCreatePayload, mergeApplicationProperties, mergeForArtifactUpdate, resolveRollbackTarget, } from '../../safety/deployment.js';
4
+ import { errorMessage } from '../../utils/errors.js';
3
5
  export function registerApplicationTools(server, client) {
4
6
  server.registerTool('list_apps', {
5
7
  title: 'List Applications',
@@ -14,7 +16,7 @@ export function registerApplicationTools(server, client) {
14
16
  try {
15
17
  const orgId = await client.getDefaultOrgId();
16
18
  const env = await client.accessManagement.resolveEnvironment(orgId, environment);
17
- const deployments = await client.cloudHub2.getDeployments(orgId, env.id);
19
+ const deployments = await client.cloudHub2.getDetailedDeployments(orgId, env.id);
18
20
  return {
19
21
  content: [
20
22
  {
@@ -25,7 +27,7 @@ export function registerApplicationTools(server, client) {
25
27
  version: d.application?.ref?.version,
26
28
  runtime: d.target?.deploymentSettings?.runtime?.version,
27
29
  vCores: d.application?.vCores,
28
- replicas: d.target?.replicas?.length,
30
+ replicas: d.target?.replicas,
29
31
  id: d.id,
30
32
  })), null, 2),
31
33
  },
@@ -48,7 +50,7 @@ export function registerApplicationTools(server, client) {
48
50
  try {
49
51
  const orgId = await client.getDefaultOrgId();
50
52
  const env = await client.accessManagement.resolveEnvironment(orgId, environment);
51
- const deployment = await client.cloudHub2.findByName(orgId, env.id, appName);
53
+ const deployment = await client.cloudHub2.findDetailByName(orgId, env.id, appName);
52
54
  if (!deployment) {
53
55
  return {
54
56
  content: [{ type: 'text', text: `Application "${appName}" not found in ${env.name}` }],
@@ -74,13 +76,15 @@ export function registerApplicationTools(server, client) {
74
76
  jvm: deployment.target?.deploymentSettings?.jvm,
75
77
  clustered: deployment.target?.deploymentSettings?.clustered,
76
78
  updateStrategy: deployment.target?.deploymentSettings?.updateStrategy,
77
- replicas: deployment.target?.replicas?.map((r) => ({
79
+ replicas: deployment.replicas?.map((r) => ({
78
80
  id: r.id,
79
81
  state: r.state,
80
82
  location: r.deploymentLocation,
81
83
  })),
82
84
  publicUrl: deployment.target?.deploymentSettings?.http?.inbound?.publicUrl,
83
- updatedAt: deployment.updatedAt,
85
+ updatedAt: deployment.lastModifiedDate
86
+ ? new Date(deployment.lastModifiedDate).toISOString()
87
+ : undefined,
84
88
  }, null, 2),
85
89
  },
86
90
  ],
@@ -90,6 +94,62 @@ export function registerApplicationTools(server, client) {
90
94
  return mcpError(error);
91
95
  }
92
96
  });
97
+ server.registerTool('get_deployment_spec', {
98
+ title: 'Get Deployment Spec',
99
+ description: 'Returns the full current deployment spec for a CloudHub 2.0 application — the "look before you leap" view used before a redeploy or rollback. Includes the exact artifact reference (groupId:artifactId:version:packaging), Mule runtime version, deployment target (a private space ID vs a shared cloudhub-* region), vCores, replica count with per-replica state and location, update strategy, clustering, JVM args, public URL, desired vs last-successful version, and timestamps. Unlike get_app_status this always fetches full deployment detail. Use it to confirm exactly what is running before changing an artifact, and to capture the current ref so you can roll back.',
100
+ inputSchema: {
101
+ appName: z.string().describe('Application name exactly as deployed (case-insensitive match)'),
102
+ environment: z.string().describe('Environment name (e.g. "Production") or environment ID'),
103
+ },
104
+ annotations: { readOnlyHint: true },
105
+ }, async ({ appName, environment }) => {
106
+ try {
107
+ const orgId = await client.getDefaultOrgId();
108
+ const env = await client.accessManagement.resolveEnvironment(orgId, environment);
109
+ const d = await client.cloudHub2.findDetailByName(orgId, env.id, appName);
110
+ if (!d) {
111
+ return mcpText(`Application "${appName}" not found in ${env.name}`);
112
+ }
113
+ const settings = d.target?.deploymentSettings;
114
+ const targetId = d.target?.targetId || '';
115
+ const isPrivateSpace = !targetId.startsWith('cloudhub-');
116
+ return mcpText({
117
+ name: d.name,
118
+ deploymentId: d.id,
119
+ status: d.status,
120
+ ref: d.application?.ref,
121
+ runtime: settings?.runtime?.version,
122
+ target: {
123
+ provider: d.target?.provider,
124
+ targetId,
125
+ kind: isPrivateSpace ? 'private-space' : 'shared-region',
126
+ },
127
+ vCores: d.application?.vCores,
128
+ resources: settings?.resources,
129
+ replicas: {
130
+ count: d.target?.replicas ?? 0,
131
+ states: d.replicas?.map((r) => ({
132
+ id: r.id,
133
+ state: r.state,
134
+ location: r.deploymentLocation,
135
+ version: r.currentDeploymentVersion,
136
+ })),
137
+ },
138
+ updateStrategy: settings?.updateStrategy,
139
+ clustered: settings?.clustered,
140
+ autoscaling: settings?.autoscaling,
141
+ jvm: settings?.jvm,
142
+ publicUrl: settings?.http?.inbound?.publicUrl,
143
+ desiredVersion: d.desiredVersion,
144
+ lastSuccessfulVersion: d.lastSuccessfulVersion,
145
+ createdAt: d.creationDate ? new Date(d.creationDate).toISOString() : undefined,
146
+ updatedAt: d.lastModifiedDate ? new Date(d.lastModifiedDate).toISOString() : undefined,
147
+ });
148
+ }
149
+ catch (error) {
150
+ return mcpError(error);
151
+ }
152
+ });
93
153
  server.registerTool('get_app_resources', {
94
154
  title: 'Get Application Resources',
95
155
  description: 'Returns resource allocation for all apps in an environment: CPU/memory limits and reservations, vCores, replica count, autoscaling config, and JVM args. Use this to identify over-provisioned or under-provisioned applications, compare resource distribution, and optimize costs.',
@@ -101,14 +161,14 @@ export function registerApplicationTools(server, client) {
101
161
  try {
102
162
  const orgId = await client.getDefaultOrgId();
103
163
  const env = await client.accessManagement.resolveEnvironment(orgId, environment);
104
- const deployments = await client.cloudHub2.getDeployments(orgId, env.id);
164
+ const deployments = await client.cloudHub2.getDetailedDeployments(orgId, env.id);
105
165
  const resources = deployments.map((d) => ({
106
166
  name: d.name,
107
167
  status: d.status,
108
168
  vCores: d.application?.vCores,
109
169
  cpu: d.target?.deploymentSettings?.resources?.cpu,
110
170
  memory: d.target?.deploymentSettings?.resources?.memory,
111
- replicas: d.target?.replicas?.length ?? 0,
171
+ replicas: d.target?.replicas ?? 0,
112
172
  autoscaling: d.target?.deploymentSettings?.autoscaling,
113
173
  jvm: d.target?.deploymentSettings?.jvm,
114
174
  clustered: d.target?.deploymentSettings?.clustered,
@@ -142,13 +202,12 @@ export function registerApplicationTools(server, client) {
142
202
  try {
143
203
  const orgId = await client.getDefaultOrgId();
144
204
  const env = await client.accessManagement.resolveEnvironment(orgId, environment);
145
- const deployment = await client.cloudHub2.findByName(orgId, env.id, appName);
146
- if (!deployment) {
205
+ const detail = await client.cloudHub2.findDetailByName(orgId, env.id, appName);
206
+ if (!detail) {
147
207
  return {
148
208
  content: [{ type: 'text', text: `Application "${appName}" not found in ${env.name}` }],
149
209
  };
150
210
  }
151
- const detail = await client.cloudHub2.getDeployment(orgId, env.id, deployment.id);
152
211
  const config = (detail.application?.configuration ?? {});
153
212
  const propertiesService = config['mule.agent.application.properties.service'];
154
213
  const properties = (propertiesService?.properties ?? {});
@@ -272,81 +331,243 @@ export function registerApplicationTools(server, client) {
272
331
  .optional()
273
332
  .describe('Secure (encrypted) application properties as key-value pairs'),
274
333
  jvmArgs: z.string().optional().describe('JVM arguments (e.g. "-XX:MaxMetaspaceSize=256m")'),
334
+ confirm: z
335
+ .boolean()
336
+ .optional()
337
+ .describe('Set true to apply. When omitted/false, returns a dry-run preview and changes nothing.'),
275
338
  },
276
- annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
277
- }, async ({ appName, environment, groupId, artifactId, version, runtime, replicas, region, vcores, properties, secureProperties, jvmArgs, }) => {
339
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
340
+ }, async ({ appName, environment, groupId, artifactId, version, runtime, replicas, region, vcores, properties, secureProperties, jvmArgs, confirm, }) => {
278
341
  try {
279
342
  const orgId = await client.getDefaultOrgId();
280
343
  const env = await client.accessManagement.resolveEnvironment(orgId, environment);
281
- const existing = await client.cloudHub2.findByName(orgId, env.id, appName);
282
- const targetRegion = region || 'cloudhub-us-east-2';
283
- // Build configuration if properties are provided
284
- const configuration = {};
285
- if (properties || secureProperties) {
286
- configuration['mule.agent.application.properties.service'] = {
287
- applicationName: appName,
288
- ...(properties ? { properties } : {}),
289
- ...(secureProperties ? { secureProperties } : {}),
290
- };
291
- }
292
- const payload = {
293
- name: appName,
294
- application: {
295
- ref: {
296
- groupId,
297
- artifactId,
298
- version,
299
- packaging: 'jar',
300
- },
301
- desiredState: 'STARTED',
302
- ...(Object.keys(configuration).length > 0 ? { configuration } : {}),
303
- },
304
- target: {
305
- provider: 'MC',
306
- targetId: targetRegion,
307
- deploymentSettings: {
308
- runtime: { version: runtime || '4.8.0' },
309
- http: {
310
- inbound: {
311
- publicUrl: `${appName}.${targetRegion.replace('cloudhub-', '').replace(/-/g, '')}.cloudhub.io`,
312
- },
344
+ const existing = await client.cloudHub2.findDetailByName(orgId, env.id, appName);
345
+ // ── Update path: existing app → SAFE artifact-ref-only redeploy ──
346
+ // Infra params (runtime/region/vcores/replicas/jvmArgs) are intentionally NOT
347
+ // restated here; doing so previously clobbered the live deployment. They are
348
+ // ignored on redeploy and the caller is told so.
349
+ if (existing) {
350
+ const ignored = [
351
+ runtime && 'runtime',
352
+ region && 'region',
353
+ vcores && 'vcores',
354
+ replicas && 'replicas',
355
+ jvmArgs && 'jvmArgs',
356
+ properties && 'properties',
357
+ secureProperties && 'secureProperties',
358
+ ].filter(Boolean);
359
+ const merged = mergeForArtifactUpdate(existing, { groupId, artifactId, version });
360
+ const currentVersion = existing.application?.ref?.version || null;
361
+ if (!confirm) {
362
+ return dryRunPreview({
363
+ action: 'redeploy (artifact ref only)',
364
+ app: appName,
365
+ environment: env.name,
366
+ current: {
367
+ version: currentVersion,
368
+ runtime: existing.target?.deploymentSettings?.runtime?.version,
369
+ targetId: existing.target?.targetId,
370
+ replicas: existing.target?.replicas,
313
371
  },
314
- clustered: false,
315
- enforceDeployingReplicasAcrossNodes: false,
316
- updateStrategy: 'rolling',
317
- ...(jvmArgs ? { jvm: { args: jvmArgs } } : {}),
372
+ next: { ref: merged.application.ref },
373
+ preserved: 'runtime, target/space, replicas, resources, settings',
374
+ ...(ignored.length
375
+ ? {
376
+ note: `Ignored on redeploy (use update_app_settings / a fresh deploy to change infra): ${ignored.join(', ')}`,
377
+ }
378
+ : {}),
379
+ });
380
+ }
381
+ const deployment = await client.cloudHub2.updateArtifactRef(orgId, env.id, existing.id, merged.application.ref);
382
+ return mcpText({
383
+ message: `✅ Redeployed "${appName}" in ${env.name} (artifact ref only)`,
384
+ deploymentId: deployment.id,
385
+ status: deployment.status,
386
+ version: `${groupId}:${artifactId}:${version}`,
387
+ previousVersion: currentVersion,
388
+ preserved: 'runtime, target/space, replicas, resources, settings',
389
+ ...(ignored.length ? { ignored } : {}),
390
+ tip: 'Use get_app_status to monitor deployment progress.',
391
+ });
392
+ }
393
+ // ── Create path: new app → full payload from the shared builder ──
394
+ const payload = buildCreatePayload({
395
+ appName,
396
+ groupId,
397
+ artifactId,
398
+ version,
399
+ runtime,
400
+ replicas,
401
+ region,
402
+ vcores,
403
+ properties,
404
+ secureProperties,
405
+ jvmArgs,
406
+ });
407
+ if (!confirm) {
408
+ return dryRunPreview({
409
+ action: 'create new deployment',
410
+ app: appName,
411
+ environment: env.name,
412
+ next: {
413
+ version: `${groupId}:${artifactId}:${version}`,
414
+ runtime: payload.target.deploymentSettings.runtime.version,
415
+ region: payload.target.targetId,
416
+ vCores: payload.application.vCores,
417
+ replicas: payload.target.replicas,
318
418
  },
319
- replicas: replicas || 1,
320
- },
321
- };
322
- let deployment;
323
- let action;
324
- if (existing) {
325
- deployment = await client.cloudHub2.updateDeployment(orgId, env.id, existing.id, payload);
326
- action = 'Redeployed';
419
+ });
327
420
  }
328
- else {
329
- deployment = await client.cloudHub2.createDeployment(orgId, env.id, payload);
330
- action = 'Created new deployment';
421
+ const deployment = await client.cloudHub2.createDeployment(orgId, env.id, payload);
422
+ return mcpText({
423
+ message: `✅ Created new deployment for "${appName}" in ${env.name}`,
424
+ deploymentId: deployment.id,
425
+ status: deployment.status,
426
+ version: `${groupId}:${artifactId}:${version}`,
427
+ runtime: payload.target.deploymentSettings.runtime.version,
428
+ vCores: payload.application.vCores,
429
+ replicas: payload.target.replicas,
430
+ region: payload.target.targetId,
431
+ tip: 'Use get_app_status to monitor deployment progress.',
432
+ });
433
+ }
434
+ catch (error) {
435
+ return mcpError(error);
436
+ }
437
+ });
438
+ server.registerTool('update_app_artifact', {
439
+ title: 'Update Application Artifact (Safe Redeploy)',
440
+ description: 'Safely redeploys an existing CloudHub 2.0 application to a new artifact version by PATCHing ONLY the application reference. The live Mule runtime, deployment target/space, replica count, resources, and settings are all preserved — this is the correct tool for a production version bump, and is preferred over deploy_app for existing apps. Optionally waits for the deployment to reach a running state. Pass confirm:true to apply; without it you get a dry-run preview of the ref change.',
441
+ inputSchema: {
442
+ appName: z.string().describe('Application name exactly as deployed (case-insensitive match)'),
443
+ environment: z.string().describe('Environment name (e.g. "Production") or environment ID'),
444
+ version: z.string().describe('New artifact version to deploy (e.g. "1.4.12")'),
445
+ artifactId: z.string().optional().describe('Maven artifact ID. Default: keep the existing one.'),
446
+ groupId: z.string().optional().describe('Maven group ID. Default: keep the existing one.'),
447
+ packaging: z
448
+ .string()
449
+ .optional()
450
+ .describe('Artifact packaging. Default: keep existing (usually "jar").'),
451
+ wait: z
452
+ .boolean()
453
+ .optional()
454
+ .describe('Wait for the redeploy to reach a running state before returning (default: false).'),
455
+ confirm: z
456
+ .boolean()
457
+ .optional()
458
+ .describe('Set true to apply. When omitted/false, returns a dry-run preview and changes nothing.'),
459
+ },
460
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
461
+ }, async ({ appName, environment, version, artifactId, groupId, packaging, wait, confirm }) => {
462
+ try {
463
+ const orgId = await client.getDefaultOrgId();
464
+ const env = await client.accessManagement.resolveEnvironment(orgId, environment);
465
+ const existing = await client.cloudHub2.findDetailByName(orgId, env.id, appName);
466
+ if (!existing) {
467
+ return mcpText(`Application "${appName}" not found in ${env.name}. Use deploy_app to create a new deployment.`);
331
468
  }
332
- return {
333
- content: [
334
- {
335
- type: 'text',
336
- text: JSON.stringify({
337
- message: `✅ ${action} for "${appName}" in ${env.name}`,
338
- deploymentId: deployment.id,
339
- status: deployment.status,
340
- version: `${groupId}:${artifactId}:${version}`,
341
- runtime: runtime || '4.8.0',
342
- replicas: replicas || 1,
343
- region: targetRegion,
344
- previousVersion: existing?.application?.ref?.version || null,
345
- tip: 'Use get_app_status to monitor deployment progress.',
346
- }, null, 2),
347
- },
348
- ],
349
- };
469
+ const merged = mergeForArtifactUpdate(existing, { groupId, artifactId, version, packaging });
470
+ const oldRef = existing.application?.ref;
471
+ if (!confirm) {
472
+ return dryRunPreview({
473
+ action: 'update artifact ref (safe redeploy)',
474
+ app: appName,
475
+ environment: env.name,
476
+ current: { ref: oldRef },
477
+ next: { ref: merged.application.ref },
478
+ preserved: 'runtime, target/space, replicas, resources, settings',
479
+ });
480
+ }
481
+ let deployment = await client.cloudHub2.updateArtifactRef(orgId, env.id, existing.id, merged.application.ref);
482
+ let waitResult;
483
+ if (wait) {
484
+ try {
485
+ deployment = await client.cloudHub2.waitForDeployment(orgId, env.id, deployment.id);
486
+ waitResult = deployment.status;
487
+ }
488
+ catch (waitErr) {
489
+ waitResult = `did not settle: ${errorMessage(waitErr)}`;
490
+ }
491
+ }
492
+ return mcpText({
493
+ message: `✅ Updated "${appName}" in ${env.name} to v${version}`,
494
+ deploymentId: deployment.id,
495
+ status: deployment.status,
496
+ from: oldRef,
497
+ to: merged.application.ref,
498
+ preserved: 'runtime, target/space, replicas, resources, settings',
499
+ ...(wait ? { waitResult } : { tip: 'Pass wait:true or use get_app_status to monitor progress.' }),
500
+ });
501
+ }
502
+ catch (error) {
503
+ return mcpError(error);
504
+ }
505
+ });
506
+ server.registerTool('rollback_app', {
507
+ title: 'Roll Back Application',
508
+ description: 'Rolls an existing CloudHub 2.0 application back to a previous artifact by PATCHing only the application reference (runtime, target, replicas, and settings are preserved). By default it resolves the last successful or newest distinct historical artifact ref; pass toVersion to select a specific artifact version. Pass confirm:true to apply; without it you get a dry-run preview.',
509
+ inputSchema: {
510
+ appName: z.string().describe('Application name exactly as deployed (case-insensitive match)'),
511
+ environment: z.string().describe('Environment name (e.g. "Production") or environment ID'),
512
+ toVersion: z
513
+ .string()
514
+ .optional()
515
+ .describe('Artifact version to roll back to. Default: newest distinct historical artifact.'),
516
+ wait: z
517
+ .boolean()
518
+ .optional()
519
+ .describe('Wait for the rollback to reach a running state (default: false).'),
520
+ confirm: z
521
+ .boolean()
522
+ .optional()
523
+ .describe('Set true to apply. When omitted/false, returns a dry-run preview and changes nothing.'),
524
+ },
525
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
526
+ }, async ({ appName, environment, toVersion, wait, confirm }) => {
527
+ try {
528
+ const orgId = await client.getDefaultOrgId();
529
+ const env = await client.accessManagement.resolveEnvironment(orgId, environment);
530
+ const existing = await client.cloudHub2.findDetailByName(orgId, env.id, appName);
531
+ if (!existing) {
532
+ return mcpText(`Application "${appName}" not found in ${env.name}`);
533
+ }
534
+ const specs = await client.cloudHub2.getDeploymentSpecs(orgId, env.id, existing.id);
535
+ const target = resolveRollbackTarget(existing, specs, toVersion);
536
+ if (!target) {
537
+ return mcpText(`No distinct rollback target for "${appName}" in ${env.name}. ` +
538
+ 'Pass toVersion to select a specific artifact version.');
539
+ }
540
+ if (!confirm) {
541
+ return dryRunPreview({
542
+ action: 'rollback (artifact ref only)',
543
+ app: appName,
544
+ environment: env.name,
545
+ current: { ref: existing.application.ref, specId: existing.desiredVersion },
546
+ next: { ref: target.ref, sourceSpecId: target.sourceSpecId },
547
+ preserved: 'runtime, target/space, replicas, resources, settings',
548
+ });
549
+ }
550
+ let deployment = await client.cloudHub2.rollbackToRef(orgId, env.id, existing.id, target.ref);
551
+ let waitResult;
552
+ if (wait) {
553
+ try {
554
+ deployment = await client.cloudHub2.waitForDeployment(orgId, env.id, deployment.id);
555
+ waitResult = deployment.status;
556
+ }
557
+ catch (waitErr) {
558
+ waitResult = `did not settle: ${errorMessage(waitErr)}`;
559
+ }
560
+ }
561
+ return mcpText({
562
+ message: `✅ Rolled back "${appName}" in ${env.name}: ${existing.application.ref.version} → ${target.ref.version}`,
563
+ deploymentId: deployment.id,
564
+ status: deployment.status,
565
+ rolledBackFrom: existing.application.ref,
566
+ rolledBackTo: target.ref,
567
+ sourceSpecId: target.sourceSpecId,
568
+ preserved: 'runtime, target/space, replicas, resources, settings',
569
+ ...(wait ? { waitResult } : {}),
570
+ });
350
571
  }
351
572
  catch (error) {
352
573
  return mcpError(error);
@@ -370,7 +591,7 @@ export function registerApplicationTools(server, client) {
370
591
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
371
592
  }, async ({ appName, environment, properties, secureProperties }) => {
372
593
  try {
373
- if (!properties && !secureProperties) {
594
+ if (Object.keys(properties ?? {}).length + Object.keys(secureProperties ?? {}).length === 0) {
374
595
  return {
375
596
  content: [
376
597
  {
@@ -383,44 +604,15 @@ export function registerApplicationTools(server, client) {
383
604
  }
384
605
  const orgId = await client.getDefaultOrgId();
385
606
  const env = await client.accessManagement.resolveEnvironment(orgId, environment);
386
- const deployment = await client.cloudHub2.findByName(orgId, env.id, appName);
387
- if (!deployment) {
607
+ const detail = await client.cloudHub2.findDetailByName(orgId, env.id, appName);
608
+ if (!detail) {
388
609
  return {
389
610
  content: [{ type: 'text', text: `Application "${appName}" not found in ${env.name}` }],
390
611
  isError: true,
391
612
  };
392
613
  }
393
- // Fetch current deployment details to merge properties
394
- const detail = await client.cloudHub2.getDeployment(orgId, env.id, deployment.id);
395
- const existingConfig = (detail.application?.configuration ?? {});
396
- const existingService = (existingConfig['mule.agent.application.properties.service'] ?? {});
397
- const existingProps = (existingService.properties ?? {});
398
- const existingSecure = (existingService.secureProperties ?? {});
399
- const mergedProps = { ...existingProps, ...(properties || {}) };
400
- const mergedSecure = { ...existingSecure, ...(secureProperties || {}) };
401
- const runtimeVersion = deployment.target.deploymentSettings?.runtime?.version || '4.8.0';
402
- await client.cloudHub2.updateDeployment(orgId, env.id, deployment.id, {
403
- name: deployment.name,
404
- application: {
405
- ref: deployment.application.ref,
406
- desiredState: 'STARTED',
407
- configuration: {
408
- 'mule.agent.application.properties.service': {
409
- applicationName: deployment.name,
410
- properties: mergedProps,
411
- secureProperties: mergedSecure,
412
- },
413
- },
414
- },
415
- target: {
416
- provider: 'MC',
417
- targetId: deployment.target.targetId,
418
- deploymentSettings: {
419
- ...deployment.target.deploymentSettings,
420
- runtime: { version: runtimeVersion },
421
- },
422
- },
423
- });
614
+ const merged = mergeApplicationProperties(detail, properties, secureProperties);
615
+ await client.cloudHub2.updateApplicationConfiguration(orgId, env.id, detail.id, merged);
424
616
  return {
425
617
  content: [
426
618
  {
@@ -458,22 +650,7 @@ export function registerApplicationTools(server, client) {
458
650
  isError: true,
459
651
  };
460
652
  }
461
- const runtimeVersion = deployment.target.deploymentSettings?.runtime?.version || '4.8.0';
462
- await client.cloudHub2.updateDeployment(orgId, env.id, deployment.id, {
463
- name: deployment.name,
464
- application: {
465
- ref: deployment.application.ref,
466
- desiredState: 'STOPPED',
467
- },
468
- target: {
469
- provider: 'MC',
470
- targetId: deployment.target.targetId,
471
- deploymentSettings: {
472
- ...deployment.target.deploymentSettings,
473
- runtime: { version: runtimeVersion },
474
- },
475
- },
476
- });
653
+ await client.cloudHub2.setDesiredState(orgId, env.id, deployment.id, 'STOPPED');
477
654
  return {
478
655
  content: [
479
656
  {
@@ -506,22 +683,7 @@ export function registerApplicationTools(server, client) {
506
683
  isError: true,
507
684
  };
508
685
  }
509
- const runtimeVersion = deployment.target.deploymentSettings?.runtime?.version || '4.8.0';
510
- await client.cloudHub2.updateDeployment(orgId, env.id, deployment.id, {
511
- name: deployment.name,
512
- application: {
513
- ref: deployment.application.ref,
514
- desiredState: 'STARTED',
515
- },
516
- target: {
517
- provider: 'MC',
518
- targetId: deployment.target.targetId,
519
- deploymentSettings: {
520
- ...deployment.target.deploymentSettings,
521
- runtime: { version: runtimeVersion },
522
- },
523
- },
524
- });
686
+ await client.cloudHub2.setDesiredState(orgId, env.id, deployment.id, 'STARTED');
525
687
  return {
526
688
  content: [
527
689
  {