code-engine-mcp-server 1.0.4 → 1.0.7

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.
package/build/index.js CHANGED
@@ -10,7 +10,7 @@
10
10
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
12
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
13
- import { exec } from 'child_process';
13
+ import { execFile, spawn } from 'child_process';
14
14
  import { promisify } from 'util';
15
15
  import axios from 'axios';
16
16
  import { config as loadDotenv } from 'dotenv';
@@ -20,7 +20,70 @@ import { fileURLToPath } from 'url';
20
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
21
21
  loadDotenv({ path: resolve(__dirname, '../../.env') });
22
22
  loadDotenv({ path: resolve(__dirname, '../.env') });
23
- const execAsync = promisify(exec);
23
+ const execFileAsync = promisify(execFile);
24
+ // ── Input validation helpers ───────────────────────────────────────────────
25
+ // All container-facing user inputs are validated before they reach any shell
26
+ // or execFile call, preventing command injection.
27
+ function validateRuntime(r) {
28
+ const s = String(r || 'docker');
29
+ if (s !== 'docker' && s !== 'podman')
30
+ throw new Error(`Invalid container runtime "${s}" — must be "docker" or "podman"`);
31
+ return s;
32
+ }
33
+ // Image names: registry/namespace/name:tag or name@sha256:digest
34
+ function validateImageName(v) {
35
+ const s = String(v || '');
36
+ if (!s || !/^[a-zA-Z0-9._\-/:@]+$/.test(s))
37
+ throw new Error(`Invalid image name "${s}"`);
38
+ return s;
39
+ }
40
+ // Container IDs: short hex, full hex, or alphanumeric container name
41
+ function validateContainerId(v) {
42
+ const s = String(v || '');
43
+ if (!s || !/^[a-zA-Z0-9_.\-]+$/.test(s))
44
+ throw new Error(`Invalid container ID/name "${s}"`);
45
+ return s;
46
+ }
47
+ // Port mappings: hostPort:containerPort, e.g. 8080:8080
48
+ function validatePortMapping(v) {
49
+ const s = String(v || '');
50
+ if (!s || !/^\d{1,5}:\d{1,5}$/.test(s))
51
+ throw new Error(`Invalid port mapping "${s}" — expected "hostPort:containerPort"`);
52
+ return s;
53
+ }
54
+ // Environment variable names: POSIX identifier rules
55
+ function validateEnvKey(v) {
56
+ const s = String(v || '');
57
+ if (!s || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(s))
58
+ throw new Error(`Invalid environment variable name "${s}"`);
59
+ return s;
60
+ }
61
+ // Registry hostnames: hostname[:port]
62
+ function validateRegistryHost(v) {
63
+ const s = String(v || '');
64
+ if (!s || !/^[a-zA-Z0-9._\-]+(:\d+)?$/.test(s))
65
+ throw new Error(`Invalid registry hostname "${s}"`);
66
+ return s;
67
+ }
68
+ // Run registry login by piping the password via stdin — never via echo|pipe shell interpolation
69
+ function spawnWithStdin(cmd, args, stdinData) {
70
+ return new Promise((resolve, reject) => {
71
+ const proc = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'] });
72
+ let stdout = '';
73
+ let stderr = '';
74
+ proc.stdout.on('data', (d) => { stdout += d.toString(); });
75
+ proc.stderr.on('data', (d) => { stderr += d.toString(); });
76
+ proc.on('close', (code) => {
77
+ if (code === 0)
78
+ resolve({ stdout, stderr });
79
+ else
80
+ reject(new Error(`Command "${cmd} ${args.join(' ')}" exited with code ${code}: ${stderr || stdout}`));
81
+ });
82
+ proc.on('error', reject);
83
+ proc.stdin.write(stdinData);
84
+ proc.stdin.end();
85
+ });
86
+ }
24
87
  const CE_REGIONS = ['us-south', 'us-east', 'eu-de', 'eu-gb', 'jp-tok', 'jp-osa', 'au-syd', 'ca-tor', 'br-sao'];
25
88
  // Helper function to get IAM token
