code-engine-mcp-server 1.0.6 → 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.6',
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 = [
@@ -782,6 +908,19 @@ const codeEngineTools = [
782
908
  required: ['project_id', 'secret_name', 'data'],
783
909
  },
784
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
+ },
785
924
  {
786
925
  name: 'ce_renew_tls_secret_from_pem',
787
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.',
@@ -863,40 +1002,583 @@ const codeEngineTools = [
863
1002
  env_vars: { type: 'object', description: 'Key/value environment variables to set on the app' },
864
1003
  timeout_seconds: { type: 'number', description: 'Max seconds to wait for app ready (default 180)' },
865
1004
  },
866
- required: ['context_path', 'project_id_or_name', 'app_name', 'image_secret', 'icr_namespace'],
1005
+ required: ['context_path', 'project_id_or_name', 'app_name', 'image_secret', 'icr_namespace'],
1006
+ },
1007
+ },
1008
+ {
1009
+ name: 'proc_setup_custom_domain',
1010
+ description: 'PROCEDURE: Custom domain setup in one step — reads TLS certificate PEM files from disk (e.g. from certbot/Let\'s Encrypt), creates a TLS secret in Code Engine, then creates the domain mapping. Returns the CNAME target value to add in your DNS provider.',
1011
+ inputSchema: {
1012
+ type: 'object',
1013
+ properties: {
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'],
867
1558
  },
868
1559
  },
869
1560
  {
870
- name: 'proc_setup_custom_domain',
871
- description: 'PROCEDURE: Custom domain setup in one step reads TLS certificate PEM files from disk (e.g. from certbot/Let\'s Encrypt), creates a TLS secret in Code Engine, then creates the domain mapping. Returns the CNAME target value to add in your DNS provider.',
1561
+ name: 'ce_get_subnet_pool',
1562
+ description: 'Get details of a specific subnet pool in a Code Engine project',
872
1563
  inputSchema: {
873
1564
  type: 'object',
874
1565
  properties: {
875
- project_id_or_name: { type: 'string', description: 'Code Engine project name or ID' },
876
- app_name: { type: 'string', description: 'The Code Engine app to map the custom domain to' },
877
- domain_name: { type: 'string', description: 'Your custom domain (e.g. myapp.example.com)' },
878
- 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.' },
879
- cert_pem_path: { type: 'string', description: 'Path to the certificate chain PEM file — typically ~/certbot/config/live/<domain>/fullchain.pem' },
880
- key_pem_path: { type: 'string', description: 'Path to the private key PEM file — typically ~/certbot/config/live/<domain>/privkey.pem' },
1566
+ project_id: { type: 'string' },
1567
+ subnet_pool_id: { type: 'string' },
881
1568
  },
882
- 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'],
883
1570
  },
884
1571
  },
885
1572
  {
886
- name: 'proc_build_run_and_deploy',
887
- 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',
888
1575
  inputSchema: {
889
1576
  type: 'object',
890
1577
  properties: {
891
- project_id_or_name: { type: 'string', description: 'Code Engine project name or ID' },
892
- build_name: { type: 'string', description: 'Name of the existing Code Engine build configuration to run (use ce_list_builds to find it)' },
893
- app_name: { type: 'string', description: 'Application to create or update after the build succeeds' },
894
- image_secret: { type: 'string', description: 'Registry pull secret name in Code Engine (e.g. icr-pull-secret)' },
895
- port: { type: 'number', description: 'Container port the app listens on (default 8080)' },
896
- build_timeout_seconds: { type: 'number', description: 'Max seconds to wait for the build to finish (default 600)' },
897
- 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' },
898
1580
  },
899
- required: ['project_id_or_name', 'build_name', 'app_name', 'image_secret'],
1581
+ required: ['project_id', 'subnet_pool_id'],
900
1582
  },
901
1583
  },
902
1584
  ];
@@ -1018,8 +1700,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1018
1700
  return { content: [{ type: 'text', text: JSON.stringify({ valid, summary, errors, warnings, info, dockerfile: dfPath }, null, 2) }], ...(valid ? {} : { isError: true }) };
1019
1701
  }
