Issue
The CloudBees CI MCP Router returns "Unable to list controllers from Operations Center." when calling the getControllers tool, even though the same API token authenticates successfully when used directly against operations center.
The CloudBees CI MCP Router debug logs show the following error:
Full OC URI to query for controllers: https://operations-center.example.com/api/json?tree=jobs[...] Error while getting controllers from OC: javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: unable to find valid certification path to requested target Unable to list controllers from OC: java.lang.IllegalStateException: Unable to get list of controllers from OC...
This occurs when operations center is served over HTTPS using a certificate signed by an internal or private CA that is not included in the CloudBees CI MCP Router container’s default JVM truststore. The curl command run directly from a workstation succeeds because the OS truststore includes the internal CA, but the JVM inside the container does not.
Prerequisites:
-
CA certificate(s) required to build the trust chain for operations center (PEM format), obtainable from your IT team
-
keytoolavailable locally (included with any JDK installation) -
Docker access to run and configure the CloudBees CI MCP Router container
Resolution
Step 1: Obtain the required CA certificates
The JVM truststore inside the CloudBees CI MCP Router container must trust every CA in the chain used to sign operations center’s TLS certificate. Which certificates you need to import depends on your environment:
-
If operations center uses a certificate signed by a publicly trusted CA already bundled with the JDK, no additional certificates are required.
-
If operations center uses a certificate signed by a private or internal CA, you must import that root CA — and any intermediate CAs in the chain — that are not already present in the default JVM
cacerts.
To inspect the full certificate chain presented by operations center, run:
openssl s_client -showcerts -connect operations-center.example.com:443 \ </dev/null 2>/dev/null
The output contains one BEGIN CERTIFICATE / END CERTIFICATE block per certificate in the chain. The first block is the server (leaf) certificate; subsequent blocks are intermediate and root CA certificates. Save the CA certificates you need to import as individual PEM files (for example, root-ca.pem, intermediate-ca.pem).
| If the full chain is not visible in the output, or if you are unsure which certificates are missing from the JVM truststore, obtain the CA certificate files directly from your IT team. |
Step 2: Extract the default JVM cacerts from the container
Copy the default cacerts truststore from the CloudBees CI MCP Router container image to your local machine:
docker create --name mcp-router-temp cloudbees/ci-mcp-router:latest docker cp mcp-router-temp:/usr/lib/jvm/java-21/lib/security/cacerts ./cacerts-combined docker rm mcp-router-temp
The JVM path inside the container may vary by image version. If the above path does not exist, run docker run --rm --entrypoint find cloudbees/ci-mcp-router:latest /usr/lib/jvm -name cacerts to locate it.
|
Step 3: Import the required CA certificates into the combined truststore
Run the following command once for each CA certificate obtained in Step 1, using a unique -alias value per certificate:
keytool -import \ -alias root-ca \ -keystore ./cacerts-combined \ -file root-ca.pem \ -storepass changeit \ -noprompt
If you have intermediate CA certificates to import, repeat the command with a different alias and file for each one (for example, -alias intermediate-ca -file intermediate-ca.pem).
Step 4: Start the CloudBees CI MCP Router with the custom truststore
Mount the combined truststore into the container and pass the JVM truststore flags via JAVA_TOOL_OPTIONS:
docker run -p 7701:9000 \ -v $(pwd)/cacerts-combined:/deployments/cacerts-combined:ro \ -e OC_URL=https://operations-center.example.com/ \ -e JAVA_TOOL_OPTIONS="-Djavax.net.ssl.trustStore=/deployments/cacerts-combined -Djavax.net.ssl.trustStorePassword=changeit -Dquarkus.mcp.server.http.streamable.dummy-init=true" \ -e QUARKUS_HTTP_CORS_ENABLED=true \ cloudbees/ci-mcp-router:latest
JAVA_TOOL_OPTIONS applies to all JVM processes started in the container environment. If other JVM processes share the same environment, they will also use the custom truststore. Ensure the truststore volume remains accessible at all container restarts; if the volume becomes unavailable, the JVM will fail to start.
|
Step 5: Verify connectivity
After starting the container, call the getControllers tool and confirm a successful response:
curl -sN -X POST http://localhost:7701/mcp \ -H "Authorization: Basic <base64-encoded-oc-token>" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"getControllers","arguments":{}}}' | jq
A successful response lists the available controllers:
{"jsonrpc":"2.0","id":1,"result":{"isError":false,"content":[{"text":"[{\"name\":\"cc2\",\"displayName\":\"cc2\",\"endpoint\":\"http://cc2.example.com:8080\",\"status\":\"online\"}]","type":"text"}]}}
The CloudBees CI MCP Router debug logs should show:
DEBUG Fetching controllers from OC: https://operations-center.example.com/ DEBUG Full OC URI to query: https://operations-center.example.com/api/json?tree=jobs[...] DEBUG Request ... with Authorization header available: true returned status 200 INFO Fetched from OC: https://operations-center.example.com/, controllers: [http://cc2.example.com:8080]