26
89
  async function getIAMToken(apiKey) {
@@ -86,7 +149,7 @@ async function resolveProjectId(nameOrId, token) {
86
149
  // Create MCP server
87
150
  const server = new Server({
88
151
  name: 'code-engine-mcp-server',
89
- version: '1.0.3',
152
+ version: '1.0.7',
90
153
  }, {
91
154
  capabilities: {
92
155
  tools: {},
@@ -233,6 +296,69 @@ const containerTools = [
233
296
  required: ['image'],
234
297
  },
235
298
  },
299
+ {
300
+ name: 'tag_container_image',
301
+ description: 'Tag a local container image with a new name/tag — useful to retag before pushing to ICR',
302
+ inputSchema: {
303
+ type: 'object',
304
+ properties: {
305
+ source_image: { type: 'string', description: 'Existing image name/tag (e.g. myapp:latest)' },
306
+ target_image: { type: 'string', description: 'New image name/tag (e.g. us.icr.io/mynamespace/myapp:v1.0.0)' },
307
+ runtime: { type: 'string', enum: ['docker', 'podman'], description: 'Container runtime (default: auto-detected)' },
308
+ },
309
+ required: ['source_image', 'target_image'],
310
+ },
311
+ },
312
+ {
313
+ name: 'remove_local_image',
314
+ description: 'Remove a local container image to free disk space',
315
+ inputSchema: {
316
+ type: 'object',
317
+ properties: {
318
+ image_name: { type: 'string', description: 'Image name/tag to remove (e.g. us.icr.io/mynamespace/myapp:v1.0.0)' },
319
+ force: { type: 'boolean', description: 'Force removal even if the image is used by a stopped container (default: false)' },
320
+ runtime: { type: 'string', enum: ['docker', 'podman'] },
321
+ },
322
+ required: ['image_name'],
323
+ },
324
+ },
325
+ {
326
+ name: 'login_to_registry',
327
+ description: 'Log in to a container registry (e.g. IBM Container Registry) so images can be pushed/pulled. Uses IBM Cloud IAM token for ICR or username/password for other registries.',
328
+ inputSchema: {
329
+ type: 'object',
330
+ properties: {
331
+ registry: { type: 'string', description: 'Registry hostname (default: us.icr.io for ICR)' },
332
+ username: { type: 'string', description: 'Username — use "iamapikey" for ICR with an IBM Cloud API key' },
333
+ password: { type: 'string', description: 'Password or API key. For ICR leave blank to use IBMCLOUD_API_KEY env var.' },
334
+ runtime: { type: 'string', enum: ['docker', 'podman'] },
335
+ },
336
+ required: ['registry'],
337
+ },
338
+ },
339
+ {
340
+ name: 'inspect_container_image',
341
+ description: 'Inspect a local container image — shows architecture, labels, environment variables, entrypoint, exposed ports, and layer count',
342
+ inputSchema: {
343
+ type: 'object',
344
+ properties: {
345
+ image_name: { type: 'string', description: 'Image name/tag to inspect' },
346
+ runtime: { type: 'string', enum: ['docker', 'podman'] },
347
+ },
348
+ required: ['image_name'],
349
+ },
350
+ },
351
+ {
352
+ name: 'prune_images',
353
+ description: 'Remove unused/dangling container images to reclaim disk space. By default removes only dangling images; use all=true to remove all unused images.',
354
+ inputSchema: {
355
+ type: 'object',
356
+ properties: {
357
+ all: { type: 'boolean', description: 'Remove all unused images, not just dangling ones (default: false)' },
358
+ runtime: { type: 'string', enum: ['docker', 'podman'] },
359
+ },
360
+ },
361
+ },
236
362
  ];
237
363
  // Code Engine Tools
238
364
  const codeEngineTools = [
@@ -322,6 +448,8 @@ const codeEngineTools = [
322
448
  scale_cpu_limit: { type: 'string', description: 'CPU limit (e.g. 1, 0.5)' },
323
449
  scale_memory_limit: { type: 'string', description: 'Memory limit (e.g. 4G, 2G)' },
324
450
  env_vars: { type: 'object', description: 'Key/value environment variables' },
451
+ run_args: { type: 'array', items: { type: 'string' }, description: 'Arguments passed to the container entrypoint (run_arguments). Required for supergateway: ["--stdio", "npx -y <mcp-server>", "--outputTransport", "sse"]' },
452
+ run_commands: { type: 'array', items: { type: 'string' }, description: 'Override the container entrypoint (run_commands). Rarely needed — use run_args for passing flags.' },
325
453
  },
326
454
  required: ['project_id', 'name', 'image'],
327
455
  },
@@ -340,6 +468,8 @@ const codeEngineTools = [
340
468
  scale_max_instances: { type: 'number' },
341
469
  scale_cpu_limit: { type: 'string' },
342
470
  scale_memory_limit: { type: 'string' },
471
+ run_args: { type: 'array', items: { type: 'string' }, description: 'Arguments passed to the container entrypoint (run_arguments)' },
472
+ run_commands: { type: 'array', items: { type: 'string' }, description: 'Override the container entrypoint (run_commands)' },
343
473
  },
344
474
  required: ['project_id', 'app_name'],
345
475
  },
@@ -383,15 +513,16 @@ const codeEngineTools = [
383
513
  },
384
514
  {
385
515
  name: 'ce_get_app_logs',
386
- description: 'Get logs for a specific Code Engine application instance',
516
+ description: 'Get logs for a Code Engine application. Retrieves logs from all running pods (or a specific instance) via the Code Engine Kubernetes API proxy.',
387
517
  inputSchema: {
388
518
  type: 'object',
389
519
  properties: {
390
- project_id: { type: 'string' },
391
- app_name: { type: 'string' },
392
- instance_name: { type: 'string', description: 'Instance name (e.g. my-app-00001-deployment-abcde)' },
520
+ project_id: { type: 'string', description: 'Project ID or name' },
521
+ app_name: { type: 'string', description: 'Application name' },
522
+ instance_name: { type: 'string', description: 'Optional: specific pod/instance name to filter to (e.g. my-app-00001-deployment-abcde). If omitted, logs from all pods are returned.' },
523
+ tail_lines: { type: 'number', description: 'Number of log lines to return per pod (default: 100)' },
393
524
  },
394
- required: ['project_id', 'app_name', 'instance_name'],
525
+ required: ['project_id', 'app_name'],
395
526
  },
396
527
  },
397
528
  // --- Builds ---
@@ -777,6 +908,19 @@ const codeEngineTools = [
777
908
  required: ['project_id', 'secret_name', 'data'],
778
909
  },
779
910
  },
911
+ {
912
+ name: 'ce_refresh_icr_pull_secret',
913
+ description: 'Refresh a Code Engine registry pull secret for IBM Container Registry (ICR) using the server\'s own IBM Cloud API key. Automatically deletes the existing secret and recreates it with fresh credentials. Use this when deployments fail with "no_revision_ready" or "unknown" status — a common cause is a stale or expired ICR pull secret. No API key input required — the server uses its own IBMCLOUD_API_KEY.',
914
+ inputSchema: {
915
+ type: 'object',
916
+ properties: {
917
+ project_id: { type: 'string' },
918
+ secret_name: { type: 'string', description: 'Name of the registry secret to refresh (default: icr-pull-secret)' },
919
+ icr_host: { type: 'string', description: 'ICR registry hostname (default: us.icr.io)' },
920
+ },
921
+ required: ['project_id'],
922
+ },
923
+ },
780
924
  {
781
925
  name: 'ce_renew_tls_secret_from_pem',
782
926
  description: 'Renew an existing TLS secret in Code Engine by reading updated PEM files from disk. Use this when a Let\'s Encrypt cert has been renewed (every 90 days). Updates the secret in-place — no need to delete and recreate or update domain mappings.',
@@ -867,31 +1011,574 @@ const codeEngineTools = [
867
1011
  inputSchema: {
868
1012
  type: 'object',
869
1013
  properties: {
870
- project_id_or_name: { type: 'string', description: 'Code Engine project name or ID' },
871
- app_name: { type: 'string', description: 'The Code Engine app to map the custom domain to' },
872
- domain_name: { type: 'string', description: 'Your custom domain (e.g. myapp.example.com)' },
873
- tls_secret_name: { type: 'string', description: 'Name to give the TLS secret in Code Engine (e.g. myapp-tls). Must be unique in the project.' },
874
- cert_pem_path: { type: 'string', description: 'Path to the certificate chain PEM file — typically ~/certbot/config/live/<domain>/fullchain.pem' },
875
- key_pem_path: { type: 'string', description: 'Path to the private key PEM file — typically ~/certbot/config/live/<domain>/privkey.pem' },
1014
+ project_id_or_name: { type: 'string', description: 'Code Engine project name or ID' },
1015
+ app_name: { type: 'string', description: 'The Code Engine app to map the custom domain to' },
1016
+ domain_name: { type: 'string', description: 'Your custom domain (e.g. myapp.example.com)' },
1017
+ tls_secret_name: { type: 'string', description: 'Name to give the TLS secret in Code Engine (e.g. myapp-tls). Must be unique in the project.' },
1018
+ cert_pem_path: { type: 'string', description: 'Path to the certificate chain PEM file — typically ~/certbot/config/live/<domain>/fullchain.pem' },
1019
+ key_pem_path: { type: 'string', description: 'Path to the private key PEM file — typically ~/certbot/config/live/<domain>/privkey.pem' },
1020
+ },
1021
+ required: ['project_id_or_name', 'app_name', 'domain_name', 'tls_secret_name', 'cert_pem_path', 'key_pem_path'],
1022
+ },
1023
+ },
1024
+ {
1025
+ name: 'proc_build_run_and_deploy',
1026
+ description: 'PROCEDURE: Code Engine source-to-image build + deploy in one step — starts a build run from an existing build configuration, polls until it succeeds, then creates or updates the application with the new image, and waits for it to become ready. Returns the public URL. The build configuration must already exist (use ce_create_build to set one up).',
1027
+ inputSchema: {
1028
+ type: 'object',
1029
+ properties: {
1030
+ project_id_or_name: { type: 'string', description: 'Code Engine project name or ID' },
1031
+ build_name: { type: 'string', description: 'Name of the existing Code Engine build configuration to run (use ce_list_builds to find it)' },
1032
+ app_name: { type: 'string', description: 'Application to create or update after the build succeeds' },
1033
+ image_secret: { type: 'string', description: 'Registry pull secret name in Code Engine (e.g. icr-pull-secret)' },
1034
+ port: { type: 'number', description: 'Container port the app listens on (default 8080)' },
1035
+ build_timeout_seconds: { type: 'number', description: 'Max seconds to wait for the build to finish (default 600)' },
1036
+ deploy_timeout_seconds: { type: 'number', description: 'Max seconds to wait for the app to become ready (default 180)' },
1037
+ },
1038
+ required: ['project_id_or_name', 'build_name', 'app_name', 'image_secret'],
1039
+ },
1040
+ },
1041
+ // ─── App Revisions ────────────────────────────────────────────────────────────
1042
+ {
1043
+ name: 'ce_list_app_revisions',
1044
+ description: 'List all revisions (deployed versions) of a Code Engine application',
1045
+ inputSchema: {
1046
+ type: 'object',
1047
+ properties: {
1048
+ project_id: { type: 'string' },
1049
+ app_name: { type: 'string' },
1050
+ },
1051
+ required: ['project_id', 'app_name'],
1052
+ },
1053
+ },
1054
+ {
1055
+ name: 'ce_get_app_revision',
1056
+ description: 'Get details of a specific revision of a Code Engine application',
1057
+ inputSchema: {
1058
+ type: 'object',
1059
+ properties: {
1060
+ project_id: { type: 'string' },
1061
+ app_name: { type: 'string' },
1062
+ revision_name: { type: 'string' },
1063
+ },
1064
+ required: ['project_id', 'app_name', 'revision_name'],
1065
+ },
1066
+ },
1067
+ {
1068
+ name: 'ce_delete_app_revision',
1069
+ description: 'Delete a specific revision of a Code Engine application',
1070
+ inputSchema: {
1071
+ type: 'object',
1072
+ properties: {
1073
+ project_id: { type: 'string' },
1074
+ app_name: { type: 'string' },
1075
+ revision_name: { type: 'string' },
1076
+ },
1077
+ required: ['project_id', 'app_name', 'revision_name'],
1078
+ },
1079
+ },
1080
+ // ─── Update operations ────────────────────────────────────────────────────────
1081
+ {
1082
+ name: 'ce_update_job',
1083
+ description: 'Update an existing Code Engine job definition (PATCH)',
1084
+ inputSchema: {
1085
+ type: 'object',
1086
+ properties: {
1087
+ project_id: { type: 'string' },
1088
+ job_name: { type: 'string' },
1089
+ image: { type: 'string', description: 'New container image reference' },
1090
+ image_secret: { type: 'string' },
1091
+ scale_array_spec: { type: 'string', description: 'Array indices to run (e.g. "0-9")' },
1092
+ scale_cpu_limit: { type: 'string' },
1093
+ scale_memory_limit: { type: 'string' },
1094
+ env_vars: { type: 'object', description: 'Key/value environment variables' },
1095
+ },
1096
+ required: ['project_id', 'job_name'],
1097
+ },
1098
+ },
1099
+ {
1100
+ name: 'ce_update_build',
1101
+ description: 'Update an existing Code Engine build configuration (PATCH)',
1102
+ inputSchema: {
1103
+ type: 'object',
1104
+ properties: {
1105
+ project_id: { type: 'string' },
1106
+ build_name: { type: 'string' },
1107
+ output_image: { type: 'string' },
1108
+ output_secret: { type: 'string' },
1109
+ source_url: { type: 'string' },
1110
+ source_revision: { type: 'string' },
1111
+ strategy_size: { type: 'string', enum: ['small', 'medium', 'large', 'xlarge', 'xxlarge'] },
1112
+ strategy_spec_file: { type: 'string', description: 'Path to Dockerfile or buildpacks config (default: Dockerfile)' },
1113
+ },
1114
+ required: ['project_id', 'build_name'],
1115
+ },
1116
+ },
1117
+ {
1118
+ name: 'ce_update_config_map',
1119
+ description: 'Update an existing Code Engine configmap (PATCH)',
1120
+ inputSchema: {
1121
+ type: 'object',
1122
+ properties: {
1123
+ project_id: { type: 'string' },
1124
+ config_map_name: { type: 'string' },
1125
+ data: { type: 'object', description: 'New key/value data to replace configmap contents' },
1126
+ },
1127
+ required: ['project_id', 'config_map_name', 'data'],
1128
+ },
1129
+ },
1130
+ {
1131
+ name: 'ce_update_domain_mapping',
1132
+ description: 'Update an existing Code Engine custom domain mapping (PATCH) — e.g. change the target app',
1133
+ inputSchema: {
1134
+ type: 'object',
1135
+ properties: {
1136
+ project_id: { type: 'string' },
1137
+ domain_name: { type: 'string' },
1138
+ app_name: { type: 'string', description: 'New application to route traffic to' },
1139
+ tls_secret: { type: 'string', description: 'New TLS secret name (optional)' },
1140
+ },
1141
+ required: ['project_id', 'domain_name'],
1142
+ },
1143
+ },
1144
+ // ─── Functions ────────────────────────────────────────────────────────────────
1145
+ {
1146
+ name: 'ce_list_function_runtimes',
1147
+ description: 'List all available function runtimes supported by IBM Code Engine (no project required)',
1148
+ inputSchema: {
1149
+ type: 'object',
1150
+ properties: {
1151
+ region: { type: 'string', description: 'CE region (e.g. us-south). Defaults to us-south.' },
1152
+ },
1153
+ required: [],
1154
+ },
1155
+ },
1156
+ {
1157
+ name: 'ce_list_functions',
1158
+ description: 'List all serverless functions in a Code Engine project',
1159
+ inputSchema: {
1160
+ type: 'object',
1161
+ properties: {
1162
+ project_id: { type: 'string' },
1163
+ },
1164
+ required: ['project_id'],
1165
+ },
1166
+ },
1167
+ {
1168
+ name: 'ce_get_function',
1169
+ description: 'Get details of a specific serverless function in a Code Engine project',
1170
+ inputSchema: {
1171
+ type: 'object',
1172
+ properties: {
1173
+ project_id: { type: 'string' },
1174
+ function_name: { type: 'string' },
1175
+ },
1176
+ required: ['project_id', 'function_name'],
1177
+ },
1178
+ },
1179
+ {
1180
+ name: 'ce_create_function',
1181
+ description: 'Create a serverless function in a Code Engine project',
1182
+ inputSchema: {
1183
+ type: 'object',
1184
+ properties: {
1185
+ project_id: { type: 'string' },
1186
+ name: { type: 'string' },
1187
+ runtime: { type: 'string', description: 'Runtime identifier (e.g. nodejs-20, python-3.11). Use ce_list_function_runtimes to see all options.' },
1188
+ code_reference: { type: 'string', description: 'Inline code as a data URL or reference to a code bundle image' },
1189
+ code_main: { type: 'string', description: 'Entry point function name (default: main)' },
1190
+ scale_concurrency: { type: 'number', description: 'Max requests per instance (default: 1)' },
1191
+ scale_cpu_limit: { type: 'string', description: 'CPU limit (default: 1)' },
1192
+ scale_memory_limit: { type: 'string', description: 'Memory limit (default: 4G)' },
1193
+ env_vars: { type: 'object', description: 'Key/value environment variables' },
1194
+ },
1195
+ required: ['project_id', 'name', 'runtime', 'code_reference'],
1196
+ },
1197
+ },
1198
+ {
1199
+ name: 'ce_update_function',
1200
+ description: 'Update an existing Code Engine serverless function (PATCH)',
1201
+ inputSchema: {
1202
+ type: 'object',
1203
+ properties: {
1204
+ project_id: { type: 'string' },
1205
+ function_name: { type: 'string' },
1206
+ runtime: { type: 'string' },
1207
+ code_reference: { type: 'string' },
1208
+ code_main: { type: 'string' },
1209
+ scale_concurrency: { type: 'number' },
1210
+ scale_cpu_limit: { type: 'string' },
1211
+ scale_memory_limit: { type: 'string' },
1212
+ env_vars: { type: 'object' },
1213
+ },
1214
+ required: ['project_id', 'function_name'],
1215
+ },
1216
+ },
1217
+ {
1218
+ name: 'ce_delete_function',
1219
+ description: 'Delete a serverless function from a Code Engine project',
1220
+ inputSchema: {
1221
+ type: 'object',
1222
+ properties: {
1223
+ project_id: { type: 'string' },
1224
+ function_name: { type: 'string' },
1225
+ },
1226
+ required: ['project_id', 'function_name'],
1227
+ },
1228
+ },
1229
+ // ─── Service Bindings ─────────────────────────────────────────────────────────
1230
+ {
1231
+ name: 'ce_list_bindings',
1232
+ description: 'List all service bindings in a Code Engine project (bindings connect IBM Cloud services to apps/jobs)',
1233
+ inputSchema: {
1234
+ type: 'object',
1235
+ properties: {
1236
+ project_id: { type: 'string' },
1237
+ },
1238
+ required: ['project_id'],
1239
+ },
1240
+ },
1241
+ {
1242
+ name: 'ce_create_binding',
1243
+ description: 'Create a service binding to connect an IBM Cloud service instance to a Code Engine component',
1244
+ inputSchema: {
1245
+ type: 'object',
1246
+ properties: {
1247
+ project_id: { type: 'string' },
1248
+ component_name: { type: 'string', description: 'Name of the CE app or job to bind the service to' },
1249
+ component_resource_type: { type: 'string', enum: ['app_v2', 'job_v2', 'function_v2'], description: 'Resource type of the component' },
1250
+ secret_name: { type: 'string', description: 'Name of the operator secret referencing the IBM Cloud service instance' },
1251
+ prefix: { type: 'string', description: 'Prefix for environment variable names injected into the component (optional)' },
1252
+ },
1253
+ required: ['project_id', 'component_name', 'component_resource_type', 'secret_name'],
1254
+ },
1255
+ },
1256
+ {
1257
+ name: 'ce_get_binding',
1258
+ description: 'Get details of a specific service binding in a Code Engine project',
1259
+ inputSchema: {
1260
+ type: 'object',
1261
+ properties: {
1262
+ project_id: { type: 'string' },
1263
+ binding_id: { type: 'string', description: 'The binding ID (use ce_list_bindings to find it)' },
1264
+ },
1265
+ required: ['project_id', 'binding_id'],
1266
+ },
1267
+ },
1268
+ {
1269
+ name: 'ce_delete_binding',
1270
+ description: 'Delete a service binding from a Code Engine project',
1271
+ inputSchema: {
1272
+ type: 'object',
1273
+ properties: {
1274
+ project_id: { type: 'string' },
1275
+ binding_id: { type: 'string' },
1276
+ },
1277
+ required: ['project_id', 'binding_id'],
1278
+ },
1279
+ },
1280
+ // ─── Project extras ────────────────────────────────────────────────────────────
1281
+ {
1282
+ name: 'ce_get_project_status',
1283
+ description: 'Get the status details of a Code Engine project (readiness, enabled components, etc.)',
1284
+ inputSchema: {
1285
+ type: 'object',
1286
+ properties: {
1287
+ project_id: { type: 'string' },
1288
+ },
1289
+ required: ['project_id'],
1290
+ },
1291
+ },
1292
+ {
1293
+ name: 'ce_list_egress_ips',
1294
+ description: 'List the public egress IP addresses used by a Code Engine project (useful for allowlisting in firewalls)',
1295
+ inputSchema: {
1296
+ type: 'object',
1297
+ properties: {
1298
+ project_id: { type: 'string' },
1299
+ },
1300
+ required: ['project_id'],
1301
+ },
1302
+ },
1303
+ {
1304
+ name: 'ce_list_allowed_outbound_destinations',
1305
+ description: 'List allowed outbound destinations configured for a Code Engine project',
1306
+ inputSchema: {
1307
+ type: 'object',
1308
+ properties: {
1309
+ project_id: { type: 'string' },
1310
+ },
1311
+ required: ['project_id'],
1312
+ },
1313
+ },
1314
+ {
1315
+ name: 'ce_create_allowed_outbound_destination',
1316
+ description: 'Create an allowed outbound destination (CIDR block or domain) for a Code Engine project',
1317
+ inputSchema: {
1318
+ type: 'object',
1319
+ properties: {
1320
+ project_id: { type: 'string' },
1321
+ name: { type: 'string', description: 'Name for this allowed outbound destination rule' },
1322
+ type: { type: 'string', enum: ['cidr_block', 'fqdn'], description: 'Type of destination: CIDR block or fully-qualified domain name' },
1323
+ cidr_block: { type: 'string', description: 'CIDR block (required when type=cidr_block, e.g. 1.2.3.4/24)' },
1324
+ fqdn: { type: 'string', description: 'Fully-qualified domain name (required when type=fqdn, e.g. example.com)' },
1325
+ },
1326
+ required: ['project_id', 'name', 'type'],
1327
+ },
1328
+ },
1329
+ {
1330
+ name: 'ce_get_allowed_outbound_destination',
1331
+ description: 'Get details of a specific allowed outbound destination in a Code Engine project',
1332
+ inputSchema: {
1333
+ type: 'object',
1334
+ properties: {
1335
+ project_id: { type: 'string' },
1336
+ destination_name: { type: 'string' },
1337
+ },
1338
+ required: ['project_id', 'destination_name'],
1339
+ },
1340
+ },
1341
+ {
1342
+ name: 'ce_update_allowed_outbound_destination',
1343
+ description: 'Update an existing allowed outbound destination in a Code Engine project (PATCH)',
1344
+ inputSchema: {
1345
+ type: 'object',
1346
+ properties: {
1347
+ project_id: { type: 'string' },
1348
+ destination_name: { type: 'string' },
1349
+ cidr_block: { type: 'string', description: 'New CIDR block value' },
1350
+ fqdn: { type: 'string', description: 'New FQDN value' },
1351
+ },
1352
+ required: ['project_id', 'destination_name'],
1353
+ },
1354
+ },
1355
+ {
1356
+ name: 'ce_delete_allowed_outbound_destination',
1357
+ description: 'Delete an allowed outbound destination from a Code Engine project',
1358
+ inputSchema: {
1359
+ type: 'object',
1360
+ properties: {
1361
+ project_id: { type: 'string' },
1362
+ destination_name: { type: 'string' },
1363
+ },
1364
+ required: ['project_id', 'destination_name'],
1365
+ },
1366
+ },
1367
+ // ─── Persistent Data Stores ───────────────────────────────────────────────────
1368
+ {
1369
+ name: 'ce_list_persistent_data_stores',
1370
+ description: 'List all persistent data stores (cloud object storage bindings) in a Code Engine project',
1371
+ inputSchema: {
1372
+ type: 'object',
1373
+ properties: {
1374
+ project_id: { type: 'string' },
1375
+ },
1376
+ required: ['project_id'],
1377
+ },
1378
+ },
1379
+ {
1380
+ name: 'ce_create_persistent_data_store',
1381
+ description: 'Create a persistent data store binding (COS bucket) in a Code Engine project',
1382
+ inputSchema: {
1383
+ type: 'object',
1384
+ properties: {
1385
+ project_id: { type: 'string' },
1386
+ name: { type: 'string', description: 'Name for this persistent data store' },
1387
+ secret_name: { type: 'string', description: 'Name of the secret containing COS credentials' },
1388
+ bucket_name: { type: 'string', description: 'COS bucket name to bind' },
1389
+ endpoint: { type: 'string', description: 'COS endpoint URL (e.g. https://s3.us-south.cloud-object-storage.appdomain.cloud)' },
1390
+ },
1391
+ required: ['project_id', 'name', 'secret_name', 'bucket_name'],
1392
+ },
1393
+ },
1394
+ {
1395
+ name: 'ce_get_persistent_data_store',
1396
+ description: 'Get details of a specific persistent data store in a Code Engine project',
1397
+ inputSchema: {
1398
+ type: 'object',
1399
+ properties: {
1400
+ project_id: { type: 'string' },
1401
+ data_store_name: { type: 'string' },
1402
+ },
1403
+ required: ['project_id', 'data_store_name'],
1404
+ },
1405
+ },
1406
+ {
1407
+ name: 'ce_delete_persistent_data_store',
1408
+ description: 'Delete a persistent data store from a Code Engine project',
1409
+ inputSchema: {
1410
+ type: 'object',
1411
+ properties: {
1412
+ project_id: { type: 'string' },
1413
+ data_store_name: { type: 'string' },
1414
+ },
1415
+ required: ['project_id', 'data_store_name'],
1416
+ },
1417
+ },
1418
+ // ─── Fleets ────────────────────────────────────────────────────────────────────
1419
+ {
1420
+ name: 'ce_list_fleets',
1421
+ description: 'List all fleets in a Code Engine project',
1422
+ inputSchema: {
1423
+ type: 'object',
1424
+ properties: {
1425
+ project_id: { type: 'string' },
1426
+ },
1427
+ required: ['project_id'],
1428
+ },
1429
+ },
1430
+ {
1431
+ name: 'ce_create_fleet',
1432
+ description: 'Create a fleet in a Code Engine project',
1433
+ inputSchema: {
1434
+ type: 'object',
1435
+ properties: {
1436
+ project_id: { type: 'string' },
1437
+ name: { type: 'string' },
1438
+ image: { type: 'string', description: 'Container image reference' },
1439
+ image_secret: { type: 'string' },
1440
+ scale_cpu_limit: { type: 'string' },
1441
+ scale_memory_limit: { type: 'string' },
1442
+ env_vars: { type: 'object' },
1443
+ },
1444
+ required: ['project_id', 'name', 'image'],
1445
+ },
1446
+ },
1447
+ {
1448
+ name: 'ce_get_fleet',
1449
+ description: 'Get details of a specific fleet in a Code Engine project',
1450
+ inputSchema: {
1451
+ type: 'object',
1452
+ properties: {
1453
+ project_id: { type: 'string' },
1454
+ fleet_id: { type: 'string' },
1455
+ },
1456
+ required: ['project_id', 'fleet_id'],
1457
+ },
1458
+ },
1459
+ {
1460
+ name: 'ce_delete_fleet',
1461
+ description: 'Delete a fleet from a Code Engine project',
1462
+ inputSchema: {
1463
+ type: 'object',
1464
+ properties: {
1465
+ project_id: { type: 'string' },
1466
+ fleet_id: { type: 'string' },
1467
+ },
1468
+ required: ['project_id', 'fleet_id'],
1469
+ },
1470
+ },
1471
+ {
1472
+ name: 'ce_cancel_fleet',
1473
+ description: 'Cancel a running fleet in a Code Engine project',
1474
+ inputSchema: {
1475
+ type: 'object',
1476
+ properties: {
1477
+ project_id: { type: 'string' },
1478
+ fleet_id: { type: 'string' },
1479
+ },
1480
+ required: ['project_id', 'fleet_id'],
1481
+ },
1482
+ },
1483
+ // ─── Fleet Tasks ──────────────────────────────────────────────────────────────
1484
+ {
1485
+ name: 'ce_list_fleet_tasks',
1486
+ description: 'List all tasks for a fleet in a Code Engine project',
1487
+ inputSchema: {
1488
+ type: 'object',
1489
+ properties: {
1490
+ project_id: { type: 'string' },
1491
+ fleet_id: { type: 'string' },
1492
+ },
1493
+ required: ['project_id', 'fleet_id'],
1494
+ },
1495
+ },
1496
+ {
1497
+ name: 'ce_get_fleet_task',
1498
+ description: 'Get details of a specific task within a fleet',
1499
+ inputSchema: {
1500
+ type: 'object',
1501
+ properties: {
1502
+ project_id: { type: 'string' },
1503
+ fleet_id: { type: 'string' },
1504
+ task_id: { type: 'string' },
1505
+ },
1506
+ required: ['project_id', 'fleet_id', 'task_id'],
1507
+ },
1508
+ },
1509
+ // ─── Fleet Workers ────────────────────────────────────────────────────────────
1510
+ {
1511
+ name: 'ce_list_fleet_workers',
1512
+ description: 'List all workers for a fleet in a Code Engine project',
1513
+ inputSchema: {
1514
+ type: 'object',
1515
+ properties: {
1516
+ project_id: { type: 'string' },
1517
+ fleet_id: { type: 'string' },
1518
+ },
1519
+ required: ['project_id', 'fleet_id'],
1520
+ },
1521
+ },
1522
+ {
1523
+ name: 'ce_get_fleet_worker',
1524
+ description: 'Get details of a specific worker within a fleet',
1525
+ inputSchema: {
1526
+ type: 'object',
1527
+ properties: {
1528
+ project_id: { type: 'string' },
1529
+ fleet_id: { type: 'string' },
1530
+ worker_id: { type: 'string' },
1531
+ },
1532
+ required: ['project_id', 'fleet_id', 'worker_id'],
1533
+ },
1534
+ },
1535
+ // ─── Subnet Pools ─────────────────────────────────────────────────────────────
1536
+ {
1537
+ name: 'ce_list_subnet_pools',
1538
+ description: 'List all subnet pools in a Code Engine project',
1539
+ inputSchema: {
1540
+ type: 'object',
1541
+ properties: {
1542
+ project_id: { type: 'string' },
1543
+ },
1544
+ required: ['project_id'],
1545
+ },
1546
+ },
1547
+ {
1548
+ name: 'ce_create_subnet_pool',
1549
+ description: 'Create a subnet pool in a Code Engine project',
1550
+ inputSchema: {
1551
+ type: 'object',
1552
+ properties: {
1553
+ project_id: { type: 'string' },
1554
+ name: { type: 'string' },
1555
+ cidr: { type: 'string', description: 'CIDR block for the subnet pool (e.g. 10.0.0.0/24)' },
1556
+ },
1557
+ required: ['project_id', 'name', 'cidr'],
1558
+ },
1559
+ },
1560
+ {
1561
+ name: 'ce_get_subnet_pool',
1562
+ description: 'Get details of a specific subnet pool in a Code Engine project',
1563
+ inputSchema: {
1564
+ type: 'object',
1565
+ properties: {
1566
+ project_id: { type: 'string' },
1567
+ subnet_pool_id: { type: 'string' },
876
1568
  },
877
- required: ['project_id_or_name', 'app_name', 'domain_name', 'tls_secret_name', 'cert_pem_path', 'key_pem_path'],
1569
+ required: ['project_id', 'subnet_pool_id'],
878
1570
  },
879
1571
  },
880
1572
  {
881
- name: 'proc_build_run_and_deploy',
882
- description: 'PROCEDURE: Code Engine source-to-image build + deploy in one step — starts a build run from an existing build configuration, polls until it succeeds, then creates or updates the application with the new image, and waits for it to become ready. Returns the public URL. The build configuration must already exist (use ce_create_build to set one up).',
1573
+ name: 'ce_delete_subnet_pool',
1574
+ description: 'Delete a subnet pool from a Code Engine project',
883
1575
  inputSchema: {
884
1576
  type: 'object',
885
1577
  properties: {
886
- project_id_or_name: { type: 'string', description: 'Code Engine project name or ID' },
887
- build_name: { type: 'string', description: 'Name of the existing Code Engine build configuration to run (use ce_list_builds to find it)' },
888
- app_name: { type: 'string', description: 'Application to create or update after the build succeeds' },
889
- image_secret: { type: 'string', description: 'Registry pull secret name in Code Engine (e.g. icr-pull-secret)' },
890
- port: { type: 'number', description: 'Container port the app listens on (default 8080)' },
891
- build_timeout_seconds: { type: 'number', description: 'Max seconds to wait for the build to finish (default 600)' },
892
- deploy_timeout_seconds: { type: 'number', description: 'Max seconds to wait for the app to become ready (default 180)' },
1578
+ project_id: { type: 'string' },
1579
+ subnet_pool_id: { type: 'string' },
893
1580
  },
894
- required: ['project_id_or_name', 'build_name', 'app_name', 'image_secret'],
1581
+ required: ['project_id', 'subnet_pool_id'],
895
1582
  },
896
1583
  },
897
1584
  ];
@@ -1013,8 +1700,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1013
1700
  return { content: [{ type: 'text', text: JSON.stringify({ valid, summary, errors, warnings, info, dockerfile: dfPath }, null, 2) }], ...(valid ? {} : { isError: true }) };
1014
1701
  }