1020
1702
  case 'detect_container_runtime': {
1021
- const { stdout: dockerVersion } = await execAsync('docker --version').catch(() => ({ stdout: '' }));
1022
- 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: '' }));
1023
1705
  return {
1024
1706
  content: [
1025
1707
  {
@@ -1034,9 +1716,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1034
1716
  };
1035
1717
  }
1036
1718
  case 'build_container_image': {
1037
- const runtime = args.runtime || 'docker';
1038
- const cmd = `${runtime} build -t ${args.image_name} -f ${args.dockerfile_path} ${args.context_path}`;
1039
- 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);
1040
1726
  // Container runtimes write build progress to stderr — label it clearly
1041
1727
  const build_output = [stdout, stderr].filter(Boolean).join('\n').trim();
1042
1728
  return {
@@ -1045,7 +1731,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1045
1731
  type: 'text',
1046
1732
  text: JSON.stringify({
1047
1733
  success: true,
1048
- command: cmd,
1734
+ command: `${runtime} ${buildArgs.join(' ')}`,
1049
1735
  build_output,
1050
1736
  }, null, 2),
1051
1737
  },
@@ -1053,16 +1739,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1053
1739
  };
1054
1740
  }
1055
1741
  case 'push_container_image': {
1056
- const runtime = args.runtime || 'docker';
1057
- const cmd = `${runtime} push ${args.image_name}`;
1058
- 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]);
1059
1745
  return {
1060
1746
  content: [
1061
1747
  {
1062
1748
  type: 'text',
1063
1749
  text: JSON.stringify({
1064
1750
  success: true,
1065
- command: cmd,
1751
+ command: `${runtime} push ${imageName}`,
1066
1752
  output: stdout,
1067
1753
  error: stderr
1068
1754
  }, null, 2),
@@ -1071,9 +1757,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1071
1757
  };
1072
1758
  }
1073
1759
  case 'list_local_images': {
1074
- const runtime = args.runtime || 'docker';
1075
- const cmd = `${runtime} images --format "{{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.Size}}"`;
1076
- 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}}']);
1077
1762
  return {
1078
1763
  content: [
1079
1764
  {
@@ -1084,25 +1769,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1084
1769
  };
1085
1770
  }
1086
1771
  case 'test_container_locally': {
1087
- const runtime = args.runtime || 'docker';
1088
- let cmd = `${runtime} run -d`;
1772
+ const runtime = validateRuntime(args.runtime || 'docker');
1773
+ const runArgs = ['run', '-d'];
1089
1774
  if (args.port_mapping) {
1090
- cmd += ` -p ${args.port_mapping}`;
1775
+ runArgs.push('-p', validatePortMapping(args.port_mapping));
1091
1776
  }
1092
1777
  if (args.env_vars) {
1093
- Object.entries(args.env_vars).forEach(([key, value]) => {
1094
- cmd += ` -e ${key}="${value}"`;
1095
- });
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
+ }
1096
1782
  }
1097
- cmd += ` ${args.image_name}`;
1098
- const { stdout } = await execAsync(cmd);
1783
+ runArgs.push(validateImageName(args.image_name));
1784
+ const { stdout } = await execFileAsync(runtime, runArgs);
1099
1785
  return {
1100
1786
  content: [
1101
1787
  {
1102
1788
  type: 'text',
1103
1789
  text: JSON.stringify({
1104
1790
  container_id: stdout.trim(),
1105
- command: cmd,
1791
+ command: `${runtime} ${runArgs.join(' ')}`,
1106
1792
  message: 'Container started successfully'
1107
1793
  }, null, 2),
1108
1794
  },
@@ -1110,9 +1796,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1110
1796
  };
1111
1797
  }
1112
1798
  case 'get_container_logs': {
1113
- const runtime = args.runtime || 'docker';
1114
- const cmd = `${runtime} logs ${args.container_id}`;
1115
- const { stdout } = await execAsync(cmd);
1799
+ const runtime = validateRuntime(args.runtime || 'docker');
1800
+ const { stdout } = await execFileAsync(runtime, ['logs', validateContainerId(args.container_id)]);
1116
1801
  return {
1117
1802
  content: [
1118
1803
  {
@@ -1123,11 +1808,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1123
1808
  };
1124
1809
  }
1125
1810
  case 'stop_local_container': {
1126
- const runtime = args.runtime || 'docker';
1127
- const stopCmd = `${runtime} stop ${args.container_id}`;
1128
- const rmCmd = `${runtime} rm ${args.container_id}`;
1129
- await execAsync(stopCmd);
1130
- 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]);
1131
1815
  return {
1132
1816
  content: [
1133
1817
  {
@@ -1141,10 +1825,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1141
1825
  };
1142
1826
  }
1143
1827
  case 'list_local_containers': {
1144
- const runtime = args.runtime || 'docker';
1145
- const allFlag = args.all ? '-a' : '';
1146
- const cmd = `${runtime} ps ${allFlag} --format "{{.ID}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"`;
1147
- 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);
1148
1833
  return {
1149
1834
  content: [
1150
1835
  {
@@ -1154,6 +1839,60 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1154
1839
  ],
1155
1840
  };
1156
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
+ }
1157
1896
  case 'icr_list_namespaces': {
1158
1897
  const apiKey = getApiKey();
1159
1898
  const token = await getIAMToken(apiKey);
@@ -1607,6 +2346,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1607
2346
  const response = await axios.patch(`${base}/secrets/${args.secret_name}`, body, { headers: patchHeaders });
1608
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) }] };
1609
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
+ }
1610
2374
  case 'ce_renew_tls_secret_from_pem': {
1611
2375
  const fs = await import('fs');
1612
2376
  const os = await import('os');
@@ -1771,10 +2535,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1771
2535
  }
