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/README.md +165 -44
- package/build/index.js +1216 -66
- package/build/index.js.map +1 -1
- package/docs/MCP_INSPECTOR_TROUBLESHOOTING.md +70 -1
- package/docs/SETUP_INSTRUCTIONS.md +279 -87
- package/package.json +1 -1
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 {
|
|
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
|
|
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.
|
|
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: '
|
|
871
|
-
description: '
|
|
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
|
-
|
|
876
|
-
|
|
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: ['
|
|
1569
|
+
required: ['project_id', 'subnet_pool_id'],
|
|
883
1570
|
},
|
|
884
1571
|
},
|
|
885
1572
|
{
|
|
886
|
-
name: '
|
|
887
|
-
description: '
|
|
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
|
-
|
|
892
|
-
|
|
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: ['
|
|
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
|
|
1022
|
-
const { stdout: podmanVersion } = await
|
|
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
|
|
1039
|
-
const
|
|
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:
|
|
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
|
|
1058
|
-
const { stdout, stderr } = await
|
|
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:
|
|
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
|
|
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
|
-
|
|
1772
|
+
const runtime = validateRuntime(args.runtime || 'docker');
|
|
1773
|
+
const runArgs = ['run', '-d'];
|
|
1089
1774
|
if (args.port_mapping) {
|
|
1090
|
-
|
|
1775
|
+
runArgs.push('-p', validatePortMapping(args.port_mapping));
|
|
1091
1776
|
}
|
|
1092
1777
|
if (args.env_vars) {
|
|
1093
|
-
Object.entries(args.env_vars)
|
|
1094
|
-
|
|
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
|
-
|
|
1098
|
-
const { stdout } = await
|
|
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:
|
|
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
|
|
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
|
|
1128
|
-
|
|
1129
|
-
await
|
|
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
|
|
1146
|
-
|
|
1147
|
-
|
|
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()}" —
|
|
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
|
|
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
|
|
1814
|
-
const { stdout: buildStdout, stderr: buildStderr } = await
|
|
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
|
|
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)
|
|
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
|
}
|