1015
1702
  case 'detect_container_runtime': {
1016
- const { stdout: dockerVersion } = await execAsync('docker --version').catch(() => ({ stdout: '' }));
1017
- const { stdout: podmanVersion } = await execAsync('podman --version').catch(() => ({ stdout: '' }));
1703
+ const { stdout: dockerVersion } = await execFileAsync('docker', ['--version']).catch(() => ({ stdout: '' }));
1704
+ const { stdout: podmanVersion } = await execFileAsync('podman', ['--version']).catch(() => ({ stdout: '' }));
1018
1705
  return {
1019
1706
  content: [
1020
1707
  {
@@ -1029,9 +1716,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1029
1716
  };
1030
1717
  }
1031
1718
  case 'build_container_image': {
1032
- const runtime = args.runtime || 'docker';
1033
- const cmd = `${runtime} build -t ${args.image_name} -f ${args.dockerfile_path} ${args.context_path}`;
1034
- const { stdout, stderr } = await execAsync(cmd);
1719
+ const runtime = validateRuntime(args.runtime || 'docker');
1720
+ const imageName = validateImageName(args.image_name);
1721
+ const buildArgs = ['build', '-t', imageName];
1722
+ if (args.dockerfile_path)
1723
+ buildArgs.push('-f', args.dockerfile_path);
1724
+ buildArgs.push(args.context_path || '.');
1725
+ const { stdout, stderr } = await execFileAsync(runtime, buildArgs);
1035
1726
  // Container runtimes write build progress to stderr — label it clearly
1036
1727
  const build_output = [stdout, stderr].filter(Boolean).join('\n').trim();
1037
1728
  return {
@@ -1040,7 +1731,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1040
1731
  type: 'text',
1041
1732
  text: JSON.stringify({
1042
1733
  success: true,
1043
- command: cmd,
1734
+ command: `${runtime} ${buildArgs.join(' ')}`,
1044
1735
  build_output,
1045
1736
  }, null, 2),
1046
1737
  },
@@ -1048,16 +1739,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1048
1739
  };
1049
1740
  }
1050
1741
  case 'push_container_image': {
1051
- const runtime = args.runtime || 'docker';
1052
- const cmd = `${runtime} push ${args.image_name}`;
1053
- const { stdout, stderr } = await execAsync(cmd);
1742
+ const runtime = validateRuntime(args.runtime || 'docker');
1743
+ const imageName = validateImageName(args.image_name);
1744
+ const { stdout, stderr } = await execFileAsync(runtime, ['push', imageName]);
1054
1745
  return {
1055
1746
  content: [
1056
1747
  {
1057
1748
  type: 'text',
1058
1749
  text: JSON.stringify({
1059
1750
  success: true,
1060
- command: cmd,
1751
+ command: `${runtime} push ${imageName}`,
1061
1752
  output: stdout,
1062
1753
  error: stderr
1063
1754
  }, null, 2),
@@ -1066,9 +1757,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1066
1757
  };
1067
1758
  }
1068
1759
  case 'list_local_images': {
1069
- const runtime = args.runtime || 'docker';
1070
- const cmd = `${runtime} images --format "{{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.Size}}"`;
1071
- const { stdout } = await execAsync(cmd);
1760
+ const runtime = validateRuntime(args.runtime || 'docker');
1761
+ const { stdout } = await execFileAsync(runtime, ['images', '--format', '{{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.Size}}']);
1072
1762
  return {
1073
1763
  content: [
1074
1764
  {
@@ -1079,25 +1769,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1079
1769
  };
1080
1770
  }
1081
1771
  case 'test_container_locally': {
1082
- const runtime = args.runtime || 'docker';
1083
- let cmd = `${runtime} run -d`;
1772
+ const runtime = validateRuntime(args.runtime || 'docker');
1773
+ const runArgs = ['run', '-d'];
1084
1774
  if (args.port_mapping) {
1085
- cmd += ` -p ${args.port_mapping}`;
1775
+ runArgs.push('-p', validatePortMapping(args.port_mapping));
1086
1776
  }
1087
1777
  if (args.env_vars) {
1088
- Object.entries(args.env_vars).forEach(([key, value]) => {
1089
- cmd += ` -e ${key}="${value}"`;
1090
- });
1778
+ for (const [key, value] of Object.entries(args.env_vars)) {
1779
+ // Pass as a single arg: execFile does not invoke a shell so KEY=VALUE is safe
1780
+ runArgs.push('-e', `${validateEnvKey(key)}=${value}`);
1781
+ }
1091
1782
  }
1092
- cmd += ` ${args.image_name}`;
1093
- const { stdout } = await execAsync(cmd);
1783
+ runArgs.push(validateImageName(args.image_name));
1784
+ const { stdout } = await execFileAsync(runtime, runArgs);
1094
1785
  return {
1095
1786
  content: [
1096
1787
  {
1097
1788
  type: 'text',
1098
1789
  text: JSON.stringify({
1099
1790
  container_id: stdout.trim(),
1100
- command: cmd,
1791
+ command: `${runtime} ${runArgs.join(' ')}`,
1101
1792
  message: 'Container started successfully'
1102
1793
  }, null, 2),
1103
1794
  },
@@ -1105,9 +1796,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1105
1796
  };
1106
1797
  }
1107
1798
  case 'get_container_logs': {
1108
- const runtime = args.runtime || 'docker';
1109
- const cmd = `${runtime} logs ${args.container_id}`;
1110
- const { stdout } = await execAsync(cmd);
1799
+ const runtime = validateRuntime(args.runtime || 'docker');
1800
+ const { stdout } = await execFileAsync(runtime, ['logs', validateContainerId(args.container_id)]);
1111
1801
  return {
1112
1802
  content: [
1113
1803
  {
@@ -1118,11 +1808,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1118
1808
  };
1119
1809
  }
1120
1810
  case 'stop_local_container': {
1121
- const runtime = args.runtime || 'docker';
1122
- const stopCmd = `${runtime} stop ${args.container_id}`;
1123
- const rmCmd = `${runtime} rm ${args.container_id}`;
1124
- await execAsync(stopCmd);
1125
- await execAsync(rmCmd);
1811
+ const runtime = validateRuntime(args.runtime || 'docker');
1812
+ const containerId = validateContainerId(args.container_id);
1813
+ await execFileAsync(runtime, ['stop', containerId]);
1814
+ await execFileAsync(runtime, ['rm', containerId]);
1126
1815
  return {
1127
1816
  content: [
1128
1817
  {
@@ -1136,10 +1825,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1136
1825
  };
1137
1826
  }
1138
1827
  case 'list_local_containers': {
1139
- const runtime = args.runtime || 'docker';
1140
- const allFlag = args.all ? '-a' : '';
1141
- const cmd = `${runtime} ps ${allFlag} --format "{{.ID}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"`;
1142
- const { stdout } = await execAsync(cmd);
1828
+ const runtime = validateRuntime(args.runtime || 'docker');
1829
+ const psArgs = ['ps', '--format', '{{.ID}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'];
1830
+ if (args.all)
1831
+ psArgs.push('-a');
1832
+ const { stdout } = await execFileAsync(runtime, psArgs);
1143
1833
  return {
1144
1834
  content: [
1145
1835
  {
@@ -1149,6 +1839,60 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1149
1839
  ],
1150
1840
  };
1151
1841
  }
1842
+ case 'tag_container_image': {
1843
+ const runtime = validateRuntime(args.runtime || (await execFileAsync('podman', ['--version']).then(() => 'podman').catch(() => 'docker')));
1844
+ const src = validateImageName(args.source_image);
1845
+ const tgt = validateImageName(args.target_image);
1846
+ await execFileAsync(runtime, ['tag', src, tgt]);
1847
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, command: `${runtime} tag ${src} ${tgt}`, source: src, target: tgt }, null, 2) }] };
1848
+ }
1849
+ case 'remove_local_image': {
1850
+ const runtime = validateRuntime(args.runtime || (await execFileAsync('podman', ['--version']).then(() => 'podman').catch(() => 'docker')));
1851
+ const rmiArgs = ['rmi'];
1852
+ if (args.force)
1853
+ rmiArgs.push('-f');
1854
+ rmiArgs.push(validateImageName(args.image_name));
1855
+ const { stdout, stderr } = await execFileAsync(runtime, rmiArgs);
1856
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, command: `${runtime} ${rmiArgs.join(' ')}`, output: [stdout, stderr].filter(Boolean).join('\n').trim() }, null, 2) }] };
1857
+ }
1858
+ case 'login_to_registry': {
1859
+ const runtime = validateRuntime(args.runtime || (await execFileAsync('podman', ['--version']).then(() => 'podman').catch(() => 'docker')));
1860
+ const registry = validateRegistryHost(args.registry || 'us.icr.io');
1861
+ const username = args.username || 'iamapikey';
1862
+ // Use supplied password, or fall back to IBMCLOUD_API_KEY for ICR
1863
+ const password = args.password || getApiKey();
1864
+ // Pipe password via stdin — never via echo|shell to avoid command injection
1865
+ const { stdout, stderr } = await spawnWithStdin(runtime, ['login', registry, '-u', username, '--password-stdin'], password);
1866
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, registry, username, output: [stdout, stderr].filter(Boolean).join('\n').trim() }, null, 2) }] };
1867
+ }
1868
+ case 'inspect_container_image': {
1869
+ const runtime = validateRuntime(args.runtime || (await execFileAsync('podman', ['--version']).then(() => 'podman').catch(() => 'docker')));
1870
+ const { stdout } = await execFileAsync(runtime, ['inspect', validateImageName(args.image_name)]);
1871
+ const raw = JSON.parse(stdout);
1872
+ const img = Array.isArray(raw) ? raw[0] : raw;
1873
+ const summary = {
1874
+ id: img.Id?.substring(0, 12),
1875
+ created: img.Created,
1876
+ architecture: img.Architecture,
1877
+ os: img.Os,
1878
+ size_mb: img.Size ? (img.Size / 1024 / 1024).toFixed(1) : undefined,
1879
+ labels: img.Config?.Labels || img.Labels,
1880
+ env: img.Config?.Env,
1881
+ entrypoint: img.Config?.Entrypoint,
1882
+ cmd: img.Config?.Cmd,
1883
+ exposed_ports: img.Config?.ExposedPorts ? Object.keys(img.Config.ExposedPorts) : [],
1884
+ layers: img.RootFS?.Layers?.length ?? img.GraphDriver?.Data?.LowerDir?.split(':').length,
1885
+ };
1886
+ return { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] };
1887
+ }
1888
+ case 'prune_images': {
1889
+ const runtime = validateRuntime(args.runtime || (await execFileAsync('podman', ['--version']).then(() => 'podman').catch(() => 'docker')));
1890
+ const pruneArgs = ['image', 'prune', '-f'];
1891
+ if (args.all)
1892
+ pruneArgs.push('-a');
1893
+ const { stdout, stderr } = await execFileAsync(runtime, pruneArgs);
1894
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, command: `${runtime} ${pruneArgs.join(' ')}`, output: [stdout, stderr].filter(Boolean).join('\n').trim() }, null, 2) }] };
1895
+ }
1152
1896
  case 'icr_list_namespaces': {
1153
1897
  const apiKey = getApiKey();
1154
1898
  const token = await getIAMToken(apiKey);
@@ -1259,6 +2003,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1259
2003
  type: 'literal', name, value,
1260
2004
  }));