1772
2536
  if (exposedPorts.includes(80))
1773
2537
  valErrors.push('Port 80 is not allowed in Code Engine. Use port 8080.');
1774
- // nginx sed pattern check
2538
+ // nginx sed pattern check — catch both exact-space and \s* patterns that fail in Alpine BusyBox sed
1775
2539
  dfLines.filter(l => /sed\s+-i/.test(l) && /listen/.test(l)).forEach(sl => {
1776
- if (/listen\s{2,}80;/.test(sl)) {
1777
- 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`);
1778
2542
  }
1779
2543
  });
1780
2544
  if (valErrors.length > 0) {
@@ -1795,7 +2559,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1795
2559
  // 1) detect runtime
1796
2560
  let runtime = 'podman';
1797
2561
  try {
1798
- await execAsync('podman --version');
2562
+ await execFileAsync('podman', ['--version']);
1799
2563
  }
1800
2564
  catch {
1801
2565
  runtime = 'docker';
@@ -1810,20 +2574,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1810
2574
  const imageTag = args.image_tag || 'latest';
1811
2575
  const imageName = `${icrHost}/${args.icr_namespace}/${args.app_name}:${imageTag}`;
1812
2576
  const contextPath = contextPathRaw;
1813
- const buildCmd = `${runtime} build --platform linux/amd64 -t ${imageName} ${contextPath}`;
1814
- 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);
1815
2579
  const buildOutput = [buildStdout, buildStderr].filter(Boolean).join('\n').trim();
1816
2580
  // show last 20 lines of build output so it's not overwhelming
1817
2581
  const buildLines = buildOutput.split('\n');
1818
2582
  const buildSummary = buildLines.length > 20 ? `...${buildLines.slice(-20).join('\n')}` : buildOutput;
1819
2583
  steps.push(`[3/5] Built ${imageName} for linux/amd64:\n${buildSummary}`);
1820
2584
  // 4) push
1821
- const pushCmd = `${runtime} push ${imageName}`;
1822
- const { stdout: pushStdout, stderr: pushStderr } = await execAsync(pushCmd);
2585
+ const { stdout: pushStdout, stderr: pushStderr } = await execFileAsync(runtime, ['push', imageName]);
1823
2586
  const pushOutput = [pushStdout, pushStderr].filter(Boolean).join('\n').trim();
1824
2587
  steps.push(`[4/5] Pushed to ${icrHost}:\n${pushOutput}`);
1825
- // 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".
1826
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
1827
2607
  const appPayload1 = {
1828
2608
  image_reference: imageName,
1829
2609
  image_secret: args.image_secret,
@@ -1978,6 +2758,376 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1978
2758
  app_poll_history: appPollHistory,
1979
2759
  }, null, 2) }] };
1980
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
+ }
1981
3131
  default:
1982
3132
  throw new Error(`Unknown tool: ${name}`);
1983
3133
  }