1261
2005
  }
2006
+ if (args.run_args)
2007
+ body.run_arguments = args.run_args;
2008
+ if (args.run_commands)
2009
+ body.run_commands = args.run_commands;
1262
2010
  const response = await axios.post(`${base}/apps`, body, { headers });
1263
2011
  return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
1264
2012
  }
@@ -1283,6 +2031,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1283
2031
  patch.scale_cpu_limit = args.scale_cpu_limit;
1284
2032
  if (args.scale_memory_limit)
1285
2033
  patch.scale_memory_limit = args.scale_memory_limit;
2034
+ if (args.run_args)
2035
+ patch.run_arguments = args.run_args;
2036
+ if (args.run_commands)
2037
+ patch.run_commands = args.run_commands;
1286
2038
  const response = await axios.patch(`${base}/apps/${args.app_name}`, patch, {
1287
2039
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/merge-patch+json', 'If-Match': entityTag },
1288
2040
  });
@@ -1313,22 +2065,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1313
2065
  }
1314
2066
  case 'ce_get_app_logs': {
1315
2067
  const token = await getIAMToken(getApiKey());
1316
- const { base, headers } = await ceApi(args.project_id, token);
1317
- try {
1318
- const response = await axios.get(`${base}/apps/${args.app_name}/instances/${args.instance_name}/logs`, { headers });
1319
- return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
2068
+ const projectId = await resolveProjectId(args.project_id, token);
2069
+ const region = await getProjectRegion(projectId, token);
2070
+ const ceHeaders = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
2071
+ // Get app details to extract namespace/subdomain from the endpoint URL
2072
+ const appRes = await axios.get(`https://api.${region}.codeengine.cloud.ibm.com/v2/projects/${projectId}/apps/${args.app_name}`, { headers: ceHeaders });
2073
+ // Endpoint pattern: https://{app}.{subdomain}.{region}.codeengine.appdomain.cloud
2074
+ const endpoint = appRes.data.endpoint || '';
2075
+ const subdomainMatch = endpoint.match(/https?:\/\/[^.]+\.([^.]+)\.[^.]+\.codeengine/);
2076
+ if (!subdomainMatch) {
2077
+ return { content: [{ type: 'text', text: JSON.stringify({ error: 'Could not determine project namespace from app endpoint', endpoint }, null, 2) }] };
1320
2078
  }
1321
- catch (err) {
1322
- if (err.response?.status === 403) {
1323
- return { content: [{ type: 'text', text: JSON.stringify({
1324
- note: 'App instance logs are not accessible via the Code Engine REST API v2. ' +
1325
- 'Configure IBM Log Analysis (IBM Cloud Logging) for your project to retrieve logs via the IBM Cloud Logs API.',
1326
- docs: 'https://cloud.ibm.com/docs/codeengine?topic=codeengine-view-logs',
1327
- status: 403,
1328
- }, null, 2) }] };
2079
+ const namespace = subdomainMatch[1];
2080
+ const proxyBase = `https://proxy.${region}.codeengine.cloud.ibm.com`;
2081
+ const tailLines = args.tail_lines ?? 100;
2082
+ const kubeHeaders = { Authorization: `Bearer ${token}` };
2083
+ // List pods for the app via Kubernetes API proxy
2084
+ const labelSelector = encodeURIComponent(`serving.knative.dev/service=${args.app_name}`);
2085
+ const podsRes = await axios.get(`${proxyBase}/api/v1/namespaces/${namespace}/pods?labelSelector=${labelSelector}`, { headers: kubeHeaders });
2086
+ const pods = podsRes.data.items || [];
2087
+ if (pods.length === 0) {
2088
+ return { content: [{ type: 'text', text: JSON.stringify({ message: `No pods found for app '${args.app_name}'`, namespace, app: args.app_name }, null, 2) }] };
2089
+ }
2090
+ // Filter to a specific instance if requested
2091
+ const targetPods = args.instance_name
2092
+ ? pods.filter((p) => p.metadata.name === args.instance_name || p.metadata.name.startsWith(String(args.instance_name)))
2093
+ : pods;
2094
+ // Fetch logs for each pod
2095
+ const results = [];
2096
+ for (const pod of targetPods) {
2097
+ const podName = pod.metadata.name;
2098
+ const podPhase = pod.status?.phase ?? 'Unknown';
2099
+ try {
2100
+ const logRes = await axios.get(`${proxyBase}/api/v1/namespaces/${namespace}/pods/${podName}/log?container=user-container&tailLines=${tailLines}`, { headers: kubeHeaders });
2101
+ results.push({ pod: podName, status: podPhase, logs: logRes.data });
2102
+ }
2103
+ catch (logErr) {
2104
+ results.push({ pod: podName, status: podPhase, error: logErr.response?.data?.message ?? logErr.message });
1329
2105
  }
1330
- throw err;
1331
2106
  }
2107
+ return { content: [{ type: 'text', text: JSON.stringify({ app: args.app_name, namespace, region, pods_found: pods.length, results }, null, 2) }] };
1332
2108
  }
1333
2109
  case 'ce_list_builds': {
1334
2110
  const token = await getIAMToken(getApiKey());
@@ -1570,6 +2346,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1570
2346
  const response = await axios.patch(`${base}/secrets/${args.secret_name}`, body, { headers: patchHeaders });
1571
2347
  return { content: [{ type: 'text', text: JSON.stringify({ name: response.data.name, format: response.data.format, updated_at: response.data.updated_at, message: 'Secret updated successfully' }, null, 2) }] };
1572
2348
  }
2349
+ case 'ce_refresh_icr_pull_secret': {
2350
+ const apiKey = getApiKey();
2351
+ const token = await getIAMToken(apiKey);
2352
+ const { base, headers } = await ceApi(args.project_id, token);
2353
+ const secretName = args.secret_name || 'icr-pull-secret';
2354
+ const icrHost = args.icr_host || 'us.icr.io';
2355
+ // Delete existing secret if present (ignore 404)
2356
+ try {
2357
+ await axios.delete(`${base}/secrets/${secretName}`, { headers });
2358
+ }
2359
+ catch (e) {
2360
+ if (e.response?.status !== 404)
2361
+ throw e;
2362
+ }
2363
+ // Recreate with current API key credentials
2364
+ const body = { name: secretName, format: 'registry', data: { username: 'iamapikey', password: apiKey, server: icrHost } };
2365
+ const response = await axios.post(`${base}/secrets`, body, { headers });
2366
+ return { content: [{ type: 'text', text: JSON.stringify({
2367
+ name: response.data.name,
2368
+ format: response.data.format,
2369
+ server: icrHost,
2370
+ created_at: response.data.created_at,
2371
+ message: `Registry pull secret "${secretName}" refreshed with current API key credentials. You can now redeploy your application.`,
2372
+ }, null, 2) }] };
2373
+ }
1573
2374
  case 'ce_renew_tls_secret_from_pem': {
1574
2375
  const fs = await import('fs');
1575
2376
  const os = await import('os');
@@ -1734,10 +2535,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1734
2535
  }
1735
2536
  if (exposedPorts.includes(80))
1736
2537
  valErrors.push('Port 80 is not allowed in Code Engine. Use port 8080.');
1737
- // nginx sed pattern check
2538
+ // nginx sed pattern check — catch both exact-space and \s* patterns that fail in Alpine BusyBox sed
1738
2539
  dfLines.filter(l => /sed\s+-i/.test(l) && /listen/.test(l)).forEach(sl => {
1739
- if (/listen\s{2,}80;/.test(sl)) {
1740
- valWarnings.push(`Fragile nginx sed pattern: "${sl.trim()}" — use 's/listen[[:space:]]*80;/listen 8080;/g' to handle variable whitespace in nginx:alpine`);
2540
+ if (/listen\s{2,}80;/.test(sl) || /listen\\s[*+?]/.test(sl)) {
2541
+ valWarnings.push(`Fragile nginx sed pattern: "${sl.trim()}" — Alpine BusyBox sed does not support \\s*, \\s+. Use 's/listen[[:space:]]*80;/listen 8080;/g' (POSIX character class) instead`);
1741
2542
  }
1742
2543
  });
1743
2544
  if (valErrors.length > 0) {
@@ -1758,7 +2559,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1758
2559
  // 1) detect runtime
1759
2560
  let runtime = 'podman';
1760
2561
  try {
1761
- await execAsync('podman --version');
2562
+ await execFileAsync('podman', ['--version']);
1762
2563
  }
1763
2564
  catch {
1764
2565
  runtime = 'docker';
@@ -1773,20 +2574,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1773
2574
  const imageTag = args.image_tag || 'latest';
1774
2575
  const imageName = `${icrHost}/${args.icr_namespace}/${args.app_name}:${imageTag}`;
1775
2576
  const contextPath = contextPathRaw;
1776
- const buildCmd = `${runtime} build --platform linux/amd64 -t ${imageName} ${contextPath}`;
1777
- const { stdout: buildStdout, stderr: buildStderr } = await execAsync(buildCmd);
2577
+ const buildCmdArgs = ['build', '--platform', 'linux/amd64', '-t', validateImageName(imageName), contextPath];
2578
+ const { stdout: buildStdout, stderr: buildStderr } = await execFileAsync(runtime, buildCmdArgs);
1778
2579
  const buildOutput = [buildStdout, buildStderr].filter(Boolean).join('\n').trim();
1779
2580
  // show last 20 lines of build output so it's not overwhelming
1780
2581
  const buildLines = buildOutput.split('\n');
1781
2582
  const buildSummary = buildLines.length > 20 ? `...${buildLines.slice(-20).join('\n')}` : buildOutput;
1782
2583
  steps.push(`[3/5] Built ${imageName} for linux/amd64:\n${buildSummary}`);
1783
2584
  // 4) push
1784
- const pushCmd = `${runtime} push ${imageName}`;
1785
- const { stdout: pushStdout, stderr: pushStderr } = await execAsync(pushCmd);
2585
+ const { stdout: pushStdout, stderr: pushStderr } = await execFileAsync(runtime, ['push', imageName]);
1786
2586
  const pushOutput = [pushStdout, pushStderr].filter(Boolean).join('\n').trim();
1787
2587
  steps.push(`[4/5] Pushed to ${icrHost}:\n${pushOutput}`);
1788
- // 5) create or update CE app
2588
+ // 4.5) auto-refresh the ICR pull secret so Code Engine can always pull the freshly-pushed image
2589
+ // A stale secret (409 already-exists but wrong password) causes "no_revision_ready" / "reason: unknown".
1789
2590
  const { base: base1, headers: headers1 } = await ceApi(projectId1, token1);
2591
+ const pullSecretName = args.image_secret || 'icr-pull-secret';
2592
+ try {
2593
+ try {
2594
+ await axios.delete(`${base1}/secrets/${pullSecretName}`, { headers: headers1 });
2595
+ }
2596
+ catch (e) {
2597
+ if (e.response?.status !== 404)
2598
+ throw e;
2599
+ }
2600
+ await axios.post(`${base1}/secrets`, { name: pullSecretName, format: 'registry', data: { username: 'iamapikey', password: getApiKey(), server: icrHost } }, { headers: headers1 });
2601
+ steps.push(`[4.5/5] Refreshed pull secret "${pullSecretName}" with current credentials`);
2602
+ }
2603
+ catch (e) {
2604
+ steps.push(`[4.5/5] Warning: Could not refresh pull secret "${pullSecretName}": ${e.message}`);
2605
+ }
2606
+ // 5) create or update CE app
1790
2607
  const appPayload1 = {
1791
2608
  image_reference: imageName,
1792
2609
  image_secret: args.image_secret,
@@ -1941,6 +2758,376 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1941
2758
  app_poll_history: appPollHistory,
1942
2759
  }, null, 2) }] };
1943
2760
  }
2761
+ // ─── App Revisions ─────────────────────────────────────────────────────────
2762
+ case 'ce_list_app_revisions': {
2763
+ const token = await getIAMToken(getApiKey());
2764
+ const { base, headers } = await ceApi(args.project_id, token);
2765
+ const response = await axios.get(`${base}/apps/${args.app_name}/revisions`, { headers });
2766
+ return { content: [{ type: 'text', text: JSON.stringify({ revisions: response.data.revisions || [], total: response.data.revisions?.length || 0 }, null, 2) }] };
2767
+ }
2768
+ case 'ce_get_app_revision': {
2769
+ const token = await getIAMToken(getApiKey());
2770
+ const { base, headers } = await ceApi(args.project_id, token);
2771
+ const response = await axios.get(`${base}/apps/${args.app_name}/revisions/${args.revision_name}`, { headers });
2772
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
2773
+ }
2774
+ case 'ce_delete_app_revision': {
2775
+ const token = await getIAMToken(getApiKey());
2776
+ const { base, headers } = await ceApi(args.project_id, token);
2777
+ await axios.delete(`${base}/apps/${args.app_name}/revisions/${args.revision_name}`, { headers });
2778
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: `Revision ${args.revision_name} deleted` }, null, 2) }] };
2779
+ }
2780
+ // ─── Update operations ──────────────────────────────────────────────────────
2781
+ case 'ce_update_job': {
2782
+ const token = await getIAMToken(getApiKey());
2783
+ const { base, headers } = await ceApi(args.project_id, token);
2784
+ const current = await axios.get(`${base}/jobs/${args.job_name}`, { headers });
2785
+ const etag = current.data.entity_tag;
2786
+ const patch = {};
2787
+ if (args.image)
2788
+ patch.image_reference = args.image;
2789
+ if (args.image_secret)
2790
+ patch.image_secret = args.image_secret;
2791
+ if (args.scale_array_spec)
2792
+ patch.scale_array_spec = args.scale_array_spec;
2793
+ if (args.scale_cpu_limit)
2794
+ patch.scale_cpu_limit = args.scale_cpu_limit;
2795
+ if (args.scale_memory_limit)
2796
+ patch.scale_memory_limit = args.scale_memory_limit;
2797
+ if (args.env_vars) {
2798
+ patch.run_env_variables = Object.entries(args.env_vars).map(([name, value]) => ({ type: 'literal', name, value }));
2799
+ }
2800
+ const response = await axios.patch(`${base}/jobs/${args.job_name}`, patch, {
2801
+ headers: { ...headers, 'Content-Type': 'application/merge-patch+json', 'If-Match': etag },
2802
+ });
2803
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
2804
+ }
2805
+ case 'ce_update_build': {
2806
+ const token = await getIAMToken(getApiKey());
2807
+ const { base, headers } = await ceApi(args.project_id, token);
2808
+ const current = await axios.get(`${base}/builds/${args.build_name}`, { headers });
2809
+ const etag = current.data.entity_tag;
2810
+ const patch = {};
2811
+ if (args.output_image)
2812
+ patch.output_image = args.output_image;
2813
+ if (args.output_secret)
2814
+ patch.output_secret = args.output_secret;
2815
+ if (args.source_url)
2816
+ patch.source_url = args.source_url;
2817
+ if (args.source_revision)
2818
+ patch.source_revision = args.source_revision;
2819
+ if (args.strategy_size)
2820
+ patch.strategy_size = args.strategy_size;
2821
+ if (args.strategy_spec_file)
2822
+ patch.strategy_spec_file = args.strategy_spec_file;
2823
+ const response = await axios.patch(`${base}/builds/${args.build_name}`, patch, {
2824
+ headers: { ...headers, 'Content-Type': 'application/merge-patch+json', 'If-Match': etag },
2825
+ });
2826
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
2827
+ }
2828
+ case 'ce_update_config_map': {
2829
+ const token = await getIAMToken(getApiKey());
2830
+ const { base, headers } = await ceApi(args.project_id, token);
2831
+ const current = await axios.get(`${base}/config_maps/${args.config_map_name}`, { headers });
2832
+ const etag = current.data.entity_tag;
2833
+ const response = await axios.patch(`${base}/config_maps/${args.config_map_name}`, { data: args.data }, {
2834
+ headers: { ...headers, 'Content-Type': 'application/merge-patch+json', 'If-Match': etag },
2835
+ });
2836
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
2837
+ }
2838
+ case 'ce_update_domain_mapping': {
2839
+ const token = await getIAMToken(getApiKey());
2840
+ const { base, headers } = await ceApi(args.project_id, token);
2841
+ const current = await axios.get(`${base}/domain_mappings/${encodeURIComponent(args.domain_name)}`, { headers });
2842
+ const etag = current.data.entity_tag;
2843
+ const patch = {};
2844
+ if (args.app_name)
2845
+ patch.component = { resource_type: 'app_v2', name: args.app_name };
2846
+ if (args.tls_secret)
2847
+ patch.tls_secret = args.tls_secret;
2848
+ const response = await axios.patch(`${base}/domain_mappings/${encodeURIComponent(args.domain_name)}`, patch, {
2849
+ headers: { ...headers, 'Content-Type': 'application/merge-patch+json', 'If-Match': etag },
2850
+ });
2851
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
2852
+ }
2853
+ // ─── Functions ──────────────────────────────────────────────────────────────
2854
+ case 'ce_list_function_runtimes': {
2855
+ const token = await getIAMToken(getApiKey());
2856
+ const region = args.region || 'us-south';
2857
+ const response = await axios.get(`https://api.${region}.codeengine.cloud.ibm.com/v2/function_runtimes`, { headers: { Authorization: `Bearer ${token}` } });
2858
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
2859
+ }
2860
+ case 'ce_list_functions': {
2861
+ const token = await getIAMToken(getApiKey());
2862
+ const { base, headers } = await ceApi(args.project_id, token);
2863
+ const response = await axios.get(`${base}/functions`, { headers });
2864
+ return { content: [{ type: 'text', text: JSON.stringify({ functions: response.data.functions || [], total: response.data.functions?.length || 0 }, null, 2) }] };
2865
+ }
2866
+ case 'ce_get_function': {
2867
+ const token = await getIAMToken(getApiKey());
2868
+ const { base, headers } = await ceApi(args.project_id, token);
2869
+ const response = await axios.get(`${base}/functions/${args.function_name}`, { headers });
2870
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
2871
+ }
2872
+ case 'ce_create_function': {
2873
+ const token = await getIAMToken(getApiKey());
2874
+ const { base, headers } = await ceApi(args.project_id, token);
2875
+ const body = {
2876
+ name: args.name,
2877
+ runtime: args.runtime,
2878
+ code_reference: args.code_reference,
2879
+ code_main: args.code_main ?? 'main',
2880
+ scale_concurrency: args.scale_concurrency ?? 1,
2881
+ scale_cpu_limit: args.scale_cpu_limit ?? '1',
2882
+ scale_memory_limit: args.scale_memory_limit ?? '4G',
2883
+ };
2884
+ if (args.env_vars) {
2885
+ body.run_env_variables = Object.entries(args.env_vars).map(([name, value]) => ({ type: 'literal', name, value }));
2886
+ }
2887
+ const response = await axios.post(`${base}/functions`, body, { headers });
2888
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
2889
+ }
2890
+ case 'ce_update_function': {
2891
+ const token = await getIAMToken(getApiKey());
2892
+ const { base, headers } = await ceApi(args.project_id, token);
2893
+ const current = await axios.get(`${base}/functions/${args.function_name}`, { headers });
2894
+ const etag = current.data.entity_tag;
2895
+ const patch = {};
2896
+ if (args.runtime)
2897
+ patch.runtime = args.runtime;
2898
+ if (args.code_reference)
2899
+ patch.code_reference = args.code_reference;
2900
+ if (args.code_main)
2901
+ patch.code_main = args.code_main;
2902
+ if (args.scale_concurrency !== undefined)
2903
+ patch.scale_concurrency = args.scale_concurrency;
2904
+ if (args.scale_cpu_limit)
2905
+ patch.scale_cpu_limit = args.scale_cpu_limit;
2906
+ if (args.scale_memory_limit)
2907
+ patch.scale_memory_limit = args.scale_memory_limit;
2908
+ if (args.env_vars) {
2909
+ patch.run_env_variables = Object.entries(args.env_vars).map(([name, value]) => ({ type: 'literal', name, value }));
2910
+ }
2911
+ const response = await axios.patch(`${base}/functions/${args.function_name}`, patch, {
2912
+ headers: { ...headers, 'Content-Type': 'application/merge-patch+json', 'If-Match': etag },
2913
+ });
2914
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
2915
+ }
2916
+ case 'ce_delete_function': {
2917
+ const token = await getIAMToken(getApiKey());
2918
+ const { base, headers } = await ceApi(args.project_id, token);
2919
+ await axios.delete(`${base}/functions/${args.function_name}`, { headers });
2920
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: `Function ${args.function_name} deleted` }, null, 2) }] };
2921
+ }
2922
+ // ─── Service Bindings ────────────────────────────────────────────────────────
2923
+ case 'ce_list_bindings': {
2924
+ const token = await getIAMToken(getApiKey());
2925
+ const { base, headers } = await ceApi(args.project_id, token);
2926
+ const response = await axios.get(`${base}/bindings`, { headers });
2927
+ return { content: [{ type: 'text', text: JSON.stringify({ bindings: response.data.bindings || [], total: response.data.bindings?.length || 0 }, null, 2) }] };
2928
+ }
2929
+ case 'ce_create_binding': {
2930
+ const token = await getIAMToken(getApiKey());
2931
+ const { base, headers } = await ceApi(args.project_id, token);
2932
+ const body = {
2933
+ component: { resource_type: args.component_resource_type, name: args.component_name },
2934
+ secret_name: args.secret_name,
2935
+ };
2936
+ if (args.prefix)
2937
+ body.prefix = args.prefix;
2938
+ const response = await axios.post(`${base}/bindings`, body, { headers });
2939
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
2940
+ }
2941
+ case 'ce_get_binding': {
2942
+ const token = await getIAMToken(getApiKey());
2943
+ const { base, headers } = await ceApi(args.project_id, token);
2944
+ const response = await axios.get(`${base}/bindings/${args.binding_id}`, { headers });
2945
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
2946
+ }
2947
+ case 'ce_delete_binding': {
2948
+ const token = await getIAMToken(getApiKey());
2949
+ const { base, headers } = await ceApi(args.project_id, token);
2950
+ await axios.delete(`${base}/bindings/${args.binding_id}`, { headers });
2951
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: `Binding ${args.binding_id} deleted` }, null, 2) }] };
2952
+ }
2953
+ // ─── Project extras ─────────────────────────────────────────────────────────
2954
+ case 'ce_get_project_status': {
2955
+ const token = await getIAMToken(getApiKey());
2956
+ const region = await getProjectRegion(args.project_id, token);
2957
+ const response = await axios.get(`https://api.${region}.codeengine.cloud.ibm.com/v2/projects/${args.project_id}/status_details`, { headers: { Authorization: `Bearer ${token}` } });
2958
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
2959
+ }
2960
+ case 'ce_list_egress_ips': {
2961
+ const token = await getIAMToken(getApiKey());
2962
+ const { base, headers } = await ceApi(args.project_id, token);
2963
+ const response = await axios.get(`${base}/egress_ips`, { headers });
2964
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
2965
+ }
2966
+ case 'ce_list_allowed_outbound_destinations': {
2967
+ const token = await getIAMToken(getApiKey());
2968
+ const { base, headers } = await ceApi(args.project_id, token);
2969
+ const response = await axios.get(`${base}/allowed_outbound_destinations`, { headers });
2970
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
2971
+ }
2972
+ case 'ce_create_allowed_outbound_destination': {
2973
+ const token = await getIAMToken(getApiKey());
2974
+ const { base, headers } = await ceApi(args.project_id, token);
2975
+ const body = { name: args.name, type: args.type };
2976
+ if (args.cidr_block)
2977
+ body.cidr_block = args.cidr_block;
2978
+ if (args.fqdn)
2979
+ body.fqdn = args.fqdn;
2980
+ const response = await axios.post(`${base}/allowed_outbound_destinations`, body, { headers });
2981
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
2982
+ }
2983
+ case 'ce_get_allowed_outbound_destination': {
2984
+ const token = await getIAMToken(getApiKey());
2985
+ const { base, headers } = await ceApi(args.project_id, token);
2986
+ const response = await axios.get(`${base}/allowed_outbound_destinations/${args.destination_name}`, { headers });
2987
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
2988
+ }
2989
+ case 'ce_update_allowed_outbound_destination': {
2990
+ const token = await getIAMToken(getApiKey());
2991
+ const { base, headers } = await ceApi(args.project_id, token);
2992
+ const current = await axios.get(`${base}/allowed_outbound_destinations/${args.destination_name}`, { headers });
2993
+ const etag = current.data.entity_tag;
2994
+ const patch = {};
2995
+ if (args.cidr_block)
2996
+ patch.cidr_block = args.cidr_block;
2997
+ if (args.fqdn)
2998
+ patch.fqdn = args.fqdn;
2999
+ const response = await axios.patch(`${base}/allowed_outbound_destinations/${args.destination_name}`, patch, {
3000
+ headers: { ...headers, 'Content-Type': 'application/merge-patch+json', 'If-Match': etag },
3001
+ });
3002
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
3003
+ }
3004
+ case 'ce_delete_allowed_outbound_destination': {
3005
+ const token = await getIAMToken(getApiKey());
3006
+ const { base, headers } = await ceApi(args.project_id, token);
3007
+ await axios.delete(`${base}/allowed_outbound_destinations/${args.destination_name}`, { headers });
3008
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: `Allowed outbound destination ${args.destination_name} deleted` }, null, 2) }] };
3009
+ }
3010
+ // ─── Persistent Data Stores ─────────────────────────────────────────────────
3011
+ case 'ce_list_persistent_data_stores': {
3012
+ const token = await getIAMToken(getApiKey());
3013
+ const { base, headers } = await ceApi(args.project_id, token);
3014
+ const response = await axios.get(`${base}/persistent_data_stores`, { headers });
3015
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
3016
+ }
3017
+ case 'ce_create_persistent_data_store': {
3018
+ const token = await getIAMToken(getApiKey());
3019
+ const { base, headers } = await ceApi(args.project_id, token);
3020
+ const body = { name: args.name, secret_name: args.secret_name, bucket_name: args.bucket_name };
3021
+ if (args.endpoint)
3022
+ body.endpoint = args.endpoint;
3023
+ const response = await axios.post(`${base}/persistent_data_stores`, body, { headers });
3024
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
3025
+ }
3026
+ case 'ce_get_persistent_data_store': {
3027
+ const token = await getIAMToken(getApiKey());
3028
+ const { base, headers } = await ceApi(args.project_id, token);
3029
+ const response = await axios.get(`${base}/persistent_data_stores/${args.data_store_name}`, { headers });
3030
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
3031
+ }
3032
+ case 'ce_delete_persistent_data_store': {
3033
+ const token = await getIAMToken(getApiKey());
3034
+ const { base, headers } = await ceApi(args.project_id, token);
3035
+ await axios.delete(`${base}/persistent_data_stores/${args.data_store_name}`, { headers });
3036
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: `Persistent data store ${args.data_store_name} deleted` }, null, 2) }] };
3037
+ }
3038
+ // ─── Fleets ─────────────────────────────────────────────────────────────────
3039
+ case 'ce_list_fleets': {
3040
+ const token = await getIAMToken(getApiKey());
3041
+ const { base, headers } = await ceApi(args.project_id, token);
3042
+ const response = await axios.get(`${base}/fleets`, { headers });
3043
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
3044
+ }
3045
+ case 'ce_create_fleet': {
3046
+ const token = await getIAMToken(getApiKey());
3047
+ const { base, headers } = await ceApi(args.project_id, token);
3048
+ const body = { name: args.name, image_reference: args.image };
3049
+ if (args.image_secret)
3050
+ body.image_secret = args.image_secret;
3051
+ if (args.scale_cpu_limit)
3052
+ body.scale_cpu_limit = args.scale_cpu_limit;
3053
+ if (args.scale_memory_limit)
3054
+ body.scale_memory_limit = args.scale_memory_limit;
3055
+ if (args.env_vars) {
3056
+ body.run_env_variables = Object.entries(args.env_vars).map(([name, value]) => ({ type: 'literal', name, value }));
3057
+ }
3058
+ const response = await axios.post(`${base}/fleets`, body, { headers });
3059
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
3060
+ }
3061
+ case 'ce_get_fleet': {
3062
+ const token = await getIAMToken(getApiKey());
3063
+ const { base, headers } = await ceApi(args.project_id, token);
3064
+ const response = await axios.get(`${base}/fleets/${args.fleet_id}`, { headers });
3065
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
3066
+ }
3067
+ case 'ce_delete_fleet': {
3068
+ const token = await getIAMToken(getApiKey());
3069
+ const { base, headers } = await ceApi(args.project_id, token);
3070
+ await axios.delete(`${base}/fleets/${args.fleet_id}`, { headers });
3071
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: `Fleet ${args.fleet_id} deleted` }, null, 2) }] };
3072
+ }
3073
+ case 'ce_cancel_fleet': {
3074
+ const token = await getIAMToken(getApiKey());
3075
+ const { base, headers } = await ceApi(args.project_id, token);
3076
+ const response = await axios.post(`${base}/fleets/${args.fleet_id}/cancel`, {}, { headers });
3077
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
3078
+ }
3079
+ // ─── Fleet Tasks ────────────────────────────────────────────────────────────
3080
+ case 'ce_list_fleet_tasks': {
3081
+ const token = await getIAMToken(getApiKey());
3082
+ const { base, headers } = await ceApi(args.project_id, token);
3083
+ const response = await axios.get(`${base}/fleets/${args.fleet_id}/tasks`, { headers });
3084
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
3085
+ }
3086
+ case 'ce_get_fleet_task': {
3087
+ const token = await getIAMToken(getApiKey());
3088
+ const { base, headers } = await ceApi(args.project_id, token);
3089
+ const response = await axios.get(`${base}/fleets/${args.fleet_id}/tasks/${args.task_id}`, { headers });
3090
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
3091
+ }
3092
+ // ─── Fleet Workers ──────────────────────────────────────────────────────────
3093
+ case 'ce_list_fleet_workers': {
3094
+ const token = await getIAMToken(getApiKey());
3095
+ const { base, headers } = await ceApi(args.project_id, token);
3096
+ const response = await axios.get(`${base}/fleets/${args.fleet_id}/workers`, { headers });
3097
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
3098
+ }
3099
+ case 'ce_get_fleet_worker': {
3100
+ const token = await getIAMToken(getApiKey());
3101
+ const { base, headers } = await ceApi(args.project_id, token);
3102
+ const response = await axios.get(`${base}/fleets/${args.fleet_id}/workers/${args.worker_id}`, { headers });
3103
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
3104
+ }
3105
+ // ─── Subnet Pools ────────────────────────────────────────────────────────────
3106
+ case 'ce_list_subnet_pools': {
3107
+ const token = await getIAMToken(getApiKey());
3108
+ const { base, headers } = await ceApi(args.project_id, token);
3109
+ const response = await axios.get(`${base}/subnet_pools`, { headers });
3110
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
3111
+ }
3112
+ case 'ce_create_subnet_pool': {
3113
+ const token = await getIAMToken(getApiKey());
3114
+ const { base, headers } = await ceApi(args.project_id, token);
3115
+ const body = { name: args.name, cidr: args.cidr };
3116
+ const response = await axios.post(`${base}/subnet_pools`, body, { headers });
3117
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
3118
+ }
3119
+ case 'ce_get_subnet_pool': {
3120
+ const token = await getIAMToken(getApiKey());
3121
+ const { base, headers } = await ceApi(args.project_id, token);
3122
+ const response = await axios.get(`${base}/subnet_pools/${args.subnet_pool_id}`, { headers });
3123
+ return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }] };
3124
+ }
3125
+ case 'ce_delete_subnet_pool': {
3126
+ const token = await getIAMToken(getApiKey());
3127
+ const { base, headers } = await ceApi(args.project_id, token);
3128
+ await axios.delete(`${base}/subnet_pools/${args.subnet_pool_id}`, { headers });
3129
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: `Subnet pool ${args.subnet_pool_id} deleted` }, null, 2) }] };
3130
+ }
1944
3131
  default:
1945
3132
  throw new Error(`Unknown tool: ${name}`);
1946
3133
  }